@jini-ai/admin 0.4.10 → 0.4.11
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 +5 -0
- package/dist/comments/rules.d.ts +3 -3
- package/dist/comments/rules.js +3 -3
- package/dist/forms/field-settings.d.ts +1 -0
- package/dist/forms/field-settings.js +4 -4
- package/dist/forms/react/components/FieldAttributesDialog.js +1 -1
- package/dist/forms/react/hooks/use-forms-list.hooks.d.ts +1 -1
- package/dist/forms/rules.d.ts +2 -2
- package/dist/forms/rules.js +2 -2
- package/dist/redirects/react/hooks/RedirectsPage.hooks.js +1 -1
- package/dist/redirects/react/pages/RedirectsPage.js +2 -1
- package/dist/redirects/rules.d.ts +2 -2
- package/dist/redirects/rules.js +2 -2
- package/dist/seo/adapters/memory.js +1 -1
- package/dist/seo/react/components/SitemapModal.js +1 -1
- package/dist/seo/react/pages/SeoPage.d.ts +1 -1
- package/dist/seo/rules.d.ts +4 -4
- package/dist/seo/rules.js +8 -8
- package/package.json +12 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.11 — 2026-10-14
|
|
4
|
+
|
|
5
|
+
- `FieldAttributesDialog` and `SitemapModal` use the neutral `jini-domain-dialog` class instead of a host-named one; hosts style `.jini-domain-dialog` (47b9fa4f).
|
|
6
|
+
- Rides along, no API change: forms option row keys come from the editor's own row list instead of a module-level counter (6281ddc8); redirects imports its shared widgets directly instead of the React barrel (9b45c742); comment scrubs (8e508fb6, 6281ddc8).
|
|
7
|
+
|
|
3
8
|
## 0.4.10 — 2026-10-13
|
|
4
9
|
|
|
5
10
|
- Forms HTML starter: URL fields emit `type="text"` plus the registry's URL hints (`inputmode`, `autocomplete`, `spellcheck`, `autocapitalize`), so bare domains such as `tovu.dev` are no longer refused by the browser. CMS peer floor ^0.5.11.
|
package/dist/comments/rules.d.ts
CHANGED
|
@@ -31,11 +31,11 @@ export declare const KEYS: {
|
|
|
31
31
|
/** The moderation queue's name on `lib/content-refresh-bus.ts` — see `taxonomy/rules.ts`'s
|
|
32
32
|
* `TAXONOMY_RESOURCE` for why this is a plain colocated constant rather than a shared registry.
|
|
33
33
|
* Agent-writable via `comments_approve_comment`/`comments_mark_comment_spam`/
|
|
34
|
-
* `comments_trash_comment`/`comments_restore_comment` (
|
|
35
|
-
*
|
|
34
|
+
* `comments_trash_comment`/`comments_restore_comment` (the host's comment agent tools),
|
|
35
|
+
* all of which move a comment between statuses this queue lists.
|
|
36
36
|
*
|
|
37
37
|
* There is deliberately NO sibling `COMMENTS_SETTINGS_RESOURCE`: `react/hooks/use-comment-settings.hooks.ts`'s
|
|
38
|
-
* own header records a real, already-fixed lost-update bug (
|
|
38
|
+
* own header records a real, already-fixed lost-update bug (2026-08-12) whose fix is a
|
|
39
39
|
* ONE-SHOT seed guard that refuses to re-seed `settings` from any later successful load, including
|
|
40
40
|
* this bus's own out-of-band notification. Wiring that hook to the bus would either do nothing
|
|
41
41
|
* (the guard blocks it) or, if the guard were bypassed instead, reintroduce the exact silent
|
package/dist/comments/rules.js
CHANGED
|
@@ -35,11 +35,11 @@ export const KEYS = {
|
|
|
35
35
|
/** The moderation queue's name on `lib/content-refresh-bus.ts` — see `taxonomy/rules.ts`'s
|
|
36
36
|
* `TAXONOMY_RESOURCE` for why this is a plain colocated constant rather than a shared registry.
|
|
37
37
|
* Agent-writable via `comments_approve_comment`/`comments_mark_comment_spam`/
|
|
38
|
-
* `comments_trash_comment`/`comments_restore_comment` (
|
|
39
|
-
*
|
|
38
|
+
* `comments_trash_comment`/`comments_restore_comment` (the host's comment agent tools),
|
|
39
|
+
* all of which move a comment between statuses this queue lists.
|
|
40
40
|
*
|
|
41
41
|
* There is deliberately NO sibling `COMMENTS_SETTINGS_RESOURCE`: `react/hooks/use-comment-settings.hooks.ts`'s
|
|
42
|
-
* own header records a real, already-fixed lost-update bug (
|
|
42
|
+
* own header records a real, already-fixed lost-update bug (2026-08-12) whose fix is a
|
|
43
43
|
* ONE-SHOT seed guard that refuses to re-seed `settings` from any later successful load, including
|
|
44
44
|
* this bus's own out-of-band notification. Wiring that hook to the bus would either do nothing
|
|
45
45
|
* (the guard blocks it) or, if the guard were bypassed instead, reintroduce the exact silent
|
|
@@ -65,6 +65,7 @@ export declare function retypeField({ field, type }: {
|
|
|
65
65
|
field: AdminFormField;
|
|
66
66
|
type: AdminFormField["type"];
|
|
67
67
|
}, _optional?: Record<string, never>): AdminFormField;
|
|
68
|
+
/** Local React keys for option rows; identity is scoped to the editor's own row list. */
|
|
68
69
|
export interface OptionRow {
|
|
69
70
|
_rowId: number;
|
|
70
71
|
value: string;
|
|
@@ -134,8 +134,6 @@ export function retypeField({ field, type }, _optional = {}) {
|
|
|
134
134
|
next.default = field.default;
|
|
135
135
|
return next;
|
|
136
136
|
}
|
|
137
|
-
/** Local React keys for option rows; same module-counter pattern as `rules.ts`'s attribute rows. */
|
|
138
|
-
let nextOptionRowId = 0;
|
|
139
137
|
const text = (value) => (value === undefined || value === null ? "" : String(value));
|
|
140
138
|
/** Seed the draft from a stored field. @complexity O(o) in options. */
|
|
141
139
|
export function settingsFormFromField({ field }, _optional = {}) {
|
|
@@ -147,7 +145,7 @@ export function settingsFormFromField({ field }, _optional = {}) {
|
|
|
147
145
|
defaultChoices: Array.isArray(defaults) ? [...defaults] : typeof defaults === "string" && fieldSettingsFor({ type: field.type }).options ? [defaults] : [],
|
|
148
146
|
minLength: text(field.minLength), maxLength: text(field.maxLength), pattern: text(field.pattern),
|
|
149
147
|
min: text(field.min), max: text(field.max), step: text(field.step),
|
|
150
|
-
options: (field.options ?? []).map((option) => ({ _rowId:
|
|
148
|
+
options: (field.options ?? []).map((option, index) => ({ _rowId: index, value: option.value, label: option.label })),
|
|
151
149
|
accept: [...(field.accept ?? [])],
|
|
152
150
|
maxFileSizeMb: field.maxFileSize === undefined ? "" : String(Math.round((field.maxFileSize / 1024 / 1024) * 100) / 100),
|
|
153
151
|
};
|
|
@@ -167,7 +165,9 @@ export function updateOptionRow({ rows, rowId, patch }, _optional = {}) {
|
|
|
167
165
|
}
|
|
168
166
|
/** @complexity O(o). */
|
|
169
167
|
export function addOptionRow({ rows }, _optional = {}) {
|
|
170
|
-
|
|
168
|
+
// Use the highest current key so deleting or reordering rows cannot create a duplicate.
|
|
169
|
+
const rowId = rows.reduce((max, row) => Math.max(max, row._rowId), -1) + 1;
|
|
170
|
+
return [...rows, { _rowId: rowId, value: "", label: "" }];
|
|
171
171
|
}
|
|
172
172
|
/** @complexity O(o). */
|
|
173
173
|
export function removeOptionRow({ rows, rowId }, _optional = {}) {
|
|
@@ -19,7 +19,7 @@ export function FieldAttributesDialog({ field, fieldIndex, onSave, onCancel, use
|
|
|
19
19
|
// it was not judged worth the added indirection.
|
|
20
20
|
const { className, addRow, error, submit } = controller;
|
|
21
21
|
const { rows, displayName, onClose, onClassName, hasStyling } = useFieldAttributesView({ field, fieldIndex, controller, onCancel });
|
|
22
|
-
return (_jsx(Dialog, { open: true, title: `${t("Field settings —")} ${displayName}`, onClose: onClose, className: "settings-dialog
|
|
22
|
+
return (_jsx(Dialog, { open: true, title: `${t("Field settings —")} ${displayName}`, onClose: onClose, className: "settings-dialog jini-domain-dialog field-attrs-dialog", children: _jsxs("form", { onSubmit: submit, ...agentHandle({ handle: "form-field-attrs-dialog" }, {
|
|
23
23
|
role: "region",
|
|
24
24
|
label: "Field settings dialog — type-specific settings, CSS classes and HTML attributes for one form field",
|
|
25
25
|
}), children: [_jsx(FieldSettingsSection, { field: field, controller: controller, t: t }), _jsxs("details", { className: "field-settings-advanced", open: hasStyling, children: [_jsx("summary", { children: t("CSS classes and HTML attributes") }), _jsxs("div", { className: "field", children: [_jsxs("span", { className: "field-settings-label", children: [_jsx("label", { className: "field-label", htmlFor: "field-attrs-classname", children: t("CSS classes") }), _jsx(InfoTip, { label: t("Classes are unrestricted, e.g. Tailwind md:col-span-2. Attribute names must be aria-*, data-* or a safe layout attribute; event handlers like onclick are rejected.") })] }), _jsx("input", { id: "field-attrs-classname", value: className, placeholder: "e.g. md:col-span-2 w-1/2 focus:ring-2", onChange: onClassName, ...agentHandle({ handle: "form-field-attrs-classname" }, {
|
|
@@ -37,7 +37,7 @@ import { type FormDateLine } from "../../rules.js";
|
|
|
37
37
|
*
|
|
38
38
|
* the scoped formsEvents subscription (staleness-bug generalization pass, see that hook's own header):
|
|
39
39
|
* `forms_create_definition`/`forms_update_definition`/`forms_set_definition_status`
|
|
40
|
-
* (
|
|
40
|
+
* (the host's forms agent tools) are agent-callable, so this list re-invalidates
|
|
41
41
|
* `KEYS.list` on an out-of-band content-refresh notification the same way `use-taxonomy.hooks.ts`
|
|
42
42
|
* does for its own resource.
|
|
43
43
|
*/
|
package/dist/forms/rules.d.ts
CHANGED
|
@@ -31,8 +31,8 @@ import { type FieldType } from "@jini-ai/cms/forms";
|
|
|
31
31
|
/**
|
|
32
32
|
* This screen's name on `lib/content-refresh-bus.ts` — see `taxonomy/rules.ts`'s `TAXONOMY_RESOURCE`
|
|
33
33
|
* for why this is a plain colocated constant rather than a shared registry. `forms_create_definition`/
|
|
34
|
-
* `forms_update_definition`/`forms_set_definition_status` (
|
|
35
|
-
*
|
|
34
|
+
* `forms_update_definition`/`forms_set_definition_status` (the host's forms agent tools)
|
|
35
|
+
* are agent-callable, so `FormsList.tsx` needs the same "an assistant write shows up
|
|
36
36
|
* without a reload" fix `use-taxonomy.hooks.ts` shipped first.
|
|
37
37
|
*/
|
|
38
38
|
export declare const FORMS_LIST_RESOURCE = "forms";
|
package/dist/forms/rules.js
CHANGED
|
@@ -30,8 +30,8 @@ import { FIELD_TYPE_REGISTRY } from "@jini-ai/cms/forms";
|
|
|
30
30
|
/**
|
|
31
31
|
* This screen's name on `lib/content-refresh-bus.ts` — see `taxonomy/rules.ts`'s `TAXONOMY_RESOURCE`
|
|
32
32
|
* for why this is a plain colocated constant rather than a shared registry. `forms_create_definition`/
|
|
33
|
-
* `forms_update_definition`/`forms_set_definition_status` (
|
|
34
|
-
*
|
|
33
|
+
* `forms_update_definition`/`forms_set_definition_status` (the host's forms agent tools)
|
|
34
|
+
* are agent-callable, so `FormsList.tsx` needs the same "an assistant write shows up
|
|
35
35
|
* without a reload" fix `use-taxonomy.hooks.ts` shipped first.
|
|
36
36
|
*/
|
|
37
37
|
export const FORMS_LIST_RESOURCE = "forms";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createElement } from 'react';
|
|
2
|
-
import { RowMenu } from '../../../react/
|
|
2
|
+
import { RowMenu } from '../../../react/components/RowMenu/RowMenu.js';
|
|
3
3
|
import { redirectRowMenuItems } from '../../rules.js';
|
|
4
4
|
import { HitCountCell } from '../components/HitCountCell.js';
|
|
5
5
|
import { useRedirectsOptions } from './RedirectsOptions.hooks.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { DataTable
|
|
2
|
+
import { DataTable } from '../../../react/components/DataTable.js';
|
|
3
|
+
import { ConfirmDialog } from '../../../react/components/ConfirmDialog/ConfirmDialog.js';
|
|
3
4
|
import { agentHandle } from '@jini-ai/agentic';
|
|
4
5
|
import { describeApiError } from '../../../core/transport/errors.js';
|
|
5
6
|
import { ImportRedirectsForm } from '../components/ImportRedirectsForm.js';
|
|
@@ -22,8 +22,8 @@ export declare const KEYS: {
|
|
|
22
22
|
};
|
|
23
23
|
/** This screen's name on the host refresh bus — see `taxonomy/rules.ts`'s `TAXONOMY_RESOURCE`
|
|
24
24
|
* for why this is a plain colocated constant rather than a shared registry. Agent-writable via
|
|
25
|
-
* `redirects_create`/`redirects_update`/`redirects_tombstone` (
|
|
26
|
-
*
|
|
25
|
+
* `redirects_create`/`redirects_update`/`redirects_tombstone` (the host's redirect agent tools) —
|
|
26
|
+
* `redirects_import` is excluded from that catalog entirely, but the three wired
|
|
27
27
|
* single-row writes are enough to make this list go stale the same way Posts/Media did. */
|
|
28
28
|
export declare const REDIRECTS_RESOURCE = "redirects";
|
|
29
29
|
/**
|
package/dist/redirects/rules.js
CHANGED
|
@@ -18,8 +18,8 @@ export const KEYS = {
|
|
|
18
18
|
};
|
|
19
19
|
/** This screen's name on the host refresh bus — see `taxonomy/rules.ts`'s `TAXONOMY_RESOURCE`
|
|
20
20
|
* for why this is a plain colocated constant rather than a shared registry. Agent-writable via
|
|
21
|
-
* `redirects_create`/`redirects_update`/`redirects_tombstone` (
|
|
22
|
-
*
|
|
21
|
+
* `redirects_create`/`redirects_update`/`redirects_tombstone` (the host's redirect agent tools) —
|
|
22
|
+
* `redirects_import` is excluded from that catalog entirely, but the three wired
|
|
23
23
|
* single-row writes are enough to make this list go stale the same way Posts/Media did. */
|
|
24
24
|
export const REDIRECTS_RESOURCE = "redirects";
|
|
25
25
|
/**
|
|
@@ -49,7 +49,7 @@ function applySeoPatch(meta, patch) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
/** One of the three site-wide scalars the server registers as `nullable: true` (`SEO_DEFINITIONS`,
|
|
52
|
-
* `
|
|
52
|
+
* `features/seo/settings.ts`), under the real write's own rule: an omitted key
|
|
53
53
|
* (`undefined`) keeps the current value, `null` clears it, and a string overwrites. */
|
|
54
54
|
function settingOrCleared(patchValue, currentValue) {
|
|
55
55
|
if (patchValue === undefined)
|
|
@@ -64,7 +64,7 @@ export function SitemapModal({ locale, sitemapEnabled, regenerating, onRegenerat
|
|
|
64
64
|
const { title } = useSitemapPresentation({ locale, modal });
|
|
65
65
|
const { handleRegenerate } = useSitemapModalRegenerate({ onRegenerate });
|
|
66
66
|
// Native modality keeps Tab from reaching the background, as MediaPickerDialog now does too.
|
|
67
|
-
return (_jsxs(Dialog, { open: true, title: title, onClose: () => onClose(), className: "settings-dialog
|
|
67
|
+
return (_jsxs(Dialog, { open: true, title: title, onClose: () => onClose(), className: "settings-dialog jini-domain-dialog sitemap-modal", children: [_jsx("div", { className: "sitemap-modal-header", children: _jsx(SitemapModalHeaderActions, { locale: locale, modal: modal }) }), _jsx("div", { className: "sitemap-modal-body", children: _jsx(SitemapModalBody, { locale: locale, sitemapEnabled: sitemapEnabled, modal: modal }) }), _jsx("div", { className: "widget-picker-footer", children: _jsxs("span", { className: "editor-actions", children: [_jsx("button", { type: "button", className: "btn-secondary", onClick: handleRegenerate, disabled: regenerating, ...agentHandle({ handle: "seo-sitemap-modal-regenerate" }, {
|
|
68
68
|
role: "button",
|
|
69
69
|
label: "Rebuild the cached sitemap and refresh this view",
|
|
70
70
|
}), children: actionLabel({ pending: regenerating, pendingLabel: t({ locale: locale, key: "Working…" }), idleLabel: t({ locale: locale, key: "Regenerate sitemap" }) }) }), _jsx("button", { type: "button", className: "btn-secondary", onClick: onClose, "data-jini-autofocus": "", ...agentHandle({ handle: "seo-sitemap-modal-close" }, { role: "button", label: "Close the sitemap viewer" }), children: t({ locale: locale, key: "Close" }) })] }) })] }));
|
|
@@ -20,7 +20,7 @@ import { useWiredSeo } from "../hooks/use-seo.hooks.js";
|
|
|
20
20
|
* still a minimal textarea-per-rule form (unchanged from the original disclosed scope note).
|
|
21
21
|
*
|
|
22
22
|
* Copy comes from the module's injected t function. Legacy locale props remain render-contract
|
|
23
|
-
* metadata only;
|
|
23
|
+
* metadata only; the host retains its dictionaries and passes the bound translator through the module.
|
|
24
24
|
* Host publish behavior is injected as headerActions, and media selection uses its existing slot
|
|
25
25
|
* or the shared mediaPickerToken service. The original rationale is archived in SOURCE-RATIONALE.
|
|
26
26
|
*
|
package/dist/seo/rules.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare function sortIssuesBySeverity({ issues }: {
|
|
|
14
14
|
/**
|
|
15
15
|
* One edited per-entry override field's outgoing value. An emptied text/URL box means "remove this
|
|
16
16
|
* override so the entry falls back to the site default", which on the wire is `null` — the clear
|
|
17
|
-
* sentinel `SeoExtFieldsPatch` documents (`
|
|
17
|
+
* sentinel `SeoExtFieldsPatch` documents (`features/seo/types.ts`) and
|
|
18
18
|
* `setEntrySeoOverrides` implements by `delete`-ing the key.
|
|
19
19
|
*
|
|
20
20
|
* It is NOT `""`. `write-service.ts` stores `""` as a genuine override and `seo.ts` resolves
|
|
@@ -47,7 +47,7 @@ export declare function actionLabel({ pending, pendingLabel, idleLabel }: {
|
|
|
47
47
|
idleLabel: string;
|
|
48
48
|
}, _optional?: Record<string, never>): string;
|
|
49
49
|
/**
|
|
50
|
-
* One `<url>` entry from a parsed sitemap. Only the fields
|
|
50
|
+
* One `<url>` entry from a parsed sitemap. Only the fields the host's
|
|
51
51
|
* `registerSeoSitemapRoute` (`server/inbound/public-http/routes/site/sitemap.ts`) actually emits —
|
|
52
52
|
* `<loc>` always, `<lastmod>` when the entry has one — verified by reading that route rather than
|
|
53
53
|
* guessed: it never writes `changefreq`/`priority`, so `SitemapModal.tsx`'s table has no columns
|
|
@@ -88,7 +88,7 @@ export declare function isStoredMediaRef({ value }: {
|
|
|
88
88
|
/** Builds the `{slug}:public` reference `MediaRefField` writes into the field on selection —
|
|
89
89
|
* prefers the asset's readable slug over its id (readable-slugs S5b, 2026-09-23), matching
|
|
90
90
|
* `resolveSeoImageRef`'s own id-or-slug lookup server-side (`findMediaByIdOrSlug`,
|
|
91
|
-
* `
|
|
91
|
+
* `features/seo/media.ts`, S4) so a saved override reads as a slug, not an opaque
|
|
92
92
|
* uuid. Falls back to `id` when `slug` is empty — defensive only; a real upload always derives one
|
|
93
93
|
* (`deriveUniqueMediaSlug`). Same `transformName: "public"` `EmbedInsertControl.tsx` already writes
|
|
94
94
|
* for an inserted image node. An old stored `{assetId}:public` value keeps resolving regardless
|
|
@@ -110,7 +110,7 @@ export declare function buildMediaRef({ item }: {
|
|
|
110
110
|
* "string"` also resolves to `null` rather than throwing — `MediaRefField`'s `value` prop is a
|
|
111
111
|
* `fieldValue(key, resolved) ?? ""` fallback chain (`react/pages/SeoPage.tsx`), where `??` does not replace a
|
|
112
112
|
* non-nullish-but-wrong-typed result from a misbehaving caller; matches `resolveSeoImageRef`'s own
|
|
113
|
-
* "never throws over one bad reference" contract (`
|
|
113
|
+
* "never throws over one bad reference" contract (`features/seo/media.ts`).
|
|
114
114
|
*
|
|
115
115
|
* @complexity O(1) — string parsing only, no I/O (the returned URL is a template; the browser
|
|
116
116
|
* performs the actual fetch only once it is used as an `<img src>`). */
|
package/dist/seo/rules.js
CHANGED
|
@@ -23,7 +23,7 @@ export function sortIssuesBySeverity({ issues }, _optional = {}) {
|
|
|
23
23
|
/**
|
|
24
24
|
* One edited per-entry override field's outgoing value. An emptied text/URL box means "remove this
|
|
25
25
|
* override so the entry falls back to the site default", which on the wire is `null` — the clear
|
|
26
|
-
* sentinel `SeoExtFieldsPatch` documents (`
|
|
26
|
+
* sentinel `SeoExtFieldsPatch` documents (`features/seo/types.ts`) and
|
|
27
27
|
* `setEntrySeoOverrides` implements by `delete`-ing the key.
|
|
28
28
|
*
|
|
29
29
|
* It is NOT `""`. `write-service.ts` stores `""` as a genuine override and `seo.ts` resolves
|
|
@@ -89,10 +89,10 @@ export function filterSitemapEntries({ entries, query }, _optional = {}) {
|
|
|
89
89
|
* `MediaRefField`'s ref-building/preview logic (`ogImage`/`twitterImage`/`defaultOgImage` all
|
|
90
90
|
* share this shape — SPEC intent: "make the OG image selectable, with a preview"). The reference
|
|
91
91
|
* format itself is NOT reinvented here — it is verified, not guessed, against the one function that
|
|
92
|
-
* actually parses it server-side: `resolveSeoImageRef` (`
|
|
92
|
+
* actually parses it server-side: `resolveSeoImageRef` (`features/seo/media.ts`)
|
|
93
93
|
* accepts either an already-absolute URL, passed through unchanged, or a bare `{assetId}:{transformName}`
|
|
94
94
|
* pair split on the FIRST `:`. `isAbsoluteMediaRef`/`parseMediaRefAssetId` below duplicate that
|
|
95
|
-
* parsing rather than import across the app boundary (
|
|
95
|
+
* parsing rather than import across the app boundary (the admin UI has no dependency on the host server)
|
|
96
96
|
* — kept in lockstep by this doc comment naming the source of truth; a change to that split MUST
|
|
97
97
|
* update both.
|
|
98
98
|
*/
|
|
@@ -101,7 +101,7 @@ export function filterSitemapEntries({ entries, query }, _optional = {}) {
|
|
|
101
101
|
* `transformName: "public"` for the identical `{assetId}:{transformName}` shape it writes into a
|
|
102
102
|
* post body's image node. */
|
|
103
103
|
const SEO_IMAGE_TRANSFORM = "public";
|
|
104
|
-
/** Mirrors `resolveSeoImageRef`'s own `isAbsoluteUrl` (`
|
|
104
|
+
/** Mirrors `resolveSeoImageRef`'s own `isAbsoluteUrl` (`features/seo/media.ts`) —
|
|
105
105
|
* see this section's file-header doc for why this is a duplicate, not an import. */
|
|
106
106
|
const ABSOLUTE_URL_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
107
107
|
function isAbsoluteMediaRef(value) {
|
|
@@ -129,7 +129,7 @@ export function isStoredMediaRef({ value }, _optional = {}) {
|
|
|
129
129
|
/** Builds the `{slug}:public` reference `MediaRefField` writes into the field on selection —
|
|
130
130
|
* prefers the asset's readable slug over its id (readable-slugs S5b, 2026-09-23), matching
|
|
131
131
|
* `resolveSeoImageRef`'s own id-or-slug lookup server-side (`findMediaByIdOrSlug`,
|
|
132
|
-
* `
|
|
132
|
+
* `features/seo/media.ts`, S4) so a saved override reads as a slug, not an opaque
|
|
133
133
|
* uuid. Falls back to `id` when `slug` is empty — defensive only; a real upload always derives one
|
|
134
134
|
* (`deriveUniqueMediaSlug`). Same `transformName: "public"` `EmbedInsertControl.tsx` already writes
|
|
135
135
|
* for an inserted image node. An old stored `{assetId}:public` value keeps resolving regardless
|
|
@@ -149,7 +149,7 @@ export function buildMediaRef({ item }, _optional = {}) {
|
|
|
149
149
|
* "string"` also resolves to `null` rather than throwing — `MediaRefField`'s `value` prop is a
|
|
150
150
|
* `fieldValue(key, resolved) ?? ""` fallback chain (`react/pages/SeoPage.tsx`), where `??` does not replace a
|
|
151
151
|
* non-nullish-but-wrong-typed result from a misbehaving caller; matches `resolveSeoImageRef`'s own
|
|
152
|
-
* "never throws over one bad reference" contract (`
|
|
152
|
+
* "never throws over one bad reference" contract (`features/seo/media.ts`).
|
|
153
153
|
*
|
|
154
154
|
* @complexity O(1) — string parsing only, no I/O (the returned URL is a template; the browser
|
|
155
155
|
* performs the actual fetch only once it is used as an `<img src>`). */
|
|
@@ -205,7 +205,7 @@ export function shareImageWarning({ value, previewUrl, width, height, byteSize,
|
|
|
205
205
|
/** One `FormData` field as a string, with the caller's fallback for an absent field. Its own named
|
|
206
206
|
* function purely so {@link buildSeoSettingsPatch} below stays readable at a glance: written
|
|
207
207
|
* inline, the seven fields' `?? `/`||` chains scored the patch builder at the ESLint ceiling
|
|
208
|
-
* (
|
|
208
|
+
* (the admin lint policy enforces 9/9 as a hard error) for no gain in clarity. */
|
|
209
209
|
function formString(form, key, fallback = "") {
|
|
210
210
|
return String(form.get(key) ?? fallback);
|
|
211
211
|
}
|
|
@@ -214,7 +214,7 @@ function formString(form, key, fallback = "") {
|
|
|
214
214
|
*
|
|
215
215
|
* `null` is what CLEARS a site default: the server registers these three (`default_description`,
|
|
216
216
|
* `default_og_image`, `twitter_site`) as `nullable: true` and `buildScalarWrites`
|
|
217
|
-
* (`
|
|
217
|
+
* (`features/seo/settings.ts`) normalizes `null` to the `""` absent-sentinel that
|
|
218
218
|
* `getSeoSettings` reads back as `undefined`.
|
|
219
219
|
*
|
|
220
220
|
* It used to return `undefined`, under a comment claiming "an empty box means 'no site default'".
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/admin",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.11",
|
|
4
4
|
"description": "Composable admin surface for Jini-hosted products: framework-free contracts, panel registry, and route model in `/core`; React shell and panels in `/react`. Import only the layers you need.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -79,6 +79,12 @@
|
|
|
79
79
|
"./consent/react": "browser"
|
|
80
80
|
},
|
|
81
81
|
"isolation": {
|
|
82
|
+
"shared": [
|
|
83
|
+
"src/react/components/DataTable.tsx",
|
|
84
|
+
"src/react/components/ConfirmDialog",
|
|
85
|
+
"src/react/components/RowMenu",
|
|
86
|
+
"src/react/types.ts"
|
|
87
|
+
],
|
|
82
88
|
"forbidden": {
|
|
83
89
|
"universal": [
|
|
84
90
|
"node:",
|
|
@@ -476,12 +482,12 @@
|
|
|
476
482
|
"react": "^19.2.0",
|
|
477
483
|
"react-dom": "^19.2.0",
|
|
478
484
|
"@testing-library/user-event": "^14.6.1",
|
|
479
|
-
"@jini-ai/
|
|
480
|
-
"@jini-ai/ui-kit": "^0.1.4",
|
|
481
|
-
"@jini-ai/core": "^0.4.5",
|
|
482
|
-
"@jini-ai/ui": "^0.5.1",
|
|
485
|
+
"@jini-ai/agent-plugins": "^0.4.6",
|
|
483
486
|
"@jini-ai/agentic": "^0.4.4",
|
|
484
|
-
"@jini-ai/
|
|
487
|
+
"@jini-ai/ui-kit": "^0.1.4",
|
|
488
|
+
"@jini-ai/cms": "^0.5.12",
|
|
489
|
+
"@jini-ai/core": "^0.4.6",
|
|
490
|
+
"@jini-ai/ui": "^0.5.3"
|
|
485
491
|
},
|
|
486
492
|
"typesVersions": {
|
|
487
493
|
"*": {
|