@open-mercato/core 0.4.5-develop-4849712ccb → 0.4.5-develop-7f44fcf045
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/modules/auth/api/admin/nav.js +10 -7
- package/dist/modules/auth/api/admin/nav.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +28 -15
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/translations.js +9 -0
- package/dist/modules/staff/translations.js.map +7 -0
- package/dist/modules/translations/components/TranslationDrawerAction.js +97 -0
- package/dist/modules/translations/components/TranslationDrawerAction.js.map +7 -0
- package/dist/modules/translations/lib/extract-record-id.js +31 -2
- package/dist/modules/translations/lib/extract-record-id.js.map +2 -2
- package/dist/modules/translations/lib/resolve-field-list.js +3 -0
- package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +105 -36
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
- package/dist/modules/translations/widgets/injection-table.js +18 -29
- package/dist/modules/translations/widgets/injection-table.js.map +2 -2
- package/package.json +2 -2
- package/src/modules/auth/api/admin/nav.ts +13 -7
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +8 -0
- package/src/modules/staff/translations.ts +5 -0
- package/src/modules/translations/components/TranslationDrawerAction.tsx +107 -0
- package/src/modules/translations/lib/extract-record-id.ts +47 -3
- package/src/modules/translations/lib/resolve-field-list.ts +4 -0
- package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +108 -36
- package/src/modules/translations/widgets/injection-table.ts +19 -33
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/translations/lib/extract-record-id.ts"],
|
|
4
|
-
"sourcesContent": ["
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["const UUID_LIKE_PATTERN = /^[0-9a-f-]{20,}$/i\n\ntype Params = Record<string, string | string[]>\n\nfunction readParam(params: Params, key: string) {\n const value = params[key]\n\n if (!value) {\n return undefined\n }\n\n if (Array.isArray(value)) {\n return value.find((entry) => typeof entry === 'string' && entry.length > 0)\n }\n\n return value\n}\n\nexport function extractRecordId(params: Params) {\n const directRecordId = readParam(params, 'recordId')\n if (directRecordId) {\n return directRecordId\n }\n\n const directId = readParam(params, 'id')\n if (directId) {\n return directId\n }\n\n const orderedIdCandidates = Object.entries(params)\n .filter(\n ([key]) =>\n key !== 'recordId' && key !== 'id' && key.toLowerCase().endsWith('id'),\n )\n .reverse()\n\n for (const [, value] of orderedIdCandidates) {\n const segments = Array.isArray(value) ? value : [value]\n for (const seg of segments) {\n if (seg && UUID_LIKE_PATTERN.test(seg)) {\n return seg\n }\n }\n }\n\n for (const [, value] of Object.entries(params)) {\n const segments = Array.isArray(value) ? value : [value]\n for (const seg of segments) {\n if (seg && UUID_LIKE_PATTERN.test(seg)) return seg\n }\n }\n\n return undefined\n}\n"],
|
|
5
|
+
"mappings": "AAAA,MAAM,oBAAoB;AAI1B,SAAS,UAAU,QAAgB,KAAa;AAC9C,QAAM,QAAQ,OAAO,GAAG;AAExB,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,KAAK,CAAC,UAAU,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC;AAAA,EAC5E;AAEA,SAAO;AACT;AAEO,SAAS,gBAAgB,QAAgB;AAC9C,QAAM,iBAAiB,UAAU,QAAQ,UAAU;AACnD,MAAI,gBAAgB;AAClB,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,UAAU,QAAQ,IAAI;AACvC,MAAI,UAAU;AACZ,WAAO;AAAA,EACT;AAEA,QAAM,sBAAsB,OAAO,QAAQ,MAAM,EAC9C;AAAA,IACC,CAAC,CAAC,GAAG,MACH,QAAQ,cAAc,QAAQ,QAAQ,IAAI,YAAY,EAAE,SAAS,IAAI;AAAA,EACzE,EACC,QAAQ;AAEX,aAAW,CAAC,EAAE,KAAK,KAAK,qBAAqB;AAC3C,UAAM,WAAW,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AACtD,eAAW,OAAO,UAAU;AAC1B,UAAI,OAAO,kBAAkB,KAAK,GAAG,GAAG;AACtC,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEA,aAAW,CAAC,EAAE,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC9C,UAAM,WAAW,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AACtD,eAAW,OAAO,UAAU;AAC1B,UAAI,OAAO,kBAAkB,KAAK,GAAG,EAAG,QAAO;AAAA,IACjD;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -15,7 +15,9 @@ function resolveFieldList(entityType, explicitFields, customFieldDefs) {
|
|
|
15
15
|
}
|
|
16
16
|
const registered = getTranslatableFields(entityType);
|
|
17
17
|
const fields = [];
|
|
18
|
+
let hasExplicitList = false;
|
|
18
19
|
if (registered) {
|
|
20
|
+
hasExplicitList = true;
|
|
19
21
|
for (const key of registered) {
|
|
20
22
|
fields.push({
|
|
21
23
|
key,
|
|
@@ -43,6 +45,7 @@ function resolveFieldList(entityType, explicitFields, customFieldDefs) {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
}
|
|
48
|
+
if (hasExplicitList) return fields;
|
|
46
49
|
for (const def of customFieldDefs) {
|
|
47
50
|
const key = typeof def.key === "string" ? def.key.trim() : "";
|
|
48
51
|
if (!key) continue;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/translations/lib/resolve-field-list.ts"],
|
|
4
|
-
"sourcesContent": ["import { getTranslatableFields } from '@open-mercato/shared/lib/localization/translatable-fields'\nimport { getEntityFields } from '#generated/entity-fields-registry'\nimport { isTranslatableField } from './translatable-fields'\nimport { formatFieldLabel } from './helpers'\n\nexport type ResolvedField = { key: string; label: string; multiline: boolean }\n\nfunction isMultiline(key: string): boolean {\n return key === 'description' || key.includes('description') || key.includes('content')\n}\n\nexport function resolveFieldList(\n entityType: string,\n explicitFields: string[] | undefined,\n customFieldDefs: Array<{ key: string; kind: string; label?: string }>,\n): ResolvedField[] {\n if (explicitFields?.length) {\n return explicitFields.map((key) => ({\n key,\n label: formatFieldLabel(key),\n multiline: isMultiline(key),\n }))\n }\n\n const registered = getTranslatableFields(entityType)\n const fields: ResolvedField[] = []\n\n if (registered) {\n for (const key of registered) {\n fields.push({\n key,\n label: formatFieldLabel(key),\n multiline: isMultiline(key),\n })\n }\n } else {\n const parts = entityType.split(':')\n const entitySlug = parts[1]\n if (entitySlug) {\n const mod = getEntityFields(entitySlug)\n if (mod) {\n for (const raw of Object.values(mod)) {\n if (typeof raw !== 'string' || !raw.trim()) continue\n const value = raw.trim()\n if (isTranslatableField(value) && !fields.some((f) => f.key === value)) {\n fields.push({\n key: value,\n label: formatFieldLabel(value),\n multiline: isMultiline(value),\n })\n }\n }\n }\n }\n }\n\n for (const def of customFieldDefs) {\n const key = typeof def.key === 'string' ? def.key.trim() : ''\n if (!key) continue\n if (def.kind !== 'text' && def.kind !== 'multiline' && def.kind !== 'richtext') continue\n if (fields.some((f) => f.key === key)) continue\n const label = typeof def.label === 'string' && def.label.trim().length ? def.label : formatFieldLabel(key)\n fields.push({ key, label, multiline: def.kind === 'multiline' || def.kind === 'richtext' })\n }\n\n return fields\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,6BAA6B;AACtC,SAAS,uBAAuB;AAChC,SAAS,2BAA2B;AACpC,SAAS,wBAAwB;AAIjC,SAAS,YAAY,KAAsB;AACzC,SAAO,QAAQ,iBAAiB,IAAI,SAAS,aAAa,KAAK,IAAI,SAAS,SAAS;AACvF;AAEO,SAAS,iBACd,YACA,gBACA,iBACiB;AACjB,MAAI,gBAAgB,QAAQ;AAC1B,WAAO,eAAe,IAAI,CAAC,SAAS;AAAA,MAClC;AAAA,MACA,OAAO,iBAAiB,GAAG;AAAA,MAC3B,WAAW,YAAY,GAAG;AAAA,IAC5B,EAAE;AAAA,EACJ;AAEA,QAAM,aAAa,sBAAsB,UAAU;AACnD,QAAM,SAA0B,CAAC;
|
|
4
|
+
"sourcesContent": ["import { getTranslatableFields } from '@open-mercato/shared/lib/localization/translatable-fields'\nimport { getEntityFields } from '#generated/entity-fields-registry'\nimport { isTranslatableField } from './translatable-fields'\nimport { formatFieldLabel } from './helpers'\n\nexport type ResolvedField = { key: string; label: string; multiline: boolean }\n\nfunction isMultiline(key: string): boolean {\n return key === 'description' || key.includes('description') || key.includes('content')\n}\n\nexport function resolveFieldList(\n entityType: string,\n explicitFields: string[] | undefined,\n customFieldDefs: Array<{ key: string; kind: string; label?: string }>,\n): ResolvedField[] {\n if (explicitFields?.length) {\n return explicitFields.map((key) => ({\n key,\n label: formatFieldLabel(key),\n multiline: isMultiline(key),\n }))\n }\n\n const registered = getTranslatableFields(entityType)\n const fields: ResolvedField[] = []\n let hasExplicitList = false\n\n if (registered) {\n hasExplicitList = true\n for (const key of registered) {\n fields.push({\n key,\n label: formatFieldLabel(key),\n multiline: isMultiline(key),\n })\n }\n } else {\n const parts = entityType.split(':')\n const entitySlug = parts[1]\n if (entitySlug) {\n const mod = getEntityFields(entitySlug)\n if (mod) {\n for (const raw of Object.values(mod)) {\n if (typeof raw !== 'string' || !raw.trim()) continue\n const value = raw.trim()\n if (isTranslatableField(value) && !fields.some((f) => f.key === value)) {\n fields.push({\n key: value,\n label: formatFieldLabel(value),\n multiline: isMultiline(value),\n })\n }\n }\n }\n }\n }\n\n if (hasExplicitList) return fields\n\n for (const def of customFieldDefs) {\n const key = typeof def.key === 'string' ? def.key.trim() : ''\n if (!key) continue\n if (def.kind !== 'text' && def.kind !== 'multiline' && def.kind !== 'richtext') continue\n if (fields.some((f) => f.key === key)) continue\n const label = typeof def.label === 'string' && def.label.trim().length ? def.label : formatFieldLabel(key)\n fields.push({ key, label, multiline: def.kind === 'multiline' || def.kind === 'richtext' })\n }\n\n return fields\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,6BAA6B;AACtC,SAAS,uBAAuB;AAChC,SAAS,2BAA2B;AACpC,SAAS,wBAAwB;AAIjC,SAAS,YAAY,KAAsB;AACzC,SAAO,QAAQ,iBAAiB,IAAI,SAAS,aAAa,KAAK,IAAI,SAAS,SAAS;AACvF;AAEO,SAAS,iBACd,YACA,gBACA,iBACiB;AACjB,MAAI,gBAAgB,QAAQ;AAC1B,WAAO,eAAe,IAAI,CAAC,SAAS;AAAA,MAClC;AAAA,MACA,OAAO,iBAAiB,GAAG;AAAA,MAC3B,WAAW,YAAY,GAAG;AAAA,IAC5B,EAAE;AAAA,EACJ;AAEA,QAAM,aAAa,sBAAsB,UAAU;AACnD,QAAM,SAA0B,CAAC;AACjC,MAAI,kBAAkB;AAEtB,MAAI,YAAY;AACd,sBAAkB;AAClB,eAAW,OAAO,YAAY;AAC5B,aAAO,KAAK;AAAA,QACV;AAAA,QACA,OAAO,iBAAiB,GAAG;AAAA,QAC3B,WAAW,YAAY,GAAG;AAAA,MAC5B,CAAC;AAAA,IACH;AAAA,EACF,OAAO;AACL,UAAM,QAAQ,WAAW,MAAM,GAAG;AAClC,UAAM,aAAa,MAAM,CAAC;AAC1B,QAAI,YAAY;AACd,YAAM,MAAM,gBAAgB,UAAU;AACtC,UAAI,KAAK;AACP,mBAAW,OAAO,OAAO,OAAO,GAAG,GAAG;AACpC,cAAI,OAAO,QAAQ,YAAY,CAAC,IAAI,KAAK,EAAG;AAC5C,gBAAM,QAAQ,IAAI,KAAK;AACvB,cAAI,oBAAoB,KAAK,KAAK,CAAC,OAAO,KAAK,CAAC,MAAM,EAAE,QAAQ,KAAK,GAAG;AACtE,mBAAO,KAAK;AAAA,cACV,KAAK;AAAA,cACL,OAAO,iBAAiB,KAAK;AAAA,cAC7B,WAAW,YAAY,KAAK;AAAA,YAC9B,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,gBAAiB,QAAO;AAE5B,aAAW,OAAO,iBAAiB;AACjC,UAAM,MAAM,OAAO,IAAI,QAAQ,WAAW,IAAI,IAAI,KAAK,IAAI;AAC3D,QAAI,CAAC,IAAK;AACV,QAAI,IAAI,SAAS,UAAU,IAAI,SAAS,eAAe,IAAI,SAAS,WAAY;AAChF,QAAI,OAAO,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG,EAAG;AACvC,UAAM,QAAQ,OAAO,IAAI,UAAU,YAAY,IAAI,MAAM,KAAK,EAAE,SAAS,IAAI,QAAQ,iBAAiB,GAAG;AACzG,WAAO,KAAK,EAAE,KAAK,OAAO,WAAW,IAAI,SAAS,eAAe,IAAI,SAAS,WAAW,CAAC;AAAA,EAC5F;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import Link from "next/link";
|
|
5
4
|
import { useParams } from "next/navigation";
|
|
6
|
-
import
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { ExternalLink, Languages, X } from "lucide-react";
|
|
7
7
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
8
9
|
import { TranslationManager } from "../../../components/TranslationManager.js";
|
|
9
10
|
import { extractRecordId } from "../../../lib/extract-record-id.js";
|
|
10
11
|
function useTranslationAccess() {
|
|
@@ -27,50 +28,118 @@ function TranslationWidget({ context, data }) {
|
|
|
27
28
|
const entityType = context?.entityId;
|
|
28
29
|
const params = useParams();
|
|
29
30
|
const t = useT();
|
|
31
|
+
const [open, setOpen] = React.useState(false);
|
|
30
32
|
const hasAccess = useTranslationAccess();
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const contextRecordId = typeof context?.recordId === "string" && context.recordId.trim().length > 0 ? context.recordId.trim() : void 0;
|
|
34
|
+
const dataRecordId = data?.id === void 0 || data.id === null ? void 0 : String(data.id);
|
|
35
|
+
const routeRecordId = params ? extractRecordId(params) : void 0;
|
|
36
|
+
const recordId = contextRecordId ?? dataRecordId ?? routeRecordId;
|
|
37
|
+
const canRender = Boolean(entityType && recordId && hasAccess);
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
if (!open || !canRender) return;
|
|
40
|
+
const prev = document.body.style.overflow;
|
|
41
|
+
document.body.style.overflow = "hidden";
|
|
42
|
+
return () => {
|
|
43
|
+
document.body.style.overflow = prev;
|
|
44
|
+
};
|
|
45
|
+
}, [canRender, open]);
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
if (!open || !canRender) return;
|
|
48
|
+
const handleKeyDown = (event) => {
|
|
49
|
+
if (event.key === "Escape") {
|
|
50
|
+
setOpen(false);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
54
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
55
|
+
}, [canRender, open]);
|
|
56
|
+
if (!canRender) return null;
|
|
57
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
38
58
|
/* @__PURE__ */ jsx(
|
|
39
|
-
|
|
59
|
+
Button,
|
|
40
60
|
{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
type: "button",
|
|
62
|
+
variant: "ghost",
|
|
63
|
+
size: "icon",
|
|
64
|
+
onClick: () => setOpen(true),
|
|
65
|
+
"aria-label": t("translations.widgets.translationManager.fullManager", "Translation manager"),
|
|
66
|
+
title: t("translations.widgets.translationManager.fullManager", "Translation manager"),
|
|
67
|
+
children: /* @__PURE__ */ jsx(Languages, { className: "size-4" })
|
|
46
68
|
}
|
|
47
69
|
),
|
|
48
|
-
/* @__PURE__ */ jsxs(
|
|
49
|
-
/* @__PURE__ */
|
|
50
|
-
|
|
70
|
+
open ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
71
|
+
/* @__PURE__ */ jsx(
|
|
72
|
+
"div",
|
|
51
73
|
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
/* @__PURE__ */ jsx(Languages, { className: "h-3 w-3" }),
|
|
56
|
-
t("translations.widgets.translationManager.customFieldLabels", "Custom fields translations"),
|
|
57
|
-
/* @__PURE__ */ jsx(ExternalLink, { className: "h-2.5 w-2.5" })
|
|
58
|
-
]
|
|
74
|
+
className: "fixed inset-0 z-40 bg-black/20",
|
|
75
|
+
onClick: () => setOpen(false),
|
|
76
|
+
"aria-hidden": "true"
|
|
59
77
|
}
|
|
60
78
|
),
|
|
61
|
-
/* @__PURE__ */
|
|
62
|
-
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
"div",
|
|
63
81
|
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/* @__PURE__ */
|
|
70
|
-
|
|
82
|
+
className: "fixed right-0 top-0 z-50 h-full w-full max-w-4xl border-l bg-background shadow-lg",
|
|
83
|
+
role: "dialog",
|
|
84
|
+
"aria-modal": "true",
|
|
85
|
+
"aria-label": t("translations.widgets.translationManager.groupLabel", "Translations"),
|
|
86
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col", children: [
|
|
87
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3 border-b px-4 py-3", children: [
|
|
88
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
89
|
+
/* @__PURE__ */ jsx("h2", { className: "font-semibold", children: t("translations.widgets.translationManager.groupLabel", "Translations") }),
|
|
90
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("translations.widgets.translationManager.groupDescription", "Manage translations for this record across supported locales.") })
|
|
91
|
+
] }),
|
|
92
|
+
/* @__PURE__ */ jsx(
|
|
93
|
+
Button,
|
|
94
|
+
{
|
|
95
|
+
variant: "ghost",
|
|
96
|
+
size: "icon",
|
|
97
|
+
onClick: () => setOpen(false),
|
|
98
|
+
"aria-label": t("ui.dialog.close.ariaLabel", "Close"),
|
|
99
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
] }),
|
|
103
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: /* @__PURE__ */ jsx(
|
|
104
|
+
TranslationManager,
|
|
105
|
+
{
|
|
106
|
+
mode: "embedded",
|
|
107
|
+
compact: true,
|
|
108
|
+
entityType,
|
|
109
|
+
recordId,
|
|
110
|
+
baseValues: data
|
|
111
|
+
}
|
|
112
|
+
) }),
|
|
113
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-x-4 gap-y-1 border-t px-4 py-3", children: [
|
|
114
|
+
/* @__PURE__ */ jsxs(
|
|
115
|
+
Link,
|
|
116
|
+
{
|
|
117
|
+
href: `/backend/entities/system/${encodeURIComponent(entityType)}`,
|
|
118
|
+
className: "inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors",
|
|
119
|
+
children: [
|
|
120
|
+
/* @__PURE__ */ jsx(Languages, { className: "size-3" }),
|
|
121
|
+
t("translations.widgets.translationManager.customFieldLabels", "Custom fields translations"),
|
|
122
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "size-2.5" })
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
/* @__PURE__ */ jsxs(
|
|
127
|
+
Link,
|
|
128
|
+
{
|
|
129
|
+
href: "/backend/config/translations",
|
|
130
|
+
className: "inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors",
|
|
131
|
+
children: [
|
|
132
|
+
/* @__PURE__ */ jsx(Languages, { className: "size-3" }),
|
|
133
|
+
t("translations.widgets.translationManager.fullManager", "Translation manager"),
|
|
134
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "size-2.5" })
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
] })
|
|
139
|
+
] })
|
|
71
140
|
}
|
|
72
141
|
)
|
|
73
|
-
] })
|
|
142
|
+
] }) : null
|
|
74
143
|
] });
|
|
75
144
|
}
|
|
76
145
|
export {
|
package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/translations/widgets/injection/translation-manager/widget.client.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useParams } from 'next/navigation'\nimport Link from 'next/link'\nimport { ExternalLink, Languages, X } from 'lucide-react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { TranslationManager } from '../../../components/TranslationManager'\nimport { extractRecordId } from '../../../lib/extract-record-id'\n\ntype WidgetContext = { entityId?: string; recordId?: string }\ntype WidgetData = Record<string, unknown> & { id?: string | number }\n\nfunction useTranslationAccess(): boolean {\n const [hasAccess, setHasAccess] = React.useState(false)\n React.useEffect(() => {\n let mounted = true\n // Use the original fetch to bypass the global apiFetch wrapper\n // that redirects to login on 403. This lets us gracefully hide the widget\n // when the user lacks translations.view instead of crashing the page.\n const nativeFetch = ((window as any).__omOriginalFetch as typeof fetch) || fetch\n nativeFetch('/api/translations/locales', { credentials: 'include' })\n .then((res) => { if (mounted) setHasAccess(res.ok) })\n .catch(() => { if (mounted) setHasAccess(false) })\n return () => { mounted = false }\n }, [])\n return hasAccess\n}\n\nexport default function TranslationWidget({ context, data }: InjectionWidgetComponentProps<WidgetContext, WidgetData>) {\n const entityType = context?.entityId\n const params = useParams()\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const hasAccess = useTranslationAccess()\n\n const contextRecordId = typeof context?.recordId === 'string' && context.recordId.trim().length > 0\n ? context.recordId.trim()\n : undefined\n const dataRecordId = data?.id === undefined || data.id === null ? undefined : String(data.id)\n const routeRecordId = params ? extractRecordId(params as Record<string, string | string[]>) : undefined\n const recordId = contextRecordId ?? dataRecordId ?? routeRecordId\n const canRender = Boolean(entityType && recordId && hasAccess)\n\n React.useEffect(() => {\n if (!open || !canRender) return\n const prev = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n return () => {\n document.body.style.overflow = prev\n }\n }, [canRender, open])\n\n React.useEffect(() => {\n if (!open || !canRender) return\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n setOpen(false)\n }\n }\n document.addEventListener('keydown', handleKeyDown)\n return () => document.removeEventListener('keydown', handleKeyDown)\n }, [canRender, open])\n\n if (!canRender) return null\n\n return (\n <>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setOpen(true)}\n aria-label={t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n title={t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n >\n <Languages className=\"size-4\" />\n </Button>\n {open ? (\n <>\n <div\n className=\"fixed inset-0 z-40 bg-black/20\"\n onClick={() => setOpen(false)}\n aria-hidden=\"true\"\n />\n <div\n className=\"fixed right-0 top-0 z-50 h-full w-full max-w-4xl border-l bg-background shadow-lg\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('translations.widgets.translationManager.groupLabel', 'Translations')}\n >\n <div className=\"flex h-full flex-col\">\n <div className=\"flex items-start justify-between gap-3 border-b px-4 py-3\">\n <div className=\"space-y-1\">\n <h2 className=\"font-semibold\">\n {t('translations.widgets.translationManager.groupLabel', 'Translations')}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('translations.widgets.translationManager.groupDescription', 'Manage translations for this record across supported locales.')}\n </p>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setOpen(false)}\n aria-label={t('ui.dialog.close.ariaLabel', 'Close')}\n >\n <X className=\"size-4\" />\n </Button>\n </div>\n <div className=\"flex-1 overflow-y-auto px-4 py-4\">\n <TranslationManager\n mode=\"embedded\"\n compact\n entityType={entityType}\n recordId={recordId}\n baseValues={data}\n />\n </div>\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 border-t px-4 py-3\">\n <Link\n href={`/backend/entities/system/${encodeURIComponent(entityType!)}`}\n className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n <Languages className=\"size-3\" />\n {t('translations.widgets.translationManager.customFieldLabels', 'Custom fields translations')}\n <ExternalLink className=\"size-2.5\" />\n </Link>\n <Link\n href=\"/backend/config/translations\"\n className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n <Languages className=\"size-3\" />\n {t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n <ExternalLink className=\"size-2.5\" />\n </Link>\n </div>\n </div>\n </div>\n </>\n ) : null}\n </>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,39 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { translatableFields as dictionaryFields } from "../../dictionaries/translations.js";
|
|
3
|
-
import { translatableFields as entitiesFields } from "../../entities/translations.js";
|
|
4
|
-
import { translatableFields as resourcesFields } from "../../resources/translations.js";
|
|
1
|
+
import { getTranslatableFieldsRegistry } from "@open-mercato/shared/lib/localization/translatable-fields";
|
|
5
2
|
const WIDGET_ID = "translations.injection.translation-manager";
|
|
6
|
-
|
|
7
|
-
...catalogFields,
|
|
8
|
-
...dictionaryFields,
|
|
9
|
-
...entitiesFields,
|
|
10
|
-
...resourcesFields
|
|
11
|
-
};
|
|
12
|
-
const ENTRY_TEMPLATE = {
|
|
13
|
-
widgetId: WIDGET_ID,
|
|
14
|
-
kind: "group",
|
|
15
|
-
column: 2,
|
|
16
|
-
groupLabel: "translations.widgets.translationManager.groupLabel",
|
|
17
|
-
groupDescription: "translations.widgets.translationManager.groupDescription",
|
|
18
|
-
priority: 40
|
|
19
|
-
};
|
|
20
|
-
const table = {};
|
|
21
|
-
for (const entityType of Object.keys(allFields)) {
|
|
3
|
+
function addEntitySpots(table, entityType) {
|
|
22
4
|
const [module, entitySlug] = entityType.split(":");
|
|
23
|
-
if (!module || !entitySlug)
|
|
24
|
-
const fullSpot = `crud-form:${module}.${entitySlug}`;
|
|
25
|
-
table[fullSpot] =
|
|
5
|
+
if (!module || !entitySlug) return;
|
|
6
|
+
const fullSpot = `crud-form:${module}.${entitySlug}:header`;
|
|
7
|
+
table[fullSpot] = WIDGET_ID;
|
|
26
8
|
const prefix = `${module}_`;
|
|
27
|
-
if (entitySlug.startsWith(prefix))
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
9
|
+
if (!entitySlug.startsWith(prefix)) return;
|
|
10
|
+
const shortSpot = `crud-form:${module}.${entitySlug.slice(prefix.length)}:header`;
|
|
11
|
+
if (shortSpot !== fullSpot) {
|
|
12
|
+
table[shortSpot] = WIDGET_ID;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function buildInjectionTable(allFields = getTranslatableFieldsRegistry()) {
|
|
16
|
+
const table = {};
|
|
17
|
+
for (const entityType of Object.keys(allFields)) {
|
|
18
|
+
addEntitySpots(table, entityType);
|
|
32
19
|
}
|
|
20
|
+
return table;
|
|
33
21
|
}
|
|
34
|
-
const injectionTable =
|
|
22
|
+
const injectionTable = buildInjectionTable();
|
|
35
23
|
var injection_table_default = injectionTable;
|
|
36
24
|
export {
|
|
25
|
+
buildInjectionTable,
|
|
37
26
|
injection_table_default as default,
|
|
38
27
|
injectionTable
|
|
39
28
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/translations/widgets/injection-table.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'\nimport {
|
|
5
|
-
"mappings": "AACA,SAAS,
|
|
4
|
+
"sourcesContent": ["import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'\nimport { getTranslatableFieldsRegistry } from '@open-mercato/shared/lib/localization/translatable-fields'\n\nconst WIDGET_ID = 'translations.injection.translation-manager'\n\nfunction addEntitySpots(table: ModuleInjectionTable, entityType: string): void {\n const [module, entitySlug] = entityType.split(':')\n if (!module || !entitySlug) return\n\n const fullSpot = `crud-form:${module}.${entitySlug}:header`\n table[fullSpot] = WIDGET_ID\n\n const prefix = `${module}_`\n if (!entitySlug.startsWith(prefix)) return\n\n const shortSpot = `crud-form:${module}.${entitySlug.slice(prefix.length)}:header`\n if (shortSpot !== fullSpot) {\n table[shortSpot] = WIDGET_ID\n }\n}\n\nexport function buildInjectionTable(allFields: Record<string, string[]> = getTranslatableFieldsRegistry()): ModuleInjectionTable {\n const table: ModuleInjectionTable = {}\n for (const entityType of Object.keys(allFields)) {\n addEntitySpots(table, entityType)\n }\n return table\n}\n\nexport const injectionTable: ModuleInjectionTable = buildInjectionTable()\nexport default injectionTable\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,qCAAqC;AAE9C,MAAM,YAAY;AAElB,SAAS,eAAe,OAA6B,YAA0B;AAC7E,QAAM,CAAC,QAAQ,UAAU,IAAI,WAAW,MAAM,GAAG;AACjD,MAAI,CAAC,UAAU,CAAC,WAAY;AAE5B,QAAM,WAAW,aAAa,MAAM,IAAI,UAAU;AAClD,QAAM,QAAQ,IAAI;AAElB,QAAM,SAAS,GAAG,MAAM;AACxB,MAAI,CAAC,WAAW,WAAW,MAAM,EAAG;AAEpC,QAAM,YAAY,aAAa,MAAM,IAAI,WAAW,MAAM,OAAO,MAAM,CAAC;AACxE,MAAI,cAAc,UAAU;AAC1B,UAAM,SAAS,IAAI;AAAA,EACrB;AACF;AAEO,SAAS,oBAAoB,YAAsC,8BAA8B,GAAyB;AAC/H,QAAM,QAA8B,CAAC;AACrC,aAAW,cAAc,OAAO,KAAK,SAAS,GAAG;AAC/C,mBAAe,OAAO,UAAU;AAAA,EAClC;AACA,SAAO;AACT;AAEO,MAAM,iBAAuC,oBAAoB;AACxE,IAAO,0BAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.5-develop-
|
|
3
|
+
"version": "0.4.5-develop-7f44fcf045",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dependencies": {
|
|
210
|
-
"@open-mercato/shared": "0.4.5-develop-
|
|
210
|
+
"@open-mercato/shared": "0.4.5-develop-7f44fcf045",
|
|
211
211
|
"@types/semver": "^7.5.8",
|
|
212
212
|
"@xyflow/react": "^12.6.0",
|
|
213
213
|
"ai": "^6.0.0",
|
|
@@ -156,13 +156,19 @@ export async function GET(req: Request) {
|
|
|
156
156
|
href: `/backend/entities/user/${encodeURIComponent(e.entityId)}/records`
|
|
157
157
|
}))
|
|
158
158
|
if (items.length) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
const userEntitiesLegacyGroupKeys = new Set(['settings.sections.dataDesigner', 'entities.nav.group'])
|
|
160
|
+
const userEntitiesAnchor = entries.find((entry: Entry) => entry.href === '/backend/entities/user')
|
|
161
|
+
?? entries.find((entry: Entry) =>
|
|
162
|
+
entry.titleKey === 'entities.nav.userEntities' &&
|
|
163
|
+
typeof entry.groupKey === 'string' &&
|
|
164
|
+
userEntitiesLegacyGroupKeys.has(entry.groupKey),
|
|
165
|
+
)
|
|
166
|
+
if (userEntitiesAnchor) {
|
|
167
|
+
const existing = userEntitiesAnchor.children || []
|
|
162
168
|
const dynamic = items.map((it) => ({
|
|
163
|
-
groupId:
|
|
164
|
-
groupName:
|
|
165
|
-
groupKey:
|
|
169
|
+
groupId: userEntitiesAnchor.groupId,
|
|
170
|
+
groupName: userEntitiesAnchor.groupName,
|
|
171
|
+
groupKey: userEntitiesAnchor.groupKey,
|
|
166
172
|
title: it.label,
|
|
167
173
|
href: it.href,
|
|
168
174
|
enabled: true,
|
|
@@ -172,7 +178,7 @@ export async function GET(req: Request) {
|
|
|
172
178
|
const byHref = new Map<string, Entry>()
|
|
173
179
|
for (const c of existing) if (!byHref.has(c.href)) byHref.set(c.href, c)
|
|
174
180
|
for (const c of dynamic) if (!byHref.has(c.href)) byHref.set(c.href, c)
|
|
175
|
-
|
|
181
|
+
userEntitiesAnchor.children = Array.from(byHref.values())
|
|
176
182
|
}
|
|
177
183
|
}
|
|
178
184
|
} catch (e) {
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import { JobHistorySection } from '@open-mercato/core/modules/staff/components/detail/JobHistorySection'
|
|
31
31
|
import type { DictionarySelectLabels } from '@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect'
|
|
32
32
|
import { Plus } from 'lucide-react'
|
|
33
|
+
import { TranslationDrawerAction } from '@open-mercato/core/modules/translations/components/TranslationDrawerAction'
|
|
33
34
|
|
|
34
35
|
const MARKDOWN_CLASSNAME =
|
|
35
36
|
'text-sm text-muted-foreground break-words [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs'
|
|
@@ -324,6 +325,13 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
|
|
|
324
325
|
</p>
|
|
325
326
|
</div>
|
|
326
327
|
</div>
|
|
328
|
+
<TranslationDrawerAction
|
|
329
|
+
config={memberId ? {
|
|
330
|
+
entityType: 'staff:staff_team_member',
|
|
331
|
+
recordId: memberId,
|
|
332
|
+
baseValues: memberRecord ?? undefined,
|
|
333
|
+
} : null}
|
|
334
|
+
/>
|
|
327
335
|
</div>
|
|
328
336
|
|
|
329
337
|
<div className="border-b">
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Languages, X } from 'lucide-react'
|
|
5
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import { TranslationManager } from './TranslationManager'
|
|
8
|
+
|
|
9
|
+
export type TranslationDrawerActionConfig = {
|
|
10
|
+
entityType: string
|
|
11
|
+
recordId: string
|
|
12
|
+
baseValues?: Record<string, unknown>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type TranslationDrawerActionProps = {
|
|
16
|
+
config: TranslationDrawerActionConfig | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function TranslationDrawerAction({ config }: TranslationDrawerActionProps) {
|
|
20
|
+
const t = useT()
|
|
21
|
+
const [open, setOpen] = React.useState(false)
|
|
22
|
+
|
|
23
|
+
const canRender = Boolean(
|
|
24
|
+
config?.entityType && config?.recordId && String(config.recordId).trim().length > 0,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
if (!open || !canRender) return
|
|
29
|
+
const prev = document.body.style.overflow
|
|
30
|
+
document.body.style.overflow = 'hidden'
|
|
31
|
+
return () => {
|
|
32
|
+
document.body.style.overflow = prev
|
|
33
|
+
}
|
|
34
|
+
}, [canRender, open])
|
|
35
|
+
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
if (!open || !canRender) return
|
|
38
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
39
|
+
if (event.key === 'Escape') {
|
|
40
|
+
setOpen(false)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
44
|
+
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
45
|
+
}, [canRender, open])
|
|
46
|
+
|
|
47
|
+
if (!canRender) return null
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<Button
|
|
52
|
+
type="button"
|
|
53
|
+
variant="ghost"
|
|
54
|
+
size="icon"
|
|
55
|
+
onClick={() => setOpen(true)}
|
|
56
|
+
aria-label={t('translations.widgets.translationManager.fullManager', 'Translation manager')}
|
|
57
|
+
title={t('translations.widgets.translationManager.fullManager', 'Translation manager')}
|
|
58
|
+
>
|
|
59
|
+
<Languages className="size-4" />
|
|
60
|
+
</Button>
|
|
61
|
+
{open ? (
|
|
62
|
+
<>
|
|
63
|
+
<div
|
|
64
|
+
className="fixed inset-0 z-40 bg-black/20"
|
|
65
|
+
onClick={() => setOpen(false)}
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
/>
|
|
68
|
+
<div
|
|
69
|
+
className="fixed right-0 top-0 z-50 h-full w-full max-w-4xl border-l bg-background shadow-lg"
|
|
70
|
+
role="dialog"
|
|
71
|
+
aria-modal="true"
|
|
72
|
+
aria-label={t('translations.widgets.translationManager.groupLabel', 'Translations')}
|
|
73
|
+
>
|
|
74
|
+
<div className="flex h-full flex-col">
|
|
75
|
+
<div className="flex items-start justify-between gap-3 border-b px-4 py-3">
|
|
76
|
+
<div className="space-y-1">
|
|
77
|
+
<h2 className="font-semibold">
|
|
78
|
+
{t('translations.widgets.translationManager.groupLabel', 'Translations')}
|
|
79
|
+
</h2>
|
|
80
|
+
<p className="text-sm text-muted-foreground">
|
|
81
|
+
{t('translations.widgets.translationManager.groupDescription', 'Manage translations for this record across supported locales.')}
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
<Button
|
|
85
|
+
variant="ghost"
|
|
86
|
+
size="icon"
|
|
87
|
+
onClick={() => setOpen(false)}
|
|
88
|
+
aria-label={t('ui.dialog.close.ariaLabel', 'Close')}
|
|
89
|
+
>
|
|
90
|
+
<X className="size-4" />
|
|
91
|
+
</Button>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="flex-1 overflow-y-auto px-4 py-4">
|
|
94
|
+
<TranslationManager
|
|
95
|
+
mode="embedded"
|
|
96
|
+
entityType={config!.entityType}
|
|
97
|
+
recordId={config!.recordId}
|
|
98
|
+
baseValues={config!.baseValues}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</>
|
|
104
|
+
) : null}
|
|
105
|
+
</>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -1,10 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const UUID_LIKE_PATTERN = /^[0-9a-f-]{20,}$/i
|
|
2
|
+
|
|
3
|
+
type Params = Record<string, string | string[]>
|
|
4
|
+
|
|
5
|
+
function readParam(params: Params, key: string) {
|
|
6
|
+
const value = params[key]
|
|
7
|
+
|
|
8
|
+
if (!value) {
|
|
9
|
+
return undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return value.find((entry) => typeof entry === 'string' && entry.length > 0)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function extractRecordId(params: Params) {
|
|
20
|
+
const directRecordId = readParam(params, 'recordId')
|
|
21
|
+
if (directRecordId) {
|
|
22
|
+
return directRecordId
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const directId = readParam(params, 'id')
|
|
26
|
+
if (directId) {
|
|
27
|
+
return directId
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const orderedIdCandidates = Object.entries(params)
|
|
31
|
+
.filter(
|
|
32
|
+
([key]) =>
|
|
33
|
+
key !== 'recordId' && key !== 'id' && key.toLowerCase().endsWith('id'),
|
|
34
|
+
)
|
|
35
|
+
.reverse()
|
|
36
|
+
|
|
37
|
+
for (const [, value] of orderedIdCandidates) {
|
|
38
|
+
const segments = Array.isArray(value) ? value : [value]
|
|
39
|
+
for (const seg of segments) {
|
|
40
|
+
if (seg && UUID_LIKE_PATTERN.test(seg)) {
|
|
41
|
+
return seg
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
3
46
|
for (const [, value] of Object.entries(params)) {
|
|
4
47
|
const segments = Array.isArray(value) ? value : [value]
|
|
5
48
|
for (const seg of segments) {
|
|
6
|
-
if (seg &&
|
|
49
|
+
if (seg && UUID_LIKE_PATTERN.test(seg)) return seg
|
|
7
50
|
}
|
|
8
51
|
}
|
|
52
|
+
|
|
9
53
|
return undefined
|
|
10
54
|
}
|
|
@@ -24,8 +24,10 @@ export function resolveFieldList(
|
|
|
24
24
|
|
|
25
25
|
const registered = getTranslatableFields(entityType)
|
|
26
26
|
const fields: ResolvedField[] = []
|
|
27
|
+
let hasExplicitList = false
|
|
27
28
|
|
|
28
29
|
if (registered) {
|
|
30
|
+
hasExplicitList = true
|
|
29
31
|
for (const key of registered) {
|
|
30
32
|
fields.push({
|
|
31
33
|
key,
|
|
@@ -54,6 +56,8 @@ export function resolveFieldList(
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
if (hasExplicitList) return fields
|
|
60
|
+
|
|
57
61
|
for (const def of customFieldDefs) {
|
|
58
62
|
const key = typeof def.key === 'string' ? def.key.trim() : ''
|
|
59
63
|
if (!key) continue
|