@jini-ai/admin 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-plugins/package-file-tree.d.ts +3 -1
- package/dist/agent-plugins/package-file-tree.js +7 -7
- package/dist/comments/controllers/comment-queue.controller.d.ts +5 -1
- package/dist/comments/controllers/comment-queue.controller.js +2 -2
- package/dist/comments/react/hooks/use-comment-queue.hooks.js +14 -3
- package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -0
- package/dist/consent/react/ConsentSettingsPanel.js +24 -0
- package/dist/consent/react/index.d.ts +3 -0
- package/dist/consent/react/index.js +2 -0
- package/dist/consent/react/settings-panel.hooks.d.ts +19 -0
- package/dist/consent/react/settings-panel.hooks.js +30 -0
- package/dist/core/ports/forms.d.ts +23 -3
- package/dist/core/ports/forms.js +3 -3
- package/dist/core/ports/seo.d.ts +17 -0
- package/dist/forms/adapters/http.d.ts +5 -2
- package/dist/forms/adapters/http.js +7 -2
- package/dist/forms/controllers/field-attributes-dialog.controller.d.ts +59 -1
- package/dist/forms/controllers/field-attributes-dialog.controller.js +27 -2
- package/dist/forms/field-settings.d.ts +142 -0
- package/dist/forms/field-settings.js +327 -0
- package/dist/forms/form-editor-dirty.d.ts +42 -0
- package/dist/forms/form-editor-dirty.js +25 -0
- package/dist/forms/forms.module.d.ts +72 -0
- package/dist/forms/html-rules.d.ts +41 -3
- package/dist/forms/html-rules.js +118 -17
- package/dist/forms/messages.en.d.ts +72 -0
- package/dist/forms/messages.en.js +73 -0
- package/dist/forms/react/components/FieldAttributesDialog.js +20 -19
- package/dist/forms/react/components/FieldSettingsSection.d.ts +19 -0
- package/dist/forms/react/components/FieldSettingsSection.js +21 -0
- package/dist/forms/react/components/FormFieldsEditor.js +7 -8
- package/dist/forms/react/components/FormSubmissionDetail.js +5 -2
- package/dist/forms/react/hooks/forms-view.hooks.d.ts +75 -5
- package/dist/forms/react/hooks/forms-view.hooks.js +46 -2
- package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.d.ts +10 -0
- package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.js +12 -2
- package/dist/forms/react/hooks/use-form-editor.hooks.d.ts +11 -0
- package/dist/forms/react/hooks/use-form-editor.hooks.js +25 -2
- package/dist/forms/react/hooks/use-form-fields-editor.hooks.d.ts +2 -0
- package/dist/forms/react/hooks/use-form-fields-editor.hooks.js +5 -1
- package/dist/forms/react/hooks/use-form-submission-detail.hooks.d.ts +4 -0
- package/dist/forms/react/hooks/use-form-submission-detail.hooks.js +1 -0
- package/dist/forms/react/index.d.ts +216 -0
- package/dist/forms/react/pages/FormEditor.js +3 -2
- package/dist/forms/rules.d.ts +10 -1
- package/dist/forms/rules.js +8 -1
- package/dist/media/controllers/library.controller.d.ts +4 -0
- package/dist/media/controllers/library.controller.js +13 -0
- package/dist/media/react/components/MediaPicker.js +2 -2
- package/dist/media/react/hooks/EditMediaPanel.hooks.js +4 -2
- package/dist/media/react/hooks/GallerySelection.hooks.d.ts +31 -0
- package/dist/media/react/hooks/GallerySelection.hooks.js +32 -0
- package/dist/media/react/hooks/LibraryTab.hooks.d.ts +4 -0
- package/dist/media/react/hooks/LibraryTab.hooks.js +9 -3
- package/dist/media/react/hooks/MediaCard.hooks.d.ts +2 -2
- package/dist/media/react/hooks/MediaGrid.hooks.d.ts +2 -2
- package/dist/media/react/hooks/MediaPage.hooks.d.ts +4 -0
- package/dist/media/react/hooks/MediaPage.hooks.js +10 -13
- package/dist/media/react/hooks/MediaPicker.hooks.js +5 -1
- package/dist/media/react/hooks/MediaQuery.hooks.d.ts +27 -0
- package/dist/media/react/hooks/MediaQuery.hooks.js +41 -0
- package/dist/media/react/hooks/ProvidersTab.hooks.js +21 -2
- package/dist/media/react/index.d.ts +1 -0
- package/dist/media/react/index.js +1 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +5 -0
- package/dist/react/components/Sidebar.d.ts +6 -0
- package/dist/react/components/Sidebar.js +1 -0
- package/dist/react/entities/use-entity-edit.d.ts +1 -1
- package/dist/react/hooks/use-sidebar-rail.d.ts +10 -1
- package/dist/react/hooks/use-sidebar-rail.js +16 -3
- package/dist/seo/adapters/http.d.ts +11 -1
- package/dist/seo/adapters/http.js +11 -1
- package/dist/seo/controllers/entry-picker.controller.d.ts +2 -0
- package/dist/seo/controllers/entry-picker.controller.js +2 -0
- package/dist/seo/models.d.ts +8 -0
- package/dist/seo/react/components/MediaRefField.js +2 -2
- package/dist/seo/react/hooks/MediaRefField.hooks.d.ts +4 -2
- package/dist/seo/react/hooks/MediaRefField.hooks.js +8 -3
- package/dist/seo/react/hooks/SeoQuery.hooks.d.ts +42 -0
- package/dist/seo/react/hooks/SeoQuery.hooks.js +65 -0
- package/dist/seo/react/hooks/use-entry-picker.hooks.js +7 -9
- package/dist/seo/react/hooks/use-seo-entry-panel.hooks.js +9 -2
- package/dist/seo/react/hooks/use-seo.hooks.js +22 -4
- package/dist/seo/react/hooks/use-share-image-warning.hooks.d.ts +19 -0
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +79 -0
- package/dist/seo/react/hooks/use-sitemap-modal.hooks.js +5 -3
- package/dist/seo/react/pages/SeoPage.js +1 -1
- package/dist/seo/rules.d.ts +21 -2
- package/dist/seo/rules.js +44 -4
- package/dist/widgets/react/hooks/WidgetsQuery.hooks.d.ts +37 -0
- package/dist/widgets/react/hooks/WidgetsQuery.hooks.js +70 -0
- package/dist/widgets/react/hooks/use-widget-instance-editor.hooks.js +5 -3
- package/dist/widgets/react/hooks/use-widget-region-editor.hooks.js +3 -1
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +7 -3
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +7 -3
- package/package.json +20 -14
|
@@ -41,7 +41,9 @@ export interface PackageFileTreeRow {
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function buildPackageFileTree({ paths }: {
|
|
43
43
|
paths: readonly string[];
|
|
44
|
-
},
|
|
44
|
+
}, optional?: {
|
|
45
|
+
locale?: string | string[];
|
|
46
|
+
}): PackageFileTreeNode[];
|
|
45
47
|
/**
|
|
46
48
|
* The path of the tree's top file row once every folder on the way is open — the first file in
|
|
47
49
|
* display order (files before folders, so a root file wins over anything nested); `null` for no files.
|
|
@@ -8,20 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
const collator = new Intl.Collator("en", { numeric: true, sensitivity: "base" });
|
|
10
10
|
/** Files first, then folders, each alphabetical (owner's call, 2026-09-29 — VS Code's default is the reverse). */
|
|
11
|
-
function compareNodes(a, b) {
|
|
11
|
+
function compareNodes(a, b, sorter = collator) {
|
|
12
12
|
if (a.kind !== b.kind)
|
|
13
13
|
return a.kind === "file" ? -1 : 1;
|
|
14
|
-
return
|
|
14
|
+
return sorter.compare(a.name, b.name) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
15
15
|
}
|
|
16
|
-
function freeze(folder, prefix) {
|
|
16
|
+
function freeze(folder, prefix, sorter = collator) {
|
|
17
17
|
const nodes = [];
|
|
18
18
|
for (const [name, child] of folder.folders) {
|
|
19
19
|
const path = prefix ? `${prefix}/${name}` : name;
|
|
20
|
-
nodes.push({ kind: "folder", name, path, children: freeze(child, path) });
|
|
20
|
+
nodes.push({ kind: "folder", name, path, children: freeze(child, path, sorter) });
|
|
21
21
|
}
|
|
22
22
|
for (const file of folder.files)
|
|
23
23
|
nodes.push({ kind: "file", ...file });
|
|
24
|
-
return nodes.sort(compareNodes);
|
|
24
|
+
return nodes.sort((a, b) => compareNodes(a, b, sorter));
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Groups flat package-relative paths into a folder tree. Empty segments (a leading, trailing or
|
|
@@ -30,7 +30,7 @@ function freeze(folder, prefix) {
|
|
|
30
30
|
*
|
|
31
31
|
* @complexity O(n·d + n log n) for n paths of depth d.
|
|
32
32
|
*/
|
|
33
|
-
export function buildPackageFileTree({ paths },
|
|
33
|
+
export function buildPackageFileTree({ paths }, optional = {}) {
|
|
34
34
|
const root = { folders: new Map(), files: [] };
|
|
35
35
|
const seen = new Set();
|
|
36
36
|
for (const path of paths) {
|
|
@@ -53,7 +53,7 @@ export function buildPackageFileTree({ paths }, _optional = {}) {
|
|
|
53
53
|
seen.add(normalized);
|
|
54
54
|
folder.files.push({ name: fileName, path });
|
|
55
55
|
}
|
|
56
|
-
return freeze(root, "");
|
|
56
|
+
return freeze(root, "", optional.locale === undefined ? collator : new Intl.Collator(optional.locale, { numeric: true, sensitivity: "base" }));
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* The path of the tree's top file row once every folder on the way is open — the first file in
|
|
@@ -11,9 +11,13 @@ import { type CommentsTranslator } from '../messages.en.js';
|
|
|
11
11
|
export declare function createCommentQueueController({ api, permissions }: {
|
|
12
12
|
api: AdminCommentsPort;
|
|
13
13
|
permissions?: readonly string[];
|
|
14
|
-
}, { t, onInvalidate }?: {
|
|
14
|
+
}, { t, onInvalidate, loadPage }?: {
|
|
15
15
|
t?: CommentsTranslator;
|
|
16
16
|
onInvalidate?: () => void;
|
|
17
|
+
loadPage?: (required: {
|
|
18
|
+
status: CommentStatus;
|
|
19
|
+
cursor: string;
|
|
20
|
+
}) => Promise<AdminCommentsQueuePage>;
|
|
17
21
|
}): {
|
|
18
22
|
getSnapshot: () => Readonly<CommentQueueState>;
|
|
19
23
|
subscribe: ({ listener }: {
|
|
@@ -9,7 +9,7 @@ import { englishComments } from '../messages.en.js';
|
|
|
9
9
|
* @param optional Translator and an invalidation callback for every cached status.
|
|
10
10
|
* @returns An instance-scoped store and actions; dispose makes pending settlements inert.
|
|
11
11
|
*/
|
|
12
|
-
export function createCommentQueueController({ api, permissions = [] }, { t = englishComments, onInvalidate } = {}) {
|
|
12
|
+
export function createCommentQueueController({ api, permissions = [] }, { t = englishComments, onInvalidate, loadPage } = {}) {
|
|
13
13
|
const store = createControllerStore({ initial: {
|
|
14
14
|
status: 'pending', items: null, nextCursor: null, error: null, loadingMore: false,
|
|
15
15
|
rowState: {}, pendingPurge: null,
|
|
@@ -91,7 +91,7 @@ export function createCommentQueueController({ api, permissions = [] }, { t = en
|
|
|
91
91
|
store.set({ patch: { loadingMore: true } });
|
|
92
92
|
publishPages();
|
|
93
93
|
try {
|
|
94
|
-
const page = await api.listCommentsQueue({}, { status, cursor: nextCursor });
|
|
94
|
+
const page = await (loadPage ? loadPage({ status, cursor: nextCursor }) : api.listCommentsQueue({}, { status, cursor: nextCursor }));
|
|
95
95
|
if (generation !== pageGeneration || store.signal.aborted)
|
|
96
96
|
return;
|
|
97
97
|
morePages = [...morePages, ...page.items];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useCallback, useEffect } from 'react';
|
|
2
|
-
import { useFetchQuery, useInvalidate } from '@jini-ai/ui/fetch-query';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { useCachedLoader, useFetchQuery, useInvalidate } from '@jini-ai/ui/fetch-query';
|
|
3
3
|
import { useController } from '../../../core/react/use-controller.js';
|
|
4
4
|
import { createCommentQueueController } from '../../controllers/comment-queue.controller.js';
|
|
5
5
|
import { KEYS, emptyRowState } from '../../rules.js';
|
|
@@ -13,12 +13,23 @@ const NO_GRANTS = [];
|
|
|
13
13
|
export function useCommentQueue({ api, permissions = NO_GRANTS }, { t = englishComments, events } = {}) {
|
|
14
14
|
const invalidate = useInvalidate();
|
|
15
15
|
const invalidateQueue = useCallback(() => invalidate({ key: KEYS.queueRoot }), [invalidate]);
|
|
16
|
+
const loadPageRef = useRef(null);
|
|
16
17
|
const { controller, snapshot } = useController({
|
|
17
|
-
create: () => createCommentQueueController({ api, permissions }, { t, onInvalidate: invalidateQueue }),
|
|
18
|
+
create: () => createCommentQueueController({ api, permissions }, { t, onInvalidate: invalidateQueue, loadPage: required => loadPageRef.current(required) }),
|
|
18
19
|
dependencies: [api, invalidateQueue],
|
|
19
20
|
});
|
|
20
21
|
useEffect(() => { controller?.configure({ permissions, t }); }, [controller, permissions, t]);
|
|
21
22
|
const status = snapshot?.status ?? 'pending';
|
|
23
|
+
const cursor = snapshot?.nextCursor ?? '';
|
|
24
|
+
const cursorKey = useMemo(() => [...KEYS.queue(status), 'cursor', cursor], [status, cursor]);
|
|
25
|
+
const fetchCursor = useCallback(() => api.listCommentsQueue({}, { status, cursor }), [api, status, cursor]);
|
|
26
|
+
const cursorLoader = useCachedLoader({ key: cursorKey, fetch: fetchCursor }, { staleTime: 0 });
|
|
27
|
+
// The gesture remains controller-owned, including its same-tick lock and generation fence.
|
|
28
|
+
// Only I/O changes: cursor pages now share page one's provider and status invalidation prefix.
|
|
29
|
+
loadPageRef.current = ({ status: requestedStatus, cursor: requestedCursor }) => cursorLoader.load({
|
|
30
|
+
key: [...KEYS.queue(requestedStatus), 'cursor', requestedCursor],
|
|
31
|
+
fetch: () => api.listCommentsQueue({}, { status: requestedStatus, cursor: requestedCursor }),
|
|
32
|
+
});
|
|
22
33
|
const firstPage = useFetchQuery({ key: KEYS.queue(status), fetch: () => api.listCommentsQueue({}, { status }) });
|
|
23
34
|
useEffect(() => { controller?.settleFirstPage({ page: firstPage.data ?? null, error: firstPage.error, status }); }, [controller, firstPage.data, firstPage.error, status]);
|
|
24
35
|
// Stable identity avoids resubscribing on every render. The host filters out unrelated
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The host's settings controller uses its existing ledger lifecycle. */
|
|
2
|
+
export interface ConsentSettingsView {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
regionMode: 'auto' | 'opt-in' | 'opt-out';
|
|
5
|
+
policyVersion: string;
|
|
6
|
+
privacyPolicyUrl: string;
|
|
7
|
+
title: string;
|
|
8
|
+
message: string;
|
|
9
|
+
acceptLabel: string;
|
|
10
|
+
rejectLabel: string;
|
|
11
|
+
preferencesLabel: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ConsentSettingsPanelProps {
|
|
14
|
+
value: ConsentSettingsView;
|
|
15
|
+
onChange(required: {
|
|
16
|
+
patch: Partial<ConsentSettingsView>;
|
|
17
|
+
}, optional?: Record<string, never>): void;
|
|
18
|
+
loading: boolean;
|
|
19
|
+
error: string | null;
|
|
20
|
+
status: string;
|
|
21
|
+
/** Host translations keyed by the exact fallback copy; unspecified entries use English. */
|
|
22
|
+
dictionary?: Readonly<Record<string, string>>;
|
|
23
|
+
}
|
|
24
|
+
/** Shared jini settings classes keep the panel in the host's design language.
|
|
25
|
+
* Native labels/selects/checkboxes support keyboard use at desktop and narrow widths. */
|
|
26
|
+
export declare function ConsentSettingsPanel({ value, onChange, loading, error, status, dictionary }: ConsentSettingsPanelProps): import("react").JSX.Element;
|
|
27
|
+
//# sourceMappingURL=ConsentSettingsPanel.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
import { useConsentSettingsPanel, usePolicyVersionInput } from './settings-panel.hooks.js';
|
|
4
|
+
const TEXT_FIELDS = [
|
|
5
|
+
['title', 'Banner title', 2000], ['message', 'Banner message', 2000],
|
|
6
|
+
['acceptLabel', 'Accept button text', 2000], ['rejectLabel', 'Reject button text', 2000],
|
|
7
|
+
['preferencesLabel', 'Preferences label', 2000], ['policyVersion', 'Policy version', 128],
|
|
8
|
+
['privacyPolicyUrl', 'Privacy policy page URL', 2000],
|
|
9
|
+
];
|
|
10
|
+
/** Shared jini settings classes keep the panel in the host's design language.
|
|
11
|
+
* Native labels/selects/checkboxes support keyboard use at desktop and narrow widths. */
|
|
12
|
+
export function ConsentSettingsPanel({ value, onChange, loading, error, status, dictionary = {} }) {
|
|
13
|
+
const id = useId();
|
|
14
|
+
const t = (text) => dictionary[text] ?? text;
|
|
15
|
+
const handlers = useConsentSettingsPanel({ value, onChange });
|
|
16
|
+
return _jsxs("section", { className: "jini-settings-section", "aria-labelledby": `${id}-heading`, children: [_jsx("div", { className: "jini-section-head", children: _jsxs("div", { children: [_jsx("h4", { id: `${id}-heading`, children: t('Site cookie consent') }), _jsx("p", { className: "jini-hint", children: t('Provides the consent mechanism, not legal advice. Review your texts, categories, and privacy policy for your site.') })] }) }), _jsx("p", { className: "jini-hint", children: t('Off by default. In opt-in regions, optional scripts stay paused until a visitor chooses. Unclassified third-party scripts use the marketing category. Global Privacy Control disables analytics and marketing.') }), _jsxs("button", { type: "button", className: value.enabled ? 'jini-toggle-row on' : 'jini-toggle-row', role: "switch", "aria-checked": value.enabled, "aria-labelledby": `${id}-enabled`, "aria-describedby": `${id}-hint`, disabled: loading, onClick: handlers.toggle, children: [_jsxs("span", { className: "jini-toggle-row-text", children: [_jsx("span", { id: `${id}-enabled`, className: "jini-toggle-row-label", children: t('Enable cookie consent on this site') }), _jsx("span", { id: `${id}-hint`, className: "jini-toggle-row-hint", children: t('Applies to published site pages.') })] }), _jsx("span", { className: "jini-toggle-row-switch", "aria-hidden": "true" })] }), _jsxs("fieldset", { disabled: loading, className: "jini-consent-settings-fields", children: [_jsx("legend", { children: t('Banner settings') }), _jsxs("label", { className: "jini-consent-settings-field", children: [_jsx("span", { className: "jini-field-label", children: t('Region mode') }), _jsxs("select", { className: "jini-input", value: value.regionMode, onChange: handlers.region, children: [_jsx("option", { value: "auto", children: t('Automatic (unknown region: opt-in)') }), _jsx("option", { value: "opt-in", children: t('Opt-in everywhere (EU / UK)') }), _jsx("option", { value: "opt-out", children: t('Opt-out everywhere (California)') })] })] }), _jsx("p", { className: "jini-hint", children: t('Automatic uses a trusted request-region header when configured. California permits optional categories until opt-out; Global Privacy Control always disables analytics and marketing.') }), TEXT_FIELDS.map(([key, label, maxLength]) => _jsxs("label", { className: "jini-consent-settings-field", children: [_jsx("span", { className: "jini-field-label", children: t(label) }), key === 'policyVersion' ? _jsx(PolicyVersionInput, { value: value.policyVersion, onChange: onChange }) : key === 'message' ? _jsx("textarea", { className: "jini-input", value: value[key], maxLength: maxLength, rows: 3, onChange: handlers.text(key) }) : _jsx("input", { className: "jini-input", type: "text", value: value[key], maxLength: maxLength, onChange: handlers.text(key) })] }, key)), _jsx("p", { className: "jini-hint", children: t('Link a published privacy policy page with its URL, such as /privacy. Change the policy version to ask visitors to choose again.') })] }), error && _jsx("p", { className: "jini-hint jini-hint-error", role: "alert", children: error }), _jsx("p", { className: "jini-hint", role: "status", children: status })] });
|
|
17
|
+
}
|
|
18
|
+
/** Keep transient empty edits local; only a valid policy version reaches autosave.
|
|
19
|
+
* Native validity gives keyboard users an actionable error. @complexity O(version length), <=128. */
|
|
20
|
+
function PolicyVersionInput({ value, onChange }) {
|
|
21
|
+
const { draft, change, blur } = usePolicyVersionInput({ value, onChange });
|
|
22
|
+
return _jsx("input", { className: "jini-input", type: "text", value: draft, required: true, pattern: String.raw `.*\S.*`, maxLength: 128, onChange: change, onBlur: blur });
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=ConsentSettingsPanel.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ChangeEvent, type FocusEvent } from 'react';
|
|
2
|
+
import type { ConsentSettingsPanelProps, ConsentSettingsView } from './ConsentSettingsPanel.js';
|
|
3
|
+
/** Keep value conversions and toggle behavior out of the view. React owns event signatures. */
|
|
4
|
+
export declare function useConsentSettingsPanel({ value, onChange }: Pick<ConsentSettingsPanelProps, 'value' | 'onChange'>, _options?: {}): {
|
|
5
|
+
toggle: () => void;
|
|
6
|
+
region: (event: ChangeEvent<HTMLSelectElement>) => void;
|
|
7
|
+
text: (key: keyof ConsentSettingsView) => (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
8
|
+
};
|
|
9
|
+
/** Keep transient empty edits local; only a valid policy version reaches autosave.
|
|
10
|
+
* Native validity gives keyboard users an actionable error. Valid edits save before navigation.
|
|
11
|
+
* @complexity O(version length), <=128. */
|
|
12
|
+
export declare function usePolicyVersionInput({ value, onChange }: Pick<ConsentSettingsPanelProps, 'onChange'> & {
|
|
13
|
+
value: string;
|
|
14
|
+
}, _options?: {}): {
|
|
15
|
+
draft: string;
|
|
16
|
+
change: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
17
|
+
blur: (event: FocusEvent<HTMLInputElement>) => void;
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=settings-panel.hooks.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
/** Keep value conversions and toggle behavior out of the view. React owns event signatures. */
|
|
3
|
+
export function useConsentSettingsPanel({ value, onChange }, _options = {}) {
|
|
4
|
+
return {
|
|
5
|
+
toggle: () => onChange({ patch: { enabled: !value.enabled } }),
|
|
6
|
+
region: (event) => onChange({ patch: { regionMode: event.target.value } }),
|
|
7
|
+
text: (key) => (event) => onChange({ patch: { [key]: event.target.value } }),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** Keep transient empty edits local; only a valid policy version reaches autosave.
|
|
11
|
+
* Native validity gives keyboard users an actionable error. Valid edits save before navigation.
|
|
12
|
+
* @complexity O(version length), <=128. */
|
|
13
|
+
export function usePolicyVersionInput({ value, onChange }, _options = {}) {
|
|
14
|
+
const [draft, setDraft] = useState(value);
|
|
15
|
+
useEffect(() => setDraft(value), [value]);
|
|
16
|
+
return {
|
|
17
|
+
draft,
|
|
18
|
+
change: (event) => {
|
|
19
|
+
const next = event.currentTarget.value;
|
|
20
|
+
setDraft(next);
|
|
21
|
+
if (event.currentTarget.checkValidity())
|
|
22
|
+
onChange({ patch: { policyVersion: next } });
|
|
23
|
+
},
|
|
24
|
+
blur: (event) => {
|
|
25
|
+
if (!event.currentTarget.checkValidity())
|
|
26
|
+
event.currentTarget.reportValidity();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=settings-panel.hooks.js.map
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
*
|
|
55
55
|
* `FormFieldType` and `FormDefinitionStatus` are reference-implementation-specific vocabularies — OPEN, via the
|
|
56
56
|
* `T | (string & {})` idiom `seo.ts`'s file header introduces for this port set. `FormFieldType` in
|
|
57
|
-
* particular
|
|
58
|
-
*
|
|
59
|
-
* contract standing in the way.
|
|
57
|
+
* particular names only the original four kinds; the reference implementation's field registry
|
|
58
|
+
* (`@jini-ai/cms/forms` `FIELD_TYPE_REGISTRY`) now owns the full vocabulary, and a host offering a
|
|
59
|
+
* richer form builder needs room to add its own without this contract standing in the way.
|
|
60
60
|
*/
|
|
61
61
|
/** A form field's input kind — open, see file header. */
|
|
62
62
|
export type FormFieldType = "text" | "email" | "textarea" | "checkbox" | (string & {});
|
|
@@ -71,6 +71,21 @@ export interface AdminFormField {
|
|
|
71
71
|
readonly maxLength?: number | null;
|
|
72
72
|
readonly className?: string | undefined;
|
|
73
73
|
readonly attributes?: Readonly<Record<string, string>> | undefined;
|
|
74
|
+
readonly placeholder?: string | undefined;
|
|
75
|
+
readonly helpText?: string | undefined;
|
|
76
|
+
readonly default?: string | number | boolean | readonly string[] | undefined;
|
|
77
|
+
readonly options?: ReadonlyArray<{
|
|
78
|
+
readonly value: string;
|
|
79
|
+
readonly label: string;
|
|
80
|
+
}> | undefined;
|
|
81
|
+
readonly minLength?: number | undefined;
|
|
82
|
+
readonly pattern?: string | undefined;
|
|
83
|
+
readonly min?: number | string | undefined;
|
|
84
|
+
readonly max?: number | string | undefined;
|
|
85
|
+
readonly step?: number | undefined;
|
|
86
|
+
readonly accept?: readonly string[] | undefined;
|
|
87
|
+
readonly maxFileSize?: number | undefined;
|
|
88
|
+
readonly checkboxValue?: string | undefined;
|
|
74
89
|
}
|
|
75
90
|
export interface AdminFormNotifyConfig {
|
|
76
91
|
readonly enabled: boolean;
|
|
@@ -150,5 +165,10 @@ export interface AdminFormsPort {
|
|
|
150
165
|
formId: string;
|
|
151
166
|
submissionId: string;
|
|
152
167
|
}, optionalArgs?: Record<string, never>): Promise<void>;
|
|
168
|
+
/** Download URL for a file answer's stored media. Optional: without it the view shows the
|
|
169
|
+
* filename as plain text. */
|
|
170
|
+
submissionFileUrl?(requiredArgs: {
|
|
171
|
+
mediaId: string;
|
|
172
|
+
}, optionalArgs?: Record<string, never>): string;
|
|
153
173
|
}
|
|
154
174
|
//# sourceMappingURL=forms.d.ts.map
|
package/dist/core/ports/forms.js
CHANGED
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
*
|
|
55
55
|
* `FormFieldType` and `FormDefinitionStatus` are reference-implementation-specific vocabularies — OPEN, via the
|
|
56
56
|
* `T | (string & {})` idiom `seo.ts`'s file header introduces for this port set. `FormFieldType` in
|
|
57
|
-
* particular
|
|
58
|
-
*
|
|
59
|
-
* contract standing in the way.
|
|
57
|
+
* particular names only the original four kinds; the reference implementation's field registry
|
|
58
|
+
* (`@jini-ai/cms/forms` `FIELD_TYPE_REGISTRY`) now owns the full vocabulary, and a host offering a
|
|
59
|
+
* richer form builder needs room to add its own without this contract standing in the way.
|
|
60
60
|
*/
|
|
61
61
|
export {};
|
|
62
62
|
//# sourceMappingURL=forms.js.map
|
package/dist/core/ports/seo.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export interface AdminSeoOpenGraph {
|
|
|
74
74
|
readonly url: string;
|
|
75
75
|
/** Already resolved to a renderable URL — never a media ref. */
|
|
76
76
|
readonly image?: string;
|
|
77
|
+
/** Source reference before resizing, used to diagnose the selected share image. */
|
|
78
|
+
readonly imageRef?: string;
|
|
77
79
|
readonly siteName?: string;
|
|
78
80
|
}
|
|
79
81
|
export interface AdminSeoTwitterCard {
|
|
@@ -81,6 +83,8 @@ export interface AdminSeoTwitterCard {
|
|
|
81
83
|
readonly title: string;
|
|
82
84
|
readonly description?: string;
|
|
83
85
|
readonly image?: string;
|
|
86
|
+
/** Source reference before resizing, used to diagnose the selected share image. */
|
|
87
|
+
readonly imageRef?: string;
|
|
84
88
|
readonly site?: string;
|
|
85
89
|
}
|
|
86
90
|
export interface AdminSeoRobotsDirective {
|
|
@@ -172,6 +176,19 @@ export interface AdminSeoPort {
|
|
|
172
176
|
}): Promise<{
|
|
173
177
|
text: string;
|
|
174
178
|
}>;
|
|
179
|
+
/** Optional host-bound probe: byte size is unknown when Content-Length is unavailable. */
|
|
180
|
+
inspectShareImage?(required: {
|
|
181
|
+
url: string;
|
|
182
|
+
}, optional?: {
|
|
183
|
+
signal?: AbortSignal;
|
|
184
|
+
}): Promise<{
|
|
185
|
+
status: number;
|
|
186
|
+
byteSize: number | null;
|
|
187
|
+
}>;
|
|
188
|
+
/** Resolve local rendered share URLs back to their original source for image diagnostics. */
|
|
189
|
+
mediaPreviewUrl?(required: {
|
|
190
|
+
value: string;
|
|
191
|
+
}, optional?: Record<string, never>): string | null;
|
|
175
192
|
/** Original thumbnail URL; media-reference resolution stays on the server. */
|
|
176
193
|
mediaOriginalUrl(requiredArgs: {
|
|
177
194
|
id: string;
|
|
@@ -13,11 +13,14 @@ export interface FormsTransportPort {
|
|
|
13
13
|
path: string;
|
|
14
14
|
}, optional?: Record<string, never>): string;
|
|
15
15
|
}
|
|
16
|
-
/** Preserve the existing forms wire, including its data envelopes and explicit authoring route.
|
|
16
|
+
/** Preserve the existing forms wire, including its data envelopes and explicit authoring route.
|
|
17
|
+
* `mediaBasePath` (the host's media collection path) enables file-answer download links. */
|
|
17
18
|
export declare function createHttpFormsApi({ transport, basePath }: {
|
|
18
19
|
transport: FormsTransportPort;
|
|
19
20
|
basePath: string;
|
|
20
|
-
},
|
|
21
|
+
}, { mediaBasePath }?: {
|
|
22
|
+
mediaBasePath?: string;
|
|
23
|
+
}): AdminFormsPort;
|
|
21
24
|
/** The same generic POST used by both host screens; no form lifecycle is reimplemented here. */
|
|
22
25
|
export declare function createHttpFormsTrash({ transport, basePath }: {
|
|
23
26
|
transport: FormsTransportPort;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
/** Preserve the existing forms wire, including its data envelopes and explicit authoring route.
|
|
2
|
-
|
|
1
|
+
/** Preserve the existing forms wire, including its data envelopes and explicit authoring route.
|
|
2
|
+
* `mediaBasePath` (the host's media collection path) enables file-answer download links. */
|
|
3
|
+
export function createHttpFormsApi({ transport, basePath }, { mediaBasePath } = {}) {
|
|
3
4
|
const base = basePath.replace(/\/$/, '');
|
|
4
5
|
const formPath = (id) => `${base}/${encodeURIComponent(id)}`;
|
|
5
6
|
const submissionPath = (formId, submissionId) => `${formPath(formId)}/submissions${submissionId === undefined ? '' : `/${encodeURIComponent(submissionId)}`}`;
|
|
7
|
+
const mediaBase = mediaBasePath?.replace(/\/$/, '');
|
|
6
8
|
return {
|
|
9
|
+
...(mediaBase === undefined ? {} : {
|
|
10
|
+
submissionFileUrl: ({ mediaId }, _optional = {}) => transport.url({ path: `${mediaBase}/${encodeURIComponent(mediaId)}/original` }),
|
|
11
|
+
}),
|
|
7
12
|
async listFormDefinitions(_required, _optional = {}) {
|
|
8
13
|
return (await transport.request({ path: base, method: 'GET' })).data;
|
|
9
14
|
},
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { AdminFormField } from "../models.js";
|
|
2
2
|
import { type AttrRow } from "../rules.js";
|
|
3
|
+
import { type FieldSettingsForm, type OptionRow } from "../field-settings.js";
|
|
3
4
|
export interface FieldAttributesDialogState {
|
|
4
5
|
className: string;
|
|
5
6
|
rows: AttrRow[];
|
|
7
|
+
/** The type's builder settings (field-settings.ts), edited as text until Save. */
|
|
8
|
+
settings: FieldSettingsForm;
|
|
6
9
|
error: string | null;
|
|
7
10
|
}
|
|
8
11
|
export declare function initialFieldAttributesDialogState({ field }: {
|
|
@@ -12,6 +15,31 @@ export declare function initialFieldAttributesDialogState({ field }: {
|
|
|
12
15
|
export declare function createFieldAttributesDialogController({ field }: {
|
|
13
16
|
field: AdminFormField;
|
|
14
17
|
}, _optional?: {}): {
|
|
18
|
+
setSettings({ patch }: {
|
|
19
|
+
patch: Partial<FieldSettingsForm>;
|
|
20
|
+
}, _options?: {}): void;
|
|
21
|
+
/** `single` replaces the selection: a dropdown or radio group has one default. */
|
|
22
|
+
toggleDefaultChoice({ value, on, single }: {
|
|
23
|
+
value: string;
|
|
24
|
+
on: boolean;
|
|
25
|
+
single: boolean;
|
|
26
|
+
}, _options?: {}): void;
|
|
27
|
+
toggleAccept({ value, on }: {
|
|
28
|
+
value: string;
|
|
29
|
+
on: boolean;
|
|
30
|
+
}, _options?: {}): void;
|
|
31
|
+
updateOption({ rowId, patch }: {
|
|
32
|
+
rowId: number;
|
|
33
|
+
patch: Partial<Pick<OptionRow, "value" | "label">>;
|
|
34
|
+
}, _options?: {}): void;
|
|
35
|
+
addOption(_required: Record<string, never>, _options?: {}): void;
|
|
36
|
+
removeOption({ rowId }: {
|
|
37
|
+
rowId: number;
|
|
38
|
+
}, _options?: {}): void;
|
|
39
|
+
moveOption({ rowId, offset }: {
|
|
40
|
+
rowId: number;
|
|
41
|
+
offset: -1 | 1;
|
|
42
|
+
}, _options?: {}): void;
|
|
15
43
|
updateRow({ rowId, patch }: {
|
|
16
44
|
rowId: number;
|
|
17
45
|
patch: Partial<AttrRow>;
|
|
@@ -20,7 +48,37 @@ export declare function createFieldAttributesDialogController({ field }: {
|
|
|
20
48
|
rowId: number;
|
|
21
49
|
}, _options?: {}): void;
|
|
22
50
|
addRow(_required: Record<string, never>, _options?: {}): void;
|
|
23
|
-
|
|
51
|
+
/** Settings are checked first: they sit above the attribute rows in the dialog. */
|
|
52
|
+
submit(_required: Record<string, never>, _options?: {}): {
|
|
53
|
+
ok: false;
|
|
54
|
+
error: string;
|
|
55
|
+
} | {
|
|
56
|
+
ok: true;
|
|
57
|
+
patch: {
|
|
58
|
+
id?: string;
|
|
59
|
+
label?: string;
|
|
60
|
+
type?: import("../../core/index.js").FormFieldType;
|
|
61
|
+
required?: boolean;
|
|
62
|
+
maxLength?: number | null;
|
|
63
|
+
className?: string | undefined;
|
|
64
|
+
attributes?: (Readonly<Record<string, string>> & Record<string, string>) | undefined;
|
|
65
|
+
placeholder?: string | undefined;
|
|
66
|
+
helpText?: string | undefined;
|
|
67
|
+
default?: string | number | boolean | readonly string[] | undefined;
|
|
68
|
+
options?: ReadonlyArray<{
|
|
69
|
+
readonly value: string;
|
|
70
|
+
readonly label: string;
|
|
71
|
+
}> | undefined;
|
|
72
|
+
minLength?: number | undefined;
|
|
73
|
+
pattern?: string | undefined;
|
|
74
|
+
min?: number | string | undefined;
|
|
75
|
+
max?: number | string | undefined;
|
|
76
|
+
step?: number | undefined;
|
|
77
|
+
accept?: readonly string[] | undefined;
|
|
78
|
+
maxFileSize?: number | undefined;
|
|
79
|
+
checkboxValue?: string | undefined;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
24
82
|
getSnapshot: () => Readonly<FieldAttributesDialogState>;
|
|
25
83
|
subscribe: ({ listener }: {
|
|
26
84
|
listener: () => void;
|
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { createFormsDraftStore } from "./draft-store.js";
|
|
2
2
|
import { attrRowsFromField, updateAttrRow, removeAttrRow, addAttrRow, buildFieldAttributesPatch } from "../rules.js";
|
|
3
|
+
import { addOptionRow, buildFieldSettingsPatch, moveOptionRow, removeOptionRow, settingsFormFromField, updateOptionRow } from "../field-settings.js";
|
|
3
4
|
export function initialFieldAttributesDialogState({ field }, _optional = {}) {
|
|
4
|
-
return { className: field.className ?? "", rows: attrRowsFromField({ field }), error: null };
|
|
5
|
+
return { className: field.className ?? "", rows: attrRowsFromField({ field }), settings: settingsFormFromField({ field }), error: null };
|
|
5
6
|
}
|
|
7
|
+
/** Add or remove one value from a list setting (accepted uploads, default choices). */
|
|
8
|
+
const toggled = (values, value, on) => on ? (values.includes(value) ? values : [...values, value]) : values.filter((item) => item !== value);
|
|
6
9
|
/** A headless section store. React owns subscriptions/effects, never this state. */
|
|
7
10
|
export function createFieldAttributesDialogController({ field }, _optional = {}) {
|
|
8
11
|
const store = createFormsDraftStore({ initial: initialFieldAttributesDialogState({ field }) });
|
|
12
|
+
const editSettings = (change) => store.update({ key: 'settings', value: change });
|
|
13
|
+
const editOptions = (change) => editSettings(settings => ({ ...settings, options: change(settings.options) }));
|
|
9
14
|
return {
|
|
10
15
|
...store,
|
|
16
|
+
setSettings({ patch }, _options = {}) {
|
|
17
|
+
editSettings(settings => ({ ...settings, ...patch }));
|
|
18
|
+
},
|
|
19
|
+
/** `single` replaces the selection: a dropdown or radio group has one default. */
|
|
20
|
+
toggleDefaultChoice({ value, on, single }, _options = {}) {
|
|
21
|
+
editSettings(settings => ({ ...settings, defaultChoices: single ? (on && value ? [value] : []) : toggled(settings.defaultChoices, value, on) }));
|
|
22
|
+
},
|
|
23
|
+
toggleAccept({ value, on }, _options = {}) {
|
|
24
|
+
editSettings(settings => ({ ...settings, accept: toggled(settings.accept, value, on) }));
|
|
25
|
+
},
|
|
26
|
+
updateOption({ rowId, patch }, _options = {}) {
|
|
27
|
+
editOptions(rows => updateOptionRow({ rows, rowId, patch }));
|
|
28
|
+
},
|
|
29
|
+
addOption(_required, _options = {}) { editOptions(rows => addOptionRow({ rows })); },
|
|
30
|
+
removeOption({ rowId }, _options = {}) { editOptions(rows => removeOptionRow({ rows, rowId })); },
|
|
31
|
+
moveOption({ rowId, offset }, _options = {}) { editOptions(rows => moveOptionRow({ rows, rowId, offset })); },
|
|
11
32
|
updateRow({ rowId, patch }, _options = {}) {
|
|
12
33
|
store.update({ key: 'rows', value: rows => updateAttrRow({ rows, rowId, patch }) });
|
|
13
34
|
},
|
|
@@ -17,8 +38,12 @@ export function createFieldAttributesDialogController({ field }, _optional = {})
|
|
|
17
38
|
addRow(_required, _options = {}) {
|
|
18
39
|
store.update({ key: 'rows', value: rows => addAttrRow({ rows }) });
|
|
19
40
|
},
|
|
41
|
+
/** Settings are checked first: they sit above the attribute rows in the dialog. */
|
|
20
42
|
submit(_required, _options = {}) {
|
|
21
|
-
const
|
|
43
|
+
const snapshot = store.getSnapshot();
|
|
44
|
+
const settings = buildFieldSettingsPatch({ form: snapshot.settings, field });
|
|
45
|
+
const attributes = buildFieldAttributesPatch(snapshot);
|
|
46
|
+
const result = !settings.ok ? settings : !attributes.ok ? attributes : { ok: true, patch: { ...settings.patch, ...attributes.patch } };
|
|
22
47
|
store.update({ key: 'error', value: result.ok ? null : result.error });
|
|
23
48
|
return result;
|
|
24
49
|
},
|