@open-mercato/core 0.6.6-develop.6314.1.c7b8291aa2 → 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/ai-tools/_shared.js +158 -0
- package/dist/modules/customers/ai-tools/_shared.js.map +7 -0
- package/dist/modules/customers/ai-tools/companies-pack.js +15 -121
- package/dist/modules/customers/ai-tools/companies-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/people-pack.js +9 -107
- package/dist/modules/customers/ai-tools/people-pack.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarScreen.js +1 -0
- package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventPeekPopover.js +12 -1
- package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +3 -0
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/types.js.map +1 -1
- 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/ai-tools/_shared.ts +270 -0
- package/src/modules/customers/ai-tools/companies-pack.ts +17 -157
- package/src/modules/customers/ai-tools/people-pack.ts +11 -133
- package/src/modules/customers/components/calendar/CalendarScreen.tsx +1 -0
- package/src/modules/customers/components/calendar/EventPeekPopover.tsx +29 -11
- package/src/modules/customers/components/calendar/TimeGrid.tsx +3 -0
- package/src/modules/customers/components/calendar/types.ts +1 -0
- package/src/modules/customers/components/detail/EntityTagsDialog.tsx +89 -71
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- 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
|
@@ -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)
|
|
@@ -312,6 +312,7 @@
|
|
|
312
312
|
"customers.calendar.peek.attendee": "1 Teilnehmer",
|
|
313
313
|
"customers.calendar.peek.attendees": "{count} Teilnehmer",
|
|
314
314
|
"customers.calendar.peek.edit": "Bearbeiten",
|
|
315
|
+
"customers.calendar.peek.editForbidden": "Sie haben keine Berechtigung, Termine zu bearbeiten",
|
|
315
316
|
"customers.calendar.peek.join": "Beitreten",
|
|
316
317
|
"customers.calendar.platform.meet": "Meet",
|
|
317
318
|
"customers.calendar.platform.slack": "Slack",
|
|
@@ -312,6 +312,7 @@
|
|
|
312
312
|
"customers.calendar.peek.attendee": "1 attendee",
|
|
313
313
|
"customers.calendar.peek.attendees": "{count} attendees",
|
|
314
314
|
"customers.calendar.peek.edit": "Edit",
|
|
315
|
+
"customers.calendar.peek.editForbidden": "You don't have permission to edit events",
|
|
315
316
|
"customers.calendar.peek.join": "Join",
|
|
316
317
|
"customers.calendar.platform.meet": "Meet",
|
|
317
318
|
"customers.calendar.platform.slack": "Slack",
|
|
@@ -312,6 +312,7 @@
|
|
|
312
312
|
"customers.calendar.peek.attendee": "1 asistente",
|
|
313
313
|
"customers.calendar.peek.attendees": "{count} asistentes",
|
|
314
314
|
"customers.calendar.peek.edit": "Editar",
|
|
315
|
+
"customers.calendar.peek.editForbidden": "No tienes permiso para editar eventos",
|
|
315
316
|
"customers.calendar.peek.join": "Unirse",
|
|
316
317
|
"customers.calendar.platform.meet": "Meet",
|
|
317
318
|
"customers.calendar.platform.slack": "Slack",
|
|
@@ -312,6 +312,7 @@
|
|
|
312
312
|
"customers.calendar.peek.attendee": "1 uczestnik",
|
|
313
313
|
"customers.calendar.peek.attendees": "{count} uczestników",
|
|
314
314
|
"customers.calendar.peek.edit": "Edytuj",
|
|
315
|
+
"customers.calendar.peek.editForbidden": "Nie masz uprawnień do edycji wydarzeń",
|
|
315
316
|
"customers.calendar.peek.join": "Dołącz",
|
|
316
317
|
"customers.calendar.platform.meet": "Meet",
|
|
317
318
|
"customers.calendar.platform.slack": "Slack",
|
|
@@ -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,
|
|
@@ -107,6 +107,8 @@ const listRecordsQuerySchema = z
|
|
|
107
107
|
pageSize: z.coerce.number().int().min(1).max(100).optional(),
|
|
108
108
|
sortField: z.string().optional(),
|
|
109
109
|
sortDir: z.enum(['asc', 'desc']).optional(),
|
|
110
|
+
search: z.string().optional(),
|
|
111
|
+
searchFields: z.string().optional(),
|
|
110
112
|
withDeleted: z.coerce.boolean().optional(),
|
|
111
113
|
format: z.enum(['csv', 'json', 'xml', 'markdown']).optional(),
|
|
112
114
|
exportScope: z.enum(['full']).optional(),
|
|
@@ -145,10 +147,15 @@ export async function GET(req: Request) {
|
|
|
145
147
|
const sortField = url.searchParams.get('sortField') || 'id'
|
|
146
148
|
const sortDir = (url.searchParams.get('sortDir') || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'
|
|
147
149
|
const withDeleted = parseBooleanWithDefault(url.searchParams.get('withDeleted'), false)
|
|
150
|
+
const searchTerm = (url.searchParams.get('search') || '').trim()
|
|
151
|
+
const searchFields = (url.searchParams.get('searchFields') || '')
|
|
152
|
+
.split(',')
|
|
153
|
+
.map((field) => field.trim())
|
|
154
|
+
.filter(Boolean)
|
|
148
155
|
|
|
149
156
|
const qpEntries: Array<[string, string]> = []
|
|
150
157
|
for (const [key, val] of url.searchParams.entries()) {
|
|
151
|
-
if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full'].includes(key)) continue
|
|
158
|
+
if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full','search','searchFields'].includes(key)) continue
|
|
152
159
|
qpEntries.push([key, val])
|
|
153
160
|
}
|
|
154
161
|
|
|
@@ -246,6 +253,15 @@ export async function GET(req: Request) {
|
|
|
246
253
|
// even before the first record exists.
|
|
247
254
|
if (isCustomEntity) qopts.forceCustomEntityStorage = true
|
|
248
255
|
for (const [k, v] of qpEntries) buildFilter(k, v, isCustomEntity)
|
|
256
|
+
// Server-side full-result search: match the term against the requested fields
|
|
257
|
+
// (defaults to `id`) before pagination so totals/exports stay consistent with
|
|
258
|
+
// the active search instead of filtering only the current client page (#3229).
|
|
259
|
+
if (searchTerm) {
|
|
260
|
+
const fields = searchFields.length ? searchFields : ['id']
|
|
261
|
+
const pattern = `%${searchTerm}%`
|
|
262
|
+
const orClauses = fields.map((field) => ({ [field]: { $ilike: pattern } }))
|
|
263
|
+
;(filtersObj as any).$or = orClauses
|
|
264
|
+
}
|
|
249
265
|
const res = await qe.query(entityId as any, qopts)
|
|
250
266
|
const rawItems = res.items || []
|
|
251
267
|
const viewPageItems = rawItems.map(mapRow)
|
|
@@ -97,7 +97,16 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
|
|
|
97
97
|
keyExtras: [scopeVersion],
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
-
//
|
|
100
|
+
// Fields searched server-side: every visible column plus the base `id`.
|
|
101
|
+
const searchableFields = React.useMemo(() => {
|
|
102
|
+
const fields = (columns || [])
|
|
103
|
+
.map((col) => (col as any).accessorKey)
|
|
104
|
+
.filter((key): key is string => typeof key === 'string' && key.length > 0)
|
|
105
|
+
return Array.from(new Set(['id', ...fields]))
|
|
106
|
+
}, [columns])
|
|
107
|
+
|
|
108
|
+
// Fetch records whenever paging/sorting/filters/search change. Search is applied
|
|
109
|
+
// server-side (before pagination) so totals and exports stay consistent (#3229).
|
|
101
110
|
React.useEffect(() => {
|
|
102
111
|
let cancelled = false
|
|
103
112
|
const run = async () => {
|
|
@@ -107,6 +116,11 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
|
|
|
107
116
|
params.set('entityId', entityId)
|
|
108
117
|
params.set('page', String(page))
|
|
109
118
|
params.set('pageSize', String(pageSize))
|
|
119
|
+
const trimmedSearch = search.trim()
|
|
120
|
+
if (trimmedSearch) {
|
|
121
|
+
params.set('search', trimmedSearch)
|
|
122
|
+
if (searchableFields.length) params.set('searchFields', searchableFields.join(','))
|
|
123
|
+
}
|
|
110
124
|
const s = sorting?.[0]
|
|
111
125
|
if (s?.id) {
|
|
112
126
|
params.set('sortField', String(s.id))
|
|
@@ -154,7 +168,7 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
|
|
|
154
168
|
}
|
|
155
169
|
if (entityId) run()
|
|
156
170
|
return () => { cancelled = true }
|
|
157
|
-
}, [entityId, page, pageSize, sorting, filterValues, scopeVersion])
|
|
171
|
+
}, [entityId, page, pageSize, sorting, filterValues, scopeVersion, search, searchableFields])
|
|
158
172
|
|
|
159
173
|
// Build columns from custom field definitions only (no data round-trip)
|
|
160
174
|
React.useEffect(() => {
|
|
@@ -175,15 +189,9 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
|
|
|
175
189
|
setColumns(cols)
|
|
176
190
|
}, [cfDefs])
|
|
177
191
|
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const q = search.trim().toLowerCase()
|
|
182
|
-
return (rawData || []).filter((row: any) => {
|
|
183
|
-
const values = Object.values(row || {})
|
|
184
|
-
return values.some((v) => normalizeCell(v).toLowerCase().includes(q))
|
|
185
|
-
})
|
|
186
|
-
}, [rawData, search])
|
|
192
|
+
// Search is server-side (see fetch effect); render the fetched page as-is so
|
|
193
|
+
// pagination totals and exports stay consistent with the active search (#3229).
|
|
194
|
+
const data = rawData
|
|
187
195
|
|
|
188
196
|
const viewExportColumns = React.useMemo(() => {
|
|
189
197
|
return (columns || [])
|
|
@@ -213,8 +221,13 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
|
|
|
213
221
|
qp.set('sortField', String(sort.id))
|
|
214
222
|
qp.set('sortDir', sort.desc ? 'desc' : 'asc')
|
|
215
223
|
}
|
|
224
|
+
const trimmedSearch = search.trim()
|
|
225
|
+
if (trimmedSearch) {
|
|
226
|
+
qp.set('search', trimmedSearch)
|
|
227
|
+
if (searchableFields.length) qp.set('searchFields', searchableFields.join(','))
|
|
228
|
+
}
|
|
216
229
|
return `/api/entities/records?${qp.toString()}`
|
|
217
|
-
}, [entityId, sorting])
|
|
230
|
+
}, [entityId, sorting, search, searchableFields])
|
|
218
231
|
|
|
219
232
|
const exportConfig = React.useMemo(() => {
|
|
220
233
|
const safeEntityId = entityId.replace(/[^a-z0-9_-]/gi, '_') || 'records'
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import { type Kysely, sql } from 'kysely'
|
|
3
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
4
|
+
import {
|
|
5
|
+
buildCollectionTags,
|
|
6
|
+
isCrudCacheEnabled,
|
|
7
|
+
resolveCrudCache,
|
|
8
|
+
} from '@open-mercato/shared/lib/crud/cache'
|
|
3
9
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'
|
|
4
10
|
import { resolveMessageContext } from '../../lib/routeHelpers'
|
|
5
11
|
import { unreadCountResponseSchema } from '../openapi'
|
|
@@ -8,6 +14,16 @@ export const metadata = {
|
|
|
8
14
|
GET: { requireAuth: true },
|
|
9
15
|
}
|
|
10
16
|
|
|
17
|
+
const UNREAD_COUNT_RESOURCE = 'messages.message'
|
|
18
|
+
const UNREAD_COUNT_TTL_MS = 10_000
|
|
19
|
+
|
|
20
|
+
function buildUnreadCountCacheKey(params: {
|
|
21
|
+
userId: string
|
|
22
|
+
orgId: string | null
|
|
23
|
+
}): string {
|
|
24
|
+
return `messages:unread-count:u=${params.userId}:org=${params.orgId ?? 'null'}`
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
function getDb(em: EntityManager): Kysely<any> {
|
|
12
28
|
return em.getKysely<any>()
|
|
13
29
|
}
|
|
@@ -17,6 +33,19 @@ export async function GET(req: Request) {
|
|
|
17
33
|
const em = ctx.container.resolve('em') as EntityManager
|
|
18
34
|
const db = getDb(em) as any
|
|
19
35
|
|
|
36
|
+
const orgId = scope.organizationId ?? null
|
|
37
|
+
const cache = isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null
|
|
38
|
+
const cacheKey = cache
|
|
39
|
+
? buildUnreadCountCacheKey({ userId: scope.userId, orgId })
|
|
40
|
+
: null
|
|
41
|
+
|
|
42
|
+
if (cache && cacheKey) {
|
|
43
|
+
const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))
|
|
44
|
+
if (typeof cached === 'number') {
|
|
45
|
+
return Response.json({ unreadCount: cached })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
let query = db
|
|
21
50
|
.selectFrom('message_recipients as r')
|
|
22
51
|
.innerJoin('messages as m', 'm.id', 'r.message_id')
|
|
@@ -38,6 +67,17 @@ export async function GET(req: Request) {
|
|
|
38
67
|
.executeTakeFirst() as { count: string | number } | undefined
|
|
39
68
|
const count = Number(row?.count ?? 0)
|
|
40
69
|
|
|
70
|
+
if (cache && cacheKey) {
|
|
71
|
+
try {
|
|
72
|
+
await runWithCacheTenant(scope.tenantId, () =>
|
|
73
|
+
cache.set(cacheKey, count, {
|
|
74
|
+
ttl: UNREAD_COUNT_TTL_MS,
|
|
75
|
+
tags: buildCollectionTags(UNREAD_COUNT_RESOURCE, scope.tenantId, [orgId]),
|
|
76
|
+
}),
|
|
77
|
+
)
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
|
|
41
81
|
return Response.json({ unreadCount: count })
|
|
42
82
|
}
|
|
43
83
|
|
|
@@ -11,6 +11,7 @@ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/u
|
|
|
11
11
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
12
12
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
13
13
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
14
|
+
import { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'
|
|
14
15
|
|
|
15
16
|
const MUTATION_CONTEXT_ID = 'query_index.status.list:actions'
|
|
16
17
|
|
|
@@ -127,7 +128,7 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
|
|
|
127
128
|
if (!record.vectorEnabled) return <span>—</span>
|
|
128
129
|
const ok = record.vectorCount != null && record.baseCount != null && record.vectorCount === record.baseCount
|
|
129
130
|
const display = formatCount(record.vectorCount)
|
|
130
|
-
const className = ok ? 'text-
|
|
131
|
+
const className = ok ? 'text-status-success-text' : 'text-status-warning-text'
|
|
131
132
|
return <span className={className}>{display}</span>
|
|
132
133
|
},
|
|
133
134
|
meta: { priority: 2 },
|
|
@@ -141,7 +142,7 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
|
|
|
141
142
|
if (!record.fulltextEnabled) return <span>—</span>
|
|
142
143
|
const ok = record.fulltextCount != null && record.baseCount != null && record.fulltextCount === record.baseCount
|
|
143
144
|
const display = formatCount(record.fulltextCount)
|
|
144
|
-
const className = ok ? 'text-
|
|
145
|
+
const className = ok ? 'text-status-success-text' : 'text-status-warning-text'
|
|
145
146
|
return <span className={className}>{display}</span>
|
|
146
147
|
},
|
|
147
148
|
meta: { priority: 2 },
|
|
@@ -159,17 +160,14 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
|
|
|
159
160
|
const label = jobProgress
|
|
160
161
|
? t('query_index.table.status.withProgress', { status: statusText, progress: jobProgress })
|
|
161
162
|
: statusText
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
: ok
|
|
171
|
-
? 'text-green-600'
|
|
172
|
-
: 'text-muted-foreground'
|
|
163
|
+
let variant: StatusBadgeVariant = 'neutral'
|
|
164
|
+
if (job) {
|
|
165
|
+
if (job.status === 'stalled') variant = 'error'
|
|
166
|
+
else if (job.status === 'reindexing' || job.status === 'purging') variant = 'warning'
|
|
167
|
+
else variant = ok ? 'success' : 'neutral'
|
|
168
|
+
} else {
|
|
169
|
+
variant = ok ? 'success' : 'neutral'
|
|
170
|
+
}
|
|
173
171
|
|
|
174
172
|
const lines: string[] = []
|
|
175
173
|
|
|
@@ -203,8 +201,12 @@ function createColumns(t: Translator): ColumnDef<Row>[] {
|
|
|
203
201
|
}
|
|
204
202
|
|
|
205
203
|
return (
|
|
206
|
-
<div className="space-y-1">
|
|
207
|
-
<
|
|
204
|
+
<div className="space-y-1.5">
|
|
205
|
+
<div>
|
|
206
|
+
<StatusBadge variant={variant} dot>
|
|
207
|
+
{label}
|
|
208
|
+
</StatusBadge>
|
|
209
|
+
</div>
|
|
208
210
|
{lines.length > 0 && (
|
|
209
211
|
<div className="text-xs text-muted-foreground">
|
|
210
212
|
{lines.map((line, idx) => (
|