hazo_config 2.4.3 → 2.6.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/CHANGE_LOG.md +73 -0
- package/dist/components/app_config.d.ts.map +1 -1
- package/dist/components/app_config.js +14 -13
- 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 +8 -4
- package/dist/components/app_config_list_editor/components/bulk_edit_modal.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/components/bulk_edit_modal.js +5 -4
- package/dist/components/app_config_list_editor/components/color_swatch_picker.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/components/color_swatch_picker.js +2 -1
- package/dist/components/app_config_list_editor/components/delete_dialog.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/components/delete_dialog.js +2 -1
- package/dist/components/app_config_list_editor/components/edit_modal.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/components/edit_modal.js +14 -7
- package/dist/components/app_config_list_editor/components/empty_state.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/components/empty_state.js +2 -1
- package/dist/components/app_config_list_editor/components/import_export_buttons.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/components/import_export_buttons.js +8 -7
- package/dist/components/app_config_list_editor/components/list_item_row.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/components/list_item_row.js +4 -3
- package/dist/components/app_config_list_editor/components/save_status_indicator.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/components/save_status_indicator.js +2 -1
- package/dist/components/app_config_list_editor/components/search_bar.d.ts.map +1 -1
- package/dist/components/app_config_list_editor/components/search_bar.js +2 -1
- package/dist/components/config_editor.d.ts.map +1 -1
- package/dist/components/config_editor.js +5 -4
- package/dist/components/config_viewer.d.ts.map +1 -1
- package/dist/components/config_viewer.js +3 -2
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/lib/clone_to_override.d.ts +26 -0
- package/dist/lib/clone_to_override.d.ts.map +1 -0
- package/dist/lib/clone_to_override.js +60 -0
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +7 -0
- package/dist/lib/override_count.d.ts +14 -0
- package/dist/lib/override_count.d.ts.map +1 -0
- package/dist/lib/override_count.js +32 -0
- package/dist/lib/strings.d.ts +2 -0
- package/dist/lib/strings.d.ts.map +1 -0
- package/dist/lib/strings.js +18 -0
- package/dist/lib/two_tier.d.ts +60 -0
- package/dist/lib/two_tier.d.ts.map +1 -0
- package/dist/lib/two_tier.js +56 -0
- package/dist/locales/en.json +104 -0
- package/dist/server-data/index.d.ts +17 -0
- package/dist/server-data/index.d.ts.map +1 -0
- package/dist/server-data/index.js +16 -0
- package/package.json +20 -5
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Pencil, Trash2 } from 'lucide-react';
|
|
3
3
|
import { cn } from 'hazo_ui';
|
|
4
|
+
import { t } from '../../../lib/strings.js';
|
|
4
5
|
export function ListItemRow({ item, index, columns, render_item, render_item_indicator, on_edit, on_delete, show_checkbox, is_selected, on_toggle_select, }) {
|
|
5
6
|
// Custom render overrides everything
|
|
6
7
|
if (render_item) {
|
|
7
|
-
return (_jsxs("div", { className: "cls_list_editor_row flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors", children: [show_checkbox && (_jsx("input", { type: "checkbox", checked: is_selected ?? false, onChange: on_toggle_select, className: "shrink-0 w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500", "aria-label":
|
|
8
|
+
return (_jsxs("div", { className: "cls_list_editor_row flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors", children: [show_checkbox && (_jsx("input", { type: "checkbox", checked: is_selected ?? false, onChange: on_toggle_select, className: "shrink-0 w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500", "aria-label": t('list_item.select_item_aria') })), _jsx("div", { className: "flex-1 min-w-0", children: render_item(item, index) }), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [_jsx("button", { type: "button", onClick: on_edit, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors", "aria-label": t('list_item.edit_item_aria'), children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx("button", { type: "button", onClick: on_delete, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors", "aria-label": t('list_item.delete_item_aria'), children: _jsx(Trash2, { className: "w-4 h-4" }) })] })] }));
|
|
8
9
|
}
|
|
9
10
|
// Default rendering based on column definitions
|
|
10
11
|
const primary_cols = columns.filter(c => c.list_display === 'primary');
|
|
11
12
|
const secondary_cols = columns.filter(c => c.list_display === 'secondary');
|
|
12
13
|
const badge_cols = columns.filter(c => c.list_display === 'badge');
|
|
13
|
-
return (_jsxs("div", { className: "cls_list_editor_row flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors", children: [show_checkbox && (_jsx("input", { type: "checkbox", checked: is_selected ?? false, onChange: on_toggle_select, className: "shrink-0 w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500", "aria-label":
|
|
14
|
+
return (_jsxs("div", { className: "cls_list_editor_row flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors", children: [show_checkbox && (_jsx("input", { type: "checkbox", checked: is_selected ?? false, onChange: on_toggle_select, className: "shrink-0 w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500", "aria-label": t('list_item.select_item_aria') })), render_item_indicator && (_jsx("div", { className: "shrink-0", children: render_item_indicator(item) })), _jsxs("div", { className: "flex-1 min-w-0", children: [primary_cols.map((col) => (_jsx("div", { className: "text-sm font-medium text-gray-900 truncate", children: col.format?.(item[col.field], item) ?? String(item[col.field] ?? '') }, col.field))), secondary_cols.map((col) => (_jsx("div", { className: "text-xs text-gray-500 truncate mt-0.5", children: col.format?.(item[col.field], item) ?? String(item[col.field] ?? '') }, col.field)))] }), badge_cols.length > 0 && (_jsx("div", { className: "flex items-center gap-2 shrink-0", children: badge_cols.map((col) => {
|
|
14
15
|
const badge_class = typeof col.badge_class === 'function'
|
|
15
16
|
? col.badge_class(item[col.field], item)
|
|
16
17
|
: col.badge_class;
|
|
17
18
|
return (_jsx("span", { className: cn('inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-mono', badge_class || 'bg-gray-100 text-gray-600'), children: col.format?.(item[col.field], item) ?? String(item[col.field] ?? '') }, col.field));
|
|
18
|
-
}) })), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [_jsx("button", { type: "button", onClick: on_edit, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors", "aria-label":
|
|
19
|
+
}) })), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [_jsx("button", { type: "button", onClick: on_edit, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors", "aria-label": t('list_item.edit_item_aria'), children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx("button", { type: "button", onClick: on_delete, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors", "aria-label": t('list_item.delete_item_aria'), children: _jsx(Trash2, { className: "w-4 h-4" }) })] })] }));
|
|
19
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save_status_indicator.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/save_status_indicator.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"save_status_indicator.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/save_status_indicator.tsx"],"names":[],"mappings":"AAUA,UAAU,wBAAwB;IAChC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAA;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,SAAS,GACV,EAAE,wBAAwB,sCA+C1B"}
|
|
@@ -5,6 +5,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
5
5
|
import { useEffect, useState } from 'react';
|
|
6
6
|
import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
|
|
7
7
|
import { cn } from 'hazo_ui';
|
|
8
|
+
import { t } from '../../../lib/strings.js';
|
|
8
9
|
export function SaveStatusIndicator({ status, className, }) {
|
|
9
10
|
const [visible, set_visible] = useState(false);
|
|
10
11
|
useEffect(() => {
|
|
@@ -22,5 +23,5 @@ export function SaveStatusIndicator({ status, className, }) {
|
|
|
22
23
|
}, [status]);
|
|
23
24
|
if (!visible && status === 'idle')
|
|
24
25
|
return null;
|
|
25
|
-
return (_jsxs("div", { className: cn('cls_save_status flex items-center gap-1.5 text-xs transition-opacity duration-300', !visible && 'opacity-0', visible && 'opacity-100', className), children: [status === 'saving' && (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "w-3.5 h-3.5 text-gray-500 animate-spin" }), _jsx("span", { className: "text-gray-500", children:
|
|
26
|
+
return (_jsxs("div", { className: cn('cls_save_status flex items-center gap-1.5 text-xs transition-opacity duration-300', !visible && 'opacity-0', visible && 'opacity-100', className), children: [status === 'saving' && (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "w-3.5 h-3.5 text-gray-500 animate-spin" }), _jsx("span", { className: "text-gray-500", children: t('save_status.saving') })] })), status === 'saved' && (_jsxs(_Fragment, { children: [_jsx(CheckCircle2, { className: "w-3.5 h-3.5 text-green-600" }), _jsx("span", { className: "text-green-600", children: t('save_status.saved') })] })), status === 'error' && (_jsxs(_Fragment, { children: [_jsx(AlertCircle, { className: "w-3.5 h-3.5 text-red-600" }), _jsx("span", { className: "text-red-600", children: t('save_status.error') })] }))] }));
|
|
26
27
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search_bar.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/search_bar.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"search_bar.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/search_bar.tsx"],"names":[],"mappings":"AAOA,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,SAAS,EACT,UAAU,EACV,UAAU,EACV,SAAS,GACV,EAAE,cAAc,+BA6BhB"}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Pill-shaped search input with icon, clear button, and item count
|
|
4
4
|
import { Search, X } from 'lucide-react';
|
|
5
5
|
import { cn } from 'hazo_ui';
|
|
6
|
+
import { t } from '../../../lib/strings.js';
|
|
6
7
|
export function SearchBar({ value, on_change, item_count, item_label, className, }) {
|
|
7
|
-
return (_jsxs("div", { className: cn('cls_search_bar flex items-center gap-3', className), children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" }), _jsx("input", { type: "text", value: value, onChange: (e) => on_change(e.target.value), placeholder:
|
|
8
|
+
return (_jsxs("div", { className: cn('cls_search_bar flex items-center gap-3', className), children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" }), _jsx("input", { type: "text", value: value, onChange: (e) => on_change(e.target.value), placeholder: t('search.placeholder', { item_label }), className: "w-full pl-9 pr-8 py-2 text-sm rounded-full border border-gray-200 bg-white focus:outline-none focus:ring-2 focus:ring-violet-500/20 focus:border-violet-300 transition-colors", "aria-label": t('search.aria_label', { item_label }) }), value && (_jsx("button", { type: "button", onClick: () => on_change(''), className: "absolute right-2.5 top-1/2 -translate-y-1/2 w-5 h-5 flex items-center justify-center rounded-full text-gray-400 hover:text-gray-600 hover:bg-gray-100", "aria-label": t('search.clear_aria_label'), children: _jsx(X, { className: "w-3.5 h-3.5" }) }))] }), _jsxs("span", { className: "text-sm text-gray-500 whitespace-nowrap", children: [item_count, " ", item_label] })] }));
|
|
8
9
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config_editor.d.ts","sourceRoot":"","sources":["../../src/components/config_editor.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"config_editor.d.ts","sourceRoot":"","sources":["../../src/components/config_editor.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAKrD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,eAAe,EAAE,cAAc,CAAA;IAC/B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACjC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAWD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAgYpD,CAAA"}
|
|
@@ -6,6 +6,7 @@ import React, { useState, useCallback, useMemo } from 'react';
|
|
|
6
6
|
import { cn } from 'hazo_ui';
|
|
7
7
|
import { RefreshCw, Save, Trash2, Eye, EyeOff } from 'lucide-react';
|
|
8
8
|
import { useConfigSections, is_sensitive_field, mask_value, DEFAULT_SENSITIVE_PATTERNS } from './use_config_sections.js';
|
|
9
|
+
import { t } from '../lib/strings.js';
|
|
9
10
|
/**
|
|
10
11
|
* Config editor component
|
|
11
12
|
* Provides interface for viewing and editing configuration with refresh and save capabilities
|
|
@@ -208,9 +209,9 @@ export const ConfigEditor = ({ config_provider, className, on_update, on_error,
|
|
|
208
209
|
const current_section_data = useMemo(() => {
|
|
209
210
|
return selected_section ? sections[selected_section] : null;
|
|
210
211
|
}, [selected_section, sections]);
|
|
211
|
-
return (_jsxs("div", { className: cn('cls_config_editor space-y-4', className), children: [_jsxs("div", { className: "cls_config_editor_header flex items-center justify-between border-b pb-2", children: [_jsxs("h2", { className: "text-xl font-semibold", children: [
|
|
212
|
+
return (_jsxs("div", { className: cn('cls_config_editor space-y-4', className), children: [_jsxs("div", { className: "cls_config_editor_header flex items-center justify-between border-b pb-2", children: [_jsxs("h2", { className: "text-xl font-semibold", children: [t('config_editor.title'), has_unsaved_changes && (_jsx("span", { className: "ml-2 text-sm text-amber-600", children: t('config_editor.unsaved_suffix') }))] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: handle_refresh, className: "px-3 py-1 border rounded hover:bg-muted flex items-center gap-2", "aria-label": t('config_editor.refresh_aria'), type: "button", children: [_jsx(RefreshCw, { size: 16 }), t('config_editor.refresh')] }), _jsxs("button", { onClick: handle_save, className: cn('px-3 py-1 rounded flex items-center gap-2', has_unsaved_changes
|
|
212
213
|
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
|
213
|
-
: 'bg-muted text-muted-foreground'), "aria-label":
|
|
214
|
+
: 'bg-muted text-muted-foreground'), "aria-label": t('config_editor.save_aria'), type: "button", disabled: !has_unsaved_changes, children: [_jsx(Save, { size: 16 }), t('config_editor.save')] })] })] }), _jsxs("div", { className: "cls_config_editor_content grid grid-cols-1 md:grid-cols-3 gap-4", children: [_jsxs("div", { className: "cls_config_sections_list border rounded p-4", children: [_jsx("h3", { className: "font-semibold mb-2", children: t('config_editor.sections_heading') }), _jsx("div", { className: "space-y-1", role: "listbox", "aria-label": t('config_editor.sections_aria'), children: section_list.map((section_name) => (_jsx("button", { onClick: () => handle_section_select(section_name), className: cn('w-full text-left px-2 py-1 rounded text-sm', selected_section === section_name
|
|
214
215
|
? 'bg-primary text-primary-foreground'
|
|
215
216
|
: 'hover:bg-muted'), role: "option", "aria-selected": selected_section === section_name, type: "button", children: section_name }, section_name))) })] }), _jsx("div", { className: "cls_config_section_content md:col-span-2 border rounded p-4", children: selected_section ? (_jsxs(_Fragment, { children: [_jsx("h3", { className: "font-semibold mb-4", children: selected_section }), _jsxs("div", { className: "space-y-3", children: [current_section_data &&
|
|
216
217
|
Object.entries(current_section_data).map(([key, value]) => {
|
|
@@ -218,6 +219,6 @@ export const ConfigEditor = ({ config_provider, className, on_update, on_error,
|
|
|
218
219
|
const field_id = `config-${selected_section}-${key}`;
|
|
219
220
|
const show_value = !is_sensitive || is_revealed(selected_section, key);
|
|
220
221
|
const display_value = get_field_value(selected_section, key, value);
|
|
221
|
-
return (_jsxs("div", { className: "cls_config_item", children: [_jsx("label", { htmlFor: field_id, className: "block text-sm font-medium mb-1", children: key }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { id: field_id, type: is_sensitive && !show_value ? 'password' : 'text', value: is_sensitive && !show_value ? mask_value(display_value) : display_value, onChange: (e) => handle_field_change(selected_section, key, e.target.value), className: "flex-1 px-2 py-1 border rounded", readOnly: is_sensitive && !show_value }), is_sensitive && (_jsx("button", { onClick: () => toggle_reveal(selected_section, key), className: "text-gray-500 hover:text-gray-700", "aria-label": show_value ? '
|
|
222
|
-
}), _jsxs("div", { className: "cls_add_new_key border-t pt-3 mt-3", children: [_jsx("h4", { className: "text-sm font-medium mb-2", children:
|
|
222
|
+
return (_jsxs("div", { className: "cls_config_item", children: [_jsx("label", { htmlFor: field_id, className: "block text-sm font-medium mb-1", children: key }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { id: field_id, type: is_sensitive && !show_value ? 'password' : 'text', value: is_sensitive && !show_value ? mask_value(display_value) : display_value, onChange: (e) => handle_field_change(selected_section, key, e.target.value), className: "flex-1 px-2 py-1 border rounded", readOnly: is_sensitive && !show_value }), is_sensitive && (_jsx("button", { onClick: () => toggle_reveal(selected_section, key), className: "text-gray-500 hover:text-gray-700", "aria-label": show_value ? t('common.hide_value') : t('common.show_value'), type: "button", children: show_value ? _jsx(EyeOff, { size: 18 }) : _jsx(Eye, { size: 18 }) })), _jsx("button", { onClick: () => handle_delete_key(selected_section, key), className: "text-red-500 hover:text-red-700", "aria-label": t('config_editor.delete_key_aria', { key }), type: "button", children: _jsx(Trash2, { size: 18 }) })] })] }, key));
|
|
223
|
+
}), _jsxs("div", { className: "cls_add_new_key border-t pt-3 mt-3", children: [_jsx("h4", { className: "text-sm font-medium mb-2", children: t('config_editor.add_new_key_heading') }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "new-key-name", className: "sr-only", children: t('config_editor.key_name_label') }), _jsx("input", { id: "new-key-name", type: "text", placeholder: t('config_editor.key_name_label'), value: new_key, onChange: handle_new_key_change, className: "w-full px-2 py-1 border rounded text-sm" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "new-key-value", className: "sr-only", children: t('config_editor.value_label') }), _jsx("input", { id: "new-key-value", type: "text", placeholder: t('config_editor.value_label'), value: new_value, onChange: handle_new_value_change, className: "w-full px-2 py-1 border rounded text-sm" })] }), _jsx("button", { onClick: handle_add_key, className: "px-3 py-1 bg-primary text-primary-foreground rounded hover:bg-primary/90 text-sm", type: "button", disabled: !new_key || !new_value, children: t('config_editor.add_key') })] })] })] })] })) : (_jsx("div", { className: "text-center text-muted-foreground py-8", children: t('config_editor.select_section_prompt') })) })] })] }));
|
|
223
224
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config_viewer.d.ts","sourceRoot":"","sources":["../../src/components/config_viewer.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"config_viewer.d.ts","sourceRoot":"","sources":["../../src/components/config_viewer.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAKrD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,eAAe,EAAE,cAAc,CAAA;IAC/B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACjC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA2MpD,CAAA"}
|
|
@@ -6,6 +6,7 @@ import React, { useState, useCallback, useMemo } from 'react';
|
|
|
6
6
|
import { cn } from 'hazo_ui';
|
|
7
7
|
import { Pencil, CheckCircle2, XCircle, Eye, EyeOff } from 'lucide-react';
|
|
8
8
|
import { useConfigSections, is_sensitive_field, mask_value, DEFAULT_SENSITIVE_PATTERNS } from './use_config_sections.js';
|
|
9
|
+
import { t } from '../lib/strings.js';
|
|
9
10
|
/**
|
|
10
11
|
* Config viewer component
|
|
11
12
|
* Displays configuration sections and allows viewing/editing values
|
|
@@ -108,11 +109,11 @@ export const ConfigViewer = ({ config_provider, className, on_update, on_error,
|
|
|
108
109
|
* Memoized section entries for rendering
|
|
109
110
|
*/
|
|
110
111
|
const section_entries = useMemo(() => Object.entries(sections), [sections]);
|
|
111
|
-
return (_jsx("div", { className: cn('cls_config_viewer space-y-4', className), children: section_entries.length === 0 ? (_jsx("div", { className: "p-4 text-center text-muted-foreground", children:
|
|
112
|
+
return (_jsx("div", { className: cn('cls_config_viewer space-y-4', className), children: section_entries.length === 0 ? (_jsx("div", { className: "p-4 text-center text-muted-foreground", children: t('config_viewer.empty') })) : (section_entries.map(([section_name, section_data]) => (_jsxs("div", { className: "cls_config_section border rounded-lg p-4", children: [_jsx("h3", { className: "text-lg font-semibold mb-3", children: section_name }), _jsx("div", { className: "space-y-2", children: Object.entries(section_data).map(([key, value]) => {
|
|
112
113
|
const is_editing = editing?.section === section_name && editing?.key === key;
|
|
113
114
|
const is_sensitive = should_mask_field(key);
|
|
114
115
|
const field_id = `config-${section_name}-${key}`;
|
|
115
116
|
const show_value = !is_sensitive || is_revealed(section_name, key);
|
|
116
|
-
return (_jsx("div", { className: "cls_config_item flex items-center gap-2", children: _jsxs("div", { className: "flex-1", children: [_jsx("label", { htmlFor: field_id, className: "text-sm font-medium text-muted-foreground", children: key }), is_editing ? (_jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("input", { id: field_id, type: is_sensitive ? 'password' : 'text', value: edit_value, onChange: handle_edit_change, className: "flex-1 px-2 py-1 border rounded text-sm", autoFocus: true }), _jsx("button", { onClick: save_edit, className: "text-green-600 hover:text-green-700", "aria-label":
|
|
117
|
+
return (_jsx("div", { className: "cls_config_item flex items-center gap-2", children: _jsxs("div", { className: "flex-1", children: [_jsx("label", { htmlFor: field_id, className: "text-sm font-medium text-muted-foreground", children: key }), is_editing ? (_jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("input", { id: field_id, type: is_sensitive ? 'password' : 'text', value: edit_value, onChange: handle_edit_change, className: "flex-1 px-2 py-1 border rounded text-sm", autoFocus: true }), _jsx("button", { onClick: save_edit, className: "text-green-600 hover:text-green-700", "aria-label": t('config_viewer.save_changes_aria'), type: "button", children: _jsx(CheckCircle2, { size: 20 }) }), _jsx("button", { onClick: cancel_edit, className: "text-red-600 hover:text-red-700", "aria-label": t('config_viewer.cancel_editing_aria'), type: "button", children: _jsx(XCircle, { size: 20 }) })] })) : (_jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("div", { id: field_id, className: "flex-1 px-2 py-1 bg-muted rounded text-sm font-mono", children: show_value ? (value || t('config_viewer.empty_value')) : mask_value(value) }), is_sensitive && (_jsx("button", { onClick: () => toggle_reveal(section_name, key), className: "text-gray-500 hover:text-gray-700", "aria-label": show_value ? t('common.hide_value') : t('common.show_value'), type: "button", children: show_value ? _jsx(EyeOff, { size: 18 }) : _jsx(Eye, { size: 18 }) })), _jsx("button", { onClick: () => start_edit(section_name, key), className: "text-blue-600 hover:text-blue-700", "aria-label": t('config_viewer.edit_key_aria', { key }), type: "button", children: _jsx(Pencil, { size: 18 }) })] }))] }) }, key));
|
|
117
118
|
}) })] }, section_name)))) }));
|
|
118
119
|
};
|
|
@@ -3,7 +3,7 @@ import { type VariantProps } from "class-variance-authority";
|
|
|
3
3
|
declare const buttonVariants: (props?: ({
|
|
4
4
|
variant?: "default" | "link" | "secondary" | "destructive" | "outline" | "ghost" | null | undefined;
|
|
5
5
|
size?: "default" | "sm" | "lg" | "icon" | null | undefined;
|
|
6
|
-
} & import("class-variance-authority/
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
8
8
|
asChild?: boolean;
|
|
9
9
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/components/ui/button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,cAAc;;;
|
|
1
|
+
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/components/ui/button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,cAAc;;;8EA2BnB,CAAA;AAED,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EACnD,YAAY,CAAC,OAAO,cAAc,CAAC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,QAAA,MAAM,MAAM,uFAWX,CAAA;AAGD,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Clone a global (platform-default) two-tier config row into a
|
|
3
|
+
* scope-level override row.
|
|
4
|
+
*
|
|
5
|
+
* hazo_connect is an optional peer dependency here (see `hazo_config/server-data`,
|
|
6
|
+
* the entry point that re-exports this). It is loaded via dynamic import()
|
|
7
|
+
* with bundler-ignore hints, matching `config_loader.ts`'s optional
|
|
8
|
+
* `hazo_core/errors` integration: `hazo_connect/server` and
|
|
9
|
+
* `hazo_connect/nextjs/setup` are ESM-only subpath exports, and this
|
|
10
|
+
* package's build uses moduleResolution:"node" (predates subpath-exports
|
|
11
|
+
* resolution), so TS can't resolve them statically even though they load
|
|
12
|
+
* fine at runtime.
|
|
13
|
+
*/
|
|
14
|
+
export declare class CloneToOverrideError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Clone the global row `globalRowId` from `table` into a new row scoped to
|
|
19
|
+
* `orgScopeId`. Strips `id`/`created_at`/`updated_at` and stamps
|
|
20
|
+
* `cloned_from_at` on the new row.
|
|
21
|
+
*
|
|
22
|
+
* @throws {CloneToOverrideError} when the source row does not exist, is not
|
|
23
|
+
* itself a global row (`scope_id` must be `null`), or the insert returns no row.
|
|
24
|
+
*/
|
|
25
|
+
export declare function cloneToOverride(table: string, globalRowId: string, orgScopeId: string): Promise<Record<string, unknown>>;
|
|
26
|
+
//# sourceMappingURL=clone_to_override.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone_to_override.d.ts","sourceRoot":"","sources":["../../src/lib/clone_to_override.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA0BH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAoBlC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Clone a global (platform-default) two-tier config row into a
|
|
3
|
+
* scope-level override row.
|
|
4
|
+
*
|
|
5
|
+
* hazo_connect is an optional peer dependency here (see `hazo_config/server-data`,
|
|
6
|
+
* the entry point that re-exports this). It is loaded via dynamic import()
|
|
7
|
+
* with bundler-ignore hints, matching `config_loader.ts`'s optional
|
|
8
|
+
* `hazo_core/errors` integration: `hazo_connect/server` and
|
|
9
|
+
* `hazo_connect/nextjs/setup` are ESM-only subpath exports, and this
|
|
10
|
+
* package's build uses moduleResolution:"node" (predates subpath-exports
|
|
11
|
+
* resolution), so TS can't resolve them statically even though they load
|
|
12
|
+
* fine at runtime.
|
|
13
|
+
*/
|
|
14
|
+
async function loadCreateCrudService() {
|
|
15
|
+
// @ts-ignore — hazo_connect/server is a valid subpath export; moduleResolution:"node"
|
|
16
|
+
// predates subpath exports so TS can't resolve it statically, but it loads correctly at runtime.
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
18
|
+
const mod = (await import(/* turbopackIgnore: true */ /* webpackIgnore: true */ 'hazo_connect/server'));
|
|
19
|
+
return mod.createCrudService;
|
|
20
|
+
}
|
|
21
|
+
async function loadGetHazoConnectSingleton() {
|
|
22
|
+
// @ts-ignore — hazo_connect/nextjs/setup is a valid subpath export; see loadCreateCrudService above.
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
24
|
+
const mod = (await import(/* turbopackIgnore: true */ /* webpackIgnore: true */ 'hazo_connect/nextjs/setup'));
|
|
25
|
+
return mod.getHazoConnectSingleton;
|
|
26
|
+
}
|
|
27
|
+
export class CloneToOverrideError extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'CloneToOverrideError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Clone the global row `globalRowId` from `table` into a new row scoped to
|
|
35
|
+
* `orgScopeId`. Strips `id`/`created_at`/`updated_at` and stamps
|
|
36
|
+
* `cloned_from_at` on the new row.
|
|
37
|
+
*
|
|
38
|
+
* @throws {CloneToOverrideError} when the source row does not exist, is not
|
|
39
|
+
* itself a global row (`scope_id` must be `null`), or the insert returns no row.
|
|
40
|
+
*/
|
|
41
|
+
export async function cloneToOverride(table, globalRowId, orgScopeId) {
|
|
42
|
+
const createCrudService = await loadCreateCrudService();
|
|
43
|
+
const getHazoConnectSingleton = await loadGetHazoConnectSingleton();
|
|
44
|
+
const svc = createCrudService(getHazoConnectSingleton(), table);
|
|
45
|
+
const row = await svc.findById(globalRowId);
|
|
46
|
+
if (!row)
|
|
47
|
+
throw new CloneToOverrideError('Global row not found');
|
|
48
|
+
if (row.scope_id !== null)
|
|
49
|
+
throw new CloneToOverrideError('Source row is not a global (scope_id must be null)');
|
|
50
|
+
// Strip the id and timestamps; set new scope
|
|
51
|
+
const { id: _omit, created_at: _ca, updated_at: _ua, ...rest } = row;
|
|
52
|
+
const inserted = await svc.insert({
|
|
53
|
+
...rest,
|
|
54
|
+
scope_id: orgScopeId,
|
|
55
|
+
cloned_from_at: new Date().toISOString(),
|
|
56
|
+
});
|
|
57
|
+
if (!inserted[0])
|
|
58
|
+
throw new CloneToOverrideError('Insert returned no row');
|
|
59
|
+
return inserted[0];
|
|
60
|
+
}
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export { MockConfigProvider } from './mock_config_provider.js';
|
|
12
12
|
export { ConfigErrorCode } from './types.js';
|
|
13
|
+
export { t } from './strings.js';
|
|
13
14
|
export type { ConfigProvider, HazoConfigOptions, Logger, HazoConfigError, LogData } from './types.js';
|
|
14
15
|
export type { ConfigType, AppConfigItem, AppConfigContext, AppConfigProps, UseAppConfigResult } from './app_config_types.js';
|
|
16
|
+
export { mergeTwoTier } from './two_tier.js';
|
|
17
|
+
export type { TwoTierRow, MergeOptions } from './two_tier.js';
|
|
15
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAI5C,OAAO,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAChC,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,MAAM,EACN,eAAe,EACf,OAAO,EACR,MAAM,YAAY,CAAA;AAGnB,YAAY,EACV,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EACnB,MAAM,uBAAuB,CAAA;AAK9B,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA"}
|
package/dist/lib/index.js
CHANGED
|
@@ -11,3 +11,10 @@
|
|
|
11
11
|
// Client-safe exports (no Node.js dependencies)
|
|
12
12
|
export { MockConfigProvider } from './mock_config_provider.js';
|
|
13
13
|
export { ConfigErrorCode } from './types.js';
|
|
14
|
+
// i18n — namespaced translator for this package's own baked-in UI strings
|
|
15
|
+
// (config editor/viewer, AppConfig, AppConfigListEditor). See strings.ts.
|
|
16
|
+
export { t } from './strings.js';
|
|
17
|
+
// Two-tier (global default + scope-level override) merge -- pure, no
|
|
18
|
+
// database dependency. See 'hazo_config/server-data' for the hazo_connect-
|
|
19
|
+
// backed clone/override-count helpers that operate on the same shape.
|
|
20
|
+
export { mergeTwoTier } from './two_tier.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Count scope-level override rows for a two-tier config key.
|
|
3
|
+
*
|
|
4
|
+
* hazo_connect is an optional peer dependency here (see `hazo_config/server-data`,
|
|
5
|
+
* the entry point that re-exports this) and is loaded via dynamic import()
|
|
6
|
+
* for the same reason as `clone_to_override.ts` -- see that file's header.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Count how many override (non-global) rows exist in `table` for the given
|
|
10
|
+
* `keyColumn`/`keyValue`. A row counts as an override when its `scope_id`
|
|
11
|
+
* is not `null`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getOverrideCount(table: string, keyColumn: string, keyValue: string): Promise<number>;
|
|
14
|
+
//# sourceMappingURL=override_count.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"override_count.d.ts","sourceRoot":"","sources":["../../src/lib/override_count.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA8BH;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAOjB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Count scope-level override rows for a two-tier config key.
|
|
3
|
+
*
|
|
4
|
+
* hazo_connect is an optional peer dependency here (see `hazo_config/server-data`,
|
|
5
|
+
* the entry point that re-exports this) and is loaded via dynamic import()
|
|
6
|
+
* for the same reason as `clone_to_override.ts` -- see that file's header.
|
|
7
|
+
*/
|
|
8
|
+
async function loadCreateCrudService() {
|
|
9
|
+
// @ts-ignore — hazo_connect/server is a valid subpath export; moduleResolution:"node"
|
|
10
|
+
// predates subpath exports so TS can't resolve it statically, but it loads correctly at runtime.
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
12
|
+
const mod = (await import(/* turbopackIgnore: true */ /* webpackIgnore: true */ 'hazo_connect/server'));
|
|
13
|
+
return mod.createCrudService;
|
|
14
|
+
}
|
|
15
|
+
async function loadGetHazoConnectSingleton() {
|
|
16
|
+
// @ts-ignore — hazo_connect/nextjs/setup is a valid subpath export; see loadCreateCrudService above.
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
18
|
+
const mod = (await import(/* turbopackIgnore: true */ /* webpackIgnore: true */ 'hazo_connect/nextjs/setup'));
|
|
19
|
+
return mod.getHazoConnectSingleton;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Count how many override (non-global) rows exist in `table` for the given
|
|
23
|
+
* `keyColumn`/`keyValue`. A row counts as an override when its `scope_id`
|
|
24
|
+
* is not `null`.
|
|
25
|
+
*/
|
|
26
|
+
export async function getOverrideCount(table, keyColumn, keyValue) {
|
|
27
|
+
const createCrudService = await loadCreateCrudService();
|
|
28
|
+
const getHazoConnectSingleton = await loadGetHazoConnectSingleton();
|
|
29
|
+
const svc = createCrudService(getHazoConnectSingleton(), table);
|
|
30
|
+
const rows = (await svc.list((q) => q.where(keyColumn, 'eq', keyValue).select('id,scope_id'))) ?? [];
|
|
31
|
+
return rows.filter((r) => r.scope_id !== null).length;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../../src/lib/strings.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,CAAC,gCAA8D,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// hazo_config/src/lib/strings.ts — i18n plumbing for hazo_config's own
|
|
2
|
+
// baked-in UI strings (config editor/viewer, AppConfig, and the
|
|
3
|
+
// AppConfigListEditor family), via hazo_core's canonical bridge
|
|
4
|
+
// (`hazo_core/i18n`).
|
|
5
|
+
//
|
|
6
|
+
// hazo_config's tsconfig.json uses `moduleResolution: "bundler"` (not
|
|
7
|
+
// `Node16`), and `resolveJsonModule: true` is set — so a plain JSON import
|
|
8
|
+
// (no `with { type: 'json' }` attribute) resolves cleanly. tsconfig.build.json
|
|
9
|
+
// previously overrode `moduleResolution` to the classic `"node"` resolver,
|
|
10
|
+
// which cannot follow a package.json `exports` subpath like `hazo_core/i18n`
|
|
11
|
+
// (TS2307) — it was switched to `"bundler"` (matching tsconfig.json) as part
|
|
12
|
+
// of this package's i18n adoption. All of this package's components that use
|
|
13
|
+
// `t()` are `'use client'` components, so a plain import (browser-safe, no
|
|
14
|
+
// `createRequire`) is used rather than the Node-only `createRequire`
|
|
15
|
+
// fallback documented for server-only consumers of this pattern.
|
|
16
|
+
import en from '../locales/en.json';
|
|
17
|
+
import { createPackageT } from 'hazo_core/i18n';
|
|
18
|
+
export const t = createPackageT('hazo_config', en);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Two-tier (global default + scope-level override) config merge.
|
|
3
|
+
*
|
|
4
|
+
* Companion to the AppConfig database-backed configuration story (see
|
|
5
|
+
* `app_config_types.ts`): a global row has `scope_id: null`; an override row
|
|
6
|
+
* has the caller's `scope_id` and shadows the global row that shares its key.
|
|
7
|
+
* Pure and framework-agnostic -- the caller fetches `globals`/`overrides`
|
|
8
|
+
* however it likes (hazo_connect, a plain fetch, a mock) and passes both
|
|
9
|
+
* arrays in.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Minimum shape a two-tier row must have. `scope_id: null` marks a global
|
|
13
|
+
* (platform-default) row; any other value marks a scope-level override.
|
|
14
|
+
*/
|
|
15
|
+
export interface TwoTierRow {
|
|
16
|
+
id: string;
|
|
17
|
+
scope_id: string | null;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
[k: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Options for {@link mergeTwoTier}.
|
|
23
|
+
*/
|
|
24
|
+
export interface MergeOptions<T extends TwoTierRow> {
|
|
25
|
+
/** Platform-default rows (`scope_id: null`). */
|
|
26
|
+
globals: T[];
|
|
27
|
+
/** Scope-level override rows. */
|
|
28
|
+
overrides: T[];
|
|
29
|
+
/** Extracts the key two rows are matched on (e.g. a `code` column, or a composite of several columns). */
|
|
30
|
+
keyFn: (row: T) => string;
|
|
31
|
+
/**
|
|
32
|
+
* When true, skip key-based shadowing entirely and return the union of
|
|
33
|
+
* enabled globals and enabled overrides (both tiers additive, not
|
|
34
|
+
* override-wins-by-key).
|
|
35
|
+
*/
|
|
36
|
+
additive?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Merge a global/override row pair into the effective list.
|
|
40
|
+
*
|
|
41
|
+
* Default (non-additive) mode: for each key, an override row shadows the
|
|
42
|
+
* global row that shares it; a key present only in one tier passes through
|
|
43
|
+
* as-is. A row (global or override) is included only while `enabled` is
|
|
44
|
+
* true -- an override with `enabled: false` hides an enabled global at the
|
|
45
|
+
* same key, and a disabled global stays hidden unless an override
|
|
46
|
+
* re-enables it.
|
|
47
|
+
*
|
|
48
|
+
* Additive mode (`additive: true`) skips key matching and returns every
|
|
49
|
+
* enabled row from both tiers.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* mergeTwoTier({
|
|
53
|
+
* globals: [{ id: 'g1', scope_id: null, code: 'invoice', enabled: true }],
|
|
54
|
+
* overrides: [{ id: 'o1', scope_id: 'org-A', code: 'invoice', enabled: true }],
|
|
55
|
+
* keyFn: (r) => r.code,
|
|
56
|
+
* })
|
|
57
|
+
* // -> [{ id: 'o1', scope_id: 'org-A', code: 'invoice', enabled: true }]
|
|
58
|
+
*/
|
|
59
|
+
export declare function mergeTwoTier<T extends TwoTierRow>(opts: MergeOptions<T>): T[];
|
|
60
|
+
//# sourceMappingURL=two_tier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"two_tier.d.ts","sourceRoot":"","sources":["../../src/lib/two_tier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;IAChB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,UAAU;IAChD,gDAAgD;IAChD,OAAO,EAAE,CAAC,EAAE,CAAA;IACZ,iCAAiC;IACjC,SAAS,EAAE,CAAC,EAAE,CAAA;IACd,0GAA0G;IAC1G,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAyB7E"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Two-tier (global default + scope-level override) config merge.
|
|
3
|
+
*
|
|
4
|
+
* Companion to the AppConfig database-backed configuration story (see
|
|
5
|
+
* `app_config_types.ts`): a global row has `scope_id: null`; an override row
|
|
6
|
+
* has the caller's `scope_id` and shadows the global row that shares its key.
|
|
7
|
+
* Pure and framework-agnostic -- the caller fetches `globals`/`overrides`
|
|
8
|
+
* however it likes (hazo_connect, a plain fetch, a mock) and passes both
|
|
9
|
+
* arrays in.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Merge a global/override row pair into the effective list.
|
|
13
|
+
*
|
|
14
|
+
* Default (non-additive) mode: for each key, an override row shadows the
|
|
15
|
+
* global row that shares it; a key present only in one tier passes through
|
|
16
|
+
* as-is. A row (global or override) is included only while `enabled` is
|
|
17
|
+
* true -- an override with `enabled: false` hides an enabled global at the
|
|
18
|
+
* same key, and a disabled global stays hidden unless an override
|
|
19
|
+
* re-enables it.
|
|
20
|
+
*
|
|
21
|
+
* Additive mode (`additive: true`) skips key matching and returns every
|
|
22
|
+
* enabled row from both tiers.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* mergeTwoTier({
|
|
26
|
+
* globals: [{ id: 'g1', scope_id: null, code: 'invoice', enabled: true }],
|
|
27
|
+
* overrides: [{ id: 'o1', scope_id: 'org-A', code: 'invoice', enabled: true }],
|
|
28
|
+
* keyFn: (r) => r.code,
|
|
29
|
+
* })
|
|
30
|
+
* // -> [{ id: 'o1', scope_id: 'org-A', code: 'invoice', enabled: true }]
|
|
31
|
+
*/
|
|
32
|
+
export function mergeTwoTier(opts) {
|
|
33
|
+
const { globals, overrides, keyFn, additive } = opts;
|
|
34
|
+
if (additive) {
|
|
35
|
+
return [...globals, ...overrides].filter((r) => r.enabled);
|
|
36
|
+
}
|
|
37
|
+
const overrideByKey = new Map();
|
|
38
|
+
for (const r of overrides)
|
|
39
|
+
overrideByKey.set(keyFn(r), r);
|
|
40
|
+
const result = [];
|
|
41
|
+
const seenKeys = new Set();
|
|
42
|
+
for (const g of globals) {
|
|
43
|
+
const k = keyFn(g);
|
|
44
|
+
seenKeys.add(k);
|
|
45
|
+
const override = overrideByKey.get(k);
|
|
46
|
+
const effective = override ?? g;
|
|
47
|
+
if (effective.enabled)
|
|
48
|
+
result.push(effective);
|
|
49
|
+
}
|
|
50
|
+
for (const o of overrides) {
|
|
51
|
+
const k = keyFn(o);
|
|
52
|
+
if (!seenKeys.has(k) && o.enabled)
|
|
53
|
+
result.push(o);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|