@open-mercato/core 0.6.6-develop.6330.1.a261878aa8 → 0.6.6-develop.6332.1.6e73f0f55b
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/dist/modules/attachments/components/AttachmentPartitionSettings.js +3 -0
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
- package/dist/modules/auth/api/logout.js +2 -3
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +2 -2
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/session/refresh.js +5 -6
- package/dist/modules/auth/api/session/refresh.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +2 -2
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/backend/roles/[id]/edit/page.js +12 -0
- package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +12 -0
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/data/entities.js +2 -2
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/requestRedirect.js +31 -3
- package/dist/modules/auth/lib/requestRedirect.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +3 -3
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/api/sets/route.js +3 -3
- package/dist/modules/business_rules/api/sets/route.js.map +2 -2
- package/dist/modules/business_rules/backend/rules/[id]/page.js +12 -1
- package/dist/modules/business_rules/backend/rules/[id]/page.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/[id]/page.js +12 -1
- package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
- package/dist/modules/catalog/api/product-media/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +12 -0
- package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +12 -1
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
- package/dist/modules/catalog/components/PriceKindSettings.js +13 -3
- package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
- package/dist/modules/catalog/lib/bulkDelete.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js +15 -3
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +2 -1
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +12 -1
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +3 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id].js +3 -3
- package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id].js +3 -3
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
- package/dist/modules/customers/api/addresses/route.js +4 -2
- package/dist/modules/customers/api/addresses/route.js.map +2 -2
- package/dist/modules/customers/api/tags/route.js +8 -3
- package/dist/modules/customers/api/tags/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +13 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +24 -9
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +53 -23
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js +13 -3
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +13 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +5 -5
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/commands/pipeline-stages.js +24 -2
- package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
- package/dist/modules/customers/commands/pipelines.js +24 -2
- package/dist/modules/customers/commands/pipelines.js.map +2 -2
- package/dist/modules/customers/components/detail/AddressesSection.js +46 -29
- package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +5 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/notesAdapter.js +25 -18
- package/dist/modules/customers/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +38 -19
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +1 -1
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +32 -15
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-schedule-service.js +19 -5
- package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +3 -3
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js +3 -3
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionariesManager.js +12 -1
- package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +12 -0
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js +12 -0
- package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js.map +2 -2
- package/dist/modules/entities/api/entities.js +2 -2
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +7 -5
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
- package/dist/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.js.map +2 -2
- package/dist/modules/feature_toggles/data/entities.js +1 -1
- package/dist/modules/feature_toggles/data/entities.js.map +2 -2
- package/dist/modules/feature_toggles/data/validators.js +2 -1
- package/dist/modules/feature_toggles/data/validators.js.map +2 -2
- package/dist/modules/feature_toggles/lib/queries.js +2 -1
- package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +12 -1
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +12 -1
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/components/detail/notesAdapter.js +25 -18
- package/dist/modules/resources/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +14 -1
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +11 -10
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/payments.js +6 -1
- package/dist/modules/sales/commands/payments.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +3 -3
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/commands/shared.js +3 -3
- package/dist/modules/sales/commands/shared.js.map +2 -2
- package/dist/modules/sales/commands/shipments.js +6 -1
- package/dist/modules/sales/commands/shipments.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentDialog.js +6 -3
- package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +7 -3
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +6 -3
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +7 -3
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/di.js +39 -3
- package/dist/modules/sales/di.js.map +2 -2
- package/dist/modules/sales/setup.js +2 -0
- package/dist/modules/sales/setup.js.map +2 -2
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +12 -1
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +12 -1
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +12 -1
- package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -1
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/commands/job-histories.js +3 -3
- package/dist/modules/staff/commands/job-histories.js.map +2 -2
- package/dist/modules/staff/components/detail/addressesAdapter.js +33 -24
- package/dist/modules/staff/components/detail/addressesAdapter.js.map +2 -2
- package/dist/modules/staff/components/detail/notesAdapter.js +25 -18
- package/dist/modules/staff/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js +37 -0
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +7 -1
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js +3 -3
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +12 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +11 -1
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +3 -0
- package/src/modules/auth/api/logout.ts +2 -3
- package/src/modules/auth/api/roles/acl/route.ts +2 -2
- package/src/modules/auth/api/session/refresh.ts +5 -6
- package/src/modules/auth/api/users/acl/route.ts +2 -2
- package/src/modules/auth/backend/roles/[id]/edit/page.tsx +17 -0
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +17 -0
- package/src/modules/auth/data/entities.ts +2 -2
- package/src/modules/auth/lib/requestRedirect.ts +44 -2
- package/src/modules/business_rules/api/rules/route.ts +3 -3
- package/src/modules/business_rules/api/sets/route.ts +3 -3
- package/src/modules/business_rules/backend/rules/[id]/page.tsx +17 -1
- package/src/modules/business_rules/backend/sets/[id]/page.tsx +18 -1
- package/src/modules/catalog/api/product-media/route.ts +4 -0
- package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +17 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -0
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +16 -1
- package/src/modules/catalog/components/PriceKindSettings.tsx +15 -7
- package/src/modules/catalog/lib/bulkDelete.ts +6 -0
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -3
- package/src/modules/currencies/backend/currencies/page.tsx +2 -1
- package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +17 -1
- package/src/modules/currencies/backend/exchange-rates/page.tsx +3 -2
- package/src/modules/customer_accounts/api/admin/roles/[id].ts +3 -3
- package/src/modules/customer_accounts/api/admin/users/[id].ts +3 -3
- package/src/modules/customers/api/addresses/route.ts +2 -0
- package/src/modules/customers/api/tags/route.ts +6 -1
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +19 -2
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +29 -8
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +47 -20
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.ts +13 -3
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -2
- package/src/modules/customers/commands/interactions.ts +5 -5
- package/src/modules/customers/commands/pipeline-stages.ts +26 -2
- package/src/modules/customers/commands/pipelines.ts +26 -2
- package/src/modules/customers/components/detail/AddressesSection.tsx +50 -28
- package/src/modules/customers/components/detail/DealForm.tsx +12 -0
- package/src/modules/customers/components/detail/notesAdapter.ts +29 -18
- package/src/modules/customers/i18n/de.json +2 -0
- package/src/modules/customers/i18n/en.json +2 -0
- package/src/modules/customers/i18n/es.json +2 -0
- package/src/modules/customers/i18n/pl.json +2 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +38 -20
- package/src/modules/data_sync/api/schedules/route.ts +1 -1
- package/src/modules/data_sync/backend/data-sync/page.tsx +32 -15
- package/src/modules/data_sync/lib/sync-schedule-service.ts +30 -5
- package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +3 -3
- package/src/modules/dictionaries/api/[dictionaryId]/route.ts +3 -3
- package/src/modules/dictionaries/components/DictionariesManager.tsx +21 -1
- package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +2 -0
- package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +17 -0
- package/src/modules/directory/backend/directory/tenants/[id]/edit/page.tsx +17 -0
- package/src/modules/entities/api/entities.ts +2 -2
- package/src/modules/entities/api/records.ts +7 -5
- package/src/modules/feature_toggles/api/overrides/route.ts +2 -2
- package/src/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.tsx +12 -0
- package/src/modules/feature_toggles/data/entities.ts +1 -1
- package/src/modules/feature_toggles/data/validators.ts +1 -0
- package/src/modules/feature_toggles/lib/queries.ts +1 -0
- package/src/modules/inbox_ops/api/settings/route.ts +2 -2
- package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +17 -1
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +17 -1
- package/src/modules/resources/components/detail/notesAdapter.ts +29 -18
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -1
- package/src/modules/sales/commands/documents.ts +11 -10
- package/src/modules/sales/commands/payments.ts +11 -0
- package/src/modules/sales/commands/returns.ts +3 -3
- package/src/modules/sales/commands/shared.ts +10 -4
- package/src/modules/sales/commands/shipments.ts +14 -0
- package/src/modules/sales/components/documents/PaymentDialog.tsx +7 -3
- package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -3
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +7 -3
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +8 -3
- package/src/modules/sales/di.ts +68 -9
- package/src/modules/sales/setup.ts +2 -0
- package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +17 -1
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +17 -1
- package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +17 -1
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +17 -1
- package/src/modules/staff/commands/job-histories.ts +3 -3
- package/src/modules/staff/components/detail/addressesAdapter.ts +40 -23
- package/src/modules/staff/components/detail/notesAdapter.ts +28 -18
- package/src/modules/translations/api/[entityType]/[entityId]/route.ts +74 -0
- package/src/modules/translations/components/TranslationManager.tsx +14 -1
- package/src/modules/workflows/api/definitions/[id]/route.ts +3 -3
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +18 -1
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +19 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
3
|
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
3
4
|
import { findAndCountWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
|
-
import {
|
|
5
|
+
import { enforceCommandOptimisticLockWithGuards, enforceRecordGoneIsConflict } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
5
6
|
import { SyncSchedule } from '../data/entities'
|
|
6
7
|
|
|
7
8
|
type SyncScope = {
|
|
@@ -131,18 +132,28 @@ export function createSyncScheduleService(em: EntityManager, schedulerService?:
|
|
|
131
132
|
fullSync: boolean
|
|
132
133
|
isEnabled: boolean
|
|
133
134
|
expectedUpdatedAt?: string | null
|
|
134
|
-
}, scope: SyncScope): Promise<SyncSchedule> {
|
|
135
|
+
}, scope: SyncScope, container?: AwilixContainer): Promise<SyncSchedule> {
|
|
135
136
|
const existing = input.id
|
|
136
137
|
? await getById(input.id, scope)
|
|
137
138
|
: await getByKey(input.integrationId, input.entityType, input.direction, scope)
|
|
138
139
|
|
|
139
|
-
if (existing) {
|
|
140
|
-
|
|
140
|
+
if (existing && container) {
|
|
141
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
141
142
|
resourceKind: 'data_sync.schedule',
|
|
142
143
|
resourceId: existing.id,
|
|
143
144
|
current: existing.updatedAt ?? null,
|
|
144
145
|
expected: input.expectedUpdatedAt ?? null,
|
|
145
146
|
})
|
|
147
|
+
} else if (!existing && input.expectedUpdatedAt) {
|
|
148
|
+
// Concurrent delete-then-edit race: the client edited a schedule that was
|
|
149
|
+
// removed before this keyed upsert ran. Surface the unified conflict
|
|
150
|
+
// instead of silently re-creating it. No-op when no expected version was
|
|
151
|
+
// sent (a genuine create) or when OM_OPTIMISTIC_LOCK is off.
|
|
152
|
+
enforceRecordGoneIsConflict({
|
|
153
|
+
resourceKind: 'data_sync.schedule',
|
|
154
|
+
resourceId: input.id ?? `${input.integrationId}:${input.entityType}:${input.direction}`,
|
|
155
|
+
expected: input.expectedUpdatedAt,
|
|
156
|
+
})
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
const row = existing ?? em.create(SyncSchedule, {
|
|
@@ -200,10 +211,24 @@ export function createSyncScheduleService(em: EntityManager, schedulerService?:
|
|
|
200
211
|
return row
|
|
201
212
|
},
|
|
202
213
|
|
|
203
|
-
async deleteSchedule(
|
|
214
|
+
async deleteSchedule(
|
|
215
|
+
id: string,
|
|
216
|
+
scope: SyncScope,
|
|
217
|
+
container?: AwilixContainer,
|
|
218
|
+
expectedUpdatedAt?: string | null,
|
|
219
|
+
): Promise<boolean> {
|
|
204
220
|
const row = await getById(id, scope)
|
|
205
221
|
if (!row) return false
|
|
206
222
|
|
|
223
|
+
if (container) {
|
|
224
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
225
|
+
resourceKind: 'data_sync.schedule',
|
|
226
|
+
resourceId: row.id,
|
|
227
|
+
current: row.updatedAt ?? null,
|
|
228
|
+
expected: expectedUpdatedAt ?? null,
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
207
232
|
const scheduledJobId = row.scheduledJobId ?? row.id
|
|
208
233
|
await requireScheduler().unregister(scheduledJobId)
|
|
209
234
|
|
|
@@ -4,7 +4,7 @@ import { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictiona
|
|
|
4
4
|
import { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'
|
|
5
5
|
import { updateDictionaryEntrySchema } from '@open-mercato/core/modules/dictionaries/data/validators'
|
|
6
6
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
7
|
-
import {
|
|
7
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
8
8
|
import {
|
|
9
9
|
runCrudMutationGuardAfterSuccess,
|
|
10
10
|
validateCrudMutationGuard,
|
|
@@ -76,7 +76,7 @@ export async function PATCH(req: Request, ctx: { params?: { dictionaryId?: strin
|
|
|
76
76
|
})
|
|
77
77
|
const dictionary = await loadDictionary(context, dictionaryId)
|
|
78
78
|
const entry = await loadEntry(context, dictionary, entryId)
|
|
79
|
-
|
|
79
|
+
await enforceCommandOptimisticLockWithGuards(context.container, {
|
|
80
80
|
resourceKind: 'dictionaries.entry',
|
|
81
81
|
resourceId: entry.id,
|
|
82
82
|
current: entry.updatedAt ?? null,
|
|
@@ -178,7 +178,7 @@ export async function DELETE(req: Request, ctx: { params?: { dictionaryId?: stri
|
|
|
178
178
|
})
|
|
179
179
|
const dictionary = await loadDictionary(context, dictionaryId)
|
|
180
180
|
const entry = await loadEntry(context, dictionary, entryId)
|
|
181
|
-
|
|
181
|
+
await enforceCommandOptimisticLockWithGuards(context.container, {
|
|
182
182
|
resourceKind: 'dictionaries.entry',
|
|
183
183
|
resourceId: entry.id,
|
|
184
184
|
current: entry.updatedAt ?? null,
|
|
@@ -3,7 +3,7 @@ import { z } from 'zod'
|
|
|
3
3
|
import { Dictionary } from '@open-mercato/core/modules/dictionaries/data/entities'
|
|
4
4
|
import { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'
|
|
5
5
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
|
-
import {
|
|
6
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
7
7
|
import {
|
|
8
8
|
runCrudMutationGuardAfterSuccess,
|
|
9
9
|
validateCrudMutationGuard,
|
|
@@ -115,7 +115,7 @@ export async function PATCH(req: Request, ctx: { params?: { dictionaryId?: strin
|
|
|
115
115
|
const payload = updateSchema.parse(await req.json().catch(() => ({})))
|
|
116
116
|
const dictionary = await loadDictionary(context, dictionaryId)
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
await enforceCommandOptimisticLockWithGuards(context.container, {
|
|
119
119
|
resourceKind: 'dictionaries.dictionary',
|
|
120
120
|
resourceId: dictionary.id,
|
|
121
121
|
current: dictionary.updatedAt ?? null,
|
|
@@ -236,7 +236,7 @@ export async function DELETE(req: Request, ctx: { params?: { dictionaryId?: stri
|
|
|
236
236
|
const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })
|
|
237
237
|
const dictionary = await loadDictionary(context, dictionaryId)
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
await enforceCommandOptimisticLockWithGuards(context.container, {
|
|
240
240
|
resourceKind: 'dictionaries.dictionary',
|
|
241
241
|
resourceId: dictionary.id,
|
|
242
242
|
current: dictionary.updatedAt ?? null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { useSearchParams } from 'next/navigation'
|
|
4
|
+
import { useSearchParams, usePathname } from 'next/navigation'
|
|
5
5
|
import { Book, Plus, Pencil, Trash2 } from 'lucide-react'
|
|
6
6
|
import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
|
|
7
7
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
@@ -18,6 +18,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
|
18
18
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
19
19
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
20
20
|
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
21
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
21
22
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
22
23
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
23
24
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
@@ -58,6 +59,7 @@ export function DictionariesManager() {
|
|
|
58
59
|
const t = useT()
|
|
59
60
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
60
61
|
const searchParams = useSearchParams()
|
|
62
|
+
const pathname = usePathname()
|
|
61
63
|
const [items, setItems] = React.useState<DictionarySummary[]>([])
|
|
62
64
|
const [selectedId, setSelectedId] = React.useState<string | null>(null)
|
|
63
65
|
const [loading, setLoading] = React.useState(true)
|
|
@@ -366,6 +368,24 @@ export function DictionariesManager() {
|
|
|
366
368
|
|
|
367
369
|
const selectedDictionary = items.find((item) => item.id === selectedId) ?? null
|
|
368
370
|
|
|
371
|
+
// Publish the open dictionary's record context to the AppShell-owned
|
|
372
|
+
// `backend:record:current` mount so the enterprise record_locks widget resolves
|
|
373
|
+
// `dictionaries.dictionary` + id while the manager is open. Editing here is
|
|
374
|
+
// inline/dialog-driven (not a `[id]` detail route or a CrudForm with a stable
|
|
375
|
+
// `injectionSpotId`), so mounting presence on the manager keyed to the selected
|
|
376
|
+
// dictionary is the simplest place to hold the lock — it mirrors the
|
|
377
|
+
// `dictionaries.dictionary` resourceKind the dictionary PATCH route guards on.
|
|
378
|
+
// Cleared automatically when no dictionary is selected or on unmount.
|
|
379
|
+
useSetCurrentRecordInjectionContext(
|
|
380
|
+
buildRecordInjectionContext({
|
|
381
|
+
resourceKind: 'dictionaries.dictionary',
|
|
382
|
+
resourceId: selectedDictionary?.id ?? null,
|
|
383
|
+
updatedAt: selectedDictionary?.updatedAt ?? null,
|
|
384
|
+
data: (selectedDictionary ?? null) as Record<string, unknown> | null,
|
|
385
|
+
path: pathname,
|
|
386
|
+
}),
|
|
387
|
+
)
|
|
388
|
+
|
|
369
389
|
return (
|
|
370
390
|
<div className="grid gap-6 lg:grid-cols-[320px_1fr]">
|
|
371
391
|
<div className="rounded-lg border bg-card p-4 shadow-sm">
|
|
@@ -276,6 +276,8 @@ export function DictionaryEntriesEditor({ dictionaryId, dictionaryName, readOnly
|
|
|
276
276
|
await invalidateDictionaryEntries(queryClient, dictionaryId)
|
|
277
277
|
flash(t('dictionaries.config.entries.success.delete', 'Dictionary entry deleted.'), 'success')
|
|
278
278
|
} catch (err) {
|
|
279
|
+
// Route a concurrent-edit 409 through the single conflict surface (matching
|
|
280
|
+
// the entry save path); fall back to a flash for any other delete failure.
|
|
279
281
|
if (surfaceRecordConflict(err, t)) {
|
|
280
282
|
return
|
|
281
283
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
+
import { usePathname } from 'next/navigation'
|
|
3
4
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
4
5
|
import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
|
|
5
6
|
import { E } from '#generated/entities.ids.generated'
|
|
@@ -17,6 +18,7 @@ import { deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
|
17
18
|
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
18
19
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
19
20
|
import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
21
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
20
22
|
|
|
21
23
|
type TreeResponse = {
|
|
22
24
|
items: OrganizationTreeNode[]
|
|
@@ -46,6 +48,7 @@ const TREE_PADDING = 12
|
|
|
46
48
|
export default function EditOrganizationPage({ params }: { params?: { id?: string } }) {
|
|
47
49
|
const orgId = params?.id
|
|
48
50
|
const t = useT()
|
|
51
|
+
const pathname = usePathname()
|
|
49
52
|
const [initialValues, setInitialValues] = React.useState<Record<string, unknown> | null>(null)
|
|
50
53
|
const [pathLabel, setPathLabel] = React.useState<string>('')
|
|
51
54
|
const [tenantId, setTenantId] = React.useState<string | null>(null)
|
|
@@ -270,6 +273,20 @@ export default function EditOrganizationPage({ params }: { params?: { id?: strin
|
|
|
270
273
|
{ id: 'custom', title: t('directory.organizations.form.group.customFields', 'Custom Data'), column: 2, kind: 'customFields' },
|
|
271
274
|
]), [detailFields, t])
|
|
272
275
|
|
|
276
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
277
|
+
// mount so the enterprise record_locks widget resolves `directory.organization` + id
|
|
278
|
+
// explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the held
|
|
279
|
+
// lock matches the save-time conflict surface for the same organization.
|
|
280
|
+
useSetCurrentRecordInjectionContext(
|
|
281
|
+
buildRecordInjectionContext({
|
|
282
|
+
resourceKind: 'directory.organization',
|
|
283
|
+
resourceId: orgId || null,
|
|
284
|
+
updatedAt: typeof initialValues?.updatedAt === 'string' ? initialValues.updatedAt : null,
|
|
285
|
+
data: initialValues,
|
|
286
|
+
path: pathname,
|
|
287
|
+
}),
|
|
288
|
+
)
|
|
289
|
+
|
|
273
290
|
if (!orgId) {
|
|
274
291
|
return (
|
|
275
292
|
<Page>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
+
import { usePathname } from 'next/navigation'
|
|
3
4
|
import { E } from '#generated/entities.ids.generated'
|
|
4
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
5
6
|
import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
|
|
@@ -9,6 +10,7 @@ import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
|
9
10
|
import { readApiResultOrThrow, apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
10
11
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
11
12
|
import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
13
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
12
14
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
13
15
|
import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
|
|
14
16
|
|
|
@@ -21,6 +23,7 @@ type TenantFormValues = {
|
|
|
21
23
|
|
|
22
24
|
export default function EditTenantPage({ params }: { params?: { id?: string } }) {
|
|
23
25
|
const tenantId = params?.id
|
|
26
|
+
const pathname = usePathname()
|
|
24
27
|
const [initial, setInitial] = React.useState<TenantFormValues | null>(null)
|
|
25
28
|
const [loading, setLoading] = React.useState(true)
|
|
26
29
|
const [error, setError] = React.useState<string | null>(null)
|
|
@@ -84,6 +87,20 @@ export default function EditTenantPage({ params }: { params?: { id?: string } })
|
|
|
84
87
|
return () => { cancelled = true }
|
|
85
88
|
}, [tenantId, t])
|
|
86
89
|
|
|
90
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
91
|
+
// mount so the enterprise record_locks widget resolves `directory.tenant` + id
|
|
92
|
+
// explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the held
|
|
93
|
+
// lock matches the save-time conflict surface for the same tenant.
|
|
94
|
+
useSetCurrentRecordInjectionContext(
|
|
95
|
+
buildRecordInjectionContext({
|
|
96
|
+
resourceKind: 'directory.tenant',
|
|
97
|
+
resourceId: tenantId || null,
|
|
98
|
+
updatedAt: initial?.updatedAt ?? null,
|
|
99
|
+
data: initial as Record<string, unknown> | null,
|
|
100
|
+
path: pathname,
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
|
|
87
104
|
if (!tenantId) return null
|
|
88
105
|
|
|
89
106
|
if (isNotFound) {
|
|
@@ -8,7 +8,7 @@ import { upsertCustomEntitySchema } from '@open-mercato/core/modules/entities/da
|
|
|
8
8
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
9
9
|
import { isSystemEntitySelectable } from '@open-mercato/shared/lib/entities/system-entities'
|
|
10
10
|
import { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'
|
|
11
|
-
import {
|
|
11
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
12
12
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
13
13
|
import {
|
|
14
14
|
beginEntitiesMutationGuard,
|
|
@@ -132,7 +132,7 @@ export async function POST(req: Request) {
|
|
|
132
132
|
let ent = await em.findOne(CustomEntity, where)
|
|
133
133
|
if (ent) {
|
|
134
134
|
try {
|
|
135
|
-
|
|
135
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
136
136
|
resourceKind: CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND,
|
|
137
137
|
resourceId: ent.id,
|
|
138
138
|
current: ent.updatedAt ?? null,
|
|
@@ -11,7 +11,7 @@ import { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared
|
|
|
11
11
|
import { setRecordCustomFields } from '../lib/helpers'
|
|
12
12
|
import { CustomFieldValue } from '../data/entities'
|
|
13
13
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
14
|
-
import {
|
|
14
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
15
15
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
16
16
|
import { getModules } from '@open-mercato/shared/lib/i18n/server'
|
|
17
17
|
import { assertEntityAclForRequest } from '../lib/entityAcl'
|
|
@@ -460,7 +460,8 @@ export async function PUT(req: Request) {
|
|
|
460
460
|
const { entityId, recordId, values } = parsed.data
|
|
461
461
|
|
|
462
462
|
try {
|
|
463
|
-
const
|
|
463
|
+
const container = await createRequestContainer()
|
|
464
|
+
const { resolve } = container
|
|
464
465
|
const de = resolve('dataEngine') as any
|
|
465
466
|
const em = resolve('em') as any
|
|
466
467
|
const rbac = resolve('rbacService') as RbacService
|
|
@@ -510,7 +511,7 @@ export async function PUT(req: Request) {
|
|
|
510
511
|
entityId: rid,
|
|
511
512
|
organizationId: targetOrgId,
|
|
512
513
|
})
|
|
513
|
-
|
|
514
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
514
515
|
resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,
|
|
515
516
|
resourceId: rid,
|
|
516
517
|
current: currentUpdatedAt,
|
|
@@ -558,7 +559,8 @@ export async function DELETE(req: Request) {
|
|
|
558
559
|
const { entityId, recordId } = parsed.data
|
|
559
560
|
|
|
560
561
|
try {
|
|
561
|
-
const
|
|
562
|
+
const container = await createRequestContainer()
|
|
563
|
+
const { resolve } = container
|
|
562
564
|
const de = resolve('dataEngine') as any
|
|
563
565
|
const em = resolve('em') as any
|
|
564
566
|
const rbac = resolve('rbacService') as RbacService
|
|
@@ -576,7 +578,7 @@ export async function DELETE(req: Request) {
|
|
|
576
578
|
entityId: recordId,
|
|
577
579
|
organizationId: targetOrgId,
|
|
578
580
|
})
|
|
579
|
-
|
|
581
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
580
582
|
resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,
|
|
581
583
|
resourceId: recordId,
|
|
582
584
|
current: currentUpdatedAt,
|
|
@@ -5,7 +5,7 @@ import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
|
5
5
|
import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
|
|
6
6
|
import { getOverrides } from '../../lib/queries'
|
|
7
7
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
|
-
import {
|
|
8
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
9
9
|
import {
|
|
10
10
|
runCrudMutationGuardAfterSuccess,
|
|
11
11
|
validateCrudMutationGuard,
|
|
@@ -100,7 +100,7 @@ export async function PUT(req: Request) {
|
|
|
100
100
|
toggle: { id: parsed.data.toggleId },
|
|
101
101
|
})
|
|
102
102
|
if (existingOverride) {
|
|
103
|
-
|
|
103
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
104
104
|
resourceKind: 'feature_toggles.feature_toggle_override',
|
|
105
105
|
resourceId: existingOverride.id,
|
|
106
106
|
current: existingOverride.updatedAt ?? null,
|
|
@@ -57,6 +57,18 @@ export default function EditFeatureTogglePage({ params }: { params?: { id?: stri
|
|
|
57
57
|
)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// record_locks scope decision (Phase 6): the global feature toggle is a
|
|
61
|
+
// NON-TENANT, superadmin-only entity. record_locks enrichment (presence,
|
|
62
|
+
// acquire/heartbeat, conflict upgrade) is tenant-scoped — its decorator skips
|
|
63
|
+
// enrichment when no tenant/resource resolves — so a global (null-tenant) record
|
|
64
|
+
// cannot participate in record_locks. This surface therefore stays OSS-floor
|
|
65
|
+
// guarded ONLY: the CrudForm auto-derives the optimistic-lock header from
|
|
66
|
+
// `optimisticLockUpdatedAt`, the route enforces it via the OSS floor, and a stale
|
|
67
|
+
// save surfaces the unified conflict bar. No `backend:record:current` presence is
|
|
68
|
+
// mounted: it would never resolve a tenant-scoped lock, and this is a
|
|
69
|
+
// single-superadmin surface with no concurrent-editor expectation. Per-tenant
|
|
70
|
+
// override values (`api/global/[id]/override`) ARE tenant-scoped and are enabled
|
|
71
|
+
// in Phase 6b.
|
|
60
72
|
return (
|
|
61
73
|
<Page>
|
|
62
74
|
<PageBody>
|
|
@@ -67,7 +67,7 @@ export class FeatureToggleOverride {
|
|
|
67
67
|
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
68
68
|
createdAt: Date = new Date()
|
|
69
69
|
|
|
70
|
-
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
|
|
70
|
+
@Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
|
|
71
71
|
updatedAt: Date = new Date()
|
|
72
72
|
|
|
73
73
|
@Property({ name: 'value', type: 'jsonb' })
|
|
@@ -104,6 +104,7 @@ export const overrideListResponseSchema = z.object({
|
|
|
104
104
|
name: z.string(),
|
|
105
105
|
category: z.string(),
|
|
106
106
|
isOverride: z.boolean(),
|
|
107
|
+
updatedAt: z.string().nullable().optional(),
|
|
107
108
|
});
|
|
108
109
|
|
|
109
110
|
export type OverrideListResponse = z.infer<typeof overrideListResponseSchema>
|
|
@@ -2,7 +2,7 @@ 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
4
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
-
import {
|
|
5
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
6
6
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
7
7
|
import { InboxSettings } from '../../data/entities'
|
|
8
8
|
import { updateSettingsSchema } from '../../data/validators'
|
|
@@ -104,7 +104,7 @@ export async function PATCH(req: Request) {
|
|
|
104
104
|
// Optimistic lock: refuse a stale overwrite when two tabs edit the same inbox
|
|
105
105
|
// settings record. Strictly additive — a no-op without the expected-version header.
|
|
106
106
|
try {
|
|
107
|
-
|
|
107
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
108
108
|
resourceKind: 'inbox_ops.settings',
|
|
109
109
|
resourceId: settings.id,
|
|
110
110
|
current: settings.updatedAt ?? null,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { useRouter } from 'next/navigation'
|
|
4
|
+
import { usePathname, useRouter } from 'next/navigation'
|
|
5
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
6
|
import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
7
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
7
8
|
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
8
9
|
import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
9
10
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
@@ -19,6 +20,7 @@ export default function ResourcesResourceTypeEditPage({ params }: { params?: { i
|
|
|
19
20
|
const resourceTypeId = params?.id ?? ''
|
|
20
21
|
const t = useT()
|
|
21
22
|
const router = useRouter()
|
|
23
|
+
const pathname = usePathname()
|
|
22
24
|
const [initialValues, setInitialValues] = React.useState<ResourceTypeFormValues | null>(null)
|
|
23
25
|
const [loading, setLoading] = React.useState(true)
|
|
24
26
|
const [error, setError] = React.useState<string | null>(null)
|
|
@@ -108,6 +110,20 @@ export default function ResourcesResourceTypeEditPage({ params }: { params?: { i
|
|
|
108
110
|
router.push('/backend/resources/resource-types')
|
|
109
111
|
}, [resourceCount, resourceTypeId, router, t])
|
|
110
112
|
|
|
113
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
114
|
+
// mount so the enterprise record_locks widget resolves `resources.resourceType` + id
|
|
115
|
+
// explicitly. The resourceKind mirrors the ResourceTypeCrudForm `versionHistory` so the
|
|
116
|
+
// held lock matches the save-time conflict surface for the same resource type.
|
|
117
|
+
useSetCurrentRecordInjectionContext(
|
|
118
|
+
buildRecordInjectionContext({
|
|
119
|
+
resourceKind: 'resources.resourceType',
|
|
120
|
+
resourceId: resourceTypeId || null,
|
|
121
|
+
updatedAt: initialValues?.updatedAt ?? null,
|
|
122
|
+
data: initialValues as Record<string, unknown> | null,
|
|
123
|
+
path: pathname,
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
|
|
111
127
|
if (isNotFound) {
|
|
112
128
|
return (
|
|
113
129
|
<Page>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { useRouter, useSearchParams } from 'next/navigation'
|
|
4
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|
5
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
6
|
import { FormHeader } from '@open-mercato/ui/backend/forms'
|
|
7
7
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
@@ -12,6 +12,7 @@ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors
|
|
|
12
12
|
import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
13
13
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
14
14
|
import { ActivitiesSection, NotesSection, RecordNotFoundState, type SectionAction, type TagOption } from '@open-mercato/ui/backend/detail'
|
|
15
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
15
16
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
16
17
|
import { VersionHistoryAction } from '@open-mercato/ui/backend/version-history'
|
|
17
18
|
import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
|
|
@@ -91,6 +92,7 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
|
|
|
91
92
|
const t = useT()
|
|
92
93
|
const detailTranslator = React.useMemo(() => createTranslatorWithFallback(t), [t])
|
|
93
94
|
const router = useRouter()
|
|
95
|
+
const pathname = usePathname()
|
|
94
96
|
const searchParams = useSearchParams()
|
|
95
97
|
const [initialValues, setInitialValues] = React.useState<Record<string, unknown> | null>(null)
|
|
96
98
|
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
@@ -602,6 +604,20 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
|
|
|
602
604
|
? initialValues.name.trim()
|
|
603
605
|
: t('resources.resources.detail.untitled', 'Unnamed resource')
|
|
604
606
|
|
|
607
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
608
|
+
// mount so the enterprise record_locks widget resolves `resources.resource` + id
|
|
609
|
+
// explicitly. The resourceKind mirrors the VersionHistoryAction config / ResourceCrudForm
|
|
610
|
+
// `versionHistory` so the held lock matches the save-time conflict surface for the resource.
|
|
611
|
+
useSetCurrentRecordInjectionContext(
|
|
612
|
+
buildRecordInjectionContext({
|
|
613
|
+
resourceKind: 'resources.resource',
|
|
614
|
+
resourceId: resourceId || null,
|
|
615
|
+
updatedAt: typeof initialValues?.updatedAt === 'string' ? initialValues.updatedAt : null,
|
|
616
|
+
data: initialValues,
|
|
617
|
+
path: pathname,
|
|
618
|
+
}),
|
|
619
|
+
)
|
|
620
|
+
|
|
605
621
|
if (isNotFound) {
|
|
606
622
|
return (
|
|
607
623
|
<Page>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
3
|
+
import { apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
4
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
4
5
|
import { mapCommentSummary, type NotesDataAdapter } from '@open-mercato/ui/backend/detail/NotesSection'
|
|
5
6
|
|
|
6
7
|
type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
|
|
@@ -61,33 +62,43 @@ export function createResourceNotesAdapter(
|
|
|
61
62
|
)
|
|
62
63
|
return response.result ?? {}
|
|
63
64
|
},
|
|
64
|
-
update: async ({ id, patch }) => {
|
|
65
|
+
update: async ({ id, patch, updatedAt }) => {
|
|
65
66
|
const payload: Record<string, unknown> = { id }
|
|
66
67
|
if (patch.body !== undefined) payload.body = patch.body
|
|
67
68
|
if (patch.appearanceIcon !== undefined) payload.appearanceIcon = patch.appearanceIcon
|
|
68
69
|
if (patch.appearanceColor !== undefined) payload.appearanceColor = patch.appearanceColor
|
|
70
|
+
// Run through the guarded-mutation runner AND send the optimistic-lock header
|
|
71
|
+
// (note's loaded updatedAt) so a stale edit fails with a 409; the shared
|
|
72
|
+
// NotesSection host surfaces it through surfaceRecordConflict instead of
|
|
73
|
+
// silently overwriting.
|
|
69
74
|
await runWrite(
|
|
70
|
-
() =>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
() => withScopedApiRequestHeaders(
|
|
76
|
+
buildOptimisticLockHeader(updatedAt ?? null),
|
|
77
|
+
() => apiCallOrThrow(
|
|
78
|
+
'/api/resources/comments',
|
|
79
|
+
{
|
|
80
|
+
method: 'PUT',
|
|
81
|
+
headers: { 'content-type': 'application/json' },
|
|
82
|
+
body: JSON.stringify(payload),
|
|
83
|
+
},
|
|
84
|
+
{ errorMessage: translator('resources.resources.detail.notes.updateError', 'Failed to update note.') },
|
|
85
|
+
),
|
|
78
86
|
),
|
|
79
87
|
{ operation: 'updateNote', id },
|
|
80
88
|
)
|
|
81
89
|
},
|
|
82
|
-
delete: async ({ id }) => {
|
|
90
|
+
delete: async ({ id, updatedAt }) => {
|
|
83
91
|
await runWrite(
|
|
84
|
-
() =>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
() => withScopedApiRequestHeaders(
|
|
93
|
+
buildOptimisticLockHeader(updatedAt ?? null),
|
|
94
|
+
() => apiCallOrThrow(
|
|
95
|
+
`/api/resources/comments?id=${encodeURIComponent(id)}`,
|
|
96
|
+
{
|
|
97
|
+
method: 'DELETE',
|
|
98
|
+
headers: { 'content-type': 'application/json' },
|
|
99
|
+
},
|
|
100
|
+
{ errorMessage: translator('resources.resources.detail.notes.deleteError', 'Failed to delete note') },
|
|
101
|
+
),
|
|
91
102
|
),
|
|
92
103
|
{ operation: 'deleteNote', id },
|
|
93
104
|
)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"use client"
|
|
4
4
|
|
|
5
5
|
import * as React from 'react'
|
|
6
|
-
import { useSearchParams, useRouter } from 'next/navigation'
|
|
6
|
+
import { useSearchParams, useRouter, usePathname } from 'next/navigation'
|
|
7
7
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
8
8
|
import {
|
|
9
9
|
CustomDataSection,
|
|
@@ -68,6 +68,7 @@ import { ICON_SUGGESTIONS } from '@open-mercato/core/modules/customers/lib/dicti
|
|
|
68
68
|
import { readMarkdownPreferenceCookie, writeMarkdownPreferenceCookie } from '@open-mercato/core/modules/customers/lib/markdownPreference'
|
|
69
69
|
import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
70
70
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
71
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
71
72
|
|
|
72
73
|
function formatMessageAmount(amount: number | null | undefined, currency: string | null | undefined): string | null {
|
|
73
74
|
if (typeof amount !== 'number' || !Number.isFinite(amount)) return null
|
|
@@ -1892,6 +1893,7 @@ export default function SalesDocumentDetailPage({
|
|
|
1892
1893
|
const t = useT()
|
|
1893
1894
|
const router = useRouter()
|
|
1894
1895
|
const searchParams = useSearchParams()
|
|
1896
|
+
const pathname = usePathname()
|
|
1895
1897
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
1896
1898
|
const [loading, setLoading] = React.useState(true)
|
|
1897
1899
|
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
@@ -1972,6 +1974,18 @@ export default function SalesDocumentDetailPage({
|
|
|
1972
1974
|
},
|
|
1973
1975
|
[detailInjectionContext, runMutation],
|
|
1974
1976
|
)
|
|
1977
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
1978
|
+
// mount so record_locks gets presence + the action-log base (updatedAt/data) for
|
|
1979
|
+
// this order/quote. Sub-resource sections inherit this context — no second mount.
|
|
1980
|
+
useSetCurrentRecordInjectionContext(
|
|
1981
|
+
buildRecordInjectionContext({
|
|
1982
|
+
resourceKind: `sales.${kind}`,
|
|
1983
|
+
resourceId: record?.id ?? null,
|
|
1984
|
+
updatedAt: record?.updatedAt ?? null,
|
|
1985
|
+
data: record as Record<string, unknown> | null,
|
|
1986
|
+
path: pathname,
|
|
1987
|
+
}),
|
|
1988
|
+
)
|
|
1975
1989
|
const clearCustomerError = React.useCallback(() => setCustomerError(null), [])
|
|
1976
1990
|
const { data: currencyDictionary } = useCurrencyDictionary()
|
|
1977
1991
|
const scopeVersion = useOrganizationScopeVersion()
|
|
@@ -4244,6 +4258,7 @@ export default function SalesDocumentDetailPage({
|
|
|
4244
4258
|
currencyCode={record.currencyCode ?? null}
|
|
4245
4259
|
organizationId={(record as any)?.organizationId ?? (record as any)?.organization_id ?? null}
|
|
4246
4260
|
tenantId={(record as any)?.tenantId ?? (record as any)?.tenant_id ?? null}
|
|
4261
|
+
documentUpdatedAt={record.updatedAt ?? null}
|
|
4247
4262
|
shippingAddressSnapshot={shippingSnapshot ?? null}
|
|
4248
4263
|
onActionChange={handleSectionActionChange}
|
|
4249
4264
|
onAddComment={appendShipmentComment}
|
|
@@ -4300,6 +4315,7 @@ export default function SalesDocumentDetailPage({
|
|
|
4300
4315
|
currencyCode={record.currencyCode ?? null}
|
|
4301
4316
|
organizationId={(record as any)?.organizationId ?? (record as any)?.organization_id ?? null}
|
|
4302
4317
|
tenantId={(record as any)?.tenantId ?? (record as any)?.tenant_id ?? null}
|
|
4318
|
+
documentUpdatedAt={record.updatedAt ?? null}
|
|
4303
4319
|
onActionChange={handleSectionActionChange}
|
|
4304
4320
|
onPaymentsChange={(payments) => setHasPayments(payments.length > 0)}
|
|
4305
4321
|
onTotalsChange={() => {
|