hazo_config 2.0.2 → 2.1.0
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/README.md +12 -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 +4 -1
- package/MIGRATION_V2.md +0 -531
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
|
|
|
@@ -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,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_config",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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
|
+
],
|
|
8
11
|
"exports": {
|
|
9
12
|
".": {
|
|
10
13
|
"types": "./dist/index.d.ts",
|
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
|