@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,9 +1,36 @@
|
|
|
1
1
|
import type { ChangeEvent } from 'react';
|
|
2
2
|
export declare function useProvidersTab(_required?: Record<string, never>, _optional?: Record<string, never>): {
|
|
3
|
+
reload: () => void;
|
|
4
|
+
reloadDisabled: boolean;
|
|
5
|
+
reloadLabel: string;
|
|
6
|
+
unreachable: boolean;
|
|
7
|
+
empty: boolean;
|
|
8
|
+
save: () => void;
|
|
9
|
+
saveLabel: string;
|
|
10
|
+
saveDisabled: boolean;
|
|
11
|
+
saved: boolean;
|
|
12
|
+
saveError: boolean;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
settingsDisabled: boolean;
|
|
3
15
|
rows: {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
16
|
+
invalid: boolean;
|
|
17
|
+
hint: string | null;
|
|
18
|
+
hintClassName: string;
|
|
19
|
+
hintRole: "alert" | undefined;
|
|
20
|
+
ariaInvalid: true | undefined;
|
|
21
|
+
clearLabel: string;
|
|
22
|
+
clearDisabled: boolean;
|
|
23
|
+
clear: () => void;
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
baseUrlPlaceholder: string;
|
|
26
|
+
baseUrlLabel: string;
|
|
27
|
+
onBaseUrl: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
28
|
+
model: string;
|
|
29
|
+
modelLabel: string;
|
|
30
|
+
modelsId: string;
|
|
31
|
+
hasModels: boolean;
|
|
32
|
+
onModel: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
33
|
+
toggle: () => void;
|
|
7
34
|
credential: string;
|
|
8
35
|
credentialType: "text" | "password";
|
|
9
36
|
credentialPlaceholder: string;
|
|
@@ -15,41 +42,14 @@ export declare function useProvidersTab(_required?: Record<string, never>, _opti
|
|
|
15
42
|
};
|
|
16
43
|
toggleLabel: string;
|
|
17
44
|
visible: boolean;
|
|
18
|
-
toggle(): void;
|
|
19
45
|
onCredential: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
onBaseUrl: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
24
|
-
model: string;
|
|
25
|
-
modelLabel: string;
|
|
26
|
-
modelsId: string;
|
|
27
|
-
hasModels: boolean;
|
|
28
|
-
onModel: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
29
|
-
clearLabel: string;
|
|
30
|
-
clearDisabled: boolean;
|
|
31
|
-
clear: () => void;
|
|
32
|
-
invalid: boolean;
|
|
33
|
-
hint: string | null;
|
|
34
|
-
hintClassName: string;
|
|
35
|
-
hintRole: "alert" | undefined;
|
|
36
|
-
ariaInvalid: true | undefined;
|
|
46
|
+
statusLabel: string | null;
|
|
47
|
+
statusClassName: string;
|
|
48
|
+
divider: boolean;
|
|
37
49
|
id: string;
|
|
38
50
|
label: string;
|
|
39
51
|
defaultBaseUrl?: string;
|
|
40
52
|
models?: readonly string[];
|
|
41
53
|
}[];
|
|
42
|
-
reload: () => void;
|
|
43
|
-
reloadDisabled: boolean;
|
|
44
|
-
reloadLabel: string;
|
|
45
|
-
unreachable: boolean;
|
|
46
|
-
empty: boolean;
|
|
47
|
-
save: () => void;
|
|
48
|
-
saveLabel: string;
|
|
49
|
-
saveDisabled: boolean;
|
|
50
|
-
saved: boolean;
|
|
51
|
-
saveError: boolean;
|
|
52
|
-
disabled: boolean;
|
|
53
|
-
settingsDisabled: boolean;
|
|
54
54
|
};
|
|
55
55
|
//# sourceMappingURL=ProvidersTab.hooks.d.ts.map
|
|
@@ -3,6 +3,12 @@ import { MEDIA_QUERY_KEYS, useMediaProvidersQuery } from './MediaQuery.hooks.js'
|
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { useMediaProvidersTab, sortProvidersByConfigured, isEntryPresent, isMarkerOnlyEntry, maskedKeyLabel, isProviderBaseUrlInvalid, invalidBaseUrlProviderIds } from '@jini-ai/ui';
|
|
5
5
|
import { useMediaPorts } from './MediaPorts.hooks.js';
|
|
6
|
+
/** Host adapters supply canonical IDs (media/PORT.md); keep them literal in the editor.
|
|
7
|
+
* Pure operations preserve values and copy maps in O(n) time/space; ID lookup is O(1). */
|
|
8
|
+
const catalogPolicy = {
|
|
9
|
+
canonicalMediaProviderId: ({ id }, _options = {}) => id,
|
|
10
|
+
normalizeMediaProviderMap: ({ providers }, _options = {}) => ({ ...providers }),
|
|
11
|
+
};
|
|
6
12
|
function configuration({ items }, _optional = {}) {
|
|
7
13
|
return Object.fromEntries(items.filter(item => item.configured || item.apiKeyTail || item.baseUrl?.trim() || item.model?.trim())
|
|
8
14
|
.map(item => [item.id, { apiKeyConfigured: item.configured,
|
|
@@ -10,6 +16,108 @@ function configuration({ items }, _optional = {}) {
|
|
|
10
16
|
...(item.baseUrl === undefined ? {} : { baseUrl: item.baseUrl }),
|
|
11
17
|
...(item.model === undefined ? {} : { model: item.model }) }]));
|
|
12
18
|
}
|
|
19
|
+
/** Drops a cleared provider's stored state; one that never held any needs no call. */
|
|
20
|
+
async function removeIfStored({ port, item, signal }, _optional = {}) {
|
|
21
|
+
if (item.configured || item.baseUrl || item.model)
|
|
22
|
+
await port.removeCredential({ id: item.id }, { signal });
|
|
23
|
+
}
|
|
24
|
+
/** Absent endpoint and model compare and send as blank, the stored "use the default". */
|
|
25
|
+
function settingsOf({ source }, _optional = {}) {
|
|
26
|
+
return { baseUrl: source.baseUrl ?? '', model: source.model ?? '' };
|
|
27
|
+
}
|
|
28
|
+
async function saveProviderSettings({ port, item, entry, signal }, _optional = {}) {
|
|
29
|
+
const next = settingsOf({ source: entry }), stored = settingsOf({ source: item });
|
|
30
|
+
if (next.baseUrl === stored.baseUrl && next.model === stored.model)
|
|
31
|
+
return;
|
|
32
|
+
if (!port.saveSettings)
|
|
33
|
+
throw new Error('Provider settings unavailable');
|
|
34
|
+
await port.saveSettings({ id: item.id, ...next }, { signal });
|
|
35
|
+
}
|
|
36
|
+
async function saveProvider({ port, item, signal }, { entry } = {}) {
|
|
37
|
+
if (!entry)
|
|
38
|
+
return removeIfStored({ port, item, signal });
|
|
39
|
+
await saveProviderSettings({ port, item, entry, signal });
|
|
40
|
+
if (entry.apiKey?.trim())
|
|
41
|
+
await port.saveCredential({ id: item.id, credential: entry.apiKey }, { signal });
|
|
42
|
+
}
|
|
43
|
+
async function saveSequentially({ port, providers, lastRead, signal }, _optional = {}) {
|
|
44
|
+
// Older hosts retain the one-button editor; writes remain sequential and failures
|
|
45
|
+
// retain local edits. Atomic whole-set hosts should implement saveChanges instead.
|
|
46
|
+
// Submitted ids the last read never returned (catalog-only providers getting their
|
|
47
|
+
// first key or settings) start from an empty stored state, or their writes are lost.
|
|
48
|
+
const known = new Set(lastRead.map(item => item.id));
|
|
49
|
+
const unread = Object.keys(providers).filter(id => !known.has(id)).map(id => ({ id, configured: false }));
|
|
50
|
+
for (const item of [...lastRead, ...unread])
|
|
51
|
+
await saveProvider({ port, item, signal }, { entry: providers[item.id] });
|
|
52
|
+
const items = await port.list({}, { signal });
|
|
53
|
+
if (!items)
|
|
54
|
+
throw new Error('Provider service unavailable');
|
|
55
|
+
return items;
|
|
56
|
+
}
|
|
57
|
+
function rowStatus({ entry, pending }, _optional = {}) {
|
|
58
|
+
const saved = isMarkerOnlyEntry(entry);
|
|
59
|
+
const mask = saved ? maskedKeyLabel(entry) : null;
|
|
60
|
+
return {
|
|
61
|
+
mask,
|
|
62
|
+
statusLabel: saved ? `Saved (${mask})` : pending ? 'Unsaved' : null,
|
|
63
|
+
statusClassName: saved ? 'jini-field-status-badge jini-field-status-badge-success' : 'jini-field-status-badge',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function baseUrlHint({ entry, item }, _optional = {}) {
|
|
67
|
+
const invalid = isProviderBaseUrlInvalid(entry);
|
|
68
|
+
return {
|
|
69
|
+
invalid, hint: invalid ? 'Enter an absolute http:// or https:// URL that is not a private or internal address.'
|
|
70
|
+
: !entry.baseUrl?.trim() && item.defaultBaseUrl ? `Uses ${item.defaultBaseUrl} by default.` : null,
|
|
71
|
+
hintClassName: invalid ? 'jini-field-hint jini-hint-error' : 'jini-field-hint',
|
|
72
|
+
hintRole: invalid ? 'alert' : undefined,
|
|
73
|
+
ariaInvalid: invalid || undefined,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function credentialFields({ item, entry, editor, visible }, { mask = null } = {}) {
|
|
77
|
+
return {
|
|
78
|
+
credential: entry.apiKey ?? '', credentialType: visible ? 'text' : 'password',
|
|
79
|
+
credentialPlaceholder: mask ?? 'Paste your API key',
|
|
80
|
+
credentialAttrs: { 'data-jini-part': 'media.provider.credential', 'data-agent-private': true,
|
|
81
|
+
'data-agent-element': `media-provider-${item.id}-api-key`, 'aria-label': `${item.label} credential` },
|
|
82
|
+
toggleLabel: `${item.label} ${visible ? 'Hide' : 'Show'}`, visible,
|
|
83
|
+
onCredential: (event) => editor.updateProvider(item.id, { apiKey: event.currentTarget.value }),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function settingsFields({ item, entry, editor }, _optional = {}) {
|
|
87
|
+
return {
|
|
88
|
+
baseUrl: entry.baseUrl ?? '', baseUrlPlaceholder: item.defaultBaseUrl || 'https://api.example.com',
|
|
89
|
+
baseUrlLabel: `${item.label} base URL`,
|
|
90
|
+
onBaseUrl: (event) => editor.updateProvider(item.id, { baseUrl: event.currentTarget.value }),
|
|
91
|
+
model: entry.model ?? '', modelLabel: `${item.label} model`, modelsId: `jini-media-provider-models-${item.id}`, hasModels: !!item.models?.length,
|
|
92
|
+
onModel: (event) => editor.updateProvider(item.id, { model: event.currentTarget.value }),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function providerRow({ item, entry, editor, visible, toggle }, { divider = false, disabled = false } = {}) {
|
|
96
|
+
const { mask, ...status } = rowStatus({ entry, pending: editor.pendingProviderIds.has(item.id) });
|
|
97
|
+
return {
|
|
98
|
+
...item,
|
|
99
|
+
divider,
|
|
100
|
+
...status,
|
|
101
|
+
...credentialFields({ item, entry, editor, visible }, { mask }),
|
|
102
|
+
toggle,
|
|
103
|
+
...settingsFields({ item, entry, editor }),
|
|
104
|
+
clearLabel: `${item.label} Clear`, clearDisabled: !isEntryPresent(entry) || disabled,
|
|
105
|
+
clear: () => editor.clearProvider(item.id),
|
|
106
|
+
...baseUrlHint({ entry, item }),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function editorView({ editor, port }, _optional = {}) {
|
|
110
|
+
const loading = editor.load.status === 'loading', saving = editor.save.status === 'saving';
|
|
111
|
+
return { reload: editor.reload, reloadDisabled: loading,
|
|
112
|
+
reloadLabel: loading ? 'Reloading…' : 'Reload',
|
|
113
|
+
unreachable: editor.load.status === 'unreachable', empty: !editor.hasAnyConfigured,
|
|
114
|
+
save: editor.saveChanges, saveLabel: saving ? 'Saving…' : 'Save changes',
|
|
115
|
+
saveDisabled: !port || saving || !editor.pendingProviderIds.size || !!invalidBaseUrlProviderIds(editor.providers).length,
|
|
116
|
+
saved: editor.save.status === 'saved', saveError: editor.save.status === 'save-error', disabled: !port,
|
|
117
|
+
// Fail closed: a host that can persist neither the whole set nor per-provider settings
|
|
118
|
+
// must not accept endpoint/model edits that would only surface as a failed save.
|
|
119
|
+
settingsDisabled: !port || (!port.saveChanges && !port.saveSettings) };
|
|
120
|
+
}
|
|
13
121
|
export function useProvidersTab(_required = {}, _optional = {}) {
|
|
14
122
|
const { mediaProviders } = useMediaPorts();
|
|
15
123
|
const cached = useMediaProvidersQuery({ port: mediaProviders });
|
|
@@ -46,44 +154,15 @@ export function useProvidersTab(_required = {}, _optional = {}) {
|
|
|
46
154
|
if (!mediaProviders)
|
|
47
155
|
throw new Error('Provider service unavailable');
|
|
48
156
|
const signal = lifetime.current.signal;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
lastRead
|
|
52
|
-
return configuration({ items });
|
|
53
|
-
}
|
|
54
|
-
// Older hosts retain the one-button editor; writes remain sequential and failures
|
|
55
|
-
// retain local edits. Atomic whole-set hosts should implement saveChanges instead.
|
|
56
|
-
// Submitted ids the last read never returned (catalog-only providers getting their
|
|
57
|
-
// first key or settings) start from an empty stored state, or their writes are lost.
|
|
58
|
-
const known = new Set(lastRead.map(item => item.id));
|
|
59
|
-
const unread = Object.keys(providers).filter(id => !known.has(id)).map(id => ({ id, configured: false }));
|
|
60
|
-
for (const item of [...lastRead, ...unread]) {
|
|
61
|
-
const entry = providers[item.id];
|
|
62
|
-
if (!entry) {
|
|
63
|
-
if (item.configured || item.baseUrl || item.model)
|
|
64
|
-
await mediaProviders.removeCredential({ id: item.id }, { signal });
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if ((entry.baseUrl ?? '') !== (item.baseUrl ?? '') || (entry.model ?? '') !== (item.model ?? '')) {
|
|
68
|
-
if (!mediaProviders.saveSettings)
|
|
69
|
-
throw new Error('Provider settings unavailable');
|
|
70
|
-
await mediaProviders.saveSettings({ id: item.id, baseUrl: entry.baseUrl ?? '', model: entry.model ?? '' }, { signal });
|
|
71
|
-
}
|
|
72
|
-
if (entry.apiKey?.trim())
|
|
73
|
-
await mediaProviders.saveCredential({ id: item.id, credential: entry.apiKey }, { signal });
|
|
74
|
-
}
|
|
75
|
-
const items = await mediaProviders.list({}, { signal });
|
|
76
|
-
if (!items)
|
|
77
|
-
throw new Error('Provider service unavailable');
|
|
157
|
+
const items = mediaProviders.saveChanges
|
|
158
|
+
? await mediaProviders.saveChanges({ providers }, { signal })
|
|
159
|
+
: await saveSequentially({ port: mediaProviders, providers, lastRead, signal });
|
|
78
160
|
lastRead = items;
|
|
79
161
|
return configuration({ items });
|
|
80
162
|
},
|
|
81
163
|
};
|
|
82
164
|
}, [mediaProviders]);
|
|
83
|
-
const save = useFetchMutation({ run:
|
|
84
|
-
const result = await rawEditorPort.saveMediaProviders(input);
|
|
85
|
-
return result;
|
|
86
|
-
} }, { invalidates: [MEDIA_QUERY_KEYS.providers] });
|
|
165
|
+
const save = useFetchMutation({ run: ({ input }) => rawEditorPort.saveMediaProviders(input) }, { invalidates: [MEDIA_QUERY_KEYS.providers] });
|
|
87
166
|
const saves = useRef(save);
|
|
88
167
|
saves.current = save;
|
|
89
168
|
const editorPort = useMemo(() => ({
|
|
@@ -94,54 +173,15 @@ export function useProvidersTab(_required = {}, _optional = {}) {
|
|
|
94
173
|
},
|
|
95
174
|
saveMediaProviders: (providers) => saves.current.mutate({ input: providers }),
|
|
96
175
|
}), [rawEditorPort]);
|
|
97
|
-
const editor = useMediaProvidersTab({ port: editorPort });
|
|
176
|
+
const editor = useMediaProvidersTab({ port: editorPort, catalogPolicy });
|
|
98
177
|
const catalog = mediaProviders?.catalog ?? discoveredCatalog;
|
|
99
178
|
const pinnedIds = mediaProviders?.pinnedProviderIds ?? [];
|
|
100
179
|
const ordered = sortProvidersByConfigured(catalog, editor.providers, pinnedIds);
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const modelsId = `jini-media-provider-models-${item.id}`;
|
|
108
|
-
return {
|
|
109
|
-
...item,
|
|
110
|
-
divider: index > 0 && !pinnedIds.includes(item.id) && pinnedIds.includes(ordered[index - 1].id),
|
|
111
|
-
statusLabel: saved ? `Saved (${mask})` : editor.pendingProviderIds.has(item.id) ? 'Unsaved' : null,
|
|
112
|
-
statusClassName: saved ? 'jini-field-status-badge jini-field-status-badge-success' : 'jini-field-status-badge',
|
|
113
|
-
credential: entry.apiKey ?? '', credentialType: visible ? 'text' : 'password',
|
|
114
|
-
credentialPlaceholder: mask ?? 'Paste your API key',
|
|
115
|
-
credentialAttrs: { 'data-jini-part': 'media.provider.credential', 'data-agent-private': true,
|
|
116
|
-
'data-agent-element': `media-provider-${item.id}-api-key`, 'aria-label': `${item.label} credential` },
|
|
117
|
-
toggleLabel: `${item.label} ${visible ? 'Hide' : 'Show'}`, visible,
|
|
118
|
-
toggle() { setVisibleKeys(current => { const next = new Set(current); if (next.has(item.id))
|
|
119
|
-
next.delete(item.id);
|
|
120
|
-
else
|
|
121
|
-
next.add(item.id); return next; }); },
|
|
122
|
-
onCredential: (event) => editor.updateProvider(item.id, { apiKey: event.currentTarget.value }),
|
|
123
|
-
baseUrl: entry.baseUrl ?? '', baseUrlPlaceholder: item.defaultBaseUrl || 'https://api.example.com',
|
|
124
|
-
baseUrlLabel: `${item.label} base URL`,
|
|
125
|
-
onBaseUrl: (event) => editor.updateProvider(item.id, { baseUrl: event.currentTarget.value }),
|
|
126
|
-
model: entry.model ?? '', modelLabel: `${item.label} model`, modelsId, hasModels: !!item.models?.length,
|
|
127
|
-
onModel: (event) => editor.updateProvider(item.id, { model: event.currentTarget.value }),
|
|
128
|
-
clearLabel: `${item.label} Clear`, clearDisabled: !isEntryPresent(entry) || !mediaProviders,
|
|
129
|
-
clear: () => editor.clearProvider(item.id),
|
|
130
|
-
invalid, hint: invalid ? 'Enter an absolute http:// or https:// URL that is not a private or internal address.'
|
|
131
|
-
: !entry.baseUrl?.trim() && item.defaultBaseUrl ? `Uses ${item.defaultBaseUrl} by default.` : null,
|
|
132
|
-
hintClassName: invalid ? 'jini-field-hint jini-hint-error' : 'jini-field-hint',
|
|
133
|
-
hintRole: invalid ? 'alert' : undefined,
|
|
134
|
-
ariaInvalid: invalid || undefined,
|
|
135
|
-
};
|
|
136
|
-
});
|
|
137
|
-
return { rows, reload: editor.reload, reloadDisabled: editor.load.status === 'loading',
|
|
138
|
-
reloadLabel: editor.load.status === 'loading' ? 'Reloading…' : 'Reload',
|
|
139
|
-
unreachable: editor.load.status === 'unreachable', empty: !editor.hasAnyConfigured,
|
|
140
|
-
save: editor.saveChanges, saveLabel: editor.save.status === 'saving' ? 'Saving…' : 'Save changes',
|
|
141
|
-
saveDisabled: !mediaProviders || editor.save.status === 'saving' || !editor.pendingProviderIds.size || !!invalidBaseUrlProviderIds(editor.providers).length,
|
|
142
|
-
saved: editor.save.status === 'saved', saveError: editor.save.status === 'save-error', disabled: !mediaProviders,
|
|
143
|
-
// Fail closed: a host that can persist neither the whole set nor per-provider settings
|
|
144
|
-
// must not accept endpoint/model edits that would only surface as a failed save.
|
|
145
|
-
settingsDisabled: !mediaProviders || (!mediaProviders.saveChanges && !mediaProviders.saveSettings) };
|
|
180
|
+
const toggleKey = (id) => setVisibleKeys(current => { const next = new Set(current); if (next.has(id))
|
|
181
|
+
next.delete(id);
|
|
182
|
+
else
|
|
183
|
+
next.add(id); return next; });
|
|
184
|
+
const rows = ordered.map((item, index) => providerRow({ item, entry: editor.providers[item.id] ?? {}, editor, visible: visibleKeys.has(item.id), toggle: () => toggleKey(item.id) }, { divider: index > 0 && !pinnedIds.includes(item.id) && pinnedIds.includes(ordered[index - 1].id), disabled: !mediaProviders }));
|
|
185
|
+
return { rows, ...editorView({ editor, port: mediaProviders }) };
|
|
146
186
|
}
|
|
147
187
|
//# sourceMappingURL=ProvidersTab.hooks.js.map
|
|
@@ -18,12 +18,13 @@ export declare function readMediaFile({ file }: {
|
|
|
18
18
|
signal?: AbortSignal;
|
|
19
19
|
createReader?: () => FileReader;
|
|
20
20
|
}): Promise<UploadInput>;
|
|
21
|
+
type ReadFile = (required: {
|
|
22
|
+
file: File;
|
|
23
|
+
}, optional?: {
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}) => Promise<UploadInput>;
|
|
21
26
|
export declare function useUploadButton(props: UploadButtonProps, { readFile }?: {
|
|
22
|
-
readFile?:
|
|
23
|
-
file: File;
|
|
24
|
-
}, optional?: {
|
|
25
|
-
signal?: AbortSignal;
|
|
26
|
-
}) => Promise<UploadInput>;
|
|
27
|
+
readFile?: ReadFile;
|
|
27
28
|
}): {
|
|
28
29
|
alt: string;
|
|
29
30
|
withAlt: boolean;
|
|
@@ -52,4 +53,5 @@ export declare function useUploadButton(props: UploadButtonProps, { readFile }?:
|
|
|
52
53
|
onPress: () => void | undefined;
|
|
53
54
|
label: string;
|
|
54
55
|
};
|
|
56
|
+
export {};
|
|
55
57
|
//# sourceMappingURL=UploadButton.hooks.d.ts.map
|
|
@@ -26,6 +26,22 @@ export function readMediaFile({ file }, { signal, createReader = () => new FileR
|
|
|
26
26
|
reader.readAsDataURL(file);
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
+
/** Uploads files in order, recording each landed name in `uploaded` so a mid-batch failure
|
|
30
|
+
* still reports what succeeded. Resolves false when the toolbar unmounted mid-batch. */
|
|
31
|
+
async function sendFiles({ files, signal, uploaded, alt, upload, readFile }, _optional = {}) {
|
|
32
|
+
const trimmed = alt.trim();
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const input = await readFile({ file }, { signal });
|
|
35
|
+
if (signal.aborted)
|
|
36
|
+
return false;
|
|
37
|
+
if (!await upload({ input, ...(trimmed ? { alt: trimmed } : {}) }))
|
|
38
|
+
throw new Error(`Upload failed for ${file.name}`);
|
|
39
|
+
if (signal.aborted)
|
|
40
|
+
return false;
|
|
41
|
+
uploaded.push(file.name);
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
29
45
|
export function useUploadButton(props, { readFile = readMediaFile } = {}) {
|
|
30
46
|
// Advisory picker formats mirror the host's standard ceiling, including video. The
|
|
31
47
|
// injected server still validates/sniffs bytes; a file picker is never a security boundary.
|
|
@@ -40,13 +56,27 @@ export function useUploadButton(props, { readFile = readMediaFile } = {}) {
|
|
|
40
56
|
lifetime.current = abort;
|
|
41
57
|
return () => abort.abort();
|
|
42
58
|
}, []);
|
|
59
|
+
// Multi-select hosts take the whole pick; single-file toolbars keep only the first.
|
|
60
|
+
const limit = (files) => files.slice(0, props.multiple === false ? 1 : undefined);
|
|
43
61
|
function onFile(event) {
|
|
44
62
|
if (!inFlight.current && !props.disabled) {
|
|
45
|
-
setSelectedFiles(Array.from(event.currentTarget.files ?? [])
|
|
63
|
+
setSelectedFiles(limit(Array.from(event.currentTarget.files ?? [])));
|
|
46
64
|
setError(null);
|
|
47
65
|
setSuccess(null);
|
|
48
66
|
}
|
|
49
67
|
}
|
|
68
|
+
function settle({ files, uploaded }, _optional = {}) {
|
|
69
|
+
// Keep only failed/unattempted files: retry must not duplicate successes in a batch.
|
|
70
|
+
setSelectedFiles(files.slice(uploaded.length));
|
|
71
|
+
setBusy(false);
|
|
72
|
+
if (uploaded.length)
|
|
73
|
+
setSuccess(`Uploaded ${uploaded.join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
function clearForm() {
|
|
76
|
+
setAlt('');
|
|
77
|
+
if (inputRef.current)
|
|
78
|
+
inputRef.current.value = '';
|
|
79
|
+
}
|
|
50
80
|
async function uploadFiles(files) {
|
|
51
81
|
if (!files.length || inFlight.current || props.disabled)
|
|
52
82
|
return;
|
|
@@ -56,22 +86,9 @@ export function useUploadButton(props, { readFile = readMediaFile } = {}) {
|
|
|
56
86
|
setError(null);
|
|
57
87
|
setSuccess(null);
|
|
58
88
|
const uploaded = [];
|
|
59
|
-
let remaining = [...files];
|
|
60
89
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (signal.aborted)
|
|
64
|
-
return;
|
|
65
|
-
if (!await props.upload({ input, ...(alt.trim() ? { alt: alt.trim() } : {}) }))
|
|
66
|
-
throw new Error(`Upload failed for ${file.name}`);
|
|
67
|
-
if (signal.aborted)
|
|
68
|
-
return;
|
|
69
|
-
uploaded.push(file.name);
|
|
70
|
-
remaining = remaining.slice(1);
|
|
71
|
-
}
|
|
72
|
-
setAlt('');
|
|
73
|
-
if (inputRef.current)
|
|
74
|
-
inputRef.current.value = '';
|
|
90
|
+
if (await sendFiles({ files, signal, uploaded, alt, upload: props.upload, readFile }))
|
|
91
|
+
clearForm();
|
|
75
92
|
}
|
|
76
93
|
catch (error) {
|
|
77
94
|
if (!signal.aborted)
|
|
@@ -79,13 +96,8 @@ export function useUploadButton(props, { readFile = readMediaFile } = {}) {
|
|
|
79
96
|
}
|
|
80
97
|
finally {
|
|
81
98
|
inFlight.current = false;
|
|
82
|
-
if (!signal.aborted)
|
|
83
|
-
|
|
84
|
-
setSelectedFiles(remaining);
|
|
85
|
-
setBusy(false);
|
|
86
|
-
if (uploaded.length)
|
|
87
|
-
setSuccess(`Uploaded ${uploaded.join(', ')}`);
|
|
88
|
-
}
|
|
99
|
+
if (!signal.aborted)
|
|
100
|
+
settle({ files, uploaded });
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
103
|
return {
|
|
@@ -104,7 +116,7 @@ export function useUploadButton(props, { readFile = readMediaFile } = {}) {
|
|
|
104
116
|
event.preventDefault();
|
|
105
117
|
if (inFlight.current || props.disabled)
|
|
106
118
|
return;
|
|
107
|
-
const files = Array.from(event.dataTransfer.files)
|
|
119
|
+
const files = limit(Array.from(event.dataTransfer.files));
|
|
108
120
|
setSelectedFiles(files);
|
|
109
121
|
void uploadFiles(files);
|
|
110
122
|
},
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Styling: scoped feature presentation used to travel with compiled JS (a `mediaStyles` string in
|
|
2
|
+
// `media/styles.ts`), which also overrode host dialog CSS that could accidentally display a closed
|
|
3
|
+
// dialog or shrink a large media preview. Host CSS now supplies presentation and the closed-dialog
|
|
4
|
+
// safeguards on jini- classes, so no style string ships from this module.
|
|
1
5
|
export { useGallerySelection } from './hooks/GallerySelection.hooks.js';
|
|
2
6
|
import { createElement } from 'react';
|
|
3
7
|
import { bindReact } from '../../core/react/bind-react.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { TabViewProps } from '../../../core/react/bind-react.js';
|
|
2
|
-
export declare function LibraryTab(props: TabViewProps
|
|
2
|
+
export declare function LibraryTab(props: TabViewProps): import("react").JSX.Element;
|
|
3
3
|
export default LibraryTab;
|
|
4
4
|
//# sourceMappingURL=LibraryTab.d.ts.map
|
|
@@ -6,9 +6,9 @@ import { UploadButton } from '../components/UploadButton.js';
|
|
|
6
6
|
import { EditMediaPanel } from '../components/EditMediaPanel.js';
|
|
7
7
|
import { MediaLightbox } from '../components/MediaLightbox.js';
|
|
8
8
|
import { MediaPurgeDialog } from '../components/MediaPurgeDialog.js';
|
|
9
|
-
export function LibraryTab(props
|
|
9
|
+
export function LibraryTab(props) {
|
|
10
10
|
const vm = useLibraryTab(props);
|
|
11
|
-
return (_jsxs("section", { className: "jini-media-library", "data-jini-part": "media.library", children: [vm.canUpload && _jsx(UploadButton, { upload: vm.upload, disabled: vm.busy }), _jsx("div", { className: "jini-media-order-control", children: _jsx(Select, { label: "Order by", attrs: { 'data-jini-part': 'media.sort', 'data-agent-element': 'media-order-by' }, value: vm.orderBy, onValueChange: vm.onOrder, options: [{ value: 'created', label: 'Created (newest first)' }, { value: 'alphabetical', label: 'Alphabetical (A–Z)' }] }) }), _jsxs("div", { className: "jini-media-library-filters", hidden: true, children: [_jsx(TextField, { attrs: { 'data-jini-part': 'media.search', 'data-agent-element': 'media-search' }, label: "Search media", value: vm.search, onValueChange: vm.onSearch }), _jsx(Select, { label: "Status", value: vm.statusFilter, onValueChange: vm.onStatus, options: [{ value: 'all', label: 'All statuses' }, { value: 'active', label: 'Active' }, { value: 'trashed', label: 'Trash' }] })] }), _jsx("p", { className: "jini-media-success", role: "status", "aria-live": "polite", "aria-atomic": "true", children: vm.success }), vm.untyped && _jsx("p", { className: "jini-notice", children: vm.t.untyped }), vm.snapshot?.error && _jsxs("div", { className: "jini-notice jini-notice-error", children: [_jsx("p", { className: "jini-notice-text", role: "alert", children: vm.snapshot.error }), _jsx(Button, { onPress: vm.onRetry, children: "Retry" })] }), vm.loading ? (_jsx("p", { className: "jini-notice", role: "status", children: vm.t.loading })) : (_jsx(MediaGrid, { gridRef: vm.gridRef, emptyLabel: vm.emptyLabel, actions: vm.actions, api: vm.mediaApi })), _jsx(MediaLightbox, { items: vm.items, api: vm.mediaApi, activeIndex: vm.lightboxIndex, onNavigate: vm.onNavigate, onClose: vm.closeLightbox }), vm.editing && (_jsx(EditMediaPanel, { api: vm.mediaApi, item: vm.editing, onSaved: vm.saved, onClose: vm.closeEdit }, vm.editing.id)), _jsx(MediaPurgeDialog, {
|
|
11
|
+
return (_jsxs("section", { className: "jini-media-library", "data-jini-part": "media.library", children: [vm.canUpload && _jsx(UploadButton, { upload: vm.upload, disabled: vm.busy }), _jsx("div", { className: "jini-media-order-control", children: _jsx(Select, { label: "Order by", attrs: { 'data-jini-part': 'media.sort', 'data-agent-element': 'media-order-by' }, value: vm.orderBy, onValueChange: vm.onOrder, options: [{ value: 'created', label: 'Created (newest first)' }, { value: 'alphabetical', label: 'Alphabetical (A–Z)' }] }) }), _jsxs("div", { className: "jini-media-library-filters", hidden: true, children: [_jsx(TextField, { attrs: { 'data-jini-part': 'media.search', 'data-agent-element': 'media-search' }, label: "Search media", value: vm.search, onValueChange: vm.onSearch }), _jsx(Select, { label: "Status", value: vm.statusFilter, onValueChange: vm.onStatus, options: [{ value: 'all', label: 'All statuses' }, { value: 'active', label: 'Active' }, { value: 'trashed', label: 'Trash' }] })] }), _jsx("p", { className: "jini-media-success", role: "status", "aria-live": "polite", "aria-atomic": "true", children: vm.success }), vm.untyped && _jsx("p", { className: "jini-notice", children: vm.t.untyped }), vm.snapshot?.error && _jsxs("div", { className: "jini-notice jini-notice-error", children: [_jsx("p", { className: "jini-notice-text", role: "alert", children: vm.snapshot.error }), _jsx(Button, { onPress: vm.onRetry, children: "Retry" })] }), vm.loading ? (_jsx("p", { className: "jini-notice", role: "status", children: vm.t.loading })) : (_jsx(MediaGrid, { gridRef: vm.gridRef, emptyLabel: vm.emptyLabel, actions: vm.actions, api: vm.mediaApi })), _jsx(MediaLightbox, { items: vm.items, api: vm.mediaApi, activeIndex: vm.lightboxIndex, onNavigate: vm.onNavigate, onClose: vm.closeLightbox }), vm.editing && (_jsx(EditMediaPanel, { api: vm.mediaApi, item: vm.editing, onSaved: vm.saved, onClose: vm.closeEdit }, vm.editing.id)), _jsx(MediaPurgeDialog, { ...vm.purgeDialog, onCancel: vm.cancelPurge, onConfirm: vm.confirmPurge })] }));
|
|
12
12
|
}
|
|
13
13
|
export default LibraryTab;
|
|
14
14
|
//# sourceMappingURL=LibraryTab.js.map
|
package/dist/media/rules.d.ts
CHANGED
|
@@ -56,6 +56,13 @@ export declare function formatByteSize({ bytes }: {
|
|
|
56
56
|
}, { locale }?: {
|
|
57
57
|
locale?: string;
|
|
58
58
|
}): string;
|
|
59
|
+
/** The file name a media card shows. A media record keeps only its extension-less title, so the
|
|
60
|
+
* extension comes from the type sniffed from its bytes; an unknown type, or a title that already
|
|
61
|
+
* ends with that extension, shows the title as-is. */
|
|
62
|
+
export declare function mediaFileName({ title, contentType }: {
|
|
63
|
+
title: string;
|
|
64
|
+
contentType: string | null;
|
|
65
|
+
}, _optional?: Record<string, never>): string;
|
|
59
66
|
/** Full-library counts, independent of search/order/type selection; Providers has no count. */
|
|
60
67
|
export declare function countMediaTabs({ media }: {
|
|
61
68
|
media: readonly MediaAsset[];
|
package/dist/media/rules.js
CHANGED
|
@@ -75,8 +75,16 @@ export function isRetryableMediaError({ error }, _optional = {}) {
|
|
|
75
75
|
const failure = error;
|
|
76
76
|
if (typeof failure.retryable === 'boolean')
|
|
77
77
|
return failure.retryable;
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
return isRetryableHttpStatus({ status: mediaErrorHttpStatus({ failure }) }) || error instanceof TypeError;
|
|
79
|
+
}
|
|
80
|
+
/** `status`, else `statusCode`, else the `HTTP nnn` an older transport put in its message —
|
|
81
|
+
* `NaN` when none is present. */
|
|
82
|
+
function mediaErrorHttpStatus({ failure }, _optional = {}) {
|
|
83
|
+
return failure.status ?? failure.statusCode ?? Number(failure.message?.match(/HTTP\s+(\d{3})/i)?.[1]);
|
|
84
|
+
}
|
|
85
|
+
/** Request timeout, rate limiting and any 5xx are temporary; `NaN` and every other status are not. */
|
|
86
|
+
function isRetryableHttpStatus({ status }, _optional = {}) {
|
|
87
|
+
return status === 408 || status === 429 || (status >= 500 && status <= 599);
|
|
80
88
|
}
|
|
81
89
|
/** Blank means native size; invalid values reach the server's positive-integer validator. */
|
|
82
90
|
export function parseOptionalPixelSize({ value }, _optional = {}) {
|
|
@@ -94,6 +102,21 @@ export function formatByteSize({ bytes }, { locale = 'en-US' } = {}) {
|
|
|
94
102
|
const mb = kb / 1024;
|
|
95
103
|
return mb < 1024 ? `${decimal.format(mb)} MB` : `${decimal.format(mb / 1024)} GB`;
|
|
96
104
|
}
|
|
105
|
+
/** The extension each sniffed media type is stored as; JPEG shows as the common ".jpg". */
|
|
106
|
+
const MEDIA_EXTENSION_BY_CONTENT_TYPE = {
|
|
107
|
+
'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp', 'image/avif': 'avif',
|
|
108
|
+
'image/svg+xml': 'svg', 'video/mp4': 'mp4', 'video/webm': 'webm', 'audio/wav': 'wav', 'audio/mpeg': 'mp3',
|
|
109
|
+
'audio/flac': 'flac', 'audio/mp4': 'm4a', 'application/pdf': 'pdf',
|
|
110
|
+
};
|
|
111
|
+
/** The file name a media card shows. A media record keeps only its extension-less title, so the
|
|
112
|
+
* extension comes from the type sniffed from its bytes; an unknown type, or a title that already
|
|
113
|
+
* ends with that extension, shows the title as-is. */
|
|
114
|
+
export function mediaFileName({ title, contentType }, _optional = {}) {
|
|
115
|
+
const extension = contentType ? MEDIA_EXTENSION_BY_CONTENT_TYPE[contentType] : undefined;
|
|
116
|
+
if (!extension || title.toLowerCase().endsWith(`.${extension}`))
|
|
117
|
+
return title;
|
|
118
|
+
return `${title}.${extension}`;
|
|
119
|
+
}
|
|
97
120
|
/** Full-library counts, independent of search/order/type selection; Providers has no count. */
|
|
98
121
|
export function countMediaTabs({ media }, _optional = {}) {
|
|
99
122
|
return { all: media.length, images: filterMediaByTab({ media, tab: 'images' }).length,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { agentHandle } from '@jini-ai/agentic';
|
|
3
2
|
import { useConfirmController } from '@jini-ai/ui-kit/react';
|
|
4
3
|
import { resolveTone, toneClassName } from '../../types.js';
|
|
5
4
|
import { useConfirmDialog, useConfirmDialogCancelLabel } from './ConfirmDialog.hooks.js';
|
|
5
|
+
import { confirmDialogAgentAttrs, withoutAccessibleName } from './confirm-dialog-attrs.js';
|
|
6
6
|
/**
|
|
7
7
|
* UI-kit's confirmation planner owns the sub-handle segments appended to the caller's base:
|
|
8
8
|
* literals chosen by the mechanism, never host data. Neither needs sanitizing, unlike
|
|
@@ -39,16 +39,13 @@ export function ConfirmDialog({ useDialog = useConfirmDialog, agentHandle: baseH
|
|
|
39
39
|
...(props.pending === undefined ? {} : { pending: props.pending }),
|
|
40
40
|
onConfirm: () => props.onConfirm(), onCancel: () => props.onCancel(),
|
|
41
41
|
}, {
|
|
42
|
-
// UI-kit names selects separately;
|
|
43
|
-
agent:
|
|
44
|
-
...(optional.role === undefined ? {} : { role: optional.role === 'select' ? 'field' : optional.role }),
|
|
45
|
-
...(optional.label === undefined ? {} : { label: optional.label }),
|
|
46
|
-
})),
|
|
42
|
+
// UI-kit names selects separately; see `confirmDialogAgentAttrs`.
|
|
43
|
+
agent: confirmDialogAgentAttrs,
|
|
47
44
|
});
|
|
48
45
|
const { titleId, dialogRef, cancelRef, handleNativeCancel, handleBackdropClick } = useDialog({ open: props.open, onCancel: () => { controller.requestDismiss({ reason: 'close' }); }, document }, { pending: controller.pending });
|
|
49
46
|
// Keep admin's concise accessible names; the shared planner still supplies consequence-aware agent labels.
|
|
50
|
-
const {
|
|
51
|
-
const {
|
|
47
|
+
const cancelAttrs = withoutAccessibleName({ attrs: controller.cancel.attrs });
|
|
48
|
+
const confirmAttrs = withoutAccessibleName({ attrs: controller.confirm.attrs });
|
|
52
49
|
return (_jsxs("dialog", { ref: dialogRef, className: "confirm-dialog", "aria-labelledby": titleId, onCancel: handleNativeCancel, onClick: handleBackdropClick, children: [_jsx("h2", { id: titleId, children: props.title }), _jsxs("div", { className: "confirm-dialog-body", children: [props.body, controller.error] }), _jsxs("div", { className: "confirm-dialog-actions", children: [_jsx("button", { ref: cancelRef, type: "button", className: "btn-secondary", disabled: controller.pending, onClick: () => controller.cancel.onPress?.({}), ...cancelAttrs, children: cancelLabel }), _jsx("button", { type: "button", className: toneClassName({ tone }), disabled: controller.pending, onClick: () => controller.confirm.onPress?.({}), ...confirmAttrs, children: props.confirmLabel })] })] }));
|
|
53
50
|
}
|
|
54
51
|
//# sourceMappingURL=ConfirmDialog.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentAttrsPort, KitAttrs } from '@jini-ai/ui-kit';
|
|
2
|
+
/**
|
|
3
|
+
* The agent-attribute port `ConfirmDialog` hands UI-kit's confirm controller. UI-kit names selects
|
|
4
|
+
* separately; agentic publishes them as fields and still owns handle validation. Absent role/label
|
|
5
|
+
* options stay absent rather than being sent as `undefined`.
|
|
6
|
+
*
|
|
7
|
+
* @complexity O(1).
|
|
8
|
+
*/
|
|
9
|
+
export declare const confirmDialogAgentAttrs: AgentAttrsPort;
|
|
10
|
+
/**
|
|
11
|
+
* Keep admin's concise accessible names; the shared planner still supplies consequence-aware agent
|
|
12
|
+
* labels. Returns the action's attributes without UI-kit's `aria-label`. UI-kit's `ButtonProps`
|
|
13
|
+
* types `attrs` as optional, so a missing set yields an empty one.
|
|
14
|
+
*
|
|
15
|
+
* @complexity O(k) in the number of attributes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function withoutAccessibleName({ attrs }: {
|
|
18
|
+
attrs: KitAttrs | undefined;
|
|
19
|
+
}, _optional?: Record<string, never>): KitAttrs;
|
|
20
|
+
//# sourceMappingURL=confirm-dialog-attrs.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { agentHandle } from '@jini-ai/agentic';
|
|
2
|
+
/**
|
|
3
|
+
* The agent-attribute port `ConfirmDialog` hands UI-kit's confirm controller. UI-kit names selects
|
|
4
|
+
* separately; agentic publishes them as fields and still owns handle validation. Absent role/label
|
|
5
|
+
* options stay absent rather than being sent as `undefined`.
|
|
6
|
+
*
|
|
7
|
+
* @complexity O(1).
|
|
8
|
+
*/
|
|
9
|
+
export const confirmDialogAgentAttrs = (required, optional = {}) => agentHandle(required, {
|
|
10
|
+
...(optional.role === undefined ? {} : { role: optional.role === 'select' ? 'field' : optional.role }),
|
|
11
|
+
...(optional.label === undefined ? {} : { label: optional.label }),
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Keep admin's concise accessible names; the shared planner still supplies consequence-aware agent
|
|
15
|
+
* labels. Returns the action's attributes without UI-kit's `aria-label`. UI-kit's `ButtonProps`
|
|
16
|
+
* types `attrs` as optional, so a missing set yields an empty one.
|
|
17
|
+
*
|
|
18
|
+
* @complexity O(k) in the number of attributes.
|
|
19
|
+
*/
|
|
20
|
+
export function withoutAccessibleName({ attrs }, _optional = {}) {
|
|
21
|
+
const { 'aria-label': _accessibleName, ...rest } = attrs ?? {};
|
|
22
|
+
return rest;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=confirm-dialog-attrs.js.map
|