@jini-ai/admin 0.4.11 → 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 +9 -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/ports/seo.d.ts +20 -0
- 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/adapters/http.js +5 -0
- package/dist/seo/controllers/seo.controller.js +4 -2
- package/dist/seo/messages.en.d.ts +15 -10
- package/dist/seo/messages.en.js +15 -10
- package/dist/seo/models.d.ts +11 -0
- package/dist/seo/react/components/MediaRefField.d.ts +17 -5
- package/dist/seo/react/components/MediaRefField.js +31 -4
- package/dist/seo/react/hooks/MediaRefField.hooks.js +12 -2
- package/dist/seo/react/hooks/SeoPage.hooks.d.ts +1 -0
- package/dist/seo/react/hooks/SeoPage.hooks.js +2 -2
- package/dist/seo/react/hooks/SeoQuery.hooks.d.ts +1 -0
- package/dist/seo/react/hooks/SeoQuery.hooks.js +1 -0
- package/dist/seo/react/hooks/use-entry-picker.hooks.js +7 -2
- package/dist/seo/react/hooks/use-seo-entry-panel.hooks.js +13 -2
- package/dist/seo/react/hooks/use-share-image-warning.hooks.d.ts +4 -1
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +48 -33
- package/dist/seo/react/index.d.ts +45 -30
- package/dist/seo/react/pages/SeoPage.d.ts +6 -4
- package/dist/seo/react/pages/SeoPage.js +51 -29
- package/dist/seo/rules.d.ts +91 -0
- package/dist/seo/rules.js +161 -6
- package/dist/seo/seo.module.d.ts +15 -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 +19 -10
- 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,47 +1,77 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/** Records `name` as passed, or throws naming the provider and the failed check. */
|
|
2
|
+
function pass({ run, ok, name }) {
|
|
3
|
+
if (!ok)
|
|
4
|
+
throw new Error(`Source control conformance failed for provider "${run.providerId}": ${name}`);
|
|
5
|
+
run.checks.push(name);
|
|
6
|
+
}
|
|
7
|
+
/** Passes `name` only when `attempt` rejects; the rejection reason itself is irrelevant to the contract. */
|
|
8
|
+
async function refuses({ run, attempt, name }) {
|
|
9
|
+
let rejected = false;
|
|
10
|
+
try {
|
|
11
|
+
await attempt();
|
|
9
12
|
}
|
|
10
13
|
catch {
|
|
11
14
|
rejected = true;
|
|
12
|
-
}
|
|
13
|
-
|
|
15
|
+
}
|
|
16
|
+
pass({ run, ok: rejected, name });
|
|
17
|
+
}
|
|
18
|
+
async function checkCreate({ run }) {
|
|
19
|
+
const { api, providerId, connection } = run;
|
|
20
|
+
pass({ run, ok: (await api.providers({})).some(p => p.id === providerId), name: 'provider is listed' });
|
|
21
|
+
const first = await api.create({ label: 'conformance-first', connection });
|
|
22
|
+
run.ids.push(first.id);
|
|
23
|
+
pass({ run, ok: first.configured && !!first.id, name: 'create returns configured summary' });
|
|
24
|
+
pass({ run, ok: !('token' in first) && !('connection' in first), name: 'saved secrets never read back' });
|
|
25
|
+
pass({ run, ok: (await api.list({})).some(r => r.id === first.id), name: 'list contains created row' });
|
|
26
|
+
await refuses({ run, attempt: () => api.create({ label: 'conformance-first', connection }), name: 'duplicate label rejects' });
|
|
27
|
+
return first;
|
|
28
|
+
}
|
|
29
|
+
async function checkDefaultSlot({ run }) {
|
|
30
|
+
const { api, providerId, connection } = run;
|
|
31
|
+
const second = await api.create({ label: 'conformance-second', connection, isDefault: true });
|
|
32
|
+
run.ids.push(second.id);
|
|
33
|
+
const list = await api.list({});
|
|
34
|
+
pass({ run, ok: list.filter(r => r.providerId === providerId && r.isDefault).length === 1 && list.find(r => r.id === second.id)?.isDefault === true, name: 'server owns unique default slot' });
|
|
35
|
+
}
|
|
36
|
+
async function checkUpdates({ run, first }) {
|
|
37
|
+
const { api, providerId, connection } = run;
|
|
38
|
+
const renamed = await api.update({ id: first.id, patch: { label: 'conformance-renamed' } });
|
|
39
|
+
pass({ run, ok: renamed.label === 'conformance-renamed' && renamed.configured, name: 'metadata-only update preserves configured credential' });
|
|
40
|
+
pass({ run, ok: first.label === 'conformance-first', name: 'prior snapshots remain immutable' });
|
|
41
|
+
const replaced = await api.update({ id: first.id, patch: { connection } });
|
|
42
|
+
pass({ run, ok: replaced.id === first.id && replaced.label === renamed.label, name: 'replacement retains identity and label' });
|
|
43
|
+
await refuses({ run, attempt: () => api.update({ id: first.id, patch: { connection: { providerId, token: '' } } }), name: 'blank replacement rejects' });
|
|
44
|
+
return renamed;
|
|
45
|
+
}
|
|
46
|
+
async function checkAborts({ run, first, renamed }) {
|
|
47
|
+
const { api } = run;
|
|
48
|
+
const abort = new AbortController();
|
|
49
|
+
abort.abort();
|
|
50
|
+
await refuses({ run, attempt: () => api.list({}, { signal: abort.signal }), name: 'aborted list rejects' });
|
|
51
|
+
await refuses({ run, attempt: () => api.update({ id: first.id, patch: { label: 'bad' } }, { signal: abort.signal }), name: 'aborted write rejects' });
|
|
52
|
+
pass({ run, ok: (await api.list({})).find(r => r.id === first.id)?.label === renamed.label, name: 'aborted write has no effects' });
|
|
53
|
+
}
|
|
54
|
+
async function checkRemove({ run, first }) {
|
|
55
|
+
const { api } = run;
|
|
56
|
+
await api.remove({ id: first.id });
|
|
57
|
+
await api.remove({ id: first.id });
|
|
58
|
+
pass({ run, ok: !(await api.list({})).some(r => r.id === first.id), name: 'remove is idempotent' });
|
|
59
|
+
await refuses({ run, attempt: () => api.update({ id: first.id, patch: {} }), name: 'missing update rejects' });
|
|
60
|
+
}
|
|
61
|
+
/** Creates/deletes named test credentials: run only on a disposable authorized backend.
|
|
62
|
+
* Supply connectionFields for providers with required fields beyond the canonical token. */
|
|
63
|
+
export async function runSourceControlApiConformance({ api, providerId }, { connectionFields = {} } = {}) {
|
|
64
|
+
const run = { api, providerId, connection: { ...connectionFields, providerId, token: 'conformance-only-token' }, ids: [], checks: [] };
|
|
14
65
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
await rejects(() => api.create({ label: 'conformance-first', connection }), 'duplicate label rejects');
|
|
22
|
-
const second = await api.create({ label: 'conformance-second', connection, isDefault: true });
|
|
23
|
-
ids.push(second.id);
|
|
24
|
-
const list = await api.list({});
|
|
25
|
-
assert(list.filter(r => r.providerId === providerId && r.isDefault).length === 1 && list.find(r => r.id === second.id)?.isDefault === true, 'server owns unique default slot');
|
|
26
|
-
const renamed = await api.update({ id: first.id, patch: { label: 'conformance-renamed' } });
|
|
27
|
-
assert(renamed.label === 'conformance-renamed' && renamed.configured, 'metadata-only update preserves configured credential');
|
|
28
|
-
assert(first.label === 'conformance-first', 'prior snapshots remain immutable');
|
|
29
|
-
const replaced = await api.update({ id: first.id, patch: { connection } });
|
|
30
|
-
assert(replaced.id === first.id && replaced.label === renamed.label, 'replacement retains identity and label');
|
|
31
|
-
await rejects(() => api.update({ id: first.id, patch: { connection: { providerId, token: '' } } }), 'blank replacement rejects');
|
|
32
|
-
const abort = new AbortController();
|
|
33
|
-
abort.abort();
|
|
34
|
-
await rejects(() => api.list({}, { signal: abort.signal }), 'aborted list rejects');
|
|
35
|
-
await rejects(() => api.update({ id: first.id, patch: { label: 'bad' } }, { signal: abort.signal }), 'aborted write rejects');
|
|
36
|
-
assert((await api.list({})).find(r => r.id === first.id)?.label === renamed.label, 'aborted write has no effects');
|
|
37
|
-
await api.remove({ id: first.id });
|
|
38
|
-
await api.remove({ id: first.id });
|
|
39
|
-
assert(!(await api.list({})).some(r => r.id === first.id), 'remove is idempotent');
|
|
40
|
-
await rejects(() => api.update({ id: first.id, patch: {} }), 'missing update rejects');
|
|
41
|
-
return Object.freeze(checks);
|
|
66
|
+
const first = await checkCreate({ run });
|
|
67
|
+
await checkDefaultSlot({ run });
|
|
68
|
+
const renamed = await checkUpdates({ run, first });
|
|
69
|
+
await checkAborts({ run, first, renamed });
|
|
70
|
+
await checkRemove({ run, first });
|
|
71
|
+
return Object.freeze(run.checks);
|
|
42
72
|
}
|
|
43
73
|
finally {
|
|
44
|
-
for (const id of ids)
|
|
74
|
+
for (const id of run.ids)
|
|
45
75
|
await api.remove({ id });
|
|
46
76
|
}
|
|
47
77
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { createControllerStore } from '../../core/module/controller-store.js';
|
|
2
2
|
import { blankSourceControlDraft, sourceControlProviders, defaultSourceControlCredential, sourceControlReady, buildSourceControlConnection, sourceControlSummary, sourceControlErrorMessage } from '../rules.js';
|
|
3
|
+
/** The provider and draft a save would submit, or null when the loaded state cannot accept a save for `providerId`. */
|
|
4
|
+
function readySaveTarget({ state, providerId }) {
|
|
5
|
+
const draft = state.drafts[providerId] ?? blankSourceControlDraft({});
|
|
6
|
+
const info = sourceControlProviders({ providers: state.providers, credentials: state.credentials }).find(p => p.id === providerId);
|
|
7
|
+
return state.loaded && !draft.saving && info && sourceControlReady({ info, draft }) ? { info, draft } : null;
|
|
8
|
+
}
|
|
9
|
+
/** Replaces the row with `summary`'s id in place, or appends `summary` when it is new. */
|
|
10
|
+
function spliceCredential({ credentials, summary }) {
|
|
11
|
+
// Preserve server order: the first row is the defensive default when no row is marked default.
|
|
12
|
+
return Object.freeze(credentials.some(c => c.id === summary.id) ? credentials.map(c => c.id === summary.id ? summary : c) : [...credentials, summary]);
|
|
13
|
+
}
|
|
3
14
|
export function createSourceControlController({ api, permissions = [] }, _optional = {}) {
|
|
4
15
|
const grants = new Set(permissions), store = createControllerStore({ initial: { providers: [], credentials: [], drafts: {}, loading: false, loaded: false, error: null, catalogError: null } });
|
|
5
16
|
let generation = 0, credentialRevision = 0;
|
|
@@ -35,22 +46,23 @@ export function createSourceControlController({ api, permissions = [] }, _option
|
|
|
35
46
|
if (ticket === generation)
|
|
36
47
|
store.set({ patch: { loading: false } });
|
|
37
48
|
}
|
|
49
|
+
/** Replaces the provider's current default credential, or creates one labelled 'default'. */
|
|
50
|
+
function writeCredential({ current, connection }) {
|
|
51
|
+
return current ? api.update({ id: current.id, patch: { connection } }, { signal: store.signal }) : api.create({ label: 'default', connection }, { signal: store.signal });
|
|
52
|
+
}
|
|
38
53
|
async function save({ providerId }, _optional = {}) {
|
|
39
|
-
const state = store.getSnapshot(),
|
|
40
|
-
|
|
41
|
-
if (!canWrite() || !state.loaded || draft.saving || !info || !sourceControlReady({ info, draft }))
|
|
54
|
+
const state = store.getSnapshot(), target = canWrite() ? readySaveTarget({ state, providerId }) : null;
|
|
55
|
+
if (!target)
|
|
42
56
|
return false;
|
|
43
|
-
const current = defaultSourceControlCredential({ credentials: state.credentials, providerId }), connection = buildSourceControlConnection(
|
|
57
|
+
const current = defaultSourceControlCredential({ credentials: state.credentials, providerId }), connection = buildSourceControlConnection(target);
|
|
44
58
|
patchDraft(providerId, { saving: true, error: null });
|
|
45
59
|
try {
|
|
46
|
-
const row =
|
|
60
|
+
const row = await writeCredential({ current, connection });
|
|
47
61
|
if (store.signal.aborted)
|
|
48
62
|
return false;
|
|
49
63
|
credentialRevision++;
|
|
50
64
|
// One round trip per write: splice the summary, then erase accepted secret drafts immediately.
|
|
51
|
-
|
|
52
|
-
// Preserve server order: the first row is the defensive default when no row is marked default.
|
|
53
|
-
store.set({ patch: { credentials: Object.freeze(credentials.some(c => c.id === row.id) ? credentials.map(c => c.id === row.id ? summary : c) : [...credentials, summary]) } });
|
|
65
|
+
store.set({ patch: { credentials: spliceCredential({ credentials: store.getSnapshot().credentials, summary: sourceControlSummary({ row }) }) } });
|
|
54
66
|
patchDraft(providerId, blankSourceControlDraft({}));
|
|
55
67
|
return true;
|
|
56
68
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TabViewProps } from '../../../core/react/bind-react.js';
|
|
2
|
+
import type { SourceControlCredential } from '../../models.js';
|
|
2
3
|
export declare function useProvidersTab({ permissions }: TabViewProps, _optional?: {}): {
|
|
3
4
|
denied: boolean;
|
|
4
5
|
loading: boolean;
|
|
@@ -6,7 +7,7 @@ export declare function useProvidersTab({ permissions }: TabViewProps, _optional
|
|
|
6
7
|
id: string;
|
|
7
8
|
label: string;
|
|
8
9
|
heading: string;
|
|
9
|
-
saved:
|
|
10
|
+
saved: SourceControlCredential | undefined;
|
|
10
11
|
defaultOpen: boolean;
|
|
11
12
|
canEdit: boolean;
|
|
12
13
|
unlisted: boolean;
|
|
@@ -2,19 +2,26 @@ import { useController } from '../../../core/react/use-controller.js';
|
|
|
2
2
|
import { createSourceControlController } from '../../controllers/source-control.controller.js';
|
|
3
3
|
import { blankSourceControlDraft, sourceControlProviders, defaultSourceControlCredential, sourceControlReady, safeSourceControlUrl } from '../../rules.js';
|
|
4
4
|
import { useSourceControlPorts } from './SourceControlPorts.hooks.js';
|
|
5
|
+
/** Stand-in snapshot until the controller mounts: nothing listed yet, still loading. */
|
|
6
|
+
const UNMOUNTED = { providers: [], credentials: [], drafts: {}, loading: true, loaded: false, error: null, catalogError: null };
|
|
7
|
+
function fieldView({ info, field, draft }, { controller }) {
|
|
8
|
+
return { name: field.name, label: `${info.label} ${field.label}`, value: draft.values[field.name] ?? '', secret: !!field.secret, required: !!field.required, hint: field.userHelp ?? field.hint ?? field.help, onValueChange({ value }) { controller?.setField({ providerId: info.id, name: field.name, value }); } };
|
|
9
|
+
}
|
|
10
|
+
function rowView({ info, context }, { controller }) {
|
|
11
|
+
const { credentials, drafts, first, canWrite } = context;
|
|
12
|
+
const saved = defaultSourceControlCredential({ credentials, providerId: info.id }), draft = drafts[info.id] ?? blankSourceControlDraft({}), tokenPageUrl = safeSourceControlUrl({ url: info.tokenPageUrl });
|
|
13
|
+
return { id: info.id, label: info.label, heading: saved ? info.label : `Connect ${info.label}`, saved, defaultOpen: info.id === first, canEdit: canWrite && info.listed, unlisted: !info.listed, connected: !!saved,
|
|
14
|
+
token: draft.token, tokenLabel: `${info.label} ${info.tokenLabel}`, hint: saved ? 'Leave blank to keep the current token.' : 'Once saved, the token is never displayed again.', saving: draft.saving, error: draft.error,
|
|
15
|
+
disabled: !canWrite || !sourceControlReady({ info, draft }), help: info.help, tokenPageUrl, hasGuidance: !!info.help || !!tokenPageUrl,
|
|
16
|
+
fields: info.fields.map(field => fieldView({ info, field, draft }, { controller })),
|
|
17
|
+
onToken({ value }) { controller?.setToken({ providerId: info.id, value }); }, save() { void controller?.save({ providerId: info.id }); }, };
|
|
18
|
+
}
|
|
5
19
|
export function useProvidersTab({ permissions = [] }, _optional = {}) {
|
|
6
20
|
const ports = useSourceControlPorts({}), key = permissions.join('\0'), canRead = permissions.includes('source-control.read'), canWrite = canRead && permissions.includes('source-control.credentials.write');
|
|
7
|
-
const { controller, snapshot
|
|
8
|
-
const
|
|
21
|
+
const { controller, snapshot } = useController({ create: () => createSourceControlController({ api: ports.sourceControlApi, permissions }), dependencies: [ports.sourceControlApi, key] }, { start: ({ controller }) => { void controller.load({}); } });
|
|
22
|
+
const state = snapshot ?? UNMOUNTED, credentials = state.credentials, infos = sourceControlProviders({ providers: state.providers, credentials });
|
|
9
23
|
const first = infos.find(p => p.listed && !defaultSourceControlCredential({ credentials, providerId: p.id }))?.id;
|
|
10
|
-
const rows = infos.map(info => {
|
|
11
|
-
|
|
12
|
-
return { id: info.id, label: info.label, heading: saved ? info.label : `Connect ${info.label}`, saved, defaultOpen: info.id === first, canEdit: canWrite && info.listed, unlisted: !info.listed, connected: !!saved,
|
|
13
|
-
token: draft.token, tokenLabel: `${info.label} ${info.tokenLabel}`, hint: saved ? 'Leave blank to keep the current token.' : 'Once saved, the token is never displayed again.', saving: draft.saving, error: draft.error,
|
|
14
|
-
disabled: !canWrite || !sourceControlReady({ info, draft }), help: info.help, tokenPageUrl: safeSourceControlUrl({ url: info.tokenPageUrl }), hasGuidance: !!info.help || !!safeSourceControlUrl({ url: info.tokenPageUrl }),
|
|
15
|
-
fields: info.fields.map(field => ({ name: field.name, label: `${info.label} ${field.label}`, value: draft.values[field.name] ?? '', secret: !!field.secret, required: !!field.required, hint: field.userHelp ?? field.hint ?? field.help, onValueChange({ value }) { controller?.setField({ providerId: info.id, name: field.name, value }); } })),
|
|
16
|
-
onToken({ value }) { controller?.setToken({ providerId: info.id, value }); }, save() { void controller?.save({ providerId: info.id }); }, };
|
|
17
|
-
});
|
|
18
|
-
return { denied: !canRead, loading: !state || state.loading, rows, errors: [state?.error, state?.catalogError].filter((s) => !!s), reload() { void controller?.load({}); }, canManage: !!ports.sourceControlNavigation, manage() { ports.sourceControlNavigation?.open({ kind: 'source-control' }); } };
|
|
24
|
+
const rows = infos.map(info => rowView({ info, context: { credentials, drafts: state.drafts, first, canWrite } }, { controller }));
|
|
25
|
+
return { denied: !canRead, loading: state.loading, rows, errors: [state.error, state.catalogError].filter((s) => !!s), reload() { void controller?.load({}); }, canManage: !!ports.sourceControlNavigation, manage() { ports.sourceControlNavigation?.open({ kind: 'source-control' }); } };
|
|
19
26
|
}
|
|
20
27
|
//# sourceMappingURL=ProvidersTab.hooks.js.map
|
|
@@ -9,6 +9,14 @@ export async function runWidgetsApiConformance({ api }, _optional = {}) {
|
|
|
9
9
|
catch {
|
|
10
10
|
rejected = true;
|
|
11
11
|
} assert(rejected, name); }
|
|
12
|
+
const probe = { api, assert, rejects };
|
|
13
|
+
const { widget, updated } = await checkInstanceLifecycle({ probe });
|
|
14
|
+
await checkRegionPlacements({ probe, widget, updated });
|
|
15
|
+
await checkAbortsAndMissing({ probe, widget, updated });
|
|
16
|
+
await checkTrash({ probe, widget });
|
|
17
|
+
return checks;
|
|
18
|
+
}
|
|
19
|
+
async function checkInstanceLifecycle({ probe: { api, assert, rejects } }, _optional = {}) {
|
|
12
20
|
const input = { widgetType: 'text', title: 'Conformance widget', config: { body: 'initial' } };
|
|
13
21
|
const { widget } = await api.createWidget(input);
|
|
14
22
|
assert(widget.id.length > 0 && widget.slug.length > 0 && widget.status === 'active', 'create active instance');
|
|
@@ -22,6 +30,9 @@ export async function runWidgetsApiConformance({ api }, _optional = {}) {
|
|
|
22
30
|
assert(updated.title === widget.title && updated.version > widget.version, 'omitted title preserved and version advances');
|
|
23
31
|
assert(widget.config.body === 'initial' && firstList.widgets.find(w => w.id === widget.id)?.version === widget.version, 'prior snapshots stay detached');
|
|
24
32
|
await rejects(() => api.updateWidget({ id: widget.id, baseVersion: widget.version, config: {} }), 'stale instance write rejects');
|
|
33
|
+
return { widget, updated };
|
|
34
|
+
}
|
|
35
|
+
async function checkRegionPlacements({ probe: { api, assert, rejects }, widget, updated }, _optional = {}) {
|
|
25
36
|
const regionKey = `conformance-${widget.id}`;
|
|
26
37
|
const { area } = await api.bindWidgetRegion({ regionKey });
|
|
27
38
|
const initialAreaVersion = area.version;
|
|
@@ -40,6 +51,8 @@ export async function runWidgetsApiConformance({ api }, _optional = {}) {
|
|
|
40
51
|
assert(placed.placements[2]?.broken === true, 'broken reference remains visible');
|
|
41
52
|
assert((await api.listWidgetRegions({})).regions.find(r => r.regionKey === regionKey)?.placementCount === placements.length, 'region list placement count updates');
|
|
42
53
|
await rejects(() => api.mutateWidgetRegionPlacements({ regionKey, baseVersion: area.version, placements: [] }), 'stale region write rejects');
|
|
54
|
+
}
|
|
55
|
+
async function checkAbortsAndMissing({ probe: { api, assert, rejects }, widget, updated }, _optional = {}) {
|
|
43
56
|
const abort = new AbortController();
|
|
44
57
|
abort.abort();
|
|
45
58
|
await rejects(() => api.listWidgets({}, { signal: abort.signal }), 'aborted list rejects');
|
|
@@ -47,9 +60,10 @@ export async function runWidgetsApiConformance({ api }, _optional = {}) {
|
|
|
47
60
|
assert((await api.getWidget({ id: widget.id })).widget.title === widget.title, 'aborted write has no effects');
|
|
48
61
|
await rejects(() => api.getWidget({ id: 'missing-conformance-widget' }), 'missing instance rejects');
|
|
49
62
|
await rejects(() => api.getWidgetRegion({ regionKey: 'missing-conformance-region' }), 'missing region rejects');
|
|
63
|
+
}
|
|
64
|
+
async function checkTrash({ probe: { api, assert }, widget }, _optional = {}) {
|
|
50
65
|
await api.trashWidget({ id: widget.id });
|
|
51
66
|
assert(!(await api.listWidgets({})).widgets.some(w => w.id === widget.id), 'trash removes active listing');
|
|
52
67
|
assert((await api.listWidgets({ includeInactive: true })).widgets.find(w => w.id === widget.id)?.status === 'trash', 'trash retained for shared lifecycle');
|
|
53
|
-
return checks;
|
|
54
68
|
}
|
|
55
69
|
//# sourceMappingURL=widgets-api.conformance.js.map
|
|
@@ -14,6 +14,8 @@ export function createWidgetInstanceEditorController({ api, params: initialParam
|
|
|
14
14
|
config: isNew ? defaultConfig(params.widgetType ?? 'text') : {}, message: null, error: null, fieldErrors: [],
|
|
15
15
|
loading: !isNew, saving: false, widgetType: resolveEditorWidgetType({ isNew, queryWidgetType: params.widgetType, widget: null }),
|
|
16
16
|
} });
|
|
17
|
+
const isLiveLoad = ({ ticket }) => ticket === generation && !store.signal.aborted;
|
|
18
|
+
const isLiveSave = ({ savingFor }) => currentKey() === savingFor && !store.signal.aborted;
|
|
17
19
|
async function load(_required = {}, _optional = {}) {
|
|
18
20
|
const ticket = ++generation;
|
|
19
21
|
const isNew = params.widgetId === null;
|
|
@@ -22,27 +24,31 @@ export function createWidgetInstanceEditorController({ api, params: initialParam
|
|
|
22
24
|
store.set({ patch: { widget: null, title: '', config: defaultConfig(params.widgetType ?? 'text'), whereUsed: { count: 0, references: [] }, loading: false, widgetType: params.widgetType } });
|
|
23
25
|
return;
|
|
24
26
|
}
|
|
25
|
-
|
|
27
|
+
await loadExisting({ ticket, requestedId: params.widgetId });
|
|
28
|
+
}
|
|
29
|
+
async function loadExisting({ ticket, requestedId }, _optional = {}) {
|
|
26
30
|
store.set({ patch: { loading: true, error: null } });
|
|
27
31
|
try {
|
|
28
32
|
const result = await api.getWidget({ id: requestedId }, { signal: store.signal });
|
|
29
|
-
if (ticket
|
|
30
|
-
|
|
31
|
-
store.set({ patch: { widget: result.widget, title: result.widget.title, config: result.widget.config, whereUsed: result.whereUsed, widgetType: result.widget.widgetType } });
|
|
32
|
-
// Replace rather than push: an old id bookmark should not add a Back-button stop.
|
|
33
|
-
const redirect = widgetSlugRedirectPath({ requestedId, widget: result.widget }, { slugRedirectPath });
|
|
34
|
-
if (redirect)
|
|
35
|
-
navigate?.(redirect, { replace: true });
|
|
33
|
+
if (isLiveLoad({ ticket }))
|
|
34
|
+
applyLoaded({ requestedId, result });
|
|
36
35
|
}
|
|
37
36
|
catch (error) {
|
|
38
|
-
if (ticket
|
|
37
|
+
if (isLiveLoad({ ticket }))
|
|
39
38
|
store.set({ patch: { error: describeApiError({ e: widgetApiError({ error }) ?? error, fallback: t('failed to load widget') }) } });
|
|
40
39
|
}
|
|
41
40
|
finally {
|
|
42
|
-
if (ticket
|
|
41
|
+
if (isLiveLoad({ ticket }))
|
|
43
42
|
store.set({ patch: { loading: false } });
|
|
44
43
|
}
|
|
45
44
|
}
|
|
45
|
+
function applyLoaded({ requestedId, result }, _optional = {}) {
|
|
46
|
+
store.set({ patch: { widget: result.widget, title: result.widget.title, config: result.widget.config, whereUsed: result.whereUsed, widgetType: result.widget.widgetType } });
|
|
47
|
+
// Replace rather than push: an old id bookmark should not add a Back-button stop.
|
|
48
|
+
const redirect = widgetSlugRedirectPath({ requestedId, widget: result.widget }, { slugRedirectPath });
|
|
49
|
+
if (redirect)
|
|
50
|
+
navigate?.(redirect, { replace: true });
|
|
51
|
+
}
|
|
46
52
|
async function save(_required = {}, _optional = {}) {
|
|
47
53
|
const { widgetType, isNew, widget, title, config } = store.getSnapshot();
|
|
48
54
|
if (!widgetType)
|
|
@@ -50,28 +56,31 @@ export function createWidgetInstanceEditorController({ api, params: initialParam
|
|
|
50
56
|
const savingFor = currentKey();
|
|
51
57
|
store.set({ patch: { saving: true, message: null, error: null, fieldErrors: [] } });
|
|
52
58
|
try {
|
|
53
|
-
if (isNew)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
if (!widget)
|
|
60
|
-
return;
|
|
61
|
-
const { widget: saved } = await api.updateWidget({ id: widget.id, baseVersion: widget.version, title, config }, { signal: store.signal });
|
|
62
|
-
if (currentKey() !== savingFor || store.signal.aborted)
|
|
63
|
-
return;
|
|
64
|
-
store.set({ patch: { widget: saved, title: saved.title, config: saved.config, widgetType: saved.widgetType, message: t('Saved · version {version}').replace('{version}', String(saved.version)) } });
|
|
59
|
+
if (isNew)
|
|
60
|
+
await createAndOpen({ widgetType, title, config });
|
|
61
|
+
else if (widget)
|
|
62
|
+
await updateExisting({ widget, title, config, savingFor });
|
|
65
63
|
}
|
|
66
64
|
catch (error) {
|
|
67
|
-
if (
|
|
65
|
+
if (isLiveSave({ savingFor }))
|
|
68
66
|
store.set({ patch: resolveWidgetSaveError({ error }, { t }) });
|
|
69
67
|
}
|
|
70
68
|
finally {
|
|
71
|
-
if (
|
|
69
|
+
if (isLiveSave({ savingFor }))
|
|
72
70
|
store.set({ patch: { saving: false } });
|
|
73
71
|
}
|
|
74
72
|
}
|
|
73
|
+
async function createAndOpen({ widgetType, title, config }, _optional = {}) {
|
|
74
|
+
const { widget: created } = await api.createWidget({ widgetType, title, config }, { signal: store.signal });
|
|
75
|
+
// Creation intentionally navigates even if route identity changed while it was in flight.
|
|
76
|
+
navigate?.(`/widgets/${created.slug}`);
|
|
77
|
+
}
|
|
78
|
+
async function updateExisting({ widget, title, config, savingFor }, _optional = {}) {
|
|
79
|
+
const { widget: saved } = await api.updateWidget({ id: widget.id, baseVersion: widget.version, title, config }, { signal: store.signal });
|
|
80
|
+
if (!isLiveSave({ savingFor }))
|
|
81
|
+
return;
|
|
82
|
+
store.set({ patch: { widget: saved, title: saved.title, config: saved.config, widgetType: saved.widgetType, message: t('Saved · version {version}').replace('{version}', String(saved.version)) } });
|
|
83
|
+
}
|
|
75
84
|
return {
|
|
76
85
|
getSnapshot: store.getSnapshot, subscribe: store.subscribe, load, save,
|
|
77
86
|
setParams({ params: next }, _optional = {}) {
|
|
@@ -6,17 +6,17 @@ import { widgetsEnglish } from '../messages.en.js';
|
|
|
6
6
|
export function createWidgetsLibraryController({ api }, { t = widgetsEnglish } = {}) {
|
|
7
7
|
const store = createControllerStore({ initial: { widgets: null, error: null, skippedCount: 0, skippedIds: [], createType: 'text', pendingTrash: null, trashingId: null } });
|
|
8
8
|
let generation = 0;
|
|
9
|
+
const isLiveRead = ({ ticket }) => ticket === generation && !store.signal.aborted;
|
|
9
10
|
async function load(_required = {}, _optional = {}) {
|
|
10
11
|
const ticket = ++generation;
|
|
11
12
|
try {
|
|
12
13
|
// No includeInactive: trashed widgets belong to the shared Trash screen.
|
|
13
14
|
const result = await api.listWidgets({}, { signal: store.signal });
|
|
14
|
-
if (ticket
|
|
15
|
-
|
|
16
|
-
store.set({ patch: { widgets: result.widgets, skippedCount: result.skippedCount ?? 0, skippedIds: result.skippedIds ?? [] } });
|
|
15
|
+
if (isLiveRead({ ticket }))
|
|
16
|
+
store.set({ patch: { widgets: result.widgets, skippedCount: result.skippedCount ?? 0, skippedIds: result.skippedIds ?? [] } });
|
|
17
17
|
}
|
|
18
18
|
catch (e) {
|
|
19
|
-
if (ticket
|
|
19
|
+
if (isLiveRead({ ticket }))
|
|
20
20
|
store.set({ patch: { error: describeApiError({ e: widgetApiError({ error: e }) ?? e, fallback: t('failed to load widgets') }) } });
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -30,18 +30,24 @@ export function createWidgetsLibraryController({ api }, { t = widgetsEnglish } =
|
|
|
30
30
|
void load();
|
|
31
31
|
}
|
|
32
32
|
catch (error) {
|
|
33
|
-
|
|
34
|
-
if (e?.code === 'NOT_FOUND')
|
|
35
|
-
void load();
|
|
36
|
-
else
|
|
37
|
-
store.set({ patch: { error: e?.code === 'TRASH_VERSION_CHANGED' ? t('This item changed since you loaded it. Reload and try again.') : describeApiError({ e: e ?? error, fallback: t('delete failed') }) } });
|
|
33
|
+
handleTrashFailure({ error });
|
|
38
34
|
}
|
|
39
35
|
finally {
|
|
40
|
-
|
|
41
|
-
const current = store.getSnapshot();
|
|
42
|
-
store.set({ patch: { trashingId: current.trashingId === widget.id ? null : current.trashingId, pendingTrash: current.pendingTrash?.id === widget.id ? null : current.pendingTrash } });
|
|
36
|
+
settleTrash({ widget });
|
|
43
37
|
}
|
|
44
38
|
}
|
|
39
|
+
function handleTrashFailure({ error }, _optional = {}) {
|
|
40
|
+
const e = widgetApiError({ error });
|
|
41
|
+
if (e?.code === 'NOT_FOUND')
|
|
42
|
+
void load();
|
|
43
|
+
else
|
|
44
|
+
store.set({ patch: { error: e?.code === 'TRASH_VERSION_CHANGED' ? t('This item changed since you loaded it. Reload and try again.') : describeApiError({ e: e ?? error, fallback: t('delete failed') }) } });
|
|
45
|
+
}
|
|
46
|
+
function settleTrash({ widget }, _optional = {}) {
|
|
47
|
+
// A's completion cannot close a newly opened confirmation for B.
|
|
48
|
+
const current = store.getSnapshot();
|
|
49
|
+
store.set({ patch: { trashingId: current.trashingId === widget.id ? null : current.trashingId, pendingTrash: current.pendingTrash?.id === widget.id ? null : current.pendingTrash } });
|
|
50
|
+
}
|
|
45
51
|
return {
|
|
46
52
|
getSnapshot: store.getSnapshot, subscribe: store.subscribe, load, confirmTrash,
|
|
47
53
|
setCreateType({ type }, _optional = {}) { store.set({ patch: { createType: type } }); },
|
|
@@ -6,20 +6,21 @@ import { widgetsEnglish } from '../messages.en.js';
|
|
|
6
6
|
export function createWidgetRegionEditorController({ api, regionKey: initialKey }, { t = widgetsEnglish } = {}) {
|
|
7
7
|
const store = createControllerStore({ initial: { area: null, placements: [], message: null, error: null, loading: true, saving: false } });
|
|
8
8
|
let regionKey = initialKey, generation = 0;
|
|
9
|
+
const isLiveRead = ({ ticket }) => ticket === generation && !store.signal.aborted;
|
|
9
10
|
async function load(_required = {}, _optional = {}) {
|
|
10
11
|
const ticket = ++generation;
|
|
11
12
|
store.set({ patch: { loading: true, error: null, saving: false } });
|
|
12
13
|
try {
|
|
13
14
|
const result = await api.getWidgetRegion({ regionKey }, { signal: store.signal });
|
|
14
|
-
if (ticket
|
|
15
|
+
if (isLiveRead({ ticket }))
|
|
15
16
|
store.set({ patch: { area: result.area, placements: result.placements } });
|
|
16
17
|
}
|
|
17
18
|
catch (error) {
|
|
18
|
-
if (ticket
|
|
19
|
+
if (isLiveRead({ ticket }))
|
|
19
20
|
store.set({ patch: { error: describeApiError({ e: widgetApiError({ error }) ?? error, fallback: t('failed to load region') }) } });
|
|
20
21
|
}
|
|
21
22
|
finally {
|
|
22
|
-
if (ticket
|
|
23
|
+
if (isLiveRead({ ticket }))
|
|
23
24
|
store.set({ patch: { loading: false } });
|
|
24
25
|
}
|
|
25
26
|
}
|
|
@@ -40,7 +41,7 @@ export function createWidgetRegionEditorController({ api, regionKey: initialKey
|
|
|
40
41
|
let stale = false;
|
|
41
42
|
try {
|
|
42
43
|
const { area: saved } = await api.mutateWidgetRegionPlacements({ regionKey, baseVersion: area.version, placements: placements.map(p => ({ placementId: p.placementId, widgetEntryId: p.widgetEntryId, enabled: p.enabled })) }, { signal: store.signal });
|
|
43
|
-
stale = ticket
|
|
44
|
+
stale = !isLiveRead({ ticket });
|
|
44
45
|
if (stale)
|
|
45
46
|
return;
|
|
46
47
|
store.set({ patch: { area: saved, message: t('Saved · version {version}').replace('{version}', String(saved.version)) } });
|
|
@@ -48,7 +49,7 @@ export function createWidgetRegionEditorController({ api, regionKey: initialKey
|
|
|
48
49
|
void load();
|
|
49
50
|
}
|
|
50
51
|
catch (error) {
|
|
51
|
-
stale = ticket
|
|
52
|
+
stale = !isLiveRead({ ticket });
|
|
52
53
|
if (!stale)
|
|
53
54
|
store.set({ patch: { error: resolveWidgetRegionSaveError({ error }, { t }) } });
|
|
54
55
|
}
|
|
@@ -61,6 +61,7 @@ export declare function useWidgetInstanceEditorView({ widgetId, widgetType, widg
|
|
|
61
61
|
widgetTypes: readonly WidgetTypeOption[];
|
|
62
62
|
useWidgetInstanceEditorHook?: typeof useWiredWidgetInstanceEditor | undefined;
|
|
63
63
|
}, _optional?: Record<string, never>): {
|
|
64
|
+
gate: import("../../rules.js").WidgetEditorGate;
|
|
64
65
|
guard: import("../../rules.js").WidgetInstanceGuard | null;
|
|
65
66
|
onBackClick: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
66
67
|
isNew: boolean;
|
|
@@ -19,12 +19,13 @@ export function useWidgetRegionEditorView({ regionKey, useWidgetRegionEditorHook
|
|
|
19
19
|
const state = useWidgetRegionEditorHook(regionKey);
|
|
20
20
|
return { ...state, placementHandles: buildAgentListHandles({ prefix: 'widget-region-placement', ids: state.placements.map(p => p.placementId) }) };
|
|
21
21
|
}
|
|
22
|
-
import {
|
|
22
|
+
import { widgetEditorGate } from '../../rules.js';
|
|
23
23
|
import { useWiredWidgetInstanceEditor } from './use-widget-instance-editor.hooks.js';
|
|
24
24
|
/** The back-link cancellation also stops the host's document-level internal-link interceptor. */
|
|
25
25
|
export function useWidgetInstanceEditorView({ widgetId, widgetType, widgetTypes, useWidgetInstanceEditorHook = useWiredWidgetInstanceEditor }, _optional = {}) {
|
|
26
26
|
const state = useWidgetInstanceEditorHook({ widgetId, widgetType });
|
|
27
|
-
|
|
27
|
+
const gate = widgetEditorGate({ ...state, types: widgetTypes });
|
|
28
|
+
return { ...state, gate, guard: gate.guard, onBackClick: (event) => { if (!state.confirmLeave())
|
|
28
29
|
event.preventDefault(); } };
|
|
29
30
|
}
|
|
30
31
|
//# sourceMappingURL=page-views.hooks.js.map
|
|
@@ -13,7 +13,7 @@ export interface WidgetRegionsController {
|
|
|
13
13
|
import type { AdminWidgetsPort, WidgetsEventsPort } from '../../ports.js';
|
|
14
14
|
export declare function useWidgetRegions({ api }: {
|
|
15
15
|
api: AdminWidgetsPort;
|
|
16
|
-
}, { t,
|
|
16
|
+
}, { t, events, navigate }?: {
|
|
17
17
|
t?: Translate | undefined;
|
|
18
18
|
locale?: string | undefined;
|
|
19
19
|
events?: WidgetsEventsPort | undefined;
|
|
@@ -6,7 +6,8 @@ import { createWidgetRegionsController } from '../../controllers/regions.control
|
|
|
6
6
|
import { widgetsEnglish } from '../../messages.en.js';
|
|
7
7
|
import { WIDGETS_REGIONS_RESOURCE } from '../../rules.js';
|
|
8
8
|
import { useWidgetsPorts, useWidgetsOptions } from './WidgetsPorts.hooks.js';
|
|
9
|
-
|
|
9
|
+
import { useWidgetsNavigate } from './navigation.hooks.js';
|
|
10
|
+
export function useWidgetRegions({ api }, { t = widgetsEnglish, events, navigate } = {}) {
|
|
10
11
|
const cached = useWidgetRegionsQuery({ api });
|
|
11
12
|
const invalidate = useInvalidate();
|
|
12
13
|
const latest = useRef({ t, navigate });
|
|
@@ -14,7 +15,13 @@ export function useWidgetRegions({ api }, { t = widgetsEnglish, locale = 'en', e
|
|
|
14
15
|
const { controller, snapshot } = useController({ create: () => createWidgetRegionsController({ api: cached.api }, { t: key => latest.current.t(key), navigate: path => latest.current.navigate?.(path) }), dependencies: [cached.api] }, { start: ({ controller }) => { void controller.load(); } });
|
|
15
16
|
useEffect(() => { if (!controller)
|
|
16
17
|
return; return events?.subscribe({ resource: WIDGETS_REGIONS_RESOURCE, onRefresh: () => invalidate({ key: WIDGET_QUERY_KEYS.regions }) }); }, [controller, events, invalidate]);
|
|
17
|
-
return { ...(
|
|
18
|
+
return { ...mergeRegionsState({ snapshot, data: cached.query.data, queryError: cached.query.error }), setNewRegionKey: value => controller?.setNewRegionKey({ value }), bind: async () => { await controller?.bind(); }, t };
|
|
19
|
+
}
|
|
20
|
+
const EMPTY_REGIONS_STATE = { regions: null, error: null, newRegionKey: '', binding: false };
|
|
21
|
+
/** Cached query data wins for the list; a controller error wins over a query error. */
|
|
22
|
+
function mergeRegionsState({ snapshot, data, queryError }, _optional = {}) {
|
|
23
|
+
const state = snapshot ?? EMPTY_REGIONS_STATE;
|
|
24
|
+
return { ...state, regions: data?.regions ?? state.regions, error: state.error ?? queryError?.message ?? null };
|
|
18
25
|
}
|
|
19
26
|
export function useWiredWidgetRegions() {
|
|
20
27
|
const { widgetsApi: api, widgetsEvents: events, widgetsNavigation } = useWidgetsPorts();
|
|
@@ -22,5 +29,4 @@ export function useWiredWidgetRegions() {
|
|
|
22
29
|
const navigate = useWidgetsNavigate({ navigation: widgetsNavigation, navigationBase });
|
|
23
30
|
return useWidgetRegions({ api }, { t, locale, events, navigate });
|
|
24
31
|
}
|
|
25
|
-
import { useWidgetsNavigate } from './navigation.hooks.js';
|
|
26
32
|
//# sourceMappingURL=use-widget-regions.hooks.js.map
|
|
@@ -14,11 +14,18 @@ export function useWidgetsLibrary({ api }, { t = widgetsEnglish, locale = 'en',
|
|
|
14
14
|
const { controller, snapshot } = useController({ create: () => createWidgetsLibraryController({ api: cached.api }, { t: key => latestT.current(key) }), dependencies: [cached.api] }, { start: ({ controller }) => { void controller.load(); } });
|
|
15
15
|
useEffect(() => { if (!controller)
|
|
16
16
|
return; return events?.subscribe({ resource: WIDGETS_LIBRARY_RESOURCE, onRefresh: () => invalidate({ key: WIDGET_QUERY_KEYS.library }) }); }, [controller, events, invalidate]);
|
|
17
|
-
|
|
18
|
-
return { ...state, widgets: cached.query.data?.widgets ?? state.widgets, skippedCount: cached.query.data?.skippedCount ?? state.skippedCount, skippedIds: cached.query.data?.skippedIds ?? state.skippedIds, error: state.error ?? cached.query.error?.message ?? null, trashing: state.pendingTrash !== null && state.trashingId === state.pendingTrash.id,
|
|
17
|
+
return { ...mergeLibraryState({ snapshot, data: cached.query.data, queryError: cached.query.error }),
|
|
19
18
|
setCreateType: type => controller?.setCreateType({ type }), requestTrash: widget => controller?.requestTrash({ widget }),
|
|
20
19
|
confirmTrash: async () => { await controller?.confirmTrash(); }, cancelTrash: () => controller?.cancelTrash(), t, locale };
|
|
21
20
|
}
|
|
21
|
+
const EMPTY_LIBRARY_STATE = { widgets: null, error: null, skippedCount: 0, skippedIds: [], createType: 'text', pendingTrash: null, trashingId: null };
|
|
22
|
+
function mergeLibraryState({ snapshot, data, queryError }, _optional = {}) {
|
|
23
|
+
const state = snapshot ?? EMPTY_LIBRARY_STATE;
|
|
24
|
+
return { ...state, ...cachedListFields({ state, data }), error: state.error ?? queryError?.message ?? null, trashing: state.pendingTrash !== null && state.trashingId === state.pendingTrash.id };
|
|
25
|
+
}
|
|
26
|
+
function cachedListFields({ state, data }, _optional = {}) {
|
|
27
|
+
return { widgets: data?.widgets ?? state.widgets, skippedCount: data?.skippedCount ?? state.skippedCount, skippedIds: data?.skippedIds ?? state.skippedIds };
|
|
28
|
+
}
|
|
22
29
|
export function useWiredWidgetsLibrary() {
|
|
23
30
|
const { widgetsApi: api, widgetsEvents: events } = useWidgetsPorts();
|
|
24
31
|
const { t, locale } = useWidgetsOptions();
|
|
@@ -18,6 +18,6 @@ export interface WidgetInstanceEditorProps {
|
|
|
18
18
|
*/
|
|
19
19
|
useWidgetInstanceEditorHook?: typeof useWiredWidgetInstanceEditor | undefined;
|
|
20
20
|
}
|
|
21
|
-
export declare function WidgetInstanceEditor(props: WidgetInstanceEditorProps): import("react").JSX.Element
|
|
21
|
+
export declare function WidgetInstanceEditor(props: WidgetInstanceEditorProps): import("react").JSX.Element;
|
|
22
22
|
export default WidgetInstanceEditor;
|
|
23
23
|
//# sourceMappingURL=WidgetInstanceEditor.d.ts.map
|
|
@@ -18,14 +18,13 @@ import { WhereUsedBanner, WidgetInstanceGuardNotice } from '../components/Widget
|
|
|
18
18
|
export function WidgetInstanceEditor(props) {
|
|
19
19
|
const { slots: { ConfigFields: WidgetConfigFields }, widgetTypes } = useWidgetsOptions();
|
|
20
20
|
const { widgetId, widgetType: queryWidgetType, useWidgetInstanceEditorHook = useWiredWidgetInstanceEditor } = props;
|
|
21
|
-
const { isNew, widget, whereUsed, title, setTitle, config, setConfig, message, error, fieldErrors, loading, saving,
|
|
22
|
-
if (guard)
|
|
23
|
-
return _jsx(WidgetInstanceGuardNotice, { guard: guard, t: t });
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
return null;
|
|
21
|
+
const { isNew, widget, whereUsed, title, setTitle, config, setConfig, message, error, fieldErrors, loading, saving, save, confirmLeave, t, locale, gate, onBackClick, } = useWidgetInstanceEditorView({ widgetId, widgetType: queryWidgetType, useWidgetInstanceEditorHook, widgetTypes });
|
|
22
|
+
if (gate.guard !== null)
|
|
23
|
+
return _jsx(WidgetInstanceGuardNotice, { guard: gate.guard, t: t });
|
|
24
|
+
// `widgetInstanceGuard`'s "no-type" case already covers a null `widgetType` above, but TS can't
|
|
25
|
+
// see through that opaque function call; `widgetEditorGate` hands back the narrowed type so the
|
|
26
|
+
// JSX below needs neither a `!` assertion nor a never-taken null check.
|
|
27
|
+
const { widgetType } = gate;
|
|
29
28
|
// The host slot binds shared-component translations; screen copy uses the module t.
|
|
30
29
|
const sharedT = t;
|
|
31
30
|
return (_jsxs("div", { className: "page", children: [_jsxs("div", { className: "page-header page-header-split", ...agentHandle({ handle: "widget-instance-header" }, {
|