@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6245.1.2be3b151f8
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/auth/commands/users.js +1 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +9 -1
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +107 -12
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +33 -1
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +37 -0
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +37 -0
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarToolbar.js +4 -6
- package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventBlock.js +2 -6
- package/dist/modules/customers/components/calendar/EventBlock.js.map +2 -2
- package/dist/modules/customers/components/calendar/UpcomingCards.js +9 -7
- package/dist/modules/customers/components/calendar/UpcomingCards.js.map +2 -2
- package/dist/modules/customers/lib/calendar/format.js +27 -0
- package/dist/modules/customers/lib/calendar/format.js.map +7 -0
- package/dist/modules/entities/api/encryption.js +109 -38
- package/dist/modules/entities/api/encryption.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +22 -9
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +10 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +20 -5
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +17 -1
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +2 -0
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/page.js +4 -3
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/integrations/data/entities.js +2 -2
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +32 -0
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +4 -2
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +42 -10
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/payment_gateways/api/status/route.js +91 -6
- package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
- package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
- package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
- package/dist/modules/sales/components/StatusSettings.js +66 -31
- package/dist/modules/sales/components/StatusSettings.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +46 -13
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +43 -8
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +19 -10
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +25 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
- package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
- package/src/modules/customers/api/pipelines/route.ts +112 -13
- package/src/modules/customers/api/settings/address-format/route.ts +36 -1
- package/src/modules/customers/api/tags/assign/route.ts +39 -0
- package/src/modules/customers/api/tags/unassign/route.ts +39 -0
- package/src/modules/customers/components/calendar/CalendarToolbar.tsx +4 -7
- package/src/modules/customers/components/calendar/EventBlock.tsx +2 -6
- package/src/modules/customers/components/calendar/UpcomingCards.tsx +9 -7
- 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/customers/lib/calendar/format.ts +25 -0
- package/src/modules/entities/api/encryption.ts +122 -39
- package/src/modules/entities/components/EncryptionManager.tsx +28 -9
- package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
- package/src/modules/integrations/api/[id]/route.ts +8 -1
- package/src/modules/integrations/api/[id]/state/route.ts +20 -4
- package/src/modules/integrations/api/[id]/version/route.ts +18 -1
- package/src/modules/integrations/api/route.ts +3 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
- package/src/modules/integrations/backend/integrations/page.tsx +8 -5
- package/src/modules/integrations/data/entities.ts +2 -2
- package/src/modules/integrations/lib/credentials-service.ts +35 -0
- package/src/modules/integrations/lib/state-service.ts +3 -0
- package/src/modules/notifications/lib/notificationService.ts +74 -11
- package/src/modules/payment_gateways/api/status/route.ts +97 -5
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
- package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
- package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
- package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
- package/src/modules/sales/components/StatusSettings.tsx +74 -33
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
- package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
- package/src/modules/staff/i18n/de.json +5 -5
- package/src/modules/staff/i18n/es.json +5 -5
- package/src/modules/staff/i18n/pl.json +5 -5
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +57 -13
- package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +57 -9
- package/src/modules/workflows/backend/definitions/page.tsx +19 -10
|
@@ -4,8 +4,16 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
4
4
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
5
5
|
import { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'
|
|
6
6
|
import { upsertEncryptionMapSchema } from '@open-mercato/core/modules/entities/data/validators'
|
|
7
|
+
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
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'
|
|
7
13
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
8
14
|
|
|
15
|
+
const ENCRYPTION_MAP_RESOURCE_KIND = 'entities.encryption_map'
|
|
16
|
+
|
|
9
17
|
export const metadata = {
|
|
10
18
|
GET: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },
|
|
11
19
|
POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },
|
|
@@ -18,6 +26,16 @@ function resolveScope(auth: { tenantId?: string | null; orgId?: string | null })
|
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
|
|
29
|
+
function toIsoOrNull(value: Date | string | null | undefined): string | null {
|
|
30
|
+
if (value == null) return null
|
|
31
|
+
if (value instanceof Date) {
|
|
32
|
+
const ms = value.getTime()
|
|
33
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : null
|
|
34
|
+
}
|
|
35
|
+
const trimmed = String(value).trim()
|
|
36
|
+
return trimmed.length ? trimmed : null
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
export async function GET(req: Request) {
|
|
22
40
|
const url = new URL(req.url)
|
|
23
41
|
const entityId = url.searchParams.get('entityId') || ''
|
|
@@ -26,8 +44,8 @@ export async function GET(req: Request) {
|
|
|
26
44
|
if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
27
45
|
const { tenantId, organizationId } = resolveScope(auth)
|
|
28
46
|
|
|
29
|
-
const
|
|
30
|
-
const em = resolve('em') as any
|
|
47
|
+
const container = await createRequestContainer()
|
|
48
|
+
const em = container.resolve('em') as any
|
|
31
49
|
const repo = em.getRepository(EncryptionMap)
|
|
32
50
|
// Prefer tenant+org, then tenant-global, then global
|
|
33
51
|
const candidates = [
|
|
@@ -51,52 +69,114 @@ export async function GET(req: Request) {
|
|
|
51
69
|
organizationId,
|
|
52
70
|
fields: record?.fieldsJson ?? [],
|
|
53
71
|
isActive: record?.isActive ?? true,
|
|
72
|
+
updatedAt: toIsoOrNull(record?.updatedAt),
|
|
54
73
|
})
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
export async function POST(req: Request) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
try {
|
|
78
|
+
const body = await req.json().catch(() => ({}))
|
|
79
|
+
const parsed = upsertEncryptionMapSchema.safeParse(body)
|
|
80
|
+
if (!parsed.success) {
|
|
81
|
+
return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 400 })
|
|
82
|
+
}
|
|
83
|
+
const auth = await getAuthFromRequest(req)
|
|
84
|
+
if (!auth?.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
85
|
+
const scope = resolveScope(auth)
|
|
86
|
+
const payload = parsed.data
|
|
87
|
+
const tenantId: string = auth.tenantId
|
|
88
|
+
const organizationId = scope.organizationId
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
const container = await createRequestContainer()
|
|
91
|
+
const em = container.resolve('em') as any
|
|
92
|
+
const repo = em.getRepository(EncryptionMap)
|
|
93
|
+
const existing = await repo.findOne({ entityId: payload.entityId, tenantId, organizationId, deletedAt: null })
|
|
94
|
+
|
|
95
|
+
// Reject stale writes: a save started from an older tab must not silently
|
|
96
|
+
// overwrite a newer encryption configuration. No-op when the client did not
|
|
97
|
+
// send the expected-version header (strictly additive).
|
|
98
|
+
if (existing) {
|
|
99
|
+
enforceCommandOptimisticLock({
|
|
100
|
+
resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,
|
|
101
|
+
resourceId: existing.id,
|
|
102
|
+
current: existing.updatedAt,
|
|
103
|
+
request: req,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Mutation-guard contract for custom write routes. The resource is the
|
|
108
|
+
// encryption map for this entity scoped to the tenant/organization.
|
|
109
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
82
110
|
tenantId,
|
|
83
111
|
organizationId,
|
|
84
|
-
|
|
85
|
-
|
|
112
|
+
userId: auth.sub,
|
|
113
|
+
resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,
|
|
114
|
+
resourceId: existing?.id ?? payload.entityId,
|
|
115
|
+
operation: existing ? 'update' : 'create',
|
|
116
|
+
requestMethod: req.method,
|
|
117
|
+
requestHeaders: req.headers,
|
|
118
|
+
mutationPayload: payload,
|
|
86
119
|
})
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
if (guardResult && !guardResult.ok) {
|
|
121
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
122
|
+
}
|
|
89
123
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
124
|
+
let saved: any
|
|
125
|
+
if (existing) {
|
|
126
|
+
existing.fieldsJson = payload.fields
|
|
127
|
+
existing.isActive = payload.isActive ?? true
|
|
128
|
+
existing.updatedAt = new Date()
|
|
129
|
+
await em.persist(existing).flush()
|
|
130
|
+
saved = existing
|
|
131
|
+
} else {
|
|
132
|
+
const map = repo.create({
|
|
133
|
+
entityId: payload.entityId,
|
|
134
|
+
tenantId,
|
|
135
|
+
organizationId,
|
|
136
|
+
fieldsJson: payload.fields,
|
|
137
|
+
isActive: payload.isActive ?? true,
|
|
138
|
+
})
|
|
139
|
+
await em.persist(map).flush()
|
|
140
|
+
saved = map
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
144
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
145
|
+
tenantId,
|
|
146
|
+
organizationId,
|
|
147
|
+
userId: auth.sub,
|
|
148
|
+
resourceKind: ENCRYPTION_MAP_RESOURCE_KIND,
|
|
149
|
+
resourceId: saved?.id ?? payload.entityId,
|
|
150
|
+
operation: existing ? 'update' : 'create',
|
|
151
|
+
requestMethod: req.method,
|
|
152
|
+
requestHeaders: req.headers,
|
|
153
|
+
metadata: guardResult.metadata ?? null,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const svc = container.resolve('tenantEncryptionService') as { invalidateMap?: (e: string, t: string | null, o: string | null) => Promise<void> }
|
|
159
|
+
await svc?.invalidateMap?.(payload.entityId, tenantId, organizationId)
|
|
160
|
+
} catch {
|
|
161
|
+
// best-effort cache bust
|
|
162
|
+
}
|
|
96
163
|
|
|
97
|
-
|
|
164
|
+
return NextResponse.json({ ok: true, updatedAt: toIsoOrNull(saved?.updatedAt) })
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (isCrudHttpError(err)) {
|
|
167
|
+
return NextResponse.json(err.body, { status: err.status })
|
|
168
|
+
}
|
|
169
|
+
throw err
|
|
170
|
+
}
|
|
98
171
|
}
|
|
99
172
|
|
|
173
|
+
const conflictResponseSchema = z.object({
|
|
174
|
+
error: z.string(),
|
|
175
|
+
code: z.string(),
|
|
176
|
+
currentUpdatedAt: z.string(),
|
|
177
|
+
expectedUpdatedAt: z.string(),
|
|
178
|
+
})
|
|
179
|
+
|
|
100
180
|
export const openApi: OpenApiRouteDoc = {
|
|
101
181
|
tag: 'Entities',
|
|
102
182
|
summary: 'Manage encryption maps',
|
|
@@ -105,13 +185,16 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
105
185
|
summary: 'Fetch encryption map',
|
|
106
186
|
description: 'Returns the encrypted field map for the current tenant/organization scope.',
|
|
107
187
|
query: z.object({ entityId: z.string() }),
|
|
108
|
-
responses: [{ status: 200, description: 'Map', schema: z.object({ entityId: z.string(), fields: z.array(z.object({ field: z.string(), hashField: z.string().nullable().optional() })), isActive: z.boolean().optional() }) }],
|
|
188
|
+
responses: [{ status: 200, description: 'Map', schema: z.object({ entityId: z.string(), fields: z.array(z.object({ field: z.string(), hashField: z.string().nullable().optional() })), isActive: z.boolean().optional(), updatedAt: z.string().nullable().optional() }) }],
|
|
109
189
|
},
|
|
110
190
|
POST: {
|
|
111
191
|
summary: 'Upsert encryption map',
|
|
112
|
-
description: 'Creates or updates the encryption map for the current tenant/organization scope.',
|
|
192
|
+
description: 'Creates or updates the encryption map for the current tenant/organization scope. Enforces optimistic locking when the caller sends the expected version header.',
|
|
113
193
|
requestBody: { contentType: 'application/json', schema: upsertEncryptionMapSchema },
|
|
114
|
-
responses: [
|
|
194
|
+
responses: [
|
|
195
|
+
{ status: 200, description: 'Saved', schema: z.object({ ok: z.boolean(), updatedAt: z.string().nullable().optional() }) },
|
|
196
|
+
{ status: 409, description: 'Optimistic-lock conflict (stale write)', schema: conflictResponseSchema },
|
|
197
|
+
],
|
|
115
198
|
},
|
|
116
199
|
},
|
|
117
200
|
}
|
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
} from '@open-mercato/ui/primitives/select'
|
|
14
14
|
import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
15
15
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
16
|
-
import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
16
|
+
import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
17
17
|
import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
18
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
19
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
18
20
|
import { useCustomFieldDefs, type CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'
|
|
19
21
|
import { Plus, Save, Trash2 } from 'lucide-react'
|
|
20
22
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
@@ -34,6 +36,7 @@ type EncryptionMapResponse = {
|
|
|
34
36
|
entityId: string
|
|
35
37
|
fields?: Array<{ field: string; hashField?: string | null }>
|
|
36
38
|
isActive?: boolean
|
|
39
|
+
updatedAt?: string | null
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
type CanonicalOption = { value: string; label?: string }
|
|
@@ -225,6 +228,8 @@ export function EncryptionManager() {
|
|
|
225
228
|
lastMapSignatureRef.current = signature
|
|
226
229
|
}, [mapSignature, map, canonicalOptions, hasUserEdited])
|
|
227
230
|
|
|
231
|
+
const { runMutation } = useGuardedMutation({ contextId: 'entities.encryption-map' })
|
|
232
|
+
|
|
228
233
|
const mutation = useMutation({
|
|
229
234
|
mutationFn: async () => {
|
|
230
235
|
const trimmed = fields
|
|
@@ -243,15 +248,29 @@ export function EncryptionManager() {
|
|
|
243
248
|
if (!trimmed.length) {
|
|
244
249
|
throw new Error(t('entities.encryption.errors.noFields', 'Add at least one field to encrypt'))
|
|
245
250
|
}
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
const payload = { entityId: selectedEntityId, fields: trimmed, isActive }
|
|
252
|
+
// Route the write through the guarded mutation helper so global injection
|
|
253
|
+
// modules (mutation guard, record-lock conflict handling) can run, and send
|
|
254
|
+
// the loaded map's version so a stale tab cannot silently overwrite a newer
|
|
255
|
+
// encryption configuration (the server rejects mismatches with a 409).
|
|
256
|
+
return runMutation({
|
|
257
|
+
operation: async () => {
|
|
258
|
+
const res = await withScopedApiRequestHeaders(
|
|
259
|
+
buildOptimisticLockHeader(map?.updatedAt ?? null),
|
|
260
|
+
() => apiCall('/api/entities/encryption', {
|
|
261
|
+
method: 'POST',
|
|
262
|
+
headers: { 'content-type': 'application/json' },
|
|
263
|
+
body: JSON.stringify(payload),
|
|
264
|
+
}),
|
|
265
|
+
)
|
|
266
|
+
if (!res.ok) {
|
|
267
|
+
await raiseCrudError(res.response, t('entities.encryption.errors.save', 'Failed to save encryption map'))
|
|
268
|
+
}
|
|
269
|
+
return true
|
|
270
|
+
},
|
|
271
|
+
context: {},
|
|
272
|
+
mutationPayload: payload,
|
|
250
273
|
})
|
|
251
|
-
if (!res.ok) {
|
|
252
|
-
await raiseCrudError(res.response, t('entities.encryption.errors.save', 'Failed to save encryption map'))
|
|
253
|
-
}
|
|
254
|
-
return true
|
|
255
274
|
},
|
|
256
275
|
onSuccess: () => {
|
|
257
276
|
flash(t('entities.encryption.flash.saved', 'Encryption map saved'), 'success')
|
|
@@ -3,6 +3,8 @@ import { z } from 'zod'
|
|
|
3
3
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
4
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
5
|
import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
|
|
6
|
+
import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
7
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
8
|
import { emitIntegrationsEvent } from '../../../events'
|
|
7
9
|
import { saveCredentialsSchema } from '../../../data/validators'
|
|
8
10
|
import {
|
|
@@ -58,8 +60,10 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
58
60
|
const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
59
61
|
|
|
60
62
|
let values: Record<string, unknown> | null
|
|
63
|
+
let updatedAt: Date | null
|
|
61
64
|
try {
|
|
62
65
|
values = await credentialsService.resolve(integration.id, scope)
|
|
66
|
+
updatedAt = await credentialsService.resolveUpdatedAt(integration.id, scope)
|
|
63
67
|
} catch (error) {
|
|
64
68
|
if (isCredentialsEncryptionUnavailableError(error)) {
|
|
65
69
|
return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })
|
|
@@ -71,6 +75,7 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
71
75
|
integrationId: integration.id,
|
|
72
76
|
schema: credentialsService.getSchema(integration.id),
|
|
73
77
|
credentials: values ?? {},
|
|
78
|
+
updatedAt: updatedAt?.toISOString() ?? null,
|
|
74
79
|
})
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -130,6 +135,24 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
130
135
|
const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
|
|
131
136
|
const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
132
137
|
|
|
138
|
+
try {
|
|
139
|
+
const currentUpdatedAt = await credentialsService.resolveUpdatedAt(integration.id, scope)
|
|
140
|
+
enforceCommandOptimisticLock({
|
|
141
|
+
resourceKind: 'integrations.integration',
|
|
142
|
+
resourceId: integration.id,
|
|
143
|
+
current: currentUpdatedAt,
|
|
144
|
+
request: req,
|
|
145
|
+
})
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (isCrudHttpError(error)) {
|
|
148
|
+
return NextResponse.json(error.body, { status: error.status })
|
|
149
|
+
}
|
|
150
|
+
if (isCredentialsEncryptionUnavailableError(error)) {
|
|
151
|
+
return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })
|
|
152
|
+
}
|
|
153
|
+
throw error
|
|
154
|
+
}
|
|
155
|
+
|
|
133
156
|
const credentialFieldErrors = collectCredentialUrlValidationErrors(
|
|
134
157
|
credentialsService.getSchema(integration.id),
|
|
135
158
|
payloadData.credentials,
|
|
@@ -59,11 +59,15 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
59
59
|
const logService = container.resolve('integrationLogService') as IntegrationLogService
|
|
60
60
|
const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
61
61
|
|
|
62
|
-
const [credentials, state, analyticsMap] = await Promise.all([
|
|
62
|
+
const [credentials, credentialsUpdatedAt, state, analyticsMap] = await Promise.all([
|
|
63
63
|
credentialsService.resolve(integration.id, scope).catch((err) => {
|
|
64
64
|
if (err instanceof CredentialsEncryptionUnavailableError) return null
|
|
65
65
|
throw err
|
|
66
66
|
}),
|
|
67
|
+
credentialsService.resolveUpdatedAt(integration.id, scope).catch((err) => {
|
|
68
|
+
if (err instanceof CredentialsEncryptionUnavailableError) return null
|
|
69
|
+
throw err
|
|
70
|
+
}),
|
|
67
71
|
stateService.resolveState(integration.id, scope),
|
|
68
72
|
logService.aggregateAnalytics([integration.id], scope, 30),
|
|
69
73
|
])
|
|
@@ -102,6 +106,7 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
102
106
|
lastHealthCheckedAt: resolvedState.lastHealthCheckedAt?.toISOString() ?? null,
|
|
103
107
|
lastHealthLatencyMs: resolvedState.lastHealthLatencyMs,
|
|
104
108
|
enabledAt: resolvedState.enabledAt?.toISOString() ?? null,
|
|
109
|
+
updatedAt: resolvedState.updatedAt?.toISOString() ?? null,
|
|
105
110
|
},
|
|
106
111
|
}
|
|
107
112
|
}),
|
|
@@ -132,8 +137,10 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
132
137
|
lastHealthCheckedAt: state.lastHealthCheckedAt?.toISOString() ?? null,
|
|
133
138
|
lastHealthLatencyMs: state.lastHealthLatencyMs,
|
|
134
139
|
enabledAt: state.enabledAt?.toISOString() ?? null,
|
|
140
|
+
updatedAt: state.updatedAt?.toISOString() ?? null,
|
|
135
141
|
},
|
|
136
142
|
hasCredentials,
|
|
143
|
+
credentialsUpdatedAt: credentialsUpdatedAt?.toISOString() ?? null,
|
|
137
144
|
healthStatus,
|
|
138
145
|
analytics,
|
|
139
146
|
},
|
|
@@ -3,6 +3,8 @@ import { z } from 'zod'
|
|
|
3
3
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
4
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
5
|
import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
|
|
6
|
+
import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
7
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
8
|
import { emitIntegrationsEvent } from '../../../events'
|
|
7
9
|
import { updateStateSchema } from '../../../data/validators'
|
|
8
10
|
import type { IntegrationStateService } from '../../../lib/state-service'
|
|
@@ -80,6 +82,22 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
const stateService = container.resolve('integrationStateService') as IntegrationStateService
|
|
85
|
+
const stateScope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const current = await stateService.resolveState(integration.id, stateScope)
|
|
89
|
+
enforceCommandOptimisticLock({
|
|
90
|
+
resourceKind: 'integrations.integration',
|
|
91
|
+
resourceId: integration.id,
|
|
92
|
+
current: current.updatedAt,
|
|
93
|
+
request: req,
|
|
94
|
+
})
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (isCrudHttpError(error)) {
|
|
97
|
+
return NextResponse.json(error.body, { status: error.status })
|
|
98
|
+
}
|
|
99
|
+
throw error
|
|
100
|
+
}
|
|
83
101
|
|
|
84
102
|
const state = await stateService.upsert(
|
|
85
103
|
integration.id,
|
|
@@ -87,10 +105,7 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
87
105
|
isEnabled: payloadData.isEnabled,
|
|
88
106
|
reauthRequired: payloadData.reauthRequired,
|
|
89
107
|
},
|
|
90
|
-
|
|
91
|
-
organizationId: auth.orgId as string,
|
|
92
|
-
tenantId: auth.tenantId,
|
|
93
|
-
},
|
|
108
|
+
stateScope,
|
|
94
109
|
)
|
|
95
110
|
|
|
96
111
|
await emitIntegrationsEvent('integrations.state.updated', {
|
|
@@ -117,5 +132,6 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
117
132
|
isEnabled: state.isEnabled,
|
|
118
133
|
reauthRequired: state.reauthRequired,
|
|
119
134
|
apiVersion: state.apiVersion ?? null,
|
|
135
|
+
updatedAt: state.updatedAt?.toISOString() ?? null,
|
|
120
136
|
})
|
|
121
137
|
}
|
|
@@ -3,6 +3,8 @@ import { z } from 'zod'
|
|
|
3
3
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
4
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
5
|
import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
|
|
6
|
+
import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
7
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
8
|
import { emitIntegrationsEvent } from '../../../events'
|
|
7
9
|
import { updateVersionSchema } from '../../../data/validators'
|
|
8
10
|
import type { IntegrationStateService } from '../../../lib/state-service'
|
|
@@ -102,7 +104,22 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
102
104
|
const stateService = container.resolve('integrationStateService') as IntegrationStateService
|
|
103
105
|
const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
104
106
|
|
|
105
|
-
const
|
|
107
|
+
const currentState = await stateService.resolveState(integration.id, scope)
|
|
108
|
+
try {
|
|
109
|
+
enforceCommandOptimisticLock({
|
|
110
|
+
resourceKind: 'integrations.integration',
|
|
111
|
+
resourceId: integration.id,
|
|
112
|
+
current: currentState.updatedAt,
|
|
113
|
+
request: req,
|
|
114
|
+
})
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (isCrudHttpError(error)) {
|
|
117
|
+
return NextResponse.json(error.body, { status: error.status })
|
|
118
|
+
}
|
|
119
|
+
throw error
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const before = currentState.apiVersion
|
|
106
123
|
await stateService.upsert(integration.id, { apiVersion: payloadData.apiVersion }, scope)
|
|
107
124
|
|
|
108
125
|
await emitIntegrationsEvent('integrations.version.changed', {
|
|
@@ -93,6 +93,7 @@ export async function GET(req: Request) {
|
|
|
93
93
|
lastHealthCheckedAt: string | null
|
|
94
94
|
lastHealthLatencyMs: number | null
|
|
95
95
|
enabledAt: string | null
|
|
96
|
+
stateUpdatedAt: string | null
|
|
96
97
|
sortEnabledAtMs: number
|
|
97
98
|
sortTitle: string
|
|
98
99
|
sortCategory: string
|
|
@@ -140,6 +141,7 @@ export async function GET(req: Request) {
|
|
|
140
141
|
lastHealthCheckedAt: state.lastHealthCheckedAt?.toISOString() ?? null,
|
|
141
142
|
lastHealthLatencyMs: state.lastHealthLatencyMs,
|
|
142
143
|
enabledAt: state.enabledAt?.toISOString() ?? null,
|
|
144
|
+
stateUpdatedAt: state.updatedAt?.toISOString() ?? null,
|
|
143
145
|
sortEnabledAtMs: enabledAtMs,
|
|
144
146
|
sortTitle: integration.title.toLowerCase(),
|
|
145
147
|
sortCategory: (integration.category ?? '').toLowerCase(),
|
|
@@ -220,6 +222,7 @@ export async function GET(req: Request) {
|
|
|
220
222
|
lastHealthCheckedAt: row.lastHealthCheckedAt,
|
|
221
223
|
lastHealthLatencyMs: row.lastHealthLatencyMs,
|
|
222
224
|
enabledAt: row.enabledAt,
|
|
225
|
+
stateUpdatedAt: row.stateUpdatedAt,
|
|
223
226
|
analytics: analytics ?? {
|
|
224
227
|
lastActivityAt: null,
|
|
225
228
|
totalCount: 0,
|
|
@@ -24,7 +24,8 @@ import { PasswordInput } from '@open-mercato/ui/primitives/password-input'
|
|
|
24
24
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
25
25
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
|
|
26
26
|
import { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'
|
|
27
|
-
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
27
|
+
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
28
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
28
29
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
29
30
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
30
31
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -105,8 +106,10 @@ type IntegrationDetail = {
|
|
|
105
106
|
lastHealthCheckedAt: string | null
|
|
106
107
|
lastHealthLatencyMs: number | null
|
|
107
108
|
enabledAt: string | null
|
|
109
|
+
updatedAt: string | null
|
|
108
110
|
}
|
|
109
111
|
hasCredentials: boolean
|
|
112
|
+
credentialsUpdatedAt?: string | null
|
|
110
113
|
healthStatus: 'healthy' | 'degraded' | 'unhealthy' | 'unconfigured'
|
|
111
114
|
analytics: IntegrationLogAnalytics
|
|
112
115
|
}
|
|
@@ -423,6 +426,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
423
426
|
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
424
427
|
|
|
425
428
|
const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})
|
|
429
|
+
const [credentialsUpdatedAt, setCredentialsUpdatedAt] = React.useState<string | null>(null)
|
|
426
430
|
const [credentialsFormKey, setCredentialsFormKey] = React.useState(0)
|
|
427
431
|
const [isSavingCredentials, setIsSavingCredentials] = React.useState(false)
|
|
428
432
|
|
|
@@ -486,11 +490,14 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
486
490
|
const loadCredentials = React.useCallback(async () => {
|
|
487
491
|
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
488
492
|
if (!currentIntegrationId) return
|
|
489
|
-
const call = await apiCall<{ credentials: Record<string, unknown
|
|
493
|
+
const call = await apiCall<{ credentials: Record<string, unknown>; updatedAt?: string | null }>(
|
|
490
494
|
`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`,
|
|
491
495
|
undefined,
|
|
492
496
|
{ fallback: null },
|
|
493
497
|
)
|
|
498
|
+
if (call.ok && call.result) {
|
|
499
|
+
setCredentialsUpdatedAt(call.result.updatedAt ?? null)
|
|
500
|
+
}
|
|
494
501
|
if (call.ok && call.result?.credentials) {
|
|
495
502
|
const next = { ...call.result.credentials }
|
|
496
503
|
if (currentIntegrationId === 'storage_s3') {
|
|
@@ -671,11 +678,14 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
671
678
|
const call = await runMutationWithContext({
|
|
672
679
|
actionId: 'toggle-state',
|
|
673
680
|
mutationPayload: { integrationId: currentIntegrationId, isEnabled: enabled },
|
|
674
|
-
operation: () =>
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
681
|
+
operation: () => withScopedApiRequestHeaders(
|
|
682
|
+
buildOptimisticLockHeader(detail?.state.updatedAt),
|
|
683
|
+
() => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/state`, {
|
|
684
|
+
method: 'PUT',
|
|
685
|
+
headers: { 'Content-Type': 'application/json' },
|
|
686
|
+
body: JSON.stringify({ isEnabled: enabled }),
|
|
687
|
+
}, { fallback: null }),
|
|
688
|
+
),
|
|
679
689
|
})
|
|
680
690
|
if (call.ok) {
|
|
681
691
|
setDetail((prev) => prev ? {
|
|
@@ -687,6 +697,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
687
697
|
},
|
|
688
698
|
} : prev)
|
|
689
699
|
flash(t('integrations.detail.stateUpdated'), 'success')
|
|
700
|
+
void refreshDetail()
|
|
690
701
|
} else {
|
|
691
702
|
flash(t('integrations.detail.stateError'), 'error')
|
|
692
703
|
}
|
|
@@ -695,7 +706,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
695
706
|
} finally {
|
|
696
707
|
setIsTogglingState(false)
|
|
697
708
|
}
|
|
698
|
-
}, [resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
709
|
+
}, [detail?.state.updatedAt, refreshDetail, resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
699
710
|
|
|
700
711
|
const handleSaveCredentials = React.useCallback(async (values: Record<string, unknown>) => {
|
|
701
712
|
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
@@ -719,17 +730,21 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
719
730
|
actionId: 'save-credentials',
|
|
720
731
|
tabId: 'credentials',
|
|
721
732
|
mutationPayload: { integrationId: currentIntegrationId, credentials: sanitizedValues },
|
|
722
|
-
operation: () =>
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
733
|
+
operation: () => withScopedApiRequestHeaders(
|
|
734
|
+
buildOptimisticLockHeader(credentialsUpdatedAt),
|
|
735
|
+
() => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`, {
|
|
736
|
+
method: 'PUT',
|
|
737
|
+
headers: { 'Content-Type': 'application/json' },
|
|
738
|
+
body: JSON.stringify({ credentials: sanitizedValues }),
|
|
739
|
+
}, { fallback: null }),
|
|
740
|
+
),
|
|
727
741
|
})
|
|
728
742
|
|
|
729
743
|
if (call.ok) {
|
|
730
744
|
setCredValues(sanitizedValues)
|
|
731
745
|
setCredentialsFormKey((current) => current + 1)
|
|
732
746
|
flash(t('integrations.detail.credentials.saved'), 'success')
|
|
747
|
+
void loadCredentials()
|
|
733
748
|
return
|
|
734
749
|
}
|
|
735
750
|
|
|
@@ -745,7 +760,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
745
760
|
} finally {
|
|
746
761
|
setIsSavingCredentials(false)
|
|
747
762
|
}
|
|
748
|
-
}, [resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
763
|
+
}, [credentialsUpdatedAt, loadCredentials, resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
749
764
|
|
|
750
765
|
const handleVersionChange = React.useCallback(async (version: string) => {
|
|
751
766
|
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
@@ -755,22 +770,26 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
755
770
|
actionId: 'change-version',
|
|
756
771
|
tabId: 'version',
|
|
757
772
|
mutationPayload: { integrationId: currentIntegrationId, apiVersion: version },
|
|
758
|
-
operation: () =>
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
773
|
+
operation: () => withScopedApiRequestHeaders(
|
|
774
|
+
buildOptimisticLockHeader(detail?.state.updatedAt),
|
|
775
|
+
() => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/version`, {
|
|
776
|
+
method: 'PUT',
|
|
777
|
+
headers: { 'Content-Type': 'application/json' },
|
|
778
|
+
body: JSON.stringify({ apiVersion: version }),
|
|
779
|
+
}, { fallback: null }),
|
|
780
|
+
),
|
|
763
781
|
})
|
|
764
782
|
if (call.ok) {
|
|
765
783
|
setDetail((prev) => prev ? { ...prev, state: { ...prev.state, apiVersion: version } } : prev)
|
|
766
784
|
flash(t('integrations.detail.version.saved'), 'success')
|
|
785
|
+
void refreshDetail()
|
|
767
786
|
} else {
|
|
768
787
|
flash(t('integrations.detail.version.saveError'), 'error')
|
|
769
788
|
}
|
|
770
789
|
} catch {
|
|
771
790
|
flash(t('integrations.detail.version.saveError'), 'error')
|
|
772
791
|
}
|
|
773
|
-
}, [resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
792
|
+
}, [detail?.state.updatedAt, refreshDetail, resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
774
793
|
|
|
775
794
|
const handleHealthCheck = React.useCallback(async () => {
|
|
776
795
|
const currentIntegrationId = resolveCurrentIntegrationId()
|