@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.
- 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/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/html-rules.js +10 -13
- 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/html-attributes.d.ts +1 -8
- package/dist/media/html-attributes.js +6 -161
- 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 +2 -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/SeoPage.hooks.d.ts +1 -21
- package/dist/seo/react/hooks/SeoPage.hooks.js +0 -8
- 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 +17 -3
- 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 +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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 = {}) {
|
|
90
|
-
|
|
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() {
|
|
59
|
-
|
|
60
|
-
|
|
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 = {}) {
|
|
10
|
-
|
|
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
|
package/dist/security/rules.js
CHANGED
|
@@ -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 = {}) {
|
|
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 = {}) {
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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) {
|
|
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
|