@open-mercato/core 0.6.6-develop.6317.1.b3be10ab84 → 0.6.6-develop.6330.1.a261878aa8

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 (67) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +7 -12
  3. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
  4. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +5 -2
  5. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  6. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -1
  7. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  8. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js +2 -1
  9. package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js.map +2 -2
  10. package/dist/modules/customers/components/detail/EntityTagsDialog.js +76 -67
  11. package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +3 -3
  12. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +4 -3
  13. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  14. package/dist/modules/data_sync/api/runs.js +1 -0
  15. package/dist/modules/data_sync/api/runs.js.map +2 -2
  16. package/dist/modules/data_sync/backend/data-sync/page.js +3 -1
  17. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  18. package/dist/modules/data_sync/data/validators.js +1 -0
  19. package/dist/modules/data_sync/data/validators.js.map +2 -2
  20. package/dist/modules/data_sync/lib/sync-run-service.js +20 -0
  21. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  22. package/dist/modules/entities/api/records.js +11 -1
  23. package/dist/modules/entities/api/records.js.map +2 -2
  24. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +17 -10
  25. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  26. package/dist/modules/messages/api/unread-count/route.js +32 -0
  27. package/dist/modules/messages/api/unread-count/route.js.map +2 -2
  28. package/dist/modules/query_index/components/QueryIndexesTable.js +13 -5
  29. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  30. package/dist/modules/shipping_carriers/i18n/en.js +11 -1
  31. package/dist/modules/shipping_carriers/i18n/en.js.map +2 -2
  32. package/dist/modules/shipping_carriers/i18n/pl.js +11 -1
  33. package/dist/modules/shipping_carriers/i18n/pl.js.map +2 -2
  34. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js +25 -0
  35. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js.map +7 -0
  36. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js +3 -17
  37. package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js.map +2 -2
  38. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +19 -14
  39. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
  40. package/package.json +7 -7
  41. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +7 -10
  42. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +6 -7
  43. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +3 -2
  44. package/src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx +3 -2
  45. package/src/modules/customers/components/detail/EntityTagsDialog.tsx +89 -71
  46. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +6 -3
  47. package/src/modules/data_sync/api/runs.ts +1 -0
  48. package/src/modules/data_sync/backend/data-sync/page.tsx +3 -1
  49. package/src/modules/data_sync/data/validators.ts +1 -0
  50. package/src/modules/data_sync/i18n/de.json +1 -0
  51. package/src/modules/data_sync/i18n/en.json +1 -0
  52. package/src/modules/data_sync/i18n/es.json +1 -0
  53. package/src/modules/data_sync/i18n/pl.json +1 -0
  54. package/src/modules/data_sync/lib/sync-run-service.ts +23 -0
  55. package/src/modules/entities/api/records.ts +17 -1
  56. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +25 -12
  57. package/src/modules/messages/api/unread-count/route.ts +40 -0
  58. package/src/modules/query_index/components/QueryIndexesTable.tsx +17 -15
  59. package/src/modules/shipping_carriers/i18n/de.json +11 -1
  60. package/src/modules/shipping_carriers/i18n/en.json +11 -1
  61. package/src/modules/shipping_carriers/i18n/en.ts +10 -0
  62. package/src/modules/shipping_carriers/i18n/es.json +11 -1
  63. package/src/modules/shipping_carriers/i18n/pl.json +11 -1
  64. package/src/modules/shipping_carriers/i18n/pl.ts +10 -0
  65. package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.tsx +35 -0
  66. package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.tsx +3 -22
  67. package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +30 -13
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.tsx"],
4
- "sourcesContent": ["import type { InjectionColumnWidget } from '@open-mercato/shared/modules/widgets/injection'\n\nconst SHIPPING_STATUS_COLORS: Record<string, string> = {\n pending: 'bg-gray-100 text-gray-700',\n label_created: 'bg-slate-100 text-slate-700',\n picked_up: 'bg-sky-100 text-sky-700',\n in_transit: 'bg-blue-100 text-blue-700',\n out_for_delivery: 'bg-indigo-100 text-indigo-700',\n delivered: 'bg-green-100 text-green-700',\n returned: 'bg-amber-100 text-amber-700',\n cancelled: 'bg-red-100 text-red-700',\n failed_delivery: 'bg-orange-100 text-orange-700',\n}\n\nfunction formatShippingStatusLabel(value: string): string {\n return value.replace(/_/g, ' ')\n}\n\nconst widget: InjectionColumnWidget = {\n metadata: {\n id: 'shipping_carriers.injection.tracking-status-badge',\n priority: 45,\n },\n columns: [\n {\n id: 'carrier_status_badge',\n header: 'shipping_carriers.column.shippingStatus',\n accessorKey: '_carrier.status',\n sortable: false,\n cell: ({ getValue }) => {\n const value = getValue()\n if (typeof value !== 'string' || value.length === 0) return null\n const colors = SHIPPING_STATUS_COLORS[value] ?? 'bg-gray-100 text-gray-700'\n return (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${colors}`}>\n {formatShippingStatusLabel(value)}\n </span>\n )\n },\n },\n ],\n}\n\nexport default widget\n"],
5
- "mappings": "AAkCU;AAhCV,MAAM,yBAAiD;AAAA,EACrD,SAAS;AAAA,EACT,eAAe;AAAA,EACf,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX,UAAU;AAAA,EACV,WAAW;AAAA,EACX,iBAAiB;AACnB;AAEA,SAAS,0BAA0B,OAAuB;AACxD,SAAO,MAAM,QAAQ,MAAM,GAAG;AAChC;AAEA,MAAM,SAAgC;AAAA,EACpC,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,UAAU;AAAA,EACZ;AAAA,EACA,SAAS;AAAA,IACP;AAAA,MACE,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,UAAU;AAAA,MACV,MAAM,CAAC,EAAE,SAAS,MAAM;AACtB,cAAM,QAAQ,SAAS;AACvB,YAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,cAAM,SAAS,uBAAuB,KAAK,KAAK;AAChD,eACE,oBAAC,UAAK,WAAW,yEAAyE,MAAM,IAC7F,oCAA0B,KAAK,GAClC;AAAA,MAEJ;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;",
4
+ "sourcesContent": ["import * as React from 'react'\nimport type { InjectionColumnWidget } from '@open-mercato/shared/modules/widgets/injection'\nimport ShippingStatusBadgeWidget from './widget.client'\n\nconst widget: InjectionColumnWidget = {\n metadata: {\n id: 'shipping_carriers.injection.tracking-status-badge',\n priority: 45,\n },\n columns: [\n {\n id: 'carrier_status_badge',\n header: 'shipping_carriers.column.shippingStatus',\n accessorKey: '_carrier.status',\n sortable: false,\n cell: ({ getValue }) => {\n const value = getValue()\n if (typeof value !== 'string' || value.length === 0) return null\n return React.createElement(ShippingStatusBadgeWidget, { status: value })\n },\n },\n ],\n}\n\nexport default widget\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;AAEvB,OAAO,+BAA+B;AAEtC,MAAM,SAAgC;AAAA,EACpC,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,UAAU;AAAA,EACZ;AAAA,EACA,SAAS;AAAA,IACP;AAAA,MACE,IAAI;AAAA,MACJ,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,UAAU;AAAA,MACV,MAAM,CAAC,EAAE,SAAS,MAAM;AACtB,cAAM,QAAQ,SAAS;AACvB,YAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,eAAO,MAAM,cAAc,2BAA2B,EAAE,QAAQ,MAAM,CAAC;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;",
6
6
  "names": []
7
7
  }
@@ -3,8 +3,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useParams } from "next/navigation";
5
5
  import Link from "next/link";
6
+ import { useQuery } from "@tanstack/react-query";
6
7
  import { ExternalLink, Languages } from "lucide-react";
7
8
  import { useT } from "@open-mercato/shared/lib/i18n/context";
9
+ import { hasAllFeatures } from "@open-mercato/shared/lib/auth/featureMatch";
10
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
8
11
  import { Button } from "@open-mercato/ui/primitives/button";
9
12
  import {
10
13
  Drawer,
@@ -16,21 +19,23 @@ import {
16
19
  } from "@open-mercato/ui/primitives/drawer";
17
20
  import { TranslationManager } from "../../../components/TranslationManager.js";
18
21
  import { extractRecordId } from "../../../lib/extract-record-id.js";
22
+ const TRANSLATION_ACCESS_FEATURES = ["translations.view"];
19
23
  function useTranslationAccess() {
20
- const [hasAccess, setHasAccess] = React.useState(false);
21
- React.useEffect(() => {
22
- let mounted = true;
23
- const nativeFetch = window.__omOriginalFetch || fetch;
24
- nativeFetch("/api/translations/locales", { credentials: "include" }).then((res) => {
25
- if (mounted) setHasAccess(res.ok);
26
- }).catch(() => {
27
- if (mounted) setHasAccess(false);
28
- });
29
- return () => {
30
- mounted = false;
31
- };
32
- }, []);
33
- return hasAccess;
24
+ const { data } = useQuery({
25
+ queryKey: ["translations", "widget-access", ...TRANSLATION_ACCESS_FEATURES],
26
+ queryFn: async () => {
27
+ const res = await apiCall("/api/auth/feature-check", {
28
+ method: "POST",
29
+ headers: { "content-type": "application/json" },
30
+ body: JSON.stringify({ features: TRANSLATION_ACCESS_FEATURES })
31
+ });
32
+ if (!res.ok) return false;
33
+ return hasAllFeatures(TRANSLATION_ACCESS_FEATURES, res.result?.granted ?? []);
34
+ },
35
+ staleTime: 5 * 60 * 1e3,
36
+ retry: false
37
+ });
38
+ return data === true;
34
39
  }
35
40
  function TranslationWidget({ context, data }) {
36
41
  const entityType = context?.entityId;
@@ -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 { useParams } from 'next/navigation'\nimport Link from 'next/link'\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 { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Drawer,\n DrawerBody,\n DrawerContent,\n DrawerDescription,\n DrawerHeader,\n DrawerTitle,\n} from '@open-mercato/ui/primitives/drawer'\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 if (!canRender) return null\n\n return (\n <Drawer open={open} onOpenChange={setOpen}>\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 <DrawerContent className=\"max-w-4xl\">\n <DrawerHeader>\n <DrawerTitle>\n {t('translations.widgets.translationManager.groupLabel', 'Translations')}\n </DrawerTitle>\n <DrawerDescription>\n {t(\n 'translations.widgets.translationManager.groupDescription',\n 'Manage translations for this record across supported locales.',\n )}\n </DrawerDescription>\n </DrawerHeader>\n <DrawerBody>\n <TranslationManager\n mode=\"embedded\"\n compact\n entityType={entityType}\n recordId={recordId}\n baseValues={data}\n />\n </DrawerBody>\n {/* Custom footer: 2 inline link affordances (not action buttons),\n wrap-friendly + left-aligned. DS DrawerFooter layouts assume\n buttons in the trailing slot, so we hand-roll the row with\n the DrawerFooter padding convention (`px-6 pt-4 pb-5`). */}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 px-6 pt-4 pb-5\">\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 </DrawerContent>\n </Drawer>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useParams } from 'next/navigation'\nimport Link from 'next/link'\nimport { useQuery } from '@tanstack/react-query'\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 { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Drawer,\n DrawerBody,\n DrawerContent,\n DrawerDescription,\n DrawerHeader,\n DrawerTitle,\n} from '@open-mercato/ui/primitives/drawer'\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\ntype FeatureCheckResponse = {\n ok: boolean\n granted: string[]\n userId: string\n}\n\nconst TRANSLATION_ACCESS_FEATURES = ['translations.view']\n\n// Probe access through the shared apiCall/React Query path so multiple injected\n// widget instances dedupe a single request via the query key. feature-check\n// always answers 200 with the granted feature list, so a user without\n// translations.view is hidden gracefully \u2014 no login redirect, no forbidden flash.\nfunction useTranslationAccess(): boolean {\n const { data } = useQuery<boolean>({\n queryKey: ['translations', 'widget-access', ...TRANSLATION_ACCESS_FEATURES],\n queryFn: async () => {\n const res = await apiCall<FeatureCheckResponse>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: TRANSLATION_ACCESS_FEATURES }),\n })\n if (!res.ok) return false\n return hasAllFeatures(TRANSLATION_ACCESS_FEATURES, res.result?.granted ?? [])\n },\n staleTime: 5 * 60 * 1000,\n retry: false,\n })\n return data === true\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 if (!canRender) return null\n\n return (\n <Drawer open={open} onOpenChange={setOpen}>\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 <DrawerContent className=\"max-w-4xl\">\n <DrawerHeader>\n <DrawerTitle>\n {t('translations.widgets.translationManager.groupLabel', 'Translations')}\n </DrawerTitle>\n <DrawerDescription>\n {t(\n 'translations.widgets.translationManager.groupDescription',\n 'Manage translations for this record across supported locales.',\n )}\n </DrawerDescription>\n </DrawerHeader>\n <DrawerBody>\n <TranslationManager\n mode=\"embedded\"\n compact\n entityType={entityType}\n recordId={recordId}\n baseValues={data}\n />\n </DrawerBody>\n {/* Custom footer: 2 inline link affordances (not action buttons),\n wrap-friendly + left-aligned. DS DrawerFooter layouts assume\n buttons in the trailing slot, so we hand-roll the row with\n the DrawerFooter padding convention (`px-6 pt-4 pb-5`). */}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 px-6 pt-4 pb-5\">\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 </DrawerContent>\n </Drawer>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6317.1.b3be10ab84",
3
+ "version": "0.6.6-develop.6330.1.a261878aa8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -246,16 +246,16 @@
246
246
  "zod": "^4.4.3"
247
247
  },
248
248
  "peerDependencies": {
249
- "@open-mercato/ai-assistant": "0.6.6-develop.6317.1.b3be10ab84",
250
- "@open-mercato/shared": "0.6.6-develop.6317.1.b3be10ab84",
251
- "@open-mercato/ui": "0.6.6-develop.6317.1.b3be10ab84",
249
+ "@open-mercato/ai-assistant": "0.6.6-develop.6330.1.a261878aa8",
250
+ "@open-mercato/shared": "0.6.6-develop.6330.1.a261878aa8",
251
+ "@open-mercato/ui": "0.6.6-develop.6330.1.a261878aa8",
252
252
  "react": "^19.0.0",
253
253
  "react-dom": "^19.0.0"
254
254
  },
255
255
  "devDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6317.1.b3be10ab84",
257
- "@open-mercato/shared": "0.6.6-develop.6317.1.b3be10ab84",
258
- "@open-mercato/ui": "0.6.6-develop.6317.1.b3be10ab84",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6330.1.a261878aa8",
257
+ "@open-mercato/shared": "0.6.6-develop.6330.1.a261878aa8",
258
+ "@open-mercato/ui": "0.6.6-develop.6330.1.a261878aa8",
259
259
  "@testing-library/dom": "^10.4.1",
260
260
  "@testing-library/jest-dom": "^6.9.1",
261
261
  "@testing-library/react": "^16.3.1",
@@ -7,6 +7,7 @@ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
7
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
8
8
  import type { CrudField, CrudFormGroup, CrudFormGroupComponentProps } from '@open-mercato/ui/backend/CrudForm'
9
9
  import { Button } from '@open-mercato/ui/primitives/button'
10
+ import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
10
11
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
11
12
  import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
12
13
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
@@ -106,12 +107,9 @@ function PortalPermissionsEditor({ values, setValue }: CrudFormGroupComponentPro
106
107
  <div className="flex items-center justify-between border-b px-4 py-3">
107
108
  <span className="text-sm font-semibold">{t(group.labelKey, group.fallback)}</span>
108
109
  <label className="flex items-center gap-2 text-xs text-muted-foreground">
109
- <input
110
- type="checkbox"
111
- checked={allSelected}
112
- ref={(el) => { if (el) el.indeterminate = someSelected && !allSelected }}
113
- onChange={() => handleGroupToggle(groupFeatures)}
114
- className="rounded border-border"
110
+ <Checkbox
111
+ checked={allSelected ? true : (someSelected ? 'indeterminate' : false)}
112
+ onCheckedChange={() => handleGroupToggle(groupFeatures)}
115
113
  />
116
114
  {t('customer_accounts.admin.roleDetail.selectAll', 'Select all')}
117
115
  </label>
@@ -121,11 +119,10 @@ function PortalPermissionsEditor({ values, setValue }: CrudFormGroupComponentPro
121
119
  const feature = PORTAL_FEATURES.find((portalFeature) => portalFeature.id === featureId)
122
120
  return (
123
121
  <label key={featureId} className="flex items-start gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors">
124
- <input
125
- type="checkbox"
122
+ <Checkbox
126
123
  checked={features.includes(featureId)}
127
- onChange={() => handleFeatureToggle(featureId)}
128
- className="mt-0.5 rounded border-border"
124
+ onCheckedChange={() => handleFeatureToggle(featureId)}
125
+ className="mt-0.5"
129
126
  />
130
127
  <div className="space-y-0.5">
131
128
  <div className="text-sm font-medium">{feature ? t(feature.labelKey, feature.fallback) : featureId}</div>
@@ -191,20 +191,19 @@ function CreateUserDialog({
191
191
  {roleOptions.map((role) => {
192
192
  const isSelected = selectedRoleIds.includes(role.id)
193
193
  return (
194
- <button
194
+ <Button
195
195
  key={role.id}
196
196
  type="button"
197
+ size="2xs"
198
+ variant={isSelected ? 'secondary' : 'outline'}
199
+ aria-pressed={isSelected}
197
200
  onClick={() => setSelectedRoleIds((prev) =>
198
201
  prev.includes(role.id) ? prev.filter((rid) => rid !== role.id) : [...prev, role.id],
199
202
  )}
200
- className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors ${
201
- isSelected
202
- ? 'border-primary bg-primary/10 text-primary'
203
- : 'border-border bg-background text-muted-foreground hover:bg-muted'
204
- }`}
203
+ className="rounded-full"
205
204
  >
206
205
  {role.label}
207
- </button>
206
+ </Button>
208
207
  )
209
208
  })}
210
209
  </div>
@@ -8,6 +8,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
8
8
  import { Button } from '@open-mercato/ui/primitives/button'
9
9
  import { Input } from '@open-mercato/ui/primitives/input'
10
10
  import { EmailInput } from '@open-mercato/ui/primitives/email-input'
11
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
11
12
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
12
13
 
13
14
  interface AccountStatusData {
@@ -305,9 +306,9 @@ export default function AccountStatusWidget({ context }: AccountStatusProps) {
305
306
  <div className="space-y-1 text-sm">
306
307
  <div className="flex justify-between">
307
308
  <span className="text-muted-foreground">{t('common.status', 'Status')}</span>
308
- <span className={data.isActive ? 'text-green-600' : 'text-red-600'}>
309
+ <StatusBadge variant={data.isActive ? 'success' : 'error'} dot>
309
310
  {data.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}
310
- </span>
311
+ </StatusBadge>
311
312
  </div>
312
313
  <div className="flex justify-between">
313
314
  <span className="text-muted-foreground">{t('common.email', 'Email')}</span>
@@ -3,6 +3,7 @@
3
3
  import { useQuery } from '@tanstack/react-query'
4
4
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
6
7
 
7
8
  interface CompanyUser {
8
9
  id: string
@@ -59,9 +60,9 @@ export default function CompanyUsersWidget({ context }: CompanyUsersProps) {
59
60
  <div className="text-xs text-muted-foreground">{user.email}</div>
60
61
  </div>
61
62
  <div className="flex items-center gap-2">
62
- <span className={`text-xs ${user.isActive ? 'text-green-600' : 'text-red-600'}`}>
63
+ <StatusBadge variant={user.isActive ? 'success' : 'error'} dot>
63
64
  {user.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}
64
- </span>
65
+ </StatusBadge>
65
66
  <a
66
67
  href={`/backend/customer_accounts/users/${user.id}`}
67
68
  className="text-xs text-primary hover:underline"
@@ -438,53 +438,49 @@ export function EntityTagsDialog({
438
438
  const loadData = React.useCallback(async () => {
439
439
  setLoading(true)
440
440
  try {
441
- let kindSettings: KindSetting[] = []
442
441
  const scopedQuery = new URLSearchParams()
443
442
  if (entityOrganizationId) {
444
443
  scopedQuery.set('organizationId', entityOrganizationId)
445
444
  }
446
- try {
447
- const settingsCall = await apiCall<{ items?: KindSetting[] }>(
448
- `/api/customers/dictionaries/kind-settings${scopedQuery.size ? `?${scopedQuery.toString()}` : ''}`,
449
- { cache: 'no-store', headers: { 'x-om-unauthorized-redirect': '0' } },
450
- )
451
- if (settingsCall.ok && settingsCall.result?.items) {
452
- kindSettings = settingsCall.result.items
445
+
446
+ const loadKindSettings = async (): Promise<KindSetting[]> => {
447
+ try {
448
+ const settingsCall = await apiCall<{ items?: KindSetting[] }>(
449
+ `/api/customers/dictionaries/kind-settings${scopedQuery.size ? `?${scopedQuery.toString()}` : ''}`,
450
+ { cache: 'no-store', headers: { 'x-om-unauthorized-redirect': '0' } },
451
+ )
452
+ if (settingsCall.ok && settingsCall.result?.items) {
453
+ return settingsCall.result.items
454
+ }
455
+ } catch {
456
+ // Default category order works without explicit settings rows.
453
457
  }
454
- } catch {
455
- // Default category order works without explicit settings rows.
458
+ return []
456
459
  }
457
460
 
458
- const settingsMap = new Map(kindSettings.map((setting) => [setting.kind, setting]))
459
-
460
- const selectedTagEntries = Array.isArray(entityData.tags)
461
- ? entityData.tags.map((tag) => ({
462
- id: tag.id,
463
- value: tag.id,
464
- label: tag.label,
465
- color: tag.color ?? null,
466
- }))
467
- : []
468
-
469
- let assignedLabelIds: string[] = []
470
- let selectedLabelEntries: CategoryOption[] = []
471
- try {
472
- const labelsQuery = new URLSearchParams()
473
- labelsQuery.set('entityId', entityId)
474
- labelsQuery.set('pageSize', '1')
475
- if (entityOrganizationId) {
476
- labelsQuery.set('organizationId', entityOrganizationId)
477
- }
478
- const labelsCall = await apiCall<{
479
- items?: LabelItem[]
480
- assignedIds?: string[]
481
- }>(`/api/customers/labels?${labelsQuery.toString()}`, {
482
- cache: 'no-store',
483
- headers: { 'x-om-unauthorized-redirect': '0' },
484
- })
485
- const labelsData = labelsCall.ok ? labelsCall.result : null
486
- assignedLabelIds = labelsData?.assignedIds ?? []
487
- if (assignedLabelIds.length > 0) {
461
+ const loadLabelData = async (): Promise<{
462
+ assignedLabelIds: string[]
463
+ selectedLabelEntries: CategoryOption[]
464
+ }> => {
465
+ try {
466
+ const labelsQuery = new URLSearchParams()
467
+ labelsQuery.set('entityId', entityId)
468
+ labelsQuery.set('pageSize', '1')
469
+ if (entityOrganizationId) {
470
+ labelsQuery.set('organizationId', entityOrganizationId)
471
+ }
472
+ const labelsCall = await apiCall<{
473
+ items?: LabelItem[]
474
+ assignedIds?: string[]
475
+ }>(`/api/customers/labels?${labelsQuery.toString()}`, {
476
+ cache: 'no-store',
477
+ headers: { 'x-om-unauthorized-redirect': '0' },
478
+ })
479
+ const labelsData = labelsCall.ok ? labelsCall.result : null
480
+ const assignedLabelIds = labelsData?.assignedIds ?? []
481
+ if (assignedLabelIds.length === 0) {
482
+ return { assignedLabelIds, selectedLabelEntries: [] }
483
+ }
488
484
  const detailQuery = new URLSearchParams({
489
485
  ids: assignedLabelIds.join(','),
490
486
  pageSize: String(Math.min(assignedLabelIds.length, 100)),
@@ -500,44 +496,36 @@ export function EntityTagsDialog({
500
496
  },
501
497
  )
502
498
  const selectedLabels = selectedLabelsCall.ok ? selectedLabelsCall.result?.items ?? [] : []
503
- selectedLabelEntries = selectedLabels.map((label) => ({
499
+ const selectedLabelEntries = selectedLabels.map((label) => ({
504
500
  id: label.id,
505
501
  value: label.id,
506
502
  label: label.label,
507
503
  color: null,
508
504
  }))
505
+ return { assignedLabelIds, selectedLabelEntries }
506
+ } catch {
507
+ return { assignedLabelIds: [], selectedLabelEntries: [] }
509
508
  }
510
- } catch {
511
- assignedLabelIds = []
512
- selectedLabelEntries = []
513
509
  }
514
510
 
515
- const categoryDefs = buildApplicableCategories(entityType, kindSettings)
516
- const loadedCategories: CategorySection[] = []
511
+ const selectedTagEntries = Array.isArray(entityData.tags)
512
+ ? entityData.tags.map((tag) => ({
513
+ id: tag.id,
514
+ value: tag.id,
515
+ label: tag.label,
516
+ color: tag.color ?? null,
517
+ }))
518
+ : []
517
519
 
518
- for (const categoryDef of categoryDefs) {
519
- if (categoryDef.source === 'tags') {
520
- loadedCategories.push({
521
- ...categoryDef,
522
- label: t(categoryDef.labelKey, categoryDef.labelFallback),
523
- description: t(categoryDef.descriptionKey, categoryDef.descriptionFallback),
524
- entries: sortOptions(selectedTagEntries),
525
- selectionMode: categoryDef.selectionMode ?? 'multi',
526
- })
527
- continue
528
- }
520
+ // The label chain is independent of kind settings and the dictionary
521
+ // fan-out, so kick it off immediately and let it resolve in parallel.
522
+ const labelDataPromise = loadLabelData()
529
523
 
530
- if (categoryDef.source === 'labels') {
531
- loadedCategories.push({
532
- ...categoryDef,
533
- label: t(categoryDef.labelKey, categoryDef.labelFallback),
534
- description: t(categoryDef.descriptionKey, categoryDef.descriptionFallback),
535
- entries: sortOptions(selectedLabelEntries),
536
- selectionMode: categoryDef.selectionMode ?? 'multi',
537
- })
538
- continue
539
- }
524
+ const kindSettings = await loadKindSettings()
525
+ const settingsMap = new Map(kindSettings.map((setting) => [setting.kind, setting]))
526
+ const categoryDefs = buildApplicableCategories(entityType, kindSettings)
540
527
 
528
+ const loadDictionaryCategory = async (categoryDef: CategoryDef): Promise<CategorySection> => {
541
529
  try {
542
530
  const dictionaryUrl = new URL(`/api/customers/dictionaries/${categoryDef.routeKind}`, 'http://localhost')
543
531
  if (entityOrganizationId) {
@@ -568,7 +556,7 @@ export function EntityTagsDialog({
568
556
  color: null,
569
557
  })
570
558
  })
571
- loadedCategories.push({
559
+ return {
572
560
  ...categoryDef,
573
561
  label: categoryDef.labelKey
574
562
  ? t(categoryDef.labelKey, categoryDef.labelFallback)
@@ -582,7 +570,7 @@ export function EntityTagsDialog({
582
570
  : categoryDef.descriptionFallback,
583
571
  entries: sortOptions(entries),
584
572
  selectionMode,
585
- })
573
+ }
586
574
  } catch {
587
575
  const setting = categoryDef.settingKind
588
576
  ? settingsMap.get(categoryDef.settingKind)
@@ -594,7 +582,7 @@ export function EntityTagsDialog({
594
582
  label: value,
595
583
  color: null,
596
584
  }))
597
- loadedCategories.push({
585
+ return {
598
586
  ...categoryDef,
599
587
  label: categoryDef.labelKey
600
588
  ? t(categoryDef.labelKey, categoryDef.labelFallback)
@@ -608,10 +596,40 @@ export function EntityTagsDialog({
608
596
  : categoryDef.descriptionFallback,
609
597
  entries: fallbackEntries,
610
598
  selectionMode,
611
- })
599
+ }
612
600
  }
613
601
  }
614
602
 
603
+ // Independent dictionary requests fan out together instead of awaiting
604
+ // each one sequentially; Promise.all preserves categoryDefs order so the
605
+ // subsequent sort and selection initialization stay stable.
606
+ const loadedCategories = await Promise.all(
607
+ categoryDefs.map(async (categoryDef): Promise<CategorySection> => {
608
+ if (categoryDef.source === 'tags') {
609
+ return {
610
+ ...categoryDef,
611
+ label: t(categoryDef.labelKey, categoryDef.labelFallback),
612
+ description: t(categoryDef.descriptionKey, categoryDef.descriptionFallback),
613
+ entries: sortOptions(selectedTagEntries),
614
+ selectionMode: categoryDef.selectionMode ?? 'multi',
615
+ }
616
+ }
617
+ if (categoryDef.source === 'labels') {
618
+ const { selectedLabelEntries } = await labelDataPromise
619
+ return {
620
+ ...categoryDef,
621
+ label: t(categoryDef.labelKey, categoryDef.labelFallback),
622
+ description: t(categoryDef.descriptionKey, categoryDef.descriptionFallback),
623
+ entries: sortOptions(selectedLabelEntries),
624
+ selectionMode: categoryDef.selectionMode ?? 'multi',
625
+ }
626
+ }
627
+ return loadDictionaryCategory(categoryDef)
628
+ }),
629
+ )
630
+
631
+ const { assignedLabelIds, selectedLabelEntries } = await labelDataPromise
632
+
615
633
  loadedCategories.sort((left, right) => {
616
634
  const leftSortOrder =
617
635
  left.settingKind && settingsMap.has(left.settingKind)
@@ -193,9 +193,12 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
193
193
  setLoading(true)
194
194
  setError(null)
195
195
  try {
196
- await loadCatalog()
197
- if (kind === 'role') await loadRoleData(tenantIdRef.current, organizationIdRef.current)
198
- else await loadUserData(tenantIdRef.current, organizationIdRef.current)
196
+ await Promise.all([
197
+ loadCatalog(),
198
+ kind === 'role'
199
+ ? loadRoleData(tenantIdRef.current, organizationIdRef.current)
200
+ : loadUserData(tenantIdRef.current, organizationIdRef.current),
201
+ ])
199
202
  } catch (err) {
200
203
  console.error('Failed to load widget visibility data', err)
201
204
  if (!cancelled) {
@@ -25,6 +25,7 @@ export async function GET(req: Request) {
25
25
  entityType: url.searchParams.get('entityType') ?? undefined,
26
26
  direction: url.searchParams.get('direction') ?? undefined,
27
27
  status: url.searchParams.get('status') ?? undefined,
28
+ search: url.searchParams.get('search') ?? undefined,
28
29
  page: url.searchParams.get('page') ?? undefined,
29
30
  pageSize: url.searchParams.get('pageSize') ?? undefined,
30
31
  })
@@ -167,6 +167,7 @@ export default function SyncRunsDashboardPage() {
167
167
  params.set('pageSize', '20')
168
168
  if (filterValues.status) params.set('status', filterValues.status as string)
169
169
  if (filterValues.direction) params.set('direction', filterValues.direction as string)
170
+ if (search.trim()) params.set('search', search.trim())
170
171
  const fallback: ResponsePayload = { items: [], total: 0, page, totalPages: 1 }
171
172
  const call = await apiCall<ResponsePayload>(
172
173
  `/api/data_sync/runs?${params.toString()}`,
@@ -188,7 +189,7 @@ export default function SyncRunsDashboardPage() {
188
189
  }
189
190
  load()
190
191
  return () => { cancelled = true }
191
- }, [page, filterValues, reloadToken, scopeVersion, t])
192
+ }, [page, filterValues, search, reloadToken, scopeVersion, t])
192
193
 
193
194
  React.useEffect(() => {
194
195
  let cancelled = false
@@ -965,6 +966,7 @@ export default function SyncRunsDashboardPage() {
965
966
  onFiltersClear={handleFiltersClear}
966
967
  searchValue={search}
967
968
  onSearchChange={(value) => { setSearch(value); setPage(1) }}
969
+ searchPlaceholder={t('data_sync.dashboard.searchPlaceholder')}
968
970
  perspective={{ tableId: 'data_sync.runs' }}
969
971
  onRowClick={(row) => {
970
972
  router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`)
@@ -28,6 +28,7 @@ export const listSyncRunsQuerySchema = z.object({
28
28
  entityType: z.string().optional(),
29
29
  direction: z.enum(['import', 'export']).optional(),
30
30
  status: z.enum(['pending', 'running', 'completed', 'failed', 'cancelled', 'paused']).optional(),
31
+ search: z.string().trim().min(1).max(200).optional(),
31
32
  page: z.coerce.number().int().min(1).default(1),
32
33
  pageSize: z.coerce.number().int().min(1).max(100).default(20),
33
34
  })
@@ -47,6 +47,7 @@
47
47
  "data_sync.dashboard.schedule.timezone": "Timezone",
48
48
  "data_sync.dashboard.schedule.title": "Recurring schedule",
49
49
  "data_sync.dashboard.schedule.type": "Schedule type",
50
+ "data_sync.dashboard.searchPlaceholder": "Nach Integration, Entitätstyp, Status oder Lauf-ID suchen",
50
51
  "data_sync.dashboard.start.batchSize": "Batch Size",
51
52
  "data_sync.dashboard.start.description": "Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.",
52
53
  "data_sync.dashboard.start.error": "Failed to start sync run",
@@ -47,6 +47,7 @@
47
47
  "data_sync.dashboard.schedule.timezone": "Timezone",
48
48
  "data_sync.dashboard.schedule.title": "Recurring schedule",
49
49
  "data_sync.dashboard.schedule.type": "Schedule type",
50
+ "data_sync.dashboard.searchPlaceholder": "Search by integration, entity type, status, or run ID",
50
51
  "data_sync.dashboard.start.batchSize": "Batch Size",
51
52
  "data_sync.dashboard.start.description": "Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.",
52
53
  "data_sync.dashboard.start.error": "Failed to start sync run",
@@ -47,6 +47,7 @@
47
47
  "data_sync.dashboard.schedule.timezone": "Timezone",
48
48
  "data_sync.dashboard.schedule.title": "Recurring schedule",
49
49
  "data_sync.dashboard.schedule.type": "Schedule type",
50
+ "data_sync.dashboard.searchPlaceholder": "Buscar por integración, tipo de entidad, estado o ID de ejecución",
50
51
  "data_sync.dashboard.start.batchSize": "Batch Size",
51
52
  "data_sync.dashboard.start.description": "Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.",
52
53
  "data_sync.dashboard.start.error": "Failed to start sync run",
@@ -47,6 +47,7 @@
47
47
  "data_sync.dashboard.schedule.timezone": "Timezone",
48
48
  "data_sync.dashboard.schedule.title": "Recurring schedule",
49
49
  "data_sync.dashboard.schedule.type": "Schedule type",
50
+ "data_sync.dashboard.searchPlaceholder": "Szukaj po integracji, typie encji, statusie lub ID przebiegu",
50
51
  "data_sync.dashboard.start.batchSize": "Batch Size",
51
52
  "data_sync.dashboard.start.description": "Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.",
52
53
  "data_sync.dashboard.start.error": "Failed to start sync run",
@@ -1,8 +1,26 @@
1
1
  import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
2
2
  import { findAndCountWithDecryption, findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
3
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
4
+ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
4
5
  import { SyncCursor, SyncRun } from '../data/entities'
5
6
 
7
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
8
+
9
+ function buildRunSearchFilter(search: string): FilterQuery<SyncRun>[] | null {
10
+ const trimmed = search.trim()
11
+ if (!trimmed) return null
12
+ const pattern = `%${escapeLikePattern(trimmed)}%`
13
+ const conditions: FilterQuery<SyncRun>[] = [
14
+ { integrationId: { $ilike: pattern } },
15
+ { entityType: { $ilike: pattern } },
16
+ { status: { $ilike: pattern } },
17
+ ]
18
+ if (UUID_PATTERN.test(trimmed)) {
19
+ conditions.push({ id: trimmed })
20
+ }
21
+ return conditions
22
+ }
23
+
6
24
  type SyncScope = {
7
25
  organizationId: string
8
26
  tenantId: string
@@ -89,6 +107,7 @@ export function createSyncRunService(em: EntityManager) {
89
107
  entityType?: string
90
108
  direction?: 'import' | 'export'
91
109
  status?: string
110
+ search?: string
92
111
  page: number
93
112
  pageSize: number
94
113
  }, scope: SyncScope): Promise<{ items: SyncRun[]; total: number }> {
@@ -102,6 +121,10 @@ export function createSyncRunService(em: EntityManager) {
102
121
  if (query.entityType) where.entityType = query.entityType
103
122
  if (query.direction) where.direction = query.direction
104
123
  if (query.status) where.status = query.status as SyncRun['status']
124
+ if (query.search) {
125
+ const searchConditions = buildRunSearchFilter(query.search)
126
+ if (searchConditions) where.$or = searchConditions
127
+ }
105
128
 
106
129
  const [items, total] = await findAndCountWithDecryption(
107
130
  em,