@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.
Files changed (25) hide show
  1. package/dist/modules/auth/api/admin/nav.js +10 -7
  2. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  3. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +28 -15
  4. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  5. package/dist/modules/staff/translations.js +9 -0
  6. package/dist/modules/staff/translations.js.map +7 -0
  7. package/dist/modules/translations/components/TranslationDrawerAction.js +97 -0
  8. package/dist/modules/translations/components/TranslationDrawerAction.js.map +7 -0
  9. package/dist/modules/translations/lib/extract-record-id.js +31 -2
  10. package/dist/modules/translations/lib/extract-record-id.js.map +2 -2
  11. package/dist/modules/translations/lib/resolve-field-list.js +3 -0
  12. package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
  13. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +105 -36
  14. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
  15. package/dist/modules/translations/widgets/injection-table.js +18 -29
  16. package/dist/modules/translations/widgets/injection-table.js.map +2 -2
  17. package/package.json +2 -2
  18. package/src/modules/auth/api/admin/nav.ts +13 -7
  19. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +8 -0
  20. package/src/modules/staff/translations.ts +5 -0
  21. package/src/modules/translations/components/TranslationDrawerAction.tsx +107 -0
  22. package/src/modules/translations/lib/extract-record-id.ts +47 -3
  23. package/src/modules/translations/lib/resolve-field-list.ts +4 -0
  24. package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +108 -36
  25. 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": ["export function extractRecordId(params: Record<string, string | string[]>): string | undefined {\n if (params.id) return String(Array.isArray(params.id) ? params.id[0] : params.id)\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 && /^[0-9a-f-]{20,}$/i.test(seg)) return seg\n }\n }\n return undefined\n}\n"],
5
- "mappings": "AAAO,SAAS,gBAAgB,QAA+D;AAC7F,MAAI,OAAO,GAAI,QAAO,OAAO,MAAM,QAAQ,OAAO,EAAE,IAAI,OAAO,GAAG,CAAC,IAAI,OAAO,EAAE;AAChF,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,oBAAoB,KAAK,GAAG,EAAG,QAAO;AAAA,IACnD;AAAA,EACF;AACA,SAAO;AACT;",
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;AAEjC,MAAI,YAAY;AACd,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,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;",
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 { ExternalLink, Languages } from "lucide-react";
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 recordId = React.useMemo(() => {
32
- if (data?.id) return String(data.id);
33
- if (params) return extractRecordId(params);
34
- return void 0;
35
- }, [data?.id, params]);
36
- if (!entityType || !recordId || !hasAccess) return null;
37
- return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
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
- TranslationManager,
59
+ Button,
40
60
  {
41
- mode: "embedded",
42
- compact: true,
43
- entityType,
44
- recordId,
45
- baseValues: data
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("div", { className: "flex flex-wrap gap-x-4 gap-y-1 border-t pt-3", children: [
49
- /* @__PURE__ */ jsxs(
50
- Link,
70
+ open ? /* @__PURE__ */ jsxs(Fragment, { children: [
71
+ /* @__PURE__ */ jsx(
72
+ "div",
51
73
  {
52
- href: `/backend/entities/system/${entityType}`,
53
- className: "inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors",
54
- children: [
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__ */ jsxs(
62
- Link,
79
+ /* @__PURE__ */ jsx(
80
+ "div",
63
81
  {
64
- href: "/backend/config/translations",
65
- className: "inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors",
66
- children: [
67
- /* @__PURE__ */ jsx(Languages, { className: "h-3 w-3" }),
68
- t("translations.widgets.translationManager.fullManager", "Translation manager"),
69
- /* @__PURE__ */ jsx(ExternalLink, { className: "h-2.5 w-2.5" })
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 {
@@ -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 Link from 'next/link'\nimport { useParams } from 'next/navigation'\nimport { ExternalLink, Languages } from 'lucide-react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { TranslationManager } from '../../../components/TranslationManager'\nimport { extractRecordId } from '../../../lib/extract-record-id'\n\ntype WidgetContext = { entityId?: 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 hasAccess = useTranslationAccess()\n\n const recordId = React.useMemo(() => {\n if (data?.id) return String(data.id)\n if (params) return extractRecordId(params as Record<string, string | string[]>)\n return undefined\n }, [data?.id, params])\n\n if (!entityType || !recordId || !hasAccess) return null\n\n return (\n <div className=\"space-y-3\">\n <TranslationManager\n mode=\"embedded\"\n compact\n entityType={entityType}\n recordId={recordId}\n baseValues={data}\n />\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 border-t pt-3\">\n <Link\n href={`/backend/entities/system/${entityType}`}\n className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n <Languages className=\"h-3 w-3\" />\n {t('translations.widgets.translationManager.customFieldLabels', 'Custom fields translations')}\n <ExternalLink className=\"h-2.5 w-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=\"h-3 w-3\" />\n {t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n <ExternalLink className=\"h-2.5 w-2.5\" />\n </Link>\n </div>\n </div>\n )\n}\n"],
5
- "mappings": ";AA8CM,cAQE,YARF;AA5CN,YAAY,WAAW;AACvB,OAAO,UAAU;AACjB,SAAS,iBAAiB;AAC1B,SAAS,cAAc,iBAAiB;AAExC,SAAS,YAAY;AACrB,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAKhC,SAAS,uBAAgC;AACvC,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,KAAK;AACtD,QAAM,UAAU,MAAM;AACpB,QAAI,UAAU;AAId,UAAM,cAAgB,OAAe,qBAAsC;AAC3E,gBAAY,6BAA6B,EAAE,aAAa,UAAU,CAAC,EAChE,KAAK,CAAC,QAAQ;AAAE,UAAI,QAAS,cAAa,IAAI,EAAE;AAAA,IAAE,CAAC,EACnD,MAAM,MAAM;AAAE,UAAI,QAAS,cAAa,KAAK;AAAA,IAAE,CAAC;AACnD,WAAO,MAAM;AAAE,gBAAU;AAAA,IAAM;AAAA,EACjC,GAAG,CAAC,CAAC;AACL,SAAO;AACT;AAEe,SAAR,kBAAmC,EAAE,SAAS,KAAK,GAA6D;AACrH,QAAM,aAAa,SAAS;AAC5B,QAAM,SAAS,UAAU;AACzB,QAAM,IAAI,KAAK;AACf,QAAM,YAAY,qBAAqB;AAEvC,QAAM,WAAW,MAAM,QAAQ,MAAM;AACnC,QAAI,MAAM,GAAI,QAAO,OAAO,KAAK,EAAE;AACnC,QAAI,OAAQ,QAAO,gBAAgB,MAA2C;AAC9E,WAAO;AAAA,EACT,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC;AAErB,MAAI,CAAC,cAAc,CAAC,YAAY,CAAC,UAAW,QAAO;AAEnD,SACE,qBAAC,SAAI,WAAU,aACb;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,YAAY;AAAA;AAAA,IACd;AAAA,IACA,qBAAC,SAAI,WAAU,gDACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,4BAA4B,UAAU;AAAA,UAC5C,WAAU;AAAA,UAEV;AAAA,gCAAC,aAAU,WAAU,WAAU;AAAA,YAC9B,EAAE,6DAA6D,4BAA4B;AAAA,YAC5F,oBAAC,gBAAa,WAAU,eAAc;AAAA;AAAA;AAAA,MACxC;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,WAAU;AAAA,UAEV;AAAA,gCAAC,aAAU,WAAU,WAAU;AAAA,YAC9B,EAAE,uDAAuD,qBAAqB;AAAA,YAC/E,oBAAC,gBAAa,WAAU,eAAc;AAAA;AAAA;AAAA,MACxC;AAAA,OACF;AAAA,KACF;AAEJ;",
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 { translatableFields as catalogFields } from "../../catalog/translations.js";
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
- const allFields = {
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) continue;
24
- const fullSpot = `crud-form:${module}.${entitySlug}`;
25
- table[fullSpot] = [{ ...ENTRY_TEMPLATE }];
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
- const shortSpot = `crud-form:${module}.${entitySlug.slice(prefix.length)}`;
29
- if (shortSpot !== fullSpot) {
30
- table[shortSpot] = [{ ...ENTRY_TEMPLATE }];
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 = table;
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 { translatableFields as catalogFields } from '../../catalog/translations'\nimport { translatableFields as dictionaryFields } from '../../dictionaries/translations'\nimport { translatableFields as entitiesFields } from '../../entities/translations'\nimport { translatableFields as resourcesFields } from '../../resources/translations'\n\nconst WIDGET_ID = 'translations.injection.translation-manager'\n\nconst allFields: Record<string, string[]> = {\n ...catalogFields,\n ...dictionaryFields,\n ...entitiesFields,\n ...resourcesFields,\n}\n\nconst ENTRY_TEMPLATE = {\n widgetId: WIDGET_ID,\n kind: 'group' as const,\n column: 2 as const,\n groupLabel: 'translations.widgets.translationManager.groupLabel',\n groupDescription: 'translations.widgets.translationManager.groupDescription',\n priority: 40,\n}\n\nconst table: ModuleInjectionTable = {}\nfor (const entityType of Object.keys(allFields)) {\n const [module, entitySlug] = entityType.split(':')\n if (!module || !entitySlug) continue\n\n // Full form: crud-form:catalog.catalog_product (auto-generated by CrudForm from entityId)\n const fullSpot = `crud-form:${module}.${entitySlug}`\n table[fullSpot] = [{ ...ENTRY_TEMPLATE }]\n\n // Short form: crud-form:catalog.product (hardcoded in some pages)\n const prefix = `${module}_`\n if (entitySlug.startsWith(prefix)) {\n const shortSpot = `crud-form:${module}.${entitySlug.slice(prefix.length)}`\n if (shortSpot !== fullSpot) {\n table[shortSpot] = [{ ...ENTRY_TEMPLATE }]\n }\n }\n}\n\nexport const injectionTable: ModuleInjectionTable = table\nexport default injectionTable\n"],
5
- "mappings": "AACA,SAAS,sBAAsB,qBAAqB;AACpD,SAAS,sBAAsB,wBAAwB;AACvD,SAAS,sBAAsB,sBAAsB;AACrD,SAAS,sBAAsB,uBAAuB;AAEtD,MAAM,YAAY;AAElB,MAAM,YAAsC;AAAA,EAC1C,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AAEA,MAAM,iBAAiB;AAAA,EACrB,UAAU;AAAA,EACV,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,UAAU;AACZ;AAEA,MAAM,QAA8B,CAAC;AACrC,WAAW,cAAc,OAAO,KAAK,SAAS,GAAG;AAC/C,QAAM,CAAC,QAAQ,UAAU,IAAI,WAAW,MAAM,GAAG;AACjD,MAAI,CAAC,UAAU,CAAC,WAAY;AAG5B,QAAM,WAAW,aAAa,MAAM,IAAI,UAAU;AAClD,QAAM,QAAQ,IAAI,CAAC,EAAE,GAAG,eAAe,CAAC;AAGxC,QAAM,SAAS,GAAG,MAAM;AACxB,MAAI,WAAW,WAAW,MAAM,GAAG;AACjC,UAAM,YAAY,aAAa,MAAM,IAAI,WAAW,MAAM,OAAO,MAAM,CAAC;AACxE,QAAI,cAAc,UAAU;AAC1B,YAAM,SAAS,IAAI,CAAC,EAAE,GAAG,eAAe,CAAC;AAAA,IAC3C;AAAA,EACF;AACF;AAEO,MAAM,iBAAuC;AACpD,IAAO,0BAAQ;",
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-4849712ccb",
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-4849712ccb",
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 dd = roots.find((it: Entry) => it.groupKey === 'entities.nav.group' && it.titleKey === 'entities.nav.userEntities')
160
- if (dd) {
161
- const existing = dd.children || []
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: dd.groupId,
164
- groupName: dd.groupName,
165
- groupKey: dd.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
- dd.children = Array.from(byHref.values())
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,5 @@
1
+ export const translatableFields: Record<string, string[]> = {
2
+ 'staff:staff_team_member': ['display_name', 'description'],
3
+ }
4
+
5
+ export default translatableFields
@@ -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
- export function extractRecordId(params: Record<string, string | string[]>): string | undefined {
2
- if (params.id) return String(Array.isArray(params.id) ? params.id[0] : params.id)
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 && /^[0-9a-f-]{20,}$/i.test(seg)) return 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