@open-mercato/core 0.6.6-develop.6330.1.a261878aa8 → 0.6.6-develop.6331.1.a33b8e99b7
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/roles/acl/route.js +2 -2
- package/dist/modules/auth/api/roles/acl/route.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/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/roles/acl/route.ts +2 -2
- 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/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
|
@@ -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={() => {
|
|
@@ -4981,7 +4981,7 @@ const updateQuoteCommand: CommandHandler<
|
|
|
4981
4981
|
if (!quote)
|
|
4982
4982
|
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
4983
4983
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
4984
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
4984
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
4985
4985
|
const shouldInvalidateSentToken = (quote.status ?? null) === "sent";
|
|
4986
4986
|
if (shouldInvalidateSentToken) {
|
|
4987
4987
|
quote.acceptanceToken = null;
|
|
@@ -5213,6 +5213,7 @@ const updateOrderCommand: CommandHandler<
|
|
|
5213
5213
|
if (!order)
|
|
5214
5214
|
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
5215
5215
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
5216
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
5216
5217
|
const previousStatus = normalizeStatusValue(order.status);
|
|
5217
5218
|
let statusChangeNote: SalesNote | null = null;
|
|
5218
5219
|
const shouldRecalculateTotals =
|
|
@@ -6055,7 +6056,7 @@ const convertQuoteToOrderCommand: CommandHandler<
|
|
|
6055
6056
|
),
|
|
6056
6057
|
});
|
|
6057
6058
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
6058
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
6059
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
6059
6060
|
const snapshot = await loadQuoteSnapshot(em, payload.quoteId);
|
|
6060
6061
|
if (!snapshot)
|
|
6061
6062
|
throw new CrudHttpError(404, {
|
|
@@ -6545,7 +6546,7 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
6545
6546
|
if (!order)
|
|
6546
6547
|
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
6547
6548
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
6548
|
-
enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
6549
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
6549
6550
|
|
|
6550
6551
|
const [existingLines, adjustments] = await Promise.all([
|
|
6551
6552
|
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
@@ -6866,7 +6867,7 @@ const orderLineDeleteCommand: CommandHandler<
|
|
|
6866
6867
|
),
|
|
6867
6868
|
});
|
|
6868
6869
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
6869
|
-
enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
6870
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
6870
6871
|
const shipmentCount = await em.count(SalesShipmentItem, {
|
|
6871
6872
|
orderLine: parsed.id,
|
|
6872
6873
|
shipment: { deletedAt: null },
|
|
@@ -7037,7 +7038,7 @@ const quoteLineUpsertCommand: CommandHandler<
|
|
|
7037
7038
|
if (!quote)
|
|
7038
7039
|
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
7039
7040
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
7040
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7041
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7041
7042
|
const [existingLines, adjustments] = await Promise.all([
|
|
7042
7043
|
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|
|
7043
7044
|
em.find(
|
|
@@ -7349,7 +7350,7 @@ const quoteLineDeleteCommand: CommandHandler<
|
|
|
7349
7350
|
if (!quote)
|
|
7350
7351
|
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
7351
7352
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
7352
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7353
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7353
7354
|
const existingLines = await em.find(
|
|
7354
7355
|
SalesQuoteLine,
|
|
7355
7356
|
{ quote },
|
|
@@ -7503,7 +7504,7 @@ const orderAdjustmentUpsertCommand: CommandHandler<
|
|
|
7503
7504
|
if (!order)
|
|
7504
7505
|
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
7505
7506
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
7506
|
-
enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
7507
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
7507
7508
|
if (parsed.scope === "line") {
|
|
7508
7509
|
throw new CrudHttpError(400, {
|
|
7509
7510
|
error: "Line-scoped adjustments are not supported yet.",
|
|
@@ -7797,7 +7798,7 @@ const orderAdjustmentDeleteCommand: CommandHandler<
|
|
|
7797
7798
|
if (!order)
|
|
7798
7799
|
throw new CrudHttpError(404, { error: "Sales order not found" });
|
|
7799
7800
|
ensureOrderScope(ctx, order.organizationId, order.tenantId);
|
|
7800
|
-
enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
7801
|
+
await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
|
|
7801
7802
|
|
|
7802
7803
|
const [existingLines, adjustments] = await Promise.all([
|
|
7803
7804
|
em.find(SalesOrderLine, { order }, { orderBy: { lineNumber: "asc" } }),
|
|
@@ -7962,7 +7963,7 @@ const quoteAdjustmentUpsertCommand: CommandHandler<
|
|
|
7962
7963
|
if (!quote)
|
|
7963
7964
|
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
7964
7965
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
7965
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7966
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
7966
7967
|
if (parsed.scope === "line") {
|
|
7967
7968
|
throw new CrudHttpError(400, {
|
|
7968
7969
|
error: "Line-scoped adjustments are not supported yet.",
|
|
@@ -8254,7 +8255,7 @@ const quoteAdjustmentDeleteCommand: CommandHandler<
|
|
|
8254
8255
|
if (!quote)
|
|
8255
8256
|
throw new CrudHttpError(404, { error: "Sales quote not found" });
|
|
8256
8257
|
ensureQuoteScope(ctx, quote.organizationId, quote.tenantId);
|
|
8257
|
-
enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
8258
|
+
await enforceSalesDocumentOptimisticLock(ctx, quote, SALES_RESOURCE_KIND_QUOTE);
|
|
8258
8259
|
|
|
8259
8260
|
const [existingLines, adjustments] = await Promise.all([
|
|
8260
8261
|
em.find(SalesQuoteLine, { quote }, { orderBy: { lineNumber: "asc" } }),
|