@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +7 -12
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +5 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -1
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js +2 -1
- package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js.map +2 -2
- package/dist/modules/customers/components/detail/EntityTagsDialog.js +76 -67
- package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +3 -3
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +4 -3
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/data_sync/api/runs.js +1 -0
- package/dist/modules/data_sync/api/runs.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +3 -1
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/data/validators.js +1 -0
- package/dist/modules/data_sync/data/validators.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-run-service.js +20 -0
- package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
- package/dist/modules/entities/api/records.js +11 -1
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +17 -10
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/messages/api/unread-count/route.js +32 -0
- package/dist/modules/messages/api/unread-count/route.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +13 -5
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/shipping_carriers/i18n/en.js +11 -1
- package/dist/modules/shipping_carriers/i18n/en.js.map +2 -2
- package/dist/modules/shipping_carriers/i18n/pl.js +11 -1
- package/dist/modules/shipping_carriers/i18n/pl.js.map +2 -2
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js +25 -0
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js.map +7 -0
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js +3 -17
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js.map +2 -2
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +19 -14
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +7 -10
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +6 -7
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +3 -2
- package/src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx +3 -2
- package/src/modules/customers/components/detail/EntityTagsDialog.tsx +89 -71
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +6 -3
- package/src/modules/data_sync/api/runs.ts +1 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +3 -1
- package/src/modules/data_sync/data/validators.ts +1 -0
- package/src/modules/data_sync/i18n/de.json +1 -0
- package/src/modules/data_sync/i18n/en.json +1 -0
- package/src/modules/data_sync/i18n/es.json +1 -0
- package/src/modules/data_sync/i18n/pl.json +1 -0
- package/src/modules/data_sync/lib/sync-run-service.ts +23 -0
- package/src/modules/entities/api/records.ts +17 -1
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +25 -12
- package/src/modules/messages/api/unread-count/route.ts +40 -0
- package/src/modules/query_index/components/QueryIndexesTable.tsx +17 -15
- package/src/modules/shipping_carriers/i18n/de.json +11 -1
- package/src/modules/shipping_carriers/i18n/en.json +11 -1
- package/src/modules/shipping_carriers/i18n/en.ts +10 -0
- package/src/modules/shipping_carriers/i18n/es.json +11 -1
- package/src/modules/shipping_carriers/i18n/pl.json +11 -1
- package/src/modules/shipping_carriers/i18n/pl.ts +10 -0
- package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.tsx +35 -0
- package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.tsx +3 -22
- package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +30 -13
package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js.map
CHANGED
|
@@ -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'\
|
|
5
|
-
"mappings": "
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
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 { 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\
|
|
5
|
-
"mappings": ";
|
|
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.
|
|
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.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
<
|
|
125
|
-
type="checkbox"
|
|
122
|
+
<Checkbox
|
|
126
123
|
checked={features.includes(featureId)}
|
|
127
|
-
|
|
128
|
-
className="mt-0.5
|
|
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
|
-
<
|
|
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=
|
|
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
|
-
</
|
|
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
|
-
<
|
|
309
|
+
<StatusBadge variant={data.isActive ? 'success' : 'error'} dot>
|
|
309
310
|
{data.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}
|
|
310
|
-
</
|
|
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
|
-
<
|
|
63
|
+
<StatusBadge variant={user.isActive ? 'success' : 'error'} dot>
|
|
63
64
|
{user.isActive ? t('common.active', 'Active') : t('common.inactive', 'Inactive')}
|
|
64
|
-
</
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
455
|
-
// Default category order works without explicit settings rows.
|
|
458
|
+
return []
|
|
456
459
|
}
|
|
457
460
|
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
|
516
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
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,
|