hazo_config 1.4.2 → 2.0.2
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/MIGRATION_V2.md +531 -0
- package/README.md +201 -10
- package/dist/components/app_config.d.ts +1 -1
- package/dist/components/app_config.d.ts.map +1 -1
- package/dist/components/app_config.js +123 -86
- package/dist/components/app_config_list_editor/app_config_list_editor.d.ts +7 -0
- package/dist/components/app_config_list_editor/app_config_list_editor.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/app_config_list_editor.js +128 -0
- package/dist/components/app_config_list_editor/components/color_swatch_picker.d.ts +9 -0
- package/dist/components/app_config_list_editor/components/color_swatch_picker.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/components/color_swatch_picker.js +15 -0
- package/dist/components/app_config_list_editor/components/delete_dialog.d.ts +10 -0
- package/dist/components/app_config_list_editor/components/delete_dialog.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/components/delete_dialog.js +9 -0
- package/dist/components/app_config_list_editor/components/edit_modal.d.ts +19 -0
- package/dist/components/app_config_list_editor/components/edit_modal.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/components/edit_modal.js +97 -0
- package/dist/components/app_config_list_editor/components/empty_state.d.ts +8 -0
- package/dist/components/app_config_list_editor/components/empty_state.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/components/empty_state.js +8 -0
- package/dist/components/app_config_list_editor/components/list_item_row.d.ts +14 -0
- package/dist/components/app_config_list_editor/components/list_item_row.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/components/list_item_row.js +14 -0
- package/dist/components/app_config_list_editor/components/save_status_indicator.d.ts +7 -0
- package/dist/components/app_config_list_editor/components/save_status_indicator.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/components/save_status_indicator.js +25 -0
- package/dist/components/app_config_list_editor/components/search_bar.d.ts +10 -0
- package/dist/components/app_config_list_editor/components/search_bar.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/components/search_bar.js +8 -0
- package/dist/components/app_config_list_editor/index.d.ts +3 -0
- package/dist/components/app_config_list_editor/index.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/index.js +2 -0
- package/dist/components/app_config_list_editor/types.d.ts +93 -0
- package/dist/components/app_config_list_editor/types.d.ts.map +1 -0
- package/dist/components/app_config_list_editor/types.js +14 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/ui/alert-dialog.d.ts +21 -0
- package/dist/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/components/ui/alert-dialog.js +26 -0
- package/dist/components/ui/button.d.ts +12 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +33 -0
- package/dist/components/ui/dialog.d.ts +20 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +22 -0
- package/dist/components/ui/input.d.ts +4 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +8 -0
- package/dist/components/use_app_config.d.ts +3 -4
- package/dist/components/use_app_config.d.ts.map +1 -1
- package/dist/components/use_app_config.js +51 -17
- package/dist/lib/app_config_types.d.ts +19 -17
- package/dist/lib/app_config_types.d.ts.map +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Hazo Config Component Library
|
|
2
2
|
|
|
3
|
-
A React component library for managing configuration with
|
|
3
|
+
A React component library for managing configuration with database-backed storage, supporting both INI files and app configuration. Includes React UI components with JSON editing capabilities.
|
|
4
4
|
|
|
5
5
|
## Tech Stack
|
|
6
6
|
|
|
@@ -11,6 +11,49 @@ A React component library for managing configuration with Storybook for componen
|
|
|
11
11
|
- **Storybook** - Component development and testing
|
|
12
12
|
- **Vite** - Build tool
|
|
13
13
|
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
1. Install the package:
|
|
17
|
+
```bash
|
|
18
|
+
npm install hazo_config
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. Add CSS custom properties to your `globals.css`:
|
|
22
|
+
```css
|
|
23
|
+
@layer base {
|
|
24
|
+
:root {
|
|
25
|
+
--background: 0 0% 100%;
|
|
26
|
+
--foreground: 222.2 84% 4.9%;
|
|
27
|
+
--primary: 222.2 47.4% 11.2%;
|
|
28
|
+
--primary-foreground: 210 40% 98%;
|
|
29
|
+
--muted: 210 40% 96.1%;
|
|
30
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
31
|
+
--accent: 210 40% 96.1%;
|
|
32
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
33
|
+
--border: 214.3 31.8% 91.4%;
|
|
34
|
+
--input: 214.3 31.8% 91.4%;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
3. If using Tailwind v4, add the `@source` directive to `globals.css`:
|
|
40
|
+
```css
|
|
41
|
+
@import "tailwindcss";
|
|
42
|
+
|
|
43
|
+
@source "../node_modules/hazo_config/dist";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
4. Import and use components:
|
|
47
|
+
```typescript
|
|
48
|
+
// Client components
|
|
49
|
+
import { AppConfig, ConfigViewer, MockConfigProvider } from 'hazo_config'
|
|
50
|
+
|
|
51
|
+
// Server-side (API routes, server components)
|
|
52
|
+
import { HazoConfig } from 'hazo_config/server'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
See detailed setup instructions below.
|
|
56
|
+
|
|
14
57
|
## Usage
|
|
15
58
|
|
|
16
59
|
### Installation
|
|
@@ -19,13 +62,61 @@ A React component library for managing configuration with Storybook for componen
|
|
|
19
62
|
npm install hazo_config
|
|
20
63
|
```
|
|
21
64
|
|
|
22
|
-
###
|
|
65
|
+
### Styling Setup (Required)
|
|
66
|
+
|
|
67
|
+
This package uses Tailwind CSS utility classes for styling. You must configure both Tailwind and CSS custom properties.
|
|
68
|
+
|
|
69
|
+
#### CSS Custom Properties (Required for All Versions)
|
|
70
|
+
|
|
71
|
+
Add these CSS custom properties to your `globals.css` or main CSS file. These are used for theming and dark mode support:
|
|
23
72
|
|
|
24
|
-
|
|
73
|
+
```css
|
|
74
|
+
@layer base {
|
|
75
|
+
:root {
|
|
76
|
+
--background: 0 0% 100%;
|
|
77
|
+
--foreground: 222.2 84% 4.9%;
|
|
78
|
+
--primary: 222.2 47.4% 11.2%;
|
|
79
|
+
--primary-foreground: 210 40% 98%;
|
|
80
|
+
--muted: 210 40% 96.1%;
|
|
81
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
82
|
+
--accent: 210 40% 96.1%;
|
|
83
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
84
|
+
--border: 214.3 31.8% 91.4%;
|
|
85
|
+
--input: 214.3 31.8% 91.4%;
|
|
86
|
+
--card: 0 0% 100%;
|
|
87
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.dark {
|
|
91
|
+
--background: 222.2 84% 4.9%;
|
|
92
|
+
--foreground: 210 40% 98%;
|
|
93
|
+
--primary: 210 40% 98%;
|
|
94
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
95
|
+
--muted: 217.2 32.6% 17.5%;
|
|
96
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
97
|
+
--accent: 217.2 32.6% 17.5%;
|
|
98
|
+
--accent-foreground: 210 40% 98%;
|
|
99
|
+
--border: 217.2 32.6% 17.5%;
|
|
100
|
+
--input: 217.2 32.6% 17.5%;
|
|
101
|
+
--card: 222.2 84% 4.9%;
|
|
102
|
+
--card-foreground: 210 40% 98%;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
25
106
|
|
|
26
107
|
#### Tailwind v3 Setup
|
|
27
108
|
|
|
28
|
-
If using Tailwind v3,
|
|
109
|
+
If using Tailwind v3, ensure your `tailwind.config.js` includes:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
module.exports = {
|
|
113
|
+
content: [
|
|
114
|
+
'./app/**/*.{js,ts,jsx,tsx}',
|
|
115
|
+
'./components/**/*.{js,ts,jsx,tsx}',
|
|
116
|
+
],
|
|
117
|
+
// ... rest of config
|
|
118
|
+
}
|
|
119
|
+
```
|
|
29
120
|
|
|
30
121
|
#### Tailwind v4 Setup (Critical)
|
|
31
122
|
|
|
@@ -51,7 +142,8 @@ This package provides separate entry points for server and client code to preven
|
|
|
51
142
|
|
|
52
143
|
```typescript
|
|
53
144
|
// Client code (React components, browser)
|
|
54
|
-
import { ConfigViewer, ConfigEditor, MockConfigProvider } from 'hazo_config'
|
|
145
|
+
import { ConfigViewer, ConfigEditor, AppConfig, MockConfigProvider } from 'hazo_config'
|
|
146
|
+
import type { AppConfigItem, AppConfigContext, ConfigType } from 'hazo_config'
|
|
55
147
|
|
|
56
148
|
// Server code (API routes, Next.js server components)
|
|
57
149
|
import { HazoConfig } from 'hazo_config/server'
|
|
@@ -59,6 +151,17 @@ import { HazoConfig } from 'hazo_config/server'
|
|
|
59
151
|
|
|
60
152
|
**Why?** The `HazoConfig` class uses Node.js `fs` and `path` modules. Importing it in client code causes "Module not found: Can't resolve 'fs'" errors. The `/server` entry point uses the `server-only` package to prevent accidental client imports.
|
|
61
153
|
|
|
154
|
+
### Major Version 2.0 Changes
|
|
155
|
+
|
|
156
|
+
**v2.0.0** introduces breaking changes with database-backed configuration support:
|
|
157
|
+
|
|
158
|
+
- **New Component**: `AppConfig` for database-backed configuration with JSON value support
|
|
159
|
+
- **Schema Change**: Replaces `org_id` with `scope_id` for flexible scoping
|
|
160
|
+
- **JSON Support**: Store structured configuration as JSON with in-UI editing
|
|
161
|
+
- **Type System**: New `ConfigType` ('general' | 'json') for value type management
|
|
162
|
+
|
|
163
|
+
See [MIGRATION_V2.md](./MIGRATION_V2.md) for detailed migration guide from v1.x.
|
|
164
|
+
|
|
62
165
|
### Example: Server-side Config Loading
|
|
63
166
|
|
|
64
167
|
```typescript
|
|
@@ -69,7 +172,7 @@ const config = new HazoConfig({ filePath: './config.ini' })
|
|
|
69
172
|
const dbHost = config.get('database', 'host')
|
|
70
173
|
```
|
|
71
174
|
|
|
72
|
-
### Example: Client-side Config Display
|
|
175
|
+
### Example: Client-side Config Display (INI-based)
|
|
73
176
|
|
|
74
177
|
```typescript
|
|
75
178
|
// components/settings.tsx (React component)
|
|
@@ -85,6 +188,89 @@ export function Settings() {
|
|
|
85
188
|
}
|
|
86
189
|
```
|
|
87
190
|
|
|
191
|
+
### Example: Database-backed App Config (v2.0+)
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// app/settings/page.tsx (Next.js server component)
|
|
195
|
+
'use client'
|
|
196
|
+
|
|
197
|
+
import { AppConfig } from 'hazo_config'
|
|
198
|
+
import type { AppConfigContext, AppConfigItem, ConfigType } from 'hazo_config'
|
|
199
|
+
import { fetch_config, save_config, delete_config } from '@/lib/config_actions'
|
|
200
|
+
|
|
201
|
+
export default function SettingsPage() {
|
|
202
|
+
const context: AppConfigContext = {
|
|
203
|
+
scope_id: 'your-scope-id',
|
|
204
|
+
user_id: 'optional-user-id'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<AppConfig
|
|
209
|
+
title="Application Settings"
|
|
210
|
+
context={context}
|
|
211
|
+
fetch_config={fetch_config}
|
|
212
|
+
save_config={save_config}
|
|
213
|
+
delete_config={delete_config}
|
|
214
|
+
/>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The AppConfig component supports:
|
|
220
|
+
- JSON and text configuration values
|
|
221
|
+
- In-line editing with validation
|
|
222
|
+
- Sensitive field masking (passwords, tokens, keys)
|
|
223
|
+
- Section-based organization
|
|
224
|
+
- Type conversion between general and JSON
|
|
225
|
+
|
|
226
|
+
### Example: Generic List Editor (v2.0.2+)
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { AppConfigListEditor } from 'hazo_config'
|
|
230
|
+
import type { ColumnDef } from 'hazo_config'
|
|
231
|
+
|
|
232
|
+
interface Tag {
|
|
233
|
+
[key: string]: unknown
|
|
234
|
+
tag_id: string
|
|
235
|
+
tag_label: string
|
|
236
|
+
color: string
|
|
237
|
+
description: string
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const TAG_COLUMNS: ColumnDef<Tag>[] = [
|
|
241
|
+
{ field: 'tag_label', label: 'Label', type: 'text', required: true, list_display: 'primary' },
|
|
242
|
+
{ field: 'tag_id', label: 'Tag ID', type: 'text', required: true, list_display: 'badge' },
|
|
243
|
+
{ field: 'color', label: 'Color', type: 'color_swatch', color_options: ['bg-blue-100 text-blue-800', 'bg-green-100 text-green-800'] },
|
|
244
|
+
{ field: 'description', label: 'Description', type: 'textarea', list_display: 'secondary' },
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
export function TagManager({ tags, onTagsChange }) {
|
|
248
|
+
return (
|
|
249
|
+
<AppConfigListEditor<Tag>
|
|
250
|
+
items={tags}
|
|
251
|
+
on_items_change={onTagsChange}
|
|
252
|
+
columns={TAG_COLUMNS}
|
|
253
|
+
id_field="tag_id"
|
|
254
|
+
auto_id_from="tag_label"
|
|
255
|
+
title="Classification Tags"
|
|
256
|
+
description="Manage tags for document categorization."
|
|
257
|
+
enable_search={true}
|
|
258
|
+
delete_confirmation={(tag) => `Delete "${tag.tag_label}"?`}
|
|
259
|
+
/>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The `AppConfigListEditor` is a generic, reusable CRUD list editor for arrays of structured objects. It supports:
|
|
265
|
+
- Dynamic form fields (text, textarea, number, select, color_swatch, toggle)
|
|
266
|
+
- Auto-ID generation from a source field
|
|
267
|
+
- Search/filter for long lists
|
|
268
|
+
- Delete confirmation dialogs
|
|
269
|
+
- Color swatch picker
|
|
270
|
+
- Save status indicators (saving/saved/error)
|
|
271
|
+
- Custom item rendering (indicator, preview, full row override)
|
|
272
|
+
- Field validation with inline error messages
|
|
273
|
+
|
|
88
274
|
## Development
|
|
89
275
|
|
|
90
276
|
### Local Setup
|
|
@@ -125,13 +311,18 @@ npm run build-storybook
|
|
|
125
311
|
hazo_config/
|
|
126
312
|
├── src/
|
|
127
313
|
│ ├── components/ # React components (client-safe)
|
|
128
|
-
│ │ ├── config_viewer.tsx
|
|
129
|
-
│ │ ├── config_editor.tsx
|
|
314
|
+
│ │ ├── config_viewer.tsx # INI config viewer
|
|
315
|
+
│ │ ├── config_editor.tsx # INI config editor
|
|
316
|
+
│ │ ├── app_config.tsx # Database-backed config (v2.0+)
|
|
317
|
+
│ │ ├── app_config_list_editor/ # Generic CRUD list editor (v2.0.2+)
|
|
318
|
+
│ │ ├── ui/ # shadcn/ui primitives (Dialog, Button, etc.)
|
|
319
|
+
│ │ ├── use_app_config.ts # Hook for app config
|
|
130
320
|
│ │ └── *.stories.tsx
|
|
131
321
|
│ ├── lib/ # Core config management
|
|
132
|
-
│ │ ├── config_loader.ts
|
|
322
|
+
│ │ ├── config_loader.ts # HazoConfig class (Node.js)
|
|
133
323
|
│ │ ├── mock_config_provider.ts # Client-safe mock
|
|
134
|
-
│ │
|
|
324
|
+
│ │ ├── types.ts # ConfigProvider interfaces
|
|
325
|
+
│ │ └── app_config_types.ts # AppConfig interfaces (v2.0+)
|
|
135
326
|
│ ├── server/ # Server-only entry point
|
|
136
327
|
│ │ └── index.ts
|
|
137
328
|
│ ├── styles/ # Global styles
|
|
@@ -3,7 +3,7 @@ import type { AppConfigProps } from '../lib/app_config_types.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* AppConfig component
|
|
5
5
|
* Provides interface for viewing and editing database-backed configuration
|
|
6
|
-
* Supports
|
|
6
|
+
* Supports sensitive field masking and both general (text) and json types
|
|
7
7
|
* @param props - Component props
|
|
8
8
|
* @returns React component
|
|
9
9
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app_config.d.ts","sourceRoot":"","sources":["../../src/components/app_config.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAK7D,OAAO,KAAK,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"app_config.d.ts","sourceRoot":"","sources":["../../src/components/app_config.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAK7D,OAAO,KAAK,EAAE,cAAc,EAA6B,MAAM,4BAA4B,CAAA;AA0P3F;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CA6a9C,CAAA"}
|
|
@@ -1,29 +1,99 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
// AppConfig component for managing database-backed configuration
|
|
3
|
-
// Provides UI for viewing and editing
|
|
3
|
+
// Provides UI for viewing and editing configuration stored in hazo_app_config table
|
|
4
4
|
import React, { useState, useCallback, useMemo } from 'react';
|
|
5
5
|
import { cn } from '../lib/utils.js';
|
|
6
|
-
import { RefreshCw, Save, Trash2, Eye, EyeOff, Plus } from 'lucide-react';
|
|
6
|
+
import { RefreshCw, Save, Trash2, Eye, EyeOff, Plus, X } from 'lucide-react';
|
|
7
7
|
import { useAppConfig } from './use_app_config.js';
|
|
8
8
|
import { is_sensitive_field, mask_value, DEFAULT_SENSITIVE_PATTERNS } from './use_config_sections.js';
|
|
9
|
+
const JsonEditor = ({ value, onChange, onValidationChange, placeholder, className }) => {
|
|
10
|
+
const [text_value, set_text_value] = useState(() => JSON.stringify(value, null, 2));
|
|
11
|
+
const [validation_error, set_validation_error] = useState();
|
|
12
|
+
const handle_change = (e) => {
|
|
13
|
+
const new_text = e.target.value;
|
|
14
|
+
set_text_value(new_text);
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(new_text);
|
|
17
|
+
set_validation_error(undefined);
|
|
18
|
+
onChange(parsed);
|
|
19
|
+
onValidationChange?.(true);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const error_msg = err instanceof Error ? err.message : 'Invalid JSON format';
|
|
23
|
+
set_validation_error(error_msg);
|
|
24
|
+
onValidationChange?.(false, error_msg);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx("textarea", { value: text_value, onChange: handle_change, className: cn('w-full px-3 py-2 border rounded-md font-mono text-sm min-h-[120px] focus:outline-none focus:ring-2 focus:ring-primary', validation_error ? 'border-red-500 focus:ring-red-500' : 'border-input', className), placeholder: placeholder }), validation_error && (_jsxs("div", { className: "flex items-start gap-2 text-sm text-red-600 bg-red-50 p-3 rounded-md", children: [_jsx(X, { className: "h-4 w-4 mt-0.5 flex-shrink-0" }), _jsx("span", { children: validation_error })] }))] }));
|
|
28
|
+
};
|
|
29
|
+
const KeyDialog = ({ mode, section_name, on_close, on_save }) => {
|
|
30
|
+
const [new_section, set_new_section] = useState('');
|
|
31
|
+
const [new_key, set_new_key] = useState('');
|
|
32
|
+
const [new_value, set_new_value] = useState('');
|
|
33
|
+
const [new_type, set_new_type] = useState('general');
|
|
34
|
+
const [is_saving, set_is_saving] = useState(false);
|
|
35
|
+
const [is_json_valid, set_is_json_valid] = useState(true);
|
|
36
|
+
const handle_save = async () => {
|
|
37
|
+
const target_section = mode === 'add-section' ? new_section : section_name;
|
|
38
|
+
if (!target_section || !new_key)
|
|
39
|
+
return;
|
|
40
|
+
if (new_type === 'json' && !is_json_valid) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
set_is_saving(true);
|
|
44
|
+
try {
|
|
45
|
+
let value_to_save = new_value;
|
|
46
|
+
if (new_type === 'json') {
|
|
47
|
+
try {
|
|
48
|
+
value_to_save = JSON.parse(new_value);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
value_to_save = {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
await on_save(target_section, new_key, value_to_save, new_type);
|
|
55
|
+
on_close();
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.error('Failed to save:', err);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
set_is_saving(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const can_save = mode === 'add-section'
|
|
65
|
+
? new_section && new_key && new_value && is_json_valid
|
|
66
|
+
: new_key && new_value && is_json_valid;
|
|
67
|
+
if (!mode)
|
|
68
|
+
return null;
|
|
69
|
+
return (_jsx("div", { className: "fixed inset-0 bg-black/50 flex items-center justify-center z-50", children: _jsxs("div", { className: "bg-white rounded-lg shadow-xl max-w-md w-full mx-4", children: [_jsxs("div", { className: "flex items-center justify-between p-6 border-b", children: [_jsx("h3", { className: "text-lg font-semibold", children: mode === 'add-section' ? 'Add New Section' : 'Add New Key' }), _jsx("button", { onClick: on_close, className: "text-muted-foreground hover:text-foreground", type: "button", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsxs("div", { className: "p-6 space-y-4", children: [mode === 'add-section' && (_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Section Name" }), _jsx("input", { type: "text", placeholder: "e.g., integrations, notifications", value: new_section, onChange: (e) => set_new_section(e.target.value), className: "w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary", autoFocus: true })] })), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Key Name" }), _jsx("input", { type: "text", placeholder: "e.g., api_key, webhook_url", value: new_key, onChange: (e) => set_new_key(e.target.value), className: "w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary", autoFocus: mode !== 'add-section' })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Type" }), _jsxs("div", { className: "flex gap-4", children: [_jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [_jsx("input", { type: "radio", name: "type", value: "general", checked: new_type === 'general', onChange: (e) => set_new_type(e.target.value), className: "w-4 h-4 text-primary" }), _jsx("span", { className: "text-sm", children: "General (Text)" })] }), _jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [_jsx("input", { type: "radio", name: "type", value: "json", checked: new_type === 'json', onChange: (e) => set_new_type(e.target.value), className: "w-4 h-4 text-primary" }), _jsx("span", { className: "text-sm", children: "JSON (Object)" })] })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Value" }), new_type === 'json' ? (_jsx(JsonEditor, { value: new_value ? (() => {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(new_value);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
})() : {}, onChange: (val) => set_new_value(JSON.stringify(val, null, 2)), onValidationChange: (isValid) => {
|
|
77
|
+
set_is_json_valid(isValid);
|
|
78
|
+
}, placeholder: '{"key": "value"}' })) : (_jsx("input", { type: "text", placeholder: "Enter value", value: new_value, onChange: (e) => set_new_value(e.target.value), className: "w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary" }))] })] }), _jsxs("div", { className: "flex items-center justify-end gap-3 p-6 border-t bg-muted/30", children: [_jsx("button", { onClick: on_close, className: "px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground", type: "button", children: "Cancel" }), _jsx("button", { onClick: handle_save, disabled: !can_save || is_saving, className: cn('px-4 py-2 text-sm font-medium rounded-md', can_save && !is_saving
|
|
79
|
+
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
|
80
|
+
: 'bg-muted text-muted-foreground cursor-not-allowed'), type: "button", children: is_saving ? 'Saving...' : mode === 'add-section' ? 'Create Section' : 'Add Key' })] })] }) }));
|
|
81
|
+
};
|
|
9
82
|
/**
|
|
10
83
|
* AppConfig component
|
|
11
84
|
* Provides interface for viewing and editing database-backed configuration
|
|
12
|
-
* Supports
|
|
85
|
+
* Supports sensitive field masking and both general (text) and json types
|
|
13
86
|
* @param props - Component props
|
|
14
87
|
* @returns React component
|
|
15
88
|
*/
|
|
16
|
-
export const AppConfig = ({
|
|
17
|
-
const { sections, is_loading, error, reload, set_value, delete_value } = useAppConfig(
|
|
89
|
+
export const AppConfig = ({ context, fetch_config, save_config, delete_config, className, on_update, on_error, sensitive_fields, sensitive_patterns = DEFAULT_SENSITIVE_PATTERNS, disable_auto_mask = false, title = 'Configuration', }) => {
|
|
90
|
+
const { sections, is_loading, error, reload, set_value, delete_value, } = useAppConfig(context, fetch_config, save_config, delete_config);
|
|
18
91
|
const [selected_section, set_selected_section] = useState(null);
|
|
19
|
-
const [new_section, set_new_section] = useState('');
|
|
20
|
-
const [new_key, set_new_key] = useState('');
|
|
21
|
-
const [new_value, set_new_value] = useState('');
|
|
22
92
|
const [editing_fields, set_editing_fields] = useState(new Map());
|
|
23
93
|
const [revealed_fields, set_revealed_fields] = useState(new Set());
|
|
24
94
|
const [has_unsaved_changes, set_has_unsaved_changes] = useState(false);
|
|
25
95
|
const [is_saving, set_is_saving] = useState(false);
|
|
26
|
-
const [
|
|
96
|
+
const [dialog_mode, set_dialog_mode] = useState(null);
|
|
27
97
|
// Report errors to parent
|
|
28
98
|
React.useEffect(() => {
|
|
29
99
|
if (error && on_error) {
|
|
@@ -36,12 +106,6 @@ export const AppConfig = ({ level, context, fetch_config, save_config, delete_co
|
|
|
36
106
|
set_selected_section(Object.keys(sections)[0]);
|
|
37
107
|
}
|
|
38
108
|
}, [sections, selected_section]);
|
|
39
|
-
/**
|
|
40
|
-
* Default title based on level
|
|
41
|
-
*/
|
|
42
|
-
const display_title = useMemo(() => {
|
|
43
|
-
return title ?? (level === 'org' ? 'Organization Settings' : 'User Settings');
|
|
44
|
-
}, [title, level]);
|
|
45
109
|
/**
|
|
46
110
|
* Check if a field should be masked
|
|
47
111
|
*/
|
|
@@ -82,10 +146,13 @@ export const AppConfig = ({ level, context, fetch_config, save_config, delete_co
|
|
|
82
146
|
/**
|
|
83
147
|
* Get the current editing value for a field, or the original value
|
|
84
148
|
*/
|
|
85
|
-
const get_field_value = useCallback((section, key,
|
|
149
|
+
const get_field_value = useCallback((section, key, item) => {
|
|
86
150
|
const field_id = get_field_id(section, key);
|
|
87
151
|
const editing = editing_fields.get(field_id);
|
|
88
|
-
|
|
152
|
+
if (editing) {
|
|
153
|
+
return editing.value;
|
|
154
|
+
}
|
|
155
|
+
return item.config_type === 'json' ? item.config_value_json : item.config_value_text;
|
|
89
156
|
}, [editing_fields, get_field_id]);
|
|
90
157
|
/**
|
|
91
158
|
* Handle field value change (stores in local state, not database)
|
|
@@ -125,7 +192,10 @@ export const AppConfig = ({ level, context, fetch_config, save_config, delete_co
|
|
|
125
192
|
try {
|
|
126
193
|
// Save all pending edits to the database
|
|
127
194
|
for (const edit of editing_fields.values()) {
|
|
128
|
-
|
|
195
|
+
const item = sections[edit.section]?.[edit.key];
|
|
196
|
+
if (item) {
|
|
197
|
+
await set_value(edit.section, edit.key, edit.value, item.config_type);
|
|
198
|
+
}
|
|
129
199
|
}
|
|
130
200
|
set_editing_fields(new Map());
|
|
131
201
|
set_has_unsaved_changes(false);
|
|
@@ -141,11 +211,13 @@ export const AppConfig = ({ level, context, fetch_config, save_config, delete_co
|
|
|
141
211
|
finally {
|
|
142
212
|
set_is_saving(false);
|
|
143
213
|
}
|
|
144
|
-
}, [editing_fields, set_value, on_update, on_error]);
|
|
214
|
+
}, [editing_fields, sections, set_value, on_update, on_error]);
|
|
145
215
|
/**
|
|
146
216
|
* Handle delete key
|
|
147
217
|
*/
|
|
148
218
|
const handle_delete_key = useCallback(async (section, key) => {
|
|
219
|
+
if (!window.confirm(`Delete key "${key}"?`))
|
|
220
|
+
return;
|
|
149
221
|
try {
|
|
150
222
|
await delete_value(section, key);
|
|
151
223
|
// Remove from editing state if present
|
|
@@ -166,63 +238,26 @@ export const AppConfig = ({ level, context, fetch_config, save_config, delete_co
|
|
|
166
238
|
}
|
|
167
239
|
}, [delete_value, get_field_id, on_update, on_error]);
|
|
168
240
|
/**
|
|
169
|
-
*
|
|
241
|
+
* Handle adding a key from dialog
|
|
170
242
|
*/
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
set_new_value('');
|
|
178
|
-
set_new_section('');
|
|
179
|
-
set_show_add_section(false);
|
|
180
|
-
// Select the new section if it was just created
|
|
181
|
-
if (show_add_section) {
|
|
182
|
-
set_selected_section(target_section);
|
|
183
|
-
}
|
|
184
|
-
if (on_update) {
|
|
185
|
-
on_update();
|
|
186
|
-
}
|
|
243
|
+
const handle_dialog_save = useCallback(async (section, key, value, type) => {
|
|
244
|
+
try {
|
|
245
|
+
await set_value(section, key, value, type);
|
|
246
|
+
// Select the section if it was just created
|
|
247
|
+
if (dialog_mode === 'add-section') {
|
|
248
|
+
set_selected_section(section);
|
|
187
249
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
on_error(err instanceof Error ? err : new Error(String(err)));
|
|
191
|
-
}
|
|
250
|
+
if (on_update) {
|
|
251
|
+
on_update();
|
|
192
252
|
}
|
|
193
253
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}, []);
|
|
201
|
-
/**
|
|
202
|
-
* Handle new value input change
|
|
203
|
-
*/
|
|
204
|
-
const handle_new_value_change = useCallback((e) => {
|
|
205
|
-
set_new_value(e.target.value);
|
|
206
|
-
}, []);
|
|
207
|
-
/**
|
|
208
|
-
* Handle new section input change
|
|
209
|
-
*/
|
|
210
|
-
const handle_new_section_change = useCallback((e) => {
|
|
211
|
-
set_new_section(e.target.value);
|
|
212
|
-
}, []);
|
|
213
|
-
/**
|
|
214
|
-
* Handle section selection
|
|
215
|
-
*/
|
|
216
|
-
const handle_section_select = useCallback((section_name) => {
|
|
217
|
-
set_selected_section(section_name);
|
|
218
|
-
}, []);
|
|
219
|
-
/**
|
|
220
|
-
* Toggle add new section mode
|
|
221
|
-
*/
|
|
222
|
-
const toggle_add_section = useCallback(() => {
|
|
223
|
-
set_show_add_section(prev => !prev);
|
|
224
|
-
set_new_section('');
|
|
225
|
-
}, []);
|
|
254
|
+
catch (err) {
|
|
255
|
+
if (on_error) {
|
|
256
|
+
on_error(err instanceof Error ? err : new Error(String(err)));
|
|
257
|
+
}
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
}, [set_value, dialog_mode, on_update, on_error]);
|
|
226
261
|
/**
|
|
227
262
|
* Memoized section list
|
|
228
263
|
*/
|
|
@@ -236,18 +271,20 @@ export const AppConfig = ({ level, context, fetch_config, save_config, delete_co
|
|
|
236
271
|
if (is_loading) {
|
|
237
272
|
return (_jsx("div", { className: cn('cls_app_config flex items-center justify-center p-8', className), children: _jsx("div", { className: "text-muted-foreground", children: "Loading configuration..." }) }));
|
|
238
273
|
}
|
|
239
|
-
return (_jsxs("div", { className: cn('cls_app_config space-y-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
274
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: cn('cls_app_config space-y-6', className), children: [_jsxs("div", { className: "cls_app_config_header flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-bold", children: title }), has_unsaved_changes && (_jsx("p", { className: "text-sm text-amber-600 mt-1", children: "You have unsaved changes" }))] }), _jsxs("div", { className: "flex gap-3", children: [_jsxs("button", { onClick: handle_refresh, className: "px-4 py-2 border border-input rounded-md hover:bg-accent hover:text-accent-foreground flex items-center gap-2 transition-colors", type: "button", disabled: is_saving, children: [_jsx(RefreshCw, { className: "h-4 w-4" }), "Refresh"] }), _jsxs("button", { onClick: handle_save, className: cn('px-4 py-2 rounded-md flex items-center gap-2 transition-colors', has_unsaved_changes
|
|
275
|
+
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
|
276
|
+
: 'bg-muted text-muted-foreground cursor-not-allowed'), type: "button", disabled: !has_unsaved_changes || is_saving, children: [_jsx(Save, { className: "h-4 w-4" }), is_saving ? 'Saving...' : 'Save'] })] })] }), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-4 gap-6", children: [_jsx("div", { className: "lg:col-span-1", children: _jsxs("div", { className: "bg-card border border-border rounded-lg overflow-hidden", children: [_jsx("div", { className: "p-4 border-b bg-muted/30", children: _jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h3", { className: "font-semibold", children: "Sections" }), _jsx("button", { onClick: () => set_dialog_mode('add-section'), className: "text-primary hover:text-primary/80 transition-colors", type: "button", children: _jsx(Plus, { className: "h-5 w-5" }) })] }) }), _jsx("div", { className: "p-2", children: section_list.length === 0 ? (_jsx("div", { className: "text-sm text-muted-foreground text-center py-8 px-4", children: "No sections yet. Click + to add your first section." })) : (_jsx("div", { className: "space-y-1", children: section_list.map((section_name) => (_jsx("button", { onClick: () => set_selected_section(section_name), className: cn('w-full text-left px-3 py-2 rounded-md text-sm font-medium transition-colors', selected_section === section_name
|
|
277
|
+
? 'bg-primary text-primary-foreground'
|
|
278
|
+
: 'hover:bg-muted'), type: "button", children: section_name }, section_name))) })) })] }) }), _jsx("div", { className: "lg:col-span-3", children: selected_section && current_section_data ? (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h3", { className: "text-xl font-semibold", children: selected_section }), _jsxs("button", { onClick: () => set_dialog_mode('add'), className: "px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 flex items-center gap-2 transition-colors", type: "button", children: [_jsx(Plus, { className: "h-4 w-4" }), "Add Key"] })] }), _jsxs("div", { className: "space-y-3", children: [Object.entries(current_section_data).map(([key, item]) => {
|
|
279
|
+
const is_sensitive = should_mask_field(key);
|
|
280
|
+
const show_value = !is_sensitive || is_revealed(selected_section, key);
|
|
281
|
+
const display_value = get_field_value(selected_section, key, item);
|
|
282
|
+
return (_jsxs("div", { className: "bg-card border border-border rounded-lg p-4 hover:shadow-sm transition-shadow", children: [_jsxs("div", { className: "flex items-start justify-between mb-3", children: [_jsx("div", { className: "flex-1", children: _jsxs("div", { className: "flex items-center gap-2 mb-1", children: [_jsx("span", { className: "font-medium", children: key }), _jsx("span", { className: cn('text-xs px-2 py-0.5 rounded-full font-medium', item.config_type === 'json'
|
|
283
|
+
? 'bg-purple-100 text-purple-700'
|
|
284
|
+
: 'bg-gray-100 text-gray-700'), children: item.config_type })] }) }), _jsxs("div", { className: "flex items-center gap-2", children: [is_sensitive && (_jsx("button", { onClick: () => toggle_reveal(selected_section, key), className: "text-muted-foreground hover:text-foreground transition-colors", type: "button", children: show_value ? _jsx(EyeOff, { className: "h-4 w-4" }) : _jsx(Eye, { className: "h-4 w-4" }) })), _jsx("button", { onClick: () => handle_delete_key(selected_section, key), className: "text-red-500 hover:text-red-700 transition-colors", type: "button", children: _jsx(Trash2, { className: "h-4 w-4" }) })] })] }), item.config_type === 'json' ? (_jsx(JsonEditor, { value: typeof display_value === 'object' ? display_value : {}, onChange: (val) => handle_field_change(selected_section, key, val), className: "bg-muted/30" })) : (_jsx("input", { type: is_sensitive && !show_value ? 'password' : 'text', value: is_sensitive && !show_value
|
|
285
|
+
? mask_value(String(display_value))
|
|
286
|
+
: String(display_value), onChange: (e) => handle_field_change(selected_section, key, e.target.value), className: "w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary", readOnly: is_sensitive && !show_value }))] }, key));
|
|
287
|
+
}), Object.keys(current_section_data).length === 0 && (_jsx("div", { className: "text-center py-12 text-muted-foreground", children: "No keys in this section yet. Click \"Add Key\" to get started." }))] })] })) : (_jsx("div", { className: "flex items-center justify-center h-64 bg-muted/30 border border-dashed border-border rounded-lg", children: _jsx("div", { className: "text-center text-muted-foreground", children: section_list.length > 0
|
|
288
|
+
? 'Select a section to view and edit keys'
|
|
289
|
+
: 'Click + to create your first configuration section' }) })) })] })] }), _jsx(KeyDialog, { mode: dialog_mode, section_name: selected_section || undefined, on_close: () => set_dialog_mode(null), on_save: handle_dialog_save })] }));
|
|
253
290
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AppConfigListEditorProps } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* AppConfigListEditor - A polished CRUD list editor for config item arrays.
|
|
4
|
+
* Purely callback-based: no API calls, no database access.
|
|
5
|
+
*/
|
|
6
|
+
export declare function AppConfigListEditor<T extends Record<string, unknown>>({ items, on_items_change, columns, id_field, auto_id_from, title, description, enable_search, search_threshold, render_item, render_item_indicator, render_preview, delete_confirmation, max_items, id_editable_after_create, className, save_status, }: AppConfigListEditorProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=app_config_list_editor.d.ts.map
|
|
@@ -0,0 +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;AAQ7F;;;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,2CAsP7B"}
|