@open-mercato/core 0.6.6-develop.6205.1.109e4b6a84 → 0.6.6-develop.6227.1.2695efff30
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/currencies/backend/currencies/[id]/page.js +33 -8
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +63 -27
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +35 -14
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js +104 -41
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +57 -8
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +5 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +30 -14
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +29 -19
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +166 -64
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
- package/dist/modules/customers/components/AddressFormatSettings.js +31 -16
- package/dist/modules/customers/components/AddressFormatSettings.js.map +2 -2
- package/dist/modules/customers/components/DictionarySettings.js +57 -31
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/PipelineSettings.js +156 -76
- package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/AgendaList.js +10 -4
- package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +18 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/MonthGrid.js +5 -2
- package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +6 -0
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js +2 -0
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +4 -1
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/lib/calendar/labels.js +33 -0
- package/dist/modules/customers/lib/calendar/labels.js.map +7 -0
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +32 -11
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/[id]/route.js +59 -0
- package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/route.js +44 -0
- package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +31 -0
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/cancel.js +31 -0
- package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js +31 -0
- package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +59 -3
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +33 -4
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/directory/api/organization-branding/route.js +54 -2
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/directory/backend/directory/branding/page.js +15 -9
- package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/page.js +28 -9
- package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/page.js +29 -13
- package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
- package/dist/modules/entities/api/definitions.batch.js +15 -0
- package/dist/modules/entities/api/definitions.batch.js.map +2 -2
- package/dist/modules/entities/api/definitions.js +26 -0
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/definitions.mutation-guard.js +57 -0
- package/dist/modules/entities/api/definitions.mutation-guard.js.map +7 -0
- package/dist/modules/entities/api/definitions.restore.js +15 -0
- package/dist/modules/entities/api/definitions.restore.js.map +2 -2
- package/dist/modules/entities/api/entities.js +31 -3
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +18 -0
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +28 -10
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js +38 -1
- package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/emails/[id]/route.js +30 -0
- package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +30 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +31 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +31 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +68 -39
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +14 -4
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
- package/dist/modules/payment_gateways/api/cancel/route.js +37 -0
- package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/capture/route.js +37 -0
- package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/guards.js +31 -0
- package/dist/modules/payment_gateways/api/guards.js.map +7 -0
- package/dist/modules/payment_gateways/api/refund/route.js +37 -0
- package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/sessions/route.js +37 -0
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/planner/api/availability-date-specific.js +11 -1
- package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js +20 -3
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
- package/dist/modules/planner/commands/availability-date-specific.js +16 -0
- package/dist/modules/planner/commands/availability-date-specific.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +109 -40
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/query_index/api/purge.js +35 -3
- package/dist/modules/query_index/api/purge.js.map +2 -2
- package/dist/modules/query_index/api/reindex.js +41 -3
- package/dist/modules/query_index/api/reindex.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +57 -24
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +31 -0
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/tracking/refresh/route.js +55 -0
- package/dist/modules/shipping_carriers/api/tracking/refresh/route.js.map +7 -0
- package/dist/modules/shipping_carriers/data/validators.js +6 -1
- package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +28 -8
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipping-service.js +37 -7
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/workers/status-poller.js +1 -1
- package/dist/modules/shipping_carriers/workers/status-poller.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +49 -20
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +40 -10
- package/src/modules/currencies/backend/currencies/page.tsx +68 -29
- package/src/modules/currencies/backend/exchange-rates/page.tsx +40 -15
- package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +110 -41
- package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +69 -7
- package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +16 -8
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +32 -14
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +32 -20
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +171 -64
- package/src/modules/customers/components/AddressFormatSettings.tsx +39 -19
- package/src/modules/customers/components/DictionarySettings.tsx +63 -29
- package/src/modules/customers/components/PipelineSettings.tsx +165 -80
- package/src/modules/customers/components/calendar/AgendaList.tsx +13 -8
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +16 -2
- package/src/modules/customers/components/calendar/MonthGrid.tsx +5 -2
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +6 -0
- package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +2 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +7 -0
- package/src/modules/customers/i18n/de.json +11 -7
- package/src/modules/customers/i18n/en.json +6 -2
- package/src/modules/customers/i18n/es.json +11 -7
- package/src/modules/customers/i18n/pl.json +11 -7
- package/src/modules/customers/lib/calendar/labels.ts +42 -0
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +39 -11
- package/src/modules/data_sync/api/mappings/[id]/route.ts +63 -0
- package/src/modules/data_sync/api/mappings/route.ts +48 -0
- package/src/modules/data_sync/api/run.ts +33 -0
- package/src/modules/data_sync/api/runs/[id]/cancel.ts +34 -0
- package/src/modules/data_sync/api/runs/[id]/retry.ts +33 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +64 -2
- package/src/modules/data_sync/api/schedules/route.ts +36 -4
- package/src/modules/directory/api/organization-branding/route.ts +61 -0
- package/src/modules/directory/backend/directory/branding/page.tsx +16 -10
- package/src/modules/directory/backend/directory/organizations/page.tsx +35 -8
- package/src/modules/directory/backend/directory/tenants/page.tsx +37 -13
- package/src/modules/entities/api/definitions.batch.ts +17 -0
- package/src/modules/entities/api/definitions.mutation-guard.ts +80 -0
- package/src/modules/entities/api/definitions.restore.ts +17 -0
- package/src/modules/entities/api/definitions.ts +30 -0
- package/src/modules/entities/api/entities.ts +35 -3
- package/src/modules/entities/api/records.ts +20 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +33 -10
- package/src/modules/feature_toggles/api/overrides/route.ts +44 -1
- package/src/modules/inbox_ops/api/emails/[id]/route.ts +32 -0
- package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +32 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +33 -0
- package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +33 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +80 -42
- package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +21 -4
- package/src/modules/payment_gateways/api/cancel/route.ts +39 -0
- package/src/modules/payment_gateways/api/capture/route.ts +39 -0
- package/src/modules/payment_gateways/api/guards.ts +59 -0
- package/src/modules/payment_gateways/api/refund/route.ts +39 -0
- package/src/modules/payment_gateways/api/sessions/route.ts +40 -0
- package/src/modules/planner/api/availability-date-specific.ts +10 -0
- package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +26 -5
- package/src/modules/planner/commands/availability-date-specific.ts +24 -0
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +122 -41
- package/src/modules/query_index/api/purge.ts +37 -3
- package/src/modules/query_index/api/reindex.ts +43 -3
- package/src/modules/query_index/components/QueryIndexesTable.tsx +66 -24
- package/src/modules/shipping_carriers/api/shipments/route.ts +31 -0
- package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +53 -0
- package/src/modules/shipping_carriers/data/validators.ts +5 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +29 -8
- package/src/modules/shipping_carriers/lib/shipping-service.ts +43 -7
- package/src/modules/shipping_carriers/workers/status-poller.ts +1 -1
- package/src/modules/translations/components/TranslationManager.tsx +65 -21
|
@@ -539,6 +539,26 @@ export async function DELETE(req: Request) {
|
|
|
539
539
|
if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
|
|
540
540
|
const isCustomEntity = entityKind === 'custom'
|
|
541
541
|
await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {
|
|
545
|
+
entityType: entityId,
|
|
546
|
+
entityId: recordId,
|
|
547
|
+
organizationId: targetOrgId,
|
|
548
|
+
})
|
|
549
|
+
enforceCommandOptimisticLock({
|
|
550
|
+
resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,
|
|
551
|
+
resourceId: recordId,
|
|
552
|
+
current: currentUpdatedAt,
|
|
553
|
+
request: req,
|
|
554
|
+
})
|
|
555
|
+
} catch (lockError) {
|
|
556
|
+
if (isCrudHttpError(lockError)) {
|
|
557
|
+
return NextResponse.json(lockError.body, { status: lockError.status })
|
|
558
|
+
}
|
|
559
|
+
throw lockError
|
|
560
|
+
}
|
|
561
|
+
|
|
542
562
|
await de.deleteCustomEntityRecord({ entityId, recordId, organizationId: targetOrgId, tenantId: auth.tenantId!, soft: true })
|
|
543
563
|
return NextResponse.json({ ok: true })
|
|
544
564
|
} catch (e) {
|
|
@@ -17,6 +17,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
|
17
17
|
import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
18
18
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
19
19
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
20
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
20
21
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
21
22
|
import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
|
|
22
23
|
import { useRecordsEntityGuard } from '@open-mercato/core/modules/entities/components/useRecordsEntityGuard'
|
|
@@ -67,6 +68,7 @@ export default function RecordsPage({ params }: { params: { entityId?: string }
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function RecordsPageInner({ params }: { params: { entityId?: string } }) {
|
|
71
|
+
const t = useT()
|
|
70
72
|
const entityId = decodeURIComponent(params?.entityId || '')
|
|
71
73
|
const [sorting, setSorting] = React.useState<SortingState>([{ id: 'id', desc: false }])
|
|
72
74
|
const [page, setPage] = React.useState(1)
|
|
@@ -80,6 +82,16 @@ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
|
|
|
80
82
|
const [loading, setLoading] = React.useState(false)
|
|
81
83
|
const scopeVersion = useOrganizationScopeVersion()
|
|
82
84
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
85
|
+
const deleteMutationContextId = `entities.user.records.${entityId}:single-delete`
|
|
86
|
+
const { runMutation: runDeleteMutation, retryLastMutation: retryDeleteMutation } = useGuardedMutation<{
|
|
87
|
+
formId: string
|
|
88
|
+
resourceKind: string
|
|
89
|
+
resourceId: string
|
|
90
|
+
retryLastMutation: () => Promise<boolean>
|
|
91
|
+
}>({
|
|
92
|
+
contextId: deleteMutationContextId,
|
|
93
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
94
|
+
})
|
|
83
95
|
const { data: cfDefs = [] } = useCustomFieldDefs(entityId, {
|
|
84
96
|
enabled: Boolean(entityId),
|
|
85
97
|
keyExtras: [scopeVersion],
|
|
@@ -367,22 +379,33 @@ export RECORD_ID="<record uuid>"`}</code></pre>
|
|
|
367
379
|
items={[
|
|
368
380
|
{ id: 'edit', label: 'Edit', href: `/backend/entities/user/${encodeURIComponent(entityId)}/records/${encodeURIComponent(String((row as any).id))}` },
|
|
369
381
|
{ id: 'delete', label: 'Delete', destructive: true, onSelect: async () => {
|
|
382
|
+
const recordId = String((row as any).id)
|
|
370
383
|
try {
|
|
371
384
|
const confirmed = await confirm({
|
|
372
385
|
title: 'Delete this record?',
|
|
373
386
|
variant: 'destructive',
|
|
374
387
|
})
|
|
375
388
|
if (!confirmed) return
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
389
|
+
await runDeleteMutation({
|
|
390
|
+
operation: async () => {
|
|
391
|
+
const deleteCall = await withScopedApiRequestHeaders(
|
|
392
|
+
buildOptimisticLockHeader((row as any).updatedAt),
|
|
393
|
+
() => apiCall(
|
|
394
|
+
`/api/entities/records?entityId=${encodeURIComponent(entityId)}&recordId=${encodeURIComponent(recordId)}`,
|
|
395
|
+
{ method: 'DELETE' },
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
if (!deleteCall.ok) {
|
|
399
|
+
await raiseCrudError(deleteCall.response, 'Failed to delete record')
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
context: {
|
|
403
|
+
formId: deleteMutationContextId,
|
|
404
|
+
resourceKind: 'entities.record',
|
|
405
|
+
resourceId: recordId,
|
|
406
|
+
retryLastMutation: retryDeleteMutation,
|
|
407
|
+
},
|
|
408
|
+
})
|
|
386
409
|
const j = await readApiResultOrThrow<RecordsResponse>(
|
|
387
410
|
`/api/entities/records?entityId=${encodeURIComponent(entityId)}&page=${page}&pageSize=${pageSize}`,
|
|
388
411
|
undefined,
|
|
@@ -6,6 +6,10 @@ import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/op
|
|
|
6
6
|
import { getOverrides } from '../../lib/queries'
|
|
7
7
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
8
|
import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
9
|
+
import {
|
|
10
|
+
runCrudMutationGuardAfterSuccess,
|
|
11
|
+
validateCrudMutationGuard,
|
|
12
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
9
13
|
import { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'
|
|
10
14
|
import { FeatureToggleOverride } from '../../data/entities'
|
|
11
15
|
import { buildContext } from '../../lib/utils'
|
|
@@ -75,7 +79,7 @@ export async function PUT(req: Request) {
|
|
|
75
79
|
return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
const { scope } = await resolveFeatureCheckContext({
|
|
82
|
+
const { scope, organizationId } = await resolveFeatureCheckContext({
|
|
79
83
|
container: ctx.container,
|
|
80
84
|
auth: ctx.auth,
|
|
81
85
|
request: req
|
|
@@ -104,6 +108,31 @@ export async function PUT(req: Request) {
|
|
|
104
108
|
})
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
// Run the CRUD mutation guard lifecycle so this custom write route honors
|
|
112
|
+
// module-level guard injections (and after-success hooks) like every other
|
|
113
|
+
// mutating endpoint. The override either updates an existing row or creates a
|
|
114
|
+
// new one, so the resource id falls back to the toggle id when no row exists.
|
|
115
|
+
const guardUserId = ctx.auth?.userId ?? ctx.auth?.sub ?? null
|
|
116
|
+
if (!guardUserId) {
|
|
117
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
118
|
+
}
|
|
119
|
+
const guardResourceKind = 'feature_toggles.feature_toggle_override'
|
|
120
|
+
const guardResourceId = existingOverride?.id ?? parsed.data.toggleId
|
|
121
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
122
|
+
tenantId: scope.tenantId,
|
|
123
|
+
organizationId: organizationId ?? null,
|
|
124
|
+
userId: guardUserId,
|
|
125
|
+
resourceKind: guardResourceKind,
|
|
126
|
+
resourceId: guardResourceId,
|
|
127
|
+
operation: 'update',
|
|
128
|
+
requestMethod: req.method,
|
|
129
|
+
requestHeaders: req.headers,
|
|
130
|
+
mutationPayload: parsed.data,
|
|
131
|
+
})
|
|
132
|
+
if (guardResult && !guardResult.ok) {
|
|
133
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
134
|
+
}
|
|
135
|
+
|
|
107
136
|
const commandBus = ctx.container.resolve('commandBus') as CommandBus
|
|
108
137
|
const { result, logEntry } = await commandBus.execute<ProcessedChangeOverrideStateInput, { overrideToggleId: string | null }>('feature_toggles.overrides.changeState', {
|
|
109
138
|
input: {
|
|
@@ -115,6 +144,20 @@ export async function PUT(req: Request) {
|
|
|
115
144
|
ctx,
|
|
116
145
|
})
|
|
117
146
|
|
|
147
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
148
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
149
|
+
tenantId: scope.tenantId,
|
|
150
|
+
organizationId: organizationId ?? null,
|
|
151
|
+
userId: guardUserId,
|
|
152
|
+
resourceKind: guardResourceKind,
|
|
153
|
+
resourceId: result?.overrideToggleId ?? guardResourceId,
|
|
154
|
+
operation: 'update',
|
|
155
|
+
requestMethod: req.method,
|
|
156
|
+
requestHeaders: req.headers,
|
|
157
|
+
metadata: guardResult.metadata ?? null,
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
118
161
|
const response = NextResponse.json({
|
|
119
162
|
ok: true,
|
|
120
163
|
overrideToggleId: result?.overrideToggleId ?? null,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
3
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
|
+
import {
|
|
5
|
+
runCrudMutationGuardAfterSuccess,
|
|
6
|
+
validateCrudMutationGuard,
|
|
7
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
4
8
|
import { InboxEmail } from '../../../data/entities'
|
|
5
9
|
import {
|
|
6
10
|
resolveRequestContext,
|
|
@@ -62,6 +66,20 @@ export async function DELETE(req: Request) {
|
|
|
62
66
|
|
|
63
67
|
const ctx = await resolveRequestContext(req)
|
|
64
68
|
|
|
69
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
70
|
+
tenantId: ctx.tenantId,
|
|
71
|
+
organizationId: ctx.organizationId,
|
|
72
|
+
userId: ctx.userId,
|
|
73
|
+
resourceKind: 'inbox_ops:inbox_email',
|
|
74
|
+
resourceId: id,
|
|
75
|
+
operation: 'delete',
|
|
76
|
+
requestMethod: req.method,
|
|
77
|
+
requestHeaders: req.headers,
|
|
78
|
+
})
|
|
79
|
+
if (guardResult && !guardResult.ok) {
|
|
80
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
81
|
+
}
|
|
82
|
+
|
|
65
83
|
const updated = await ctx.em.nativeUpdate(
|
|
66
84
|
InboxEmail,
|
|
67
85
|
{
|
|
@@ -77,6 +95,20 @@ export async function DELETE(req: Request) {
|
|
|
77
95
|
return NextResponse.json({ error: 'Email not found' }, { status: 404 })
|
|
78
96
|
}
|
|
79
97
|
|
|
98
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
99
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
100
|
+
tenantId: ctx.tenantId,
|
|
101
|
+
organizationId: ctx.organizationId,
|
|
102
|
+
userId: ctx.userId,
|
|
103
|
+
resourceKind: 'inbox_ops:inbox_email',
|
|
104
|
+
resourceId: id,
|
|
105
|
+
operation: 'delete',
|
|
106
|
+
requestMethod: req.method,
|
|
107
|
+
requestHeaders: req.headers,
|
|
108
|
+
metadata: guardResult.metadata ?? null,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
return NextResponse.json({ ok: true })
|
|
81
113
|
} catch (err) {
|
|
82
114
|
if (err instanceof UnauthorizedError) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
3
|
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
4
|
+
import {
|
|
5
|
+
runCrudMutationGuardAfterSuccess,
|
|
6
|
+
validateCrudMutationGuard,
|
|
7
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
4
8
|
import { acceptAllActions } from '../../../../lib/executionEngine'
|
|
5
9
|
import { resolveCache, invalidateCountsCache } from '../../../../lib/cache'
|
|
6
10
|
import {
|
|
@@ -22,6 +26,20 @@ export async function POST(req: Request) {
|
|
|
22
26
|
const proposal = await resolveProposal(new URL(req.url), ctx)
|
|
23
27
|
if (isErrorResponse(proposal)) return proposal
|
|
24
28
|
|
|
29
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
30
|
+
tenantId: ctx.tenantId,
|
|
31
|
+
organizationId: ctx.organizationId,
|
|
32
|
+
userId: ctx.userId,
|
|
33
|
+
resourceKind: 'inbox_ops:inbox_proposal',
|
|
34
|
+
resourceId: proposal.id,
|
|
35
|
+
operation: 'custom',
|
|
36
|
+
requestMethod: req.method,
|
|
37
|
+
requestHeaders: req.headers,
|
|
38
|
+
})
|
|
39
|
+
if (guardResult && !guardResult.ok) {
|
|
40
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
const entities = resolveCrossModuleEntities(ctx.container)
|
|
26
44
|
const { results, stoppedOnFailure } = await acceptAllActions(
|
|
27
45
|
proposal.id,
|
|
@@ -31,6 +49,20 @@ export async function POST(req: Request) {
|
|
|
31
49
|
const succeeded = results.filter((r) => r.success).length
|
|
32
50
|
const failed = results.filter((r) => !r.success).length
|
|
33
51
|
|
|
52
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
53
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
54
|
+
tenantId: ctx.tenantId,
|
|
55
|
+
organizationId: ctx.organizationId,
|
|
56
|
+
userId: ctx.userId,
|
|
57
|
+
resourceKind: 'inbox_ops:inbox_proposal',
|
|
58
|
+
resourceId: proposal.id,
|
|
59
|
+
operation: 'custom',
|
|
60
|
+
requestMethod: req.method,
|
|
61
|
+
requestHeaders: req.headers,
|
|
62
|
+
metadata: guardResult.metadata ?? null,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
34
66
|
const cache = resolveCache(ctx.container)
|
|
35
67
|
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))
|
|
36
68
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
3
|
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
4
|
+
import {
|
|
5
|
+
runCrudMutationGuardAfterSuccess,
|
|
6
|
+
validateCrudMutationGuard,
|
|
7
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
4
8
|
import { actionEditSchema, validateActionPayloadForType } from '../../../../../data/validators'
|
|
5
9
|
import { emitInboxOpsEvent } from '../../../../../events'
|
|
6
10
|
import { resolveCache, invalidateCountsCache } from '../../../../../lib/cache'
|
|
@@ -39,9 +43,38 @@ export async function PATCH(req: Request) {
|
|
|
39
43
|
return NextResponse.json({ error: payloadValidation.error }, { status: 400 })
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
47
|
+
tenantId: ctx.tenantId,
|
|
48
|
+
organizationId: ctx.organizationId,
|
|
49
|
+
userId: ctx.userId,
|
|
50
|
+
resourceKind: 'inbox_ops:inbox_proposal_action',
|
|
51
|
+
resourceId: action.id,
|
|
52
|
+
operation: 'update',
|
|
53
|
+
requestMethod: req.method,
|
|
54
|
+
requestHeaders: req.headers,
|
|
55
|
+
mutationPayload: parsed.data,
|
|
56
|
+
})
|
|
57
|
+
if (guardResult && !guardResult.ok) {
|
|
58
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
action.payload = mergedPayload
|
|
43
62
|
await ctx.em.flush()
|
|
44
63
|
|
|
64
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
65
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
66
|
+
tenantId: ctx.tenantId,
|
|
67
|
+
organizationId: ctx.organizationId,
|
|
68
|
+
userId: ctx.userId,
|
|
69
|
+
resourceKind: 'inbox_ops:inbox_proposal_action',
|
|
70
|
+
resourceId: action.id,
|
|
71
|
+
operation: 'update',
|
|
72
|
+
requestMethod: req.method,
|
|
73
|
+
requestHeaders: req.headers,
|
|
74
|
+
metadata: guardResult.metadata ?? null,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
const cache = resolveCache(ctx.container)
|
|
46
79
|
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))
|
|
47
80
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
3
|
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
4
|
+
import {
|
|
5
|
+
runCrudMutationGuardAfterSuccess,
|
|
6
|
+
validateCrudMutationGuard,
|
|
7
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
4
8
|
import { categorizeProposalSchema } from '../../../../data/validators'
|
|
5
9
|
import { resolveCache, invalidateCountsCache } from '../../../../lib/cache'
|
|
6
10
|
import {
|
|
@@ -27,10 +31,39 @@ export async function POST(req: Request) {
|
|
|
27
31
|
return NextResponse.json({ error: `Invalid category: ${issues}` }, { status: 400 })
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
35
|
+
tenantId: ctx.tenantId,
|
|
36
|
+
organizationId: ctx.organizationId,
|
|
37
|
+
userId: ctx.userId,
|
|
38
|
+
resourceKind: 'inbox_ops:inbox_proposal',
|
|
39
|
+
resourceId: proposal.id,
|
|
40
|
+
operation: 'update',
|
|
41
|
+
requestMethod: req.method,
|
|
42
|
+
requestHeaders: req.headers,
|
|
43
|
+
mutationPayload: parsed.data,
|
|
44
|
+
})
|
|
45
|
+
if (guardResult && !guardResult.ok) {
|
|
46
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
const previousCategory = proposal.category || null
|
|
31
50
|
proposal.category = parsed.data.category
|
|
32
51
|
await ctx.em.flush()
|
|
33
52
|
|
|
53
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
54
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
55
|
+
tenantId: ctx.tenantId,
|
|
56
|
+
organizationId: ctx.organizationId,
|
|
57
|
+
userId: ctx.userId,
|
|
58
|
+
resourceKind: 'inbox_ops:inbox_proposal',
|
|
59
|
+
resourceId: proposal.id,
|
|
60
|
+
operation: 'update',
|
|
61
|
+
requestMethod: req.method,
|
|
62
|
+
requestHeaders: req.headers,
|
|
63
|
+
metadata: guardResult.metadata ?? null,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
34
67
|
const cache = resolveCache(ctx.container)
|
|
35
68
|
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))
|
|
36
69
|
|
|
@@ -5,6 +5,7 @@ import type { UseQueryResult } from '@tanstack/react-query'
|
|
|
5
5
|
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
6
6
|
import type { MessageActionsProps, MessageContentProps } from '@open-mercato/shared/modules/messages/types'
|
|
7
7
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
8
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
8
9
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
10
|
import type {
|
|
10
11
|
ActionResult,
|
|
@@ -21,6 +22,13 @@ type RequestAndRefreshOptions = {
|
|
|
21
22
|
|
|
22
23
|
type ConversationActionKind = 'archiveConversation' | 'deleteConversation' | 'markAllUnread'
|
|
23
24
|
|
|
25
|
+
type MessageDetailMutationContext = {
|
|
26
|
+
resourceKind: 'message' | 'conversation'
|
|
27
|
+
messageId: string
|
|
28
|
+
action: string
|
|
29
|
+
retryLastMutation: () => Promise<boolean>
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
type UseMessageDetailsActionsInput = {
|
|
25
33
|
id: string
|
|
26
34
|
t: TranslateFn
|
|
@@ -51,6 +59,10 @@ export function useMessageDetailsActions({
|
|
|
51
59
|
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = React.useState(false)
|
|
52
60
|
const [activeConversationAction, setActiveConversationAction] = React.useState<ConversationActionKind | null>(null)
|
|
53
61
|
|
|
62
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<MessageDetailMutationContext>({
|
|
63
|
+
contextId: 'messages-detail-actions',
|
|
64
|
+
})
|
|
65
|
+
|
|
54
66
|
const requestAndRefresh = React.useCallback(async (
|
|
55
67
|
url: string,
|
|
56
68
|
method: 'PUT' | 'DELETE',
|
|
@@ -58,18 +70,24 @@ export function useMessageDetailsActions({
|
|
|
58
70
|
) => {
|
|
59
71
|
setUpdatingState(true)
|
|
60
72
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
await runMutation({
|
|
74
|
+
operation: async () => {
|
|
75
|
+
const call = await apiCall<{ ok?: boolean }>(url, { method })
|
|
76
|
+
if (!call.ok) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
toErrorMessage(call.result)
|
|
79
|
+
?? t('messages.errors.stateChangeFailed', 'Failed to update message state.'),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
if (options?.skipDetailAutoMarkRead) {
|
|
83
|
+
await refreshDetailWithoutAutoMarkRead()
|
|
84
|
+
} else {
|
|
85
|
+
await detailQuery.refetch()
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
context: { resourceKind: 'message', messageId: id, action: 'state-change', retryLastMutation },
|
|
89
|
+
mutationPayload: { messageId: id, action: 'state-change', url, method },
|
|
90
|
+
})
|
|
73
91
|
} catch (err) {
|
|
74
92
|
flash(
|
|
75
93
|
err instanceof Error
|
|
@@ -80,17 +98,23 @@ export function useMessageDetailsActions({
|
|
|
80
98
|
} finally {
|
|
81
99
|
setUpdatingState(false)
|
|
82
100
|
}
|
|
83
|
-
}, [detailQuery, refreshDetailWithoutAutoMarkRead, t])
|
|
101
|
+
}, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])
|
|
84
102
|
|
|
85
103
|
const handleDelete = React.useCallback(async () => {
|
|
86
104
|
setUpdatingState(true)
|
|
87
105
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
await runMutation({
|
|
107
|
+
operation: async () => {
|
|
108
|
+
const call = await apiCall<{ ok?: boolean }>(`/api/messages/${encodeURIComponent(id)}`, {
|
|
109
|
+
method: 'DELETE',
|
|
110
|
+
})
|
|
111
|
+
if (!call.ok) {
|
|
112
|
+
throw new Error(toErrorMessage(call.result) ?? t('messages.errors.deleteFailed', 'Failed to delete message.'))
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
context: { resourceKind: 'message', messageId: id, action: 'delete', retryLastMutation },
|
|
116
|
+
mutationPayload: { messageId: id, action: 'delete' },
|
|
90
117
|
})
|
|
91
|
-
if (!call.ok) {
|
|
92
|
-
throw new Error(toErrorMessage(call.result) ?? t('messages.errors.deleteFailed', 'Failed to delete message.'))
|
|
93
|
-
}
|
|
94
118
|
flash(t('messages.flash.deleted', 'Message deleted.'), 'success')
|
|
95
119
|
onDeleted()
|
|
96
120
|
} catch (err) {
|
|
@@ -103,7 +127,7 @@ export function useMessageDetailsActions({
|
|
|
103
127
|
} finally {
|
|
104
128
|
setUpdatingState(false)
|
|
105
129
|
}
|
|
106
|
-
}, [id, onDeleted, t])
|
|
130
|
+
}, [id, onDeleted, retryLastMutation, runMutation, t])
|
|
107
131
|
|
|
108
132
|
const runConversationAction = React.useCallback(async (
|
|
109
133
|
action: ConversationActionKind,
|
|
@@ -117,13 +141,19 @@ export function useMessageDetailsActions({
|
|
|
117
141
|
) => {
|
|
118
142
|
setActiveConversationAction(action)
|
|
119
143
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
await runMutation({
|
|
145
|
+
operation: async () => {
|
|
146
|
+
const call = await apiCall<{ ok?: boolean; affectedCount?: number }>(options.url, { method: options.method })
|
|
147
|
+
if (!call.ok) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
toErrorMessage(call.result)
|
|
150
|
+
?? t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
context: { resourceKind: 'conversation', messageId: id, action, retryLastMutation },
|
|
155
|
+
mutationPayload: { messageId: id, action, url: options.url, method: options.method },
|
|
156
|
+
})
|
|
127
157
|
|
|
128
158
|
flash(options.successMessage, 'success')
|
|
129
159
|
|
|
@@ -147,7 +177,7 @@ export function useMessageDetailsActions({
|
|
|
147
177
|
} finally {
|
|
148
178
|
setActiveConversationAction(null)
|
|
149
179
|
}
|
|
150
|
-
}, [detailQuery, refreshDetailWithoutAutoMarkRead, t])
|
|
180
|
+
}, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])
|
|
151
181
|
|
|
152
182
|
const archiveConversation = React.useCallback(async (messageId?: string) => {
|
|
153
183
|
const targetMessageId = messageId ?? id
|
|
@@ -194,21 +224,29 @@ export function useMessageDetailsActions({
|
|
|
194
224
|
const executeAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {
|
|
195
225
|
setExecutingActionId(action.id)
|
|
196
226
|
try {
|
|
197
|
-
const call = await
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
227
|
+
const call = await runMutation({
|
|
228
|
+
operation: async () => {
|
|
229
|
+
const result = await apiCall<ActionResult>(
|
|
230
|
+
`/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,
|
|
231
|
+
{
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: { 'Content-Type': 'application/json' },
|
|
234
|
+
body: JSON.stringify(payload ?? {}),
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if (!result.ok) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
toErrorMessage(result.result)
|
|
241
|
+
?? t('messages.errors.actionFailed', 'Failed to execute action.'),
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return result
|
|
203
246
|
},
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
throw new Error(
|
|
208
|
-
toErrorMessage(call.result)
|
|
209
|
-
?? t('messages.errors.actionFailed', 'Failed to execute action.'),
|
|
210
|
-
)
|
|
211
|
-
}
|
|
247
|
+
context: { resourceKind: 'message', messageId: id, action: 'execute-action', retryLastMutation },
|
|
248
|
+
mutationPayload: { messageId: id, action: 'execute-action', actionId: action.id },
|
|
249
|
+
})
|
|
212
250
|
|
|
213
251
|
flash(t('messages.flash.actionSuccess', 'Action completed.'), 'success')
|
|
214
252
|
|
|
@@ -233,7 +271,7 @@ export function useMessageDetailsActions({
|
|
|
233
271
|
} finally {
|
|
234
272
|
setExecutingActionId(null)
|
|
235
273
|
}
|
|
236
|
-
}, [detailQuery, id, t])
|
|
274
|
+
}, [detailQuery, id, retryLastMutation, runMutation, t])
|
|
237
275
|
|
|
238
276
|
const handleExecuteAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {
|
|
239
277
|
if (executingActionId || detail?.actionTaken) return
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
5
|
import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
6
7
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
7
8
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
9
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
@@ -35,6 +36,8 @@ const emptySettings: NotificationDeliveryConfig = {
|
|
|
35
36
|
},
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
const SETTINGS_CONTEXT_ID = 'notifications-settings'
|
|
40
|
+
|
|
38
41
|
export function NotificationSettingsPageClient() {
|
|
39
42
|
const t = useT()
|
|
40
43
|
const [settings, setSettings] = React.useState<NotificationDeliveryConfig | null>(null)
|
|
@@ -42,6 +45,15 @@ export function NotificationSettingsPageClient() {
|
|
|
42
45
|
const [saving, setSaving] = React.useState(false)
|
|
43
46
|
const [error, setError] = React.useState<string | null>(null)
|
|
44
47
|
|
|
48
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
49
|
+
formId: string
|
|
50
|
+
resourceKind: string
|
|
51
|
+
retryLastMutation: () => Promise<boolean>
|
|
52
|
+
}>({
|
|
53
|
+
contextId: SETTINGS_CONTEXT_ID,
|
|
54
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
55
|
+
})
|
|
56
|
+
|
|
45
57
|
const fetchSettings = React.useCallback(async () => {
|
|
46
58
|
setLoading(true)
|
|
47
59
|
setError(null)
|
|
@@ -96,10 +108,15 @@ export function NotificationSettingsPageClient() {
|
|
|
96
108
|
if (!settings) return
|
|
97
109
|
setSaving(true)
|
|
98
110
|
try {
|
|
99
|
-
const response = await
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
const response = await runMutation({
|
|
112
|
+
operation: () =>
|
|
113
|
+
apiCall<SettingsResponse>('/api/notifications/settings', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify(settings),
|
|
117
|
+
}),
|
|
118
|
+
context: { formId: SETTINGS_CONTEXT_ID, resourceKind: 'notifications.settings', retryLastMutation },
|
|
119
|
+
mutationPayload: { settings },
|
|
103
120
|
})
|
|
104
121
|
if (!response.ok) {
|
|
105
122
|
const message = response.result?.error || t('notifications.settings.saveError', 'Failed to save notification settings')
|