hazo_config 2.0.2 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGE_LOG.md +47 -0
- package/README.md +12 -0
- package/SETUP_CHECKLIST.md +132 -0
- package/dist/components/app_config_list_editor/app_config_list_editor.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/app_config_list_editor.js +4 -3
- package/dist/components/app_config_list_editor/components/import_export_buttons.d.ts +12 -0
- package/dist/components/app_config_list_editor/components/import_export_buttons.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/components/import_export_buttons.js +74 -0
- package/dist/components/app_config_list_editor/index.d.ts +2 -1
- package/dist/components/app_config_list_editor/index.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/index.js +1 -0
- package/dist/components/app_config_list_editor/types.d.ts +9 -0
- package/dist/components/app_config_list_editor/types.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/utils/json_import_export.d.ts +26 -0
- package/dist/components/app_config_list_editor/utils/json_import_export.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/utils/json_import_export.js +112 -0
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/package.json +12 -3
- package/MIGRATION_V2.md +0 -531
package/CHANGE_LOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.1.1
|
|
4
|
+
- chore: add `engines` field (`node >= 18.0.0`) and `prepublishOnly` script
|
|
5
|
+
- chore: widen React peer deps to support React 18 and 19
|
|
6
|
+
- chore: add CHANGE_LOG.md, SETUP_CHECKLIST.md, AGENTS.md, config/ sample, design/ architecture
|
|
7
|
+
- chore: clean up test-app duplicate transitive dependencies
|
|
8
|
+
- chore: update .npmignore for new dev files
|
|
9
|
+
|
|
10
|
+
## 2.1.0
|
|
11
|
+
- feat: add JSON import/export to AppConfigListEditor
|
|
12
|
+
- Export utility functions: `validate_import_data`, `merge_items`, `export_items_to_json`, `generate_export_filename`
|
|
13
|
+
|
|
14
|
+
## 2.0.2
|
|
15
|
+
- feat: add AppConfigListEditor component with shadcn/ui
|
|
16
|
+
- Generic CRUD list editor for arrays of structured objects (tags, document types, etc.)
|
|
17
|
+
- Supports field types: text, textarea, color_swatch, select, number, toggle
|
|
18
|
+
|
|
19
|
+
## 2.0.1
|
|
20
|
+
- chore: add test-app build artifacts to .gitignore
|
|
21
|
+
|
|
22
|
+
## 2.0.0
|
|
23
|
+
- **BREAKING**: Renamed `org_id` to `scope_id` in `AppConfigItem` and `AppConfigContext`
|
|
24
|
+
- **BREAKING**: Split `config_value` into `config_value_text` and `config_value_json`
|
|
25
|
+
- **BREAKING**: Added `config_type` field (`'general'` | `'json'`)
|
|
26
|
+
- feat: add database-backed AppConfig component with JSON editing support
|
|
27
|
+
- feat: add `useAppConfig` hook for async database operations
|
|
28
|
+
- See `MIGRATION_V2.md` for upgrade guide
|
|
29
|
+
|
|
30
|
+
## 1.4.2
|
|
31
|
+
- fix: separate server/client exports to prevent Next.js bundling errors
|
|
32
|
+
- Added `hazo_config/server` entry point with `server-only` guard
|
|
33
|
+
|
|
34
|
+
## 1.4.1
|
|
35
|
+
- fix: update JS output issues
|
|
36
|
+
|
|
37
|
+
## 1.4.0
|
|
38
|
+
- fix: add missing files to package.json
|
|
39
|
+
- fix: ES module exports with explicit .js extensions
|
|
40
|
+
- Added package development rules
|
|
41
|
+
|
|
42
|
+
## 1.0.0
|
|
43
|
+
- Initial release
|
|
44
|
+
- HazoConfig class with INI file support, TTL-based caching
|
|
45
|
+
- ConfigViewer and ConfigEditor React components
|
|
46
|
+
- MockConfigProvider for testing
|
|
47
|
+
- Sensitive field masking (auto-detection of passwords, tokens, keys)
|
package/README.md
CHANGED
|
@@ -270,6 +270,18 @@ The `AppConfigListEditor` is a generic, reusable CRUD list editor for arrays of
|
|
|
270
270
|
- Save status indicators (saving/saved/error)
|
|
271
271
|
- Custom item rendering (indicator, preview, full row override)
|
|
272
272
|
- Field validation with inline error messages
|
|
273
|
+
- **JSON import/export** — export items as `.json` file, import from file with validation and duplicate skipping
|
|
274
|
+
|
|
275
|
+
#### JSON Import/Export
|
|
276
|
+
|
|
277
|
+
The list editor includes built-in Export and Import buttons in the section bar. Export downloads the current items as a formatted JSON file. Import opens a file picker, validates the JSON against column definitions, and merges new items into the list (skipping duplicates by ID).
|
|
278
|
+
|
|
279
|
+
Utility functions are also exported for programmatic use:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { validate_import_data, merge_items, export_items_to_json, generate_export_filename } from 'hazo_config'
|
|
283
|
+
import type { ImportResult } from 'hazo_config'
|
|
284
|
+
```
|
|
273
285
|
|
|
274
286
|
## Development
|
|
275
287
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Setup Checklist
|
|
2
|
+
|
|
3
|
+
Step-by-step setup for consuming apps using hazo_config.
|
|
4
|
+
|
|
5
|
+
## 1. Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install hazo_config
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 2. Tailwind CSS Setup
|
|
12
|
+
|
|
13
|
+
### Tailwind v4
|
|
14
|
+
Add `@source` directive in your globals.css to ensure Tailwind compiles classes from the package:
|
|
15
|
+
|
|
16
|
+
```css
|
|
17
|
+
@import "tailwindcss";
|
|
18
|
+
@source "../node_modules/hazo_config/dist";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Tailwind v3
|
|
22
|
+
Add the dist directory to your `tailwind.config.js` content array:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
module.exports = {
|
|
26
|
+
content: [
|
|
27
|
+
'./src/**/*.{js,ts,jsx,tsx}',
|
|
28
|
+
'./node_modules/hazo_config/dist/**/*.{js,ts,jsx,tsx}',
|
|
29
|
+
],
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 3. CSS Custom Properties (Optional)
|
|
34
|
+
|
|
35
|
+
Add these CSS variables if you want themed styling:
|
|
36
|
+
|
|
37
|
+
```css
|
|
38
|
+
:root {
|
|
39
|
+
--hazo-config-bg: #ffffff;
|
|
40
|
+
--hazo-config-border: #e2e8f0;
|
|
41
|
+
--hazo-config-text: #1a202c;
|
|
42
|
+
--hazo-config-muted: #718096;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 4. Import Components
|
|
47
|
+
|
|
48
|
+
### Client-side (components, browser)
|
|
49
|
+
```typescript
|
|
50
|
+
import { ConfigViewer, ConfigEditor, AppConfig, MockConfigProvider } from 'hazo_config'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Server-side (API routes, server components)
|
|
54
|
+
```typescript
|
|
55
|
+
import { HazoConfig } from 'hazo_config/server'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 5. INI File Config (if using HazoConfig)
|
|
59
|
+
|
|
60
|
+
Create your config file:
|
|
61
|
+
```ini
|
|
62
|
+
[database]
|
|
63
|
+
host = localhost
|
|
64
|
+
port = 5432
|
|
65
|
+
|
|
66
|
+
[app]
|
|
67
|
+
name = My Application
|
|
68
|
+
debug = false
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Initialize in server code:
|
|
72
|
+
```typescript
|
|
73
|
+
import { HazoConfig } from 'hazo_config/server'
|
|
74
|
+
|
|
75
|
+
const config = new HazoConfig({
|
|
76
|
+
filePath: './config/app_config.ini',
|
|
77
|
+
create_if_missing: true,
|
|
78
|
+
cache_ttl_ms: 5000,
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 6. Database Config (if using AppConfig)
|
|
83
|
+
|
|
84
|
+
Create the `hazo_app_config` table in your database:
|
|
85
|
+
|
|
86
|
+
### PostgreSQL
|
|
87
|
+
```sql
|
|
88
|
+
CREATE TABLE IF NOT EXISTS hazo_app_config (
|
|
89
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
90
|
+
scope_id UUID,
|
|
91
|
+
user_id UUID,
|
|
92
|
+
config_section TEXT NOT NULL,
|
|
93
|
+
config_name TEXT NOT NULL,
|
|
94
|
+
config_value_text TEXT DEFAULT '',
|
|
95
|
+
config_value_json JSONB DEFAULT '{}',
|
|
96
|
+
config_type TEXT NOT NULL DEFAULT 'general',
|
|
97
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
98
|
+
changed_at TIMESTAMPTZ DEFAULT NOW()
|
|
99
|
+
);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### SQLite
|
|
103
|
+
```sql
|
|
104
|
+
CREATE TABLE IF NOT EXISTS hazo_app_config (
|
|
105
|
+
id TEXT PRIMARY KEY,
|
|
106
|
+
scope_id TEXT,
|
|
107
|
+
user_id TEXT,
|
|
108
|
+
config_section TEXT NOT NULL,
|
|
109
|
+
config_name TEXT NOT NULL,
|
|
110
|
+
config_value_text TEXT DEFAULT '',
|
|
111
|
+
config_value_json TEXT DEFAULT '{}',
|
|
112
|
+
config_type TEXT NOT NULL DEFAULT 'general',
|
|
113
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
114
|
+
changed_at TEXT DEFAULT (datetime('now'))
|
|
115
|
+
);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 7. Next.js Configuration
|
|
119
|
+
|
|
120
|
+
Add to `next.config.js`:
|
|
121
|
+
```javascript
|
|
122
|
+
const nextConfig = {
|
|
123
|
+
transpilePackages: ['hazo_config'],
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 8. Verify
|
|
128
|
+
|
|
129
|
+
- [ ] `import { ConfigViewer } from 'hazo_config'` resolves without errors
|
|
130
|
+
- [ ] `import { HazoConfig } from 'hazo_config/server'` works in server code
|
|
131
|
+
- [ ] Tailwind classes from hazo_config render correctly (check backgrounds, borders)
|
|
132
|
+
- [ ] Sensitive fields are masked in ConfigViewer/ConfigEditor
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app_config_list_editor.d.ts","sourceRoot":"","sources":["../../../src/components/app_config_list_editor/app_config_list_editor.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,wBAAwB,EAAqC,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"app_config_list_editor.d.ts","sourceRoot":"","sources":["../../../src/components/app_config_list_editor/app_config_list_editor.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,wBAAwB,EAAqC,MAAM,YAAY,CAAA;AAS7F;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACrE,KAAK,EACL,eAAe,EACf,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,KAAK,EACL,WAAW,EACX,aAAqB,EACrB,gBAAoB,EACpB,WAAW,EACX,qBAAqB,EACrB,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,wBAAgC,EAChC,SAAS,EACT,WAAoB,GACrB,EAAE,wBAAwB,CAAC,CAAC,CAAC,2CAgQ7B"}
|
|
@@ -10,6 +10,7 @@ import { SearchBar } from './components/search_bar.js';
|
|
|
10
10
|
import { EmptyState } from './components/empty_state.js';
|
|
11
11
|
import { DeleteDialog } from './components/delete_dialog.js';
|
|
12
12
|
import { SaveStatusIndicator } from './components/save_status_indicator.js';
|
|
13
|
+
import { ImportExportButtons } from './components/import_export_buttons.js';
|
|
13
14
|
/**
|
|
14
15
|
* AppConfigListEditor - A polished CRUD list editor for config item arrays.
|
|
15
16
|
* Purely callback-based: no API calls, no database access.
|
|
@@ -122,7 +123,7 @@ export function AppConfigListEditor({ items, on_items_change, columns, id_field,
|
|
|
122
123
|
}
|
|
123
124
|
return String(delete_state.item[id_field] ?? '');
|
|
124
125
|
}, [delete_state, columns, id_field]);
|
|
125
|
-
return (_jsxs("div", { className: cn('cls_list_editor', className), children: [(title || description) && (_jsxs("div", { className: "cls_list_editor_header mb-6", children: [title && (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("h2", { className: "text-2xl font-bold text-gray-900", children: title }), _jsx(SaveStatusIndicator, { status: save_status })] })), description && (_jsx("p", { className: "text-sm text-gray-500 mt-1", children: description }))] })), _jsxs("div", { className: "cls_list_editor_section_bar flex items-center justify-between mb-3", children: [_jsx("div", { className: "flex items-center gap-2", children: _jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-gray-500", children: section_label }) }), _jsxs("button", { type: "button", onClick: handle_add, disabled: is_max_reached, className: cn('inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-full transition-colors', is_max_reached
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
return (_jsxs("div", { className: cn('cls_list_editor', className), children: [(title || description) && (_jsxs("div", { className: "cls_list_editor_header mb-6", children: [title && (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("h2", { className: "text-2xl font-bold text-gray-900", children: title }), _jsx(SaveStatusIndicator, { status: save_status })] })), description && (_jsx("p", { className: "text-sm text-gray-500 mt-1", children: description }))] })), _jsxs("div", { className: "cls_list_editor_section_bar flex items-center justify-between mb-3", children: [_jsx("div", { className: "flex items-center gap-2", children: _jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-gray-500", children: section_label }) }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(ImportExportButtons, { items: items, on_items_change: on_items_change, columns: columns, id_field: id_field, title: title, max_items: max_items }), _jsxs("button", { type: "button", onClick: handle_add, disabled: is_max_reached, className: cn('inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-full transition-colors', is_max_reached
|
|
127
|
+
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
|
128
|
+
: 'bg-violet-600 hover:bg-violet-700 text-white'), title: is_max_reached ? `Maximum of ${max_items} items reached` : undefined, children: [_jsx(Plus, { className: "w-4 h-4" }), "Add"] })] })] }), show_search && (_jsx("div", { className: "mb-3", children: _jsx(SearchBar, { value: search_term, on_change: set_search_term, item_count: filtered_items.length, item_label: item_label }) })), _jsx("div", { className: "cls_list_editor_card rounded-xl border border-gray-200 bg-white overflow-hidden", children: items.length === 0 ? (_jsx(EmptyState, { item_label: item_label, on_add: handle_add })) : filtered_items.length === 0 ? (_jsxs("div", { className: "py-8 text-center text-sm text-gray-500", children: ["No ", item_label, " matching \u201C", search_term, "\u201D"] })) : (_jsx("div", { className: "divide-y divide-gray-100", children: filtered_items.map((item, index) => (_jsx(ListItemRow, { item: item, index: index, columns: columns, render_item: render_item, render_item_indicator: render_item_indicator, on_edit: () => handle_edit(item), on_delete: () => handle_delete_request(item) }, String(item[id_field])))) })) }), is_max_reached && (_jsxs("p", { className: "text-xs text-gray-400 mt-2", children: ["Maximum of ", max_items, " ", item_label, " reached."] })), edit_state && (_jsx(EditModal, { open: true, mode: edit_state.mode, item: edit_state.item, columns: columns, id_field: id_field, auto_id_from: auto_id_from, id_editable_after_create: id_editable_after_create, existing_ids: existing_ids, item_type_label: item_type_label, render_preview: render_preview, on_save: handle_save, on_cancel: handle_cancel_edit })), delete_state && (_jsx(DeleteDialog, { open: true, item_name: delete_item_name, message: delete_message, on_confirm: handle_delete_confirm, on_cancel: handle_cancel_delete }))] }));
|
|
128
129
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ColumnDef } from '../types.js';
|
|
2
|
+
interface ImportExportButtonsProps<T extends Record<string, unknown>> {
|
|
3
|
+
items: T[];
|
|
4
|
+
on_items_change: (items: T[]) => void;
|
|
5
|
+
columns: ColumnDef<T>[];
|
|
6
|
+
id_field: keyof T & string;
|
|
7
|
+
title?: string;
|
|
8
|
+
max_items?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function ImportExportButtons<T extends Record<string, unknown>>({ items, on_items_change, columns, id_field, title, max_items, }: ImportExportButtonsProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=import_export_buttons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import_export_buttons.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/import_export_buttons.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAQ5C,UAAU,wBAAwB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAClE,KAAK,EAAE,CAAC,EAAE,CAAA;IACV,eAAe,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,IAAI,CAAA;IACrC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACrE,KAAK,EACL,eAAe,EACf,OAAO,EACP,QAAQ,EACR,KAAK,EACL,SAAS,GACV,EAAE,wBAAwB,CAAC,CAAC,CAAC,2CA0H7B"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Import/Export buttons for AppConfigListEditor
|
|
3
|
+
// Provides JSON export (download) and import (file picker) with transient status messages
|
|
4
|
+
import { useRef, useState, useCallback, useEffect } from 'react';
|
|
5
|
+
import { Download, Upload } from 'lucide-react';
|
|
6
|
+
import { cn } from '../../../lib/utils.js';
|
|
7
|
+
import { validate_import_data, merge_items, export_items_to_json, generate_export_filename, } from '../utils/json_import_export.js';
|
|
8
|
+
export function ImportExportButtons({ items, on_items_change, columns, id_field, title, max_items, }) {
|
|
9
|
+
const file_input_ref = useRef(null);
|
|
10
|
+
const [status_message, set_status_message] = useState(null);
|
|
11
|
+
// Auto-fade status message after 3 seconds
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!status_message)
|
|
14
|
+
return;
|
|
15
|
+
const timer = setTimeout(() => set_status_message(null), 3000);
|
|
16
|
+
return () => clearTimeout(timer);
|
|
17
|
+
}, [status_message]);
|
|
18
|
+
const handle_export = useCallback(() => {
|
|
19
|
+
const filename = generate_export_filename(title);
|
|
20
|
+
export_items_to_json(items, filename);
|
|
21
|
+
}, [items, title]);
|
|
22
|
+
const handle_import_click = useCallback(() => {
|
|
23
|
+
// Reset input so the same file can be re-imported
|
|
24
|
+
if (file_input_ref.current) {
|
|
25
|
+
file_input_ref.current.value = '';
|
|
26
|
+
}
|
|
27
|
+
file_input_ref.current?.click();
|
|
28
|
+
}, []);
|
|
29
|
+
const handle_file_selected = useCallback((event) => {
|
|
30
|
+
const file = event.target.files?.[0];
|
|
31
|
+
if (!file)
|
|
32
|
+
return;
|
|
33
|
+
const reader = new FileReader();
|
|
34
|
+
reader.onload = (e) => {
|
|
35
|
+
const text = e.target?.result;
|
|
36
|
+
if (typeof text !== 'string') {
|
|
37
|
+
set_status_message({ text: 'Failed to read file', is_error: true });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let data;
|
|
41
|
+
try {
|
|
42
|
+
data = JSON.parse(text);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
set_status_message({ text: 'Invalid JSON file', is_error: true });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const { valid_items, errors } = validate_import_data(data, columns, id_field);
|
|
49
|
+
if (valid_items.length === 0) {
|
|
50
|
+
const msg = errors.length > 0 ? errors[0] : 'No valid items found';
|
|
51
|
+
set_status_message({ text: msg, is_error: true });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const result = merge_items(items, valid_items, id_field, max_items);
|
|
55
|
+
on_items_change(result.merged_items);
|
|
56
|
+
const parts = [];
|
|
57
|
+
if (result.added_count > 0) {
|
|
58
|
+
parts.push(`Added ${result.added_count}`);
|
|
59
|
+
}
|
|
60
|
+
if (result.skipped_count > 0) {
|
|
61
|
+
parts.push(`${result.skipped_count} skipped`);
|
|
62
|
+
}
|
|
63
|
+
if (errors.length > 0) {
|
|
64
|
+
parts.push(`${errors.length} invalid`);
|
|
65
|
+
}
|
|
66
|
+
set_status_message({
|
|
67
|
+
text: parts.join(', '),
|
|
68
|
+
is_error: result.added_count === 0,
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
reader.readAsText(file);
|
|
72
|
+
}, [items, columns, id_field, max_items, on_items_change]);
|
|
73
|
+
return (_jsxs("div", { className: "cls_import_export flex items-center gap-1.5", children: [status_message && (_jsx("span", { className: cn('cls_import_status text-xs transition-opacity duration-300', status_message.is_error ? 'text-red-600' : 'text-green-600'), children: status_message.text })), _jsxs("button", { type: "button", onClick: handle_export, className: "inline-flex items-center gap-1 px-2 py-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full transition-colors", title: "Export as JSON", children: [_jsx(Download, { className: "w-3.5 h-3.5" }), "Export"] }), _jsxs("button", { type: "button", onClick: handle_import_click, className: "inline-flex items-center gap-1 px-2 py-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full transition-colors", title: "Import from JSON", children: [_jsx(Upload, { className: "w-3.5 h-3.5" }), "Import"] }), _jsx("input", { ref: file_input_ref, type: "file", accept: ".json", onChange: handle_file_selected, className: "hidden" })] }));
|
|
74
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { AppConfigListEditor } from './app_config_list_editor.js';
|
|
2
|
-
export type { AppConfigListEditorProps, ColumnDef } from './types.js';
|
|
2
|
+
export type { AppConfigListEditorProps, ColumnDef, ImportResult } from './types.js';
|
|
3
|
+
export { validate_import_data, merge_items, export_items_to_json, generate_export_filename, } from './utils/json_import_export.js';
|
|
3
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/app_config_list_editor/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AACjE,YAAY,EAAE,wBAAwB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/app_config_list_editor/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AACjE,YAAY,EAAE,wBAAwB,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACnF,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,+BAA+B,CAAA"}
|
|
@@ -84,6 +84,15 @@ export interface EditModalState<T> {
|
|
|
84
84
|
export interface DeleteDialogState<T> {
|
|
85
85
|
item: T;
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Result of a JSON import merge operation
|
|
89
|
+
*/
|
|
90
|
+
export interface ImportResult<T> {
|
|
91
|
+
merged_items: T[];
|
|
92
|
+
added_count: number;
|
|
93
|
+
skipped_count: number;
|
|
94
|
+
errors: string[];
|
|
95
|
+
}
|
|
87
96
|
/**
|
|
88
97
|
* Slugify a string for auto-ID generation.
|
|
89
98
|
* Converts to lowercase, replaces non-alphanumeric chars with underscores,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/app_config_list_editor/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/app_config_list_editor/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ColumnDef, ImportResult } from '../types.js';
|
|
2
|
+
export type { ColumnDef, ImportResult };
|
|
3
|
+
/**
|
|
4
|
+
* Validate parsed JSON data against column definitions.
|
|
5
|
+
* Returns valid items and a list of error messages.
|
|
6
|
+
*/
|
|
7
|
+
export declare function validate_import_data<T extends Record<string, unknown>>(data: unknown, columns: ColumnDef<T>[], id_field: keyof T & string): {
|
|
8
|
+
valid_items: T[];
|
|
9
|
+
errors: string[];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Merge incoming items with existing items.
|
|
13
|
+
* Skips items whose ID already exists in the existing list.
|
|
14
|
+
* Respects max_items cap.
|
|
15
|
+
*/
|
|
16
|
+
export declare function merge_items<T extends Record<string, unknown>>(existing: T[], incoming: T[], id_field: keyof T & string, max_items?: number): ImportResult<T>;
|
|
17
|
+
/**
|
|
18
|
+
* Trigger a browser download of items as a JSON file.
|
|
19
|
+
*/
|
|
20
|
+
export declare function export_items_to_json<T>(items: T[], filename: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Generate a snake_case filename from a title string.
|
|
23
|
+
* e.g. "Classification Tags" -> "classification_tags.json"
|
|
24
|
+
*/
|
|
25
|
+
export declare function generate_export_filename(title?: string): string;
|
|
26
|
+
//# sourceMappingURL=json_import_export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json_import_export.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/utils/json_import_export.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1D,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;AAEvC;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpE,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,EACvB,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,GACzB;IAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAwDxC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,QAAQ,EAAE,CAAC,EAAE,EACb,QAAQ,EAAE,CAAC,EAAE,EACb,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,EAC1B,SAAS,CAAC,EAAE,MAAM,GACjB,YAAY,CAAC,CAAC,CAAC,CA0BjB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAW1E;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ/D"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// JSON import/export utilities for AppConfigListEditor
|
|
2
|
+
// Pure functions for validating, merging, and exporting config item arrays
|
|
3
|
+
/**
|
|
4
|
+
* Validate parsed JSON data against column definitions.
|
|
5
|
+
* Returns valid items and a list of error messages.
|
|
6
|
+
*/
|
|
7
|
+
export function validate_import_data(data, columns, id_field) {
|
|
8
|
+
const errors = [];
|
|
9
|
+
const valid_items = [];
|
|
10
|
+
if (!Array.isArray(data)) {
|
|
11
|
+
return { valid_items: [], errors: ['Expected a JSON array'] };
|
|
12
|
+
}
|
|
13
|
+
if (data.length === 0) {
|
|
14
|
+
return { valid_items: [], errors: ['JSON array is empty'] };
|
|
15
|
+
}
|
|
16
|
+
for (let i = 0; i < data.length; i++) {
|
|
17
|
+
const item = data[i];
|
|
18
|
+
const item_errors = [];
|
|
19
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item)) {
|
|
20
|
+
errors.push(`Item ${i + 1}: not a valid object`);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const record = item;
|
|
24
|
+
// Check id_field exists and is non-empty
|
|
25
|
+
if (!(id_field in record) || record[id_field] === '' || record[id_field] === null || record[id_field] === undefined) {
|
|
26
|
+
item_errors.push(`missing required field "${id_field}"`);
|
|
27
|
+
}
|
|
28
|
+
// Check required fields and basic type compatibility
|
|
29
|
+
for (const col of columns) {
|
|
30
|
+
const val = record[col.field];
|
|
31
|
+
if (col.required && (val === undefined || val === null || val === '')) {
|
|
32
|
+
item_errors.push(`missing required field "${col.field}"`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (val === undefined || val === null)
|
|
36
|
+
continue;
|
|
37
|
+
// Basic type checks
|
|
38
|
+
if (col.type === 'number' && typeof val !== 'number') {
|
|
39
|
+
item_errors.push(`"${col.field}" should be a number`);
|
|
40
|
+
}
|
|
41
|
+
if (col.type === 'toggle' && typeof val !== 'boolean') {
|
|
42
|
+
item_errors.push(`"${col.field}" should be a boolean`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (item_errors.length > 0) {
|
|
46
|
+
errors.push(`Item ${i + 1}: ${item_errors.join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
valid_items.push(record);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { valid_items, errors };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Merge incoming items with existing items.
|
|
56
|
+
* Skips items whose ID already exists in the existing list.
|
|
57
|
+
* Respects max_items cap.
|
|
58
|
+
*/
|
|
59
|
+
export function merge_items(existing, incoming, id_field, max_items) {
|
|
60
|
+
const existing_ids = new Set(existing.map((item) => String(item[id_field])));
|
|
61
|
+
let skipped_count = 0;
|
|
62
|
+
const to_add = [];
|
|
63
|
+
for (const item of incoming) {
|
|
64
|
+
const id = String(item[id_field]);
|
|
65
|
+
if (existing_ids.has(id)) {
|
|
66
|
+
skipped_count++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Respect max_items cap
|
|
70
|
+
if (max_items !== undefined && max_items !== null && existing.length + to_add.length >= max_items) {
|
|
71
|
+
skipped_count++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
to_add.push(item);
|
|
75
|
+
existing_ids.add(id);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
merged_items: [...existing, ...to_add],
|
|
79
|
+
added_count: to_add.length,
|
|
80
|
+
skipped_count,
|
|
81
|
+
errors: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Trigger a browser download of items as a JSON file.
|
|
86
|
+
*/
|
|
87
|
+
export function export_items_to_json(items, filename) {
|
|
88
|
+
const json = JSON.stringify(items, null, 2);
|
|
89
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
90
|
+
const url = URL.createObjectURL(blob);
|
|
91
|
+
const a = document.createElement('a');
|
|
92
|
+
a.href = url;
|
|
93
|
+
a.download = filename;
|
|
94
|
+
document.body.appendChild(a);
|
|
95
|
+
a.click();
|
|
96
|
+
document.body.removeChild(a);
|
|
97
|
+
URL.revokeObjectURL(url);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Generate a snake_case filename from a title string.
|
|
101
|
+
* e.g. "Classification Tags" -> "classification_tags.json"
|
|
102
|
+
*/
|
|
103
|
+
export function generate_export_filename(title) {
|
|
104
|
+
if (!title)
|
|
105
|
+
return 'config_items.json';
|
|
106
|
+
const slug = title
|
|
107
|
+
.toLowerCase()
|
|
108
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
109
|
+
.replace(/^_+|_+$/g, '')
|
|
110
|
+
.replace(/_+/g, '_');
|
|
111
|
+
return `${slug}.json`;
|
|
112
|
+
}
|
|
@@ -9,5 +9,6 @@ export type { UseConfigSectionsResult } from './use_config_sections.js';
|
|
|
9
9
|
export { useAppConfig } from './use_app_config.js';
|
|
10
10
|
export type { UseAppConfigResult } from '../lib/app_config_types.js';
|
|
11
11
|
export { AppConfigListEditor } from './app_config_list_editor/index.js';
|
|
12
|
-
export type { AppConfigListEditorProps, ColumnDef } from './app_config_list_editor/index.js';
|
|
12
|
+
export type { AppConfigListEditorProps, ColumnDef, ImportResult } from './app_config_list_editor/index.js';
|
|
13
|
+
export { validate_import_data, merge_items, export_items_to_json, generate_export_filename, } from './app_config_list_editor/index.js';
|
|
13
14
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAGhE,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,UAAU,EACV,0BAA0B,EAC3B,MAAM,0BAA0B,CAAA;AACjC,YAAY,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAEvE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAGpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAA;AACvE,YAAY,EAAE,wBAAwB,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAGhE,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,UAAU,EACV,0BAA0B,EAC3B,MAAM,0BAA0B,CAAA;AACjC,YAAY,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAEvE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAGpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAA;AACvE,YAAY,EAAE,wBAAwB,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAA;AAC1G,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,mCAAmC,CAAA"}
|
package/dist/components/index.js
CHANGED
|
@@ -8,3 +8,4 @@ export { useConfigSections, is_sensitive_field, mask_value, DEFAULT_SENSITIVE_PA
|
|
|
8
8
|
export { useAppConfig } from './use_app_config.js';
|
|
9
9
|
// AppConfigListEditor exports
|
|
10
10
|
export { AppConfigListEditor } from './app_config_list_editor/index.js';
|
|
11
|
+
export { validate_import_data, merge_items, export_items_to_json, generate_export_filename, } from './app_config_list_editor/index.js';
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_config",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Config wrapper with error handling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"CHANGE_LOG.md",
|
|
11
|
+
"SETUP_CHECKLIST.md"
|
|
12
|
+
],
|
|
8
13
|
"exports": {
|
|
9
14
|
".": {
|
|
10
15
|
"types": "./dist/index.d.ts",
|
|
@@ -25,6 +30,7 @@
|
|
|
25
30
|
},
|
|
26
31
|
"scripts": {
|
|
27
32
|
"build": "tsc -p tsconfig.build.json",
|
|
33
|
+
"prepublishOnly": "npm run build",
|
|
28
34
|
"storybook": "npx storybook dev -p 6006",
|
|
29
35
|
"build-storybook": "npx storybook build",
|
|
30
36
|
"test": "vitest run",
|
|
@@ -51,6 +57,9 @@
|
|
|
51
57
|
"url": "https://github.com/pub12/hazo_config/issues"
|
|
52
58
|
},
|
|
53
59
|
"homepage": "https://github.com/pub12/hazo_config#readme",
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=18.0.0"
|
|
62
|
+
},
|
|
54
63
|
"dependencies": {
|
|
55
64
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
56
65
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
@@ -89,8 +98,8 @@
|
|
|
89
98
|
"vitest": "^1.6.1"
|
|
90
99
|
},
|
|
91
100
|
"peerDependencies": {
|
|
92
|
-
"react": "^18.
|
|
93
|
-
"react-dom": "^18.
|
|
101
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
102
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
94
103
|
"tailwindcss": "^3.0.0"
|
|
95
104
|
},
|
|
96
105
|
"peerDependenciesMeta": {
|
package/MIGRATION_V2.md
DELETED
|
@@ -1,531 +0,0 @@
|
|
|
1
|
-
# Migration Guide: hazo_config v1 → v2
|
|
2
|
-
|
|
3
|
-
## Breaking Changes Summary
|
|
4
|
-
|
|
5
|
-
Version 2.0.0 introduces a major schema change to support JSON configuration values and replace `org_id` with `scope_id`. This is a **BREAKING CHANGE** requiring code updates and database migration.
|
|
6
|
-
|
|
7
|
-
## What Changed
|
|
8
|
-
|
|
9
|
-
### 1. Database Schema
|
|
10
|
-
|
|
11
|
-
**Old Schema:**
|
|
12
|
-
```sql
|
|
13
|
-
CREATE TABLE hazo_app_config (
|
|
14
|
-
id TEXT PRIMARY KEY,
|
|
15
|
-
org_id TEXT,
|
|
16
|
-
user_id TEXT,
|
|
17
|
-
config_section TEXT NOT NULL,
|
|
18
|
-
config_name TEXT NOT NULL,
|
|
19
|
-
config_value TEXT NOT NULL,
|
|
20
|
-
created_at TEXT,
|
|
21
|
-
changed_at TEXT
|
|
22
|
-
)
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
**New Schema:**
|
|
26
|
-
```sql
|
|
27
|
-
CREATE TABLE hazo_app_config (
|
|
28
|
-
id TEXT PRIMARY KEY,
|
|
29
|
-
scope_id TEXT, -- ← CHANGED: org_id → scope_id
|
|
30
|
-
user_id TEXT,
|
|
31
|
-
config_section TEXT NOT NULL,
|
|
32
|
-
config_name TEXT NOT NULL,
|
|
33
|
-
config_value_text TEXT, -- ← NEW: for general type
|
|
34
|
-
config_value_json TEXT, -- ← NEW: for json type (JSONB in PostgreSQL)
|
|
35
|
-
config_type TEXT NOT NULL, -- ← NEW: 'general' or 'json'
|
|
36
|
-
created_at TEXT,
|
|
37
|
-
changed_at TEXT,
|
|
38
|
-
CHECK (
|
|
39
|
-
(config_type = 'general' AND config_value_text IS NOT NULL AND config_value_json IS NULL) OR
|
|
40
|
-
(config_type = 'json' AND config_value_json IS NOT NULL AND config_value_text IS NULL)
|
|
41
|
-
)
|
|
42
|
-
)
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 2. Type Definitions
|
|
46
|
-
|
|
47
|
-
**Removed:**
|
|
48
|
-
- `ConfigLevel` type ('org' | 'user')
|
|
49
|
-
|
|
50
|
-
**Added:**
|
|
51
|
-
- `ConfigType` type ('json' | 'general')
|
|
52
|
-
|
|
53
|
-
**Changed Interface:**
|
|
54
|
-
```typescript
|
|
55
|
-
// OLD
|
|
56
|
-
interface AppConfigItem {
|
|
57
|
-
org_id: string | null
|
|
58
|
-
config_value: string
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// NEW
|
|
62
|
-
interface AppConfigItem {
|
|
63
|
-
scope_id: string | null // ← org_id renamed
|
|
64
|
-
config_value_text: string // ← new field
|
|
65
|
-
config_value_json: object // ← new field
|
|
66
|
-
config_type: ConfigType // ← new field
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// OLD
|
|
70
|
-
interface AppConfigContext {
|
|
71
|
-
org_id: string
|
|
72
|
-
user_id?: string
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// NEW
|
|
76
|
-
interface AppConfigContext {
|
|
77
|
-
scope_id: string // ← org_id renamed
|
|
78
|
-
user_id?: string
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### 3. Component API Changes
|
|
83
|
-
|
|
84
|
-
**AppConfig Component:**
|
|
85
|
-
```typescript
|
|
86
|
-
// OLD
|
|
87
|
-
<AppConfig
|
|
88
|
-
level="org" // ← REMOVED
|
|
89
|
-
context={{ org_id: '...' }}
|
|
90
|
-
fetch_config={fetch_config}
|
|
91
|
-
save_config={save_config}
|
|
92
|
-
delete_config={delete_config}
|
|
93
|
-
/>
|
|
94
|
-
|
|
95
|
-
// NEW
|
|
96
|
-
<AppConfig
|
|
97
|
-
title="Scope Settings" // ← Now required (no default from level)
|
|
98
|
-
context={{ scope_id: '...' }}
|
|
99
|
-
fetch_config={fetch_config}
|
|
100
|
-
save_config={save_config}
|
|
101
|
-
delete_config={delete_config}
|
|
102
|
-
/>
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
**Hook Signature:**
|
|
106
|
-
```typescript
|
|
107
|
-
// OLD
|
|
108
|
-
useAppConfig(
|
|
109
|
-
level: ConfigLevel,
|
|
110
|
-
context: AppConfigContext,
|
|
111
|
-
fetch_config: (level, context) => Promise<AppConfigItem[]>,
|
|
112
|
-
save_config: (item) => Promise<void>,
|
|
113
|
-
delete_config: (section, name, level, context) => Promise<void>
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
// NEW
|
|
117
|
-
useAppConfig(
|
|
118
|
-
context: AppConfigContext,
|
|
119
|
-
fetch_config: (context) => Promise<AppConfigItem[]>,
|
|
120
|
-
save_config: (item) => Promise<void>,
|
|
121
|
-
delete_config: (section, name, context) => Promise<void>
|
|
122
|
-
)
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
**Hook Return Value:**
|
|
126
|
-
```typescript
|
|
127
|
-
// OLD
|
|
128
|
-
sections: Record<string, Record<string, string>>
|
|
129
|
-
set_value: (section, key, value: string) => Promise<void>
|
|
130
|
-
|
|
131
|
-
// NEW
|
|
132
|
-
sections: Record<string, Record<string, AppConfigItem>> // Full items
|
|
133
|
-
set_value: (section, key, value: string | object, type: ConfigType) => Promise<void>
|
|
134
|
-
change_type: (section, key, new_type: ConfigType) => Promise<void> // NEW
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### 4. Server Action Changes
|
|
138
|
-
|
|
139
|
-
**Fetch Config:**
|
|
140
|
-
```typescript
|
|
141
|
-
// OLD
|
|
142
|
-
async function fetch_config(
|
|
143
|
-
level: ConfigLevel,
|
|
144
|
-
context: AppConfigContext
|
|
145
|
-
): Promise<AppConfigItem[]>
|
|
146
|
-
|
|
147
|
-
// NEW
|
|
148
|
-
async function fetch_config(
|
|
149
|
-
context: AppConfigContext
|
|
150
|
-
): Promise<AppConfigItem[]>
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**Save Config:**
|
|
154
|
-
```typescript
|
|
155
|
-
// OLD
|
|
156
|
-
const item = {
|
|
157
|
-
org_id: context.org_id,
|
|
158
|
-
user_id: level === 'user' ? context.user_id : null,
|
|
159
|
-
config_value: value
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// NEW
|
|
163
|
-
const item = {
|
|
164
|
-
scope_id: context.scope_id,
|
|
165
|
-
user_id: context.user_id ?? null,
|
|
166
|
-
config_value_text: type === 'general' ? value : '',
|
|
167
|
-
config_value_json: type === 'json' ? value : {},
|
|
168
|
-
config_type: type
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
**Delete Config:**
|
|
173
|
-
```typescript
|
|
174
|
-
// OLD
|
|
175
|
-
async function delete_config(
|
|
176
|
-
section: string,
|
|
177
|
-
name: string,
|
|
178
|
-
level: ConfigLevel,
|
|
179
|
-
context: AppConfigContext
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
// NEW
|
|
183
|
-
async function delete_config(
|
|
184
|
-
section: string,
|
|
185
|
-
name: string,
|
|
186
|
-
context: AppConfigContext
|
|
187
|
-
)
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
## Migration Steps
|
|
191
|
-
|
|
192
|
-
### 1. Database Migration
|
|
193
|
-
|
|
194
|
-
**PostgreSQL:**
|
|
195
|
-
```sql
|
|
196
|
-
-- Add new columns
|
|
197
|
-
ALTER TABLE hazo_app_config
|
|
198
|
-
ADD COLUMN scope_id TEXT,
|
|
199
|
-
ADD COLUMN config_value_text TEXT,
|
|
200
|
-
ADD COLUMN config_value_json JSONB,
|
|
201
|
-
ADD COLUMN config_type TEXT;
|
|
202
|
-
|
|
203
|
-
-- Migrate existing data
|
|
204
|
-
UPDATE hazo_app_config SET
|
|
205
|
-
scope_id = org_id,
|
|
206
|
-
config_value_text = config_value,
|
|
207
|
-
config_value_json = '{}'::jsonb,
|
|
208
|
-
config_type = 'general';
|
|
209
|
-
|
|
210
|
-
-- Drop old columns
|
|
211
|
-
ALTER TABLE hazo_app_config
|
|
212
|
-
DROP COLUMN org_id,
|
|
213
|
-
DROP COLUMN config_value;
|
|
214
|
-
|
|
215
|
-
-- Add constraints
|
|
216
|
-
ALTER TABLE hazo_app_config
|
|
217
|
-
ALTER COLUMN config_type SET NOT NULL,
|
|
218
|
-
ADD CONSTRAINT check_type_values CHECK (config_type IN ('json', 'general')),
|
|
219
|
-
ADD CONSTRAINT check_value_exclusivity CHECK (
|
|
220
|
-
(config_type = 'general' AND config_value_text IS NOT NULL AND config_value_json IS NULL) OR
|
|
221
|
-
(config_type = 'json' AND config_value_json IS NOT NULL AND config_value_text IS NULL)
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
-- Update indexes
|
|
225
|
-
DROP INDEX IF EXISTS idx_hazo_app_config_org;
|
|
226
|
-
DROP INDEX IF EXISTS idx_hazo_app_config_unique;
|
|
227
|
-
|
|
228
|
-
CREATE INDEX idx_hazo_app_config_scope ON hazo_app_config(scope_id);
|
|
229
|
-
CREATE UNIQUE INDEX idx_hazo_app_config_unique ON hazo_app_config(scope_id, user_id, config_section, config_name);
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
**SQLite:**
|
|
233
|
-
```sql
|
|
234
|
-
-- SQLite doesn't support dropping columns, so recreate the table
|
|
235
|
-
CREATE TABLE hazo_app_config_new (
|
|
236
|
-
id TEXT PRIMARY KEY,
|
|
237
|
-
scope_id TEXT,
|
|
238
|
-
user_id TEXT,
|
|
239
|
-
config_section TEXT NOT NULL,
|
|
240
|
-
config_name TEXT NOT NULL,
|
|
241
|
-
config_value_text TEXT,
|
|
242
|
-
config_value_json TEXT,
|
|
243
|
-
config_type TEXT NOT NULL CHECK (config_type IN ('json', 'general')),
|
|
244
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
245
|
-
changed_at TEXT,
|
|
246
|
-
CHECK (
|
|
247
|
-
(config_type = 'general' AND config_value_text IS NOT NULL AND config_value_json IS NULL) OR
|
|
248
|
-
(config_type = 'json' AND config_value_json IS NOT NULL AND config_value_text IS NULL)
|
|
249
|
-
)
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
-- Migrate data
|
|
253
|
-
INSERT INTO hazo_app_config_new (id, scope_id, user_id, config_section, config_name, config_value_text, config_value_json, config_type, created_at, changed_at)
|
|
254
|
-
SELECT id, org_id, user_id, config_section, config_name, config_value, NULL, 'general', created_at, changed_at
|
|
255
|
-
FROM hazo_app_config;
|
|
256
|
-
|
|
257
|
-
-- Replace old table
|
|
258
|
-
DROP TABLE hazo_app_config;
|
|
259
|
-
ALTER TABLE hazo_app_config_new RENAME TO hazo_app_config;
|
|
260
|
-
|
|
261
|
-
-- Recreate indexes
|
|
262
|
-
CREATE INDEX idx_hazo_app_config_scope ON hazo_app_config(scope_id);
|
|
263
|
-
CREATE INDEX idx_hazo_app_config_user ON hazo_app_config(user_id);
|
|
264
|
-
CREATE UNIQUE INDEX idx_hazo_app_config_unique ON hazo_app_config(scope_id, user_id, config_section, config_name);
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### 2. Code Migration
|
|
268
|
-
|
|
269
|
-
**Update Component Usage:**
|
|
270
|
-
```typescript
|
|
271
|
-
// OLD
|
|
272
|
-
import { AppConfig } from 'hazo_config/components'
|
|
273
|
-
|
|
274
|
-
<AppConfig
|
|
275
|
-
level="org"
|
|
276
|
-
context={{ org_id: DEMO_ORG_ID }}
|
|
277
|
-
fetch_config={fetch_config}
|
|
278
|
-
save_config={save_config}
|
|
279
|
-
delete_config={delete_config}
|
|
280
|
-
/>
|
|
281
|
-
|
|
282
|
-
// NEW
|
|
283
|
-
import { AppConfig } from 'hazo_config/components'
|
|
284
|
-
|
|
285
|
-
<AppConfig
|
|
286
|
-
title="Scope Settings"
|
|
287
|
-
context={{ scope_id: DEMO_SCOPE_ID }}
|
|
288
|
-
fetch_config={fetch_config}
|
|
289
|
-
save_config={save_config}
|
|
290
|
-
delete_config={delete_config}
|
|
291
|
-
/>
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
**Update Server Actions:**
|
|
295
|
-
```typescript
|
|
296
|
-
// OLD
|
|
297
|
-
export async function fetch_config(
|
|
298
|
-
level: ConfigLevel,
|
|
299
|
-
context: AppConfigContext
|
|
300
|
-
): Promise<AppConfigItem[]> {
|
|
301
|
-
const query = new QueryBuilder()
|
|
302
|
-
.from('hazo_app_config')
|
|
303
|
-
.where('org_id', 'eq', context.org_id)
|
|
304
|
-
|
|
305
|
-
if (level === 'org') {
|
|
306
|
-
query.where('user_id', 'is', null)
|
|
307
|
-
} else {
|
|
308
|
-
query.where('user_id', 'eq', context.user_id)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return await hazo.query(query)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// NEW
|
|
315
|
-
export async function fetch_config(
|
|
316
|
-
context: AppConfigContext
|
|
317
|
-
): Promise<AppConfigItem[]> {
|
|
318
|
-
const query = new QueryBuilder()
|
|
319
|
-
.from('hazo_app_config')
|
|
320
|
-
.where('scope_id', 'eq', context.scope_id)
|
|
321
|
-
|
|
322
|
-
if (context.user_id) {
|
|
323
|
-
query.where('user_id', 'eq', context.user_id)
|
|
324
|
-
} else {
|
|
325
|
-
query.where('user_id', 'is', null)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const result = await hazo.query(query)
|
|
329
|
-
|
|
330
|
-
// Parse JSON for SQLite (stored as TEXT)
|
|
331
|
-
return result.map(item => ({
|
|
332
|
-
...item,
|
|
333
|
-
config_value_json: item.config_value_json
|
|
334
|
-
? JSON.parse(item.config_value_json)
|
|
335
|
-
: {}
|
|
336
|
-
}))
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export async function save_config(
|
|
340
|
-
item: Omit<AppConfigItem, 'id' | 'created_at' | 'changed_at'>
|
|
341
|
-
): Promise<void> {
|
|
342
|
-
// Validate mutual exclusivity
|
|
343
|
-
if (item.config_type === 'general' && !item.config_value_text) {
|
|
344
|
-
throw new Error('General type requires config_value_text')
|
|
345
|
-
}
|
|
346
|
-
if (item.config_type === 'json' && !item.config_value_json) {
|
|
347
|
-
throw new Error('JSON type requires config_value_json')
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// For SQLite: stringify JSON
|
|
351
|
-
const db_data = {
|
|
352
|
-
config_value_text: item.config_type === 'general' ? item.config_value_text : null,
|
|
353
|
-
config_value_json: item.config_type === 'json'
|
|
354
|
-
? JSON.stringify(item.config_value_json)
|
|
355
|
-
: null,
|
|
356
|
-
config_type: item.config_type,
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// ... rest of save logic
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export async function delete_config(
|
|
363
|
-
section: string,
|
|
364
|
-
name: string,
|
|
365
|
-
context: AppConfigContext
|
|
366
|
-
): Promise<void> {
|
|
367
|
-
const query = new QueryBuilder()
|
|
368
|
-
.from('hazo_app_config')
|
|
369
|
-
.where('scope_id', 'eq', context.scope_id)
|
|
370
|
-
.where('config_section', 'eq', section)
|
|
371
|
-
.where('config_name', 'eq', name)
|
|
372
|
-
|
|
373
|
-
if (context.user_id) {
|
|
374
|
-
query.where('user_id', 'eq', context.user_id)
|
|
375
|
-
} else {
|
|
376
|
-
query.where('user_id', 'is', null)
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
await hazo.query(query, 'DELETE')
|
|
380
|
-
}
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
**Update Hook Usage:**
|
|
384
|
-
```typescript
|
|
385
|
-
// OLD
|
|
386
|
-
const {
|
|
387
|
-
sections,
|
|
388
|
-
set_value,
|
|
389
|
-
delete_value
|
|
390
|
-
} = useAppConfig('org', context, fetch_config, save_config, delete_config)
|
|
391
|
-
|
|
392
|
-
// Access value
|
|
393
|
-
const value = sections.general.company_name // string
|
|
394
|
-
|
|
395
|
-
// Set value
|
|
396
|
-
await set_value('general', 'company_name', 'New Name')
|
|
397
|
-
|
|
398
|
-
// NEW
|
|
399
|
-
const {
|
|
400
|
-
sections,
|
|
401
|
-
set_value,
|
|
402
|
-
delete_value,
|
|
403
|
-
change_type
|
|
404
|
-
} = useAppConfig(context, fetch_config, save_config, delete_config)
|
|
405
|
-
|
|
406
|
-
// Access value
|
|
407
|
-
const item = sections.general.company_name // AppConfigItem
|
|
408
|
-
const value = item.config_type === 'json'
|
|
409
|
-
? item.config_value_json
|
|
410
|
-
: item.config_value_text
|
|
411
|
-
|
|
412
|
-
// Set general value
|
|
413
|
-
await set_value('general', 'company_name', 'New Name', 'general')
|
|
414
|
-
|
|
415
|
-
// Set JSON value
|
|
416
|
-
await set_value('features', 'flags', { beta: true }, 'json')
|
|
417
|
-
|
|
418
|
-
// Change type
|
|
419
|
-
await change_type('general', 'company_name', 'json')
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
### 3. Import Updates
|
|
423
|
-
|
|
424
|
-
**Type Imports:**
|
|
425
|
-
```typescript
|
|
426
|
-
// OLD
|
|
427
|
-
import type { ConfigLevel, AppConfigItem } from 'hazo_config'
|
|
428
|
-
|
|
429
|
-
// NEW
|
|
430
|
-
import type { ConfigType, AppConfigItem } from 'hazo_config'
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
## New Features in v2
|
|
434
|
-
|
|
435
|
-
### 1. JSON Configuration Values
|
|
436
|
-
|
|
437
|
-
You can now store structured configuration as JSON:
|
|
438
|
-
|
|
439
|
-
```typescript
|
|
440
|
-
await set_value('features', 'enabled_features', {
|
|
441
|
-
analytics: true,
|
|
442
|
-
notifications: false,
|
|
443
|
-
api_access: true
|
|
444
|
-
}, 'json')
|
|
445
|
-
|
|
446
|
-
await set_value('branding', 'theme_config', {
|
|
447
|
-
dark_mode: true,
|
|
448
|
-
accent_color: '#3B82F6',
|
|
449
|
-
font: 'Inter'
|
|
450
|
-
}, 'json')
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### 2. Type Conversion
|
|
454
|
-
|
|
455
|
-
Convert between general and JSON types:
|
|
456
|
-
|
|
457
|
-
```typescript
|
|
458
|
-
// Convert text to JSON
|
|
459
|
-
await change_type('general', 'settings', 'json')
|
|
460
|
-
// Will try JSON.parse(), fallback to { value: text }
|
|
461
|
-
|
|
462
|
-
// Convert JSON to text
|
|
463
|
-
await change_type('features', 'flags', 'general')
|
|
464
|
-
// Will JSON.stringify() the object
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
### 3. UI Enhancements
|
|
468
|
-
|
|
469
|
-
- Type badge showing 'json' or 'general'
|
|
470
|
-
- JSON editor with syntax validation
|
|
471
|
-
- Type change button with confirmation
|
|
472
|
-
- Type selector when adding new keys
|
|
473
|
-
|
|
474
|
-
## Testing Migration
|
|
475
|
-
|
|
476
|
-
Before migrating production:
|
|
477
|
-
|
|
478
|
-
1. **Backup your database**
|
|
479
|
-
2. **Test migration on a copy:**
|
|
480
|
-
```bash
|
|
481
|
-
# PostgreSQL
|
|
482
|
-
pg_dump production_db > backup.sql
|
|
483
|
-
createdb test_db
|
|
484
|
-
psql test_db < backup.sql
|
|
485
|
-
# Run migration scripts on test_db
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
3. **Update and test your application:**
|
|
489
|
-
```bash
|
|
490
|
-
npm install hazo_config@^2.0.0
|
|
491
|
-
npm run build
|
|
492
|
-
npm test
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
4. **Verify data integrity:**
|
|
496
|
-
- All `config_value` migrated to `config_value_text`
|
|
497
|
-
- All items have `config_type = 'general'`
|
|
498
|
-
- All items have `scope_id` matching old `org_id`
|
|
499
|
-
- Unique constraints still enforced
|
|
500
|
-
|
|
501
|
-
## Rollback Plan
|
|
502
|
-
|
|
503
|
-
If you need to rollback:
|
|
504
|
-
|
|
505
|
-
```sql
|
|
506
|
-
-- PostgreSQL
|
|
507
|
-
ALTER TABLE hazo_app_config
|
|
508
|
-
ADD COLUMN org_id TEXT,
|
|
509
|
-
ADD COLUMN config_value TEXT;
|
|
510
|
-
|
|
511
|
-
UPDATE hazo_app_config SET
|
|
512
|
-
org_id = scope_id,
|
|
513
|
-
config_value = COALESCE(config_value_text, config_value_json::text);
|
|
514
|
-
|
|
515
|
-
ALTER TABLE hazo_app_config
|
|
516
|
-
DROP COLUMN scope_id,
|
|
517
|
-
DROP COLUMN config_value_text,
|
|
518
|
-
DROP COLUMN config_value_json,
|
|
519
|
-
DROP COLUMN config_type;
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
## Support
|
|
523
|
-
|
|
524
|
-
For migration assistance:
|
|
525
|
-
- File an issue: https://github.com/pub12/hazo_config/issues
|
|
526
|
-
- Email: support@example.com
|
|
527
|
-
|
|
528
|
-
## Version History
|
|
529
|
-
|
|
530
|
-
- **v2.0.0**: Schema migration (scope_id, JSON support)
|
|
531
|
-
- **v1.4.2**: Last version before breaking changes
|