@jini-ai/admin 0.4.12 → 0.6.10
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 +70 -2
- 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 +76 -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 +23 -14
- 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
|
@@ -15,5 +15,5 @@ export interface FieldSettingsSectionProps {
|
|
|
15
15
|
* placeholder (slider, file…) focused the Help text ⓘ, whose tip opened half behind the dialog
|
|
16
16
|
* (Chrome check, 2026-10-11). Focus must land on an input, never on an ⓘ.
|
|
17
17
|
*/
|
|
18
|
-
export declare function FieldSettingsSection({ field, controller, t }: FieldSettingsSectionProps
|
|
18
|
+
export declare function FieldSettingsSection({ field, controller, t }: FieldSettingsSectionProps): import("react").JSX.Element;
|
|
19
19
|
//# sourceMappingURL=FieldSettingsSection.d.ts.map
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { agentHandle } from '@jini-ai/agentic';
|
|
3
|
-
import { InfoTip } from '@jini-ai/ui/admin-widgets';
|
|
4
|
-
import { MAX_UPLOAD_MB } from '../../field-settings.js';
|
|
5
3
|
import { useFieldSettingsView } from '../hooks/forms-view.hooks.js';
|
|
4
|
+
import { FieldSettingsDefaultRows, FieldSettingsLimitRows, FieldSettingsOptionsRow, FieldSettingsTextRows, FieldSettingsUploadRows } from './FieldSettingsRows.js';
|
|
6
5
|
/**
|
|
7
6
|
* The settings half of the field dialog: one label-beside-control row per setting the field's
|
|
8
7
|
* registry kind supports (`field-settings.ts` decides which). Markup only; every handler comes from
|
|
@@ -13,9 +12,9 @@ import { useFieldSettingsView } from '../hooks/forms-view.hooks.js';
|
|
|
13
12
|
* placeholder (slider, file…) focused the Help text ⓘ, whose tip opened half behind the dialog
|
|
14
13
|
* (Chrome check, 2026-10-11). Focus must land on an input, never on an ⓘ.
|
|
15
14
|
*/
|
|
16
|
-
export function FieldSettingsSection({ field, controller, t }
|
|
15
|
+
export function FieldSettingsSection({ field, controller, t }) {
|
|
17
16
|
const view = useFieldSettingsView({ field, controller, t });
|
|
18
|
-
const { settings
|
|
19
|
-
return (_jsxs("div", { className: "field-settings", ...agentHandle({ handle: 'form-field-settings' }, { role: 'region', label: "This field's type-specific settings" }), children: [
|
|
17
|
+
const { settings } = view;
|
|
18
|
+
return (_jsxs("div", { className: "field-settings", ...agentHandle({ handle: 'form-field-settings' }, { role: 'region', label: "This field's type-specific settings" }), children: [_jsx(FieldSettingsTextRows, { view: view, t: t }), settings.options ? _jsx(FieldSettingsOptionsRow, { view: view, t: t }) : null, _jsx(FieldSettingsDefaultRows, { view: view, t: t }), _jsx(FieldSettingsLimitRows, { view: view, t: t }), settings.upload ? _jsx(FieldSettingsUploadRows, { view: view, t: t }) : null] }));
|
|
20
19
|
}
|
|
21
20
|
//# sourceMappingURL=FieldSettingsSection.js.map
|
|
@@ -21,7 +21,7 @@ export interface FormEditorFieldsBodyProps {
|
|
|
21
21
|
onCopyEmbed: () => void;
|
|
22
22
|
copyFeedback: string | null;
|
|
23
23
|
}
|
|
24
|
-
export declare function FormEditorFieldsBody(props: FormEditorFieldsBodyProps
|
|
24
|
+
export declare function FormEditorFieldsBody(props: FormEditorFieldsBodyProps): import("react").JSX.Element;
|
|
25
25
|
/** The page-header title/description text — split out because `FormEditor`'s isNew-dependent copy
|
|
26
26
|
* (two ternaries, one with a `||` fallback) is otherwise indistinguishable, in the complexity
|
|
27
27
|
* count, from the branches that actually decide what's on screen. */
|
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useFormFieldsBodyView } from "../hooks/forms-view.hooks.js";
|
|
3
3
|
import { agentHandle } from '@jini-ai/agentic';
|
|
4
|
-
import {
|
|
5
|
-
export function FormEditorFieldsBody(props
|
|
4
|
+
import { FormEditorActions, FormEditorModeSwitch } from "./FormEditorFieldsBodyParts.js";
|
|
5
|
+
export function FormEditorFieldsBody(props) {
|
|
6
6
|
const handlers = useFormFieldsBodyView(props);
|
|
7
|
-
const { name,
|
|
7
|
+
const { name, slug, isNew, t } = props;
|
|
8
8
|
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "field-group", children: _jsxs("div", { className: "field-row", children: [_jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: "form-name", children: t("Name") }), _jsx("input", { id: "form-name", value: name, onChange: handlers.onName, ...agentHandle({ handle: "form-editor-name" }, { role: "field", label: "This form's display name" }) })] }), _jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: "form-slug", children: t("Slug") }), _jsx("input", { id: "form-slug", value: slug, disabled: !isNew, onChange: handlers.onSlug, ...agentHandle({ handle: "form-editor-slug" }, {
|
|
9
9
|
role: "field",
|
|
10
10
|
label: "This form's URL slug — only editable while creating a new form",
|
|
11
|
-
}) })] })] }) }),
|
|
12
|
-
// Reversible-but-access-affecting (turns off the live site's ability to accept
|
|
13
|
-
// submissions through this form) — `.btn-warning`, not `.btn-danger`: nothing is
|
|
14
|
-
// deleted, and the same control flips right back to "Enable". Re-enabling is the safe
|
|
15
|
-
// direction, so it stays `.btn-secondary` rather than inheriting the warning look.
|
|
16
|
-
_jsx("button", { type: "button", className: form.status === "active" ? "btn-warning" : "btn-secondary", disabled: saving, onClick: onStatusToggle, ...agentHandle({ handle: "form-editor-status-toggle" }, {
|
|
17
|
-
role: "button",
|
|
18
|
-
label: "Disable or re-enable this form — disabling stops it accepting new submissions without deleting it",
|
|
19
|
-
}), children: form.status === "active" ? t("Disable") : t("Enable") })) : null, _jsx("button", { type: "button", disabled: saving, onClick: onSave, ...agentHandle({ handle: "form-editor-save" }, {
|
|
20
|
-
role: "button",
|
|
21
|
-
label: "Save this form's name, slug, fields and notification settings",
|
|
22
|
-
}), children: isNew ? t("Create form") : t("Save") })] })] }));
|
|
11
|
+
}) })] })] }) }), _jsx("div", { className: "field-group", children: _jsx(FormEditorModeSwitch, { props: props, handlers: handlers }) }), _jsx(FormEditorActions, { props: props })] }));
|
|
23
12
|
}
|
|
24
13
|
//# sourceMappingURL=FormEditorFieldsBody.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { useFormFieldsBodyView } from "../hooks/forms-view.hooks.js";
|
|
2
|
+
import type { FormEditorFieldsBodyProps } from "./FormEditorFieldsBody.js";
|
|
3
|
+
/**
|
|
4
|
+
* @file The two halves of `FormEditorFieldsBody` that branch on its props — the Builder/HTML switch
|
|
5
|
+
* and the action row — split out so each one's branches are counted on their own. Markup only.
|
|
6
|
+
*/
|
|
7
|
+
type FieldsBodyHandlers = ReturnType<typeof useFormFieldsBodyView>;
|
|
8
|
+
/** Builder/HTML switch plus whichever editor the current mode shows. */
|
|
9
|
+
export declare function FormEditorModeSwitch({ props, handlers }: {
|
|
10
|
+
props: FormEditorFieldsBodyProps;
|
|
11
|
+
handlers: FieldsBodyHandlers;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
/** Copy embed, copy feedback, Disable/Enable (saved forms only) and Save/Create. */
|
|
14
|
+
export declare function FormEditorActions({ props }: {
|
|
15
|
+
props: FormEditorFieldsBodyProps;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=FormEditorFieldsBodyParts.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { agentHandle } from '@jini-ai/agentic';
|
|
3
|
+
import { FormFieldsEditor } from "./FormFieldsEditor.js";
|
|
4
|
+
/** Builder/HTML switch plus whichever editor the current mode shows. */
|
|
5
|
+
export function FormEditorModeSwitch({ props, handlers }) {
|
|
6
|
+
const { fields, existingFieldIds, onFieldsChange, t, mode, html } = props;
|
|
7
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "segmented", role: "group", "aria-label": t("Form authoring mode"), children: [_jsx("button", { type: "button", className: mode === "builder" ? "is-active" : "", "aria-pressed": mode === "builder", onClick: handlers.onBuilder, children: t("Builder") }), _jsx("button", { type: "button", className: mode === "html" ? "is-active" : "", "aria-pressed": mode === "html", onClick: handlers.onHtmlMode, children: t("HTML") })] }), mode === "html" ? (_jsx("textarea", { className: "page-html-source", value: html, onChange: handlers.onHtml, spellCheck: false, "aria-label": t("Form HTML") })) : (_jsx("div", { className: "table-scroll", children: _jsx(FormFieldsEditor, { fields: fields, existingFieldIds: existingFieldIds, onChange: onFieldsChange, t: t }) }))] }));
|
|
8
|
+
}
|
|
9
|
+
/** Copy embed, copy feedback, Disable/Enable (saved forms only) and Save/Create. */
|
|
10
|
+
export function FormEditorActions({ props }) {
|
|
11
|
+
const { isNew, form, saving, onStatusToggle, onSave, t, onCopyEmbed, copyFeedback } = props;
|
|
12
|
+
/* `form-actions` is a spacing-only hook layered on top of the shared `.editor-actions`
|
|
13
|
+
flex row. `.editor-actions` sets direction/gap/alignment but no top margin, and this row
|
|
14
|
+
is not a `.field-group`, so the `.field-group + .field-group` rhythm that separates every
|
|
15
|
+
other block on this screen skips it — leaving Disable/Save flush against the Recipients
|
|
16
|
+
input. Fixed here rather than by adding a margin to `.editor-actions` itself, because that
|
|
17
|
+
class is shared with the other editor screens and a global change would shift spacing on
|
|
18
|
+
screens nobody has looked at yet. */
|
|
19
|
+
return (_jsxs("div", { className: "editor-actions form-actions", children: [form ? _jsx("button", { type: "button", className: "btn-secondary", onClick: onCopyEmbed, children: t("Copy HTML embed") }) : null, copyFeedback ? _jsx("span", { role: "status", children: copyFeedback }) : null, !isNew && form ? (
|
|
20
|
+
// Reversible-but-access-affecting (turns off the live site's ability to accept
|
|
21
|
+
// submissions through this form) — `.btn-warning`, not `.btn-danger`: nothing is
|
|
22
|
+
// deleted, and the same control flips right back to "Enable". Re-enabling is the safe
|
|
23
|
+
// direction, so it stays `.btn-secondary` rather than inheriting the warning look.
|
|
24
|
+
_jsx("button", { type: "button", className: form.status === "active" ? "btn-warning" : "btn-secondary", disabled: saving, onClick: onStatusToggle, ...agentHandle({ handle: "form-editor-status-toggle" }, {
|
|
25
|
+
role: "button",
|
|
26
|
+
label: "Disable or re-enable this form — disabling stops it accepting new submissions without deleting it",
|
|
27
|
+
}), children: form.status === "active" ? t("Disable") : t("Enable") })) : null, _jsx("button", { type: "button", disabled: saving, onClick: onSave, ...agentHandle({ handle: "form-editor-save" }, {
|
|
28
|
+
role: "button",
|
|
29
|
+
label: "Save this form's name, slug, fields and notification settings",
|
|
30
|
+
}), children: isNew ? t("Create form") : t("Save") })] }));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=FormEditorFieldsBodyParts.js.map
|
|
@@ -8,5 +8,5 @@ export interface FormSubmissionsProps {
|
|
|
8
8
|
t: (key: string) => string;
|
|
9
9
|
answerColumns?: FormAnswerColumn[];
|
|
10
10
|
}
|
|
11
|
-
export declare function FormSubmissions({ formId, useFormSubmissionsHook, t, answerColumns }: FormSubmissionsProps
|
|
11
|
+
export declare function FormSubmissions({ formId, useFormSubmissionsHook, t, answerColumns }: FormSubmissionsProps): import("react").JSX.Element;
|
|
12
12
|
//# sourceMappingURL=FormSubmissions.d.ts.map
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useSubmissionsView } from "../hooks/forms-view.hooks.js";
|
|
3
|
-
import { DataTable } from '../../../react/components/DataTable.js';
|
|
4
|
-
import { agentHandle } from '@jini-ai/agentic';
|
|
5
3
|
import { useWiredFormSubmissions } from '../hooks/use-form-submissions.hooks.js';
|
|
6
4
|
import { useFormSubmissionDate } from '../hooks/use-form-submission-date.hooks.js';
|
|
7
5
|
import { FormSubmissionDetail } from "./FormSubmissionDetail.js";
|
|
8
|
-
import {
|
|
9
|
-
export function FormSubmissions({ formId, useFormSubmissionsHook = useWiredFormSubmissions, t, answerColumns = [] }
|
|
6
|
+
import { FormSubmissionsTable } from "./FormSubmissionsTable.js";
|
|
7
|
+
export function FormSubmissions({ formId, useFormSubmissionsHook = useWiredFormSubmissions, t, answerColumns = [] }) {
|
|
10
8
|
const formatDate = useFormSubmissionDate({});
|
|
11
9
|
const controller = useFormSubmissionsHook({ formId });
|
|
12
10
|
const { submissions, nextCursor, error, selectedId, loadingMore } = controller;
|
|
@@ -27,20 +25,6 @@ export function FormSubmissions({ formId, useFormSubmissionsHook = useWiredFormS
|
|
|
27
25
|
// own post-early-return computation. `submissions` grows only on an explicit "Load more" click
|
|
28
26
|
// (`use-form-submissions.hooks.ts`'s cursor-append), so this O(n) pass tracks real data changes,
|
|
29
27
|
// not incidental re-renders.
|
|
30
|
-
return (
|
|
31
|
-
{ key: "submitted-at", header: t("Submitted at"), cell: (s) => _jsx(FormSubmissionDate, { date: formatDate({ iso: s.submittedAt }) }) },
|
|
32
|
-
{ key: "source-ip", header: t("Source IP"), cell: (s) => s.sourceIp },
|
|
33
|
-
...answerColumns,
|
|
34
|
-
{
|
|
35
|
-
key: "view",
|
|
36
|
-
cell: (s, index) => (_jsx("button", { type: "button", onClick: s.view, ...agentHandle({ handle: s.handle }, {
|
|
37
|
-
role: "button",
|
|
38
|
-
label: "Open this submission's full details",
|
|
39
|
-
}), children: t("View") })),
|
|
40
|
-
},
|
|
41
|
-
] }), nextCursor ? (_jsx("button", { type: "button", className: "btn-secondary", disabled: loadingMore, onClick: loadMore, ...agentHandle({ handle: "form-submissions-load-more" }, {
|
|
42
|
-
role: "button",
|
|
43
|
-
label: "Load the next page of submissions",
|
|
44
|
-
}), children: loadingMore ? t("Loading…") : t("Load more") })) : null] }));
|
|
28
|
+
return (_jsx(FormSubmissionsTable, { rows: rows, loadMore: loadMore, error: error, nextCursor: nextCursor, loadingMore: loadingMore, formatDate: formatDate, answerColumns: answerColumns, t: t }));
|
|
45
29
|
}
|
|
46
30
|
//# sourceMappingURL=FormSubmissions.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FormAnswerColumn } from '../../html-rules.js';
|
|
2
|
+
import type { useSubmissionsView } from "../hooks/forms-view.hooks.js";
|
|
3
|
+
import type { useFormSubmissionDate } from '../hooks/use-form-submission-date.hooks.js';
|
|
4
|
+
/**
|
|
5
|
+
* @file The loaded-list half of `FormSubmissions`: an optional error above the table, the table
|
|
6
|
+
* itself, and "Load more" while a next page exists. Markup only.
|
|
7
|
+
*/
|
|
8
|
+
export interface FormSubmissionsTableProps {
|
|
9
|
+
rows: ReturnType<typeof useSubmissionsView>["rows"];
|
|
10
|
+
loadMore: () => void;
|
|
11
|
+
error: string | null;
|
|
12
|
+
nextCursor: string | null;
|
|
13
|
+
loadingMore: boolean;
|
|
14
|
+
formatDate: ReturnType<typeof useFormSubmissionDate>;
|
|
15
|
+
answerColumns: FormAnswerColumn[];
|
|
16
|
+
t: (key: string) => string;
|
|
17
|
+
}
|
|
18
|
+
export declare function FormSubmissionsTable({ rows, loadMore, error, nextCursor, loadingMore, formatDate, answerColumns, t }: FormSubmissionsTableProps): import("react").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=FormSubmissionsTable.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DataTable } from '../../../react/components/DataTable.js';
|
|
3
|
+
import { agentHandle } from '@jini-ai/agentic';
|
|
4
|
+
import { FormSubmissionDate } from "./FormSubmissionDate.js";
|
|
5
|
+
export function FormSubmissionsTable({ rows, loadMore, error, nextCursor, loadingMore, formatDate, answerColumns, t }) {
|
|
6
|
+
return (_jsxs("div", { children: [error ? _jsx("div", { className: "notice error", children: error }) : null, _jsx(DataTable, { rows: rows, rowKey: (s) => s.id, columns: [
|
|
7
|
+
{ key: "submitted-at", header: t("Submitted at"), cell: (s) => _jsx(FormSubmissionDate, { date: formatDate({ iso: s.submittedAt }) }) },
|
|
8
|
+
{ key: "source-ip", header: t("Source IP"), cell: (s) => s.sourceIp },
|
|
9
|
+
...answerColumns,
|
|
10
|
+
{
|
|
11
|
+
key: "view",
|
|
12
|
+
cell: (s) => (_jsx("button", { type: "button", onClick: s.view, ...agentHandle({ handle: s.handle }, {
|
|
13
|
+
role: "button",
|
|
14
|
+
label: "Open this submission's full details",
|
|
15
|
+
}), children: t("View") })),
|
|
16
|
+
},
|
|
17
|
+
] }), nextCursor ? (_jsx("button", { type: "button", className: "btn-secondary", disabled: loadingMore, onClick: loadMore, ...agentHandle({ handle: "form-submissions-load-more" }, {
|
|
18
|
+
role: "button",
|
|
19
|
+
label: "Load the next page of submissions",
|
|
20
|
+
}), children: loadingMore ? t("Loading…") : t("Load more") })) : null] }));
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=FormSubmissionsTable.js.map
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { queryMedia } from '../rules.js';
|
|
2
|
+
const METADATA_KEYS = ['title', 'slug', 'alt', 'caption', 'credit', 'width', 'height', 'cssClass', 'htmlAttributes'];
|
|
3
|
+
/** Only metadata keys with a defined value; null stays, so a patch can clear a nullable field. */
|
|
4
|
+
function pickMetadata({ patch }) {
|
|
5
|
+
return Object.fromEntries(Object.entries(patch).filter(([key, value]) => METADATA_KEYS.includes(key) && value !== undefined));
|
|
6
|
+
}
|
|
7
|
+
/** Decoded length of a base64 string: 3 bytes per 4 chars, minus trailing padding. */
|
|
8
|
+
function base64ByteSize({ dataBase64 }) {
|
|
9
|
+
const padding = dataBase64.match(/=+$/)?.[0].length ?? 0;
|
|
10
|
+
return Math.max(0, Math.floor(dataBase64.length * 3 / 4) - padding);
|
|
11
|
+
}
|
|
2
12
|
export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().toISOString() } = {}) {
|
|
3
13
|
const items = new Map(assets.map((item) => [item.id, Object.freeze({ ...item })]));
|
|
4
14
|
let sequence = 0;
|
|
@@ -20,7 +30,7 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
|
|
|
20
30
|
// The REAL server ignores the declared string and stores sniffContentType(bytes) instead.
|
|
21
31
|
// This fake has no bytes to sniff — its input is an opaque base64 string a test made up —
|
|
22
32
|
// so it echoes what the caller declared. Tests needing disagreement must seed assets directly.
|
|
23
|
-
return { byteSize:
|
|
33
|
+
return { byteSize: base64ByteSize(input), contentType: input.contentType, sha256: `memory-content-${++sequence}` };
|
|
24
34
|
}
|
|
25
35
|
return {
|
|
26
36
|
replaceSupported: true,
|
|
@@ -38,15 +48,16 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
|
|
|
38
48
|
const timestamp = now();
|
|
39
49
|
return put({
|
|
40
50
|
id,
|
|
41
|
-
title:
|
|
42
|
-
alt:
|
|
43
|
-
caption:
|
|
44
|
-
credit:
|
|
45
|
-
slug:
|
|
46
|
-
width:
|
|
47
|
-
height:
|
|
48
|
-
cssClass:
|
|
49
|
-
htmlAttributes:
|
|
51
|
+
title: input.filename,
|
|
52
|
+
alt: '',
|
|
53
|
+
caption: '',
|
|
54
|
+
credit: '',
|
|
55
|
+
slug: id,
|
|
56
|
+
width: null,
|
|
57
|
+
height: null,
|
|
58
|
+
cssClass: null,
|
|
59
|
+
htmlAttributes: null,
|
|
60
|
+
...pickMetadata({ patch: options }),
|
|
50
61
|
status: 'active',
|
|
51
62
|
publicUrl: null,
|
|
52
63
|
createdAt: timestamp,
|
|
@@ -58,14 +69,13 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
|
|
|
58
69
|
async update({ id, patch }, options = {}) {
|
|
59
70
|
check(options);
|
|
60
71
|
const previous = get(id);
|
|
61
|
-
|
|
62
|
-
return put({ ...previous, ...metadata, version: previous.version + 1, updatedAt: now() });
|
|
72
|
+
return put({ ...previous, ...pickMetadata({ patch }), version: previous.version + 1, updatedAt: now() });
|
|
63
73
|
},
|
|
64
74
|
async replace({ id, upload }, options = {}) {
|
|
65
75
|
check(options);
|
|
66
76
|
const previous = get(id);
|
|
67
77
|
if (previous.status !== 'active')
|
|
68
|
-
throw new Error(
|
|
78
|
+
throw new Error(`Cannot replace trashed media: ${id} (restore it first)`);
|
|
69
79
|
return put({
|
|
70
80
|
...previous,
|
|
71
81
|
...content(upload),
|
|
@@ -92,7 +102,7 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
|
|
|
92
102
|
async delete({ id }, options = {}) {
|
|
93
103
|
check(options);
|
|
94
104
|
if (get(id).status !== 'trashed')
|
|
95
|
-
throw new Error(
|
|
105
|
+
throw new Error(`Trash media before permanent deletion: ${id}`);
|
|
96
106
|
items.delete(id);
|
|
97
107
|
return { purged: true };
|
|
98
108
|
},
|
|
@@ -102,11 +112,11 @@ export function createMemoryMediaApi({ assets = [] }, { now = () => new Date().t
|
|
|
102
112
|
}
|
|
103
113
|
export function createMemoryMediaProviders({ providers = [] }, _optional = {}) {
|
|
104
114
|
const items = new Map(providers.map((item) => [item.id, Object.freeze({ ...item })]));
|
|
105
|
-
function write(id,
|
|
115
|
+
function write(id, patch) {
|
|
106
116
|
const previous = items.get(id);
|
|
107
117
|
if (!previous)
|
|
108
|
-
throw new Error(
|
|
109
|
-
const next = Object.freeze({ ...previous,
|
|
118
|
+
throw new Error(`Unknown media provider: ${id}`);
|
|
119
|
+
const next = Object.freeze({ ...previous, ...patch });
|
|
110
120
|
items.set(id, next);
|
|
111
121
|
return next;
|
|
112
122
|
}
|
|
@@ -117,22 +127,17 @@ export function createMemoryMediaProviders({ providers = [] }, _optional = {}) {
|
|
|
117
127
|
},
|
|
118
128
|
async saveSettings({ id, baseUrl, model }, options = {}) {
|
|
119
129
|
options.signal?.throwIfAborted();
|
|
120
|
-
|
|
121
|
-
if (!previous)
|
|
122
|
-
throw new Error('Unknown provider');
|
|
123
|
-
const next = Object.freeze({ ...previous, baseUrl, model });
|
|
124
|
-
items.set(id, next);
|
|
125
|
-
return next;
|
|
130
|
+
return write(id, { baseUrl, model });
|
|
126
131
|
},
|
|
127
132
|
async saveCredential({ id, credential }, options = {}) {
|
|
128
133
|
options.signal?.throwIfAborted();
|
|
129
134
|
if (!credential.trim())
|
|
130
|
-
throw new Error(
|
|
131
|
-
return write(id, true);
|
|
135
|
+
throw new Error(`Credential is required for media provider: ${id}`);
|
|
136
|
+
return write(id, { configured: true });
|
|
132
137
|
},
|
|
133
138
|
async removeCredential({ id }, options = {}) {
|
|
134
139
|
options.signal?.throwIfAborted();
|
|
135
|
-
return write(id, false);
|
|
140
|
+
return write(id, { configured: false });
|
|
136
141
|
},
|
|
137
142
|
};
|
|
138
143
|
}
|
|
@@ -31,11 +31,12 @@ export function createEditMediaController({ api, item }, _optional = {}) {
|
|
|
31
31
|
store.set({
|
|
32
32
|
patch: { error: error instanceof Error ? error.message : 'Failed to save media metadata' },
|
|
33
33
|
});
|
|
34
|
-
return false;
|
|
35
34
|
}
|
|
36
35
|
finally {
|
|
37
36
|
store.set({ patch: { saving: false } });
|
|
38
37
|
}
|
|
38
|
+
// Reached only through the catch above: the failure is already recorded in `error`.
|
|
39
|
+
return false;
|
|
39
40
|
}
|
|
40
41
|
return {
|
|
41
42
|
getSnapshot: store.getSnapshot,
|
|
@@ -23,6 +23,39 @@ export function createLibraryController({ api }, { query = {} } = {}) {
|
|
|
23
23
|
retryTimer = null;
|
|
24
24
|
}
|
|
25
25
|
let readAbort = null, generation = 0;
|
|
26
|
+
/** Store patch for an accepted full library: filtered rows plus whole-library tab counts.
|
|
27
|
+
* @complexity O(n) in the library size. */
|
|
28
|
+
function libraryView({ media }, _optional = {}) {
|
|
29
|
+
return { items: queryMedia({ media, query: store.getSnapshot().query }), counts: countMediaTabs({ media }) };
|
|
30
|
+
}
|
|
31
|
+
/** Commits a read that is still the latest one; items are frozen so consumers cannot mutate rows. */
|
|
32
|
+
function acceptRead({ all }, _optional = {}) {
|
|
33
|
+
readFailed = false;
|
|
34
|
+
retryAttempt = 0;
|
|
35
|
+
library = all;
|
|
36
|
+
const { items, counts } = libraryView({ media: all });
|
|
37
|
+
store.set({
|
|
38
|
+
patch: {
|
|
39
|
+
items: Object.freeze(items.map((item) => Object.freeze({ ...item }))),
|
|
40
|
+
counts,
|
|
41
|
+
hasUntyped: all.some((item) => item.contentType === null),
|
|
42
|
+
error: null,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Publishes the latest read's failure and, when the failure is transient, schedules a retry. */
|
|
47
|
+
function failRead({ error }, _optional = {}) {
|
|
48
|
+
readFailed = true;
|
|
49
|
+
store.set({
|
|
50
|
+
patch: { loading: false, error: error instanceof Error ? error.message : 'Failed to load media' },
|
|
51
|
+
});
|
|
52
|
+
// Retry reads only, never destructive writes. Keep trying at a capped delay so
|
|
53
|
+
// recovery needs no manual repair, while an offline service isn't hammered.
|
|
54
|
+
if (isRetryableMediaError({ error })) {
|
|
55
|
+
const delay = Math.min(1000 * 2 ** Math.min(retryAttempt++, 5), 30000);
|
|
56
|
+
retryTimer = setTimeout(() => { retryTimer = null; void load(); }, delay);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
26
59
|
async function load(_required = {}, _optional = {}) {
|
|
27
60
|
if (store.signal.aborted)
|
|
28
61
|
return;
|
|
@@ -34,35 +67,13 @@ export function createLibraryController({ api }, { query = {} } = {}) {
|
|
|
34
67
|
store.set({ patch: { loading: true, error: null } });
|
|
35
68
|
try {
|
|
36
69
|
const all = await api.list({}, { signal: abort.signal });
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
readFailed = false;
|
|
40
|
-
retryAttempt = 0;
|
|
41
|
-
library = all;
|
|
42
|
-
store.set({
|
|
43
|
-
patch: {
|
|
44
|
-
items: Object.freeze(items.map((item) => Object.freeze({ ...item }))),
|
|
45
|
-
counts: countMediaTabs({ media: all }),
|
|
46
|
-
hasUntyped: all.some((item) => item.contentType === null),
|
|
47
|
-
error: null,
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
}
|
|
70
|
+
if (ticket === generation)
|
|
71
|
+
acceptRead({ all });
|
|
51
72
|
}
|
|
52
73
|
catch (error) {
|
|
53
74
|
// A later background failure must not blank an already-rendered screen.
|
|
54
|
-
if (!abort.signal.aborted && ticket === generation)
|
|
55
|
-
|
|
56
|
-
store.set({
|
|
57
|
-
patch: { loading: false, error: error instanceof Error ? error.message : 'Failed to load media' },
|
|
58
|
-
});
|
|
59
|
-
// Retry reads only, never destructive writes. Keep trying at a capped delay so
|
|
60
|
-
// recovery needs no manual repair, while an offline service isn't hammered.
|
|
61
|
-
if (isRetryableMediaError({ error })) {
|
|
62
|
-
const delay = Math.min(1000 * 2 ** Math.min(retryAttempt++, 5), 30000);
|
|
63
|
-
retryTimer = setTimeout(() => { retryTimer = null; void load(); }, delay);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
75
|
+
if (!abort.signal.aborted && ticket === generation)
|
|
76
|
+
failRead({ error });
|
|
66
77
|
}
|
|
67
78
|
finally {
|
|
68
79
|
if (ticket === generation)
|
|
@@ -76,7 +87,7 @@ export function createLibraryController({ api }, { query = {} } = {}) {
|
|
|
76
87
|
return;
|
|
77
88
|
if (all) {
|
|
78
89
|
library = all;
|
|
79
|
-
store.set({ patch: {
|
|
90
|
+
store.set({ patch: { ...libraryView({ media: all }), hasUntyped: all.some(item => item.contentType === null), error: null } });
|
|
80
91
|
}
|
|
81
92
|
if (error)
|
|
82
93
|
store.set({ patch: { error: error.message } });
|
|
@@ -100,17 +111,18 @@ export function createLibraryController({ api }, { query = {} } = {}) {
|
|
|
100
111
|
readAbort?.abort();
|
|
101
112
|
cancelRetry();
|
|
102
113
|
store.set({ patch: { loading: false, error: null } });
|
|
114
|
+
// A single exit: a write disposed mid-flight or a failed write reports false.
|
|
115
|
+
let saved = false;
|
|
103
116
|
try {
|
|
104
117
|
await run();
|
|
105
|
-
if (store.signal.aborted)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
if (!store.signal.aborted) {
|
|
119
|
+
await load();
|
|
120
|
+
saved = true;
|
|
121
|
+
}
|
|
109
122
|
}
|
|
110
123
|
catch (error) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return false;
|
|
124
|
+
// The store ignores patches once disposed, so a write failing after dispose publishes nothing.
|
|
125
|
+
store.set({ patch: { loading: false, error: error instanceof Error ? error.message : 'Media write failed' } });
|
|
114
126
|
}
|
|
115
127
|
finally {
|
|
116
128
|
if (id === undefined)
|
|
@@ -118,6 +130,7 @@ export function createLibraryController({ api }, { query = {} } = {}) {
|
|
|
118
130
|
const pendingIds = store.getSnapshot().pendingIds.filter(value => value !== id);
|
|
119
131
|
store.set({ patch: { busy: pendingIds.length > 0, pendingIds } });
|
|
120
132
|
}
|
|
133
|
+
return saved;
|
|
121
134
|
});
|
|
122
135
|
writes = operation.catch(() => undefined);
|
|
123
136
|
return operation;
|
|
@@ -168,7 +181,7 @@ export function createLibraryController({ api }, { query = {} } = {}) {
|
|
|
168
181
|
// A failed follow-up read must not leave a permanently deleted card focusable.
|
|
169
182
|
if (library) {
|
|
170
183
|
library = library.filter(row => row.id !== item.id);
|
|
171
|
-
store.set({ patch:
|
|
184
|
+
store.set({ patch: libraryView({ media: library }) });
|
|
172
185
|
}
|
|
173
186
|
});
|
|
174
187
|
if (saved)
|
|
@@ -46,11 +46,12 @@ export function createProvidersController({ api }, _optional = {}) {
|
|
|
46
46
|
}
|
|
47
47
|
catch {
|
|
48
48
|
store.set({ patch: { error: 'Failed to save provider credentials' } });
|
|
49
|
-
return false;
|
|
50
49
|
}
|
|
51
50
|
finally {
|
|
52
51
|
store.set({ patch: { saving: false } });
|
|
53
52
|
}
|
|
53
|
+
// Reached only through the catch above: the failure is already recorded in `error`.
|
|
54
|
+
return false;
|
|
54
55
|
}
|
|
55
56
|
return {
|
|
56
57
|
getSnapshot: store.getSnapshot,
|
|
@@ -80,7 +80,9 @@ const ASCII_WHITESPACE_OR_CONTROL_MAX_CODE_POINT = 0x20;
|
|
|
80
80
|
function isUnsafeMediaUrlValue(value) {
|
|
81
81
|
let stripped = "";
|
|
82
82
|
for (const char of value) {
|
|
83
|
-
|
|
83
|
+
// `char` is never empty, so `charCodeAt(0)` is always a number; for an astral character it is
|
|
84
|
+
// the high surrogate (>= 0xD800), which is kept exactly as the full code point would be.
|
|
85
|
+
if (char.charCodeAt(0) > ASCII_WHITESPACE_OR_CONTROL_MAX_CODE_POINT)
|
|
84
86
|
stripped += char;
|
|
85
87
|
}
|
|
86
88
|
stripped = stripped.toLowerCase();
|
|
@@ -165,15 +167,11 @@ export function parseMediaHtmlAttributes({ text }, _optional = {}) {
|
|
|
165
167
|
// Non-whitespace text between the previous match and this one is a fragment the token pattern
|
|
166
168
|
// could not parse as a name (e.g. a stray quote) — the tokenizer has lost sync, so everything
|
|
167
169
|
// parsed so far is discarded rather than trusted.
|
|
168
|
-
const skipped = trimmed.slice(consumed, match.index);
|
|
169
|
-
if (skipped
|
|
170
|
-
|
|
171
|
-
errors.push(malformed);
|
|
172
|
-
return { attributes: {}, errors, error: malformed };
|
|
173
|
-
}
|
|
170
|
+
const skipped = trimmed.slice(consumed, match.index).trim();
|
|
171
|
+
if (skipped !== "")
|
|
172
|
+
return malformedMediaHtmlAttributes({ errors, fragment: skipped });
|
|
174
173
|
consumed = match.index + match[0].length;
|
|
175
|
-
const name = match
|
|
176
|
-
const value = match[2] ?? match[3] ?? match[4] ?? "";
|
|
174
|
+
const { name, value } = readMediaHtmlAttributeToken({ match });
|
|
177
175
|
const rejection = classifyMediaHtmlAttributeToken(name, value);
|
|
178
176
|
if (rejection) {
|
|
179
177
|
errors.push(rejection);
|
|
@@ -181,14 +179,30 @@ export function parseMediaHtmlAttributes({ text }, _optional = {}) {
|
|
|
181
179
|
}
|
|
182
180
|
attributes[name] = value;
|
|
183
181
|
}
|
|
184
|
-
const trailing = trimmed.slice(consumed);
|
|
185
|
-
if (trailing
|
|
186
|
-
|
|
187
|
-
errors.push(malformed);
|
|
188
|
-
return { attributes: {}, errors, error: malformed };
|
|
189
|
-
}
|
|
182
|
+
const trailing = trimmed.slice(consumed).trim();
|
|
183
|
+
if (trailing !== "")
|
|
184
|
+
return malformedMediaHtmlAttributes({ errors, fragment: trailing });
|
|
190
185
|
return { attributes, errors, error: errors[0] ?? null };
|
|
191
186
|
}
|
|
187
|
+
/** The lowercased name and the value of one {@link HTML_ATTRIBUTE_TOKEN} match: whichever of the
|
|
188
|
+
* double-quoted, single-quoted or unquoted value groups matched, or `""` for a bare boolean name.
|
|
189
|
+
*
|
|
190
|
+
* @complexity O(n) in the name's length (one lowercase pass).
|
|
191
|
+
*/
|
|
192
|
+
function readMediaHtmlAttributeToken({ match }, _optional = {}) {
|
|
193
|
+
return { name: match[1].toLowerCase(), value: match[2] ?? match[3] ?? match[4] ?? "" };
|
|
194
|
+
}
|
|
195
|
+
/** The fail-closed result once the tokenizer loses sync: `fragment` (already trimmed) is appended
|
|
196
|
+
* to `errors` as the final `malformed` entry, and every accepted attribute is discarded — see
|
|
197
|
+
* {@link parseMediaHtmlAttributes}'s header for why. Mutates `errors` (the caller's own list).
|
|
198
|
+
*
|
|
199
|
+
* @complexity O(1).
|
|
200
|
+
*/
|
|
201
|
+
function malformedMediaHtmlAttributes({ errors, fragment }, _optional = {}) {
|
|
202
|
+
const malformed = { reason: "malformed", attribute: fragment };
|
|
203
|
+
errors.push(malformed);
|
|
204
|
+
return { attributes: {}, errors, error: malformed };
|
|
205
|
+
}
|
|
192
206
|
/** The hint names the rejected token so operators can distinguish a typo from a hard no.
|
|
193
207
|
* This is client feedback only; the server must independently enforce write/render policy. */
|
|
194
208
|
export function describeMediaHtmlAttributeError({ error }, _optional = {}) {
|
package/dist/media/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { mediaApiToken, mediaProvidersToken } from './ports.js';
|
|
|
3
3
|
export type { MediaApiPort, MediaProvidersPort, MediaEventsPort } from './ports.js';
|
|
4
4
|
export type { MediaAsset, MediaQuery, UploadInput, MediaMetadataPatch, MediaRequestOptions, MediaProvider, MediaProviderOption, MediaProviderConfiguration, MediaProviderConfigurationMap, MediaTabId, MediaContentTabId, MediaOrderBy, } from './models.js';
|
|
5
5
|
export { mediaMessagesEn } from './messages.en.js';
|
|
6
|
-
export { filterMediaByTab, resolveActiveTab, queryMedia, diffMediaMetadata, acceptsMedia, safeMediaUrl, canReplaceMedia, canRestoreMedia, isRetryableMediaError, parseOptionalPixelSize, formatByteSize, formatUploadDate, countMediaTabs, mediaEmbedSnippet, mediaRowHandles, } from './rules.js';
|
|
6
|
+
export { filterMediaByTab, resolveActiveTab, queryMedia, diffMediaMetadata, acceptsMedia, safeMediaUrl, canReplaceMedia, canRestoreMedia, isRetryableMediaError, parseOptionalPixelSize, formatByteSize, formatUploadDate, mediaFileName, countMediaTabs, mediaEmbedSnippet, mediaRowHandles, } from './rules.js';
|
|
7
7
|
export { createLibraryController } from './controllers/library.controller.js';
|
|
8
8
|
export type { LibraryState } from './controllers/library.controller.js';
|
|
9
9
|
export { createEditMediaController } from './controllers/edit-media.controller.js';
|
package/dist/media/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { mediaModule } from './media.module.js';
|
|
2
2
|
export { mediaApiToken, mediaProvidersToken } from './ports.js';
|
|
3
3
|
export { mediaMessagesEn } from './messages.en.js';
|
|
4
|
-
export { filterMediaByTab, resolveActiveTab, queryMedia, diffMediaMetadata, acceptsMedia, safeMediaUrl, canReplaceMedia, canRestoreMedia, isRetryableMediaError, parseOptionalPixelSize, formatByteSize, formatUploadDate, countMediaTabs, mediaEmbedSnippet, mediaRowHandles, } from './rules.js';
|
|
4
|
+
export { filterMediaByTab, resolveActiveTab, queryMedia, diffMediaMetadata, acceptsMedia, safeMediaUrl, canReplaceMedia, canRestoreMedia, isRetryableMediaError, parseOptionalPixelSize, formatByteSize, formatUploadDate, mediaFileName, countMediaTabs, mediaEmbedSnippet, mediaRowHandles, } from './rules.js';
|
|
5
5
|
export { createLibraryController } from './controllers/library.controller.js';
|
|
6
6
|
export { createEditMediaController } from './controllers/edit-media.controller.js';
|
|
7
7
|
export { createProvidersController } from './controllers/providers.controller.js';
|