@open-mercato/core 0.6.6-develop.6257.1.6d0af84d26 → 0.6.6-develop.6290.1.4bb5a8ba3f
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 +2 -2
- package/dist/generated/entities/carrier_shipment_idempotency_key/index.js +19 -0
- package/dist/generated/entities/carrier_shipment_idempotency_key/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +2 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +10 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/lib/productResolution.js +45 -0
- package/dist/modules/catalog/lib/productResolution.js.map +7 -0
- package/dist/modules/communication_channels/workers/channel-import-history.js +1 -1
- package/dist/modules/communication_channels/workers/channel-import-history.js.map +2 -2
- package/dist/modules/currencies/api/fetch-configs/route.js +34 -0
- package/dist/modules/currencies/api/fetch-configs/route.js.map +2 -2
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js +17 -15
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +176 -161
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +75 -59
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +17 -10
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +6 -4
- package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +2 -2
- package/dist/modules/customers/components/detail/AiActionChips.js +5 -2
- package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +19 -21
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/cancel.js +1 -1
- package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +9 -45
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +11 -17
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +12 -10
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/data_sync/lib/id-mapping.js +46 -1
- package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/syncRunStatus.js +30 -0
- package/dist/modules/data_sync/lib/syncRunStatus.js.map +7 -0
- package/dist/modules/data_sync/workers/sync-scheduled.js +13 -14
- package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
- package/dist/modules/directory/components/TenantSelect.js +29 -16
- package/dist/modules/directory/components/TenantSelect.js.map +2 -2
- package/dist/modules/entities/api/records.js +6 -0
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/feature_toggles/api/global/route.js +17 -15
- package/dist/modules/feature_toggles/api/global/route.js.map +2 -2
- package/dist/modules/feature_toggles/api/openapi.js +2 -2
- package/dist/modules/feature_toggles/api/openapi.js.map +2 -2
- package/dist/modules/feature_toggles/commands/global.js +14 -0
- package/dist/modules/feature_toggles/commands/global.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +6 -0
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js +3 -1
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetails.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +12 -5
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js +10 -1
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.js.map +2 -2
- package/dist/modules/payment_gateways/api/cancel/route.js +5 -0
- package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/capture/route.js +5 -0
- package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/refund/route.js +5 -0
- package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +52 -25
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/status-machine.js +13 -0
- package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/route.js +8 -2
- package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
- package/dist/modules/perspectives/data/entities.js +33 -3
- package/dist/modules/perspectives/data/entities.js.map +2 -2
- package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js +63 -0
- package/dist/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.js.map +7 -0
- package/dist/modules/perspectives/services/perspectiveService.js +17 -0
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/dist/modules/planner/commands/availability-weekly.js +33 -1
- package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +14 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.js +35 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +18 -19
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/progress/api/active/route.js +1 -1
- package/dist/modules/progress/api/active/route.js.map +2 -2
- package/dist/modules/progress/lib/progressService.js.map +1 -1
- package/dist/modules/progress/lib/progressServiceImpl.js +33 -8
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/query_index/api/status.js +19 -24
- package/dist/modules/query_index/api/status.js.map +3 -3
- package/dist/modules/query_index/lib/coverage.js +35 -0
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +11 -6
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/resources/api/resource-types.js +5 -37
- package/dist/modules/resources/api/resource-types.js.map +2 -2
- package/dist/modules/resources/api/resourceTypeCounts.js +35 -0
- package/dist/modules/resources/api/resourceTypeCounts.js.map +7 -0
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +1 -1
- package/dist/modules/resources/backend/resources/resource-types/page.js +2 -1
- package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnDialog.js +4 -1
- package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +17 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/di.js +4 -0
- package/dist/modules/sales/di.js.map +2 -2
- package/dist/modules/sales/services/salesOrderService.js +26 -0
- package/dist/modules/sales/services/salesOrderService.js.map +7 -0
- package/dist/modules/shipping_carriers/api/shipments/route.js +8 -0
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/shipping_carriers/data/entities.js +38 -0
- package/dist/modules/shipping_carriers/data/entities.js.map +2 -2
- package/dist/modules/shipping_carriers/data/validators.js +2 -1
- package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipment-idempotency.js +76 -0
- package/dist/modules/shipping_carriers/lib/shipment-idempotency.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js +2 -1
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +20 -2
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipping-service.js +82 -43
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js +14 -0
- package/dist/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-ui/ListView.js +15 -6
- package/dist/modules/staff/lib/timesheets-ui/ListView.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +9 -18
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/generated/entities/carrier_shipment_idempotency_key/index.ts +8 -0
- package/generated/entities.ids.generated.ts +2 -1
- package/generated/entity-fields-registry.ts +10 -0
- package/package.json +9 -8
- package/src/modules/catalog/lib/productResolution.ts +75 -0
- package/src/modules/communication_channels/workers/channel-import-history.ts +1 -1
- package/src/modules/currencies/api/fetch-configs/route.ts +36 -0
- package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +20 -17
- package/src/modules/currencies/i18n/de.json +2 -1
- package/src/modules/currencies/i18n/en.json +2 -1
- package/src/modules/currencies/i18n/es.json +2 -1
- package/src/modules/currencies/i18n/pl.json +2 -1
- package/src/modules/customers/api/companies/[id]/route.ts +214 -189
- package/src/modules/customers/api/people/[id]/route.ts +106 -80
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +19 -12
- package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +8 -6
- package/src/modules/customers/components/detail/AiActionChips.tsx +6 -3
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +16 -18
- package/src/modules/data_sync/api/runs/[id]/cancel.ts +1 -1
- package/src/modules/data_sync/backend/data-sync/page.tsx +11 -58
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +13 -20
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +10 -8
- package/src/modules/data_sync/lib/id-mapping.ts +58 -1
- package/src/modules/data_sync/lib/sync-engine.ts +2 -2
- package/src/modules/data_sync/lib/syncRunStatus.ts +39 -0
- package/src/modules/data_sync/workers/sync-scheduled.ts +13 -14
- package/src/modules/directory/components/TenantSelect.tsx +33 -16
- package/src/modules/entities/api/records.ts +14 -0
- package/src/modules/feature_toggles/api/global/route.ts +16 -14
- package/src/modules/feature_toggles/api/openapi.ts +2 -2
- package/src/modules/feature_toggles/commands/global.ts +15 -0
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +7 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetails.ts +2 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +14 -4
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsQueries.ts +15 -1
- package/src/modules/payment_gateways/api/cancel/route.ts +5 -0
- package/src/modules/payment_gateways/api/capture/route.ts +5 -0
- package/src/modules/payment_gateways/api/refund/route.ts +5 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +58 -25
- package/src/modules/payment_gateways/lib/status-machine.ts +16 -0
- package/src/modules/perspectives/api/[tableId]/route.ts +8 -2
- package/src/modules/perspectives/data/entities.ts +43 -3
- package/src/modules/perspectives/migrations/.snapshot-open-mercato.json +69 -25
- package/src/modules/perspectives/migrations/Migration20260619120000_perspectives_live_row_uniqueness.ts +65 -0
- package/src/modules/perspectives/services/perspectiveService.ts +17 -0
- package/src/modules/planner/commands/availability-weekly.ts +48 -1
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +31 -10
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.ts +36 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +22 -23
- package/src/modules/progress/api/active/route.ts +1 -1
- package/src/modules/progress/lib/progressService.ts +2 -2
- package/src/modules/progress/lib/progressServiceImpl.ts +33 -8
- package/src/modules/query_index/api/status.ts +28 -32
- package/src/modules/query_index/lib/coverage.ts +63 -0
- package/src/modules/query_index/lib/engine.ts +37 -24
- package/src/modules/resources/api/resource-types.ts +4 -44
- package/src/modules/resources/api/resourceTypeCounts.ts +74 -0
- package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
- package/src/modules/resources/backend/resources/resource-types/page.tsx +1 -0
- package/src/modules/sales/components/documents/ReturnDialog.tsx +4 -1
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +20 -2
- package/src/modules/sales/di.ts +6 -0
- package/src/modules/sales/services/salesOrderService.ts +38 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +8 -0
- package/src/modules/shipping_carriers/data/entities.ts +33 -0
- package/src/modules/shipping_carriers/data/validators.ts +1 -0
- package/src/modules/shipping_carriers/i18n/de.json +1 -0
- package/src/modules/shipping_carriers/i18n/en.json +1 -0
- package/src/modules/shipping_carriers/i18n/es.json +1 -0
- package/src/modules/shipping_carriers/i18n/pl.json +1 -0
- package/src/modules/shipping_carriers/lib/shipment-idempotency.ts +119 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.ts +3 -1
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +24 -2
- package/src/modules/shipping_carriers/lib/shipping-service.ts +87 -43
- package/src/modules/shipping_carriers/migrations/.snapshot-open-mercato.json +197 -0
- package/src/modules/shipping_carriers/migrations/Migration20260618173616_shipping_carriers.ts +14 -0
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/staff/lib/timesheets-ui/ListView.tsx +15 -7
- package/src/modules/translations/components/TranslationManager.tsx +17 -24
- package/src/modules/translations/i18n/de.json +1 -0
- package/src/modules/translations/i18n/en.json +1 -0
- package/src/modules/translations/i18n/es.json +1 -0
- package/src/modules/translations/i18n/pl.json +1 -0
|
@@ -5,6 +5,7 @@ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
|
5
5
|
import { FormHeader } from '@open-mercato/ui/backend/forms'
|
|
6
6
|
import { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'
|
|
7
7
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
8
|
+
import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
|
|
8
9
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
9
10
|
import { LogList, type LogListEntry } from '@open-mercato/ui/backend/LogList'
|
|
10
11
|
import { Progress } from '@open-mercato/ui/primitives/progress'
|
|
@@ -15,6 +16,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
15
16
|
import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
16
17
|
import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
|
|
17
18
|
import { RotateCcw, XCircle } from 'lucide-react'
|
|
19
|
+
import { getSyncRunStatusVariant } from '../../../../lib/syncRunStatus'
|
|
18
20
|
|
|
19
21
|
type SyncRunDetail = {
|
|
20
22
|
id: string
|
|
@@ -69,15 +71,6 @@ function formatEtaSeconds(seconds: number): string {
|
|
|
69
71
|
return `${hours}h ${minutes}m`
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
const STATUS_STYLES: Record<string, string> = {
|
|
73
|
-
pending: 'bg-gray-100 text-gray-800',
|
|
74
|
-
running: 'bg-blue-100 text-blue-800',
|
|
75
|
-
completed: 'bg-green-100 text-green-800',
|
|
76
|
-
failed: 'bg-red-100 text-red-800',
|
|
77
|
-
cancelled: 'bg-yellow-100 text-yellow-800',
|
|
78
|
-
paused: 'bg-orange-100 text-orange-800',
|
|
79
|
-
}
|
|
80
|
-
|
|
81
74
|
type SyncRunDetailPageProps = {
|
|
82
75
|
params?: {
|
|
83
76
|
id?: string | string[]
|
|
@@ -284,9 +277,9 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
|
|
|
284
277
|
statusBadge={(
|
|
285
278
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
286
279
|
<Badge variant="outline">{t(`data_sync.dashboard.direction.${run.direction}`)}</Badge>
|
|
287
|
-
<
|
|
280
|
+
<StatusBadge variant={getSyncRunStatusVariant(run.status)}>
|
|
288
281
|
{t(`data_sync.dashboard.status.${run.status}`)}
|
|
289
|
-
</
|
|
282
|
+
</StatusBadge>
|
|
290
283
|
{run.triggeredBy ? <Badge variant="outline">{run.triggeredBy}</Badge> : null}
|
|
291
284
|
</div>
|
|
292
285
|
)}
|
|
@@ -312,9 +305,9 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
|
|
|
312
305
|
<CardHeader>
|
|
313
306
|
<div className="flex items-center justify-between gap-3">
|
|
314
307
|
<CardTitle>{t('data_sync.runs.detail.progress')}</CardTitle>
|
|
315
|
-
<
|
|
308
|
+
<StatusBadge variant={getSyncRunStatusVariant(progressStatus)}>
|
|
316
309
|
{t(`data_sync.dashboard.status.${progressStatus}`)}
|
|
317
|
-
</
|
|
310
|
+
</StatusBadge>
|
|
318
311
|
</div>
|
|
319
312
|
</CardHeader>
|
|
320
313
|
<CardContent className="space-y-3">
|
|
@@ -355,37 +348,37 @@ export default function SyncRunDetailPage({ params }: SyncRunDetailPageProps) {
|
|
|
355
348
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
356
349
|
<Card>
|
|
357
350
|
<CardContent className="pt-6 text-center">
|
|
358
|
-
<div className="text-2xl font-bold text-
|
|
351
|
+
<div className="text-2xl font-bold text-status-success-text">{run.createdCount}</div>
|
|
359
352
|
<p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.created')}</p>
|
|
360
353
|
</CardContent>
|
|
361
354
|
</Card>
|
|
362
355
|
<Card>
|
|
363
356
|
<CardContent className="pt-6 text-center">
|
|
364
|
-
<div className="text-2xl font-bold text-
|
|
357
|
+
<div className="text-2xl font-bold text-status-info-text">{run.updatedCount}</div>
|
|
365
358
|
<p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.updated')}</p>
|
|
366
359
|
</CardContent>
|
|
367
360
|
</Card>
|
|
368
361
|
<Card>
|
|
369
362
|
<CardContent className="pt-6 text-center">
|
|
370
|
-
<div className="text-2xl font-bold text-
|
|
363
|
+
<div className="text-2xl font-bold text-muted-foreground">{run.skippedCount}</div>
|
|
371
364
|
<p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.skipped')}</p>
|
|
372
365
|
</CardContent>
|
|
373
366
|
</Card>
|
|
374
367
|
<Card>
|
|
375
368
|
<CardContent className="pt-6 text-center">
|
|
376
|
-
<div className="text-2xl font-bold text-
|
|
369
|
+
<div className="text-2xl font-bold text-status-error-text">{run.failedCount}</div>
|
|
377
370
|
<p className="text-sm text-muted-foreground">{t('data_sync.runs.detail.counters.failed')}</p>
|
|
378
371
|
</CardContent>
|
|
379
372
|
</Card>
|
|
380
373
|
</div>
|
|
381
374
|
|
|
382
375
|
{run.lastError && (
|
|
383
|
-
<Card className="border-
|
|
376
|
+
<Card className="border-status-error-border bg-status-error-bg">
|
|
384
377
|
<CardHeader>
|
|
385
|
-
<CardTitle className="text-
|
|
378
|
+
<CardTitle className="text-status-error-text">{t('data_sync.runs.detail.error')}</CardTitle>
|
|
386
379
|
</CardHeader>
|
|
387
380
|
<CardContent>
|
|
388
|
-
<pre className="text-sm text-
|
|
381
|
+
<pre className="text-sm text-status-error-text whitespace-pre-wrap">{run.lastError}</pre>
|
|
389
382
|
</CardContent>
|
|
390
383
|
</Card>
|
|
391
384
|
)}
|
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
SelectValue,
|
|
19
19
|
} from '@open-mercato/ui/primitives/select'
|
|
20
20
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
21
|
+
import { Switch } from '@open-mercato/ui/primitives/switch'
|
|
21
22
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
22
23
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
24
|
+
import { getSyncSummaryVariant } from '../lib/syncRunStatus'
|
|
23
25
|
import {
|
|
24
26
|
CalendarClock,
|
|
25
27
|
Play,
|
|
@@ -360,13 +362,13 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
360
362
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
361
363
|
<div className="space-y-2">
|
|
362
364
|
<div className="flex flex-wrap items-center gap-2">
|
|
363
|
-
<Badge variant=
|
|
365
|
+
<Badge variant={getSyncSummaryVariant(props.isEnabled ? 'enabled' : 'disabled')}>
|
|
364
366
|
{props.isEnabled ? <ShieldCheck className="mr-2 h-3.5 w-3.5" /> : <ShieldAlert className="mr-2 h-3.5 w-3.5" />}
|
|
365
367
|
{props.isEnabled
|
|
366
368
|
? t('data_sync.dashboard.start.status.enabled', 'Integration enabled')
|
|
367
369
|
: t('data_sync.dashboard.start.status.disabled', 'Integration disabled')}
|
|
368
370
|
</Badge>
|
|
369
|
-
<Badge variant=
|
|
371
|
+
<Badge variant={getSyncSummaryVariant(props.hasCredentials ? 'ready' : 'missing')}>
|
|
370
372
|
<CalendarClock className="mr-2 h-3.5 w-3.5" />
|
|
371
373
|
{props.hasCredentials
|
|
372
374
|
? t('data_sync.dashboard.start.status.credentialsReady', 'Credentials ready')
|
|
@@ -477,22 +479,22 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
477
479
|
</td>
|
|
478
480
|
<td className="px-3 py-3">
|
|
479
481
|
<label className="flex min-h-10 items-center gap-2 text-sm">
|
|
480
|
-
<
|
|
481
|
-
type="checkbox"
|
|
482
|
+
<Switch
|
|
482
483
|
checked={scheduleState.fullSync}
|
|
483
|
-
|
|
484
|
+
onCheckedChange={(checked) => updateScheduleEditor(row.key, { fullSync: checked }, row.entityType)}
|
|
484
485
|
disabled={controlsDisabled}
|
|
486
|
+
aria-label={t('data_sync.dashboard.start.fullSync', 'Run as full sync')}
|
|
485
487
|
/>
|
|
486
488
|
<span>{t('data_sync.integrationTab.fullSyncShort', 'Full')}</span>
|
|
487
489
|
</label>
|
|
488
490
|
</td>
|
|
489
491
|
<td className="px-3 py-3">
|
|
490
492
|
<label className="flex min-h-10 items-center gap-2 text-sm">
|
|
491
|
-
<
|
|
492
|
-
type="checkbox"
|
|
493
|
+
<Switch
|
|
493
494
|
checked={scheduleState.isEnabled}
|
|
494
|
-
|
|
495
|
+
onCheckedChange={(checked) => updateScheduleEditor(row.key, { isEnabled: checked }, row.entityType)}
|
|
495
496
|
disabled={controlsDisabled}
|
|
497
|
+
aria-label={t('data_sync.dashboard.schedule.enabled', 'Schedule enabled')}
|
|
496
498
|
/>
|
|
497
499
|
<span>{scheduleState.isEnabled ? t('data_sync.dashboard.schedule.status.shortEnabled', 'Scheduled') : t('data_sync.dashboard.schedule.status.shortDisabled', 'Paused')}</span>
|
|
498
500
|
</label>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
-
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
2
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
3
|
import { SyncExternalIdMapping } from '../../integrations/data/entities'
|
|
4
4
|
|
|
5
5
|
type MappingScope = {
|
|
@@ -119,6 +119,63 @@ export function createExternalIdMappingService(em: EntityManager) {
|
|
|
119
119
|
await em.persist(created).flush()
|
|
120
120
|
return created
|
|
121
121
|
},
|
|
122
|
+
|
|
123
|
+
async deleteExternalIdMapping(
|
|
124
|
+
integrationId: string,
|
|
125
|
+
entityType: string,
|
|
126
|
+
localId: string,
|
|
127
|
+
scope: MappingScope,
|
|
128
|
+
): Promise<boolean> {
|
|
129
|
+
const row = await findOneWithDecryption(
|
|
130
|
+
em,
|
|
131
|
+
SyncExternalIdMapping,
|
|
132
|
+
{
|
|
133
|
+
integrationId,
|
|
134
|
+
internalEntityType: entityType,
|
|
135
|
+
internalEntityId: localId,
|
|
136
|
+
organizationId: scope.organizationId,
|
|
137
|
+
tenantId: scope.tenantId,
|
|
138
|
+
deletedAt: null,
|
|
139
|
+
},
|
|
140
|
+
undefined,
|
|
141
|
+
scope,
|
|
142
|
+
)
|
|
143
|
+
if (!row) return false
|
|
144
|
+
row.deletedAt = new Date()
|
|
145
|
+
await em.flush()
|
|
146
|
+
return true
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async deleteExternalIdMappings(
|
|
150
|
+
integrationId: string,
|
|
151
|
+
entityType: string,
|
|
152
|
+
localIds: string[],
|
|
153
|
+
scope: MappingScope,
|
|
154
|
+
): Promise<number> {
|
|
155
|
+
const uniqueLocalIds = Array.from(new Set(localIds))
|
|
156
|
+
if (uniqueLocalIds.length === 0) return 0
|
|
157
|
+
const rows = await findWithDecryption(
|
|
158
|
+
em,
|
|
159
|
+
SyncExternalIdMapping,
|
|
160
|
+
{
|
|
161
|
+
integrationId,
|
|
162
|
+
internalEntityType: entityType,
|
|
163
|
+
internalEntityId: { $in: uniqueLocalIds },
|
|
164
|
+
organizationId: scope.organizationId,
|
|
165
|
+
tenantId: scope.tenantId,
|
|
166
|
+
deletedAt: null,
|
|
167
|
+
},
|
|
168
|
+
undefined,
|
|
169
|
+
scope,
|
|
170
|
+
)
|
|
171
|
+
if (rows.length === 0) return 0
|
|
172
|
+
const now = new Date()
|
|
173
|
+
for (const row of rows) {
|
|
174
|
+
row.deletedAt = now
|
|
175
|
+
}
|
|
176
|
+
await em.flush()
|
|
177
|
+
return rows.length
|
|
178
|
+
},
|
|
122
179
|
}
|
|
123
180
|
}
|
|
124
181
|
|
|
@@ -435,7 +435,7 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
435
435
|
scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
|
|
436
436
|
runId: run.id,
|
|
437
437
|
})) {
|
|
438
|
-
if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId)) {
|
|
438
|
+
if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
|
|
439
439
|
await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
|
|
440
440
|
return
|
|
441
441
|
}
|
|
@@ -579,7 +579,7 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
579
579
|
scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
|
|
580
580
|
runId: run.id,
|
|
581
581
|
})) {
|
|
582
|
-
if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId)) {
|
|
582
|
+
if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
|
|
583
583
|
await finalizeRun(run.id, 'cancelled', scope, undefined, operationalTelemetry)
|
|
584
584
|
return
|
|
585
585
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { StatusBadgeVariant, StatusMap } from '@open-mercato/ui/primitives/status-badge'
|
|
2
|
+
|
|
3
|
+
export type SyncRunStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'paused'
|
|
4
|
+
|
|
5
|
+
export const syncRunStatusVariants: StatusMap<SyncRunStatus> = {
|
|
6
|
+
pending: 'neutral',
|
|
7
|
+
running: 'info',
|
|
8
|
+
completed: 'success',
|
|
9
|
+
failed: 'error',
|
|
10
|
+
cancelled: 'warning',
|
|
11
|
+
paused: 'warning',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getSyncRunStatusVariant(status: string): StatusBadgeVariant {
|
|
15
|
+
return syncRunStatusVariants[status as SyncRunStatus] ?? 'neutral'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type SyncSummaryKind =
|
|
19
|
+
| 'enabled'
|
|
20
|
+
| 'disabled'
|
|
21
|
+
| 'ready'
|
|
22
|
+
| 'missing'
|
|
23
|
+
| 'scheduled'
|
|
24
|
+
| 'paused'
|
|
25
|
+
| 'none'
|
|
26
|
+
|
|
27
|
+
export const syncSummaryVariants: Record<SyncSummaryKind, StatusBadgeVariant> = {
|
|
28
|
+
enabled: 'success',
|
|
29
|
+
ready: 'success',
|
|
30
|
+
disabled: 'neutral',
|
|
31
|
+
missing: 'warning',
|
|
32
|
+
scheduled: 'info',
|
|
33
|
+
paused: 'warning',
|
|
34
|
+
none: 'neutral',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getSyncSummaryVariant(kind: SyncSummaryKind): StatusBadgeVariant {
|
|
38
|
+
return syncSummaryVariants[kind] ?? 'neutral'
|
|
39
|
+
}
|
|
@@ -2,9 +2,10 @@ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
|
|
|
2
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
3
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
4
|
import type { IntegrationStateService } from '../../integrations/lib/state-service'
|
|
5
|
+
import type { ProgressService } from '../../progress/lib/progressService'
|
|
5
6
|
import type { SyncRunService } from '../lib/sync-run-service'
|
|
6
7
|
import { SyncSchedule } from '../data/entities'
|
|
7
|
-
import {
|
|
8
|
+
import { startDataSyncRun } from '../lib/start-run'
|
|
8
9
|
|
|
9
10
|
type ScheduledSyncPayload = {
|
|
10
11
|
scheduleId: string
|
|
@@ -27,6 +28,7 @@ type HandlerContext = JobContext & {
|
|
|
27
28
|
export default async function handle(job: QueuedJob<ScheduledSyncPayload>, ctx: HandlerContext): Promise<void> {
|
|
28
29
|
const em = ctx.resolve<EntityManager>('em')
|
|
29
30
|
const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')
|
|
31
|
+
const progressService = ctx.resolve<ProgressService>('progressService')
|
|
30
32
|
const integrationStateService = ctx.resolve<IntegrationStateService>('integrationStateService')
|
|
31
33
|
|
|
32
34
|
const schedule = await findOneWithDecryption(
|
|
@@ -70,22 +72,19 @@ export default async function handle(job: QueuedJob<ScheduledSyncPayload>, ctx:
|
|
|
70
72
|
job.payload.scope,
|
|
71
73
|
)
|
|
72
74
|
|
|
73
|
-
const run = await syncRunService.createRun({
|
|
74
|
-
integrationId: schedule.integrationId,
|
|
75
|
-
entityType: schedule.entityType,
|
|
76
|
-
direction: schedule.direction,
|
|
77
|
-
cursor,
|
|
78
|
-
triggeredBy: 'scheduler',
|
|
79
|
-
}, job.payload.scope)
|
|
80
|
-
|
|
81
75
|
schedule.lastRunAt = new Date()
|
|
82
76
|
await em.flush()
|
|
83
77
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
runId: run.id,
|
|
88
|
-
batchSize: 100,
|
|
78
|
+
await startDataSyncRun({
|
|
79
|
+
syncRunService,
|
|
80
|
+
progressService,
|
|
89
81
|
scope: job.payload.scope,
|
|
82
|
+
input: {
|
|
83
|
+
integrationId: schedule.integrationId,
|
|
84
|
+
entityType: schedule.entityType,
|
|
85
|
+
direction: schedule.direction,
|
|
86
|
+
cursor,
|
|
87
|
+
triggeredBy: 'scheduler',
|
|
88
|
+
},
|
|
90
89
|
})
|
|
91
90
|
}
|
|
@@ -57,21 +57,11 @@ function filterTenantsByStatus(list: TenantRecord[], status: 'all' | 'active' |
|
|
|
57
57
|
return list
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
search.set('sortDir', 'asc')
|
|
66
|
-
if (status === 'active') search.set('isActive', 'true')
|
|
67
|
-
if (status === 'inactive') search.set('isActive', 'false')
|
|
68
|
-
const json = await readApiResultOrThrow<{ items?: unknown[] }>(
|
|
69
|
-
`/api/directory/tenants?${search.toString()}`,
|
|
70
|
-
undefined,
|
|
71
|
-
{ errorMessage, allowNullResult: true },
|
|
72
|
-
)
|
|
73
|
-
const items = Array.isArray(json?.items) ? json.items : []
|
|
74
|
-
const normalized = items
|
|
60
|
+
const DIRECTORY_TENANTS_PAGE_SIZE = 100
|
|
61
|
+
const DIRECTORY_TENANTS_MAX_PAGES = 200
|
|
62
|
+
|
|
63
|
+
function normalizeTenantItems(items: unknown[]): TenantRecord[] {
|
|
64
|
+
return items
|
|
75
65
|
.map((item: unknown): TenantRecord | null => {
|
|
76
66
|
if (!item || typeof item !== 'object') return null
|
|
77
67
|
const entry = item as Record<string, unknown>
|
|
@@ -84,7 +74,34 @@ async function fetchDirectoryTenants(status: 'all' | 'active' | 'inactive', erro
|
|
|
84
74
|
return { id, name, isActive }
|
|
85
75
|
})
|
|
86
76
|
.filter((tenant: TenantRecord | null): tenant is TenantRecord => tenant !== null)
|
|
87
|
-
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function fetchDirectoryTenants(status: 'all' | 'active' | 'inactive', errorMessage: string): Promise<TenantRecord[]> {
|
|
80
|
+
const collected: TenantRecord[] = []
|
|
81
|
+
let page = 1
|
|
82
|
+
let totalPages = 1
|
|
83
|
+
do {
|
|
84
|
+
const search = new URLSearchParams()
|
|
85
|
+
search.set('page', String(page))
|
|
86
|
+
search.set('pageSize', String(DIRECTORY_TENANTS_PAGE_SIZE))
|
|
87
|
+
search.set('sortField', 'name')
|
|
88
|
+
search.set('sortDir', 'asc')
|
|
89
|
+
if (status === 'active') search.set('isActive', 'true')
|
|
90
|
+
if (status === 'inactive') search.set('isActive', 'false')
|
|
91
|
+
const json = await readApiResultOrThrow<{ items?: unknown[]; totalPages?: unknown }>(
|
|
92
|
+
`/api/directory/tenants?${search.toString()}`,
|
|
93
|
+
undefined,
|
|
94
|
+
{ errorMessage, allowNullResult: true },
|
|
95
|
+
)
|
|
96
|
+
const items = Array.isArray(json?.items) ? json.items : []
|
|
97
|
+
collected.push(...normalizeTenantItems(items))
|
|
98
|
+
totalPages = typeof json?.totalPages === 'number' && Number.isFinite(json.totalPages)
|
|
99
|
+
? Math.max(1, Math.floor(json.totalPages))
|
|
100
|
+
: 1
|
|
101
|
+
if (items.length < DIRECTORY_TENANTS_PAGE_SIZE) break
|
|
102
|
+
page += 1
|
|
103
|
+
} while (page <= totalPages && page <= DIRECTORY_TENANTS_MAX_PAGES)
|
|
104
|
+
return mergeTenantLists(filterTenantsByStatus(collected, status))
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
async function fetchTenantsFromOrganizationSwitcher(status: 'all' | 'active' | 'inactive', errorMessage: string): Promise<TenantRecord[]> {
|
|
@@ -382,6 +382,13 @@ export async function POST(req: Request) {
|
|
|
382
382
|
if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
|
|
383
383
|
const isCustomEntity = entityKind === 'custom'
|
|
384
384
|
await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
|
|
385
|
+
// Strip reserved record/system columns the edit form echoes back from the loaded record
|
|
386
|
+
// (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom
|
|
387
|
+
// fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are
|
|
388
|
+
// rejected as "Unknown custom field", which fails EVERY custom-entity edit-form save.
|
|
389
|
+
for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {
|
|
390
|
+
delete (values as any)[reservedKey]
|
|
391
|
+
}
|
|
385
392
|
const norm = normalizeValues(values)
|
|
386
393
|
|
|
387
394
|
// Validate against custom field definitions
|
|
@@ -448,6 +455,13 @@ export async function PUT(req: Request) {
|
|
|
448
455
|
if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
|
|
449
456
|
const isCustomEntity = entityKind === 'custom'
|
|
450
457
|
await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
|
|
458
|
+
// Strip reserved record/system columns the edit form echoes back from the loaded record
|
|
459
|
+
// (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom
|
|
460
|
+
// fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are
|
|
461
|
+
// rejected as "Unknown custom field", which fails EVERY custom-entity edit-form save.
|
|
462
|
+
for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {
|
|
463
|
+
delete (values as any)[reservedKey]
|
|
464
|
+
}
|
|
451
465
|
const norm = normalizeValues(values)
|
|
452
466
|
|
|
453
467
|
// Validate against custom field definitions
|
|
@@ -49,6 +49,21 @@ const listFields = [
|
|
|
49
49
|
'updated_at',
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
+
export const transformFeatureToggleListItem = (item: Record<string, unknown>) => {
|
|
53
|
+
if (!item) return item
|
|
54
|
+
return {
|
|
55
|
+
id: item.id,
|
|
56
|
+
identifier: item.identifier,
|
|
57
|
+
name: item.name,
|
|
58
|
+
description: item.description ?? null,
|
|
59
|
+
category: item.category ?? null,
|
|
60
|
+
type: item.type,
|
|
61
|
+
defaultValue: item.default_value,
|
|
62
|
+
createdAt: item.created_at,
|
|
63
|
+
updatedAt: item.updated_at,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
const buildFilters = (query: FeatureToggleListQuery): Record<string, unknown> => {
|
|
53
68
|
const filters: Record<string, unknown> = {}
|
|
54
69
|
const search = query.search?.trim()
|
|
@@ -105,20 +120,7 @@ const crud = makeCrudRoute({
|
|
|
105
120
|
updatedAt: 'updated_at',
|
|
106
121
|
type: 'type',
|
|
107
122
|
},
|
|
108
|
-
transformItem:
|
|
109
|
-
if (!item) return item
|
|
110
|
-
return {
|
|
111
|
-
id: item.id,
|
|
112
|
-
identifier: item.identifier,
|
|
113
|
-
name: item.name,
|
|
114
|
-
description: item.description ?? null,
|
|
115
|
-
category: item.category ?? null,
|
|
116
|
-
type: item.type,
|
|
117
|
-
defaultValue: item.default_value,
|
|
118
|
-
created_at: item.created_at,
|
|
119
|
-
updated_at: item.updated_at,
|
|
120
|
-
}
|
|
121
|
-
},
|
|
123
|
+
transformItem: transformFeatureToggleListItem,
|
|
122
124
|
buildFilters: async (query) => buildFilters(query),
|
|
123
125
|
},
|
|
124
126
|
actions: {
|
|
@@ -28,8 +28,8 @@ export const featureToggleSchema = z
|
|
|
28
28
|
category: z.string().nullable().optional(),
|
|
29
29
|
type: toggleTypeSchema,
|
|
30
30
|
defaultValue: z.any().nullable().optional(),
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
createdAt: z.string().nullable().optional(),
|
|
32
|
+
updatedAt: z.string().nullable().optional(),
|
|
33
33
|
})
|
|
34
34
|
.passthrough()
|
|
35
35
|
|
|
@@ -6,6 +6,7 @@ import { ToggleCreateInput, toggleCreateSchema, ToggleUpdateInput, toggleUpdateS
|
|
|
6
6
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
7
7
|
import { registerCommand } from '@open-mercato/shared/lib/commands'
|
|
8
8
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
9
|
+
import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
9
10
|
import { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects, requireId } from '@open-mercato/shared/lib/commands/helpers'
|
|
10
11
|
import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
|
|
11
12
|
import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
|
|
@@ -49,6 +50,8 @@ type ToggleUndoPayload = {
|
|
|
49
50
|
|
|
50
51
|
const featureToggleCrudIndexer = { entityType: E.feature_toggles.feature_toggle }
|
|
51
52
|
|
|
53
|
+
const FEATURE_TOGGLE_LOCK_RESOURCE_KIND = 'feature_toggles.feature_toggle'
|
|
54
|
+
|
|
52
55
|
function featureToggleIdentifiers(
|
|
53
56
|
toggle: FeatureToggle | ToggleSnapshot,
|
|
54
57
|
ctx: { auth?: { tenantId?: string | null } | null },
|
|
@@ -217,6 +220,12 @@ const updateToggleCommand: CommandHandler<ToggleUpdateInput, { toggleId: string
|
|
|
217
220
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
218
221
|
const toggle = await em.findOne(FeatureToggle, { id: parsed.id, deletedAt: null })
|
|
219
222
|
if (!toggle) throw new CrudHttpError(404, { error: 'Toggle not found' })
|
|
223
|
+
enforceCommandOptimisticLock({
|
|
224
|
+
resourceKind: FEATURE_TOGGLE_LOCK_RESOURCE_KIND,
|
|
225
|
+
resourceId: toggle.id,
|
|
226
|
+
current: toggle.updatedAt ?? null,
|
|
227
|
+
request: ctx.request ?? null,
|
|
228
|
+
})
|
|
220
229
|
const previousIdentifier = toggle.identifier
|
|
221
230
|
if (parsed.identifier && parsed.identifier !== toggle.identifier) {
|
|
222
231
|
const existing = await em.findOne(FeatureToggle, { identifier: parsed.identifier })
|
|
@@ -344,6 +353,12 @@ const deleteToggleCommand: CommandHandler<{ body?: Record<string, unknown>; quer
|
|
|
344
353
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
345
354
|
const toggle = await em.findOne(FeatureToggle, { id, deletedAt: null })
|
|
346
355
|
if (!toggle) throw new CrudHttpError(404, { error: 'Feature toggle not found' })
|
|
356
|
+
enforceCommandOptimisticLock({
|
|
357
|
+
resourceKind: FEATURE_TOGGLE_LOCK_RESOURCE_KIND,
|
|
358
|
+
resourceId: toggle.id,
|
|
359
|
+
current: toggle.updatedAt ?? null,
|
|
360
|
+
request: ctx.request ?? null,
|
|
361
|
+
})
|
|
347
362
|
const featureTogglesService = ctx.container.resolve('featureTogglesService') as FeatureTogglesService
|
|
348
363
|
await featureTogglesService.invalidateIsEnabledCacheByIdentifierTag(toggle.identifier)
|
|
349
364
|
|
|
@@ -148,6 +148,13 @@ export default function InboxOpsProposalsPage() {
|
|
|
148
148
|
if (initialLoadComplete) loadProposals()
|
|
149
149
|
}, [page, statusFilter, categoryFilter, search, scopeVersion]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
150
150
|
|
|
151
|
+
React.useEffect(() => {
|
|
152
|
+
if (initialLoadComplete) {
|
|
153
|
+
loadCounts()
|
|
154
|
+
loadSettings()
|
|
155
|
+
}
|
|
156
|
+
}, [scopeVersion]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
157
|
+
|
|
151
158
|
const handleCopyAddress = React.useCallback(() => {
|
|
152
159
|
if (settings?.inboxAddress) {
|
|
153
160
|
navigator.clipboard.writeText(settings.inboxAddress)
|
|
@@ -61,7 +61,9 @@ export function useMessageDetails(id: string) {
|
|
|
61
61
|
attachments: queryState.attachments,
|
|
62
62
|
isArchived,
|
|
63
63
|
onDeleted: () => router.push('/backend/messages'),
|
|
64
|
+
onMarkedUnread: () => router.push('/backend/messages'),
|
|
64
65
|
refreshDetailWithoutAutoMarkRead: queryState.refreshDetailWithoutAutoMarkRead,
|
|
66
|
+
suppressAutoMarkRead: queryState.suppressAutoMarkRead,
|
|
65
67
|
})
|
|
66
68
|
|
|
67
69
|
const conversationState = useMessageDetailsConversation({
|
|
@@ -18,6 +18,7 @@ import { isSafeNavigationHref, parseObjectActionId, toErrorMessage } from '../ut
|
|
|
18
18
|
|
|
19
19
|
type RequestAndRefreshOptions = {
|
|
20
20
|
skipDetailAutoMarkRead?: boolean
|
|
21
|
+
onSuccess?: () => void
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
type ConversationActionKind =
|
|
@@ -42,7 +43,9 @@ type UseMessageDetailsActionsInput = {
|
|
|
42
43
|
attachments: MessageAttachment[] | undefined
|
|
43
44
|
isArchived: boolean
|
|
44
45
|
onDeleted: () => void
|
|
46
|
+
onMarkedUnread: () => void
|
|
45
47
|
refreshDetailWithoutAutoMarkRead: () => Promise<MessageDetail | null>
|
|
48
|
+
suppressAutoMarkRead: () => void
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
export function useMessageDetailsActions({
|
|
@@ -53,7 +56,9 @@ export function useMessageDetailsActions({
|
|
|
53
56
|
attachments,
|
|
54
57
|
isArchived,
|
|
55
58
|
onDeleted,
|
|
59
|
+
onMarkedUnread,
|
|
56
60
|
refreshDetailWithoutAutoMarkRead,
|
|
61
|
+
suppressAutoMarkRead,
|
|
57
62
|
}: UseMessageDetailsActionsInput) {
|
|
58
63
|
const [replyOpen, setReplyOpen] = React.useState(false)
|
|
59
64
|
const [forwardOpen, setForwardOpen] = React.useState(false)
|
|
@@ -93,6 +98,7 @@ export function useMessageDetailsActions({
|
|
|
93
98
|
context: { resourceKind: 'message', messageId: id, action: 'state-change', retryLastMutation },
|
|
94
99
|
mutationPayload: { messageId: id, action: 'state-change', url, method },
|
|
95
100
|
})
|
|
101
|
+
options?.onSuccess?.()
|
|
96
102
|
} catch (err) {
|
|
97
103
|
flash(
|
|
98
104
|
err instanceof Error
|
|
@@ -203,14 +209,15 @@ export function useMessageDetailsActions({
|
|
|
203
209
|
}, [id, runConversationAction, t])
|
|
204
210
|
|
|
205
211
|
const markConversationUnread = React.useCallback(async (messageId?: string) => {
|
|
212
|
+
suppressAutoMarkRead()
|
|
206
213
|
const targetMessageId = messageId ?? id
|
|
207
214
|
await runConversationAction('markAllUnread', {
|
|
208
215
|
url: `/api/messages/${encodeURIComponent(targetMessageId)}/conversation/read`,
|
|
209
216
|
method: 'DELETE',
|
|
210
217
|
successMessage: t('messages.flash.conversationMarkedUnread', 'Conversation marked unread.'),
|
|
211
|
-
|
|
218
|
+
onSuccess: onMarkedUnread,
|
|
212
219
|
})
|
|
213
|
-
}, [id, runConversationAction, t])
|
|
220
|
+
}, [id, onMarkedUnread, runConversationAction, suppressAutoMarkRead, t])
|
|
214
221
|
|
|
215
222
|
const markConversationRead = React.useCallback(async (messageId?: string) => {
|
|
216
223
|
const targetMessageId = messageId ?? id
|
|
@@ -385,12 +392,15 @@ export function useMessageDetailsActions({
|
|
|
385
392
|
}, [detail?.actionData?.actions])
|
|
386
393
|
|
|
387
394
|
const toggleRead = React.useCallback(async () => {
|
|
395
|
+
if (detail?.isRead) {
|
|
396
|
+
suppressAutoMarkRead()
|
|
397
|
+
}
|
|
388
398
|
await requestAndRefresh(
|
|
389
399
|
`/api/messages/${encodeURIComponent(id)}/read`,
|
|
390
400
|
detail?.isRead ? 'DELETE' : 'PUT',
|
|
391
|
-
detail?.isRead ? { skipDetailAutoMarkRead: true } : undefined,
|
|
401
|
+
detail?.isRead ? { skipDetailAutoMarkRead: true, onSuccess: onMarkedUnread } : undefined,
|
|
392
402
|
)
|
|
393
|
-
}, [detail?.isRead, id, requestAndRefresh])
|
|
403
|
+
}, [detail?.isRead, id, onMarkedUnread, requestAndRefresh, suppressAutoMarkRead])
|
|
394
404
|
|
|
395
405
|
const toggleArchive = React.useCallback(async () => {
|
|
396
406
|
await requestAndRefresh(
|