@jini-ai/admin 0.4.11 → 0.4.13

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/agent-plugins/controllers/agent-plugin-files.controller.js +14 -9
  3. package/dist/agent-plugins/controllers/agent-plugins.controller.js +12 -9
  4. package/dist/agent-plugins/package-file-tree.js +54 -51
  5. package/dist/agent-plugins/react/components/AgentPluginRow.d.ts +1 -1
  6. package/dist/agent-plugins/react/components/AgentPluginRow.js +9 -2
  7. package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.d.ts +5 -5
  8. package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +14 -5
  9. package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.d.ts +15 -13
  10. package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +54 -40
  11. package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.d.ts +13 -13
  12. package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.js +23 -11
  13. package/dist/browser/navigation.js +44 -27
  14. package/dist/comments/adapters/memory.js +30 -12
  15. package/dist/comments/conformance/comments-api.conformance.js +55 -33
  16. package/dist/comments/controllers/comment-queue.controller.js +13 -4
  17. package/dist/comments/react/hooks/use-comment-queue.hooks.d.ts +17 -2
  18. package/dist/comments/react/hooks/use-comment-queue.hooks.js +79 -30
  19. package/dist/comments/react/hooks/use-comment-settings.hooks.js +9 -5
  20. package/dist/comments/react/index.d.ts +2 -1
  21. package/dist/comments/react/index.js +1 -1
  22. package/dist/consent/react/ConsentFields.d.ts +35 -0
  23. package/dist/consent/react/ConsentFields.js +74 -0
  24. package/dist/consent/react/ConsentPreviewFrame.d.ts +14 -0
  25. package/dist/consent/react/ConsentPreviewFrame.js +14 -0
  26. package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -4
  27. package/dist/consent/react/ConsentSettingsPanel.js +6 -17
  28. package/dist/consent/react/index.d.ts +6 -1
  29. package/dist/consent/react/index.js +2 -0
  30. package/dist/consent/react/settings-panel.hooks.d.ts +28 -1
  31. package/dist/consent/react/settings-panel.hooks.js +26 -0
  32. package/dist/core/entities/rules.js +126 -81
  33. package/dist/core/manifest/rules.js +10 -10
  34. package/dist/core/module/create-admin.js +195 -117
  35. package/dist/core/ports/redirects.d.ts +8 -6
  36. package/dist/core/ports/redirects.js +8 -6
  37. package/dist/core/ports/seo.d.ts +20 -0
  38. package/dist/core/routing/rules.js +9 -4
  39. package/dist/core/transport/http.js +37 -20
  40. package/dist/forms/adapters/memory.js +22 -11
  41. package/dist/forms/conformance/forms-api.conformance.d.ts +6 -4
  42. package/dist/forms/conformance/forms-api.conformance.js +42 -46
  43. package/dist/forms/field-settings.js +150 -115
  44. package/dist/forms/html-rules.js +45 -28
  45. package/dist/forms/react/components/FieldSettingsRows.d.ts +22 -0
  46. package/dist/forms/react/components/FieldSettingsRows.js +29 -0
  47. package/dist/forms/react/components/FieldSettingsSection.d.ts +1 -1
  48. package/dist/forms/react/components/FieldSettingsSection.js +5 -6
  49. package/dist/forms/react/components/FormEditorFieldsBody.d.ts +1 -1
  50. package/dist/forms/react/components/FormEditorFieldsBody.js +4 -15
  51. package/dist/forms/react/components/FormEditorFieldsBodyParts.d.ts +18 -0
  52. package/dist/forms/react/components/FormEditorFieldsBodyParts.js +32 -0
  53. package/dist/forms/react/components/FormSubmissions.d.ts +1 -1
  54. package/dist/forms/react/components/FormSubmissions.js +4 -20
  55. package/dist/forms/react/components/FormSubmissionsTable.d.ts +19 -0
  56. package/dist/forms/react/components/FormSubmissionsTable.js +22 -0
  57. package/dist/media/adapters/memory.js +31 -26
  58. package/dist/media/controllers/edit-media.controller.js +2 -1
  59. package/dist/media/controllers/library.controller.js +48 -35
  60. package/dist/media/controllers/providers.controller.js +2 -1
  61. package/dist/media/html-attributes.js +29 -15
  62. package/dist/media/index.d.ts +1 -1
  63. package/dist/media/index.js +1 -1
  64. package/dist/media/react/components/MediaIcons.d.ts +0 -3
  65. package/dist/media/react/components/MediaIcons.js +0 -9
  66. package/dist/media/react/components/MediaLightbox.d.ts +2 -1
  67. package/dist/media/react/components/MediaLightbox.js +11 -5
  68. package/dist/media/react/hooks/LibraryTab.hooks.d.ts +55 -45
  69. package/dist/media/react/hooks/LibraryTab.hooks.js +107 -70
  70. package/dist/media/react/hooks/MediaCard.hooks.d.ts +27 -31
  71. package/dist/media/react/hooks/MediaCard.hooks.js +52 -26
  72. package/dist/media/react/hooks/MediaGrid.hooks.d.ts +1 -1
  73. package/dist/media/react/hooks/MediaLightbox.hooks.d.ts +10 -14
  74. package/dist/media/react/hooks/MediaLightbox.hooks.js +9 -56
  75. package/dist/media/react/hooks/MediaPage.hooks.d.ts +6 -0
  76. package/dist/media/react/hooks/MediaPage.hooks.js +29 -12
  77. package/dist/media/react/hooks/ProvidersTab.hooks.d.ts +33 -33
  78. package/dist/media/react/hooks/ProvidersTab.hooks.js +119 -79
  79. package/dist/media/react/hooks/UploadButton.hooks.d.ts +7 -5
  80. package/dist/media/react/hooks/UploadButton.hooks.js +36 -24
  81. package/dist/media/react/index.js +4 -0
  82. package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
  83. package/dist/media/react/tabs/LibraryTab.js +2 -2
  84. package/dist/media/rules.d.ts +7 -0
  85. package/dist/media/rules.js +25 -2
  86. package/dist/react/components/ConfirmDialog/ConfirmDialog.js +5 -8
  87. package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.d.ts +20 -0
  88. package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +24 -0
  89. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +16 -7
  90. package/dist/react/components/RowMenu/RowMenu.js +16 -10
  91. package/dist/react/components/Sidebar.js +32 -26
  92. package/dist/react/entities/EntityEdit.js +17 -14
  93. package/dist/react/entities/EntityField.d.ts +5 -2
  94. package/dist/react/entities/EntityField.js +38 -23
  95. package/dist/react/entities/EntityList.js +6 -5
  96. package/dist/react/entities/RelationTruncationNotice.d.ts +9 -0
  97. package/dist/react/entities/RelationTruncationNotice.js +9 -0
  98. package/dist/react/entities/use-entity-edit.d.ts +4 -2
  99. package/dist/react/entities/use-entity-edit.js +52 -33
  100. package/dist/react/shell/AdminShell.js +40 -23
  101. package/dist/react/shell/model.d.ts +4 -1
  102. package/dist/react/shell/model.js +3 -2
  103. package/dist/react/shell/use-admin-shell-session.js +25 -26
  104. package/dist/redirects/adapters/memory.js +9 -2
  105. package/dist/redirects/conformance/redirects-api.conformance.js +66 -36
  106. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +1 -1
  107. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +10 -3
  108. package/dist/redirects/react/hooks/wired.hooks.d.ts +1 -1
  109. package/dist/security/adapters/http.js +25 -10
  110. package/dist/security/adapters/memory.js +19 -5
  111. package/dist/security/controllers/access-tokens.controller.js +27 -26
  112. package/dist/security/controllers/other-credentials.controller.js +4 -2
  113. package/dist/security/controllers/root-key.controller.js +17 -9
  114. package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +40 -38
  115. package/dist/security/react/hooks/AccessTokensTab.hooks.js +36 -9
  116. package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +14 -14
  117. package/dist/security/react/hooks/RootKeyTab.hooks.js +12 -3
  118. package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
  119. package/dist/security/react/tabs/RootKeyTab.js +12 -3
  120. package/dist/security/rules.js +45 -25
  121. package/dist/seo/adapters/http.js +5 -0
  122. package/dist/seo/controllers/seo.controller.js +4 -2
  123. package/dist/seo/messages.en.d.ts +15 -10
  124. package/dist/seo/messages.en.js +15 -10
  125. package/dist/seo/models.d.ts +11 -0
  126. package/dist/seo/react/components/MediaRefField.d.ts +17 -5
  127. package/dist/seo/react/components/MediaRefField.js +31 -4
  128. package/dist/seo/react/hooks/MediaRefField.hooks.js +12 -2
  129. package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -0
  130. package/dist/seo/react/hooks/SeoPage.hooks.js +2 -2
  131. package/dist/seo/react/hooks/SeoQuery.hooks.d.ts +1 -0
  132. package/dist/seo/react/hooks/SeoQuery.hooks.js +1 -0
  133. package/dist/seo/react/hooks/use-entry-picker.hooks.js +7 -2
  134. package/dist/seo/react/hooks/use-seo-entry-panel.hooks.js +13 -2
  135. package/dist/seo/react/hooks/use-share-image-warning.hooks.d.ts +4 -1
  136. package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -33
  137. package/dist/seo/react/index.d.ts +45 -30
  138. package/dist/seo/react/pages/SeoPage.d.ts +6 -4
  139. package/dist/seo/react/pages/SeoPage.js +51 -29
  140. package/dist/seo/rules.d.ts +91 -0
  141. package/dist/seo/rules.js +161 -6
  142. package/dist/seo/seo.module.d.ts +15 -10
  143. package/dist/source-control/conformance/source-control-api.conformance.js +68 -38
  144. package/dist/source-control/controllers/source-control.controller.js +20 -8
  145. package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +2 -1
  146. package/dist/source-control/react/hooks/ProvidersTab.hooks.js +18 -11
  147. package/dist/widgets/conformance/widgets-api.conformance.js +15 -1
  148. package/dist/widgets/controllers/instance-editor.controller.js +33 -24
  149. package/dist/widgets/controllers/library.controller.js +18 -12
  150. package/dist/widgets/controllers/region-editor.controller.js +6 -5
  151. package/dist/widgets/react/hooks/page-views.hooks.d.ts +1 -0
  152. package/dist/widgets/react/hooks/page-views.hooks.js +3 -2
  153. package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +1 -1
  154. package/dist/widgets/react/hooks/use-widget-regions.hooks.js +9 -3
  155. package/dist/widgets/react/hooks/use-widgets-library.hooks.js +9 -2
  156. package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +1 -1
  157. package/dist/widgets/react/pages/WidgetInstanceEditor.js +7 -8
  158. package/dist/widgets/rules.d.ts +14 -2
  159. package/dist/widgets/rules.js +9 -6
  160. package/package.json +19 -10
  161. package/src/themes/default/admin.css +51 -0
  162. package/src/themes/default/chat.css +21 -0
  163. package/src/themes/default/manifest.json +29 -0
  164. package/dist/media/styles.d.ts +0 -4
  165. package/dist/media/styles.js +0 -5
@@ -1,21 +1,38 @@
1
1
  import { AdminApiError } from '../../core/transport/errors.js';
2
+ // The contract excludes slug; runtime callers cannot smuggle it through a cast either.
3
+ const PATCHABLE_KEYS = ['name', 'fields', 'notify', 'status', 'mode', 'html'];
4
+ /** The allowlisted patch keys the caller actually supplied (an explicit `undefined` is not a value). */
5
+ function suppliedPatch(patch) {
6
+ return Object.fromEntries(PATCHABLE_KEYS.filter(key => patch[key] !== undefined).map(key => [key, patch[key]]));
7
+ }
8
+ /** The core contract's add/edit-only invariant: a fields replacement must keep every current id. */
9
+ function assertNoFieldRemoved({ current, patch }) {
10
+ if (!patch.fields)
11
+ return;
12
+ const incomingIds = new Set(patch.fields.map(field => field.id));
13
+ if (current.fields.some(field => !incomingIds.has(field.id))) {
14
+ throw new AdminApiError({ message: 'existing form fields cannot be removed', status: 400 }, { code: 'FORMS_FIELD_VALIDATION_ERROR' });
15
+ }
16
+ }
2
17
  /** A scoped, deterministic in-memory API with the core contract's add/edit-only invariant. */
3
18
  export function createMemoryFormsApi(_required = {}, { forms = [], submissions = [], now = () => new Date(0).toISOString(), nextId } = {}) {
4
19
  const definitions = structuredClone([...forms]);
5
20
  const answers = structuredClone([...submissions]);
6
21
  let sequence = definitions.length;
7
- const missing = (id) => { throw new AdminApiError({ message: `form definition '${id}' was not found`, status: 404 }); };
22
+ const missing = (id, { kind = 'form definition' } = {}) => {
23
+ throw new AdminApiError({ message: `${kind} '${id}' was not found`, status: 404 });
24
+ };
8
25
  const find = (id) => definitions.find(form => form.slug === id) ?? definitions.find(form => form.id === id) ?? missing(id);
9
26
  const definitionId = (id) => definitions.find(form => form.slug === id)?.id ?? id;
10
27
  const findSubmission = (formId, id) => {
11
28
  const resolvedId = definitionId(formId);
12
- return answers.find(item => item.id === id && item.formDefinitionId === resolvedId && item.status !== 'trash') ?? missing(id);
29
+ return answers.find(item => item.id === id && item.formDefinitionId === resolvedId && item.status !== 'trash') ?? missing(id, { kind: 'form submission' });
13
30
  };
14
31
  const remove = (type, id) => {
15
32
  const rows = type === 'form' ? definitions : answers;
16
33
  const index = rows.findIndex(row => row.id === id);
17
34
  if (index < 0)
18
- missing(id);
35
+ missing(id, { kind: type === 'form' ? 'form definition' : 'form submission' });
19
36
  // The fixture retains submission payloads just as the generic Trash owner does. Definitions
20
37
  // keep their existing separate fixture projection; the core has no definition-delete method.
21
38
  if (type === 'form_submission')
@@ -42,14 +59,8 @@ export function createMemoryFormsApi(_required = {}, { forms = [], submissions =
42
59
  },
43
60
  async updateFormDefinition({ id }, patch = {}) {
44
61
  const current = definitions.find(form => form.id === id) ?? missing(id);
45
- const incomingIds = patch.fields ? new Set(patch.fields.map(field => field.id)) : null;
46
- if (incomingIds && current.fields.some(field => !incomingIds.has(field.id))) {
47
- throw new AdminApiError({ message: 'existing form fields cannot be removed', status: 400 }, { code: 'FORMS_FIELD_VALIDATION_ERROR' });
48
- }
49
- // The contract excludes slug; runtime callers cannot smuggle it through a cast either.
50
- const { name, fields, notify, status, mode, html } = patch;
51
- const applied = { ...(name !== undefined ? { name } : {}), ...(fields !== undefined ? { fields } : {}), ...(notify !== undefined ? { notify } : {}), ...(status !== undefined ? { status } : {}), ...(mode !== undefined ? { mode } : {}), ...(html !== undefined ? { html } : {}) };
52
- const updated = { ...current, ...structuredClone(applied), updatedAt: now() };
62
+ assertNoFieldRemoved({ current, patch });
63
+ const updated = { ...current, ...structuredClone(suppliedPatch(patch)), updatedAt: now() };
53
64
  definitions[definitions.indexOf(current)] = updated;
54
65
  return structuredClone(updated);
55
66
  },
@@ -3,15 +3,17 @@ export interface FormsConformanceResult {
3
3
  readonly passed: number;
4
4
  readonly failures: readonly string[];
5
5
  }
6
+ type SubmissionSeed = {
7
+ formId: string;
8
+ submissionId: string;
9
+ };
6
10
  /** Run on an isolated API: creates definitions and trashes seeded submissions.
7
11
  * The host provides a disposable seed so conformance never touches an operator's live data. */
8
12
  export declare function runFormsApiConformance({ api }: {
9
13
  api: AdminFormsPort;
10
14
  }, { slug, submission }?: {
11
15
  slug?: string;
12
- submission?: {
13
- formId: string;
14
- submissionId: string;
15
- };
16
+ submission?: SubmissionSeed;
16
17
  }): Promise<FormsConformanceResult>;
18
+ export {};
17
19
  //# sourceMappingURL=forms-api.conformance.d.ts.map
@@ -7,6 +7,23 @@ export async function runFormsApiConformance({ api }, { slug = 'forms-conformanc
7
7
  passed++;
8
8
  else
9
9
  failures.push(name); };
10
+ const created = await checkCreateAndRead({ api, slug, check });
11
+ await checkUpdates({ api, created, check });
12
+ if (submission)
13
+ await checkSubmissions({ api, created, submission, check });
14
+ return { passed, failures };
15
+ }
16
+ /** Resolves true when the operation is refused; only that probe's rejection is absorbed. */
17
+ async function rejects({ operation }) {
18
+ try {
19
+ await operation();
20
+ return false;
21
+ }
22
+ catch {
23
+ return true;
24
+ }
25
+ }
26
+ async function checkCreateAndRead({ api, slug, check }) {
10
27
  const field = { id: 'email', label: 'Email', type: 'email', required: true, className: 'wide', attributes: { 'aria-label': 'Email' } };
11
28
  const created = await api.createFormDefinition({ name: 'Contact', slug, fields: [field] }, { notify: { enabled: true, recipients: ['owner@example.com'] } });
12
29
  check('create returns requested definition', created.name === 'Contact' && created.slug === slug && created.status === 'active');
@@ -16,56 +33,35 @@ export async function runFormsApiConformance({ api }, { slug = 'forms-conformanc
16
33
  const snapshot = await api.getFormDefinition({ id: created.id });
17
34
  Object.assign(snapshot.fields[0], { label: 'Changed only in the read snapshot' });
18
35
  check('read snapshots cannot mutate stored fields', (await api.getFormDefinition({ id: created.id })).fields[0]?.label === 'Email');
19
- const updated = await api.updateFormDefinition({ id: created.id }, { name: 'Renamed', fields: [...created.fields, { id: 'message', label: 'Message', type: 'textarea', required: false }] });
36
+ return created;
37
+ }
38
+ async function checkUpdates({ api, created, check }) {
39
+ const { id } = created;
40
+ const updated = await api.updateFormDefinition({ id }, { name: 'Renamed', fields: [...created.fields, { id: 'message', label: 'Message', type: 'textarea', required: false }] });
20
41
  check('name patch and added field persist', updated.name === 'Renamed' && updated.fields.length === 2);
21
42
  check('untouched notify survives update', updated.notify.enabled && updated.notify.recipients[0] === 'owner@example.com');
22
- let refused = false;
23
- try {
24
- await api.updateFormDefinition({ id: created.id }, { fields: [] });
25
- }
26
- catch {
27
- refused = true;
28
- }
29
- check('existing fields cannot be removed', refused);
30
- check('rejected removal leaves fields intact', (await api.getFormDefinition({ id: created.id })).fields.length === 2);
31
- const disabled = await api.updateFormDefinition({ id: created.id }, { status: 'disabled' });
32
- check('definition disable is reversible', disabled.status === 'disabled' && (await api.updateFormDefinition({ id: created.id }, { status: 'active' })).status === 'active');
33
- const html = await api.updateFormDefinition({ id: created.id }, { mode: 'html', html: '<input name="email">' });
43
+ check('existing fields cannot be removed', await rejects({ operation: () => api.updateFormDefinition({ id }, { fields: [] }) }));
44
+ check('rejected removal leaves fields intact', (await api.getFormDefinition({ id })).fields.length === 2);
45
+ const disabled = await api.updateFormDefinition({ id }, { status: 'disabled' });
46
+ check('definition disable is reversible', disabled.status === 'disabled' && (await api.updateFormDefinition({ id }, { status: 'active' })).status === 'active');
47
+ const html = await api.updateFormDefinition({ id }, { mode: 'html', html: '<input name="email">' });
34
48
  check('HTML authoring survives update', html.mode === 'html' && html.html === '<input name="email">');
35
- check('builder switch is explicit', (await api.updateFormDefinition({ id: created.id }, { mode: 'builder' })).mode === 'builder');
36
- check('empty submission page has null cursor', (await api.listFormSubmissions({ formId: created.id })).nextCursor === null);
49
+ check('builder switch is explicit', (await api.updateFormDefinition({ id }, { mode: 'builder' })).mode === 'builder');
50
+ check('empty submission page has null cursor', (await api.listFormSubmissions({ formId: id })).nextCursor === null);
37
51
  check('definition has no delete operation', !('deleteFormDefinition' in api));
38
- if (submission) {
39
- const page = await api.listFormSubmissions({ formId: submission.formId }, { limit: 1 });
40
- check('submission page is form scoped', page.items.every(item => item.formDefinitionId === submission.formId));
41
- check('submission detail is scoped', (await api.getFormSubmission(submission)).formDefinitionId === submission.formId);
42
- let wrongFormRejected = false;
43
- try {
44
- await api.getFormSubmission({ formId: created.id, submissionId: submission.submissionId });
45
- }
46
- catch {
47
- wrongFormRejected = true;
48
- }
49
- check('a submission of another form is unavailable', wrongFormRejected);
50
- let wrongFormDeleteRejected = false;
51
- try {
52
- await api.deleteFormSubmission({ formId: created.id, submissionId: submission.submissionId });
53
- }
54
- catch {
55
- wrongFormDeleteRejected = true;
56
- }
57
- check('cross-form trash is rejected without removing the submission', wrongFormDeleteRejected && (await api.getFormSubmission(submission)).id === submission.submissionId);
52
+ }
53
+ async function checkSubmissions({ api, created, submission, check }) {
54
+ const crossForm = { formId: created.id, submissionId: submission.submissionId };
55
+ const page = await api.listFormSubmissions({ formId: submission.formId }, { limit: 1 });
56
+ check('submission page is form scoped', page.items.every(item => item.formDefinitionId === submission.formId));
57
+ check('submission detail is scoped', (await api.getFormSubmission(submission)).formDefinitionId === submission.formId);
58
+ check('a submission of another form is unavailable', await rejects({ operation: () => api.getFormSubmission(crossForm) }));
59
+ const wrongFormDeleteRejected = await rejects({ operation: () => api.deleteFormSubmission(crossForm) });
60
+ const submissionRetained = await api.getFormSubmission(submission).then(item => item.id === submission.submissionId, () => false);
61
+ check('cross-form trash is rejected without removing the submission', wrongFormDeleteRejected && submissionRetained);
62
+ if (submissionRetained)
58
63
  await api.deleteFormSubmission(submission);
59
- let missing = false;
60
- try {
61
- await api.getFormSubmission(submission);
62
- }
63
- catch {
64
- missing = true;
65
- }
66
- check('trashed submission detail is unavailable', missing);
67
- check('trashed submission is absent from active pages', !(await api.listFormSubmissions({ formId: submission.formId })).items.some(item => item.id === submission.submissionId));
68
- }
69
- return { passed, failures };
64
+ check('trashed submission detail is unavailable', await rejects({ operation: () => api.getFormSubmission(submission) }));
65
+ check('trashed submission is absent from active pages', !(await api.listFormSubmissions({ formId: submission.formId })).items.some(item => item.id === submission.submissionId));
70
66
  }
71
67
  //# sourceMappingURL=forms-api.conformance.js.map
@@ -23,6 +23,20 @@ function defaultKindOf(kind) {
23
23
  return "temporal";
24
24
  return kind.control === "textarea" ? "textarea" : "text";
25
25
  }
26
+ const isNumeric = ({ kind }) => kind.htmlType === "number" || kind.htmlType === "range";
27
+ /** Native input type for a kind's min/max; number and range share numeric bounds. */
28
+ function boundsOf({ kind }) {
29
+ if (!BOUNDS.has(kind.htmlType))
30
+ return null;
31
+ return isNumeric({ kind }) ? "number" : kind.htmlType;
32
+ }
33
+ /** Whether a (non-hidden) kind shows a placeholder. */
34
+ function takesPlaceholder({ kind }) {
35
+ // A single-choice select shows its placeholder as the empty first option.
36
+ if (kind.control === "select")
37
+ return !kind.multiple;
38
+ return kind.textual === true || kind.htmlType === "number";
39
+ }
26
40
  /**
27
41
  * Settings for one type, from its registry kind. Unknown (legacy or host-added) types get help
28
42
  * text only, so the editor never offers a setting the server would refuse.
@@ -33,15 +47,13 @@ export function fieldSettingsFor({ type }, _optional = {}) {
33
47
  if (!kind)
34
48
  return NO_SETTINGS;
35
49
  const hidden = kind.htmlType === "hidden";
36
- const numeric = kind.htmlType === "number" || kind.htmlType === "range";
37
- const bounds = BOUNDS.has(kind.htmlType) ? (numeric ? "number" : kind.htmlType) : null;
50
+ const numeric = isNumeric({ kind });
38
51
  return {
39
- // A single-choice select shows its placeholder as the empty first option.
40
- placeholder: !hidden && ((kind.textual === true && kind.control !== "select") || kind.htmlType === "number" || (kind.control === "select" && !kind.multiple)),
52
+ placeholder: !hidden && takesPlaceholder({ kind }),
41
53
  helpText: !hidden,
42
54
  length: kind.textual === true,
43
55
  pattern: kind.textual === true && !hidden,
44
- bounds,
56
+ bounds: boundsOf({ kind }),
45
57
  step: numeric,
46
58
  options: kind.choices === true,
47
59
  multiple: kind.multiple === true,
@@ -96,53 +108,45 @@ const SEED_OPTIONS = [{ value: "Option 1", label: "Option 1" }, { value: "Option
96
108
  export function retypeField({ field, type }, _optional = {}) {
97
109
  const before = fieldSettingsFor({ type: field.type });
98
110
  const after = fieldSettingsFor({ type });
99
- const next = { id: field.id, label: field.label, type, required: after.helpText ? field.required : false };
100
- if (after.helpText && field.helpText !== undefined)
101
- next.helpText = field.helpText;
102
- if (field.className !== undefined)
103
- next.className = field.className;
104
- if (field.attributes !== undefined)
105
- next.attributes = { ...field.attributes };
106
- if (after.placeholder && field.placeholder !== undefined)
107
- next.placeholder = field.placeholder;
108
- if (after.length) {
109
- if (field.maxLength != null)
110
- next.maxLength = field.maxLength;
111
- if (field.minLength !== undefined)
112
- next.minLength = field.minLength;
113
- }
114
- if (after.pattern && field.pattern !== undefined)
115
- next.pattern = field.pattern;
116
- if (after.bounds && after.bounds === before.bounds) {
117
- if (field.min !== undefined)
118
- next.min = field.min;
119
- if (field.max !== undefined)
120
- next.max = field.max;
121
- }
122
- if (after.step && field.step !== undefined)
123
- next.step = field.step;
124
- if (after.options)
125
- next.options = field.options?.length ? field.options.map((option) => ({ ...option })) : SEED_OPTIONS.map((option) => ({ ...option }));
126
- if (after.upload) {
127
- if (field.accept !== undefined)
128
- next.accept = [...field.accept];
129
- if (field.maxFileSize !== undefined)
130
- next.maxFileSize = field.maxFileSize;
111
+ return {
112
+ id: field.id, label: field.label, type, required: after.helpText ? field.required : false,
113
+ ...setKeys({ field, keys: retainedKeys({ field, before, after }) }),
114
+ ...(after.options ? { options: (field.options?.length ? field.options : SEED_OPTIONS).map((option) => ({ ...option })) } : {}),
115
+ ...setKeys({ field, keys: after.upload ? ["accept", "maxFileSize"] : [] }),
116
+ // A default only survives when its shape is unchanged (text to text, one choice to one choice).
117
+ ...(keepsDefault({ before, after }) && field.default !== undefined ? { default: field.default } : {}),
118
+ };
119
+ }
120
+ const when = (applies, keys) => (applies ? keys : []);
121
+ /** The settings before the options that a retype keeps, in the field's key order. */
122
+ function retainedKeys({ field, before, after }) {
123
+ return [
124
+ ...when(after.helpText, ["helpText"]), "className", "attributes", ...when(after.placeholder, ["placeholder"]),
125
+ // A null max length (the wire's "cleared") is dropped rather than carried.
126
+ ...when(after.length && field.maxLength != null, ["maxLength"]), ...when(after.length, ["minLength"]),
127
+ ...when(after.pattern, ["pattern"]),
128
+ ...when(after.bounds !== null && after.bounds === before.bounds, ["min", "max"]),
129
+ ...when(after.step, ["step"]),
130
+ ];
131
+ }
132
+ const keepsDefault = ({ before, after }) => after.defaultKind !== null && after.defaultKind === before.defaultKind && after.bounds === before.bounds;
133
+ /** The listed keys the field has set, in order; attribute maps and accept lists are copied, not shared. */
134
+ function setKeys({ field, keys }) {
135
+ const kept = {};
136
+ for (const key of keys) {
137
+ const value = field[key];
138
+ if (value === undefined)
139
+ continue;
140
+ Object.assign(kept, { [key]: Array.isArray(value) ? [...value] : typeof value === "object" && value !== null ? { ...value } : value });
131
141
  }
132
- // A default only survives when its shape is unchanged (text to text, one choice to one choice).
133
- if (field.default !== undefined && after.defaultKind !== null && after.defaultKind === before.defaultKind && after.bounds === before.bounds)
134
- next.default = field.default;
135
- return next;
142
+ return kept;
136
143
  }
137
144
  const text = (value) => (value === undefined || value === null ? "" : String(value));
138
145
  /** Seed the draft from a stored field. @complexity O(o) in options. */
139
146
  export function settingsFormFromField({ field }, _optional = {}) {
140
- const defaults = field.default;
141
147
  return {
142
148
  placeholder: text(field.placeholder), helpText: text(field.helpText),
143
- defaultText: typeof defaults === "boolean" || Array.isArray(defaults) ? "" : text(defaults),
144
- defaultChecked: defaults === true || (field.checkboxValue !== undefined && defaults === field.checkboxValue),
145
- defaultChoices: Array.isArray(defaults) ? [...defaults] : typeof defaults === "string" && fieldSettingsFor({ type: field.type }).options ? [defaults] : [],
149
+ ...defaultDraft({ field }),
146
150
  minLength: text(field.minLength), maxLength: text(field.maxLength), pattern: text(field.pattern),
147
151
  min: text(field.min), max: text(field.max), step: text(field.step),
148
152
  options: (field.options ?? []).map((option, index) => ({ _rowId: index, value: option.value, label: option.label })),
@@ -150,6 +154,21 @@ export function settingsFormFromField({ field }, _optional = {}) {
150
154
  maxFileSizeMb: field.maxFileSize === undefined ? "" : String(Math.round((field.maxFileSize / 1024 / 1024) * 100) / 100),
151
155
  };
152
156
  }
157
+ /** The stored default as the dialog's three default controls. */
158
+ function defaultDraft({ field }) {
159
+ const defaults = field.default;
160
+ return {
161
+ defaultText: typeof defaults === "boolean" || Array.isArray(defaults) ? "" : text(defaults),
162
+ defaultChecked: defaults === true || (field.checkboxValue !== undefined && defaults === field.checkboxValue),
163
+ defaultChoices: defaultChoicesOf({ field }),
164
+ };
165
+ }
166
+ function defaultChoicesOf({ field }) {
167
+ const defaults = field.default;
168
+ if (Array.isArray(defaults))
169
+ return [...defaults];
170
+ return typeof defaults === "string" && fieldSettingsFor({ type: field.type }).options ? [defaults] : [];
171
+ }
153
172
  /**
154
173
  * Edit one option row. A value that still mirrors its label follows label edits, so typing labels
155
174
  * is enough for most forms; an edited value stays put.
@@ -199,59 +218,74 @@ function optionalNumber(value, error, { integer = false } = {}) {
199
218
  return Number.isFinite(number) && (!integer || (Number.isInteger(number) && number >= 0)) ? { ok: true, value: number } : { ok: false, error };
200
219
  }
201
220
  /** Text length, pattern and placeholder settings. */
202
- function textPatch(form, settings) {
203
- const patch = { placeholder: settings.placeholder && !isBlank(form.placeholder) ? form.placeholder : undefined, pattern: undefined, minLength: undefined };
204
- if (settings.length) {
205
- const min = optionalNumber(form.minLength, "Length must be a whole number.", { integer: true });
206
- const max = optionalNumber(form.maxLength, "Length must be a whole number.", { integer: true });
207
- if (!min.ok)
208
- return min;
209
- if (!max.ok)
210
- return max;
211
- if (min.value !== undefined && max.value !== undefined && min.value > max.value)
212
- return { ok: false, error: "Minimum must not be above maximum." };
213
- patch.minLength = min.value;
214
- // The wire contract clears a text length with null; unsupported types omit the key entirely.
215
- patch.maxLength = max.value ?? null;
216
- }
217
- if (settings.pattern && !isBlank(form.pattern)) {
218
- // Browser-safe too: a `v`-flag-invalid pattern is silently ignored by browsers (2026-10-11).
219
- if (!isBrowserSafeFieldPattern({ pattern: form.pattern }))
220
- return { ok: false, error: "That pattern isn't supported." };
221
- patch.pattern = form.pattern;
222
- }
223
- return { ok: true, value: patch };
221
+ function textPatch({ form, settings }) {
222
+ const length = settings.length ? lengthPatch({ form }) : { ok: true, value: {} };
223
+ if (!length.ok)
224
+ return length;
225
+ const pattern = settings.pattern ? patternOf({ form }) : { ok: true, value: undefined };
226
+ if (!pattern.ok)
227
+ return pattern;
228
+ return { ok: true, value: {
229
+ placeholder: settings.placeholder && !isBlank(form.placeholder) ? form.placeholder : undefined,
230
+ pattern: pattern.value, minLength: undefined, ...length.value,
231
+ } };
232
+ }
233
+ const MIN_ABOVE_MAX = { ok: false, error: "Minimum must not be above maximum." };
234
+ const isAbove = (min, max) => min !== undefined && max !== undefined && min > max;
235
+ function lengthPatch({ form }) {
236
+ const min = optionalNumber(form.minLength, "Length must be a whole number.", { integer: true });
237
+ const max = optionalNumber(form.maxLength, "Length must be a whole number.", { integer: true });
238
+ if (!min.ok)
239
+ return min;
240
+ if (!max.ok)
241
+ return max;
242
+ if (isAbove(min.value, max.value))
243
+ return MIN_ABOVE_MAX;
244
+ // The wire contract clears a text length with null; unsupported types omit the key entirely.
245
+ return { ok: true, value: { minLength: min.value, maxLength: max.value ?? null } };
246
+ }
247
+ function patternOf({ form }) {
248
+ if (isBlank(form.pattern))
249
+ return { ok: true, value: undefined };
250
+ // Browser-safe too: a `v`-flag-invalid pattern is silently ignored by browsers (2026-10-11).
251
+ if (!isBrowserSafeFieldPattern({ pattern: form.pattern }))
252
+ return { ok: false, error: "That pattern isn't supported." };
253
+ return { ok: true, value: form.pattern };
224
254
  }
225
255
  /** Numeric or date/time bounds, plus a numeric step. Native date/time strings compare as text. */
226
- function boundsPatch(form, settings) {
227
- const patch = { min: undefined, max: undefined, step: undefined };
228
- if (settings.bounds === "number") {
229
- const min = optionalNumber(form.min, "Enter numbers for min, max and step.");
230
- const max = optionalNumber(form.max, "Enter numbers for min, max and step.");
231
- const step = optionalNumber(form.step, "Enter numbers for min, max and step.");
232
- if (!min.ok)
233
- return min;
234
- if (!max.ok)
235
- return max;
236
- if (!step.ok)
237
- return step;
238
- if (step.value !== undefined && step.value <= 0)
239
- return { ok: false, error: "Step must be above 0." };
240
- if (min.value !== undefined && max.value !== undefined && min.value > max.value)
241
- return { ok: false, error: "Minimum must not be above maximum." };
242
- return { ok: true, value: { min: min.value, max: max.value, step: step.value } };
243
- }
244
- if (settings.bounds) {
245
- const min = isBlank(form.min) ? undefined : form.min;
246
- const max = isBlank(form.max) ? undefined : form.max;
247
- if (min !== undefined && max !== undefined && min > max)
248
- return { ok: false, error: "Minimum must not be above maximum." };
249
- return { ok: true, value: { min, max } };
250
- }
251
- return { ok: true, value: patch };
256
+ function boundsPatch({ form, settings }) {
257
+ if (settings.bounds === "number")
258
+ return numberBoundsPatch({ form });
259
+ if (settings.bounds)
260
+ return textBoundsPatch({ form });
261
+ return { ok: true, value: { min: undefined, max: undefined, step: undefined } };
262
+ }
263
+ function numberBoundsPatch({ form }) {
264
+ const error = "Enter numbers for min, max and step.";
265
+ const min = optionalNumber(form.min, error);
266
+ const max = optionalNumber(form.max, error);
267
+ const step = optionalNumber(form.step, error);
268
+ if (!min.ok)
269
+ return min;
270
+ if (!max.ok)
271
+ return max;
272
+ if (!step.ok)
273
+ return step;
274
+ if (step.value !== undefined && step.value <= 0)
275
+ return { ok: false, error: "Step must be above 0." };
276
+ if (isAbove(min.value, max.value))
277
+ return MIN_ABOVE_MAX;
278
+ return { ok: true, value: { min: min.value, max: max.value, step: step.value } };
279
+ }
280
+ function textBoundsPatch({ form }) {
281
+ const min = isBlank(form.min) ? undefined : form.min;
282
+ const max = isBlank(form.max) ? undefined : form.max;
283
+ if (isAbove(min, max))
284
+ return MIN_ABOVE_MAX;
285
+ return { ok: true, value: { min, max } };
252
286
  }
253
287
  /** Choice options: blank rows are skipped; the rest need a label and a unique value. */
254
- function optionsPatch(form, settings) {
288
+ function optionsPatch({ form, settings }) {
255
289
  if (!settings.options)
256
290
  return { ok: true, value: undefined };
257
291
  const options = form.options.filter((row) => !(isBlank(row.value) && isBlank(row.label))).map((row) => ({ value: row.value.trim(), label: row.label.trim() }));
@@ -262,26 +296,27 @@ function optionsPatch(form, settings) {
262
296
  }
263
297
  return { ok: true, value: options };
264
298
  }
299
+ const typedDefault = ({ form }) => ({ ok: true, value: isBlank(form.defaultText) ? undefined : form.defaultText });
300
+ const DEFAULT_READERS = {
301
+ // A box with an authored value submits that value, so its "checked" default is that value too.
302
+ boolean: ({ form, checkboxValue }) => ({ ok: true, value: form.defaultChecked ? checkboxValue ?? true : undefined }),
303
+ choice: ({ form, offered }) => ({ ok: true, value: form.defaultChoices.find((value) => offered.has(value)) }),
304
+ choices: ({ form, offered }) => {
305
+ const chosen = form.defaultChoices.filter((value) => offered.has(value));
306
+ return { ok: true, value: chosen.length ? chosen : undefined };
307
+ },
308
+ number: ({ form }) => optionalNumber(form.defaultText, "Default must be a number."),
309
+ text: typedDefault, textarea: typedDefault, temporal: typedDefault,
310
+ };
265
311
  /** The default in the stored shape for its kind; choices drop values no longer offered. */
266
- function defaultValue(form, settings, { options, checkboxValue }) {
312
+ function defaultValue({ form, settings }, { options, checkboxValue }) {
313
+ if (settings.defaultKind === null)
314
+ return { ok: true, value: undefined };
267
315
  const offered = new Set((options ?? []).map((option) => option.value));
268
- switch (settings.defaultKind) {
269
- // A box with an authored value submits that value, so its "checked" default is that value too.
270
- case "boolean": return { ok: true, value: form.defaultChecked ? checkboxValue ?? true : undefined };
271
- case "choice": return { ok: true, value: form.defaultChoices.find((value) => offered.has(value)) };
272
- case "choices": {
273
- const chosen = form.defaultChoices.filter((value) => offered.has(value));
274
- return { ok: true, value: chosen.length ? chosen : undefined };
275
- }
276
- case "number": return optionalNumber(form.defaultText, "Default must be a number.");
277
- case "text":
278
- case "textarea":
279
- case "temporal": return { ok: true, value: isBlank(form.defaultText) ? undefined : form.defaultText };
280
- default: return { ok: true, value: undefined };
281
- }
316
+ return DEFAULT_READERS[settings.defaultKind]({ form, offered, checkboxValue });
282
317
  }
283
318
  /** Upload limits: no boxes ticked means every safe type; size is whole megabytes up to the cap. */
284
- function uploadPatch(form, settings) {
319
+ function uploadPatch({ form, settings }) {
285
320
  if (!settings.upload)
286
321
  return { ok: true, value: {} };
287
322
  const size = optionalNumber(form.maxFileSizeMb, "Max size must be up to 10 MB.");
@@ -302,19 +337,19 @@ function uploadPatch(form, settings) {
302
337
  */
303
338
  export function buildFieldSettingsPatch({ form, field }, _optional = {}) {
304
339
  const settings = fieldSettingsFor({ type: field.type });
305
- const textual = textPatch(form, settings);
340
+ const textual = textPatch({ form, settings });
306
341
  if (!textual.ok)
307
342
  return textual;
308
- const bounds = boundsPatch(form, settings);
343
+ const bounds = boundsPatch({ form, settings });
309
344
  if (!bounds.ok)
310
345
  return bounds;
311
- const options = optionsPatch(form, settings);
346
+ const options = optionsPatch({ form, settings });
312
347
  if (!options.ok)
313
348
  return options;
314
- const fallback = defaultValue(form, settings, { options: options.value, checkboxValue: field.checkboxValue });
349
+ const fallback = defaultValue({ form, settings }, { options: options.value, checkboxValue: field.checkboxValue });
315
350
  if (!fallback.ok)
316
351
  return fallback;
317
- const upload = uploadPatch(form, settings);
352
+ const upload = uploadPatch({ form, settings });
318
353
  if (!upload.ok)
319
354
  return upload;
320
355
  return { ok: true, patch: {