@jini-ai/admin 0.6.15-rc.2 → 0.6.16

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/index.d.ts +1 -1
  16. package/dist/core/index.js +1 -1
  17. package/dist/core/manifest/rules.d.ts +20 -0
  18. package/dist/core/manifest/rules.js +34 -0
  19. package/dist/core/manifest/types.d.ts +9 -0
  20. package/dist/core/module/create-admin.js +2 -1
  21. package/dist/core/omit-keys.d.ts +7 -0
  22. package/dist/core/omit-keys.js +12 -0
  23. package/dist/core/ports/forms.d.ts +5 -3
  24. package/dist/core/transport/query-suffix.d.ts +3 -0
  25. package/dist/core/transport/query-suffix.js +5 -0
  26. package/dist/forms/adapters/http.js +3 -2
  27. package/dist/forms/adapters/memory.js +3 -2
  28. package/dist/forms/controllers/field-attributes-dialog.controller.js +17 -3
  29. package/dist/forms/controllers/form-fields-editor.controller.js +1 -2
  30. package/dist/forms/controllers/form-submission-detail.controller.js +1 -2
  31. package/dist/forms/controllers/form-submissions.controller.js +1 -2
  32. package/dist/forms/controllers/forms-list.controller.js +1 -2
  33. package/dist/forms/field-settings.js +15 -4
  34. package/dist/forms/react/hooks/forms-view.hooks.d.ts +2 -2
  35. package/dist/forms/react/hooks/forms-view.hooks.js +9 -1
  36. package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.js +1 -3
  37. package/dist/forms/react/hooks/use-form-editor.hooks.js +8 -9
  38. package/dist/forms/react/hooks/use-form-submission-detail.hooks.js +1 -4
  39. package/dist/forms/react/hooks/use-form-submissions.hooks.js +1 -5
  40. package/dist/forms/react/hooks/use-forms-list.hooks.js +1 -2
  41. package/dist/forms/rules.js +7 -2
  42. package/dist/media/adapters/memory.js +5 -3
  43. package/dist/media/controllers/library.controller.js +4 -2
  44. package/dist/media/controllers/providers.controller.js +2 -1
  45. package/dist/media/react/components/MediaCard.js +11 -1
  46. package/dist/media/react/hooks/EditMediaPanel.hooks.js +18 -7
  47. package/dist/media/react/hooks/LibraryTab.hooks.js +3 -2
  48. package/dist/media/react/hooks/ProvidersTab.hooks.js +19 -9
  49. package/dist/media/rules.js +6 -3
  50. package/dist/playground/adapters/memory.js +26 -7
  51. package/dist/playground/conformance/playground-api.conformance.js +6 -3
  52. package/dist/playground/react/hooks/PlaygroundPorts.hooks.js +6 -2
  53. package/dist/react/components/ConfirmButton/ConfirmButton.hooks.js +1 -1
  54. package/dist/react/components/ConfirmButton/ConfirmButton.js +2 -5
  55. package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +2 -2
  56. package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +3 -1
  57. package/dist/react/components/RowMenu/RowMenu.hooks.js +4 -8
  58. package/dist/react/components/Sidebar.js +1 -1
  59. package/dist/react/entities/EntityField.js +1 -0
  60. package/dist/react/entities/contribution.js +3 -1
  61. package/dist/react/entities/types.js +1 -1
  62. package/dist/react/overlays.js +2 -1
  63. package/dist/react/shell/AdminShell.hooks.js +1 -1
  64. package/dist/react/shell/use-admin-shell-session.js +2 -1
  65. package/dist/react/shell/use-admin-theme.js +6 -6
  66. package/dist/redirects/adapters/memory.js +16 -13
  67. package/dist/security/adapters/http.js +10 -11
  68. package/dist/security/adapters/memory.js +50 -15
  69. package/dist/security/conformance/security-api.conformance.js +6 -3
  70. package/dist/security/controllers/access-tokens.controller.js +7 -3
  71. package/dist/security/controllers/other-credentials.controller.js +4 -2
  72. package/dist/security/controllers/root-key.controller.js +12 -5
  73. package/dist/security/react/hooks/AccessTokensTab.hooks.js +8 -3
  74. package/dist/security/react/hooks/SecurityPorts.hooks.js +6 -2
  75. package/dist/security/rules.js +19 -5
  76. package/dist/seo/adapters/memory.js +5 -2
  77. package/dist/seo/controllers/seo-entry.controller.js +2 -1
  78. package/dist/seo/controllers/sitemap.controller.js +2 -1
  79. package/dist/seo/react/hooks/MediaPickerBridge.hooks.js +1 -1
  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 +7 -1
  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 +47 -37
@@ -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]);
@@ -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
@@ -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
@@ -13,8 +13,11 @@ export function useWidgetRegions({ api }, { t = widgetsEnglish, events, navigate
13
13
  const latest = useRef({ t, navigate });
14
14
  latest.current = { t, navigate };
15
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(); } });
16
- useEffect(() => { if (!controller)
17
- return; return events?.subscribe({ resource: WIDGETS_REGIONS_RESOURCE, onRefresh: () => invalidate({ key: WIDGET_QUERY_KEYS.regions }) }); }, [controller, events, invalidate]);
16
+ useEffect(() => {
17
+ if (!controller)
18
+ return undefined;
19
+ return events?.subscribe({ resource: WIDGETS_REGIONS_RESOURCE, onRefresh: () => invalidate({ key: WIDGET_QUERY_KEYS.regions }) });
20
+ }, [controller, events, invalidate]);
18
21
  return { ...mergeRegionsState({ snapshot, data: cached.query.data, queryError: cached.query.error }), setNewRegionKey: value => controller?.setNewRegionKey({ value }), bind: async () => { await controller?.bind(); }, t };
19
22
  }
20
23
  const EMPTY_REGIONS_STATE = { regions: null, error: null, newRegionKey: '', binding: false };
@@ -12,8 +12,11 @@ export function useWidgetsLibrary({ api }, { t = widgetsEnglish, locale = 'en',
12
12
  const latestT = useRef(t);
13
13
  latestT.current = t;
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
- useEffect(() => { if (!controller)
16
- return; return events?.subscribe({ resource: WIDGETS_LIBRARY_RESOURCE, onRefresh: () => invalidate({ key: WIDGET_QUERY_KEYS.library }) }); }, [controller, events, invalidate]);
15
+ useEffect(() => {
16
+ if (!controller)
17
+ return undefined;
18
+ return events?.subscribe({ resource: WIDGETS_LIBRARY_RESOURCE, onRefresh: () => invalidate({ key: WIDGET_QUERY_KEYS.library }) });
19
+ }, [controller, events, invalidate]);
17
20
  return { ...mergeLibraryState({ snapshot, data: cached.query.data, queryError: cached.query.error }),
18
21
  setCreateType: type => controller?.setCreateType({ type }), requestTrash: widget => controller?.requestTrash({ widget }),
19
22
  confirmTrash: async () => { await controller?.confirmTrash(); }, cancelTrash: () => controller?.cancelTrash(), t, locale };
@@ -18,7 +18,7 @@ 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, save, confirmLeave, t, locale, gate, onBackClick, } = useWidgetInstanceEditorView({ widgetId, widgetType: queryWidgetType, useWidgetInstanceEditorHook, widgetTypes });
21
+ const { isNew, widget, whereUsed, title, setTitle, config, setConfig, message, error, fieldErrors, saving, save, t, gate, onBackClick, } = useWidgetInstanceEditorView({ widgetId, widgetType: queryWidgetType, useWidgetInstanceEditorHook, widgetTypes });
22
22
  if (gate.guard !== null)
23
23
  return _jsx(WidgetInstanceGuardNotice, { guard: gate.guard, t: t });
24
24
  // `widgetInstanceGuard`'s "no-type" case already covers a null `widgetType` above, but TS can't
@@ -9,7 +9,7 @@ import { WidgetRegionEditorHeaderActions } from '../components/WidgetRegionEdito
9
9
  export function WidgetRegionEditor(props) {
10
10
  const { slots: { AddControl: WidgetAddControl }, widgetTypes } = useWidgetsOptions();
11
11
  const { regionKey, useWidgetRegionEditorHook = useWiredWidgetRegionEditor } = props;
12
- const { area, placements, message, error, loading, saving, removeAt, moveAt, toggleEnabled, addPlacement, save, t, locale, placementHandles } = useWidgetRegionEditorView({ regionKey, useWidgetRegionEditorHook });
12
+ const { area, placements, message, error, loading, saving, removeAt, moveAt, toggleEnabled, addPlacement, save, t, placementHandles } = useWidgetRegionEditorView({ regionKey, useWidgetRegionEditorHook });
13
13
  if (error && !area)
14
14
  return _jsx("div", { className: "notice error", children: error });
15
15
  if (loading)
@@ -11,7 +11,7 @@ export { WidgetsLibraryNotices } from '../components/WidgetsLibraryNotices.js';
11
11
  import { WidgetsLibraryNotices } from '../components/WidgetsLibraryNotices.js';
12
12
  export function WidgetsLibrary({ useWidgetsLibraryHook = useWiredWidgetsLibrary } = {}) {
13
13
  const { widgetTypes, headerActions, statusLabel } = useWidgetsOptions();
14
- const { widgets, error, skippedCount, skippedIds, createType, setCreateType, pendingTrash, trashing, requestTrash, confirmTrash, cancelTrash, t, locale, rowHandleById, } = useWidgetsLibraryView({ useWidgetsLibraryHook });
14
+ const { widgets, error, skippedCount, skippedIds, createType, setCreateType, pendingTrash, trashing, requestTrash, confirmTrash, cancelTrash, t, rowHandleById, } = useWidgetsLibraryView({ useWidgetsLibraryHook });
15
15
  if (error && !widgets)
16
16
  return _jsx("div", { className: "notice error", children: error });
17
17
  if (!widgets)
@@ -43,8 +43,12 @@ export function movePlacement({ items, index, direction }, _optional = {}) {
43
43
  }
44
44
  // Distinguishes draft keys created in the same millisecond without randomUUID.
45
45
  let draftPlacementSequence = 0;
46
+ function fallbackDraftPlacementId() {
47
+ draftPlacementSequence += 1;
48
+ return `p-${Date.now()}-${draftPlacementSequence}`;
49
+ }
46
50
  /** Draft identity survives reorder/remove/toggle until the server accepts the ordered placements. */
47
- export function buildDraftPlacement({ widgetInstanceId }, { id = () => globalThis.crypto?.randomUUID?.() ?? `p-${Date.now()}-${++draftPlacementSequence}` } = {}) {
51
+ export function buildDraftPlacement({ widgetInstanceId }, { id = () => globalThis.crypto?.randomUUID?.() ?? fallbackDraftPlacementId() } = {}) {
48
52
  return { placementId: id(), widgetEntryId: widgetInstanceId, enabled: true, widgetTitle: null, widgetType: null, broken: false };
49
53
  }
50
54
  /** Conflict copy is domain-specific; other failures use the existing admin error describer. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/admin",
3
- "version": "0.6.15-rc.2",
3
+ "version": "0.6.16",
4
4
  "description": "Composable admin surface for Jini-hosted products: framework-free contracts, panel registry, and route model in `/core`; React shell and panels in `/react`. Import only the layers you need.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -95,6 +95,16 @@
95
95
  "react-dom"
96
96
  ]
97
97
  }
98
+ },
99
+ "optionalPeers": {
100
+ "@jini-ai/agentic": "opt-in: agent-aware ui and protocols; import @jini-ai/admin/react — Agent-aware UI and protocols could not load @jini-ai/agentic. Install @jini-ai/agentic to enable this feature, or check its installed version and native binaries.",
101
+ "@jini-ai/ui": "opt-in: ui integrations; import @jini-ai/admin/react — UI integrations could not load @jini-ai/ui. Install @jini-ai/ui to enable this feature, or check its installed version and native binaries.",
102
+ "@jini-ai/ui-kit": "opt-in: ui kit integrations; import @jini-ai/admin/react — UI kit integrations could not load @jini-ai/ui-kit. Install @jini-ai/ui-kit to enable this feature, or check its installed version and native binaries.",
103
+ "@jini-ai/agent-plugins": "opt-in: agent plugin integrations — Agent plugin integrations could not load @jini-ai/agent-plugins. Install @jini-ai/agent-plugins to enable this feature, or check its installed version and native binaries.",
104
+ "@jini-ai/cms": "opt-in: cms integrations; import @jini-ai/admin/media — CMS integrations could not load @jini-ai/cms. Install @jini-ai/cms to enable this feature, or check its installed version and native binaries.",
105
+ "@jini-ai/core": "opt-in: core module integrations; import @jini-ai/admin/react — Core module integrations could not load @jini-ai/core. Install @jini-ai/core to enable this feature, or check its installed version and native binaries.",
106
+ "react": "opt-in: react admin bindings; import @jini-ai/admin/react — React admin bindings could not load react. Install react to enable this feature, or check its installed version.",
107
+ "react-dom": "opt-in: react admin bindings; import @jini-ai/admin/react — React admin bindings could not load react-dom. Install react-dom to enable this feature, or check its installed version."
98
108
  }
99
109
  },
100
110
  "type": "module",
@@ -448,20 +458,38 @@
448
458
  "peerDependencies": {
449
459
  "react": "^18.3.0 || ^19.0.0",
450
460
  "react-dom": "^18.3.0 || ^19.0.0",
451
- "@jini-ai/agentic": "^0.6.15-rc.2",
452
- "@jini-ai/ui": "^0.6.15-rc.2",
453
- "@jini-ai/ui-kit": "^0.6.15-rc.2",
454
- "@jini-ai/agent-plugins": "^0.6.15-rc.2",
455
- "@jini-ai/cms": "^0.6.15-rc.2",
456
- "@jini-ai/core": "^0.6.15-rc.2"
461
+ "@jini-ai/agentic": "^0.6.10",
462
+ "@jini-ai/ui": "^0.6.10",
463
+ "@jini-ai/ui-kit": "^0.6.10",
464
+ "@jini-ai/agent-plugins": "^0.6.10",
465
+ "@jini-ai/cms": "^0.6.10",
466
+ "@jini-ai/core": "^0.6.10"
467
+ },
468
+ "devDependencies": {
469
+ "@testing-library/jest-dom": "^6.6.3",
470
+ "@testing-library/react": "^16.3.0",
471
+ "@types/react": "^19.2.0",
472
+ "@types/react-dom": "^19.2.0",
473
+ "@vitest/coverage-v8": "^2.1.9",
474
+ "jsdom": "^25.0.1",
475
+ "react": "^19.2.0",
476
+ "react-dom": "^19.2.0",
477
+ "@testing-library/user-event": "^14.6.1",
478
+ "@jini-ai/ui-kit": "^0.6.16",
479
+ "@jini-ai/agent-plugins": "^0.6.16",
480
+ "@jini-ai/cms": "^0.6.16",
481
+ "@jini-ai/agentic": "^0.6.16",
482
+ "@jini-ai/ui": "^0.6.16",
483
+ "@jini-ai/core": "^0.6.16"
484
+ },
485
+ "typesVersions": {
486
+ "*": {
487
+ "core/react": [
488
+ "./dist/core/react/index.d.ts"
489
+ ]
490
+ }
457
491
  },
458
492
  "peerDependenciesMeta": {
459
- "react": {
460
- "optional": true
461
- },
462
- "react-dom": {
463
- "optional": true
464
- },
465
493
  "@jini-ai/agentic": {
466
494
  "optional": true
467
495
  },
@@ -479,30 +507,12 @@
479
507
  },
480
508
  "@jini-ai/core": {
481
509
  "optional": true
482
- }
483
- },
484
- "devDependencies": {
485
- "@testing-library/jest-dom": "^6.6.3",
486
- "@testing-library/react": "^16.3.0",
487
- "@types/react": "^19.2.0",
488
- "@types/react-dom": "^19.2.0",
489
- "@vitest/coverage-v8": "^2.1.9",
490
- "jsdom": "^25.0.1",
491
- "react": "^19.2.0",
492
- "react-dom": "^19.2.0",
493
- "@testing-library/user-event": "^14.6.1",
494
- "@jini-ai/agentic": "^0.6.15-rc.2",
495
- "@jini-ai/core": "^0.6.15-rc.2",
496
- "@jini-ai/agent-plugins": "^0.6.15-rc.2",
497
- "@jini-ai/cms": "^0.6.15-rc.2",
498
- "@jini-ai/ui": "^0.6.15-rc.2",
499
- "@jini-ai/ui-kit": "^0.6.15-rc.2"
500
- },
501
- "typesVersions": {
502
- "*": {
503
- "core/react": [
504
- "./dist/core/react/index.d.ts"
505
- ]
510
+ },
511
+ "react": {
512
+ "optional": true
513
+ },
514
+ "react-dom": {
515
+ "optional": true
506
516
  }
507
517
  },
508
518
  "scripts": {