@jini-ai/admin 0.4.12 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/agent-plugins/controllers/agent-plugin-files.controller.js +14 -9
- package/dist/agent-plugins/controllers/agent-plugins.controller.js +12 -9
- package/dist/agent-plugins/package-file-tree.js +54 -51
- package/dist/agent-plugins/react/components/AgentPluginRow.d.ts +1 -1
- package/dist/agent-plugins/react/components/AgentPluginRow.js +9 -2
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.d.ts +5 -5
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +14 -5
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.d.ts +15 -13
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.js +54 -40
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.d.ts +13 -13
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.js +23 -11
- package/dist/browser/navigation.js +44 -27
- package/dist/comments/adapters/memory.js +30 -12
- package/dist/comments/conformance/comments-api.conformance.js +55 -33
- package/dist/comments/controllers/comment-queue.controller.js +13 -4
- package/dist/comments/react/hooks/use-comment-queue.hooks.d.ts +17 -2
- package/dist/comments/react/hooks/use-comment-queue.hooks.js +79 -30
- package/dist/comments/react/hooks/use-comment-settings.hooks.js +9 -5
- package/dist/comments/react/index.d.ts +2 -1
- package/dist/comments/react/index.js +1 -1
- package/dist/consent/react/ConsentFields.d.ts +35 -0
- package/dist/consent/react/ConsentFields.js +74 -0
- package/dist/consent/react/ConsentPreviewFrame.d.ts +14 -0
- package/dist/consent/react/ConsentPreviewFrame.js +14 -0
- package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -4
- package/dist/consent/react/ConsentSettingsPanel.js +6 -17
- package/dist/consent/react/index.d.ts +6 -1
- package/dist/consent/react/index.js +2 -0
- package/dist/consent/react/settings-panel.hooks.d.ts +28 -1
- package/dist/consent/react/settings-panel.hooks.js +26 -0
- package/dist/core/entities/rules.js +126 -81
- package/dist/core/manifest/rules.js +10 -10
- package/dist/core/module/create-admin.js +195 -117
- package/dist/core/ports/redirects.d.ts +8 -6
- package/dist/core/ports/redirects.js +8 -6
- package/dist/core/routing/rules.js +9 -4
- package/dist/core/transport/http.js +37 -20
- package/dist/forms/adapters/memory.js +22 -11
- package/dist/forms/conformance/forms-api.conformance.d.ts +6 -4
- package/dist/forms/conformance/forms-api.conformance.js +42 -46
- package/dist/forms/field-settings.js +150 -115
- package/dist/forms/html-rules.js +45 -28
- package/dist/forms/react/components/FieldSettingsRows.d.ts +22 -0
- package/dist/forms/react/components/FieldSettingsRows.js +29 -0
- package/dist/forms/react/components/FieldSettingsSection.d.ts +1 -1
- package/dist/forms/react/components/FieldSettingsSection.js +5 -6
- package/dist/forms/react/components/FormEditorFieldsBody.d.ts +1 -1
- package/dist/forms/react/components/FormEditorFieldsBody.js +4 -15
- package/dist/forms/react/components/FormEditorFieldsBodyParts.d.ts +18 -0
- package/dist/forms/react/components/FormEditorFieldsBodyParts.js +32 -0
- package/dist/forms/react/components/FormSubmissions.d.ts +1 -1
- package/dist/forms/react/components/FormSubmissions.js +4 -20
- package/dist/forms/react/components/FormSubmissionsTable.d.ts +19 -0
- package/dist/forms/react/components/FormSubmissionsTable.js +22 -0
- package/dist/media/adapters/memory.js +31 -26
- package/dist/media/controllers/edit-media.controller.js +2 -1
- package/dist/media/controllers/library.controller.js +48 -35
- package/dist/media/controllers/providers.controller.js +2 -1
- package/dist/media/html-attributes.js +29 -15
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/react/components/MediaIcons.d.ts +0 -3
- package/dist/media/react/components/MediaIcons.js +0 -9
- package/dist/media/react/components/MediaLightbox.d.ts +2 -1
- package/dist/media/react/components/MediaLightbox.js +11 -5
- package/dist/media/react/hooks/LibraryTab.hooks.d.ts +55 -45
- package/dist/media/react/hooks/LibraryTab.hooks.js +107 -70
- package/dist/media/react/hooks/MediaCard.hooks.d.ts +27 -31
- package/dist/media/react/hooks/MediaCard.hooks.js +52 -26
- package/dist/media/react/hooks/MediaGrid.hooks.d.ts +1 -1
- package/dist/media/react/hooks/MediaLightbox.hooks.d.ts +10 -14
- package/dist/media/react/hooks/MediaLightbox.hooks.js +9 -56
- package/dist/media/react/hooks/MediaPage.hooks.d.ts +6 -0
- package/dist/media/react/hooks/MediaPage.hooks.js +29 -12
- package/dist/media/react/hooks/ProvidersTab.hooks.d.ts +33 -33
- package/dist/media/react/hooks/ProvidersTab.hooks.js +119 -79
- package/dist/media/react/hooks/UploadButton.hooks.d.ts +7 -5
- package/dist/media/react/hooks/UploadButton.hooks.js +36 -24
- package/dist/media/react/index.js +4 -0
- package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
- package/dist/media/react/tabs/LibraryTab.js +2 -2
- package/dist/media/rules.d.ts +7 -0
- package/dist/media/rules.js +25 -2
- package/dist/react/components/ConfirmDialog/ConfirmDialog.js +5 -8
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.d.ts +20 -0
- package/dist/react/components/ConfirmDialog/confirm-dialog-attrs.js +24 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +16 -7
- package/dist/react/components/RowMenu/RowMenu.js +16 -10
- package/dist/react/components/Sidebar.js +32 -26
- package/dist/react/entities/EntityEdit.js +17 -14
- package/dist/react/entities/EntityField.d.ts +5 -2
- package/dist/react/entities/EntityField.js +38 -23
- package/dist/react/entities/EntityList.js +6 -5
- package/dist/react/entities/RelationTruncationNotice.d.ts +9 -0
- package/dist/react/entities/RelationTruncationNotice.js +9 -0
- package/dist/react/entities/use-entity-edit.d.ts +4 -2
- package/dist/react/entities/use-entity-edit.js +52 -33
- package/dist/react/shell/AdminShell.js +40 -23
- package/dist/react/shell/model.d.ts +4 -1
- package/dist/react/shell/model.js +3 -2
- package/dist/react/shell/use-admin-shell-session.js +25 -26
- package/dist/redirects/adapters/memory.js +9 -2
- package/dist/redirects/conformance/redirects-api.conformance.js +66 -36
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +1 -1
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +10 -3
- package/dist/redirects/react/hooks/wired.hooks.d.ts +1 -1
- package/dist/security/adapters/http.js +25 -10
- package/dist/security/adapters/memory.js +19 -5
- package/dist/security/controllers/access-tokens.controller.js +27 -26
- package/dist/security/controllers/other-credentials.controller.js +4 -2
- package/dist/security/controllers/root-key.controller.js +17 -9
- package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +40 -38
- package/dist/security/react/hooks/AccessTokensTab.hooks.js +36 -9
- package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +14 -14
- package/dist/security/react/hooks/RootKeyTab.hooks.js +12 -3
- package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
- package/dist/security/react/tabs/RootKeyTab.js +12 -3
- package/dist/security/rules.js +45 -25
- package/dist/seo/controllers/seo.controller.js +4 -2
- package/dist/seo/react/components/MediaRefField.d.ts +1 -1
- package/dist/seo/react/components/MediaRefField.js +19 -2
- package/dist/seo/react/hooks/MediaRefField.hooks.js +3 -1
- package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -1
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -34
- package/dist/seo/react/pages/SeoPage.d.ts +4 -4
- package/dist/seo/react/pages/SeoPage.js +26 -6
- package/dist/seo/rules.js +39 -10
- package/dist/source-control/conformance/source-control-api.conformance.js +68 -38
- package/dist/source-control/controllers/source-control.controller.js +20 -8
- package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +2 -1
- package/dist/source-control/react/hooks/ProvidersTab.hooks.js +18 -11
- package/dist/widgets/conformance/widgets-api.conformance.js +15 -1
- package/dist/widgets/controllers/instance-editor.controller.js +33 -24
- package/dist/widgets/controllers/library.controller.js +18 -12
- package/dist/widgets/controllers/region-editor.controller.js +6 -5
- package/dist/widgets/react/hooks/page-views.hooks.d.ts +1 -0
- package/dist/widgets/react/hooks/page-views.hooks.js +3 -2
- package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +1 -1
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +9 -3
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +9 -2
- package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +1 -1
- package/dist/widgets/react/pages/WidgetInstanceEditor.js +7 -8
- package/dist/widgets/rules.d.ts +14 -2
- package/dist/widgets/rules.js +9 -6
- package/package.json +17 -8
- package/src/themes/default/admin.css +51 -0
- package/src/themes/default/chat.css +21 -0
- package/src/themes/default/manifest.json +29 -0
- package/dist/media/styles.d.ts +0 -4
- package/dist/media/styles.js +0 -5
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { createControllerStore } from '../../core/module/controller-store.js';
|
|
2
2
|
import { ROOT_KEY_PERMISSION, START_FRESH_CONFIRMATION, isRootKeyLocked } from '../rules.js';
|
|
3
|
+
/** Receivers named in a result need their new signing secret; the message tells the operator which ones. */
|
|
4
|
+
function resultMessage({ result, response }, _optional = {}) {
|
|
5
|
+
const hooks = response.affectedWebhooks;
|
|
6
|
+
return hooks?.length ? `${result} Update receivers: ${hooks.map(w => `${w.label} (${w.targetUrl})`).join(', ')}` : result;
|
|
7
|
+
}
|
|
3
8
|
export function createRootKeyController({ api, permissions = [] }, _optional = {}) {
|
|
4
9
|
const granted = permissions.includes(ROOT_KEY_PERMISSION);
|
|
5
10
|
const store = createControllerStore({ initial: { status: null, preview: null, token: '', confirm: '', loading: false, saving: false, error: null, result: null, confirming: false } });
|
|
6
11
|
let generation = 0;
|
|
7
12
|
const allowed = () => granted && !store.signal.aborted;
|
|
13
|
+
const canPreview = () => { const state = store.getSnapshot(); return allowed() && isRootKeyLocked({ status: state.status }) && !state.saving && !state.loading; };
|
|
8
14
|
async function load(_required, _optional = {}) {
|
|
9
15
|
if (!allowed())
|
|
10
16
|
return;
|
|
@@ -28,19 +34,20 @@ export function createRootKeyController({ api, permissions = [] }, _optional = {
|
|
|
28
34
|
if (!allowed() || store.getSnapshot().saving)
|
|
29
35
|
return false;
|
|
30
36
|
store.set({ patch: { saving: true, error: null } });
|
|
37
|
+
let ok = false;
|
|
31
38
|
try {
|
|
32
39
|
const response = await run();
|
|
33
|
-
store.set({ patch: { token: '', confirm: '', preview: null, confirming: false, result:
|
|
40
|
+
store.set({ patch: { token: '', confirm: '', preview: null, confirming: false, result: resultMessage({ result, response }) } });
|
|
34
41
|
await load({});
|
|
35
|
-
|
|
42
|
+
ok = !store.signal.aborted;
|
|
36
43
|
}
|
|
37
44
|
catch {
|
|
38
45
|
store.set({ patch: { error: 'Recovery refused. Check the host key configuration.' } });
|
|
39
|
-
return false;
|
|
40
46
|
}
|
|
41
47
|
finally {
|
|
42
48
|
store.set({ patch: { saving: false } });
|
|
43
49
|
}
|
|
50
|
+
return ok;
|
|
44
51
|
}
|
|
45
52
|
return {
|
|
46
53
|
getSnapshot: store.getSnapshot, subscribe: store.subscribe, load,
|
|
@@ -57,26 +64,27 @@ export function createRootKeyController({ api, permissions = [] }, _optional = {
|
|
|
57
64
|
},
|
|
58
65
|
importToken(_required, _optional = {}) { const token = store.getSnapshot().token.trim(); return token && isRootKeyLocked({ status: store.getSnapshot().status }) ? execute(() => api.importToken({ token }, { signal: store.signal }), 'Unlocked. Saved credentials work again.') : Promise.resolve(false); },
|
|
59
66
|
async preview(_required, _optional = {}) {
|
|
60
|
-
if (!
|
|
67
|
+
if (!canPreview())
|
|
61
68
|
return false;
|
|
62
69
|
const ticket = ++generation;
|
|
63
70
|
store.set({ patch: { loading: true, preview: null, confirm: '', error: null } });
|
|
71
|
+
let ok = false;
|
|
64
72
|
try {
|
|
65
73
|
const preview = await api.previewStartFresh({}, { signal: store.signal });
|
|
66
|
-
if (ticket
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
if (ticket === generation && allowed()) {
|
|
75
|
+
store.set({ patch: { preview, confirming: true } });
|
|
76
|
+
ok = true;
|
|
77
|
+
}
|
|
70
78
|
}
|
|
71
79
|
catch {
|
|
72
80
|
if (ticket === generation)
|
|
73
81
|
store.set({ patch: { error: 'Recovery preview unavailable' } });
|
|
74
|
-
return false;
|
|
75
82
|
}
|
|
76
83
|
finally {
|
|
77
84
|
if (ticket === generation)
|
|
78
85
|
store.set({ patch: { loading: false } });
|
|
79
86
|
}
|
|
87
|
+
return ok;
|
|
80
88
|
},
|
|
81
89
|
startFresh(_required, _optional = {}) { const state = store.getSnapshot(); if (!isRootKeyLocked({ status: state.status }) || !state.preview || !state.confirming || state.confirm !== START_FRESH_CONFIRMATION)
|
|
82
90
|
return Promise.resolve(false); return execute(() => api.startFresh({ confirm: state.confirm }, { signal: store.signal }), 'Done. Re-enter removed credentials.'); },
|
|
@@ -1,44 +1,11 @@
|
|
|
1
1
|
import type { TabViewProps } from '../../../core/react/bind-react.js';
|
|
2
2
|
import type { CredentialSummary, OtherCredentialSummary, CredentialCategory } from '../../models.js';
|
|
3
|
+
type Removal = {
|
|
4
|
+
row: CredentialSummary | OtherCredentialSummary;
|
|
5
|
+
type: 'access' | 'other';
|
|
6
|
+
};
|
|
3
7
|
export declare function useAccessTokensTab({ permissions }: TabViewProps, _optional?: {}): {
|
|
4
|
-
|
|
5
|
-
loading: boolean;
|
|
6
|
-
saving: boolean;
|
|
7
|
-
rows: {
|
|
8
|
-
row: CredentialSummary;
|
|
9
|
-
label: string;
|
|
10
|
-
canWrite: boolean;
|
|
11
|
-
edit(): void;
|
|
12
|
-
remove(): void;
|
|
13
|
-
makeDefault(): void;
|
|
14
|
-
}[];
|
|
15
|
-
otherRows: {
|
|
16
|
-
row: OtherCredentialSummary;
|
|
17
|
-
canWrite: boolean;
|
|
18
|
-
token: string;
|
|
19
|
-
onToken({ value }: {
|
|
20
|
-
value: string;
|
|
21
|
-
}): void;
|
|
22
|
-
replace(): void;
|
|
23
|
-
remove(): void;
|
|
24
|
-
}[];
|
|
25
|
-
providers: {
|
|
26
|
-
provider: import("../../models.js").CredentialProvider;
|
|
27
|
-
canWrite: boolean;
|
|
28
|
-
add(): void;
|
|
29
|
-
}[];
|
|
30
|
-
categories: readonly {
|
|
31
|
-
value: CredentialCategory | "all";
|
|
32
|
-
label: string;
|
|
33
|
-
}[];
|
|
34
|
-
query: string;
|
|
35
|
-
category: "all" | CredentialCategory;
|
|
36
|
-
total: number;
|
|
37
|
-
matched: number;
|
|
38
|
-
removal: {
|
|
39
|
-
row: CredentialSummary | OtherCredentialSummary;
|
|
40
|
-
type: "access" | "other";
|
|
41
|
-
} | null;
|
|
8
|
+
removal: Removal | null;
|
|
42
9
|
draft: import("../../models.js").CredentialDraft | null;
|
|
43
10
|
errors: string[];
|
|
44
11
|
canAddCustom: boolean;
|
|
@@ -90,5 +57,40 @@ export declare function useAccessTokensTab({ permissions }: TabViewProps, _optio
|
|
|
90
57
|
}, _optional?: {}): Promise<boolean>;
|
|
91
58
|
dispose(_required?: Record<string, never>, _optional?: {}): void;
|
|
92
59
|
} | null;
|
|
60
|
+
total: number;
|
|
61
|
+
matched: number;
|
|
62
|
+
rows: {
|
|
63
|
+
row: CredentialSummary;
|
|
64
|
+
label: string;
|
|
65
|
+
canWrite: boolean;
|
|
66
|
+
edit(): void;
|
|
67
|
+
remove(): void;
|
|
68
|
+
makeDefault(): void;
|
|
69
|
+
}[];
|
|
70
|
+
otherRows: {
|
|
71
|
+
row: OtherCredentialSummary;
|
|
72
|
+
canWrite: boolean;
|
|
73
|
+
token: string;
|
|
74
|
+
onToken({ value }: {
|
|
75
|
+
value: string;
|
|
76
|
+
}): void;
|
|
77
|
+
replace(): void;
|
|
78
|
+
remove(): void;
|
|
79
|
+
}[];
|
|
80
|
+
providers: {
|
|
81
|
+
provider: import("../../models.js").CredentialProvider;
|
|
82
|
+
canWrite: boolean;
|
|
83
|
+
add(): void;
|
|
84
|
+
}[];
|
|
85
|
+
categories: readonly {
|
|
86
|
+
value: CredentialCategory | "all";
|
|
87
|
+
label: string;
|
|
88
|
+
}[];
|
|
89
|
+
query: string;
|
|
90
|
+
category: "all" | CredentialCategory;
|
|
91
|
+
loading: boolean;
|
|
92
|
+
saving: boolean;
|
|
93
|
+
denied: boolean;
|
|
93
94
|
};
|
|
95
|
+
export {};
|
|
94
96
|
//# sourceMappingURL=AccessTokensTab.hooks.d.ts.map
|
|
@@ -4,6 +4,35 @@ import { createAccessTokensController } from '../../controllers/access-tokens.co
|
|
|
4
4
|
import { createOtherCredentialsController } from '../../controllers/other-credentials.controller.js';
|
|
5
5
|
import { CATEGORIES, credentialLabel, filterCredentials, filterOtherCredentials, otherWritePermission, writePermission } from '../../rules.js';
|
|
6
6
|
import { noOtherCredentials, useSecurityPorts } from './SecurityPorts.hooks.js';
|
|
7
|
+
function currentFilter({ snapshot: s }, _optional = {}) { return { query: s?.query ?? '', category: s?.category ?? 'all' }; }
|
|
8
|
+
function accessRows({ snapshot: s, controller, scope }, _optional = {}) {
|
|
9
|
+
if (!s)
|
|
10
|
+
return [];
|
|
11
|
+
const { permissions, query, category, setRemoval } = scope;
|
|
12
|
+
return filterCredentials({ rows: s.rows, providers: s.providers, query, category }).map(row => ({ row, label: credentialLabel({ row, providers: s.providers }), canWrite: permissions.includes(writePermission({ kind: row.kind })), edit() { controller?.begin({ row }); }, remove() { setRemoval({ row, type: 'access' }); }, makeDefault() { void controller?.makeDefault({ row }); } }));
|
|
13
|
+
}
|
|
14
|
+
function otherRowsView({ snapshot: o, controller, scope }, _optional = {}) {
|
|
15
|
+
if (!o)
|
|
16
|
+
return [];
|
|
17
|
+
const { permissions, query, category, setRemoval } = scope;
|
|
18
|
+
return filterOtherCredentials({ rows: o.rows, query, category }).map(row => ({ row, canWrite: permissions.includes(otherWritePermission({ store: row.store })), token: o.drafts[`${row.store}:${row.id}`] ?? '', onToken({ value }) { controller?.setDraft({ key: `${row.store}:${row.id}`, token: value }); }, replace() { void controller?.replace({ row }); }, remove() { setRemoval({ row, type: 'other' }); } }));
|
|
19
|
+
}
|
|
20
|
+
function providerOptions({ snapshot: s, controller, scope }, _optional = {}) {
|
|
21
|
+
const { permissions, query, category } = scope;
|
|
22
|
+
return (s?.providers ?? []).filter(p => (category === 'all' || p.category === category) && `${p.label} ${p.id}`.toLowerCase().includes(query.trim().toLowerCase())).map(provider => ({ provider, canWrite: permissions.includes(writePermission({ kind: provider.kind })), add() { controller?.begin({ kind: provider.kind, providerId: provider.id }); } }));
|
|
23
|
+
}
|
|
24
|
+
function tokenCounts({ access: s, other: o, query }, _optional = {}) {
|
|
25
|
+
// Count remains global across categories; the search count answers how many saved tokens this host holds.
|
|
26
|
+
const total = (s?.rows.length ?? 0) + (o?.rows.length ?? 0);
|
|
27
|
+
const matched = (s ? filterCredentials({ rows: s.rows, providers: s.providers, query, category: 'all' }).length : 0) + (o ? filterOtherCredentials({ rows: o.rows, query, category: 'all' }).length : 0);
|
|
28
|
+
return { total, matched };
|
|
29
|
+
}
|
|
30
|
+
function tabStatus({ access: s, other: o }, _optional = {}) {
|
|
31
|
+
return { loading: s?.loading ?? true, saving: (s?.saving ?? false) || (o?.saving ?? false) };
|
|
32
|
+
}
|
|
33
|
+
function errorList({ access: s, other: o }, _optional = {}) {
|
|
34
|
+
return [...(s?.error ? [s.error] : []), ...Object.values(s?.errors ?? {}), ...Object.values(o?.errors ?? {})];
|
|
35
|
+
}
|
|
7
36
|
export function useAccessTokensTab({ permissions = [] }, _optional = {}) {
|
|
8
37
|
const ports = useSecurityPorts({});
|
|
9
38
|
const key = permissions.join('\0');
|
|
@@ -12,15 +41,13 @@ export function useAccessTokensTab({ permissions = [] }, _optional = {}) {
|
|
|
12
41
|
const other = useController({ create: () => createOtherCredentialsController({ api: ports.otherCredentials ?? noOtherCredentials, permissions }), dependencies: [ports.otherCredentials, key] }, { start: ({ controller }) => { void controller.load({}); } });
|
|
13
42
|
const [removal, setRemoval] = useState(null);
|
|
14
43
|
const s = access.snapshot, o = other.snapshot;
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return { denied: !canRead, loading: s?.loading ?? true, saving: (s?.saving ?? false) || (o?.saving ?? false), rows, otherRows, providers, categories: CATEGORIES, query, category, total, matched, removal,
|
|
23
|
-
draft: s?.draft ?? null, errors: [...(s?.error ? [s.error] : []), ...Object.values(s?.errors ?? {}), ...Object.values(o?.errors ?? {})],
|
|
44
|
+
const { query, category } = currentFilter({ snapshot: s });
|
|
45
|
+
const scope = { permissions, query, category, setRemoval };
|
|
46
|
+
const rows = accessRows({ snapshot: s, controller: access.controller, scope });
|
|
47
|
+
const otherRows = otherRowsView({ snapshot: o, controller: other.controller, scope });
|
|
48
|
+
const providers = providerOptions({ snapshot: s, controller: access.controller, scope });
|
|
49
|
+
return { denied: !canRead, ...tabStatus({ access: s, other: o }), rows, otherRows, providers, categories: CATEGORIES, query, category, ...tokenCounts({ access: s, other: o, query }), removal,
|
|
50
|
+
draft: s?.draft ?? null, errors: errorList({ access: s, other: o }),
|
|
24
51
|
canAddCustom: permissions.includes('custom-credentials.write'),
|
|
25
52
|
addCustom() { access.controller?.begin({ kind: 'custom' }); },
|
|
26
53
|
onQuery({ value }) { access.controller?.filter({ query: value }); },
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
import type { TabViewProps } from '../../../core/react/bind-react.js';
|
|
2
2
|
export declare function useRootKeyTab({ permissions, params }: TabViewProps, _optional?: {}): {
|
|
3
|
-
denied: boolean;
|
|
4
|
-
loading: boolean;
|
|
5
|
-
saving: boolean;
|
|
6
|
-
status: import("../../models.js").RootKeyStatus | null | undefined;
|
|
7
|
-
error: string | null | undefined;
|
|
8
|
-
result: string | null | undefined;
|
|
9
|
-
scopeNotice: string;
|
|
10
|
-
token: string;
|
|
11
|
-
confirm: string;
|
|
12
|
-
confirming: boolean;
|
|
13
|
-
preview: import("../../models.js").RootKeyPreview | null | undefined;
|
|
14
|
-
locked: boolean;
|
|
15
|
-
canGenerate: boolean;
|
|
16
|
-
canConfirm: boolean;
|
|
17
3
|
onToken({ value }: {
|
|
18
4
|
value: string;
|
|
19
5
|
}): void;
|
|
@@ -25,5 +11,19 @@ export declare function useRootKeyTab({ permissions, params }: TabViewProps, _op
|
|
|
25
11
|
previewFresh(): void;
|
|
26
12
|
cancelFresh(): void;
|
|
27
13
|
confirmFresh(): Promise<void>;
|
|
14
|
+
locked: boolean;
|
|
15
|
+
canGenerate: boolean;
|
|
16
|
+
canConfirm: boolean;
|
|
17
|
+
token: string;
|
|
18
|
+
confirm: string;
|
|
19
|
+
confirming: boolean;
|
|
20
|
+
preview: import("../../models.js").RootKeyPreview | null | undefined;
|
|
21
|
+
scopeNotice: string;
|
|
22
|
+
loading: boolean;
|
|
23
|
+
saving: boolean;
|
|
24
|
+
status: import("../../models.js").RootKeyStatus | null | undefined;
|
|
25
|
+
error: string | null | undefined;
|
|
26
|
+
result: string | null | undefined;
|
|
27
|
+
denied: boolean;
|
|
28
28
|
};
|
|
29
29
|
//# sourceMappingURL=RootKeyTab.hooks.d.ts.map
|
|
@@ -2,14 +2,23 @@ import { useController } from '../../../core/react/use-controller.js';
|
|
|
2
2
|
import { createRootKeyController } from '../../controllers/root-key.controller.js';
|
|
3
3
|
import { ROOT_KEY_PERMISSION, START_FRESH_CONFIRMATION, isRootKeyLocked } from '../../rules.js';
|
|
4
4
|
import { unavailableRootKey, useSecurityPorts } from './SecurityPorts.hooks.js';
|
|
5
|
+
/** Before the controller exists the tab reads as loading with nothing known yet. */
|
|
6
|
+
function rootKeyProgress({ snapshot: s }, _optional = {}) {
|
|
7
|
+
return { loading: s?.loading ?? true, saving: s?.saving ?? false, status: s?.status, error: s?.error, result: s?.result };
|
|
8
|
+
}
|
|
9
|
+
function rootKeyInputs({ snapshot: s }, _optional = {}) {
|
|
10
|
+
return { token: s?.token ?? '', confirm: s?.confirm ?? '', confirming: s?.confirming ?? false, preview: s?.preview };
|
|
11
|
+
}
|
|
12
|
+
function rootKeyFlags({ snapshot: s }, _optional = {}) {
|
|
13
|
+
return { locked: isRootKeyLocked({ status: s?.status ?? null }), canGenerate: s?.status?.state === 'missing' && !s.status.active, canConfirm: !!s?.preview && s.confirm === START_FRESH_CONFIRMATION };
|
|
14
|
+
}
|
|
5
15
|
export function useRootKeyTab({ permissions = [], params }, _optional = {}) {
|
|
6
16
|
const { rootKey } = useSecurityPorts({});
|
|
7
17
|
const key = permissions.join('\0');
|
|
8
18
|
const { controller, snapshot: s } = useController({ create: () => createRootKeyController({ api: rootKey ?? unavailableRootKey, permissions }), dependencies: [rootKey, key] }, { start: ({ controller }) => { void controller.load({}); } });
|
|
9
|
-
return { denied: !rootKey || !permissions.includes(ROOT_KEY_PERMISSION),
|
|
19
|
+
return { denied: !rootKey || !permissions.includes(ROOT_KEY_PERMISSION), ...rootKeyProgress({ snapshot: s }),
|
|
10
20
|
scopeNotice: typeof params.scopeNotice === 'string' ? params.scopeNotice : 'This key protects saved credentials. Keep its backup separate from the database.',
|
|
11
|
-
|
|
12
|
-
locked: isRootKeyLocked({ status: s?.status ?? null }), canGenerate: s?.status?.state === 'missing' && !s.status.active, canConfirm: !!s?.preview && s.confirm === START_FRESH_CONFIRMATION,
|
|
21
|
+
...rootKeyInputs({ snapshot: s }), ...rootKeyFlags({ snapshot: s }),
|
|
13
22
|
onToken({ value }) { controller?.setToken({ value }); }, onConfirmText({ value }) { controller?.setConfirm({ value }); },
|
|
14
23
|
generate() { void controller?.generate({}); }, unlock() { void controller?.importToken({}); }, previewFresh() { void controller?.preview({}); },
|
|
15
24
|
cancelFresh() { controller?.cancel({}); }, async confirmFresh() { if (!await controller?.startFresh({}))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { TabViewProps } from '../../../core/react/bind-react.js';
|
|
2
|
-
export declare function RootKeyTab(props: TabViewProps
|
|
2
|
+
export declare function RootKeyTab(props: TabViewProps): import("react").JSX.Element;
|
|
3
3
|
export default RootKeyTab;
|
|
4
4
|
//# sourceMappingURL=RootKeyTab.d.ts.map
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Button, TextField, Notice, ConfirmDialog, Spinner } from '@jini-ai/ui-kit/react';
|
|
3
3
|
import { useRootKeyTab } from '../hooks/RootKeyTab.hooks.js';
|
|
4
|
-
|
|
4
|
+
function RootKeyMessages({ vm }) {
|
|
5
|
+
return _jsxs(_Fragment, { children: [vm.loading && _jsx(Spinner, { label: "Loading root key status\u2026" }), vm.error && _jsx(Notice, { tone: "danger", children: vm.error }), vm.result && _jsx(Notice, { tone: "success", children: vm.result })] });
|
|
6
|
+
}
|
|
7
|
+
function LockedActions({ vm }) {
|
|
8
|
+
return _jsxs(_Fragment, { children: [_jsx(TextField, { label: "Old root key", type: "password", value: vm.token, onValueChange: vm.onToken, disabled: vm.saving, attrs: { 'data-agent-private': 'true' } }), _jsx(Button, { onPress: vm.unlock, pending: vm.saving, children: "Unlock saved credentials" }), _jsx(Button, { onPress: vm.previewFresh, disabled: vm.saving || vm.loading, children: "Start fresh" })] });
|
|
9
|
+
}
|
|
10
|
+
function StartFreshBody({ vm }) {
|
|
11
|
+
return _jsxs(_Fragment, { children: [_jsx("p", { children: vm.preview?.detail }), _jsxs("p", { children: [vm.preview?.removes, " credentials will be removed."] }), vm.preview?.affectedWebhooks.map(w => _jsxs("p", { children: [w.label, " \u00B7 ", w.targetUrl] }, `${w.label}:${w.targetUrl}`)), _jsx(TextField, { label: "Type START FRESH", value: vm.confirm, onValueChange: vm.onConfirmText, disabled: vm.saving })] });
|
|
12
|
+
}
|
|
13
|
+
export function RootKeyTab(props) {
|
|
5
14
|
const vm = useRootKeyTab(props);
|
|
6
|
-
return vm.denied ? _jsx(Notice, { tone: "danger", children: "Permission denied" }) : _jsxs("section", { "aria-label": "Root key", children: [_jsx(Notice, { children: vm.scopeNotice }),
|
|
15
|
+
return vm.denied ? _jsx(Notice, { tone: "danger", children: "Permission denied" }) : _jsxs("section", { "aria-label": "Root key", children: [_jsx(Notice, { children: vm.scopeNotice }), _jsx(RootKeyMessages, { vm: vm }), vm.status && _jsxs("p", { children: [vm.status.state, " \u00B7 ", vm.status.fingerprint, " \u00B7 ", vm.status.source] }), vm.canGenerate && _jsx(Button, { onPress: vm.generate, pending: vm.saving, children: "Create root key" }), vm.locked && _jsx(LockedActions, { vm: vm }), vm.confirming && _jsx(ConfirmDialog, { open: true, title: "Start fresh", body: _jsx(StartFreshBody, { vm: vm }), consequence: "Saved credentials will be removed. Update affected webhook receivers with new signing secrets.", confirmLabel: "Start fresh", onConfirm: vm.confirmFresh, onCancel: vm.cancelFresh, pending: vm.saving, agentMayConfirm: false, tone: "danger" })] });
|
|
7
16
|
}
|
|
8
17
|
export default RootKeyTab;
|
|
9
18
|
//# sourceMappingURL=RootKeyTab.js.map
|
package/dist/security/rules.js
CHANGED
|
@@ -9,8 +9,9 @@ export const OTHER_STORES = ['site-assistant', 'admin-byok', 'media-provider', '
|
|
|
9
9
|
export function writePermission({ kind }, _optional = {}) { return kind === 'custom' ? 'custom-credentials.write' : kind === 'publish' ? 'system.publish' : 'source-control.credentials.write'; }
|
|
10
10
|
export function otherWritePermission({ store }, _optional = {}) { return store === 'site-assistant' || store === 'admin-byok' ? 'admin.assistant.manage' : 'admin.integrations.manage'; }
|
|
11
11
|
export function credentialStore({ api, kind }, _optional = {}) { return kind === 'source-control' ? api.sourceControl : kind === 'publish' ? api.publish : api.custom; }
|
|
12
|
+
function defaultCategory({ kind }, _optional = {}) { return kind === 'publish' ? 'hosting' : kind === 'source-control' ? 'source-control' : 'general'; }
|
|
12
13
|
export function blankCredentialDraft({ kind, providerId = '' }, _optional = {}) {
|
|
13
|
-
return { kind, providerId, label: '', token: '', values: {}, category:
|
|
14
|
+
return { kind, providerId, label: '', token: '', values: {}, category: defaultCategory({ kind }), baseUrl: '', additionalHosts: '', username: '' };
|
|
14
15
|
}
|
|
15
16
|
/** Legacy sentinel labels are display-only: opening this page must never mutate saved data. */
|
|
16
17
|
export function credentialLabel({ row, providers }, _optional = {}) {
|
|
@@ -18,46 +19,65 @@ export function credentialLabel({ row, providers }, _optional = {}) {
|
|
|
18
19
|
return row.label;
|
|
19
20
|
return `${providers.find(p => p.kind === row.kind && p.id === row.providerId)?.label ?? row.providerId} token`;
|
|
20
21
|
}
|
|
21
|
-
export function credentialCategory({ row }, _optional = {}) { return row.category ?? (
|
|
22
|
+
export function credentialCategory({ row }, _optional = {}) { return row.category ?? defaultCategory({ kind: row.kind }); }
|
|
22
23
|
export function filterCredentials({ rows, query, category, providers }, _optional = {}) {
|
|
23
24
|
const q = query.trim().toLowerCase();
|
|
24
25
|
return rows.filter(row => (category === 'all' || credentialCategory({ row }) === category) && [credentialLabel({ row, providers }), row.providerId, row.baseUrl ?? '', row.accountLabel ?? ''].join(' ').toLowerCase().includes(q));
|
|
25
26
|
}
|
|
26
27
|
export function filterOtherCredentials({ rows, query, category }, _optional = {}) { return rows.filter(row => (category === 'all' || row.category === category) && `${row.label} ${row.store}`.toLowerCase().includes(query.trim().toLowerCase())); }
|
|
28
|
+
function findProvider({ draft, providers }, _optional = {}) { return providers.find(p => p.kind === draft.kind && p.id === draft.providerId); }
|
|
29
|
+
function isDuplicateLabel({ draft, rows }, _optional = {}) {
|
|
30
|
+
const label = draft.label.trim().toLowerCase();
|
|
31
|
+
return rows.some(r => r.kind === draft.kind && r.id !== draft.id && (draft.kind === 'custom' || r.providerId === draft.providerId) && r.label.trim().toLowerCase() === label);
|
|
32
|
+
}
|
|
33
|
+
function customDraftError({ draft }, _optional = {}) {
|
|
34
|
+
try {
|
|
35
|
+
if (!['http:', 'https:'].includes(new URL(draft.baseUrl).protocol))
|
|
36
|
+
return 'Enter an HTTP or HTTPS URL';
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return 'Enter an HTTP or HTTPS URL';
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function providerDraftError({ draft, providers }, _optional = {}) {
|
|
44
|
+
if (draft.id && !draft.token.trim())
|
|
45
|
+
return null;
|
|
46
|
+
const provider = findProvider({ draft, providers });
|
|
47
|
+
if (!provider)
|
|
48
|
+
return 'Provider is unavailable';
|
|
49
|
+
if (provider.fields.some(f => f.name !== provider.tokenField && f.required && !draft.values[f.name]?.trim()))
|
|
50
|
+
return 'Complete all required fields';
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
27
53
|
/** A custom row is its own provider identity; its username can change without retyping its token. */
|
|
28
54
|
export function validateCredentialDraft({ draft, rows, providers }, _optional = {}) {
|
|
29
55
|
if (!draft.label.trim())
|
|
30
56
|
return 'Enter a name';
|
|
31
|
-
if (
|
|
57
|
+
if (isDuplicateLabel({ draft, rows }))
|
|
32
58
|
return 'That name is already in use';
|
|
33
59
|
if (!draft.id && !draft.token.trim())
|
|
34
60
|
return 'Enter an access token';
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (provider.fields.some(f => f.name !== provider.tokenField && f.required && !draft.values[f.name]?.trim()))
|
|
49
|
-
return 'Complete all required fields';
|
|
50
|
-
}
|
|
51
|
-
return null;
|
|
61
|
+
return draft.kind === 'custom' ? customDraftError({ draft }) : providerDraftError({ draft, providers });
|
|
62
|
+
}
|
|
63
|
+
function customConnection({ draft, token }, _optional = {}) {
|
|
64
|
+
const username = draft.username.trim();
|
|
65
|
+
return { token, ...(username ? { username } : {}) };
|
|
66
|
+
}
|
|
67
|
+
function providerConnection({ draft, token, providers }, _optional = {}) {
|
|
68
|
+
const provider = findProvider({ draft, providers });
|
|
69
|
+
const extras = provider ? provider.fields.filter(f => f.name !== provider.tokenField && draft.values[f.name]?.trim()).map(f => [f.name, draft.values[f.name].trim()]) : [];
|
|
70
|
+
return { providerId: draft.providerId, [provider?.tokenField ?? 'token']: token, ...Object.fromEntries(extras) };
|
|
71
|
+
}
|
|
72
|
+
function customPatchFields({ draft }, _optional = {}) {
|
|
73
|
+
return { category: draft.category, baseUrl: draft.baseUrl.trim(), additionalHosts: draft.additionalHosts.split(/[\n,]/).map(s => s.trim()).filter(Boolean), username: draft.username.trim() || null };
|
|
52
74
|
}
|
|
53
75
|
/** Omitting connection preserves the saved secret. Never send an empty or half-blank replacement. */
|
|
54
76
|
export function buildCredentialPatch({ draft, providers }, _optional = {}) {
|
|
55
|
-
const provider = providers.find(p => p.kind === draft.kind && p.id === draft.providerId);
|
|
56
77
|
const token = draft.token.trim();
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return { label: draft.label.trim(), ...(connection ? { connection } : {}), ...(draft.kind === 'custom' ? { category: draft.category, baseUrl: draft.baseUrl.trim(), additionalHosts: draft.additionalHosts.split(/[\n,]/).map(s => s.trim()).filter(Boolean), username: draft.username.trim() || null } : {}) };
|
|
78
|
+
const custom = draft.kind === 'custom';
|
|
79
|
+
const connection = token ? (custom ? customConnection({ draft, token }) : providerConnection({ draft, token, providers })) : undefined;
|
|
80
|
+
return { label: draft.label.trim(), ...(connection ? { connection } : {}), ...(custom ? customPatchFields({ draft }) : {}) };
|
|
61
81
|
}
|
|
62
82
|
export function isRootKeyLocked({ status }, _optional = {}) { return status?.state === 'missing-with-data' || status?.state === 'mismatch'; }
|
|
63
83
|
//# sourceMappingURL=rules.js.map
|
|
@@ -27,18 +27,20 @@ export function createSeoController({ api }, { t = (key) => key } = {}) {
|
|
|
27
27
|
}
|
|
28
28
|
async function regenerateSitemap(_required = {}, _optional = {}) {
|
|
29
29
|
store.set({ patch: { saving: true, error: null, notice: null } });
|
|
30
|
+
// One exit after `finally`: returning from both try and catch left a fall-through nothing took.
|
|
31
|
+
let accepted = false;
|
|
30
32
|
try {
|
|
31
33
|
await api.regenerateSeoSitemap({});
|
|
32
34
|
store.set({ patch: { notice: t('Sitemap regeneration accepted.') } });
|
|
33
|
-
|
|
35
|
+
accepted = true;
|
|
34
36
|
}
|
|
35
37
|
catch (error) {
|
|
36
38
|
store.set({ patch: { error: describeSeoError({ error, fallback: t('failed to regenerate sitemap') }) } });
|
|
37
|
-
return false;
|
|
38
39
|
}
|
|
39
40
|
finally {
|
|
40
41
|
store.set({ patch: { saving: false } });
|
|
41
42
|
}
|
|
43
|
+
return accepted;
|
|
42
44
|
}
|
|
43
45
|
return {
|
|
44
46
|
...store, load, save, regenerateSitemap,
|
|
@@ -48,5 +48,5 @@ export interface MediaRefFieldProps {
|
|
|
48
48
|
* `useDialog` prop. Defaults to the real {@link useWiredMediaRefField}. */
|
|
49
49
|
useField?: typeof useWiredMediaRefField;
|
|
50
50
|
}
|
|
51
|
-
export declare function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base, emptyHint, inherited, useField, }: MediaRefFieldProps
|
|
51
|
+
export declare function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base, emptyHint, inherited, useField, }: MediaRefFieldProps): import("react").JSX.Element;
|
|
52
52
|
//# sourceMappingURL=MediaRefField.d.ts.map
|
|
@@ -4,7 +4,7 @@ import { interpolate } from "@jini-ai/ui/i18n";
|
|
|
4
4
|
import { MediaPickerBridge } from "./MediaPickerBridge.js";
|
|
5
5
|
import { useSeoCopy } from "../hooks/SeoPorts.hooks.js";
|
|
6
6
|
import { useWiredMediaRefField, useMediaRefPresentation, handleSpread } from "../hooks/MediaRefField.hooks.js";
|
|
7
|
-
export function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base, emptyHint, inherited = false, useField = useWiredMediaRefField, }
|
|
7
|
+
export function MediaRefField({ locale, id, name, label, value, onChange, agentHandle: base, emptyHint, inherited = false, useField = useWiredMediaRefField, }) {
|
|
8
8
|
const t = useSeoCopy();
|
|
9
9
|
const { Picker = MediaPickerBridge } = useMediaRefPresentation({ value });
|
|
10
10
|
const { pickerOpen, openPicker, closePicker, handleSelect, clear, previewUrl, file, accept, onPreviewLoad, onPreviewError } = useField({ value, onChange });
|
|
@@ -17,6 +17,23 @@ export function MediaRefField({ locale, id, name, label, value, onChange, agentH
|
|
|
17
17
|
2026-10-15 (round 3): the text box is gone — for a library pick it only ever said a read-only
|
|
18
18
|
"Selected image" ("i can't change it and looks useless"), so the row names the file instead and
|
|
19
19
|
the picker is the one way in. The exact value still rides a hidden input for FormData saves. */
|
|
20
|
-
return (_jsxs("div", { className: "jini-field jini-media-ref-field", children: [_jsx("span", { className: "jini-field-label", id: labelId, children: label }), _jsxs("div", { className: "jini-media-ref-field-row", role: "group", "aria-labelledby": labelId, ...(base ? agentHandle({ handle: base }, { role: "region", label }) : {}), children: [
|
|
20
|
+
return (_jsxs("div", { className: "jini-field jini-media-ref-field", children: [_jsx("span", { className: "jini-field-label", id: labelId, children: label }), _jsxs("div", { className: "jini-media-ref-field-row", role: "group", "aria-labelledby": labelId, ...(base ? agentHandle({ handle: base }, { role: "region", label }) : {}), children: [_jsx(MediaRefFile, { file: file, emptyHint: emptyHint, label: label, locale: locale, t: t, inherited: inherited, previewUrl: previewUrl, onPreviewLoad: onPreviewLoad, onPreviewError: onPreviewError }), name ? _jsx("input", { type: "hidden", id: id, name: name, value: value }) : null, _jsx(MediaRefActions, { label: label, locale: locale, t: t, base: base, value: value, inherited: inherited, openPicker: openPicker, clear: clear })] }), pickerOpen ? (_jsx(Picker, { onSelect: handleSelect, onCancel: closePicker, accept: accept, agentHandle: base ? `${base}-dialog` : undefined })) : null] }));
|
|
21
|
+
}
|
|
22
|
+
/** The chosen file: thumbnail, name (prefixed with its source when inherited) and, for an own
|
|
23
|
+
* override, its pixel size. With no file, the field's empty hint (if any) takes the slot. */
|
|
24
|
+
function MediaRefFile({ file, emptyHint, label, locale, t, inherited, previewUrl, onPreviewLoad, onPreviewError }) {
|
|
25
|
+
if (!file)
|
|
26
|
+
return emptyHint ? _jsx("span", { className: "jini-media-ref-field-empty", children: emptyHint }) : null;
|
|
27
|
+
return (_jsxs("span", { className: inherited ? "jini-media-ref-field-file jini-media-ref-field-file-inherited" : "jini-media-ref-field-file", children: [previewUrl ? _jsx("img", { className: "jini-media-ref-field-preview", src: previewUrl, alt: `${label} preview`, onLoad: onPreviewLoad, onError: onPreviewError }, previewUrl) : null, _jsx("span", { className: "jini-media-ref-field-name", title: file.name, children: inherited ? inheritedFileName({ name: file.name, inherited, locale, t }) : file.name }), file.size && !inherited ? _jsx("span", { className: "jini-media-ref-field-size", children: file.size }) : null] }));
|
|
28
|
+
}
|
|
29
|
+
/** "Using site default: {name}" or "Using image from page content: {name}", translated. */
|
|
30
|
+
function inheritedFileName({ name, inherited, locale, t }, _optional = {}) {
|
|
31
|
+
const key = inherited === "content" ? "Using image from page content: {name}" : "Using site default: {name}";
|
|
32
|
+
return interpolate({ template: t({ locale, key, vars: { name } }), vars: { name } });
|
|
33
|
+
}
|
|
34
|
+
/** Choose (always) and Remove (only for an own value), grouped so they wrap as one unit. */
|
|
35
|
+
function MediaRefActions({ label, locale, t, base, value, inherited, openPicker, clear }) {
|
|
36
|
+
const handleBase = base === undefined ? {} : { base };
|
|
37
|
+
return (_jsxs("span", { className: "jini-media-ref-field-actions", children: [_jsx("button", { type: "button", className: "jini-btn jini-btn-secondary", onClick: openPicker, ...handleSpread({ ...handleBase, suffix: "choose", label: `Choose an image for: ${label}` }), children: t({ locale, key: "Choose image" }) }), value && !inherited ? (_jsx("button", { type: "button", className: "jini-btn jini-btn-secondary", onClick: clear, ...handleSpread({ ...handleBase, suffix: "clear", label: `Remove the selected image for: ${label}` }), children: t({ locale, key: "Remove" }) })) : null] }));
|
|
21
38
|
}
|
|
22
39
|
//# sourceMappingURL=MediaRefField.js.map
|
|
@@ -17,7 +17,9 @@ export function useMediaRefField({ value, onChange, api }, { t } = {}) {
|
|
|
17
17
|
// One cached library read shared by every image field on the screen; only a library reference
|
|
18
18
|
// needs it (a URL names itself), and a host without `listSeoMedia` falls back to the slug.
|
|
19
19
|
const list = api.listSeoMedia;
|
|
20
|
-
|
|
20
|
+
// `fetch` runs only while the query is enabled, which requires `list`: the fetch-query adapters
|
|
21
|
+
// never call a disabled read's fetch (not on mount, focus or prefix invalidation).
|
|
22
|
+
const media = useFetchQuery({ key: SEO_KEYS.media, fetch: () => list.call(api, {}) }, { enabled: Boolean(list) && isStoredMediaRef({ value }) });
|
|
21
23
|
return {
|
|
22
24
|
...diagnostics,
|
|
23
25
|
file: describeMediaRef({ value, media: media.data }, { width: naturalWidth, height: naturalHeight, t, extensionFor: extensionForMimeType }),
|
|
@@ -29,7 +29,7 @@ export declare function useAnalyzePanel({ analysis }: {
|
|
|
29
29
|
analysis: SeoEntryAnalysis;
|
|
30
30
|
}, _optional?: Record<string, never>): {
|
|
31
31
|
sortedIssues: import("../../../core/index.js").AdminSeoIssue[];
|
|
32
|
-
tone: "
|
|
32
|
+
tone: "failure" | "warning" | "success";
|
|
33
33
|
};
|
|
34
34
|
/** All seven defaults remain in one FormData submission; tabs never split this form. */
|
|
35
35
|
export declare function useSeoDefaults({ save }: {
|