@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,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, apiCall } 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'
|
|
@@ -40,6 +41,7 @@ export default function StaffTeamRoleEditPage({ params }: { params?: { id?: stri
|
|
|
40
41
|
const roleId = params?.id
|
|
41
42
|
const t = useT()
|
|
42
43
|
const router = useRouter()
|
|
44
|
+
const pathname = usePathname()
|
|
43
45
|
const scopeVersion = useOrganizationScopeVersion()
|
|
44
46
|
// optimistic-lock: TeamRoleForm forwards optimisticLockUpdatedAt from initialValues.updatedAt (wrapper auto-derives the header on save + delete).
|
|
45
47
|
const [initialValues, setInitialValues] = React.useState<TeamRoleFormValues | null>(null)
|
|
@@ -202,6 +204,20 @@ export default function StaffTeamRoleEditPage({ params }: { params?: { id?: stri
|
|
|
202
204
|
router.push('/backend/staff/team-roles')
|
|
203
205
|
}, [roleId, router, t])
|
|
204
206
|
|
|
207
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
208
|
+
// mount so the enterprise record_locks widget resolves `staff.teamRole` + id
|
|
209
|
+
// explicitly. The resourceKind mirrors the TeamRoleForm `versionHistory` so the held
|
|
210
|
+
// lock matches the save-time conflict surface for the same team role.
|
|
211
|
+
useSetCurrentRecordInjectionContext(
|
|
212
|
+
buildRecordInjectionContext({
|
|
213
|
+
resourceKind: 'staff.teamRole',
|
|
214
|
+
resourceId: roleId || null,
|
|
215
|
+
updatedAt: initialValues?.updatedAt ?? null,
|
|
216
|
+
data: initialValues as Record<string, unknown> | null,
|
|
217
|
+
path: pathname,
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
|
|
205
221
|
if (isNotFound) {
|
|
206
222
|
return (
|
|
207
223
|
<Page>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import Link from 'next/link'
|
|
5
|
-
import { useRouter, useSearchParams } from 'next/navigation'
|
|
5
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|
6
6
|
import type { ColumnDef, SortingState } from '@tanstack/react-table'
|
|
7
7
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
8
8
|
import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
@@ -16,6 +16,7 @@ import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
|
|
|
16
16
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
17
17
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
18
18
|
import { TeamForm, type TeamFormValues, buildTeamPayload } from '@open-mercato/core/modules/staff/components/TeamForm'
|
|
19
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
19
20
|
import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
|
|
20
21
|
import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
|
|
21
22
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
@@ -60,6 +61,7 @@ export default function StaffTeamEditPage({ params }: { params?: { id?: string }
|
|
|
60
61
|
const teamId = params?.id
|
|
61
62
|
const t = useT()
|
|
62
63
|
const router = useRouter()
|
|
64
|
+
const pathname = usePathname()
|
|
63
65
|
const searchParams = useSearchParams()
|
|
64
66
|
const scopeVersion = useOrganizationScopeVersion()
|
|
65
67
|
const [initialValues, setInitialValues] = React.useState<TeamFormValues | null>(null)
|
|
@@ -315,6 +317,20 @@ export default function StaffTeamEditPage({ params }: { params?: { id?: string }
|
|
|
315
317
|
router.push('/backend/staff/teams')
|
|
316
318
|
}, [teamId, router, t])
|
|
317
319
|
|
|
320
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
321
|
+
// mount so the enterprise record_locks widget resolves `staff.team` + id explicitly.
|
|
322
|
+
// The resourceKind mirrors the TeamForm `versionHistory` so the held lock matches
|
|
323
|
+
// the save-time conflict surface for the same team.
|
|
324
|
+
useSetCurrentRecordInjectionContext(
|
|
325
|
+
buildRecordInjectionContext({
|
|
326
|
+
resourceKind: 'staff.team',
|
|
327
|
+
resourceId: teamId || null,
|
|
328
|
+
updatedAt: initialValues?.updatedAt ?? null,
|
|
329
|
+
data: initialValues as Record<string, unknown> | null,
|
|
330
|
+
path: pathname,
|
|
331
|
+
}),
|
|
332
|
+
)
|
|
333
|
+
|
|
318
334
|
if (isNotFound) {
|
|
319
335
|
return (
|
|
320
336
|
<Page>
|
|
@@ -21,7 +21,7 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
|
21
21
|
import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
|
|
22
22
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
23
23
|
import {
|
|
24
|
-
|
|
24
|
+
enforceCommandOptimisticLockWithGuards,
|
|
25
25
|
enforceRecordGoneIsConflict,
|
|
26
26
|
} from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
27
27
|
import type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
|
|
@@ -218,7 +218,7 @@ const updateJobHistoryCommand: CommandHandler<StaffTeamMemberJobHistoryUpdateInp
|
|
|
218
218
|
}
|
|
219
219
|
ensureTenantScope(ctx, record.tenantId)
|
|
220
220
|
ensureOrganizationScope(ctx, record.organizationId)
|
|
221
|
-
|
|
221
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
222
222
|
resourceKind: JOB_HISTORY_LOCK_RESOURCE_KIND,
|
|
223
223
|
resourceId: record.id,
|
|
224
224
|
current: record.updatedAt ?? null,
|
|
@@ -366,7 +366,7 @@ const deleteJobHistoryCommand: CommandHandler<{ id?: string; updatedAt?: string;
|
|
|
366
366
|
}
|
|
367
367
|
ensureTenantScope(ctx, record.tenantId)
|
|
368
368
|
ensureOrganizationScope(ctx, record.organizationId)
|
|
369
|
-
|
|
369
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
370
370
|
resourceKind: JOB_HISTORY_LOCK_RESOURCE_KIND,
|
|
371
371
|
resourceId: record.id,
|
|
372
372
|
current: record.updatedAt ?? null,
|
|
@@ -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 type { AddressDataAdapter } from '@open-mercato/ui/backend/detail'
|
|
5
6
|
import type { AddressTypesAdapter } from '@open-mercato/ui/backend/detail'
|
|
6
7
|
import { loadStaffDictionaryEntries, createStaffDictionaryEntry } from './dictionaries'
|
|
@@ -77,6 +78,12 @@ export function createStaffAddressAdapter(translator: Translator): AddressDataAd
|
|
|
77
78
|
: typeof record.isPrimary === 'boolean'
|
|
78
79
|
? record.isPrimary
|
|
79
80
|
: false,
|
|
81
|
+
updatedAt:
|
|
82
|
+
typeof record.updated_at === 'string'
|
|
83
|
+
? record.updated_at
|
|
84
|
+
: typeof record.updatedAt === 'string'
|
|
85
|
+
? record.updatedAt
|
|
86
|
+
: null,
|
|
80
87
|
}
|
|
81
88
|
})
|
|
82
89
|
.filter((value): value is NonNullable<typeof value> => value !== null)
|
|
@@ -85,6 +92,7 @@ export function createStaffAddressAdapter(translator: Translator): AddressDataAd
|
|
|
85
92
|
const response = await apiCallOrThrow<Record<string, unknown>>(
|
|
86
93
|
'/api/staff/addresses',
|
|
87
94
|
{
|
|
95
|
+
// optimistic-lock-exempt: address create-only (no prior version to compare)
|
|
88
96
|
method: 'POST',
|
|
89
97
|
headers: { 'content-type': 'application/json' },
|
|
90
98
|
body: JSON.stringify({
|
|
@@ -97,30 +105,39 @@ export function createStaffAddressAdapter(translator: Translator): AddressDataAd
|
|
|
97
105
|
)
|
|
98
106
|
return response.result ?? {}
|
|
99
107
|
},
|
|
100
|
-
update: async ({ id, payload }) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
update: async ({ id, payload, updatedAt }) => {
|
|
109
|
+
// Send the optimistic-lock header (address's loaded updatedAt) so a stale
|
|
110
|
+
// edit fails with a 409; the shared AddressesSection host surfaces it through
|
|
111
|
+
// surfaceRecordConflict instead of silently overwriting.
|
|
112
|
+
await withScopedApiRequestHeaders(
|
|
113
|
+
buildOptimisticLockHeader(updatedAt ?? null),
|
|
114
|
+
() => apiCallOrThrow(
|
|
115
|
+
'/api/staff/addresses',
|
|
116
|
+
{
|
|
117
|
+
method: 'PUT',
|
|
118
|
+
headers: { 'content-type': 'application/json' },
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
id,
|
|
121
|
+
...payload,
|
|
122
|
+
country: payload.country ? payload.country.toUpperCase() : undefined,
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
{ errorMessage: translator('staff.teamMembers.detail.addresses.error', 'Failed to save address.') },
|
|
126
|
+
),
|
|
113
127
|
)
|
|
114
128
|
},
|
|
115
|
-
delete: async ({ id }) => {
|
|
116
|
-
await
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
delete: async ({ id, updatedAt }) => {
|
|
130
|
+
await withScopedApiRequestHeaders(
|
|
131
|
+
buildOptimisticLockHeader(updatedAt ?? null),
|
|
132
|
+
() => apiCallOrThrow(
|
|
133
|
+
'/api/staff/addresses',
|
|
134
|
+
{
|
|
135
|
+
method: 'DELETE',
|
|
136
|
+
headers: { 'content-type': 'application/json' },
|
|
137
|
+
body: JSON.stringify({ id }),
|
|
138
|
+
},
|
|
139
|
+
{ errorMessage: translator('staff.teamMembers.detail.addresses.error', 'Failed to delete address.') },
|
|
140
|
+
),
|
|
124
141
|
)
|
|
125
142
|
},
|
|
126
143
|
}
|
|
@@ -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
|
|
@@ -35,29 +36,38 @@ export function createStaffNotesAdapter(translator: Translator): NotesDataAdapte
|
|
|
35
36
|
)
|
|
36
37
|
return response.result ?? {}
|
|
37
38
|
},
|
|
38
|
-
update: async ({ id, patch }) => {
|
|
39
|
+
update: async ({ id, patch, updatedAt }) => {
|
|
39
40
|
const payload: Record<string, unknown> = { id }
|
|
40
41
|
if (patch.body !== undefined) payload.body = patch.body
|
|
41
42
|
if (patch.appearanceIcon !== undefined) payload.appearanceIcon = patch.appearanceIcon
|
|
42
43
|
if (patch.appearanceColor !== undefined) payload.appearanceColor = patch.appearanceColor
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
// Send the optimistic-lock header (note's loaded updatedAt) so a stale edit
|
|
45
|
+
// fails with a 409; the shared NotesSection host surfaces it through
|
|
46
|
+
// surfaceRecordConflict instead of silently overwriting.
|
|
47
|
+
await withScopedApiRequestHeaders(
|
|
48
|
+
buildOptimisticLockHeader(updatedAt ?? null),
|
|
49
|
+
() => apiCallOrThrow(
|
|
50
|
+
'/api/staff/comments',
|
|
51
|
+
{
|
|
52
|
+
method: 'PUT',
|
|
53
|
+
headers: { 'content-type': 'application/json' },
|
|
54
|
+
body: JSON.stringify(payload),
|
|
55
|
+
},
|
|
56
|
+
{ errorMessage: translator('staff.teamMembers.detail.notes.updateError', 'Failed to update note.') },
|
|
57
|
+
),
|
|
51
58
|
)
|
|
52
59
|
},
|
|
53
|
-
delete: async ({ id }) => {
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
delete: async ({ id, updatedAt }) => {
|
|
61
|
+
await withScopedApiRequestHeaders(
|
|
62
|
+
buildOptimisticLockHeader(updatedAt ?? null),
|
|
63
|
+
() => apiCallOrThrow(
|
|
64
|
+
`/api/staff/comments?id=${encodeURIComponent(id)}`,
|
|
65
|
+
{
|
|
66
|
+
method: 'DELETE',
|
|
67
|
+
headers: { 'content-type': 'application/json' },
|
|
68
|
+
},
|
|
69
|
+
{ errorMessage: translator('staff.teamMembers.detail.notes.deleteError', 'Failed to delete note') },
|
|
70
|
+
),
|
|
61
71
|
)
|
|
62
72
|
},
|
|
63
73
|
}
|
|
@@ -10,8 +10,44 @@ import {
|
|
|
10
10
|
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
11
11
|
import { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
12
12
|
import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
|
|
13
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
13
14
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
14
15
|
|
|
16
|
+
const TRANSLATION_RESOURCE_KIND = 'translations.translation'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load the existing translation row's primary key + `updated_at` (the row's own
|
|
20
|
+
* version), scoped to tenant/org. Returns `null` when no row exists yet.
|
|
21
|
+
*
|
|
22
|
+
* Optimistic locking enforces against the TRANSLATION ROW'S OWN version, not the
|
|
23
|
+
* host entity's: the host's EAV `entityType` (`module:entity`) cannot be cleanly
|
|
24
|
+
* mapped to a registered optimistic-lock reader key (those are derived from the
|
|
25
|
+
* host module's ORM entity name / events config, e.g. `CatalogProduct` →
|
|
26
|
+
* `catalog.product`, which does not equal `canonicalizeResourceTag('catalog:catalog_product')`),
|
|
27
|
+
* so there is no reliable server-side path to resolve the host's current version
|
|
28
|
+
* for an arbitrary `entityType`. Guarding the translation row's own `updated_at`
|
|
29
|
+
* closes the no-lock hole with real server-side enforcement and no cross-module
|
|
30
|
+
* coupling (mirrors the hand-written `auth.role_acl` route).
|
|
31
|
+
*/
|
|
32
|
+
async function loadTranslationRowVersion(
|
|
33
|
+
db: any,
|
|
34
|
+
entityType: string,
|
|
35
|
+
entityId: string,
|
|
36
|
+
tenantId: string,
|
|
37
|
+
organizationId: string | null,
|
|
38
|
+
): Promise<{ id: string; updatedAt: Date | string | null } | null> {
|
|
39
|
+
const row = await db
|
|
40
|
+
.selectFrom('entity_translations')
|
|
41
|
+
.select(['id', 'updated_at'])
|
|
42
|
+
.where('entity_type', '=', entityType)
|
|
43
|
+
.where('entity_id', '=', entityId)
|
|
44
|
+
.where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)
|
|
45
|
+
.where(sql<boolean>`organization_id is not distinct from ${organizationId}`)
|
|
46
|
+
.executeTakeFirst() as { id: string; updated_at: Date | string | null } | undefined
|
|
47
|
+
if (!row) return null
|
|
48
|
+
return { id: row.id, updatedAt: row.updated_at ?? null }
|
|
49
|
+
}
|
|
50
|
+
|
|
15
51
|
const paramsSchema = z.object({
|
|
16
52
|
entityType: entityTypeParamSchema,
|
|
17
53
|
entityId: entityIdParamSchema,
|
|
@@ -100,6 +136,26 @@ export async function PUT(req: Request, ctx: { params?: { entityType?: string; e
|
|
|
100
136
|
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
101
137
|
}
|
|
102
138
|
|
|
139
|
+
// Optimistic lock: refuse a stale standalone PUT so a translation save that
|
|
140
|
+
// started from an out-of-date view cannot silently clobber a concurrent edit.
|
|
141
|
+
// Strictly additive — no expected-version header → no-op. Skipped when no row
|
|
142
|
+
// exists yet (first save has no prior version to conflict with).
|
|
143
|
+
const existingVersion = await loadTranslationRowVersion(
|
|
144
|
+
context.db as any,
|
|
145
|
+
entityType,
|
|
146
|
+
entityId,
|
|
147
|
+
context.tenantId,
|
|
148
|
+
context.organizationId,
|
|
149
|
+
)
|
|
150
|
+
if (existingVersion) {
|
|
151
|
+
await enforceCommandOptimisticLockWithGuards(context.container, {
|
|
152
|
+
resourceKind: TRANSLATION_RESOURCE_KIND,
|
|
153
|
+
resourceId: existingVersion.id,
|
|
154
|
+
current: existingVersion.updatedAt,
|
|
155
|
+
request: req,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
103
159
|
const commandBus = context.container.resolve('commandBus') as CommandBus
|
|
104
160
|
const { result, logEntry } = await commandBus.execute<
|
|
105
161
|
{ entityType: string; entityId: string; translations: typeof translations; organizationId: string | null; tenantId: string },
|
|
@@ -200,6 +256,24 @@ export async function DELETE(req: Request, ctx: { params?: { entityType?: string
|
|
|
200
256
|
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
201
257
|
}
|
|
202
258
|
|
|
259
|
+
// Optimistic lock: refuse a stale standalone DELETE (same hole as PUT).
|
|
260
|
+
// Additive — no header → no-op; skipped when no row exists.
|
|
261
|
+
const existingVersion = await loadTranslationRowVersion(
|
|
262
|
+
context.db as any,
|
|
263
|
+
entityType,
|
|
264
|
+
entityId,
|
|
265
|
+
context.tenantId,
|
|
266
|
+
context.organizationId,
|
|
267
|
+
)
|
|
268
|
+
if (existingVersion) {
|
|
269
|
+
await enforceCommandOptimisticLockWithGuards(context.container, {
|
|
270
|
+
resourceKind: TRANSLATION_RESOURCE_KIND,
|
|
271
|
+
resourceId: existingVersion.id,
|
|
272
|
+
current: existingVersion.updatedAt,
|
|
273
|
+
request: req,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
203
277
|
const commandBus = context.container.resolve('commandBus') as CommandBus
|
|
204
278
|
const { logEntry } = await commandBus.execute<
|
|
205
279
|
{ entityType: string; entityId: string; organizationId: string | null; tenantId: string },
|
|
@@ -11,6 +11,7 @@ import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
|
11
11
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
12
12
|
import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
13
13
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
14
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
14
15
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
15
16
|
import { useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'
|
|
16
17
|
import { Save, Plus, X } from 'lucide-react'
|
|
@@ -194,6 +195,17 @@ export function TranslationManager({
|
|
|
194
195
|
},
|
|
195
196
|
})
|
|
196
197
|
|
|
198
|
+
// Optimistic lock keys off the TRANSLATION ROW'S OWN version (`updatedAt` from
|
|
199
|
+
// the GET response), not the host entity's: the host's EAV `entityType`
|
|
200
|
+
// (`module:entity`) has no reliable server-side mapping to a registered
|
|
201
|
+
// optimistic-lock reader, so the route enforces against the translation row's
|
|
202
|
+
// own `updated_at`. `null` for a brand-new translation (no existing row → the
|
|
203
|
+
// header is omitted and the route enforces nothing on insert).
|
|
204
|
+
const translationRowUpdatedAt = React.useMemo(() => {
|
|
205
|
+
const value = translationData?.updatedAt
|
|
206
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : null
|
|
207
|
+
}, [translationData])
|
|
208
|
+
|
|
197
209
|
const translationSignature = React.useMemo(() => JSON.stringify(translationData ?? null), [translationData])
|
|
198
210
|
const lastTranslationSignatureRef = React.useRef<string | null>(null)
|
|
199
211
|
|
|
@@ -258,7 +270,7 @@ export function TranslationManager({
|
|
|
258
270
|
return runMutation({
|
|
259
271
|
operation: async () => {
|
|
260
272
|
const res = await withScopedApiRequestHeaders(
|
|
261
|
-
buildOptimisticLockHeader(
|
|
273
|
+
buildOptimisticLockHeader(translationRowUpdatedAt),
|
|
262
274
|
() => apiCall(
|
|
263
275
|
`/api/translations/${encodeURIComponent(entityType)}/${encodeURIComponent(recordId)}`,
|
|
264
276
|
{
|
|
@@ -292,6 +304,7 @@ export function TranslationManager({
|
|
|
292
304
|
void refetchTranslation()
|
|
293
305
|
},
|
|
294
306
|
onError: (err: unknown) => {
|
|
307
|
+
if (surfaceRecordConflict(err, t)) return
|
|
295
308
|
const message = err instanceof Error ? err.message : t('translations.manager.errors.save', 'Failed to save translations')
|
|
296
309
|
flash(message, 'error')
|
|
297
310
|
},
|
|
@@ -24,7 +24,7 @@ import { getCodeWorkflow } from '../../../lib/code-registry'
|
|
|
24
24
|
import { createGenericOptimisticLockReader } from '@open-mercato/shared/lib/crud/optimistic-lock'
|
|
25
25
|
import { registerOptimisticLockReaderIfAbsent } from '@open-mercato/shared/lib/crud/optimistic-lock-store'
|
|
26
26
|
import { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
27
|
-
import {
|
|
27
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
28
28
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
29
29
|
|
|
30
30
|
registerOptimisticLockReaderIfAbsent({
|
|
@@ -201,7 +201,7 @@ export async function PUT(
|
|
|
201
201
|
let savedOverride: WorkflowDefinition
|
|
202
202
|
if (existingOverride) {
|
|
203
203
|
try {
|
|
204
|
-
|
|
204
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
205
205
|
resourceKind: 'workflows.definition',
|
|
206
206
|
resourceId: existingOverride.id,
|
|
207
207
|
current: existingOverride.updatedAt ?? null,
|
|
@@ -308,7 +308,7 @@ export async function PUT(
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
try {
|
|
311
|
-
|
|
311
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
312
312
|
resourceKind: 'workflows.definition',
|
|
313
313
|
resourceId: definition.id,
|
|
314
314
|
current: definition.updatedAt ?? null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { useRouter, useParams } from 'next/navigation'
|
|
4
|
+
import { useRouter, useParams, usePathname } from 'next/navigation'
|
|
5
5
|
import { useQuery } from '@tanstack/react-query'
|
|
6
6
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
7
7
|
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
@@ -10,6 +10,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
10
10
|
import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
11
11
|
import { apiFetch, withScopedApiHeaders } from '@open-mercato/ui/backend/utils/api'
|
|
12
12
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
13
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
13
14
|
import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
14
15
|
import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
15
16
|
import { formatWorkflowValidationError } from '../../../lib/format-validation-error'
|
|
@@ -35,6 +36,7 @@ import type { WorkflowDefinitionTrigger } from '../../../data/entities'
|
|
|
35
36
|
export default function EditWorkflowDefinitionPage() {
|
|
36
37
|
const router = useRouter()
|
|
37
38
|
const params = useParams()
|
|
39
|
+
const pathname = usePathname()
|
|
38
40
|
const t = useT()
|
|
39
41
|
const isMobile = useIsMobile()
|
|
40
42
|
|
|
@@ -221,6 +223,21 @@ export default function EditWorkflowDefinitionPage() {
|
|
|
221
223
|
router.push(`/backend/definitions/visual-editor?id=${definitionId}`)
|
|
222
224
|
}, [router, definitionId])
|
|
223
225
|
|
|
226
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
227
|
+
// mount so the enterprise record_locks widget resolves `workflows.definition` + id
|
|
228
|
+
// explicitly. The resourceKind mirrors the save-time `useGuardedMutation` context
|
|
229
|
+
// and server guard so the held lock matches the conflict surface for the same
|
|
230
|
+
// definition across both the form and visual editors.
|
|
231
|
+
useSetCurrentRecordInjectionContext(
|
|
232
|
+
buildRecordInjectionContext({
|
|
233
|
+
resourceKind: 'workflows.definition',
|
|
234
|
+
resourceId: definitionId ?? null,
|
|
235
|
+
updatedAt: typeof definition?.updatedAt === 'string' ? definition.updatedAt : null,
|
|
236
|
+
data: (definition ?? null) as Record<string, unknown> | null,
|
|
237
|
+
path: pathname,
|
|
238
|
+
}),
|
|
239
|
+
)
|
|
240
|
+
|
|
224
241
|
if (isLoading) {
|
|
225
242
|
return (
|
|
226
243
|
<Page>
|
|
@@ -8,7 +8,7 @@ import { NodeEditDialogCrudForm } from '../../../components/NodeEditDialogCrudFo
|
|
|
8
8
|
import { EdgeEditDialogCrudForm } from '../../../components/EdgeEditDialogCrudForm'
|
|
9
9
|
import type { Node, Edge, Connection } from '@xyflow/react'
|
|
10
10
|
import { useState, useCallback, useEffect } from 'react'
|
|
11
|
-
import { useRouter, useSearchParams } from 'next/navigation'
|
|
11
|
+
import { useRouter, useSearchParams, usePathname } from 'next/navigation'
|
|
12
12
|
import { graphToDefinition, definitionToGraph, validateWorkflowGraph, generateStepId, generateTransitionId, appendWorkflowEdge, ValidationError } from '../../../lib/graph-utils'
|
|
13
13
|
import { performDeleteEdgeFlow, performDeleteNodeFlow } from '../../../lib/visual-editor-delete-flow'
|
|
14
14
|
import { workflowDefinitionDataSchema } from '../../../data/validators'
|
|
@@ -35,6 +35,7 @@ import { FormHeader } from '@open-mercato/ui/backend/forms'
|
|
|
35
35
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
36
36
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
37
37
|
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
38
|
+
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
38
39
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
39
40
|
import { CircleQuestionMark, PanelTopClose, PanelTopOpen, Play, Save, Trash2 } from 'lucide-react'
|
|
40
41
|
import { NODE_TYPE_ICONS, NODE_TYPE_COLORS, NODE_TYPE_LABELS } from '../../../lib/node-type-icons'
|
|
@@ -61,6 +62,7 @@ export default function VisualEditorPage() {
|
|
|
61
62
|
const t = useT()
|
|
62
63
|
const router = useRouter()
|
|
63
64
|
const searchParams = useSearchParams()
|
|
65
|
+
const pathname = usePathname()
|
|
64
66
|
const definitionId = searchParams.get('id')
|
|
65
67
|
const isMobile = useIsMobile()
|
|
66
68
|
|
|
@@ -617,6 +619,22 @@ export default function VisualEditorPage() {
|
|
|
617
619
|
flash('Canvas cleared', 'success')
|
|
618
620
|
}, [])
|
|
619
621
|
|
|
622
|
+
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
623
|
+
// mount so the enterprise record_locks widget resolves `workflows.definition` + id
|
|
624
|
+
// explicitly. This is the highest-value record_locks target (long-lived visual
|
|
625
|
+
// edits): presence holds the lock while the graph is open, and the raw `apiCall`
|
|
626
|
+
// save already routes its 409 through `surfaceRecordConflict`. Cleared on create
|
|
627
|
+
// (no `definitionId`) and on unmount. Mirrors the form edit page's resourceKind so
|
|
628
|
+
// a lock held in either editor surfaces in the other.
|
|
629
|
+
useSetCurrentRecordInjectionContext(
|
|
630
|
+
buildRecordInjectionContext({
|
|
631
|
+
resourceKind: 'workflows.definition',
|
|
632
|
+
resourceId: definitionId,
|
|
633
|
+
updatedAt,
|
|
634
|
+
path: pathname,
|
|
635
|
+
}),
|
|
636
|
+
)
|
|
637
|
+
|
|
620
638
|
// Show loading spinner while loading definition
|
|
621
639
|
if (isLoading) {
|
|
622
640
|
return (
|