@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.
- package/dist/agent-plugins/adapters/memory.js +9 -3
- package/dist/agent-plugins/conformance/agent-plugins-api.conformance.js +1 -1
- package/dist/agent-plugins/controllers/agent-plugins.controller.js +14 -5
- package/dist/agent-plugins/package-file-tree.js +4 -6
- package/dist/agent-plugins/react/components/AgentPluginInspector.js +5 -2
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +10 -4
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +8 -5
- package/dist/agent-plugins/rules.js +3 -1
- package/dist/comments/adapters/http.js +2 -1
- package/dist/comments/adapters/memory.js +3 -1
- package/dist/comments/conformance/comments-api.conformance.js +3 -2
- package/dist/comments/controllers/comment-queue.controller.d.ts +1 -1
- package/dist/comments/controllers/comment-queue.controller.js +4 -2
- package/dist/core/entities/rules.js +13 -20
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/manifest/rules.d.ts +20 -0
- package/dist/core/manifest/rules.js +34 -0
- package/dist/core/manifest/types.d.ts +9 -0
- package/dist/core/module/create-admin.js +2 -1
- package/dist/core/omit-keys.d.ts +7 -0
- package/dist/core/omit-keys.js +12 -0
- package/dist/core/ports/forms.d.ts +5 -3
- package/dist/core/transport/query-suffix.d.ts +3 -0
- package/dist/core/transport/query-suffix.js +5 -0
- package/dist/forms/adapters/http.js +3 -2
- package/dist/forms/adapters/memory.js +3 -2
- package/dist/forms/controllers/field-attributes-dialog.controller.js +17 -3
- package/dist/forms/controllers/form-fields-editor.controller.js +1 -2
- package/dist/forms/controllers/form-submission-detail.controller.js +1 -2
- package/dist/forms/controllers/form-submissions.controller.js +1 -2
- package/dist/forms/controllers/forms-list.controller.js +1 -2
- package/dist/forms/field-settings.js +15 -4
- package/dist/forms/react/hooks/forms-view.hooks.d.ts +2 -2
- package/dist/forms/react/hooks/forms-view.hooks.js +9 -1
- package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.js +1 -3
- package/dist/forms/react/hooks/use-form-editor.hooks.js +8 -9
- package/dist/forms/react/hooks/use-form-submission-detail.hooks.js +1 -4
- package/dist/forms/react/hooks/use-form-submissions.hooks.js +1 -5
- package/dist/forms/react/hooks/use-forms-list.hooks.js +1 -2
- package/dist/forms/rules.js +7 -2
- package/dist/media/adapters/memory.js +5 -3
- package/dist/media/controllers/library.controller.js +4 -2
- package/dist/media/controllers/providers.controller.js +2 -1
- package/dist/media/react/components/MediaCard.js +11 -1
- package/dist/media/react/hooks/EditMediaPanel.hooks.js +18 -7
- package/dist/media/react/hooks/LibraryTab.hooks.js +3 -2
- package/dist/media/react/hooks/ProvidersTab.hooks.js +19 -9
- package/dist/media/rules.js +6 -3
- package/dist/playground/adapters/memory.js +26 -7
- package/dist/playground/conformance/playground-api.conformance.js +6 -3
- package/dist/playground/react/hooks/PlaygroundPorts.hooks.js +6 -2
- package/dist/react/components/ConfirmButton/ConfirmButton.hooks.js +1 -1
- package/dist/react/components/ConfirmButton/ConfirmButton.js +2 -5
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +2 -2
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +3 -1
- package/dist/react/components/RowMenu/RowMenu.hooks.js +4 -8
- package/dist/react/components/Sidebar.js +1 -1
- package/dist/react/entities/EntityField.js +1 -0
- package/dist/react/entities/contribution.js +3 -1
- package/dist/react/entities/types.js +1 -1
- package/dist/react/overlays.js +2 -1
- package/dist/react/shell/AdminShell.hooks.js +1 -1
- package/dist/react/shell/use-admin-shell-session.js +2 -1
- package/dist/react/shell/use-admin-theme.js +6 -6
- package/dist/redirects/adapters/memory.js +16 -13
- package/dist/security/adapters/http.js +10 -11
- package/dist/security/adapters/memory.js +50 -15
- package/dist/security/conformance/security-api.conformance.js +6 -3
- package/dist/security/controllers/access-tokens.controller.js +7 -3
- package/dist/security/controllers/other-credentials.controller.js +4 -2
- package/dist/security/controllers/root-key.controller.js +12 -5
- package/dist/security/react/hooks/AccessTokensTab.hooks.js +8 -3
- package/dist/security/react/hooks/SecurityPorts.hooks.js +6 -2
- package/dist/security/rules.js +19 -5
- package/dist/seo/adapters/memory.js +5 -2
- package/dist/seo/controllers/seo-entry.controller.js +2 -1
- package/dist/seo/controllers/sitemap.controller.js +2 -1
- package/dist/seo/react/hooks/MediaPickerBridge.hooks.js +1 -1
- package/dist/seo/react/hooks/use-seo.hooks.js +1 -1
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +1 -1
- package/dist/seo/react/hooks/use-sitemap-modal.hooks.js +9 -1
- package/dist/seo/react/pages/SeoPage.js +7 -1
- package/dist/source-control/adapters/memory.js +30 -14
- package/dist/source-control/controllers/source-control.controller.js +5 -2
- package/dist/source-control/react/components/SourceControlProviderRow.js +6 -1
- package/dist/source-control/react/hooks/SourceControlPorts.hooks.js +6 -2
- package/dist/source-control/rules.js +9 -3
- package/dist/widgets/adapters/http.js +2 -1
- package/dist/widgets/adapters/memory.js +4 -2
- package/dist/widgets/conformance/widgets-api.conformance.js +5 -2
- package/dist/widgets/controllers/instance-editor.controller.js +3 -2
- package/dist/widgets/controllers/library.controller.js +2 -1
- package/dist/widgets/controllers/region-editor.controller.js +2 -1
- package/dist/widgets/controllers/regions.controller.js +2 -1
- package/dist/widgets/react/components/WidgetInstanceNotices.d.ts +1 -1
- package/dist/widgets/react/components/WidgetInstanceNotices.js +2 -0
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +5 -2
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +5 -2
- package/dist/widgets/react/pages/WidgetInstanceEditor.js +1 -1
- package/dist/widgets/react/pages/WidgetRegionEditor.js +1 -1
- package/dist/widgets/react/pages/WidgetsLibrary.js +1 -1
- package/dist/widgets/rules.js +5 -1
- 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:
|
|
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
|
|
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) {
|
|
8
|
-
|
|
9
|
-
|
|
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) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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-${
|
|
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 = {}) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
fail('
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" })] }),
|
|
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 = {}) {
|
|
5
|
-
|
|
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 = {}) {
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(() => {
|
|
17
|
-
|
|
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(() => {
|
|
16
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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)
|
package/dist/widgets/rules.js
CHANGED
|
@@ -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?.() ??
|
|
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.
|
|
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.
|
|
452
|
-
"@jini-ai/ui": "^0.6.
|
|
453
|
-
"@jini-ai/ui-kit": "^0.6.
|
|
454
|
-
"@jini-ai/agent-plugins": "^0.6.
|
|
455
|
-
"@jini-ai/cms": "^0.6.
|
|
456
|
-
"@jini-ai/core": "^0.6.
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
"
|
|
487
|
-
|
|
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": {
|