@jini-ai/admin 0.4.12 → 0.6.10
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/CHANGELOG.md +70 -2
- package/dist/agent-plugins/controllers/agent-plugin-files.controller.js +14 -9
- package/dist/agent-plugins/controllers/agent-plugins.controller.js +12 -9
- package/dist/agent-plugins/package-file-tree.js +54 -51
- package/dist/agent-plugins/react/components/AgentPluginRow.d.ts +1 -1
- package/dist/agent-plugins/react/components/AgentPluginRow.js +9 -2
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.d.ts +5 -5
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +14 -5
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.d.ts +15 -13
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +54 -40
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.d.ts +13 -13
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.js +23 -11
- package/dist/browser/navigation.js +44 -27
- package/dist/comments/adapters/memory.js +30 -12
- package/dist/comments/conformance/comments-api.conformance.js +55 -33
- package/dist/comments/controllers/comment-queue.controller.js +13 -4
- package/dist/comments/react/hooks/use-comment-queue.hooks.d.ts +17 -2
- package/dist/comments/react/hooks/use-comment-queue.hooks.js +79 -30
- package/dist/comments/react/hooks/use-comment-settings.hooks.js +9 -5
- package/dist/comments/react/index.d.ts +2 -1
- package/dist/comments/react/index.js +1 -1
- package/dist/consent/react/ConsentFields.d.ts +35 -0
- package/dist/consent/react/ConsentFields.js +76 -0
- package/dist/consent/react/ConsentPreviewFrame.d.ts +14 -0
- package/dist/consent/react/ConsentPreviewFrame.js +14 -0
- package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -4
- package/dist/consent/react/ConsentSettingsPanel.js +6 -17
- package/dist/consent/react/index.d.ts +6 -1
- package/dist/consent/react/index.js +2 -0
- package/dist/consent/react/settings-panel.hooks.d.ts +28 -1
- package/dist/consent/react/settings-panel.hooks.js +26 -0
- package/dist/core/entities/rules.js +126 -81
- package/dist/core/manifest/rules.js +10 -10
- package/dist/core/module/create-admin.js +195 -117
- package/dist/core/ports/redirects.d.ts +8 -6
- package/dist/core/ports/redirects.js +8 -6
- package/dist/core/routing/rules.js +9 -4
- package/dist/core/transport/http.js +37 -20
- package/dist/forms/adapters/memory.js +22 -11
- package/dist/forms/conformance/forms-api.conformance.d.ts +6 -4
- package/dist/forms/conformance/forms-api.conformance.js +42 -46
- package/dist/forms/field-settings.js +150 -115
- package/dist/forms/html-rules.js +45 -28
- package/dist/forms/react/components/FieldSettingsRows.d.ts +22 -0
- package/dist/forms/react/components/FieldSettingsRows.js +29 -0
- package/dist/forms/react/components/FieldSettingsSection.d.ts +1 -1
- package/dist/forms/react/components/FieldSettingsSection.js +5 -6
- package/dist/forms/react/components/FormEditorFieldsBody.d.ts +1 -1
- package/dist/forms/react/components/FormEditorFieldsBody.js +4 -15
- package/dist/forms/react/components/FormEditorFieldsBodyParts.d.ts +18 -0
- package/dist/forms/react/components/FormEditorFieldsBodyParts.js +32 -0
- package/dist/forms/react/components/FormSubmissions.d.ts +1 -1
- package/dist/forms/react/components/FormSubmissions.js +4 -20
- package/dist/forms/react/components/FormSubmissionsTable.d.ts +19 -0
- package/dist/forms/react/components/FormSubmissionsTable.js +22 -0
- package/dist/media/adapters/memory.js +31 -26
- package/dist/media/controllers/edit-media.controller.js +2 -1
- package/dist/media/controllers/library.controller.js +48 -35
- package/dist/media/controllers/providers.controller.js +2 -1
- package/dist/media/html-attributes.js +29 -15
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/react/components/MediaIcons.d.ts +0 -3
- package/dist/media/react/components/MediaIcons.js +0 -9
- package/dist/media/react/components/MediaLightbox.d.ts +2 -1
- package/dist/media/react/components/MediaLightbox.js +11 -5
- package/dist/media/react/hooks/LibraryTab.hooks.d.ts +55 -45
- package/dist/media/react/hooks/LibraryTab.hooks.js +107 -70
- package/dist/media/react/hooks/MediaCard.hooks.d.ts +27 -31
- package/dist/media/react/hooks/MediaCard.hooks.js +52 -26
- package/dist/media/react/hooks/MediaGrid.hooks.d.ts +1 -1
- package/dist/media/react/hooks/MediaLightbox.hooks.d.ts +10 -14
- package/dist/media/react/hooks/MediaLightbox.hooks.js +9 -56
- package/dist/media/react/hooks/MediaPage.hooks.d.ts +6 -0
- package/dist/media/react/hooks/MediaPage.hooks.js +29 -12
- package/dist/media/react/hooks/ProvidersTab.hooks.d.ts +33 -33
- package/dist/media/react/hooks/ProvidersTab.hooks.js +119 -79
- package/dist/media/react/hooks/UploadButton.hooks.d.ts +7 -5
- package/dist/media/react/hooks/UploadButton.hooks.js +36 -24
- package/dist/media/react/index.js +4 -0
- package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
- package/dist/media/react/tabs/LibraryTab.js +2 -2
- package/dist/media/rules.d.ts +7 -0
- package/dist/media/rules.js +25 -2
- package/dist/react/components/ConfirmDialog/ConfirmDialog.js +5 -8
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.d.ts +20 -0
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +24 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +16 -7
- package/dist/react/components/RowMenu/RowMenu.js +16 -10
- package/dist/react/components/Sidebar.js +32 -26
- package/dist/react/entities/EntityEdit.js +17 -14
- package/dist/react/entities/EntityField.d.ts +5 -2
- package/dist/react/entities/EntityField.js +38 -23
- package/dist/react/entities/EntityList.js +6 -5
- package/dist/react/entities/RelationTruncationNotice.d.ts +9 -0
- package/dist/react/entities/RelationTruncationNotice.js +9 -0
- package/dist/react/entities/use-entity-edit.d.ts +4 -2
- package/dist/react/entities/use-entity-edit.js +52 -33
- package/dist/react/shell/AdminShell.js +40 -23
- package/dist/react/shell/model.d.ts +4 -1
- package/dist/react/shell/model.js +3 -2
- package/dist/react/shell/use-admin-shell-session.js +25 -26
- package/dist/redirects/adapters/memory.js +9 -2
- package/dist/redirects/conformance/redirects-api.conformance.js +66 -36
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +1 -1
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +10 -3
- package/dist/redirects/react/hooks/wired.hooks.d.ts +1 -1
- package/dist/security/adapters/http.js +25 -10
- package/dist/security/adapters/memory.js +19 -5
- package/dist/security/controllers/access-tokens.controller.js +27 -26
- package/dist/security/controllers/other-credentials.controller.js +4 -2
- package/dist/security/controllers/root-key.controller.js +17 -9
- package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +40 -38
- package/dist/security/react/hooks/AccessTokensTab.hooks.js +36 -9
- package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +14 -14
- package/dist/security/react/hooks/RootKeyTab.hooks.js +12 -3
- package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
- package/dist/security/react/tabs/RootKeyTab.js +12 -3
- package/dist/security/rules.js +45 -25
- package/dist/seo/controllers/seo.controller.js +4 -2
- package/dist/seo/react/components/MediaRefField.d.ts +1 -1
- package/dist/seo/react/components/MediaRefField.js +19 -2
- package/dist/seo/react/hooks/MediaRefField.hooks.js +3 -1
- package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -1
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -34
- package/dist/seo/react/pages/SeoPage.d.ts +4 -4
- package/dist/seo/react/pages/SeoPage.js +26 -6
- package/dist/seo/rules.js +39 -10
- package/dist/source-control/conformance/source-control-api.conformance.js +68 -38
- package/dist/source-control/controllers/source-control.controller.js +20 -8
- package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +2 -1
- package/dist/source-control/react/hooks/ProvidersTab.hooks.js +18 -11
- package/dist/widgets/conformance/widgets-api.conformance.js +15 -1
- package/dist/widgets/controllers/instance-editor.controller.js +33 -24
- package/dist/widgets/controllers/library.controller.js +18 -12
- package/dist/widgets/controllers/region-editor.controller.js +6 -5
- package/dist/widgets/react/hooks/page-views.hooks.d.ts +1 -0
- package/dist/widgets/react/hooks/page-views.hooks.js +3 -2
- package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +1 -1
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +9 -3
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +9 -2
- package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +1 -1
- package/dist/widgets/react/pages/WidgetInstanceEditor.js +7 -8
- package/dist/widgets/rules.d.ts +14 -2
- package/dist/widgets/rules.js +9 -6
- package/package.json +23 -14
- package/src/themes/default/admin.css +51 -0
- package/src/themes/default/chat.css +21 -0
- package/src/themes/default/manifest.json +29 -0
- package/dist/media/styles.d.ts +0 -4
- package/dist/media/styles.js +0 -5
|
@@ -42,40 +42,59 @@ const MAX_JSON_NODES = 10_000;
|
|
|
42
42
|
* {@link MAX_JSON_DEPTH}.
|
|
43
43
|
* @overallScore 100
|
|
44
44
|
*/
|
|
45
|
-
function describeNonJsonValue(value, depth
|
|
45
|
+
function describeNonJsonValue({ value, depth, budget }, _optional = {}) {
|
|
46
46
|
if (depth > MAX_JSON_DEPTH)
|
|
47
47
|
return `nests deeper than ${MAX_JSON_DEPTH} levels`;
|
|
48
48
|
if (++budget.nodes > MAX_JSON_NODES)
|
|
49
49
|
return `holds more than ${MAX_JSON_NODES} values`;
|
|
50
|
-
if (value === null)
|
|
51
|
-
return
|
|
52
|
-
const type = typeof value;
|
|
53
|
-
if (type === 'string' || type === 'boolean')
|
|
54
|
-
return null;
|
|
55
|
-
if (type === 'number')
|
|
56
|
-
return Number.isFinite(value) ? null : 'is a non-finite number';
|
|
57
|
-
if (type !== 'object')
|
|
58
|
-
return `is a ${type}`;
|
|
59
|
-
if (Array.isArray(value)) {
|
|
60
|
-
for (const item of value) {
|
|
61
|
-
const reason = describeNonJsonValue(item, depth + 1, budget);
|
|
62
|
-
if (reason !== null)
|
|
63
|
-
return reason;
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
50
|
+
if (value === null || typeof value !== 'object')
|
|
51
|
+
return describeNonJsonScalar(value);
|
|
67
52
|
// Rejects class instances, `Map`, `Date`, and anything else that survives `typeof === 'object'`
|
|
68
53
|
// but does not round-trip through JSON as itself.
|
|
69
|
-
|
|
70
|
-
if (proto !== Object.prototype && proto !== null)
|
|
54
|
+
if (!Array.isArray(value) && !isPlainObject(value))
|
|
71
55
|
return 'is a non-plain object';
|
|
72
56
|
for (const item of Object.values(value)) {
|
|
73
|
-
const reason = describeNonJsonValue(item, depth + 1, budget);
|
|
57
|
+
const reason = describeNonJsonValue({ value: item, depth: depth + 1, budget });
|
|
74
58
|
if (reason !== null)
|
|
75
59
|
return reason;
|
|
76
60
|
}
|
|
77
61
|
return null;
|
|
78
62
|
}
|
|
63
|
+
/** The leaf half of {@link describeNonJsonValue}: `null` or a non-object `value`. */
|
|
64
|
+
function describeNonJsonScalar(value) {
|
|
65
|
+
if (value === null)
|
|
66
|
+
return null;
|
|
67
|
+
const type = typeof value;
|
|
68
|
+
if (type === 'string' || type === 'boolean')
|
|
69
|
+
return null;
|
|
70
|
+
if (type === 'number')
|
|
71
|
+
return Number.isFinite(value) ? null : 'is a non-finite number';
|
|
72
|
+
return `is a ${type}`;
|
|
73
|
+
}
|
|
74
|
+
function isPlainObject(value) {
|
|
75
|
+
const proto = Object.getPrototypeOf(value);
|
|
76
|
+
return proto === Object.prototype || proto === null;
|
|
77
|
+
}
|
|
78
|
+
/** `null` when `typeof value` is `type`, otherwise "expected <label>, got <typeof value>". */
|
|
79
|
+
function describeWrongType({ value, type, label }, _optional = {}) {
|
|
80
|
+
return typeof value === type ? null : `expected ${label}, got ${typeof value}`;
|
|
81
|
+
}
|
|
82
|
+
function describeNonDatetime(value) {
|
|
83
|
+
if (typeof value !== 'string')
|
|
84
|
+
return `expected an ISO-8601 string, got ${typeof value}`;
|
|
85
|
+
// A `datetime` that is not parseable renders fine and orders wrongly — the exact class of
|
|
86
|
+
// fault this whole check exists for, so it is a mismatch rather than a pass.
|
|
87
|
+
return Number.isNaN(Date.parse(value)) ? 'is not a parseable ISO-8601 timestamp' : null;
|
|
88
|
+
}
|
|
89
|
+
function describeNonNumber({ value, accepts, reason }, _optional = {}) {
|
|
90
|
+
if (typeof value !== 'number')
|
|
91
|
+
return `expected a number, got ${typeof value}`;
|
|
92
|
+
return accepts(value) ? null : reason;
|
|
93
|
+
}
|
|
94
|
+
function describeNonJson(value) {
|
|
95
|
+
const reason = describeNonJsonValue({ value, depth: 0, budget: { nodes: 0 } });
|
|
96
|
+
return reason === null ? null : `is not a JSON value: it ${reason}`;
|
|
97
|
+
}
|
|
79
98
|
/**
|
|
80
99
|
* Whether `value` is a legal runtime value for `field`.
|
|
81
100
|
*
|
|
@@ -98,27 +117,21 @@ export function describeAdminEntityFieldMismatch(field, value) {
|
|
|
98
117
|
switch (field.kind) {
|
|
99
118
|
case 'text':
|
|
100
119
|
case 'relation':
|
|
101
|
-
return
|
|
120
|
+
return describeWrongType({ value, type: 'string', label: 'a string' });
|
|
102
121
|
case 'datetime':
|
|
103
|
-
|
|
104
|
-
return `expected an ISO-8601 string, got ${typeof value}`;
|
|
105
|
-
// A `datetime` that is not parseable renders fine and orders wrongly — the exact class of
|
|
106
|
-
// fault this whole check exists for, so it is a mismatch rather than a pass.
|
|
107
|
-
return Number.isNaN(Date.parse(value)) ? 'is not a parseable ISO-8601 timestamp' : null;
|
|
122
|
+
return describeNonDatetime(value);
|
|
108
123
|
case 'integer':
|
|
109
|
-
|
|
110
|
-
return `expected a number, got ${typeof value}`;
|
|
111
|
-
return Number.isInteger(value) ? null : 'is not an integer';
|
|
124
|
+
return describeNonNumber({ value, accepts: Number.isInteger, reason: 'is not an integer' });
|
|
112
125
|
case 'real':
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
return describeNonNumber({
|
|
127
|
+
value,
|
|
128
|
+
accepts: Number.isFinite,
|
|
129
|
+
reason: 'is not a finite number',
|
|
130
|
+
});
|
|
116
131
|
case 'boolean':
|
|
117
|
-
return
|
|
118
|
-
case 'json':
|
|
119
|
-
|
|
120
|
-
return reason === null ? null : `is not a JSON value: it ${reason}`;
|
|
121
|
-
}
|
|
132
|
+
return describeWrongType({ value, type: 'boolean', label: 'a boolean' });
|
|
133
|
+
case 'json':
|
|
134
|
+
return describeNonJson(value);
|
|
122
135
|
}
|
|
123
136
|
}
|
|
124
137
|
/**
|
|
@@ -132,49 +145,63 @@ export function describeAdminEntityFieldMismatch(field, value) {
|
|
|
132
145
|
* @complexity Time O(fields + row keys) plus the `json` walk; space O(violations).
|
|
133
146
|
* @overallScore 100
|
|
134
147
|
*/
|
|
135
|
-
function findRowViolations(descriptor, operation, row) {
|
|
148
|
+
function findRowViolations({ descriptor, operation, row, }, _optional = {}) {
|
|
149
|
+
const hasId = typeof row.id === 'string' && row.id !== '';
|
|
150
|
+
const rowId = hasId ? row.id : '<no id>';
|
|
151
|
+
const at = { entity: descriptor.name, operation, rowId };
|
|
136
152
|
const found = [];
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
153
|
+
if (!hasId) {
|
|
154
|
+
found.push({
|
|
155
|
+
...at,
|
|
156
|
+
field: IMPLICIT_ROW_KEY,
|
|
157
|
+
problem: 'wrong-type',
|
|
158
|
+
detail: 'every row must carry a non-empty string `id`',
|
|
159
|
+
});
|
|
143
160
|
}
|
|
144
|
-
const declared = new Set([IMPLICIT_ROW_KEY]);
|
|
145
161
|
for (const field of descriptor.fields) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (field.required === true) {
|
|
150
|
-
push(field.name, 'missing-required', 'declared `required: true` but absent from the row');
|
|
151
|
-
}
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
const mismatch = describeAdminEntityFieldMismatch(field, value);
|
|
155
|
-
if (mismatch !== null)
|
|
156
|
-
push(field.name, 'wrong-type', `declared \`${field.kind}\`; ${mismatch}`);
|
|
162
|
+
const problem = describeFieldProblem({ field, value: row[field.name] });
|
|
163
|
+
if (problem !== null)
|
|
164
|
+
found.push({ ...at, field: field.name, ...problem });
|
|
157
165
|
}
|
|
166
|
+
const declared = new Set([IMPLICIT_ROW_KEY, ...descriptor.fields.map((field) => field.name)]);
|
|
158
167
|
for (const key of Object.keys(row)) {
|
|
159
168
|
if (declared.has(key))
|
|
160
169
|
continue;
|
|
161
|
-
push(
|
|
170
|
+
found.push({
|
|
171
|
+
...at,
|
|
172
|
+
field: key,
|
|
173
|
+
problem: 'undeclared-field',
|
|
174
|
+
detail: 'present on the row but not declared on the descriptor',
|
|
175
|
+
});
|
|
162
176
|
}
|
|
163
177
|
return found;
|
|
164
178
|
}
|
|
179
|
+
/** What is wrong with one declared field's cell, or `null` when it conforms or is legally absent. */
|
|
180
|
+
function describeFieldProblem({ field, value }, _optional = {}) {
|
|
181
|
+
if (value === undefined) {
|
|
182
|
+
return field.required === true
|
|
183
|
+
? { problem: 'missing-required', detail: 'declared `required: true` but absent from the row' }
|
|
184
|
+
: null;
|
|
185
|
+
}
|
|
186
|
+
const mismatch = describeAdminEntityFieldMismatch(field, value);
|
|
187
|
+
return mismatch === null
|
|
188
|
+
? null
|
|
189
|
+
: { problem: 'wrong-type', detail: `declared \`${field.kind}\`; ${mismatch}` };
|
|
190
|
+
}
|
|
165
191
|
/**
|
|
166
192
|
* Runs `row` past its descriptor and reports anything wrong, then returns it unchanged.
|
|
167
193
|
*
|
|
168
194
|
* Deliberately identity-returning: this is an observation point, never a repair point. Coercing a
|
|
169
195
|
* bad value here would hide the adapter bug it exists to expose.
|
|
170
196
|
*
|
|
171
|
-
* @
|
|
197
|
+
* Only {@link eraseEntityPort}'s checking wrapper calls this, and that wrapper exists only when
|
|
198
|
+
* `onViolation` is set — so the sink is required here rather than re-checked.
|
|
199
|
+
*
|
|
200
|
+
* @complexity Time O(rows × fields).
|
|
172
201
|
* @overallScore 100
|
|
173
202
|
*/
|
|
174
|
-
function checkRow(descriptor, operation, row, onViolation) {
|
|
175
|
-
|
|
176
|
-
return row;
|
|
177
|
-
for (const violation of findRowViolations(descriptor, operation, row))
|
|
203
|
+
function checkRow({ descriptor, operation, row, onViolation, }, _optional = {}) {
|
|
204
|
+
for (const violation of findRowViolations({ descriptor, operation, row }))
|
|
178
205
|
onViolation(violation);
|
|
179
206
|
return row;
|
|
180
207
|
}
|
|
@@ -208,18 +235,20 @@ export function eraseEntityPort(port, options = {}) {
|
|
|
208
235
|
async list(query) {
|
|
209
236
|
const page = await erased.list(query);
|
|
210
237
|
for (const row of page.items)
|
|
211
|
-
checkRow(descriptor, 'list', row, onViolation);
|
|
238
|
+
checkRow({ descriptor, operation: 'list', row, onViolation });
|
|
212
239
|
return page;
|
|
213
240
|
},
|
|
214
241
|
async get(id) {
|
|
215
242
|
const row = await erased.get(id);
|
|
216
|
-
return row === null ? null : checkRow(descriptor, 'get', row, onViolation);
|
|
243
|
+
return row === null ? null : checkRow({ descriptor, operation: 'get', row, onViolation });
|
|
217
244
|
},
|
|
218
245
|
async create(input) {
|
|
219
|
-
|
|
246
|
+
const row = await erased.create(input);
|
|
247
|
+
return checkRow({ descriptor, operation: 'create', row, onViolation });
|
|
220
248
|
},
|
|
221
249
|
async update(id, patch) {
|
|
222
|
-
|
|
250
|
+
const row = await erased.update(id, patch);
|
|
251
|
+
return checkRow({ descriptor, operation: 'update', row, onViolation });
|
|
223
252
|
},
|
|
224
253
|
};
|
|
225
254
|
// Preserved by presence, not by a forwarding stub: a screen decides whether to render a delete
|
|
@@ -241,44 +270,60 @@ export function eraseEntityPort(port, options = {}) {
|
|
|
241
270
|
* @complexity Time O(fields); space O(problems).
|
|
242
271
|
* @overallScore 100
|
|
243
272
|
*/
|
|
244
|
-
function findDescriptorProblems(descriptor, registryKey, registeredNames) {
|
|
245
|
-
const problems = [];
|
|
273
|
+
function findDescriptorProblems({ descriptor, registryKey, registeredNames, }, _optional = {}) {
|
|
246
274
|
const at = `'${registryKey}'`;
|
|
275
|
+
return [
|
|
276
|
+
...findHeaderProblems({ descriptor, registryKey }),
|
|
277
|
+
...findFieldProblems({ descriptor, registeredNames }),
|
|
278
|
+
...findTitleFieldProblems({ descriptor }),
|
|
279
|
+
].map((problem) => `${at}: ${problem}`);
|
|
280
|
+
}
|
|
281
|
+
function findHeaderProblems({ descriptor, registryKey }, _optional = {}) {
|
|
282
|
+
const problems = [];
|
|
247
283
|
if (descriptor.name !== registryKey) {
|
|
248
284
|
// One entity, one name. Allowing these to differ would leave the URL segment, the relation
|
|
249
285
|
// target, and the registry key as three identifiers with no stated relationship between them.
|
|
250
|
-
problems.push(
|
|
286
|
+
problems.push(`registered under '${registryKey}' but its descriptor is named '${descriptor.name}'`);
|
|
251
287
|
}
|
|
252
288
|
if (descriptor.labelSingular === '' || descriptor.labelPlural === '') {
|
|
253
|
-
problems.push(
|
|
289
|
+
problems.push('labelSingular and labelPlural must both be non-empty');
|
|
254
290
|
}
|
|
255
291
|
if (descriptor.fields.length === 0) {
|
|
256
|
-
problems.push(
|
|
292
|
+
problems.push('declares no fields, so nothing but its id could ever be shown or edited');
|
|
257
293
|
}
|
|
294
|
+
return problems;
|
|
295
|
+
}
|
|
296
|
+
function findFieldProblems({ descriptor, registeredNames, }, _optional = {}) {
|
|
297
|
+
const problems = [];
|
|
258
298
|
const seen = new Set();
|
|
259
299
|
for (const field of descriptor.fields) {
|
|
260
300
|
if (field.name === '') {
|
|
261
|
-
problems.push(
|
|
301
|
+
problems.push('a field has an empty name');
|
|
262
302
|
continue;
|
|
263
303
|
}
|
|
264
304
|
if (field.name === IMPLICIT_ROW_KEY) {
|
|
265
|
-
problems.push(
|
|
305
|
+
problems.push(`'${IMPLICIT_ROW_KEY}' is implicit on every row and must not be declared as a field`);
|
|
266
306
|
}
|
|
267
307
|
if (seen.has(field.name))
|
|
268
|
-
problems.push(
|
|
308
|
+
problems.push(`field '${field.name}' is declared more than once`);
|
|
269
309
|
seen.add(field.name);
|
|
270
310
|
if (field.kind === 'relation' && !registeredNames.has(field.target)) {
|
|
271
|
-
problems.push(
|
|
311
|
+
problems.push(`field '${field.name}' targets '${field.target}', which is not a registered entity`);
|
|
272
312
|
}
|
|
273
313
|
}
|
|
314
|
+
return problems;
|
|
315
|
+
}
|
|
316
|
+
function findTitleFieldProblems({ descriptor }, _optional = {}) {
|
|
274
317
|
const titleField = descriptor.fields.find((field) => field.name === descriptor.titleField);
|
|
275
318
|
if (titleField === undefined) {
|
|
276
|
-
|
|
319
|
+
return [`titleField '${descriptor.titleField}' does not name a declared field`];
|
|
277
320
|
}
|
|
278
|
-
|
|
279
|
-
|
|
321
|
+
if (titleField.kind !== 'text') {
|
|
322
|
+
return [
|
|
323
|
+
`titleField '${descriptor.titleField}' is kind '${titleField.kind}'; a title must be 'text'`,
|
|
324
|
+
];
|
|
280
325
|
}
|
|
281
|
-
return
|
|
326
|
+
return [];
|
|
282
327
|
}
|
|
283
328
|
/**
|
|
284
329
|
* Assembles entity ports into a registry, validating every descriptor first.
|
|
@@ -306,7 +351,7 @@ function findDescriptorProblems(descriptor, registryKey, registeredNames) {
|
|
|
306
351
|
export function createAdminEntityRegistry(ports) {
|
|
307
352
|
const entries = Object.entries(ports);
|
|
308
353
|
const registeredNames = new Set(entries.map(([, port]) => port.descriptor.name));
|
|
309
|
-
const problems = entries.flatMap(([key, port]) => findDescriptorProblems(port.descriptor, key, registeredNames));
|
|
354
|
+
const problems = entries.flatMap(([key, port]) => findDescriptorProblems({ descriptor: port.descriptor, registryKey: key, registeredNames }));
|
|
310
355
|
if (problems.length > 0) {
|
|
311
356
|
throw new Error(`createAdminEntityRegistry: invalid descriptors\n- ${problems.join('\n- ')}`);
|
|
312
357
|
}
|
|
@@ -35,8 +35,9 @@ export function resolvePanels({ panels }, context = {}) {
|
|
|
35
35
|
* @complexity O(n log n) on the items within each group; O(n) otherwise.
|
|
36
36
|
*/
|
|
37
37
|
export function buildNav({ panels }, options = {}) {
|
|
38
|
+
// A `Map` iterates in first-insertion order, so it alone records the order each group's first
|
|
39
|
+
// member was registered — no separate order list (and no lookup that could miss) is needed.
|
|
38
40
|
const groups = new Map();
|
|
39
|
-
const groupOrder = [];
|
|
40
41
|
// Sentinel for "no group". A panel cannot collide with it: `group` is `string | undefined`, and
|
|
41
42
|
// an explicit empty-string group would be a labelless heading, which is the same thing.
|
|
42
43
|
const UNGROUPED = '';
|
|
@@ -44,20 +45,19 @@ export function buildNav({ panels }, options = {}) {
|
|
|
44
45
|
if (!panel.nav)
|
|
45
46
|
return;
|
|
46
47
|
const key = panel.nav.group ?? UNGROUPED;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
groups.get(key)?.push({ ...panel.nav, id: panel.id, href: panelHref({ panelId: panel.id }, options) });
|
|
48
|
+
const group = groups.get(key) ?? [];
|
|
49
|
+
groups.set(key, group);
|
|
50
|
+
group.push({ ...panel.nav, id: panel.id, href: panelHref({ panelId: panel.id }, options) });
|
|
52
51
|
});
|
|
53
52
|
// Ungrouped first regardless of when it was registered — it is the top row, not a peer group.
|
|
53
|
+
const entries = [...groups.entries()];
|
|
54
54
|
const ordered = [
|
|
55
|
-
...
|
|
56
|
-
...
|
|
55
|
+
...entries.filter(([k]) => k === UNGROUPED),
|
|
56
|
+
...entries.filter(([k]) => k !== UNGROUPED),
|
|
57
57
|
];
|
|
58
|
-
return ordered.map((key) => ({
|
|
58
|
+
return ordered.map(([key, items]) => ({
|
|
59
59
|
...(key === UNGROUPED ? {} : { label: key }),
|
|
60
|
-
items: stableSortByOrder(
|
|
60
|
+
items: stableSortByOrder(items),
|
|
61
61
|
}));
|
|
62
62
|
}
|
|
63
63
|
/** `Array.prototype.sort` is specified as stable, so equal `order` values keep registration
|