hazo_config 2.1.0 → 2.1.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/CHANGE_LOG.md +47 -0
- package/README.md +55 -0
- package/SETUP_CHECKLIST.md +175 -0
- package/dist/components/app_config_list_editor/app_config_list_editor.d.ts +1 -1
- 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 +5 -2
- package/dist/components/app_config_list_editor/components/edit_modal.d.ts +3 -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 +40 -4
- package/dist/components/app_config_list_editor/types.d.ts +11 -1
- 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.map +1 -1
- package/dist/components/app_config_list_editor/utils/json_import_export.js +3 -0
- package/package.json +10 -4
package/CHANGE_LOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.1.1
|
|
4
|
+
- chore: add `engines` field (`node >= 18.0.0`) and `prepublishOnly` script
|
|
5
|
+
- chore: widen React peer deps to support React 18 and 19
|
|
6
|
+
- chore: add CHANGE_LOG.md, SETUP_CHECKLIST.md, AGENTS.md, config/ sample, design/ architecture
|
|
7
|
+
- chore: clean up test-app duplicate transitive dependencies
|
|
8
|
+
- chore: update .npmignore for new dev files
|
|
9
|
+
|
|
10
|
+
## 2.1.0
|
|
11
|
+
- feat: add JSON import/export to AppConfigListEditor
|
|
12
|
+
- Export utility functions: `validate_import_data`, `merge_items`, `export_items_to_json`, `generate_export_filename`
|
|
13
|
+
|
|
14
|
+
## 2.0.2
|
|
15
|
+
- feat: add AppConfigListEditor component with shadcn/ui
|
|
16
|
+
- Generic CRUD list editor for arrays of structured objects (tags, document types, etc.)
|
|
17
|
+
- Supports field types: text, textarea, color_swatch, select, number, toggle
|
|
18
|
+
|
|
19
|
+
## 2.0.1
|
|
20
|
+
- chore: add test-app build artifacts to .gitignore
|
|
21
|
+
|
|
22
|
+
## 2.0.0
|
|
23
|
+
- **BREAKING**: Renamed `org_id` to `scope_id` in `AppConfigItem` and `AppConfigContext`
|
|
24
|
+
- **BREAKING**: Split `config_value` into `config_value_text` and `config_value_json`
|
|
25
|
+
- **BREAKING**: Added `config_type` field (`'general'` | `'json'`)
|
|
26
|
+
- feat: add database-backed AppConfig component with JSON editing support
|
|
27
|
+
- feat: add `useAppConfig` hook for async database operations
|
|
28
|
+
- See `MIGRATION_V2.md` for upgrade guide
|
|
29
|
+
|
|
30
|
+
## 1.4.2
|
|
31
|
+
- fix: separate server/client exports to prevent Next.js bundling errors
|
|
32
|
+
- Added `hazo_config/server` entry point with `server-only` guard
|
|
33
|
+
|
|
34
|
+
## 1.4.1
|
|
35
|
+
- fix: update JS output issues
|
|
36
|
+
|
|
37
|
+
## 1.4.0
|
|
38
|
+
- fix: add missing files to package.json
|
|
39
|
+
- fix: ES module exports with explicit .js extensions
|
|
40
|
+
- Added package development rules
|
|
41
|
+
|
|
42
|
+
## 1.0.0
|
|
43
|
+
- Initial release
|
|
44
|
+
- HazoConfig class with INI file support, TTL-based caching
|
|
45
|
+
- ConfigViewer and ConfigEditor React components
|
|
46
|
+
- MockConfigProvider for testing
|
|
47
|
+
- Sensitive field masking (auto-detection of passwords, tokens, keys)
|
package/README.md
CHANGED
|
@@ -223,6 +223,61 @@ The AppConfig component supports:
|
|
|
223
223
|
- Section-based organization
|
|
224
224
|
- Type conversion between general and JSON
|
|
225
225
|
|
|
226
|
+
#### Required Database Schema
|
|
227
|
+
|
|
228
|
+
`AppConfig` and the `useAppConfig` hook expect a single table named `hazo_app_config`. Create it before mounting the component. The file-based `HazoConfig` class does **not** need any database tables — this only applies to the database-backed `AppConfig` flow.
|
|
229
|
+
|
|
230
|
+
**PostgreSQL**
|
|
231
|
+
```sql
|
|
232
|
+
CREATE TABLE IF NOT EXISTS hazo_app_config (
|
|
233
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
234
|
+
scope_id UUID,
|
|
235
|
+
user_id UUID,
|
|
236
|
+
config_section TEXT NOT NULL,
|
|
237
|
+
config_name TEXT NOT NULL,
|
|
238
|
+
config_value_text TEXT,
|
|
239
|
+
config_value_json JSONB,
|
|
240
|
+
config_type TEXT NOT NULL,
|
|
241
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
242
|
+
changed_at TIMESTAMPTZ
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_app_config_scope ON hazo_app_config(scope_id);
|
|
246
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_app_config_user ON hazo_app_config(user_id);
|
|
247
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_app_config_unique
|
|
248
|
+
ON hazo_app_config(scope_id, user_id, config_section, config_name);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**SQLite**
|
|
252
|
+
```sql
|
|
253
|
+
CREATE TABLE IF NOT EXISTS hazo_app_config (
|
|
254
|
+
id TEXT PRIMARY KEY,
|
|
255
|
+
scope_id TEXT,
|
|
256
|
+
user_id TEXT,
|
|
257
|
+
config_section TEXT NOT NULL,
|
|
258
|
+
config_name TEXT NOT NULL,
|
|
259
|
+
config_value_text TEXT,
|
|
260
|
+
config_value_json TEXT,
|
|
261
|
+
config_type TEXT NOT NULL,
|
|
262
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
263
|
+
changed_at TEXT
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_app_config_scope ON hazo_app_config(scope_id);
|
|
267
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_app_config_user ON hazo_app_config(user_id);
|
|
268
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_app_config_unique
|
|
269
|
+
ON hazo_app_config(scope_id, user_id, config_section, config_name);
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Column meanings:
|
|
273
|
+
- `scope_id` — org/tenant id. `NULL` for global rows.
|
|
274
|
+
- `user_id` — per-user override. `NULL` for scope-level rows.
|
|
275
|
+
- `config_section` / `config_name` — the section + key as shown in the UI.
|
|
276
|
+
- `config_type` — `'general'` (use `config_value_text`) or `'json'` (use `config_value_json`).
|
|
277
|
+
- The unique index `(scope_id, user_id, config_section, config_name)` guarantees one row per scope/user/section/name. On PostgreSQL 15+, append `NULLS NOT DISTINCT` if you want `NULL` scope/user values to count as equal for uniqueness.
|
|
278
|
+
|
|
279
|
+
See [SETUP_CHECKLIST.md](./SETUP_CHECKLIST.md#6-database-config-if-using-appconfig) for full details, including a SQLite UUID-default snippet.
|
|
280
|
+
|
|
226
281
|
### Example: Generic List Editor (v2.0.2+)
|
|
227
282
|
|
|
228
283
|
```typescript
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Setup Checklist
|
|
2
|
+
|
|
3
|
+
Step-by-step setup for consuming apps using hazo_config.
|
|
4
|
+
|
|
5
|
+
## 1. Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install hazo_config
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 2. Tailwind CSS Setup
|
|
12
|
+
|
|
13
|
+
### Tailwind v4
|
|
14
|
+
Add `@source` directive in your globals.css to ensure Tailwind compiles classes from the package:
|
|
15
|
+
|
|
16
|
+
```css
|
|
17
|
+
@import "tailwindcss";
|
|
18
|
+
@source "../node_modules/hazo_config/dist";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Tailwind v3
|
|
22
|
+
Add the dist directory to your `tailwind.config.js` content array:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
module.exports = {
|
|
26
|
+
content: [
|
|
27
|
+
'./src/**/*.{js,ts,jsx,tsx}',
|
|
28
|
+
'./node_modules/hazo_config/dist/**/*.{js,ts,jsx,tsx}',
|
|
29
|
+
],
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 3. CSS Custom Properties (Optional)
|
|
34
|
+
|
|
35
|
+
Add these CSS variables if you want themed styling:
|
|
36
|
+
|
|
37
|
+
```css
|
|
38
|
+
:root {
|
|
39
|
+
--hazo-config-bg: #ffffff;
|
|
40
|
+
--hazo-config-border: #e2e8f0;
|
|
41
|
+
--hazo-config-text: #1a202c;
|
|
42
|
+
--hazo-config-muted: #718096;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 4. Import Components
|
|
47
|
+
|
|
48
|
+
### Client-side (components, browser)
|
|
49
|
+
```typescript
|
|
50
|
+
import { ConfigViewer, ConfigEditor, AppConfig, MockConfigProvider } from 'hazo_config'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Server-side (API routes, server components)
|
|
54
|
+
```typescript
|
|
55
|
+
import { HazoConfig } from 'hazo_config/server'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 5. INI File Config (if using HazoConfig)
|
|
59
|
+
|
|
60
|
+
Create your config file:
|
|
61
|
+
```ini
|
|
62
|
+
[database]
|
|
63
|
+
host = localhost
|
|
64
|
+
port = 5432
|
|
65
|
+
|
|
66
|
+
[app]
|
|
67
|
+
name = My Application
|
|
68
|
+
debug = false
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Initialize in server code:
|
|
72
|
+
```typescript
|
|
73
|
+
import { HazoConfig } from 'hazo_config/server'
|
|
74
|
+
|
|
75
|
+
const config = new HazoConfig({
|
|
76
|
+
filePath: './config/app_config.ini',
|
|
77
|
+
create_if_missing: true,
|
|
78
|
+
cache_ttl_ms: 5000,
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 6. Database Config (if using AppConfig)
|
|
83
|
+
|
|
84
|
+
The `AppConfig` component reads/writes from a single table named `hazo_app_config`. Create it (and its indexes) in your database before mounting the component.
|
|
85
|
+
|
|
86
|
+
### Columns
|
|
87
|
+
|
|
88
|
+
| Column | Type / SQLite | Type / PostgreSQL | Notes |
|
|
89
|
+
|---------------------|-----------------------------------|-----------------------------------------|--------------------------------------------------------|
|
|
90
|
+
| `id` | `TEXT PRIMARY KEY` | `UUID PRIMARY KEY DEFAULT gen_random_uuid()` | Row id. SQLite generates a UUID-like string. |
|
|
91
|
+
| `scope_id` | `TEXT` | `UUID` | Org/tenant scope. Nullable for global rows. |
|
|
92
|
+
| `user_id` | `TEXT` | `UUID` | Per-user override. Nullable for scope-level rows. |
|
|
93
|
+
| `config_section` | `TEXT NOT NULL` | `TEXT NOT NULL` | Section grouping (e.g. `database`, `app`). |
|
|
94
|
+
| `config_name` | `TEXT NOT NULL` | `TEXT NOT NULL` | Key within the section. |
|
|
95
|
+
| `config_value_text` | `TEXT` | `TEXT` | Value when `config_type = 'general'`. |
|
|
96
|
+
| `config_value_json` | `TEXT` | `JSONB` | Value when `config_type = 'json'`. |
|
|
97
|
+
| `config_type` | `TEXT NOT NULL` | `TEXT NOT NULL` | One of `'general'` or `'json'`. |
|
|
98
|
+
| `created_at` | `TEXT DEFAULT (datetime('now'))` | `TIMESTAMPTZ DEFAULT NOW()` | Insert timestamp. |
|
|
99
|
+
| `changed_at` | `TEXT` | `TIMESTAMPTZ` | Last-update timestamp (set by the app on update). |
|
|
100
|
+
|
|
101
|
+
### PostgreSQL
|
|
102
|
+
```sql
|
|
103
|
+
CREATE TABLE IF NOT EXISTS hazo_app_config (
|
|
104
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
105
|
+
scope_id UUID,
|
|
106
|
+
user_id UUID,
|
|
107
|
+
config_section TEXT NOT NULL,
|
|
108
|
+
config_name TEXT NOT NULL,
|
|
109
|
+
config_value_text TEXT,
|
|
110
|
+
config_value_json JSONB,
|
|
111
|
+
config_type TEXT NOT NULL,
|
|
112
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
113
|
+
changed_at TIMESTAMPTZ
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_app_config_scope ON hazo_app_config(scope_id);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_app_config_user ON hazo_app_config(user_id);
|
|
118
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_app_config_unique
|
|
119
|
+
ON hazo_app_config(scope_id, user_id, config_section, config_name);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
> **PostgreSQL NULL note:** by default PostgreSQL treats `NULL` values in a unique index as distinct, so two rows where `user_id IS NULL` (or `scope_id IS NULL`) for the same section/name will *not* conflict. On PostgreSQL 15+ you can add `NULLS NOT DISTINCT` to the unique index if you want the uniqueness check to treat `NULL` as equal.
|
|
123
|
+
|
|
124
|
+
### SQLite
|
|
125
|
+
```sql
|
|
126
|
+
CREATE TABLE IF NOT EXISTS hazo_app_config (
|
|
127
|
+
id TEXT PRIMARY KEY,
|
|
128
|
+
scope_id TEXT,
|
|
129
|
+
user_id TEXT,
|
|
130
|
+
config_section TEXT NOT NULL,
|
|
131
|
+
config_name TEXT NOT NULL,
|
|
132
|
+
config_value_text TEXT,
|
|
133
|
+
config_value_json TEXT,
|
|
134
|
+
config_type TEXT NOT NULL,
|
|
135
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
136
|
+
changed_at TEXT
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_app_config_scope ON hazo_app_config(scope_id);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_hazo_app_config_user ON hazo_app_config(user_id);
|
|
141
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_app_config_unique
|
|
142
|
+
ON hazo_app_config(scope_id, user_id, config_section, config_name);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> **SQLite id generation:** SQLite has no built-in UUID generator. Either generate the `id` in application code before insert, or define the column with a UUID-like default such as the one used in this package's test-app:
|
|
146
|
+
> ```sql
|
|
147
|
+
> id TEXT PRIMARY KEY DEFAULT (
|
|
148
|
+
> lower(hex(randomblob(4))) || '-' ||
|
|
149
|
+
> lower(hex(randomblob(2))) || '-4' ||
|
|
150
|
+
> substr(lower(hex(randomblob(2))), 2) || '-' ||
|
|
151
|
+
> substr('89ab', abs(random()) % 4 + 1, 1) ||
|
|
152
|
+
> substr(lower(hex(randomblob(2))), 2) || '-' ||
|
|
153
|
+
> lower(hex(randomblob(6)))
|
|
154
|
+
> )
|
|
155
|
+
> ```
|
|
156
|
+
|
|
157
|
+
### Note: `INI`-based `HazoConfig` does not need any tables
|
|
158
|
+
|
|
159
|
+
The `hazo_app_config` table is **only** required if you use the database-backed `AppConfig` component or the `useAppConfig` hook. The file-based `HazoConfig` class (Section 5) reads and writes an INI file directly and does not touch the database.
|
|
160
|
+
|
|
161
|
+
## 7. Next.js Configuration
|
|
162
|
+
|
|
163
|
+
Add to `next.config.js`:
|
|
164
|
+
```javascript
|
|
165
|
+
const nextConfig = {
|
|
166
|
+
transpilePackages: ['hazo_config'],
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## 8. Verify
|
|
171
|
+
|
|
172
|
+
- [ ] `import { ConfigViewer } from 'hazo_config'` resolves without errors
|
|
173
|
+
- [ ] `import { HazoConfig } from 'hazo_config/server'` works in server code
|
|
174
|
+
- [ ] Tailwind classes from hazo_config render correctly (check backgrounds, borders)
|
|
175
|
+
- [ ] Sensitive fields are masked in ConfigViewer/ConfigEditor
|
|
@@ -3,5 +3,5 @@ import type { AppConfigListEditorProps } from './types.js';
|
|
|
3
3
|
* AppConfigListEditor - A polished CRUD list editor for config item arrays.
|
|
4
4
|
* Purely callback-based: no API calls, no database access.
|
|
5
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;
|
|
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, edit_modal_max_width, }: AppConfigListEditorProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
7
7
|
//# sourceMappingURL=app_config_list_editor.d.ts.map
|
|
@@ -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;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,
|
|
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,EACpB,oBAAoB,GACrB,EAAE,wBAAwB,CAAC,CAAC,CAAC,2CAoQ7B"}
|
|
@@ -15,7 +15,7 @@ import { ImportExportButtons } from './components/import_export_buttons.js';
|
|
|
15
15
|
* AppConfigListEditor - A polished CRUD list editor for config item arrays.
|
|
16
16
|
* Purely callback-based: no API calls, no database access.
|
|
17
17
|
*/
|
|
18
|
-
export function AppConfigListEditor({ items, on_items_change, columns, id_field, auto_id_from, title, description, enable_search = false, search_threshold = 8, render_item, render_item_indicator, render_preview, delete_confirmation, max_items, id_editable_after_create = false, className, save_status = 'idle', }) {
|
|
18
|
+
export function AppConfigListEditor({ items, on_items_change, columns, id_field, auto_id_from, title, description, enable_search = false, search_threshold = 8, render_item, render_item_indicator, render_preview, delete_confirmation, max_items, id_editable_after_create = false, className, save_status = 'idle', edit_modal_max_width, }) {
|
|
19
19
|
const [search_term, set_search_term] = useState('');
|
|
20
20
|
const [edit_state, set_edit_state] = useState(null);
|
|
21
21
|
const [delete_state, set_delete_state] = useState(null);
|
|
@@ -57,6 +57,9 @@ export function AppConfigListEditor({ items, on_items_change, columns, id_field,
|
|
|
57
57
|
if (col.type === 'toggle') {
|
|
58
58
|
empty_item[col.field] = false;
|
|
59
59
|
}
|
|
60
|
+
if (col.type === 'tag_picker') {
|
|
61
|
+
empty_item[col.field] = [];
|
|
62
|
+
}
|
|
60
63
|
}
|
|
61
64
|
set_edit_state({
|
|
62
65
|
mode: 'create',
|
|
@@ -125,5 +128,5 @@ export function AppConfigListEditor({ items, on_items_change, columns, id_field,
|
|
|
125
128
|
}, [delete_state, columns, id_field]);
|
|
126
129
|
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
130
|
? '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 }))] }));
|
|
131
|
+
: '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, max_width_class: edit_modal_max_width })), delete_state && (_jsx(DeleteDialog, { open: true, item_name: delete_item_name, message: delete_message, on_confirm: handle_delete_confirm, on_cancel: handle_cancel_delete }))] }));
|
|
129
132
|
}
|
|
@@ -13,7 +13,9 @@ interface EditModalProps<T extends Record<string, unknown>> {
|
|
|
13
13
|
render_preview?: (item: T) => React.ReactNode;
|
|
14
14
|
on_save: (item: T) => void;
|
|
15
15
|
on_cancel: () => void;
|
|
16
|
+
/** Tailwind max-width class(es). Default: `sm:max-w-2xl`. */
|
|
17
|
+
max_width_class?: string;
|
|
16
18
|
}
|
|
17
|
-
export declare function EditModal<T extends Record<string, unknown>>({ open, mode, item: initial_item, columns, id_field, auto_id_from, id_editable_after_create, existing_ids, item_type_label, render_preview, on_save, on_cancel, }: EditModalProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export declare function EditModal<T extends Record<string, unknown>>({ open, mode, item: initial_item, columns, id_field, auto_id_from, id_editable_after_create, existing_ids, item_type_label, render_preview, on_save, on_cancel, max_width_class, }: EditModalProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
18
20
|
export {};
|
|
19
21
|
//# sourceMappingURL=edit_modal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit_modal.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/edit_modal.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA2C,MAAM,OAAO,CAAA;AAY/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAG5C,UAAU,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACxD,IAAI,EAAE,OAAO,CAAA;IACb,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IAChB,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAA;IAClC,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,eAAe,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,KAAK,CAAC,SAAS,CAAA;IAC7C,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAA;IAC1B,SAAS,EAAE,MAAM,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"edit_modal.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/edit_modal.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA2C,MAAM,OAAO,CAAA;AAY/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAG5C,UAAU,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACxD,IAAI,EAAE,OAAO,CAAA;IACb,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IAChB,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAA;IAClC,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,eAAe,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,KAAK,CAAC,SAAS,CAAA;IAC7C,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAA;IAC1B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC3D,IAAI,EACJ,IAAI,EACJ,IAAI,EAAE,YAAY,EAClB,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,wBAAwB,EACxB,YAAY,EACZ,eAAe,EACf,cAAc,EACd,OAAO,EACP,SAAS,EACT,eAAgC,GACjC,EAAE,cAAc,CAAC,CAAC,CAAC,2CAqRnB"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Edit modal component
|
|
3
3
|
// Centered dialog for creating/editing items with dynamic form fields
|
|
4
|
-
import { useState, useCallback, useEffect } from 'react';
|
|
4
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
5
5
|
import { cn } from '../../../lib/utils.js';
|
|
6
6
|
import { Input } from '../../ui/input.js';
|
|
7
7
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '../../ui/dialog.js';
|
|
8
8
|
import { Button } from '../../ui/button.js';
|
|
9
9
|
import { ColorSwatchPicker } from './color_swatch_picker.js';
|
|
10
10
|
import { slugify } from '../types.js';
|
|
11
|
-
export function EditModal({ open, mode, item: initial_item, columns, id_field, auto_id_from, id_editable_after_create, existing_ids, item_type_label, render_preview, on_save, on_cancel, }) {
|
|
11
|
+
export function EditModal({ open, mode, item: initial_item, columns, id_field, auto_id_from, id_editable_after_create, existing_ids, item_type_label, render_preview, on_save, on_cancel, max_width_class = 'sm:max-w-2xl', }) {
|
|
12
12
|
const [form_data, set_form_data] = useState({});
|
|
13
13
|
const [errors, set_errors] = useState(new Map());
|
|
14
14
|
const [user_touched_id, set_user_touched_id] = useState(false);
|
|
@@ -88,10 +88,46 @@ export function EditModal({ open, mode, item: initial_item, columns, id_field, a
|
|
|
88
88
|
const error = errors.get(col.field);
|
|
89
89
|
const is_id_field = col.field === id_field;
|
|
90
90
|
const is_disabled = is_id_field && mode === 'edit' && !id_editable_after_create;
|
|
91
|
+
// Tag picker fields — render with dedicated component
|
|
92
|
+
if (col.type === 'tag_picker') {
|
|
93
|
+
return (_jsxs("div", { className: "cls_edit_field space-y-1.5", children: [_jsxs("label", { className: "text-sm font-medium text-gray-700", children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-0.5", children: "*" })] }), _jsx(TagPickerField, { value: Array.isArray(value) ? value : (typeof value === 'string' && value ? value.split(',').map(s => s.trim()).filter(Boolean) : []), options: col.tag_options ?? [], on_change: (tags) => update_field(col.field, tags) }), error && _jsx("p", { className: "text-xs text-red-600", children: error })] }, col.field));
|
|
94
|
+
}
|
|
95
|
+
// Toggle fields render as horizontal row: label left, switch right
|
|
96
|
+
if (col.type === 'toggle') {
|
|
97
|
+
return (_jsxs("div", { className: "cls_edit_field flex items-center justify-between py-2 border-b border-gray-100 last:border-b-0", children: [_jsx("label", { className: "text-sm font-medium text-gray-700", children: col.label }), _jsx("button", { type: "button", role: "switch", "aria-checked": Boolean(value), onClick: () => update_field(col.field, !value), className: cn('relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors', value ? 'bg-primary' : 'bg-gray-200'), children: _jsx("span", { className: cn('pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition-transform', value ? 'translate-x-4' : 'translate-x-0') }) })] }, col.field));
|
|
98
|
+
}
|
|
91
99
|
return (_jsxs("div", { className: "cls_edit_field space-y-1.5", children: [_jsxs("label", { className: "text-sm font-medium text-gray-700", children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-0.5", children: "*" })] }), col.type === 'text' && (_jsx(Input, { type: "text", value: String(value ?? ''), onChange: (e) => is_id_field
|
|
92
100
|
? handle_id_change(e.target.value)
|
|
93
|
-
: update_field(col.field, e.target.value), placeholder: col.placeholder, disabled: is_disabled, className: cn('rounded-lg focus-visible:ring-violet-500/20 focus-visible:ring-offset-0', is_disabled && 'bg-gray-50 text-gray-500', error && 'border-red-400 focus-visible:ring-red-500/20') })), col.type === 'number' && (_jsx(Input, { type: "number", value: value !== undefined && value !== null ? String(value) : '', onChange: (e) => update_field(col.field, e.target.value === '' ? '' : Number(e.target.value)), placeholder: col.placeholder, className: cn('rounded-lg focus-visible:ring-violet-500/20 focus-visible:ring-offset-0', error && 'border-red-400 focus-visible:ring-red-500/20') })), col.type === 'textarea' && (_jsx("textarea", { value: String(value ?? ''), onChange: (e) => update_field(col.field, e.target.value), placeholder: col.placeholder, rows: 3, className: cn('flex w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/20 focus-visible:border-violet-300 disabled:cursor-not-allowed disabled:opacity-50', error && 'border-red-400 focus-visible:ring-red-500/20') })), col.type === 'select' && col.options && (_jsxs("select", { value: String(value ?? ''), onChange: (e) => update_field(col.field, e.target.value), className: cn('flex h-10 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/20 focus-visible:border-violet-300', error && 'border-red-400 focus-visible:ring-red-500/20'), children: [_jsx("option", { value: "", children: col.placeholder || 'Select...' }), col.options.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value)))] })), col.type === 'color_swatch' && col.color_options && (_jsx(ColorSwatchPicker, { value: String(value ?? ''), options: col.color_options, on_change: (color) => update_field(col.field, color) })),
|
|
101
|
+
: update_field(col.field, e.target.value), placeholder: col.placeholder, disabled: is_disabled, className: cn('rounded-lg focus-visible:ring-violet-500/20 focus-visible:ring-offset-0', is_disabled && 'bg-gray-50 text-gray-500', error && 'border-red-400 focus-visible:ring-red-500/20') })), col.type === 'number' && (_jsx(Input, { type: "number", value: value !== undefined && value !== null ? String(value) : '', onChange: (e) => update_field(col.field, e.target.value === '' ? '' : Number(e.target.value)), placeholder: col.placeholder, className: cn('rounded-lg focus-visible:ring-violet-500/20 focus-visible:ring-offset-0', error && 'border-red-400 focus-visible:ring-red-500/20') })), col.type === 'textarea' && (_jsx("textarea", { value: String(value ?? ''), onChange: (e) => update_field(col.field, e.target.value), placeholder: col.placeholder, rows: 3, className: cn('flex w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/20 focus-visible:border-violet-300 disabled:cursor-not-allowed disabled:opacity-50', error && 'border-red-400 focus-visible:ring-red-500/20') })), col.type === 'select' && col.options && (_jsxs("select", { value: String(value ?? ''), onChange: (e) => update_field(col.field, e.target.value), className: cn('flex h-10 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/20 focus-visible:border-violet-300', error && 'border-red-400 focus-visible:ring-red-500/20'), children: [_jsx("option", { value: "", children: col.placeholder || 'Select...' }), col.options.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value)))] })), col.type === 'color_swatch' && col.color_options && (_jsx(ColorSwatchPicker, { value: String(value ?? ''), options: col.color_options, on_change: (color) => update_field(col.field, color) })), is_id_field && auto_id_from && mode === 'create' && !user_touched_id && (_jsxs("p", { className: "text-xs text-gray-400", children: ["Auto-generated from ", columns.find(c => c.field === auto_id_from)?.label?.toLowerCase() || auto_id_from] })), error && (_jsx("p", { className: "text-xs text-red-600", children: error }))] }, col.field));
|
|
94
102
|
};
|
|
95
103
|
return (_jsx(Dialog, { open: open, onOpenChange: (is_open) => { if (!is_open)
|
|
96
|
-
on_cancel(); }, children: _jsxs(DialogContent, { className:
|
|
104
|
+
on_cancel(); }, children: _jsxs(DialogContent, { className: cn('cls_edit_modal w-[95vw] rounded-2xl p-0 gap-0 overflow-hidden max-h-[90vh] flex flex-col', max_width_class), children: [_jsx(DialogHeader, { className: "px-6 pt-6 pb-4 shrink-0", children: _jsx(DialogTitle, { children: mode === 'create' ? `New ${item_type_label}` : `Edit ${item_type_label}` }) }), _jsxs("div", { className: "px-6 pb-4 space-y-4 overflow-y-auto flex-1 min-h-0", children: [render_preview && Object.keys(form_data).length > 0 && (_jsx("div", { className: "p-3 bg-gray-50 rounded-lg border border-gray-100", children: render_preview(form_data) })), columns.map((col) => render_field(col))] }), _jsxs(DialogFooter, { className: "bg-gray-50 px-6 py-4 border-t border-gray-100 sm:justify-between shrink-0", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: on_cancel, className: "rounded-full", children: "Cancel" }), _jsx(Button, { type: "button", onClick: handle_save, className: "rounded-full bg-violet-600 hover:bg-violet-700 text-white", children: mode === 'create' ? `Add ${item_type_label}` : 'Save Changes' })] })] }) }));
|
|
105
|
+
}
|
|
106
|
+
/* ── Tag Picker Field ── */
|
|
107
|
+
function TagPickerField({ value, options, on_change }) {
|
|
108
|
+
const [search, set_search] = React.useState('');
|
|
109
|
+
const [open, set_open] = React.useState(false);
|
|
110
|
+
const ref = React.useRef(null);
|
|
111
|
+
// Close on outside click
|
|
112
|
+
React.useEffect(() => {
|
|
113
|
+
if (!open)
|
|
114
|
+
return;
|
|
115
|
+
const handler = (e) => {
|
|
116
|
+
if (ref.current && !ref.current.contains(e.target))
|
|
117
|
+
set_open(false);
|
|
118
|
+
};
|
|
119
|
+
document.addEventListener('mousedown', handler);
|
|
120
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
121
|
+
}, [open]);
|
|
122
|
+
const selected = new Set(value);
|
|
123
|
+
const filtered = options.filter(o => !selected.has(o.value) && o.label.toLowerCase().includes(search.toLowerCase()));
|
|
124
|
+
const add_tag = (tag_value) => {
|
|
125
|
+
on_change([...value, tag_value]);
|
|
126
|
+
set_search('');
|
|
127
|
+
};
|
|
128
|
+
const remove_tag = (tag_value) => {
|
|
129
|
+
on_change(value.filter(v => v !== tag_value));
|
|
130
|
+
};
|
|
131
|
+
const label_for = (v) => options.find(o => o.value === v)?.label ?? v;
|
|
132
|
+
return (_jsxs("div", { ref: ref, className: "relative", children: [_jsxs("div", { className: cn('flex flex-wrap gap-1.5 min-h-[38px] p-1.5 border rounded-lg bg-white cursor-text', open && 'ring-2 ring-violet-500/20 border-violet-400'), onClick: () => set_open(true), children: [value.map(v => (_jsxs("span", { className: "inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-violet-100 text-violet-800 text-xs font-medium", children: [label_for(v), _jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); remove_tag(v); }, className: "text-violet-500 hover:text-violet-800 ml-0.5", children: "\u00D7" })] }, v))), _jsx("input", { type: "text", value: search, onChange: e => { set_search(e.target.value); set_open(true); }, onFocus: () => set_open(true), placeholder: value.length === 0 ? 'Type to search...' : '', className: "flex-1 min-w-[80px] text-sm outline-none bg-transparent px-1 py-0.5" })] }), open && filtered.length > 0 && (_jsx("div", { className: "absolute z-50 mt-1 w-full max-h-48 overflow-y-auto rounded-lg border bg-white shadow-lg", children: filtered.map(opt => (_jsxs("button", { type: "button", onClick: () => add_tag(opt.value), className: "w-full text-left px-3 py-2 text-sm hover:bg-violet-50 transition-colors flex items-center justify-between", children: [_jsx("span", { children: opt.label }), _jsx("span", { className: "text-xs text-gray-400 font-mono", children: opt.value })] }, opt.value))) })), open && filtered.length === 0 && search && (_jsx("div", { className: "absolute z-50 mt-1 w-full rounded-lg border bg-white shadow-lg px-3 py-2 text-sm text-gray-400", children: "No matching options" }))] }));
|
|
97
133
|
}
|
|
@@ -8,7 +8,7 @@ export interface ColumnDef<T> {
|
|
|
8
8
|
/** Display label for the form */
|
|
9
9
|
label: string;
|
|
10
10
|
/** Column type determines the input control */
|
|
11
|
-
type: 'text' | 'textarea' | 'color_swatch' | 'select' | 'number' | 'toggle';
|
|
11
|
+
type: 'text' | 'textarea' | 'color_swatch' | 'select' | 'number' | 'toggle' | 'tag_picker';
|
|
12
12
|
/** Placeholder text */
|
|
13
13
|
placeholder?: string;
|
|
14
14
|
/** Whether this field is required */
|
|
@@ -22,6 +22,11 @@ export interface ColumnDef<T> {
|
|
|
22
22
|
value: string;
|
|
23
23
|
label: string;
|
|
24
24
|
}[];
|
|
25
|
+
/** Options for 'tag_picker' type — value is stored, label is displayed */
|
|
26
|
+
tag_options?: {
|
|
27
|
+
value: string;
|
|
28
|
+
label: string;
|
|
29
|
+
}[];
|
|
25
30
|
/** Options for 'color_swatch' type */
|
|
26
31
|
color_options?: string[];
|
|
27
32
|
/** Validation function - returns error message or null */
|
|
@@ -67,6 +72,11 @@ export interface AppConfigListEditorProps<T extends Record<string, unknown>> {
|
|
|
67
72
|
className?: string;
|
|
68
73
|
/** Save status indicator */
|
|
69
74
|
save_status?: 'idle' | 'saving' | 'saved' | 'error';
|
|
75
|
+
/**
|
|
76
|
+
* Tailwind class(es) that control the edit modal's max width. Overrides the
|
|
77
|
+
* default `sm:max-w-2xl`. Use e.g. `sm:max-w-4xl` or `sm:max-w-[900px]`.
|
|
78
|
+
*/
|
|
79
|
+
edit_modal_max_width?: string;
|
|
70
80
|
}
|
|
71
81
|
/**
|
|
72
82
|
* Internal state for the edit modal
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/app_config_list_editor/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B;;GAEG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IACvB,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/app_config_list_editor/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +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,
|
|
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,CA2DxC;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"}
|
|
@@ -41,6 +41,9 @@ export function validate_import_data(data, columns, id_field) {
|
|
|
41
41
|
if (col.type === 'toggle' && typeof val !== 'boolean') {
|
|
42
42
|
item_errors.push(`"${col.field}" should be a boolean`);
|
|
43
43
|
}
|
|
44
|
+
if (col.type === 'tag_picker' && !Array.isArray(val)) {
|
|
45
|
+
item_errors.push(`"${col.field}" should be an array`);
|
|
46
|
+
}
|
|
44
47
|
}
|
|
45
48
|
if (item_errors.length > 0) {
|
|
46
49
|
errors.push(`Item ${i + 1}: ${item_errors.join(', ')}`);
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_config",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
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
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"CHANGE_LOG.md",
|
|
11
|
+
"SETUP_CHECKLIST.md"
|
|
10
12
|
],
|
|
11
13
|
"exports": {
|
|
12
14
|
".": {
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
},
|
|
29
31
|
"scripts": {
|
|
30
32
|
"build": "tsc -p tsconfig.build.json",
|
|
33
|
+
"prepublishOnly": "npm run build",
|
|
31
34
|
"storybook": "npx storybook dev -p 6006",
|
|
32
35
|
"build-storybook": "npx storybook build",
|
|
33
36
|
"test": "vitest run",
|
|
@@ -54,6 +57,9 @@
|
|
|
54
57
|
"url": "https://github.com/pub12/hazo_config/issues"
|
|
55
58
|
},
|
|
56
59
|
"homepage": "https://github.com/pub12/hazo_config#readme",
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=18.0.0"
|
|
62
|
+
},
|
|
57
63
|
"dependencies": {
|
|
58
64
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
59
65
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
@@ -92,8 +98,8 @@
|
|
|
92
98
|
"vitest": "^1.6.1"
|
|
93
99
|
},
|
|
94
100
|
"peerDependencies": {
|
|
95
|
-
"react": "^18.
|
|
96
|
-
"react-dom": "^18.
|
|
101
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
102
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
97
103
|
"tailwindcss": "^3.0.0"
|
|
98
104
|
},
|
|
99
105
|
"peerDependenciesMeta": {
|