@jini-ai/admin 0.6.15-rc.1 → 0.6.15

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 (104) hide show
  1. package/dist/agent-plugins/adapters/memory.js +9 -3
  2. package/dist/agent-plugins/conformance/agent-plugins-api.conformance.js +1 -1
  3. package/dist/agent-plugins/controllers/agent-plugins.controller.js +14 -5
  4. package/dist/agent-plugins/package-file-tree.js +4 -6
  5. package/dist/agent-plugins/react/components/AgentPluginInspector.js +5 -2
  6. package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +10 -4
  7. package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +8 -5
  8. package/dist/agent-plugins/rules.js +3 -1
  9. package/dist/comments/adapters/http.js +2 -1
  10. package/dist/comments/adapters/memory.js +3 -1
  11. package/dist/comments/conformance/comments-api.conformance.js +3 -2
  12. package/dist/comments/controllers/comment-queue.controller.d.ts +1 -1
  13. package/dist/comments/controllers/comment-queue.controller.js +4 -2
  14. package/dist/core/entities/rules.js +13 -20
  15. package/dist/core/module/create-admin.js +2 -1
  16. package/dist/core/omit-keys.d.ts +7 -0
  17. package/dist/core/omit-keys.js +12 -0
  18. package/dist/core/ports/forms.d.ts +5 -3
  19. package/dist/core/transport/query-suffix.d.ts +3 -0
  20. package/dist/core/transport/query-suffix.js +5 -0
  21. package/dist/forms/adapters/http.js +3 -2
  22. package/dist/forms/adapters/memory.js +3 -2
  23. package/dist/forms/controllers/field-attributes-dialog.controller.js +17 -3
  24. package/dist/forms/controllers/form-fields-editor.controller.js +1 -2
  25. package/dist/forms/controllers/form-submission-detail.controller.js +1 -2
  26. package/dist/forms/controllers/form-submissions.controller.js +1 -2
  27. package/dist/forms/controllers/forms-list.controller.js +1 -2
  28. package/dist/forms/field-settings.js +15 -4
  29. package/dist/forms/html-rules.js +10 -13
  30. package/dist/forms/react/hooks/forms-view.hooks.d.ts +2 -2
  31. package/dist/forms/react/hooks/forms-view.hooks.js +9 -1
  32. package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.js +1 -3
  33. package/dist/forms/react/hooks/use-form-editor.hooks.js +8 -9
  34. package/dist/forms/react/hooks/use-form-submission-detail.hooks.js +1 -4
  35. package/dist/forms/react/hooks/use-form-submissions.hooks.js +1 -5
  36. package/dist/forms/react/hooks/use-forms-list.hooks.js +1 -2
  37. package/dist/forms/rules.js +7 -2
  38. package/dist/media/adapters/memory.js +5 -3
  39. package/dist/media/controllers/library.controller.js +4 -2
  40. package/dist/media/controllers/providers.controller.js +2 -1
  41. package/dist/media/html-attributes.d.ts +1 -8
  42. package/dist/media/html-attributes.js +6 -161
  43. package/dist/media/react/components/MediaCard.js +11 -1
  44. package/dist/media/react/hooks/EditMediaPanel.hooks.js +18 -7
  45. package/dist/media/react/hooks/LibraryTab.hooks.js +3 -2
  46. package/dist/media/react/hooks/ProvidersTab.hooks.js +19 -9
  47. package/dist/media/rules.js +6 -3
  48. package/dist/playground/adapters/memory.js +26 -7
  49. package/dist/playground/conformance/playground-api.conformance.js +6 -3
  50. package/dist/playground/react/hooks/PlaygroundPorts.hooks.js +6 -2
  51. package/dist/react/components/ConfirmButton/ConfirmButton.hooks.js +1 -1
  52. package/dist/react/components/ConfirmButton/ConfirmButton.js +2 -5
  53. package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +2 -2
  54. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +3 -1
  55. package/dist/react/components/RowMenu/RowMenu.hooks.js +4 -8
  56. package/dist/react/components/Sidebar.js +1 -1
  57. package/dist/react/entities/EntityField.js +1 -0
  58. package/dist/react/entities/contribution.js +2 -1
  59. package/dist/react/entities/types.js +1 -1
  60. package/dist/react/overlays.js +2 -1
  61. package/dist/react/shell/AdminShell.hooks.js +1 -1
  62. package/dist/react/shell/use-admin-shell-session.js +2 -1
  63. package/dist/react/shell/use-admin-theme.js +6 -6
  64. package/dist/redirects/adapters/memory.js +16 -13
  65. package/dist/security/adapters/http.js +10 -11
  66. package/dist/security/adapters/memory.js +50 -15
  67. package/dist/security/conformance/security-api.conformance.js +6 -3
  68. package/dist/security/controllers/access-tokens.controller.js +7 -3
  69. package/dist/security/controllers/other-credentials.controller.js +4 -2
  70. package/dist/security/controllers/root-key.controller.js +12 -5
  71. package/dist/security/react/hooks/AccessTokensTab.hooks.js +8 -3
  72. package/dist/security/react/hooks/SecurityPorts.hooks.js +6 -2
  73. package/dist/security/rules.js +19 -5
  74. package/dist/seo/adapters/memory.js +5 -2
  75. package/dist/seo/controllers/seo-entry.controller.js +2 -1
  76. package/dist/seo/controllers/sitemap.controller.js +2 -1
  77. package/dist/seo/react/hooks/MediaPickerBridge.hooks.js +1 -1
  78. package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -21
  79. package/dist/seo/react/hooks/SeoPage.hooks.js +0 -8
  80. package/dist/seo/react/hooks/use-seo.hooks.js +1 -1
  81. package/dist/seo/react/hooks/use-share-image-warning.hooks.js +1 -1
  82. package/dist/seo/react/hooks/use-sitemap-modal.hooks.js +9 -1
  83. package/dist/seo/react/pages/SeoPage.js +17 -3
  84. package/dist/source-control/adapters/memory.js +30 -14
  85. package/dist/source-control/controllers/source-control.controller.js +5 -2
  86. package/dist/source-control/react/components/SourceControlProviderRow.js +6 -1
  87. package/dist/source-control/react/hooks/SourceControlPorts.hooks.js +6 -2
  88. package/dist/source-control/rules.js +9 -3
  89. package/dist/widgets/adapters/http.js +2 -1
  90. package/dist/widgets/adapters/memory.js +4 -2
  91. package/dist/widgets/conformance/widgets-api.conformance.js +5 -2
  92. package/dist/widgets/controllers/instance-editor.controller.js +3 -2
  93. package/dist/widgets/controllers/library.controller.js +2 -1
  94. package/dist/widgets/controllers/region-editor.controller.js +2 -1
  95. package/dist/widgets/controllers/regions.controller.js +2 -1
  96. package/dist/widgets/react/components/WidgetInstanceNotices.d.ts +1 -1
  97. package/dist/widgets/react/components/WidgetInstanceNotices.js +2 -0
  98. package/dist/widgets/react/hooks/use-widget-regions.hooks.js +5 -2
  99. package/dist/widgets/react/hooks/use-widgets-library.hooks.js +5 -2
  100. package/dist/widgets/react/pages/WidgetInstanceEditor.js +1 -1
  101. package/dist/widgets/react/pages/WidgetRegionEditor.js +1 -1
  102. package/dist/widgets/react/pages/WidgetsLibrary.js +1 -1
  103. package/dist/widgets/rules.js +5 -1
  104. package/package.json +13 -13
@@ -21,7 +21,8 @@ export function createOtherCredentialsController({ api, permissions = [] }, _opt
21
21
  async function load(_required, _optional = {}) {
22
22
  if (store.signal.aborted || !grants.has('security.read'))
23
23
  return;
24
- const ticket = ++generation;
24
+ generation += 1;
25
+ const ticket = generation;
25
26
  store.set({ patch: { loading: true } });
26
27
  await Promise.all(OTHER_STORES.map(id => read(id, ticket)));
27
28
  if (ticket === generation)
@@ -31,7 +32,8 @@ export function createOtherCredentialsController({ api, permissions = [] }, _opt
31
32
  // This is tab-local; cross-session races still require a server concurrency check.
32
33
  let lane = Promise.resolve();
33
34
  function write(row, remove) {
34
- if (store.signal.aborted || !grants.has('security.read') || !grants.has(otherWritePermission({ store: row.store })) || (!remove && !row.supportsReplace))
35
+ const permitted = grants.has('security.read') && grants.has(otherWritePermission({ store: row.store }));
36
+ if (store.signal.aborted || !permitted || (!remove && !row.supportsReplace))
35
37
  return Promise.resolve(false);
36
38
  const token = store.getSnapshot().drafts[`${row.store}:${row.id}`]?.trim() ?? '';
37
39
  if (!remove && !token)
@@ -1,9 +1,10 @@
1
1
  import { createControllerStore } from '../../core/module/controller-store.js';
2
2
  import { ROOT_KEY_PERMISSION, START_FRESH_CONFIRMATION, isRootKeyLocked } from '../rules.js';
3
+ const receiverLabel = (w) => `${w.label} (${w.targetUrl})`;
3
4
  /** Receivers named in a result need their new signing secret; the message tells the operator which ones. */
4
5
  function resultMessage({ result, response }, _optional = {}) {
5
6
  const hooks = response.affectedWebhooks;
6
- return hooks?.length ? `${result} Update receivers: ${hooks.map(w => `${w.label} (${w.targetUrl})`).join(', ')}` : result;
7
+ return hooks?.length ? `${result} Update receivers: ${hooks.map(receiverLabel).join(', ')}` : result;
7
8
  }
8
9
  export function createRootKeyController({ api, permissions = [] }, _optional = {}) {
9
10
  const granted = permissions.includes(ROOT_KEY_PERMISSION);
@@ -14,7 +15,8 @@ export function createRootKeyController({ api, permissions = [] }, _optional = {
14
15
  async function load(_required, _optional = {}) {
15
16
  if (!allowed())
16
17
  return;
17
- const ticket = ++generation;
18
+ generation += 1;
19
+ const ticket = generation;
18
20
  store.set({ patch: { loading: true } });
19
21
  try {
20
22
  const status = await api.status({}, { signal: store.signal });
@@ -66,7 +68,8 @@ export function createRootKeyController({ api, permissions = [] }, _optional = {
66
68
  async preview(_required, _optional = {}) {
67
69
  if (!canPreview())
68
70
  return false;
69
- const ticket = ++generation;
71
+ generation += 1;
72
+ const ticket = generation;
70
73
  store.set({ patch: { loading: true, preview: null, confirm: '', error: null } });
71
74
  let ok = false;
72
75
  try {
@@ -86,8 +89,12 @@ export function createRootKeyController({ api, permissions = [] }, _optional = {
86
89
  }
87
90
  return ok;
88
91
  },
89
- startFresh(_required, _optional = {}) { const state = store.getSnapshot(); if (!isRootKeyLocked({ status: state.status }) || !state.preview || !state.confirming || state.confirm !== START_FRESH_CONFIRMATION)
90
- return Promise.resolve(false); return execute(() => api.startFresh({ confirm: state.confirm }, { signal: store.signal }), 'Done. Re-enter removed credentials.'); },
92
+ startFresh(_required, _optional = {}) {
93
+ const state = store.getSnapshot();
94
+ if (!isRootKeyLocked({ status: state.status }) || !state.preview || !state.confirming || state.confirm !== START_FRESH_CONFIRMATION)
95
+ return Promise.resolve(false);
96
+ return execute(() => api.startFresh({ confirm: state.confirm }, { signal: store.signal }), 'Done. Re-enter removed credentials.');
97
+ },
91
98
  cancel(_required, _optional = {}) { if (!store.getSnapshot().saving) {
92
99
  generation++;
93
100
  store.set({ patch: { confirming: false, preview: null, confirm: '', loading: false } });
@@ -55,9 +55,14 @@ export function useAccessTokensTab({ permissions = [] }, _optional = {}) {
55
55
  reload() { void access.controller?.load({}); void other.controller?.load({}); },
56
56
  cancelRemove() { if (!s?.saving && !o?.saving)
57
57
  setRemoval(null); },
58
- async confirmRemove() { if (!removal)
59
- return; const ok = removal.type === 'access' ? await access.controller?.remove({ row: removal.row }) : await other.controller?.remove({ row: removal.row }); if (!ok)
60
- throw new Error('Unable to remove credential'); setRemoval(null); },
58
+ async confirmRemove() {
59
+ if (!removal)
60
+ return;
61
+ const ok = removal.type === 'access' ? await access.controller?.remove({ row: removal.row }) : await other.controller?.remove({ row: removal.row });
62
+ if (!ok)
63
+ throw new Error('Unable to remove credential');
64
+ setRemoval(null);
65
+ },
61
66
  controller: access.controller, };
62
67
  }
63
68
  //# sourceMappingURL=AccessTokensTab.hooks.js.map
@@ -6,6 +6,10 @@ const refuse = async () => { throw new Error('Unavailable'); };
6
6
  export const unavailableRootKey = { status: refuse, generate: refuse, importToken: refuse, previewStartFresh: refuse, startFresh: refuse };
7
7
  export const noOtherCredentials = { async list() { return []; }, replace: refuse, remove: refuse };
8
8
  export const SecurityPortsContext = createContext(null);
9
- export function useSecurityPorts(_required = {}, _optional = {}) { const ports = useContext(SecurityPortsContext); if (!ports)
10
- throw new AdminConfigError({ issues: ['React scope unavailable: security'] }); return ports; }
9
+ export function useSecurityPorts(_required = {}, _optional = {}) {
10
+ const ports = useContext(SecurityPortsContext);
11
+ if (!ports)
12
+ throw new AdminConfigError({ issues: ['React scope unavailable: security'] });
13
+ return ports;
14
+ }
11
15
  //# sourceMappingURL=SecurityPorts.hooks.js.map
@@ -6,10 +6,22 @@ export const CATEGORIES = [
6
6
  { value: 'ai', label: 'AI' }, { value: 'ops', label: 'Ops' }, { value: 'general', label: 'General' },
7
7
  ];
8
8
  export const OTHER_STORES = ['site-assistant', 'admin-byok', 'media-provider', 'external-mcp'];
9
- export function writePermission({ kind }, _optional = {}) { return kind === 'custom' ? 'custom-credentials.write' : kind === 'publish' ? 'system.publish' : 'source-control.credentials.write'; }
9
+ export function writePermission({ kind }, _optional = {}) {
10
+ if (kind === 'custom')
11
+ return 'custom-credentials.write';
12
+ return kind === 'publish' ? 'system.publish' : 'source-control.credentials.write';
13
+ }
10
14
  export function otherWritePermission({ store }, _optional = {}) { return store === 'site-assistant' || store === 'admin-byok' ? 'admin.assistant.manage' : 'admin.integrations.manage'; }
11
- export function credentialStore({ api, kind }, _optional = {}) { return kind === 'source-control' ? api.sourceControl : kind === 'publish' ? api.publish : api.custom; }
12
- function defaultCategory({ kind }, _optional = {}) { return kind === 'publish' ? 'hosting' : kind === 'source-control' ? 'source-control' : 'general'; }
15
+ export function credentialStore({ api, kind }, _optional = {}) {
16
+ if (kind === 'source-control')
17
+ return api.sourceControl;
18
+ return kind === 'publish' ? api.publish : api.custom;
19
+ }
20
+ function defaultCategory({ kind }, _optional = {}) {
21
+ if (kind === 'publish')
22
+ return 'hosting';
23
+ return kind === 'source-control' ? 'source-control' : 'general';
24
+ }
13
25
  export function blankCredentialDraft({ kind, providerId = '' }, _optional = {}) {
14
26
  return { kind, providerId, label: '', token: '', values: {}, category: defaultCategory({ kind }), baseUrl: '', additionalHosts: '', username: '' };
15
27
  }
@@ -28,7 +40,8 @@ export function filterOtherCredentials({ rows, query, category }, _optional = {}
28
40
  function findProvider({ draft, providers }, _optional = {}) { return providers.find(p => p.kind === draft.kind && p.id === draft.providerId); }
29
41
  function isDuplicateLabel({ draft, rows }, _optional = {}) {
30
42
  const label = draft.label.trim().toLowerCase();
31
- return rows.some(r => r.kind === draft.kind && r.id !== draft.id && (draft.kind === 'custom' || r.providerId === draft.providerId) && r.label.trim().toLowerCase() === label);
43
+ const sameScope = (r) => r.kind === draft.kind && r.id !== draft.id && (draft.kind === 'custom' || r.providerId === draft.providerId);
44
+ return rows.some(r => sameScope(r) && r.label.trim().toLowerCase() === label);
32
45
  }
33
46
  function customDraftError({ draft }, _optional = {}) {
34
47
  try {
@@ -76,7 +89,8 @@ function customPatchFields({ draft }, _optional = {}) {
76
89
  export function buildCredentialPatch({ draft, providers }, _optional = {}) {
77
90
  const token = draft.token.trim();
78
91
  const custom = draft.kind === 'custom';
79
- const connection = token ? (custom ? customConnection({ draft, token }) : providerConnection({ draft, token, providers })) : undefined;
92
+ const connectionFor = (value) => (custom ? customConnection({ draft, token: value }) : providerConnection({ draft, token: value, providers }));
93
+ const connection = token ? connectionFor(token) : undefined;
80
94
  return { label: draft.label.trim(), ...(connection ? { connection } : {}), ...(custom ? customPatchFields({ draft }) : {}) };
81
95
  }
82
96
  export function isRootKeyLocked({ status }, _optional = {}) { return status?.state === 'missing-with-data' || status?.state === 'mismatch'; }
@@ -1,3 +1,4 @@
1
+ import { omitKeys } from '../../core/omit-keys.js';
1
2
  const defaultSettings = () => ({ titleTemplate: '%s', defaultRobots: { noindex: false, nofollow: false }, sitemapEnabled: true, robotsRules: [] });
2
3
  const clone = (value) => structuredClone(value);
3
4
  /** `patch value ?? meta value`, named — {@link applySeoPatch} calls this once per field instead of
@@ -66,7 +67,7 @@ function settingOrCleared(patchValue, currentValue) {
66
67
  * @complexity O(1) — three fixed field reads, no iteration. */
67
68
  function applySettingsPatch(current, patch) {
68
69
  const { defaultDescription, defaultOgImage, twitterSite, ...rest } = patch;
69
- const { defaultDescription: _description, defaultOgImage: _image, twitterSite: _site, ...retained } = current;
70
+ const retained = omitKeys(current, ['defaultDescription', 'defaultOgImage', 'twitterSite']);
70
71
  return {
71
72
  ...retained,
72
73
  ...rest,
@@ -106,9 +107,11 @@ export function createMemorySeoApi(required = {}, { resolveEntry } = {}) {
106
107
  // O(k) in patch keys; null removes only touched overrides, omission preserves other edits.
107
108
  for (const key of Object.keys(patch)) {
108
109
  const value = patch[key];
110
+ if (value === undefined)
111
+ continue;
109
112
  if (value === null)
110
113
  delete current[key];
111
- else if (value !== undefined)
114
+ else
112
115
  Object.assign(current, { [key]: value });
113
116
  }
114
117
  overrides[entryId] = current;
@@ -34,7 +34,8 @@ export function createSeoEntryController({ api, entryId }, { t = key => key, des
34
34
  store.set({ patch: { resolved, touched: {}, notice: t('Saved.') } });
35
35
  // Saving finishes with the PUT, before this best-effort analysis settles. A newer save's
36
36
  // analysis must supersede an earlier slow response (the source's 2026-09-07 race fix).
37
- const ticket = ++analyzeGeneration;
37
+ analyzeGeneration += 1;
38
+ const ticket = analyzeGeneration;
38
39
  void api.analyzeSeoEntry({ entryId }).then(analysis => {
39
40
  if (ticket === analyzeGeneration)
40
41
  store.set({ patch: { analysis } });
@@ -6,7 +6,8 @@ export function createSitemapController({ api, enabled }, _optional = {}) {
6
6
  async function load(_required = {}, _optional = {}) {
7
7
  if (!enabled)
8
8
  return;
9
- const ticket = ++generation;
9
+ generation += 1;
10
+ const ticket = generation;
10
11
  store.set({ patch: { status: 'loading', error: null } });
11
12
  try {
12
13
  const { text } = await api.fetchSitemapXml({}, { signal: store.signal });
@@ -8,7 +8,7 @@ export function useMediaPickerBridge(required, _optional = {}) {
8
8
  useEffect(() => {
9
9
  if (!mediaPicker) {
10
10
  setError('Media picker unavailable');
11
- return;
11
+ return undefined;
12
12
  }
13
13
  const abort = new AbortController();
14
14
  void mediaPicker.pick({ accept: callbacks.current.accept ?? ['image/*'] }, { signal: abort.signal }).then(item => {
@@ -1,4 +1,4 @@
1
- import type { FormEvent, ComponentType } from 'react';
1
+ import type { FormEvent } from 'react';
2
2
  import type { TabBarTab } from '@jini-ai/ui/tab-strip';
3
3
  import type { SeoTranslator, SeoSettingsPatch, SeoSettings, SeoEntryAnalysis } from '../../models.js';
4
4
  export { buildSeoSettingsPatch, resolveSeoTabId } from '../../rules.js';
@@ -47,24 +47,4 @@ export interface SeoTabController {
47
47
  regenerateSitemap: () => void;
48
48
  openSitemapModal: () => void;
49
49
  }
50
- /** Panel selection is presentation dispatch; this never changes the defaults form's field set. */
51
- export declare function seoTabPanel({ activeTabId, controller, components }: {
52
- activeTabId: import('../../rules.js').SeoTabId;
53
- controller: SeoTabController;
54
- components: {
55
- SeoDefaultsTab: ComponentType<{
56
- controller: SeoTabController;
57
- }>;
58
- SeoSitemapTab: ComponentType<{
59
- controller: SeoTabController;
60
- }>;
61
- SeoEntrySection: ComponentType<{
62
- locale: string;
63
- }>;
64
- };
65
- }, _optional?: Record<string, never>): import("react").ReactElement<{
66
- controller: SeoTabController;
67
- }, string | import("react").JSXElementConstructor<any>> | import("react").ReactElement<{
68
- locale: string;
69
- }, string | import("react").JSXElementConstructor<any>>;
70
50
  //# sourceMappingURL=SeoPage.hooks.d.ts.map
@@ -26,12 +26,4 @@ export function useAnalyzePanel({ analysis }, _optional = {}) {
26
26
  export function useSeoDefaults({ save }, _optional = {}) {
27
27
  return { submit: (event) => { event.preventDefault(); void save(buildSeoSettingsPatch({ form: new FormData(event.currentTarget) })); } };
28
28
  }
29
- /** Panel selection is presentation dispatch; this never changes the defaults form's field set. */
30
- export function seoTabPanel({ activeTabId, controller, components }, _optional = {}) {
31
- if (activeTabId === 'sitemap')
32
- return createElement(components.SeoSitemapTab, { controller });
33
- if (activeTabId === 'entries')
34
- return createElement(components.SeoEntrySection, { locale: controller.locale });
35
- return createElement(components.SeoDefaultsTab, { controller });
36
- }
37
29
  //# sourceMappingURL=SeoPage.hooks.js.map
@@ -27,7 +27,7 @@ export function useSeo({ api }, { t, events } = {}) {
27
27
  }, [controller, cached.query.data]);
28
28
  useEffect(() => {
29
29
  if (!controller || !events)
30
- return;
30
+ return undefined;
31
31
  return events.subscribe({ onRefresh: () => { invalidate({ key: SEO_KEYS.settings }); } });
32
32
  }, [controller, events, invalidate]);
33
33
  return {
@@ -18,7 +18,7 @@ export function useShareImageWarning({ value, previewUrl, api }, { t = key => ke
18
18
  useEffect(() => {
19
19
  const inspect = api.inspectShareImage;
20
20
  if (!previewUrl || !inspect || !isBrowserSameOrigin({ url: previewUrl }))
21
- return;
21
+ return undefined;
22
22
  const controller = new AbortController();
23
23
  let active = true;
24
24
  const timeout = setTimeout(() => {
@@ -4,10 +4,18 @@ import { useController } from '../../../core/react/use-controller.js';
4
4
  import { createSitemapController } from '../../controllers/sitemap.controller.js';
5
5
  import { parseSitemapXml, filterSitemapEntries } from '../../rules.js';
6
6
  import { useSeoPorts } from './SeoPorts.hooks.js';
7
+ /** Disabled outranks a failed fetch, which outranks a loaded document; anything else is still loading. */
8
+ function sitemapStatus({ enabled, query }) {
9
+ if (!enabled)
10
+ return 'disabled';
11
+ if (query.error)
12
+ return 'error';
13
+ return query.data ? 'ready' : 'loading';
14
+ }
7
15
  export function useSitemapModal({ api, enabled }, _optional = {}) {
8
16
  const cached = useSitemapQuery({ api, enabled });
9
17
  const { controller, snapshot } = useController({ create: () => createSitemapController({ api: cached.api, enabled }), dependencies: [cached.api, enabled] }, { start: ({ controller }) => { void controller.load({}); } });
10
- const state = { ...(snapshot ?? { filter: '', view: 'table' }), status: !enabled ? 'disabled' : cached.query.error ? 'error' : cached.query.data ? 'ready' : 'loading', error: cached.query.error?.message ?? null, xmlText: cached.query.data?.text ?? '' };
18
+ const state = { ...(snapshot ?? { filter: '', view: 'table' }), status: sitemapStatus({ enabled, query: cached.query }), error: cached.query.error?.message ?? null, xmlText: cached.query.data?.text ?? '' };
11
19
  // Parse only when text/status changes, never on each filter keystroke.
12
20
  const entries = useMemo(() => state.status === 'ready' ? parseSitemapXml({ xmlText: state.xmlText }) : [], [state.status, state.xmlText]);
13
21
  const filteredEntries = useMemo(() => filterSitemapEntries({ entries, query: state.filter }), [entries, state.filter]);
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useAnalyzePanel, useSeoDefaults, useSeoTabs, seoTabPanel } from "../hooks/SeoPage.hooks.js";
2
+ import { useAnalyzePanel, useSeoDefaults, useSeoTabs } from "../hooks/SeoPage.hooks.js";
3
3
  import { agentHandle } from "@jini-ai/agentic";
4
4
  import { TabBar } from "@jini-ai/ui/tab-strip";
5
5
  import { InfoTip, Select } from "@jini-ai/ui/admin-widgets";
@@ -52,6 +52,12 @@ function EntryPicker({ locale, entryId, onChange, useEntryPickerHook = useWiredE
52
52
  than leaving a placeholder-only control with no name at all. The i18n key is unchanged. */
53
53
  return (_jsx("div", { className: "jini-seo-entry-picker", children: _jsx(Select, { value: entryId, onChange: onChange, options: options ?? [], placeholder: t({ locale: locale, key: "Choose an entry…" }), "aria-label": t({ locale: locale, key: "Entry" }), agentHandle: "seo-entry-picker", translate: (key) => t({ locale, key }) }) }));
54
54
  }
55
+ /** Status-chip modifier for an issue: errors read as failures, warnings as warnings, anything else as success. */
56
+ function issueStatusClass(severity) {
57
+ if (severity === "error")
58
+ return "failure";
59
+ return severity === "warning" ? "warning" : "success";
60
+ }
55
61
  /** Read-only score + issues view (REQ-07) — exact field names read off `SeoAnalysis`/`SeoIssue`
56
62
  * (`src/seo/types.ts`), not guessed. No state of its own — only the severity sort, which lives in
57
63
  * `rules.ts` as `sortIssuesBySeverity`. */
@@ -62,7 +68,7 @@ function AnalyzePanel(props) {
62
68
  /* Heading + score pill on one line, issues as a compact list (owner, 2026-10-15: the bare
63
69
  "Score: 100 / No issues." read as unstyled text). The pill reuses the issue pills' status
64
70
  tones, so the score and the issues under it speak one colour language. */
65
- return (_jsxs("section", { className: "jini-seo-analyze-panel", "aria-labelledby": "seo-analysis-heading", children: [_jsxs("div", { className: "jini-seo-analyze-head", children: [_jsx("h3", { id: "seo-analysis-heading", children: t({ locale: locale, key: "Analysis" }) }), _jsxs("span", { className: `jini-status jini-status-${tone} jini-seo-score`, children: [t({ locale: locale, key: "Score:" }), " ", _jsx("strong", { children: props.analysis.score })] })] }), sortedIssues.length === 0 ? (_jsx("p", { className: "jini-seo-analyze-empty", children: t({ locale: locale, key: "No issues." }) })) : (_jsx("ul", { className: "jini-seo-issues", children: sortedIssues.map((issue, i) => (_jsxs("li", { className: "jini-seo-issue", children: [_jsx("span", { className: `jini-status jini-status-${issue.severity === "error" ? "failure" : issue.severity === "warning" ? "warning" : "success"}`, children: issue.severity }), _jsxs("span", { className: "jini-seo-issue-text", children: [issue.message, issue.field ? _jsxs("span", { className: "jini-muted-cell", children: [" (", issue.field, ")"] }) : null] })] }, `${issue.code}-${i}`))) }))] }));
71
+ return (_jsxs("section", { className: "jini-seo-analyze-panel", "aria-labelledby": "seo-analysis-heading", children: [_jsxs("div", { className: "jini-seo-analyze-head", children: [_jsx("h3", { id: "seo-analysis-heading", children: t({ locale: locale, key: "Analysis" }) }), _jsxs("span", { className: `jini-status jini-status-${tone} jini-seo-score`, children: [t({ locale: locale, key: "Score:" }), " ", _jsx("strong", { children: props.analysis.score })] })] }), sortedIssues.length === 0 ? (_jsx("p", { className: "jini-seo-analyze-empty", children: t({ locale: locale, key: "No issues." }) })) : (_jsx("ul", { className: "jini-seo-issues", children: sortedIssues.map((issue, i) => (_jsxs("li", { className: "jini-seo-issue", children: [_jsx("span", { className: `jini-status jini-status-${issueStatusClass(issue.severity)}`, children: issue.severity }), _jsxs("span", { className: "jini-seo-issue-text", children: [issue.message, issue.field ? _jsxs("span", { className: "jini-muted-cell", children: [" (", issue.field, ")"] }) : null] })] }, `${issue.code}-${i}`))) }))] }));
66
72
  }
67
73
  /** Per-entry overrides edit form (REQ-06). Pre-fills from `getSeoEntry`'s resolved meta, but
68
74
  * tracks which fields the user actually touched so `putSeoEntry` only ever sends a genuine
@@ -230,7 +236,15 @@ export function Seo({ tabId, onTabChange, useSeoHook = useWiredSeo } = {}) {
230
236
  regenerateSitemap,
231
237
  openSitemapModal,
232
238
  };
233
- return (_jsxs("div", { className: "jini-page jini-seo-page", children: [_jsxs("div", { className: "jini-page-header", children: [_jsxs("div", { className: "jini-page-header-text", children: [_jsx("p", { className: "jini-page-kicker", children: t({ locale: locale, key: "Marketing" }) }), _jsx("h1", { className: "jini-page-title", children: "SEO" }), _jsx("p", { className: "jini-page-description", children: t({ locale: locale, key: "Site-wide defaults for meta titles, descriptions, social previews, and robots directives." }) })] }), _jsx("div", { className: "jini-page-actions", children: headerActions })] }), error ? _jsx("div", { className: "jini-notice jini-notice-error", children: error }) : null, notice ? _jsx("div", { className: "jini-notice", children: notice }) : null, _jsx(TabBar, { ariaLabel: "SEO", tabs: tabs, activeId: activeTabId, onChange: changeTab, containerHandle: "seo-tab-bar" }), seoTabPanel({ activeTabId, controller, components: { SeoDefaultsTab, SeoSitemapTab, SeoEntrySection } }), sitemapModalOpen ? (_jsx(SitemapModal, { locale: locale, sitemapEnabled: settings.sitemapEnabled, regenerating: saving, onRegenerate: regenerateSitemap, onClose: closeSitemapModal })) : null] }));
239
+ return (_jsxs("div", { className: "jini-page jini-seo-page", children: [_jsxs("div", { className: "jini-page-header", children: [_jsxs("div", { className: "jini-page-header-text", children: [_jsx("p", { className: "jini-page-kicker", children: t({ locale: locale, key: "Marketing" }) }), _jsx("h1", { className: "jini-page-title", children: "SEO" }), _jsx("p", { className: "jini-page-description", children: t({ locale: locale, key: "Site-wide defaults for meta titles, descriptions, social previews, and robots directives." }) })] }), _jsx("div", { className: "jini-page-actions", children: headerActions })] }), error ? _jsx("div", { className: "jini-notice jini-notice-error", children: error }) : null, notice ? _jsx("div", { className: "jini-notice", children: notice }) : null, _jsx(TabBar, { ariaLabel: "SEO", tabs: tabs, activeId: activeTabId, onChange: changeTab, containerHandle: "seo-tab-bar" }), seoTabPanel({ activeTabId, controller }), sitemapModalOpen ? (_jsx(SitemapModal, { locale: locale, sitemapEnabled: settings.sitemapEnabled, regenerating: saving, onRegenerate: regenerateSitemap, onClose: closeSitemapModal })) : null] }));
240
+ }
241
+ /** Panel selection is presentation dispatch; this never changes the defaults form's field set. */
242
+ function seoTabPanel({ activeTabId, controller }, _optional = {}) {
243
+ if (activeTabId === 'sitemap')
244
+ return _jsx(SeoSitemapTab, { controller: controller });
245
+ if (activeTabId === 'entries')
246
+ return _jsx(SeoEntrySection, { locale: controller.locale });
247
+ return _jsx(SeoDefaultsTab, { controller: controller });
234
248
  }
235
249
  export { Seo as SeoPage };
236
250
  export default Seo;
@@ -3,28 +3,44 @@ export function createMemorySourceControlApi({ providers = [], credentials = []
3
3
  const catalog = Object.freeze(providers.map(p => Object.freeze({ ...p, ...(p.credential ? { credential: Object.freeze({ ...p.credential, fields: Object.freeze(p.credential.fields.map(f => Object.freeze({ ...f }))) }) } : {}) })));
4
4
  const rows = new Map(credentials.map(row => [row.id, sourceControlSummary({ row })]));
5
5
  let sequence = 0;
6
+ const nextSequence = () => { sequence += 1; return sequence; };
6
7
  function fail(code) { throw Object.assign(new Error(code), { code }); }
7
- function checkLabel(providerId, label, id) { if (!label.trim())
8
- fail('VALIDATION'); if ([...rows.values()].some(r => r.providerId === providerId && r.label === label.trim() && r.id !== id))
9
- fail('DUPLICATE_LABEL'); }
8
+ function checkLabel(providerId, label, id) {
9
+ if (!label.trim())
10
+ fail('VALIDATION');
11
+ if ([...rows.values()].some(r => r.providerId === providerId && r.label === label.trim() && r.id !== id))
12
+ fail('DUPLICATE_LABEL');
13
+ }
10
14
  function validate(input) { const info = sourceControlProviders({ providers: catalog, credentials: [] }).find(p => p.id === input.providerId); if (!info || !sourceControlReady({ info, draft: { ...blankSourceControlDraft({}), token: input.token, values: input } }))
11
15
  fail('VALIDATION'); }
12
- function setDefault(row) { if (row.isDefault)
13
- for (const old of rows.values())
14
- if (old.providerId === row.providerId && old.id !== row.id && old.isDefault)
15
- rows.set(old.id, sourceControlSummary({ row: { ...old, isDefault: false } })); rows.set(row.id, sourceControlSummary({ row })); return rows.get(row.id); }
16
+ function setDefault(row) {
17
+ if (row.isDefault)
18
+ for (const old of rows.values())
19
+ if (old.providerId === row.providerId && old.id !== row.id && old.isDefault)
20
+ rows.set(old.id, sourceControlSummary({ row: { ...old, isDefault: false } }));
21
+ rows.set(row.id, sourceControlSummary({ row }));
22
+ return rows.get(row.id);
23
+ }
16
24
  return {
17
25
  async providers(_r, o = {}) { o.signal?.throwIfAborted(); return catalog; },
18
26
  async list(_r, o = {}) { o.signal?.throwIfAborted(); return Object.freeze([...rows.values()]); },
19
27
  async create(input, o = {}) { o.signal?.throwIfAborted(); validate(input.connection); const providerId = input.connection.providerId, label = input.label.trim(); checkLabel(providerId, label); let id; do {
20
- id = `memory-source-${++sequence}`;
28
+ id = `memory-source-${nextSequence()}`;
21
29
  } while (rows.has(id)); const timestamp = now(); return setDefault({ id, providerId, label, configured: true, isDefault: input.isDefault ?? ![...rows.values()].some(r => r.providerId === providerId), createdAt: timestamp, updatedAt: timestamp }); },
22
- async update({ id, patch }, o = {}) { o.signal?.throwIfAborted(); const old = rows.get(id); if (!old)
23
- fail('NOT_FOUND'); if (patch.connection) {
24
- validate(patch.connection);
25
- if (patch.connection.providerId !== old.providerId)
26
- fail('VALIDATION');
27
- } const label = (patch.label ?? old.label).trim(); checkLabel(old.providerId, label, id); return setDefault({ ...old, label, isDefault: patch.isDefault ?? old.isDefault, updatedAt: now() }); },
30
+ async update({ id, patch }, o = {}) {
31
+ o.signal?.throwIfAborted();
32
+ const old = rows.get(id);
33
+ if (!old)
34
+ fail('NOT_FOUND');
35
+ if (patch.connection) {
36
+ validate(patch.connection);
37
+ if (patch.connection.providerId !== old.providerId)
38
+ fail('VALIDATION');
39
+ }
40
+ const label = (patch.label ?? old.label).trim();
41
+ checkLabel(old.providerId, label, id);
42
+ return setDefault({ ...old, label, isDefault: patch.isDefault ?? old.isDefault, updatedAt: now() });
43
+ },
28
44
  async remove({ id }, o = {}) { o.signal?.throwIfAborted(); const old = rows.get(id); rows.delete(id); if (old?.isDefault) {
29
45
  const next = [...rows.values()].find(r => r.providerId === old.providerId);
30
46
  if (next)
@@ -4,7 +4,9 @@ import { blankSourceControlDraft, sourceControlProviders, defaultSourceControlCr
4
4
  function readySaveTarget({ state, providerId }) {
5
5
  const draft = state.drafts[providerId] ?? blankSourceControlDraft({});
6
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;
7
+ if (!state.loaded || draft.saving || !info)
8
+ return null;
9
+ return sourceControlReady({ info, draft }) ? { info, draft } : null;
8
10
  }
9
11
  /** Replaces the row with `summary`'s id in place, or appends `summary` when it is new. */
10
12
  function spliceCredential({ credentials, summary }) {
@@ -20,7 +22,8 @@ export function createSourceControlController({ api, permissions = [] }, _option
20
22
  async function load(_required, _optional = {}) {
21
23
  if (!canRead())
22
24
  return;
23
- const ticket = ++generation, revision = credentialRevision;
25
+ generation += 1;
26
+ const ticket = generation, revision = credentialRevision;
24
27
  store.set({ patch: { loading: true } });
25
28
  // Catalog failure must not hide saved connections; retain each successful store independently.
26
29
  await Promise.all([
@@ -1,7 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Notice } from '@jini-ai/ui-kit/react';
3
3
  import { SourceControlCredentialFields } from './SourceControlCredentialFields.js';
4
+ function providerRowBody(row) {
5
+ if (row.unlisted)
6
+ return _jsx(Notice, { children: "Its plugin is off or missing. Manage this token on the Access tokens page." });
7
+ return row.canEdit ? _jsx(SourceControlCredentialFields, { row: row }) : _jsx(Notice, { children: "Read-only connection" });
8
+ }
4
9
  export function SourceControlProviderRow({ row }, _optional = {}) {
5
- return _jsxs("details", { name: "jini-source-control-provider", open: row.defaultOpen, "data-jini-part": "source-control.provider", children: [_jsxs("summary", { children: [_jsx("h2", { children: row.heading }), row.saved && _jsxs(_Fragment, { children: [_jsx("span", { children: "Token saved" }), _jsxs("span", { children: ["Saved ", row.saved.updatedAt] }), _jsx("span", { children: "Replace token \u25BE" })] }), !row.saved && _jsx("span", { children: "Expand \u25BE" })] }), row.unlisted ? _jsx(Notice, { children: "Its plugin is off or missing. Manage this token on the Access tokens page." }) : row.canEdit ? _jsx(SourceControlCredentialFields, { row: row }) : _jsx(Notice, { children: "Read-only connection" })] });
10
+ return _jsxs("details", { name: "jini-source-control-provider", open: row.defaultOpen, "data-jini-part": "source-control.provider", children: [_jsxs("summary", { children: [_jsx("h2", { children: row.heading }), row.saved && _jsxs(_Fragment, { children: [_jsx("span", { children: "Token saved" }), _jsxs("span", { children: ["Saved ", row.saved.updatedAt] }), _jsx("span", { children: "Replace token \u25BE" })] }), !row.saved && _jsx("span", { children: "Expand \u25BE" })] }), providerRowBody(row)] });
6
11
  }
7
12
  //# sourceMappingURL=SourceControlProviderRow.js.map
@@ -1,6 +1,10 @@
1
1
  import { createContext, useContext } from 'react';
2
2
  import { AdminConfigError } from '../../../core/module/index.js';
3
3
  export const SourceControlPortsContext = createContext(null);
4
- export function useSourceControlPorts(_required = {}, _optional = {}) { const ports = useContext(SourceControlPortsContext); if (!ports)
5
- throw new AdminConfigError({ issues: ['React scope unavailable: source-control'] }); return ports; }
4
+ export function useSourceControlPorts(_required = {}, _optional = {}) {
5
+ const ports = useContext(SourceControlPortsContext);
6
+ if (!ports)
7
+ throw new AdminConfigError({ issues: ['React scope unavailable: source-control'] });
8
+ return ports;
9
+ }
6
10
  //# sourceMappingURL=SourceControlPorts.hooks.js.map
@@ -22,9 +22,15 @@ export function buildSourceControlConnection({ info, draft }, _optional = {}) {
22
22
  }
23
23
  /** Explicit projection prevents accidental token, ciphertext or username disclosure. */
24
24
  export function sourceControlSummary({ row }, _optional = {}) { return Object.freeze({ id: row.id, providerId: row.providerId, label: row.label, configured: true, isDefault: row.isDefault, createdAt: row.createdAt, updatedAt: row.updatedAt }); }
25
- export function sourceControlErrorMessage({ error }, _optional = {}) { const e = error; const marker = e?.code ?? e?.message; if (marker === 'DUPLICATE_LABEL')
26
- return 'This connection was already saved — reload and try again.'; if (marker === 'VALIDATION')
27
- return 'Check the required credential fields.'; return 'Unable to save connection'; }
25
+ export function sourceControlErrorMessage({ error }, _optional = {}) {
26
+ const e = error;
27
+ const marker = e?.code ?? e?.message;
28
+ if (marker === 'DUPLICATE_LABEL')
29
+ return 'This connection was already saved — reload and try again.';
30
+ if (marker === 'VALIDATION')
31
+ return 'Check the required credential fields.';
32
+ return 'Unable to save connection';
33
+ }
28
34
  export function safeSourceControlUrl({ url }, _optional = {}) { try {
29
35
  const parsed = new URL(url);
30
36
  return parsed.protocol === 'https:' || parsed.protocol === 'http:' ? url : undefined;
@@ -1,3 +1,4 @@
1
+ import { querySuffix } from '../../core/transport/query-suffix.js';
1
2
  /** basePath is the host workspace root; the adapter adds /widgets and the shared /trash/items. */
2
3
  export function createHttpWidgetsApi({ transport, basePath }, _optional = {}) {
3
4
  const root = basePath.replace(/\/$/, '');
@@ -14,7 +15,7 @@ export function createHttpWidgetsApi({ transport, basePath }, _optional = {}) {
14
15
  if (includeInactive)
15
16
  query.set('includeInactive', 'true');
16
17
  const suffix = query.toString();
17
- return request(`${base}${suffix ? `?${suffix}` : ''}`, 'GET', undefined, options);
18
+ return request(`${base}${querySuffix(suffix)}`, 'GET', undefined, options);
18
19
  },
19
20
  getWidget({ id }, options = {}) { return request(`${base}/${encodeURIComponent(id)}`, 'GET', undefined, options); },
20
21
  createWidget(input, options = {}) { return request(base, 'POST', input, options); },
@@ -32,7 +32,8 @@ export function createMemoryWidgetsApi({ widgets = [], regions = [], areas = {},
32
32
  options.signal?.throwIfAborted();
33
33
  let id;
34
34
  do {
35
- id = `fake-${++widgetSequence}`;
35
+ widgetSequence += 1;
36
+ id = `fake-${widgetSequence}`;
36
37
  } while (rows.some(w => w.id === id || w.slug === `fake-slug-${widgetSequence}`));
37
38
  const widget = { id, workspaceId, slug: `fake-slug-${widgetSequence}`, title: input.title, status: 'active', widgetType: input.widgetType, config: copy(input.config), updatedAt: now(), version: 1 };
38
39
  rows.push(widget);
@@ -61,7 +62,8 @@ export function createMemoryWidgetsApi({ widgets = [], regions = [], areas = {},
61
62
  fail('WIDGETS_REGION_BOUND', 'Region already bound');
62
63
  let id;
63
64
  do {
64
- id = `fake-area-${++areaSequence}`;
65
+ areaSequence += 1;
66
+ id = `fake-area-${areaSequence}`;
65
67
  } while (bindings.some(b => b.areaEntryId === id) || [...regionRows.values()].some(r => r.area.id === id));
66
68
  const area = { id, workspaceId, regionKey, updatedAt: now(), version: 1 };
67
69
  bindings.push({ workspaceId, regionKey, areaEntryId: id, updatedAt: area.updatedAt, placementCount: 0 });
@@ -1,8 +1,11 @@
1
1
  /** Mutates an isolated adapter. Returns named checks and throws at the first contract failure. */
2
2
  export async function runWidgetsApiConformance({ api }, _optional = {}) {
3
3
  const checks = [];
4
- function assert(condition, name) { if (!condition)
5
- throw new Error(`Widgets API conformance: ${name}`); checks.push(name); }
4
+ function assert(condition, name) {
5
+ if (!condition)
6
+ throw new Error(`Widgets API conformance: ${name}`);
7
+ checks.push(name);
8
+ }
6
9
  async function rejects(run, name) { let rejected = false; try {
7
10
  await run();
8
11
  }
@@ -17,7 +17,8 @@ export function createWidgetInstanceEditorController({ api, params: initialParam
17
17
  const isLiveLoad = ({ ticket }) => ticket === generation && !store.signal.aborted;
18
18
  const isLiveSave = ({ savingFor }) => currentKey() === savingFor && !store.signal.aborted;
19
19
  async function load(_required = {}, _optional = {}) {
20
- const ticket = ++generation;
20
+ generation += 1;
21
+ const ticket = generation;
21
22
  const isNew = params.widgetId === null;
22
23
  store.set({ patch: { isNew, saving: false } });
23
24
  if (isNew) {
@@ -58,7 +59,7 @@ export function createWidgetInstanceEditorController({ api, params: initialParam
58
59
  try {
59
60
  if (isNew)
60
61
  await createAndOpen({ widgetType, title, config });
61
- else if (widget)
62
+ if (!isNew && widget)
62
63
  await updateExisting({ widget, title, config, savingFor });
63
64
  }
64
65
  catch (error) {
@@ -8,7 +8,8 @@ export function createWidgetsLibraryController({ api }, { t = widgetsEnglish } =
8
8
  let generation = 0;
9
9
  const isLiveRead = ({ ticket }) => ticket === generation && !store.signal.aborted;
10
10
  async function load(_required = {}, _optional = {}) {
11
- const ticket = ++generation;
11
+ generation += 1;
12
+ const ticket = generation;
12
13
  try {
13
14
  // No includeInactive: trashed widgets belong to the shared Trash screen.
14
15
  const result = await api.listWidgets({}, { signal: store.signal });
@@ -8,7 +8,8 @@ export function createWidgetRegionEditorController({ api, regionKey: initialKey
8
8
  let regionKey = initialKey, generation = 0;
9
9
  const isLiveRead = ({ ticket }) => ticket === generation && !store.signal.aborted;
10
10
  async function load(_required = {}, _optional = {}) {
11
- const ticket = ++generation;
11
+ generation += 1;
12
+ const ticket = generation;
12
13
  store.set({ patch: { loading: true, error: null, saving: false } });
13
14
  try {
14
15
  const result = await api.getWidgetRegion({ regionKey }, { signal: store.signal });
@@ -7,7 +7,8 @@ export function createWidgetRegionsController({ api }, { t = widgetsEnglish, nav
7
7
  const store = createControllerStore({ initial: { regions: null, error: null, newRegionKey: '', binding: false } });
8
8
  let generation = 0;
9
9
  async function load(_required = {}, _optional = {}) {
10
- const ticket = ++generation;
10
+ generation += 1;
11
+ const ticket = generation;
11
12
  try {
12
13
  const result = await api.listWidgetRegions({}, { signal: store.signal });
13
14
  if (ticket === generation && !store.signal.aborted)
@@ -10,5 +10,5 @@ import type { WidgetInstanceGuard } from '../../rules.js';
10
10
  export declare function WidgetInstanceGuardNotice({ guard, t }: {
11
11
  guard: WidgetInstanceGuard;
12
12
  t: (key: string) => string;
13
- }): import("react").JSX.Element;
13
+ }): import("react").JSX.Element | null;
14
14
  //# sourceMappingURL=WidgetInstanceNotices.d.ts.map
@@ -20,6 +20,8 @@ export function WidgetInstanceGuardNotice({ guard, t }) {
20
20
  return _jsx("div", { className: "notice error", children: t("No widget type specified.") });
21
21
  case "unknown-type":
22
22
  return _jsxs("div", { className: "notice error", children: [t('Unknown widget type "'), guard.widgetType, "\"."] });
23
+ default:
24
+ return null;
23
25
  }
24
26
  }
25
27
  //# sourceMappingURL=WidgetInstanceNotices.js.map