@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,47 +1,77 @@
1
- /** Creates/deletes named test credentials: run only on a disposable authorized backend.
2
- * Supply connectionFields for providers with required fields beyond the canonical token. */
3
- export async function runSourceControlApiConformance({ api, providerId }, { connectionFields = {} } = {}) {
4
- const checks = [], ids = [];
5
- const assert = (ok, name) => { if (!ok)
6
- throw new Error(`Source control conformance: ${name}`); checks.push(name); };
7
- async function rejects(run, name) { let rejected = false; try {
8
- await run();
1
+ /** Records `name` as passed, or throws naming the provider and the failed check. */
2
+ function pass({ run, ok, name }) {
3
+ if (!ok)
4
+ throw new Error(`Source control conformance failed for provider "${run.providerId}": ${name}`);
5
+ run.checks.push(name);
6
+ }
7
+ /** Passes `name` only when `attempt` rejects; the rejection reason itself is irrelevant to the contract. */
8
+ async function refuses({ run, attempt, name }) {
9
+ let rejected = false;
10
+ try {
11
+ await attempt();
9
12
  }
10
13
  catch {
11
14
  rejected = true;
12
- } assert(rejected, name); }
13
- const connection = { ...connectionFields, providerId, token: 'conformance-only-token' };
15
+ }
16
+ pass({ run, ok: rejected, name });
17
+ }
18
+ async function checkCreate({ run }) {
19
+ const { api, providerId, connection } = run;
20
+ pass({ run, ok: (await api.providers({})).some(p => p.id === providerId), name: 'provider is listed' });
21
+ const first = await api.create({ label: 'conformance-first', connection });
22
+ run.ids.push(first.id);
23
+ pass({ run, ok: first.configured && !!first.id, name: 'create returns configured summary' });
24
+ pass({ run, ok: !('token' in first) && !('connection' in first), name: 'saved secrets never read back' });
25
+ pass({ run, ok: (await api.list({})).some(r => r.id === first.id), name: 'list contains created row' });
26
+ await refuses({ run, attempt: () => api.create({ label: 'conformance-first', connection }), name: 'duplicate label rejects' });
27
+ return first;
28
+ }
29
+ async function checkDefaultSlot({ run }) {
30
+ const { api, providerId, connection } = run;
31
+ const second = await api.create({ label: 'conformance-second', connection, isDefault: true });
32
+ run.ids.push(second.id);
33
+ const list = await api.list({});
34
+ pass({ run, ok: list.filter(r => r.providerId === providerId && r.isDefault).length === 1 && list.find(r => r.id === second.id)?.isDefault === true, name: 'server owns unique default slot' });
35
+ }
36
+ async function checkUpdates({ run, first }) {
37
+ const { api, providerId, connection } = run;
38
+ const renamed = await api.update({ id: first.id, patch: { label: 'conformance-renamed' } });
39
+ pass({ run, ok: renamed.label === 'conformance-renamed' && renamed.configured, name: 'metadata-only update preserves configured credential' });
40
+ pass({ run, ok: first.label === 'conformance-first', name: 'prior snapshots remain immutable' });
41
+ const replaced = await api.update({ id: first.id, patch: { connection } });
42
+ pass({ run, ok: replaced.id === first.id && replaced.label === renamed.label, name: 'replacement retains identity and label' });
43
+ await refuses({ run, attempt: () => api.update({ id: first.id, patch: { connection: { providerId, token: '' } } }), name: 'blank replacement rejects' });
44
+ return renamed;
45
+ }
46
+ async function checkAborts({ run, first, renamed }) {
47
+ const { api } = run;
48
+ const abort = new AbortController();
49
+ abort.abort();
50
+ await refuses({ run, attempt: () => api.list({}, { signal: abort.signal }), name: 'aborted list rejects' });
51
+ await refuses({ run, attempt: () => api.update({ id: first.id, patch: { label: 'bad' } }, { signal: abort.signal }), name: 'aborted write rejects' });
52
+ pass({ run, ok: (await api.list({})).find(r => r.id === first.id)?.label === renamed.label, name: 'aborted write has no effects' });
53
+ }
54
+ async function checkRemove({ run, first }) {
55
+ const { api } = run;
56
+ await api.remove({ id: first.id });
57
+ await api.remove({ id: first.id });
58
+ pass({ run, ok: !(await api.list({})).some(r => r.id === first.id), name: 'remove is idempotent' });
59
+ await refuses({ run, attempt: () => api.update({ id: first.id, patch: {} }), name: 'missing update rejects' });
60
+ }
61
+ /** Creates/deletes named test credentials: run only on a disposable authorized backend.
62
+ * Supply connectionFields for providers with required fields beyond the canonical token. */
63
+ export async function runSourceControlApiConformance({ api, providerId }, { connectionFields = {} } = {}) {
64
+ const run = { api, providerId, connection: { ...connectionFields, providerId, token: 'conformance-only-token' }, ids: [], checks: [] };
14
65
  try {
15
- assert((await api.providers({})).some(p => p.id === providerId), 'provider is listed');
16
- const first = await api.create({ label: 'conformance-first', connection });
17
- ids.push(first.id);
18
- assert(first.configured && !!first.id, 'create returns configured summary');
19
- assert(!('token' in first) && !('connection' in first), 'saved secrets never read back');
20
- assert((await api.list({})).some(r => r.id === first.id), 'list contains created row');
21
- await rejects(() => api.create({ label: 'conformance-first', connection }), 'duplicate label rejects');
22
- const second = await api.create({ label: 'conformance-second', connection, isDefault: true });
23
- ids.push(second.id);
24
- const list = await api.list({});
25
- assert(list.filter(r => r.providerId === providerId && r.isDefault).length === 1 && list.find(r => r.id === second.id)?.isDefault === true, 'server owns unique default slot');
26
- const renamed = await api.update({ id: first.id, patch: { label: 'conformance-renamed' } });
27
- assert(renamed.label === 'conformance-renamed' && renamed.configured, 'metadata-only update preserves configured credential');
28
- assert(first.label === 'conformance-first', 'prior snapshots remain immutable');
29
- const replaced = await api.update({ id: first.id, patch: { connection } });
30
- assert(replaced.id === first.id && replaced.label === renamed.label, 'replacement retains identity and label');
31
- await rejects(() => api.update({ id: first.id, patch: { connection: { providerId, token: '' } } }), 'blank replacement rejects');
32
- const abort = new AbortController();
33
- abort.abort();
34
- await rejects(() => api.list({}, { signal: abort.signal }), 'aborted list rejects');
35
- await rejects(() => api.update({ id: first.id, patch: { label: 'bad' } }, { signal: abort.signal }), 'aborted write rejects');
36
- assert((await api.list({})).find(r => r.id === first.id)?.label === renamed.label, 'aborted write has no effects');
37
- await api.remove({ id: first.id });
38
- await api.remove({ id: first.id });
39
- assert(!(await api.list({})).some(r => r.id === first.id), 'remove is idempotent');
40
- await rejects(() => api.update({ id: first.id, patch: {} }), 'missing update rejects');
41
- return Object.freeze(checks);
66
+ const first = await checkCreate({ run });
67
+ await checkDefaultSlot({ run });
68
+ const renamed = await checkUpdates({ run, first });
69
+ await checkAborts({ run, first, renamed });
70
+ await checkRemove({ run, first });
71
+ return Object.freeze(run.checks);
42
72
  }
43
73
  finally {
44
- for (const id of ids)
74
+ for (const id of run.ids)
45
75
  await api.remove({ id });
46
76
  }
47
77
  }
@@ -1,5 +1,16 @@
1
1
  import { createControllerStore } from '../../core/module/controller-store.js';
2
2
  import { blankSourceControlDraft, sourceControlProviders, defaultSourceControlCredential, sourceControlReady, buildSourceControlConnection, sourceControlSummary, sourceControlErrorMessage } from '../rules.js';
3
+ /** The provider and draft a save would submit, or null when the loaded state cannot accept a save for `providerId`. */
4
+ function readySaveTarget({ state, providerId }) {
5
+ const draft = state.drafts[providerId] ?? blankSourceControlDraft({});
6
+ const info = sourceControlProviders({ providers: state.providers, credentials: state.credentials }).find(p => p.id === providerId);
7
+ return state.loaded && !draft.saving && info && sourceControlReady({ info, draft }) ? { info, draft } : null;
8
+ }
9
+ /** Replaces the row with `summary`'s id in place, or appends `summary` when it is new. */
10
+ function spliceCredential({ credentials, summary }) {
11
+ // Preserve server order: the first row is the defensive default when no row is marked default.
12
+ return Object.freeze(credentials.some(c => c.id === summary.id) ? credentials.map(c => c.id === summary.id ? summary : c) : [...credentials, summary]);
13
+ }
3
14
  export function createSourceControlController({ api, permissions = [] }, _optional = {}) {
4
15
  const grants = new Set(permissions), store = createControllerStore({ initial: { providers: [], credentials: [], drafts: {}, loading: false, loaded: false, error: null, catalogError: null } });
5
16
  let generation = 0, credentialRevision = 0;
@@ -35,22 +46,23 @@ export function createSourceControlController({ api, permissions = [] }, _option
35
46
  if (ticket === generation)
36
47
  store.set({ patch: { loading: false } });
37
48
  }
49
+ /** Replaces the provider's current default credential, or creates one labelled 'default'. */
50
+ function writeCredential({ current, connection }) {
51
+ return current ? api.update({ id: current.id, patch: { connection } }, { signal: store.signal }) : api.create({ label: 'default', connection }, { signal: store.signal });
52
+ }
38
53
  async function save({ providerId }, _optional = {}) {
39
- const state = store.getSnapshot(), draft = state.drafts[providerId] ?? blankSourceControlDraft({});
40
- const info = sourceControlProviders({ providers: state.providers, credentials: state.credentials }).find(p => p.id === providerId);
41
- if (!canWrite() || !state.loaded || draft.saving || !info || !sourceControlReady({ info, draft }))
54
+ const state = store.getSnapshot(), target = canWrite() ? readySaveTarget({ state, providerId }) : null;
55
+ if (!target)
42
56
  return false;
43
- const current = defaultSourceControlCredential({ credentials: state.credentials, providerId }), connection = buildSourceControlConnection({ info, draft });
57
+ const current = defaultSourceControlCredential({ credentials: state.credentials, providerId }), connection = buildSourceControlConnection(target);
44
58
  patchDraft(providerId, { saving: true, error: null });
45
59
  try {
46
- const row = current ? await api.update({ id: current.id, patch: { connection } }, { signal: store.signal }) : await api.create({ label: 'default', connection }, { signal: store.signal });
60
+ const row = await writeCredential({ current, connection });
47
61
  if (store.signal.aborted)
48
62
  return false;
49
63
  credentialRevision++;
50
64
  // One round trip per write: splice the summary, then erase accepted secret drafts immediately.
51
- const credentials = store.getSnapshot().credentials, summary = sourceControlSummary({ row });
52
- // Preserve server order: the first row is the defensive default when no row is marked default.
53
- store.set({ patch: { credentials: Object.freeze(credentials.some(c => c.id === row.id) ? credentials.map(c => c.id === row.id ? summary : c) : [...credentials, summary]) } });
65
+ store.set({ patch: { credentials: spliceCredential({ credentials: store.getSnapshot().credentials, summary: sourceControlSummary({ row }) }) } });
54
66
  patchDraft(providerId, blankSourceControlDraft({}));
55
67
  return true;
56
68
  }
@@ -1,4 +1,5 @@
1
1
  import type { TabViewProps } from '../../../core/react/bind-react.js';
2
+ import type { SourceControlCredential } from '../../models.js';
2
3
  export declare function useProvidersTab({ permissions }: TabViewProps, _optional?: {}): {
3
4
  denied: boolean;
4
5
  loading: boolean;
@@ -6,7 +7,7 @@ export declare function useProvidersTab({ permissions }: TabViewProps, _optional
6
7
  id: string;
7
8
  label: string;
8
9
  heading: string;
9
- saved: import("../../models.js").SourceControlCredential | undefined;
10
+ saved: SourceControlCredential | undefined;
10
11
  defaultOpen: boolean;
11
12
  canEdit: boolean;
12
13
  unlisted: boolean;
@@ -2,19 +2,26 @@ import { useController } from '../../../core/react/use-controller.js';
2
2
  import { createSourceControlController } from '../../controllers/source-control.controller.js';
3
3
  import { blankSourceControlDraft, sourceControlProviders, defaultSourceControlCredential, sourceControlReady, safeSourceControlUrl } from '../../rules.js';
4
4
  import { useSourceControlPorts } from './SourceControlPorts.hooks.js';
5
+ /** Stand-in snapshot until the controller mounts: nothing listed yet, still loading. */
6
+ const UNMOUNTED = { providers: [], credentials: [], drafts: {}, loading: true, loaded: false, error: null, catalogError: null };
7
+ function fieldView({ info, field, draft }, { controller }) {
8
+ return { name: field.name, label: `${info.label} ${field.label}`, value: draft.values[field.name] ?? '', secret: !!field.secret, required: !!field.required, hint: field.userHelp ?? field.hint ?? field.help, onValueChange({ value }) { controller?.setField({ providerId: info.id, name: field.name, value }); } };
9
+ }
10
+ function rowView({ info, context }, { controller }) {
11
+ const { credentials, drafts, first, canWrite } = context;
12
+ const saved = defaultSourceControlCredential({ credentials, providerId: info.id }), draft = drafts[info.id] ?? blankSourceControlDraft({}), tokenPageUrl = safeSourceControlUrl({ url: info.tokenPageUrl });
13
+ return { id: info.id, label: info.label, heading: saved ? info.label : `Connect ${info.label}`, saved, defaultOpen: info.id === first, canEdit: canWrite && info.listed, unlisted: !info.listed, connected: !!saved,
14
+ token: draft.token, tokenLabel: `${info.label} ${info.tokenLabel}`, hint: saved ? 'Leave blank to keep the current token.' : 'Once saved, the token is never displayed again.', saving: draft.saving, error: draft.error,
15
+ disabled: !canWrite || !sourceControlReady({ info, draft }), help: info.help, tokenPageUrl, hasGuidance: !!info.help || !!tokenPageUrl,
16
+ fields: info.fields.map(field => fieldView({ info, field, draft }, { controller })),
17
+ onToken({ value }) { controller?.setToken({ providerId: info.id, value }); }, save() { void controller?.save({ providerId: info.id }); }, };
18
+ }
5
19
  export function useProvidersTab({ permissions = [] }, _optional = {}) {
6
20
  const ports = useSourceControlPorts({}), key = permissions.join('\0'), canRead = permissions.includes('source-control.read'), canWrite = canRead && permissions.includes('source-control.credentials.write');
7
- const { controller, snapshot: state } = useController({ create: () => createSourceControlController({ api: ports.sourceControlApi, permissions }), dependencies: [ports.sourceControlApi, key] }, { start: ({ controller }) => { void controller.load({}); } });
8
- const credentials = state?.credentials ?? [], infos = sourceControlProviders({ providers: state?.providers ?? [], credentials });
21
+ const { controller, snapshot } = useController({ create: () => createSourceControlController({ api: ports.sourceControlApi, permissions }), dependencies: [ports.sourceControlApi, key] }, { start: ({ controller }) => { void controller.load({}); } });
22
+ const state = snapshot ?? UNMOUNTED, credentials = state.credentials, infos = sourceControlProviders({ providers: state.providers, credentials });
9
23
  const first = infos.find(p => p.listed && !defaultSourceControlCredential({ credentials, providerId: p.id }))?.id;
10
- const rows = infos.map(info => {
11
- const saved = defaultSourceControlCredential({ credentials, providerId: info.id }), draft = state?.drafts[info.id] ?? blankSourceControlDraft({});
12
- return { id: info.id, label: info.label, heading: saved ? info.label : `Connect ${info.label}`, saved, defaultOpen: info.id === first, canEdit: canWrite && info.listed, unlisted: !info.listed, connected: !!saved,
13
- token: draft.token, tokenLabel: `${info.label} ${info.tokenLabel}`, hint: saved ? 'Leave blank to keep the current token.' : 'Once saved, the token is never displayed again.', saving: draft.saving, error: draft.error,
14
- disabled: !canWrite || !sourceControlReady({ info, draft }), help: info.help, tokenPageUrl: safeSourceControlUrl({ url: info.tokenPageUrl }), hasGuidance: !!info.help || !!safeSourceControlUrl({ url: info.tokenPageUrl }),
15
- fields: info.fields.map(field => ({ name: field.name, label: `${info.label} ${field.label}`, value: draft.values[field.name] ?? '', secret: !!field.secret, required: !!field.required, hint: field.userHelp ?? field.hint ?? field.help, onValueChange({ value }) { controller?.setField({ providerId: info.id, name: field.name, value }); } })),
16
- onToken({ value }) { controller?.setToken({ providerId: info.id, value }); }, save() { void controller?.save({ providerId: info.id }); }, };
17
- });
18
- return { denied: !canRead, loading: !state || state.loading, rows, errors: [state?.error, state?.catalogError].filter((s) => !!s), reload() { void controller?.load({}); }, canManage: !!ports.sourceControlNavigation, manage() { ports.sourceControlNavigation?.open({ kind: 'source-control' }); } };
24
+ const rows = infos.map(info => rowView({ info, context: { credentials, drafts: state.drafts, first, canWrite } }, { controller }));
25
+ return { denied: !canRead, loading: state.loading, rows, errors: [state.error, state.catalogError].filter((s) => !!s), reload() { void controller?.load({}); }, canManage: !!ports.sourceControlNavigation, manage() { ports.sourceControlNavigation?.open({ kind: 'source-control' }); } };
19
26
  }
20
27
  //# sourceMappingURL=ProvidersTab.hooks.js.map
@@ -9,6 +9,14 @@ export async function runWidgetsApiConformance({ api }, _optional = {}) {
9
9
  catch {
10
10
  rejected = true;
11
11
  } assert(rejected, name); }
12
+ const probe = { api, assert, rejects };
13
+ const { widget, updated } = await checkInstanceLifecycle({ probe });
14
+ await checkRegionPlacements({ probe, widget, updated });
15
+ await checkAbortsAndMissing({ probe, widget, updated });
16
+ await checkTrash({ probe, widget });
17
+ return checks;
18
+ }
19
+ async function checkInstanceLifecycle({ probe: { api, assert, rejects } }, _optional = {}) {
12
20
  const input = { widgetType: 'text', title: 'Conformance widget', config: { body: 'initial' } };
13
21
  const { widget } = await api.createWidget(input);
14
22
  assert(widget.id.length > 0 && widget.slug.length > 0 && widget.status === 'active', 'create active instance');
@@ -22,6 +30,9 @@ export async function runWidgetsApiConformance({ api }, _optional = {}) {
22
30
  assert(updated.title === widget.title && updated.version > widget.version, 'omitted title preserved and version advances');
23
31
  assert(widget.config.body === 'initial' && firstList.widgets.find(w => w.id === widget.id)?.version === widget.version, 'prior snapshots stay detached');
24
32
  await rejects(() => api.updateWidget({ id: widget.id, baseVersion: widget.version, config: {} }), 'stale instance write rejects');
33
+ return { widget, updated };
34
+ }
35
+ async function checkRegionPlacements({ probe: { api, assert, rejects }, widget, updated }, _optional = {}) {
25
36
  const regionKey = `conformance-${widget.id}`;
26
37
  const { area } = await api.bindWidgetRegion({ regionKey });
27
38
  const initialAreaVersion = area.version;
@@ -40,6 +51,8 @@ export async function runWidgetsApiConformance({ api }, _optional = {}) {
40
51
  assert(placed.placements[2]?.broken === true, 'broken reference remains visible');
41
52
  assert((await api.listWidgetRegions({})).regions.find(r => r.regionKey === regionKey)?.placementCount === placements.length, 'region list placement count updates');
42
53
  await rejects(() => api.mutateWidgetRegionPlacements({ regionKey, baseVersion: area.version, placements: [] }), 'stale region write rejects');
54
+ }
55
+ async function checkAbortsAndMissing({ probe: { api, assert, rejects }, widget, updated }, _optional = {}) {
43
56
  const abort = new AbortController();
44
57
  abort.abort();
45
58
  await rejects(() => api.listWidgets({}, { signal: abort.signal }), 'aborted list rejects');
@@ -47,9 +60,10 @@ export async function runWidgetsApiConformance({ api }, _optional = {}) {
47
60
  assert((await api.getWidget({ id: widget.id })).widget.title === widget.title, 'aborted write has no effects');
48
61
  await rejects(() => api.getWidget({ id: 'missing-conformance-widget' }), 'missing instance rejects');
49
62
  await rejects(() => api.getWidgetRegion({ regionKey: 'missing-conformance-region' }), 'missing region rejects');
63
+ }
64
+ async function checkTrash({ probe: { api, assert }, widget }, _optional = {}) {
50
65
  await api.trashWidget({ id: widget.id });
51
66
  assert(!(await api.listWidgets({})).widgets.some(w => w.id === widget.id), 'trash removes active listing');
52
67
  assert((await api.listWidgets({ includeInactive: true })).widgets.find(w => w.id === widget.id)?.status === 'trash', 'trash retained for shared lifecycle');
53
- return checks;
54
68
  }
55
69
  //# sourceMappingURL=widgets-api.conformance.js.map
@@ -14,6 +14,8 @@ export function createWidgetInstanceEditorController({ api, params: initialParam
14
14
  config: isNew ? defaultConfig(params.widgetType ?? 'text') : {}, message: null, error: null, fieldErrors: [],
15
15
  loading: !isNew, saving: false, widgetType: resolveEditorWidgetType({ isNew, queryWidgetType: params.widgetType, widget: null }),
16
16
  } });
17
+ const isLiveLoad = ({ ticket }) => ticket === generation && !store.signal.aborted;
18
+ const isLiveSave = ({ savingFor }) => currentKey() === savingFor && !store.signal.aborted;
17
19
  async function load(_required = {}, _optional = {}) {
18
20
  const ticket = ++generation;
19
21
  const isNew = params.widgetId === null;
@@ -22,27 +24,31 @@ export function createWidgetInstanceEditorController({ api, params: initialParam
22
24
  store.set({ patch: { widget: null, title: '', config: defaultConfig(params.widgetType ?? 'text'), whereUsed: { count: 0, references: [] }, loading: false, widgetType: params.widgetType } });
23
25
  return;
24
26
  }
25
- const requestedId = params.widgetId;
27
+ await loadExisting({ ticket, requestedId: params.widgetId });
28
+ }
29
+ async function loadExisting({ ticket, requestedId }, _optional = {}) {
26
30
  store.set({ patch: { loading: true, error: null } });
27
31
  try {
28
32
  const result = await api.getWidget({ id: requestedId }, { signal: store.signal });
29
- if (ticket !== generation || store.signal.aborted)
30
- return;
31
- store.set({ patch: { widget: result.widget, title: result.widget.title, config: result.widget.config, whereUsed: result.whereUsed, widgetType: result.widget.widgetType } });
32
- // Replace rather than push: an old id bookmark should not add a Back-button stop.
33
- const redirect = widgetSlugRedirectPath({ requestedId, widget: result.widget }, { slugRedirectPath });
34
- if (redirect)
35
- navigate?.(redirect, { replace: true });
33
+ if (isLiveLoad({ ticket }))
34
+ applyLoaded({ requestedId, result });
36
35
  }
37
36
  catch (error) {
38
- if (ticket === generation && !store.signal.aborted)
37
+ if (isLiveLoad({ ticket }))
39
38
  store.set({ patch: { error: describeApiError({ e: widgetApiError({ error }) ?? error, fallback: t('failed to load widget') }) } });
40
39
  }
41
40
  finally {
42
- if (ticket === generation && !store.signal.aborted)
41
+ if (isLiveLoad({ ticket }))
43
42
  store.set({ patch: { loading: false } });
44
43
  }
45
44
  }
45
+ function applyLoaded({ requestedId, result }, _optional = {}) {
46
+ store.set({ patch: { widget: result.widget, title: result.widget.title, config: result.widget.config, whereUsed: result.whereUsed, widgetType: result.widget.widgetType } });
47
+ // Replace rather than push: an old id bookmark should not add a Back-button stop.
48
+ const redirect = widgetSlugRedirectPath({ requestedId, widget: result.widget }, { slugRedirectPath });
49
+ if (redirect)
50
+ navigate?.(redirect, { replace: true });
51
+ }
46
52
  async function save(_required = {}, _optional = {}) {
47
53
  const { widgetType, isNew, widget, title, config } = store.getSnapshot();
48
54
  if (!widgetType)
@@ -50,28 +56,31 @@ export function createWidgetInstanceEditorController({ api, params: initialParam
50
56
  const savingFor = currentKey();
51
57
  store.set({ patch: { saving: true, message: null, error: null, fieldErrors: [] } });
52
58
  try {
53
- if (isNew) {
54
- const { widget: created } = await api.createWidget({ widgetType, title, config }, { signal: store.signal });
55
- // Creation intentionally navigates even if route identity changed while it was in flight.
56
- navigate?.(`/widgets/${created.slug}`);
57
- return;
58
- }
59
- if (!widget)
60
- return;
61
- const { widget: saved } = await api.updateWidget({ id: widget.id, baseVersion: widget.version, title, config }, { signal: store.signal });
62
- if (currentKey() !== savingFor || store.signal.aborted)
63
- return;
64
- store.set({ patch: { widget: saved, title: saved.title, config: saved.config, widgetType: saved.widgetType, message: t('Saved · version {version}').replace('{version}', String(saved.version)) } });
59
+ if (isNew)
60
+ await createAndOpen({ widgetType, title, config });
61
+ else if (widget)
62
+ await updateExisting({ widget, title, config, savingFor });
65
63
  }
66
64
  catch (error) {
67
- if (currentKey() === savingFor && !store.signal.aborted)
65
+ if (isLiveSave({ savingFor }))
68
66
  store.set({ patch: resolveWidgetSaveError({ error }, { t }) });
69
67
  }
70
68
  finally {
71
- if (currentKey() === savingFor && !store.signal.aborted)
69
+ if (isLiveSave({ savingFor }))
72
70
  store.set({ patch: { saving: false } });
73
71
  }
74
72
  }
73
+ async function createAndOpen({ widgetType, title, config }, _optional = {}) {
74
+ const { widget: created } = await api.createWidget({ widgetType, title, config }, { signal: store.signal });
75
+ // Creation intentionally navigates even if route identity changed while it was in flight.
76
+ navigate?.(`/widgets/${created.slug}`);
77
+ }
78
+ async function updateExisting({ widget, title, config, savingFor }, _optional = {}) {
79
+ const { widget: saved } = await api.updateWidget({ id: widget.id, baseVersion: widget.version, title, config }, { signal: store.signal });
80
+ if (!isLiveSave({ savingFor }))
81
+ return;
82
+ store.set({ patch: { widget: saved, title: saved.title, config: saved.config, widgetType: saved.widgetType, message: t('Saved · version {version}').replace('{version}', String(saved.version)) } });
83
+ }
75
84
  return {
76
85
  getSnapshot: store.getSnapshot, subscribe: store.subscribe, load, save,
77
86
  setParams({ params: next }, _optional = {}) {
@@ -6,17 +6,17 @@ import { widgetsEnglish } from '../messages.en.js';
6
6
  export function createWidgetsLibraryController({ api }, { t = widgetsEnglish } = {}) {
7
7
  const store = createControllerStore({ initial: { widgets: null, error: null, skippedCount: 0, skippedIds: [], createType: 'text', pendingTrash: null, trashingId: null } });
8
8
  let generation = 0;
9
+ const isLiveRead = ({ ticket }) => ticket === generation && !store.signal.aborted;
9
10
  async function load(_required = {}, _optional = {}) {
10
11
  const ticket = ++generation;
11
12
  try {
12
13
  // No includeInactive: trashed widgets belong to the shared Trash screen.
13
14
  const result = await api.listWidgets({}, { signal: store.signal });
14
- if (ticket !== generation || store.signal.aborted)
15
- return;
16
- store.set({ patch: { widgets: result.widgets, skippedCount: result.skippedCount ?? 0, skippedIds: result.skippedIds ?? [] } });
15
+ if (isLiveRead({ ticket }))
16
+ store.set({ patch: { widgets: result.widgets, skippedCount: result.skippedCount ?? 0, skippedIds: result.skippedIds ?? [] } });
17
17
  }
18
18
  catch (e) {
19
- if (ticket === generation && !store.signal.aborted)
19
+ if (isLiveRead({ ticket }))
20
20
  store.set({ patch: { error: describeApiError({ e: widgetApiError({ error: e }) ?? e, fallback: t('failed to load widgets') }) } });
21
21
  }
22
22
  }
@@ -30,18 +30,24 @@ export function createWidgetsLibraryController({ api }, { t = widgetsEnglish } =
30
30
  void load();
31
31
  }
32
32
  catch (error) {
33
- const e = widgetApiError({ error });
34
- if (e?.code === 'NOT_FOUND')
35
- void load();
36
- else
37
- store.set({ patch: { error: e?.code === 'TRASH_VERSION_CHANGED' ? t('This item changed since you loaded it. Reload and try again.') : describeApiError({ e: e ?? error, fallback: t('delete failed') }) } });
33
+ handleTrashFailure({ error });
38
34
  }
39
35
  finally {
40
- // A's completion cannot close a newly opened confirmation for B.
41
- const current = store.getSnapshot();
42
- store.set({ patch: { trashingId: current.trashingId === widget.id ? null : current.trashingId, pendingTrash: current.pendingTrash?.id === widget.id ? null : current.pendingTrash } });
36
+ settleTrash({ widget });
43
37
  }
44
38
  }
39
+ function handleTrashFailure({ error }, _optional = {}) {
40
+ const e = widgetApiError({ error });
41
+ if (e?.code === 'NOT_FOUND')
42
+ void load();
43
+ else
44
+ store.set({ patch: { error: e?.code === 'TRASH_VERSION_CHANGED' ? t('This item changed since you loaded it. Reload and try again.') : describeApiError({ e: e ?? error, fallback: t('delete failed') }) } });
45
+ }
46
+ function settleTrash({ widget }, _optional = {}) {
47
+ // A's completion cannot close a newly opened confirmation for B.
48
+ const current = store.getSnapshot();
49
+ store.set({ patch: { trashingId: current.trashingId === widget.id ? null : current.trashingId, pendingTrash: current.pendingTrash?.id === widget.id ? null : current.pendingTrash } });
50
+ }
45
51
  return {
46
52
  getSnapshot: store.getSnapshot, subscribe: store.subscribe, load, confirmTrash,
47
53
  setCreateType({ type }, _optional = {}) { store.set({ patch: { createType: type } }); },
@@ -6,20 +6,21 @@ import { widgetsEnglish } from '../messages.en.js';
6
6
  export function createWidgetRegionEditorController({ api, regionKey: initialKey }, { t = widgetsEnglish } = {}) {
7
7
  const store = createControllerStore({ initial: { area: null, placements: [], message: null, error: null, loading: true, saving: false } });
8
8
  let regionKey = initialKey, generation = 0;
9
+ const isLiveRead = ({ ticket }) => ticket === generation && !store.signal.aborted;
9
10
  async function load(_required = {}, _optional = {}) {
10
11
  const ticket = ++generation;
11
12
  store.set({ patch: { loading: true, error: null, saving: false } });
12
13
  try {
13
14
  const result = await api.getWidgetRegion({ regionKey }, { signal: store.signal });
14
- if (ticket === generation && !store.signal.aborted)
15
+ if (isLiveRead({ ticket }))
15
16
  store.set({ patch: { area: result.area, placements: result.placements } });
16
17
  }
17
18
  catch (error) {
18
- if (ticket === generation && !store.signal.aborted)
19
+ if (isLiveRead({ ticket }))
19
20
  store.set({ patch: { error: describeApiError({ e: widgetApiError({ error }) ?? error, fallback: t('failed to load region') }) } });
20
21
  }
21
22
  finally {
22
- if (ticket === generation && !store.signal.aborted)
23
+ if (isLiveRead({ ticket }))
23
24
  store.set({ patch: { loading: false } });
24
25
  }
25
26
  }
@@ -40,7 +41,7 @@ export function createWidgetRegionEditorController({ api, regionKey: initialKey
40
41
  let stale = false;
41
42
  try {
42
43
  const { area: saved } = await api.mutateWidgetRegionPlacements({ regionKey, baseVersion: area.version, placements: placements.map(p => ({ placementId: p.placementId, widgetEntryId: p.widgetEntryId, enabled: p.enabled })) }, { signal: store.signal });
43
- stale = ticket !== generation || store.signal.aborted;
44
+ stale = !isLiveRead({ ticket });
44
45
  if (stale)
45
46
  return;
46
47
  store.set({ patch: { area: saved, message: t('Saved · version {version}').replace('{version}', String(saved.version)) } });
@@ -48,7 +49,7 @@ export function createWidgetRegionEditorController({ api, regionKey: initialKey
48
49
  void load();
49
50
  }
50
51
  catch (error) {
51
- stale = ticket !== generation || store.signal.aborted;
52
+ stale = !isLiveRead({ ticket });
52
53
  if (!stale)
53
54
  store.set({ patch: { error: resolveWidgetRegionSaveError({ error }, { t }) } });
54
55
  }
@@ -61,6 +61,7 @@ export declare function useWidgetInstanceEditorView({ widgetId, widgetType, widg
61
61
  widgetTypes: readonly WidgetTypeOption[];
62
62
  useWidgetInstanceEditorHook?: typeof useWiredWidgetInstanceEditor | undefined;
63
63
  }, _optional?: Record<string, never>): {
64
+ gate: import("../../rules.js").WidgetEditorGate;
64
65
  guard: import("../../rules.js").WidgetInstanceGuard | null;
65
66
  onBackClick: (event: MouseEvent<HTMLAnchorElement>) => void;
66
67
  isNew: boolean;
@@ -19,12 +19,13 @@ export function useWidgetRegionEditorView({ regionKey, useWidgetRegionEditorHook
19
19
  const state = useWidgetRegionEditorHook(regionKey);
20
20
  return { ...state, placementHandles: buildAgentListHandles({ prefix: 'widget-region-placement', ids: state.placements.map(p => p.placementId) }) };
21
21
  }
22
- import { widgetInstanceGuard } from '../../rules.js';
22
+ import { widgetEditorGate } from '../../rules.js';
23
23
  import { useWiredWidgetInstanceEditor } from './use-widget-instance-editor.hooks.js';
24
24
  /** The back-link cancellation also stops the host's document-level internal-link interceptor. */
25
25
  export function useWidgetInstanceEditorView({ widgetId, widgetType, widgetTypes, useWidgetInstanceEditorHook = useWiredWidgetInstanceEditor }, _optional = {}) {
26
26
  const state = useWidgetInstanceEditorHook({ widgetId, widgetType });
27
- return { ...state, guard: widgetInstanceGuard({ ...state, types: widgetTypes }), onBackClick: (event) => { if (!state.confirmLeave())
27
+ const gate = widgetEditorGate({ ...state, types: widgetTypes });
28
+ return { ...state, gate, guard: gate.guard, onBackClick: (event) => { if (!state.confirmLeave())
28
29
  event.preventDefault(); } };
29
30
  }
30
31
  //# sourceMappingURL=page-views.hooks.js.map
@@ -13,7 +13,7 @@ export interface WidgetRegionsController {
13
13
  import type { AdminWidgetsPort, WidgetsEventsPort } from '../../ports.js';
14
14
  export declare function useWidgetRegions({ api }: {
15
15
  api: AdminWidgetsPort;
16
- }, { t, locale, events, navigate }?: {
16
+ }, { t, events, navigate }?: {
17
17
  t?: Translate | undefined;
18
18
  locale?: string | undefined;
19
19
  events?: WidgetsEventsPort | undefined;
@@ -6,7 +6,8 @@ import { createWidgetRegionsController } from '../../controllers/regions.control
6
6
  import { widgetsEnglish } from '../../messages.en.js';
7
7
  import { WIDGETS_REGIONS_RESOURCE } from '../../rules.js';
8
8
  import { useWidgetsPorts, useWidgetsOptions } from './WidgetsPorts.hooks.js';
9
- export function useWidgetRegions({ api }, { t = widgetsEnglish, locale = 'en', events, navigate } = {}) {
9
+ import { useWidgetsNavigate } from './navigation.hooks.js';
10
+ export function useWidgetRegions({ api }, { t = widgetsEnglish, events, navigate } = {}) {
10
11
  const cached = useWidgetRegionsQuery({ api });
11
12
  const invalidate = useInvalidate();
12
13
  const latest = useRef({ t, navigate });
@@ -14,7 +15,13 @@ export function useWidgetRegions({ api }, { t = widgetsEnglish, locale = 'en', e
14
15
  const { controller, snapshot } = useController({ create: () => createWidgetRegionsController({ api: cached.api }, { t: key => latest.current.t(key), navigate: path => latest.current.navigate?.(path) }), dependencies: [cached.api] }, { start: ({ controller }) => { void controller.load(); } });
15
16
  useEffect(() => { if (!controller)
16
17
  return; return events?.subscribe({ resource: WIDGETS_REGIONS_RESOURCE, onRefresh: () => invalidate({ key: WIDGET_QUERY_KEYS.regions }) }); }, [controller, events, invalidate]);
17
- return { ...(snapshot ?? { regions: null, error: null, newRegionKey: '', binding: false }), regions: cached.query.data?.regions ?? snapshot?.regions ?? null, error: snapshot?.error ?? cached.query.error?.message ?? null, setNewRegionKey: value => controller?.setNewRegionKey({ value }), bind: async () => { await controller?.bind(); }, t };
18
+ return { ...mergeRegionsState({ snapshot, data: cached.query.data, queryError: cached.query.error }), setNewRegionKey: value => controller?.setNewRegionKey({ value }), bind: async () => { await controller?.bind(); }, t };
19
+ }
20
+ const EMPTY_REGIONS_STATE = { regions: null, error: null, newRegionKey: '', binding: false };
21
+ /** Cached query data wins for the list; a controller error wins over a query error. */
22
+ function mergeRegionsState({ snapshot, data, queryError }, _optional = {}) {
23
+ const state = snapshot ?? EMPTY_REGIONS_STATE;
24
+ return { ...state, regions: data?.regions ?? state.regions, error: state.error ?? queryError?.message ?? null };
18
25
  }
19
26
  export function useWiredWidgetRegions() {
20
27
  const { widgetsApi: api, widgetsEvents: events, widgetsNavigation } = useWidgetsPorts();
@@ -22,5 +29,4 @@ export function useWiredWidgetRegions() {
22
29
  const navigate = useWidgetsNavigate({ navigation: widgetsNavigation, navigationBase });
23
30
  return useWidgetRegions({ api }, { t, locale, events, navigate });
24
31
  }
25
- import { useWidgetsNavigate } from './navigation.hooks.js';
26
32
  //# sourceMappingURL=use-widget-regions.hooks.js.map
@@ -14,11 +14,18 @@ export function useWidgetsLibrary({ api }, { t = widgetsEnglish, locale = 'en',
14
14
  const { controller, snapshot } = useController({ create: () => createWidgetsLibraryController({ api: cached.api }, { t: key => latestT.current(key) }), dependencies: [cached.api] }, { start: ({ controller }) => { void controller.load(); } });
15
15
  useEffect(() => { if (!controller)
16
16
  return; return events?.subscribe({ resource: WIDGETS_LIBRARY_RESOURCE, onRefresh: () => invalidate({ key: WIDGET_QUERY_KEYS.library }) }); }, [controller, events, invalidate]);
17
- const state = snapshot ?? { widgets: null, error: null, skippedCount: 0, skippedIds: [], createType: 'text', pendingTrash: null, trashingId: null };
18
- return { ...state, widgets: cached.query.data?.widgets ?? state.widgets, skippedCount: cached.query.data?.skippedCount ?? state.skippedCount, skippedIds: cached.query.data?.skippedIds ?? state.skippedIds, error: state.error ?? cached.query.error?.message ?? null, trashing: state.pendingTrash !== null && state.trashingId === state.pendingTrash.id,
17
+ return { ...mergeLibraryState({ snapshot, data: cached.query.data, queryError: cached.query.error }),
19
18
  setCreateType: type => controller?.setCreateType({ type }), requestTrash: widget => controller?.requestTrash({ widget }),
20
19
  confirmTrash: async () => { await controller?.confirmTrash(); }, cancelTrash: () => controller?.cancelTrash(), t, locale };
21
20
  }
21
+ const EMPTY_LIBRARY_STATE = { widgets: null, error: null, skippedCount: 0, skippedIds: [], createType: 'text', pendingTrash: null, trashingId: null };
22
+ function mergeLibraryState({ snapshot, data, queryError }, _optional = {}) {
23
+ const state = snapshot ?? EMPTY_LIBRARY_STATE;
24
+ return { ...state, ...cachedListFields({ state, data }), error: state.error ?? queryError?.message ?? null, trashing: state.pendingTrash !== null && state.trashingId === state.pendingTrash.id };
25
+ }
26
+ function cachedListFields({ state, data }, _optional = {}) {
27
+ return { widgets: data?.widgets ?? state.widgets, skippedCount: data?.skippedCount ?? state.skippedCount, skippedIds: data?.skippedIds ?? state.skippedIds };
28
+ }
22
29
  export function useWiredWidgetsLibrary() {
23
30
  const { widgetsApi: api, widgetsEvents: events } = useWidgetsPorts();
24
31
  const { t, locale } = useWidgetsOptions();
@@ -18,6 +18,6 @@ export interface WidgetInstanceEditorProps {
18
18
  */
19
19
  useWidgetInstanceEditorHook?: typeof useWiredWidgetInstanceEditor | undefined;
20
20
  }
21
- export declare function WidgetInstanceEditor(props: WidgetInstanceEditorProps): import("react").JSX.Element | null;
21
+ export declare function WidgetInstanceEditor(props: WidgetInstanceEditorProps): import("react").JSX.Element;
22
22
  export default WidgetInstanceEditor;
23
23
  //# sourceMappingURL=WidgetInstanceEditor.d.ts.map
@@ -18,14 +18,13 @@ import { WhereUsedBanner, WidgetInstanceGuardNotice } from '../components/Widget
18
18
  export function WidgetInstanceEditor(props) {
19
19
  const { slots: { ConfigFields: WidgetConfigFields }, widgetTypes } = useWidgetsOptions();
20
20
  const { widgetId, widgetType: queryWidgetType, useWidgetInstanceEditorHook = useWiredWidgetInstanceEditor } = props;
21
- const { isNew, widget, whereUsed, title, setTitle, config, setConfig, message, error, fieldErrors, loading, saving, widgetType, save, confirmLeave, t, locale, guard, onBackClick, } = useWidgetInstanceEditorView({ widgetId, widgetType: queryWidgetType, useWidgetInstanceEditorHook, widgetTypes });
22
- if (guard)
23
- return _jsx(WidgetInstanceGuardNotice, { guard: guard, t: t });
24
- // Unreachable in practice — `widgetInstanceGuard`'s "no-type" case already covers a null
25
- // `widgetType` above — but TS can't see through that opaque function call, so this narrows the
26
- // type for the JSX below rather than asserting it with `!`.
27
- if (!widgetType)
28
- return null;
21
+ const { isNew, widget, whereUsed, title, setTitle, config, setConfig, message, error, fieldErrors, loading, saving, save, confirmLeave, t, locale, gate, onBackClick, } = useWidgetInstanceEditorView({ widgetId, widgetType: queryWidgetType, useWidgetInstanceEditorHook, widgetTypes });
22
+ if (gate.guard !== null)
23
+ return _jsx(WidgetInstanceGuardNotice, { guard: gate.guard, t: t });
24
+ // `widgetInstanceGuard`'s "no-type" case already covers a null `widgetType` above, but TS can't
25
+ // see through that opaque function call; `widgetEditorGate` hands back the narrowed type so the
26
+ // JSX below needs neither a `!` assertion nor a never-taken null check.
27
+ const { widgetType } = gate;
29
28
  // The host slot binds shared-component translations; screen copy uses the module t.
30
29
  const sharedT = t;
31
30
  return (_jsxs("div", { className: "page", children: [_jsxs("div", { className: "page-header page-header-split", ...agentHandle({ handle: "widget-instance-header" }, {