@open-mercato/core 0.6.6-develop.6317.1.b3be10ab84 → 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/.turbo/turbo-build.log +1 -1
- 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/customer_accounts/backend/customer_accounts/roles/[id]/page.js +7 -12
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +5 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -1
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.js +2 -1
- package/dist/modules/customer_accounts/widgets/injection/company-users/widget.client.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/EntityTagsDialog.js +76 -67
- package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +3 -3
- package/dist/modules/customers/components/detail/notesAdapter.js +25 -18
- package/dist/modules/customers/components/detail/notesAdapter.js.map +2 -2
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +4 -3
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/data_sync/api/runs.js +1 -0
- package/dist/modules/data_sync/api/runs.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 +35 -16
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/data/validators.js +1 -0
- package/dist/modules/data_sync/data/validators.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-run-service.js +20 -0
- package/dist/modules/data_sync/lib/sync-run-service.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 +18 -6
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +17 -10
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.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/messages/api/unread-count/route.js +32 -0
- package/dist/modules/messages/api/unread-count/route.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +13 -5
- package/dist/modules/query_index/components/QueryIndexesTable.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/shipping_carriers/i18n/en.js +11 -1
- package/dist/modules/shipping_carriers/i18n/en.js.map +2 -2
- package/dist/modules/shipping_carriers/i18n/pl.js +11 -1
- package/dist/modules/shipping_carriers/i18n/pl.js.map +2 -2
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js +25 -0
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.js.map +7 -0
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js +3 -17
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.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/translations/widgets/injection/translation-manager/widget.client.js +19 -14
- package/dist/modules/translations/widgets/injection/translation-manager/widget.client.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/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +7 -10
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +6 -7
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +3 -2
- package/src/modules/customer_accounts/widgets/injection/company-users/widget.client.tsx +3 -2
- 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/EntityTagsDialog.tsx +89 -71
- 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/dashboards/components/WidgetVisibilityEditor.tsx +6 -3
- package/src/modules/data_sync/api/runs.ts +1 -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 +35 -16
- package/src/modules/data_sync/data/validators.ts +1 -0
- package/src/modules/data_sync/i18n/de.json +1 -0
- package/src/modules/data_sync/i18n/en.json +1 -0
- package/src/modules/data_sync/i18n/es.json +1 -0
- package/src/modules/data_sync/i18n/pl.json +1 -0
- package/src/modules/data_sync/lib/sync-run-service.ts +23 -0
- 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 +24 -6
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +25 -12
- 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/messages/api/unread-count/route.ts +40 -0
- package/src/modules/query_index/components/QueryIndexesTable.tsx +17 -15
- 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/shipping_carriers/i18n/de.json +11 -1
- package/src/modules/shipping_carriers/i18n/en.json +11 -1
- package/src/modules/shipping_carriers/i18n/en.ts +10 -0
- package/src/modules/shipping_carriers/i18n/es.json +11 -1
- package/src/modules/shipping_carriers/i18n/pl.json +11 -1
- package/src/modules/shipping_carriers/i18n/pl.ts +10 -0
- package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.client.tsx +35 -0
- package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.tsx +3 -22
- 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/translations/widgets/injection/translation-manager/widget.client.tsx +30 -13
- 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
|
@@ -9,6 +9,13 @@ import {
|
|
|
9
9
|
validateCrudMutationGuard
|
|
10
10
|
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
11
11
|
import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
|
|
12
|
+
import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
13
|
+
const TRANSLATION_RESOURCE_KIND = "translations.translation";
|
|
14
|
+
async function loadTranslationRowVersion(db, entityType, entityId, tenantId, organizationId) {
|
|
15
|
+
const row = await db.selectFrom("entity_translations").select(["id", "updated_at"]).where("entity_type", "=", entityType).where("entity_id", "=", entityId).where(sql`tenant_id is not distinct from ${tenantId}`).where(sql`organization_id is not distinct from ${organizationId}`).executeTakeFirst();
|
|
16
|
+
if (!row) return null;
|
|
17
|
+
return { id: row.id, updatedAt: row.updated_at ?? null };
|
|
18
|
+
}
|
|
12
19
|
const paramsSchema = z.object({
|
|
13
20
|
entityType: entityTypeParamSchema,
|
|
14
21
|
entityId: entityIdParamSchema
|
|
@@ -81,6 +88,21 @@ async function PUT(req, ctx) {
|
|
|
81
88
|
if (guardResult && !guardResult.ok) {
|
|
82
89
|
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
83
90
|
}
|
|
91
|
+
const existingVersion = await loadTranslationRowVersion(
|
|
92
|
+
context.db,
|
|
93
|
+
entityType,
|
|
94
|
+
entityId,
|
|
95
|
+
context.tenantId,
|
|
96
|
+
context.organizationId
|
|
97
|
+
);
|
|
98
|
+
if (existingVersion) {
|
|
99
|
+
await enforceCommandOptimisticLockWithGuards(context.container, {
|
|
100
|
+
resourceKind: TRANSLATION_RESOURCE_KIND,
|
|
101
|
+
resourceId: existingVersion.id,
|
|
102
|
+
current: existingVersion.updatedAt,
|
|
103
|
+
request: req
|
|
104
|
+
});
|
|
105
|
+
}
|
|
84
106
|
const commandBus = context.container.resolve("commandBus");
|
|
85
107
|
const { result, logEntry } = await commandBus.execute("translations.translation.save", {
|
|
86
108
|
input: {
|
|
@@ -162,6 +184,21 @@ async function DELETE(req, ctx) {
|
|
|
162
184
|
if (guardResult && !guardResult.ok) {
|
|
163
185
|
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
164
186
|
}
|
|
187
|
+
const existingVersion = await loadTranslationRowVersion(
|
|
188
|
+
context.db,
|
|
189
|
+
entityType,
|
|
190
|
+
entityId,
|
|
191
|
+
context.tenantId,
|
|
192
|
+
context.organizationId
|
|
193
|
+
);
|
|
194
|
+
if (existingVersion) {
|
|
195
|
+
await enforceCommandOptimisticLockWithGuards(context.container, {
|
|
196
|
+
resourceKind: TRANSLATION_RESOURCE_KIND,
|
|
197
|
+
resourceId: existingVersion.id,
|
|
198
|
+
current: existingVersion.updatedAt,
|
|
199
|
+
request: req
|
|
200
|
+
});
|
|
201
|
+
}
|
|
165
202
|
const commandBus = context.container.resolve("commandBus");
|
|
166
203
|
const { logEntry } = await commandBus.execute("translations.translation.delete", {
|
|
167
204
|
input: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/translations/api/%5BentityType%5D/%5BentityId%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { sql } from 'kysely'\nimport { resolveTranslationsRouteContext, requireTranslationFeatures, resolveTranslationsActorId } from '@open-mercato/core/modules/translations/api/context'\nimport { translationBodySchema, entityTypeParamSchema, entityIdParamSchema } from '@open-mercato/core/modules/translations/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst paramsSchema = z.object({\n entityType: entityTypeParamSchema,\n entityId: entityIdParamSchema,\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['translations.view'] },\n PUT: { requireAuth: true, requireFeatures: ['translations.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['translations.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n await requireTranslationFeatures(context, ['translations.view'])\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const row = await (context.db as any)\n .selectFrom('entity_translations')\n .selectAll()\n .where('entity_type', '=', entityType)\n .where('entity_id', '=', entityId)\n .where(sql<boolean>`tenant_id is not distinct from ${context.tenantId}`)\n .where(sql<boolean>`organization_id is not distinct from ${context.organizationId}`)\n .executeTakeFirst() as Record<string, any> | undefined\n\n if (!row) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n return NextResponse.json({\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid parameters', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.GET] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport async function PUT(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n await requireTranslationFeatures(context, ['translations.manage'])\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const rawText = await req.text()\n let rawBody: unknown = {}\n if (rawText.trim().length > 0) {\n try {\n rawBody = JSON.parse(rawText)\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n }\n const translations = translationBodySchema.parse(rawBody)\n\n const guardUserId = resolveTranslationsActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'translations.translation',\n resourceId: `${entityType}:${entityId}`,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { entityType, entityId, translations },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n { entityType: string; entityId: string; translations: typeof translations; organizationId: string | null; tenantId: string },\n { rowId: string }\n >('translations.translation.save', {\n input: {\n entityType,\n entityId,\n translations,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n },\n ctx: context.commandCtx,\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'translations.translation',\n resourceId: `${entityType}:${entityId}`,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const row = await (context.db as any)\n .selectFrom('entity_translations')\n .selectAll()\n .where('id', '=', result.rowId)\n .executeTakeFirst() as Record<string, any>\n\n const response = NextResponse.json({\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'translations.translation',\n resourceId: logEntry.resourceId ?? result.rowId,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n }\n\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.PUT] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n await requireTranslationFeatures(context, ['translations.manage'])\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const guardUserId = resolveTranslationsActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'translations.translation',\n resourceId: `${entityType}:${entityId}`,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: null,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<\n { entityType: string; entityId: string; organizationId: string | null; tenantId: string },\n { deleted: boolean }\n >('translations.translation.delete', {\n input: {\n entityType,\n entityId,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n },\n ctx: context.commandCtx,\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'translations.translation',\n resourceId: `${entityType}:${entityId}`,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const response = new NextResponse(null, { status: 204 })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'translations.translation',\n resourceId: logEntry.resourceId ?? null,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n }\n\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid parameters', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.DELETE] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst translationsTag = 'Translations'\n\nconst getDoc: OpenApiMethodDoc = {\n summary: 'Get entity translations',\n description: 'Returns the full translation record for a single entity.',\n tags: [translationsTag],\n responses: [\n { status: 200, description: 'Translation record found.' },\n ],\n errors: [\n { status: 401, description: 'Authentication required' },\n { status: 404, description: 'No translations found for this entity' },\n ],\n}\n\nconst putDoc: OpenApiMethodDoc = {\n summary: 'Create or update entity translations',\n description: 'Full replacement of translations JSONB for an entity.',\n tags: [translationsTag],\n responses: [\n { status: 200, description: 'Translations saved.' },\n ],\n errors: [\n { status: 400, description: 'Validation failed' },\n { status: 401, description: 'Authentication required' },\n ],\n}\n\nconst deleteDoc: OpenApiMethodDoc = {\n summary: 'Delete entity translations',\n description: 'Removes all translations for an entity.',\n tags: [translationsTag],\n responses: [\n { status: 204, description: 'Translations deleted.' },\n ],\n errors: [\n { status: 401, description: 'Authentication required' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: translationsTag,\n summary: 'Entity translation resource',\n pathParams: paramsSchema,\n methods: {\n GET: getDoc,\n PUT: putDoc,\n DELETE: deleteDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,WAAW;AACpB,SAAS,iCAAiC,4BAA4B,kCAAkC;AACxG,SAAS,uBAAuB,uBAAuB,2BAA2B;AAClF,SAAwB,uBAAuB;AAC/C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,SAAS,kCAAkC;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { sql } from 'kysely'\nimport { resolveTranslationsRouteContext, requireTranslationFeatures, resolveTranslationsActorId } from '@open-mercato/core/modules/translations/api/context'\nimport { translationBodySchema, entityTypeParamSchema, entityIdParamSchema } from '@open-mercato/core/modules/translations/data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst TRANSLATION_RESOURCE_KIND = 'translations.translation'\n\n/**\n * Load the existing translation row's primary key + `updated_at` (the row's own\n * version), scoped to tenant/org. Returns `null` when no row exists yet.\n *\n * Optimistic locking enforces against the TRANSLATION ROW'S OWN version, not the\n * host entity's: the host's EAV `entityType` (`module:entity`) cannot be cleanly\n * mapped to a registered optimistic-lock reader key (those are derived from the\n * host module's ORM entity name / events config, e.g. `CatalogProduct` \u2192\n * `catalog.product`, which does not equal `canonicalizeResourceTag('catalog:catalog_product')`),\n * so there is no reliable server-side path to resolve the host's current version\n * for an arbitrary `entityType`. Guarding the translation row's own `updated_at`\n * closes the no-lock hole with real server-side enforcement and no cross-module\n * coupling (mirrors the hand-written `auth.role_acl` route).\n */\nasync function loadTranslationRowVersion(\n db: any,\n entityType: string,\n entityId: string,\n tenantId: string,\n organizationId: string | null,\n): Promise<{ id: string; updatedAt: Date | string | null } | null> {\n const row = await db\n .selectFrom('entity_translations')\n .select(['id', 'updated_at'])\n .where('entity_type', '=', entityType)\n .where('entity_id', '=', entityId)\n .where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)\n .where(sql<boolean>`organization_id is not distinct from ${organizationId}`)\n .executeTakeFirst() as { id: string; updated_at: Date | string | null } | undefined\n if (!row) return null\n return { id: row.id, updatedAt: row.updated_at ?? null }\n}\n\nconst paramsSchema = z.object({\n entityType: entityTypeParamSchema,\n entityId: entityIdParamSchema,\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['translations.view'] },\n PUT: { requireAuth: true, requireFeatures: ['translations.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['translations.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n await requireTranslationFeatures(context, ['translations.view'])\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const row = await (context.db as any)\n .selectFrom('entity_translations')\n .selectAll()\n .where('entity_type', '=', entityType)\n .where('entity_id', '=', entityId)\n .where(sql<boolean>`tenant_id is not distinct from ${context.tenantId}`)\n .where(sql<boolean>`organization_id is not distinct from ${context.organizationId}`)\n .executeTakeFirst() as Record<string, any> | undefined\n\n if (!row) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n return NextResponse.json({\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid parameters', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.GET] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport async function PUT(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n await requireTranslationFeatures(context, ['translations.manage'])\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const rawText = await req.text()\n let rawBody: unknown = {}\n if (rawText.trim().length > 0) {\n try {\n rawBody = JSON.parse(rawText)\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n }\n const translations = translationBodySchema.parse(rawBody)\n\n const guardUserId = resolveTranslationsActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'translations.translation',\n resourceId: `${entityType}:${entityId}`,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { entityType, entityId, translations },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n // Optimistic lock: refuse a stale standalone PUT so a translation save that\n // started from an out-of-date view cannot silently clobber a concurrent edit.\n // Strictly additive \u2014 no expected-version header \u2192 no-op. Skipped when no row\n // exists yet (first save has no prior version to conflict with).\n const existingVersion = await loadTranslationRowVersion(\n context.db as any,\n entityType,\n entityId,\n context.tenantId,\n context.organizationId,\n )\n if (existingVersion) {\n await enforceCommandOptimisticLockWithGuards(context.container, {\n resourceKind: TRANSLATION_RESOURCE_KIND,\n resourceId: existingVersion.id,\n current: existingVersion.updatedAt,\n request: req,\n })\n }\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n { entityType: string; entityId: string; translations: typeof translations; organizationId: string | null; tenantId: string },\n { rowId: string }\n >('translations.translation.save', {\n input: {\n entityType,\n entityId,\n translations,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n },\n ctx: context.commandCtx,\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'translations.translation',\n resourceId: `${entityType}:${entityId}`,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const row = await (context.db as any)\n .selectFrom('entity_translations')\n .selectAll()\n .where('id', '=', result.rowId)\n .executeTakeFirst() as Record<string, any>\n\n const response = NextResponse.json({\n entityType: row.entity_type,\n entityId: row.entity_id,\n translations: row.translations,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'translations.translation',\n resourceId: logEntry.resourceId ?? result.rowId,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n }\n\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.PUT] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { entityType?: string; entityId?: string } }) {\n try {\n const context = await resolveTranslationsRouteContext(req)\n await requireTranslationFeatures(context, ['translations.manage'])\n const { entityType, entityId } = paramsSchema.parse({\n entityType: ctx.params?.entityType,\n entityId: ctx.params?.entityId,\n })\n\n const guardUserId = resolveTranslationsActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'translations.translation',\n resourceId: `${entityType}:${entityId}`,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: null,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n // Optimistic lock: refuse a stale standalone DELETE (same hole as PUT).\n // Additive \u2014 no header \u2192 no-op; skipped when no row exists.\n const existingVersion = await loadTranslationRowVersion(\n context.db as any,\n entityType,\n entityId,\n context.tenantId,\n context.organizationId,\n )\n if (existingVersion) {\n await enforceCommandOptimisticLockWithGuards(context.container, {\n resourceKind: TRANSLATION_RESOURCE_KIND,\n resourceId: existingVersion.id,\n current: existingVersion.updatedAt,\n request: req,\n })\n }\n\n const commandBus = context.container.resolve('commandBus') as CommandBus\n const { logEntry } = await commandBus.execute<\n { entityType: string; entityId: string; organizationId: string | null; tenantId: string },\n { deleted: boolean }\n >('translations.translation.delete', {\n input: {\n entityType,\n entityId,\n organizationId: context.organizationId,\n tenantId: context.tenantId,\n },\n ctx: context.commandCtx,\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'translations.translation',\n resourceId: `${entityType}:${entityId}`,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const response = new NextResponse(null, { status: 204 })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'translations.translation',\n resourceId: logEntry.resourceId ?? null,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n }),\n )\n }\n\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: 'Invalid parameters', details: err.issues }, { status: 400 })\n }\n console.error('[translations/:entityType/:entityId.DELETE] Unexpected error', err)\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst translationsTag = 'Translations'\n\nconst getDoc: OpenApiMethodDoc = {\n summary: 'Get entity translations',\n description: 'Returns the full translation record for a single entity.',\n tags: [translationsTag],\n responses: [\n { status: 200, description: 'Translation record found.' },\n ],\n errors: [\n { status: 401, description: 'Authentication required' },\n { status: 404, description: 'No translations found for this entity' },\n ],\n}\n\nconst putDoc: OpenApiMethodDoc = {\n summary: 'Create or update entity translations',\n description: 'Full replacement of translations JSONB for an entity.',\n tags: [translationsTag],\n responses: [\n { status: 200, description: 'Translations saved.' },\n ],\n errors: [\n { status: 400, description: 'Validation failed' },\n { status: 401, description: 'Authentication required' },\n ],\n}\n\nconst deleteDoc: OpenApiMethodDoc = {\n summary: 'Delete entity translations',\n description: 'Removes all translations for an entity.',\n tags: [translationsTag],\n responses: [\n { status: 204, description: 'Translations deleted.' },\n ],\n errors: [\n { status: 401, description: 'Authentication required' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: translationsTag,\n summary: 'Entity translation resource',\n pathParams: paramsSchema,\n methods: {\n GET: getDoc,\n PUT: putDoc,\n DELETE: deleteDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -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";
|
|
@@ -149,6 +150,10 @@ function TranslationManager({
|
|
|
149
150
|
return res.result ?? null;
|
|
150
151
|
}
|
|
151
152
|
});
|
|
153
|
+
const translationRowUpdatedAt = React.useMemo(() => {
|
|
154
|
+
const value = translationData?.updatedAt;
|
|
155
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
156
|
+
}, [translationData]);
|
|
152
157
|
const translationSignature = React.useMemo(() => JSON.stringify(translationData ?? null), [translationData]);
|
|
153
158
|
const lastTranslationSignatureRef = React.useRef(null);
|
|
154
159
|
React.useEffect(() => {
|
|
@@ -200,7 +205,7 @@ function TranslationManager({
|
|
|
200
205
|
return runMutation({
|
|
201
206
|
operation: async () => {
|
|
202
207
|
const res = await withScopedApiRequestHeaders(
|
|
203
|
-
buildOptimisticLockHeader(
|
|
208
|
+
buildOptimisticLockHeader(translationRowUpdatedAt),
|
|
204
209
|
() => apiCall(
|
|
205
210
|
`/api/translations/${encodeURIComponent(entityType)}/${encodeURIComponent(recordId)}`,
|
|
206
211
|
{
|
|
@@ -234,6 +239,7 @@ function TranslationManager({
|
|
|
234
239
|
void refetchTranslation();
|
|
235
240
|
},
|
|
236
241
|
onError: (err) => {
|
|
242
|
+
if (surfaceRecordConflict(err, t)) return;
|
|
237
243
|
const message = err instanceof Error ? err.message : t("translations.manager.errors.save", "Failed to save translations");
|
|
238
244
|
flash(message, "error");
|
|
239
245
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/translations/components/TranslationManager.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { ComboboxInput } from '@open-mercato/ui/backend/inputs'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Save, Plus, X } from 'lucide-react'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { locales as defaultLocales } from '@open-mercato/shared/lib/i18n/config'\nimport { ISO_639_1, isValidIso639, getIso639Label } from '@open-mercato/shared/lib/i18n/iso639'\nimport { formatEntityLabel, buildEntityListUrl, getRecordLabel, resolveBaseValue } from '../lib/helpers'\nimport { resolveFieldList } from '../lib/resolve-field-list'\nimport type { ResolvedField } from '../lib/resolve-field-list'\n\nconst TRANSLATION_MUTATION_CONTEXT_ID = 'translations.entity-translations'\nconst SUPPORTED_LOCALES_MUTATION_CONTEXT_ID = 'translations.supported-locales'\n\ntype TranslationManagerProps = {\n entityType?: string\n recordId?: string\n baseValues?: Record<string, unknown>\n translatableFields?: string[]\n mode?: 'standalone' | 'embedded'\n compact?: boolean\n}\n\ntype EntityOption = { entityId: string; label?: string; source?: string }\n\ntype TranslationsResponse = {\n entityType: string\n entityId: string\n translations: Record<string, Record<string, unknown>>\n createdAt?: string\n updatedAt?: string\n}\n\nfunction useTranslationLocales() {\n return useQuery<string[]>({\n queryKey: ['translation-locales'],\n queryFn: async () => {\n const res = await apiCall<{ locales: string[] }>('/api/translations/locales')\n if (!res.ok) return [...defaultLocales]\n return Array.isArray(res.result?.locales) && res.result.locales.length > 0\n ? res.result.locales\n : [...defaultLocales]\n },\n staleTime: 60_000,\n })\n}\n\nexport function TranslationManager({\n entityType: propEntityType,\n recordId: propRecordId,\n baseValues: propBaseValues,\n translatableFields: propTranslatableFields,\n mode = 'standalone',\n compact = false,\n}: TranslationManagerProps) {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const isEmbedded = mode === 'embedded'\n\n const [selectedEntityType, setSelectedEntityType] = React.useState(propEntityType ?? '')\n const [selectedRecordId, setSelectedRecordId] = React.useState(propRecordId ?? '')\n const [activeLocale, setActiveLocale] = React.useState('')\n const [editedTranslations, setEditedTranslations] = React.useState<Record<string, Record<string, string>>>({})\n const editedTranslationsRef = React.useRef<Record<string, Record<string, string>>>({})\n const [hasUserEdited, setHasUserEdited] = React.useState(false)\n const hasUserEditedRef = React.useRef(false)\n\n const entityType = isEmbedded ? (propEntityType ?? '') : selectedEntityType\n const recordId = isEmbedded ? (propRecordId ?? '') : selectedRecordId\n\n const { data: locales = [...defaultLocales] } = useTranslationLocales()\n\n React.useEffect(() => {\n if (locales.length > 0 && (!activeLocale || !locales.includes(activeLocale))) {\n setActiveLocale(locales[0])\n }\n }, [locales, activeLocale])\n\n React.useEffect(() => {\n if (isEmbedded && propEntityType) setSelectedEntityType(propEntityType)\n }, [isEmbedded, propEntityType])\n\n React.useEffect(() => {\n if (isEmbedded && propRecordId) setSelectedRecordId(propRecordId)\n }, [isEmbedded, propRecordId])\n\n const { data: entities, isLoading: loadingEntities, error: entitiesError } = useQuery<{ items: EntityOption[] }>({\n queryKey: ['entities-list', scopeVersion],\n enabled: !isEmbedded,\n queryFn: async () =>\n readApiResultOrThrow('/api/entities/entities', undefined, {\n errorMessage: t('translations.manager.errors.loadEntities', 'Failed to load entities'),\n }),\n })\n\n const entitySuggestions = React.useMemo(\n () =>\n (entities?.items || []).map((item) => ({\n value: item.entityId,\n label: formatEntityLabel(item.entityId, item.label),\n description: item.entityId,\n })),\n [entities],\n )\n\n const resolveEntityLabel = React.useCallback(\n (value: string) => {\n const match = entities?.items?.find((e) => e.entityId === value)\n return match ? formatEntityLabel(match.entityId, match.label) : formatEntityLabel(value)\n },\n [entities],\n )\n\n const listUrl = React.useMemo(() => entityType ? buildEntityListUrl(entityType) : null, [entityType])\n\n const loadRecordSuggestions = React.useCallback(\n async (query?: string) => {\n if (!entityType || !listUrl) return []\n const url = `${listUrl}?pageSize=20${query ? `&search=${encodeURIComponent(query)}` : ''}`\n const res = await apiCall<{ items: Array<Record<string, unknown>> }>(url)\n if (!res.ok) return []\n const items = res.result?.items ?? []\n return items.map((item) => ({\n value: String(item.id ?? ''),\n label: getRecordLabel(item),\n }))\n },\n [entityType, listUrl],\n )\n\n const { data: recordData } = useQuery<Record<string, unknown> | null>({\n queryKey: ['translation-record-data', entityType, recordId, listUrl, scopeVersion],\n enabled: !isEmbedded && !!entityType && !!recordId && !!listUrl,\n queryFn: async () => {\n const res = await apiCall<{ items: Array<Record<string, unknown>> }>(\n // Some APIs filter by `id` (catalog), others by `ids` (resources) \u2014 send both so the one recognized by the target route's buildFilters is applied\n `${listUrl}?id=${encodeURIComponent(recordId)}&ids=${encodeURIComponent(recordId)}&pageSize=1`,\n )\n if (!res.ok) return null\n const items = res.result?.items\n return Array.isArray(items) && items.length > 0 ? items[0] : null\n },\n })\n\n const baseValues = isEmbedded ? (propBaseValues ?? {}) : (recordData ?? {})\n\n const resolveRecordLabel = React.useCallback(\n (value: string) => {\n if (recordData) return getRecordLabel(recordData)\n return value\n },\n [recordData],\n )\n\n const { data: fieldDefs = [], isLoading: loadingFieldDefs } = useCustomFieldDefs(entityType ? [entityType] : [], {\n enabled: !!entityType,\n })\n\n const fieldList = React.useMemo(\n () => resolveFieldList(entityType, propTranslatableFields, fieldDefs as Array<{ key: string; kind: string; label?: string }>),\n [entityType, propTranslatableFields, fieldDefs],\n )\n\n const {\n data: translationData,\n isLoading: loadingTranslation,\n isError: translationError,\n refetch: refetchTranslation,\n } = useQuery<TranslationsResponse | null>({\n queryKey: ['entity-translation', entityType, recordId, scopeVersion],\n enabled: !!entityType && !!recordId,\n queryFn: async () => {\n const res = await apiCall<TranslationsResponse>(\n `/api/translations/${encodeURIComponent(entityType)}/${encodeURIComponent(recordId)}`,\n )\n if (!res.ok) {\n if (res.response?.status === 404) return null\n return null\n }\n return res.result ?? null\n },\n })\n\n const translationSignature = React.useMemo(() => JSON.stringify(translationData ?? null), [translationData])\n const lastTranslationSignatureRef = React.useRef<string | null>(null)\n\n React.useEffect(() => {\n const sig = translationSignature\n if (sig === lastTranslationSignatureRef.current && hasUserEditedRef.current) return\n lastTranslationSignatureRef.current = sig\n\n if (!translationData?.translations) {\n if (!hasUserEditedRef.current) {\n editedTranslationsRef.current = {}\n setEditedTranslations({})\n }\n return\n }\n\n const parsed: Record<string, Record<string, string>> = {}\n for (const [locale, fields] of Object.entries(translationData.translations)) {\n if (!fields || typeof fields !== 'object') continue\n parsed[locale] = {}\n for (const [key, val] of Object.entries(fields)) {\n parsed[locale][key] = typeof val === 'string' ? val : ''\n }\n }\n if (!hasUserEditedRef.current) {\n editedTranslationsRef.current = parsed\n setEditedTranslations(parsed)\n }\n }, [translationSignature, translationData])\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n entityType: string\n recordId: string\n resourceKind: string\n resourceId: string\n data: TranslationsResponse | null\n retryLastMutation: () => Promise<boolean>\n }>({ contextId: TRANSLATION_MUTATION_CONTEXT_ID })\n\n const mutation = useMutation({\n mutationFn: async () => {\n if (!entityType || !recordId) {\n throw new Error(t('translations.manager.errors.selectRecord', 'Select an entity and record before saving'))\n }\n const body: Record<string, Record<string, string | null>> = {}\n for (const [locale, fields] of Object.entries(editedTranslationsRef.current)) {\n const localeFields: Record<string, string | null> = {}\n let hasValues = false\n for (const [key, val] of Object.entries(fields)) {\n if (val && val.trim().length > 0) {\n localeFields[key] = val.trim()\n hasValues = true\n }\n }\n if (hasValues) body[locale] = localeFields\n }\n if (Object.keys(body).length === 0) {\n console.warn('[translations] Save skipped: payload is empty \u2014 no locale contains any non-empty field')\n throw new Error(t('translations.manager.errors.nothingToSave', 'Nothing to save \u2014 enter a translation first'))\n }\n return runMutation({\n operation: async () => {\n const res = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(translationData?.updatedAt),\n () => apiCall(\n `/api/translations/${encodeURIComponent(entityType)}/${encodeURIComponent(recordId)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n ),\n )\n if (!res.ok) {\n throw new Error(t('translations.manager.errors.save', 'Failed to save translations'))\n }\n return true\n },\n context: {\n formId: TRANSLATION_MUTATION_CONTEXT_ID,\n entityType,\n recordId,\n resourceKind: 'translation',\n resourceId: recordId,\n data: translationData ?? null,\n retryLastMutation,\n },\n mutationPayload: body,\n })\n },\n onSuccess: () => {\n flash(t('translations.manager.flash.saved', 'Translations saved'), 'success')\n hasUserEditedRef.current = false\n setHasUserEdited(false)\n void refetchTranslation()\n },\n onError: (err: unknown) => {\n const message = err instanceof Error ? err.message : t('translations.manager.errors.save', 'Failed to save translations')\n flash(message, 'error')\n },\n })\n\n const updateFieldValue = (locale: string, fieldKey: string, value: string) => {\n hasUserEditedRef.current = true\n setHasUserEdited(true)\n const next = {\n ...editedTranslationsRef.current,\n [locale]: {\n ...editedTranslationsRef.current[locale],\n [fieldKey]: value,\n },\n }\n editedTranslationsRef.current = next\n setEditedTranslations(next)\n }\n\n const getBaseValue = (fieldKey: string): string => resolveBaseValue(baseValues, fieldKey)\n\n const renderRecordPicker = () => {\n if (isEmbedded) return null\n\n return (\n <div className=\"space-y-2\">\n <label className=\"text-xs text-muted-foreground\">\n {t('translations.manager.selectRecord', 'Select record')}\n </label>\n <ComboboxInput\n value={selectedRecordId}\n onChange={(next) => {\n setSelectedRecordId(next)\n hasUserEditedRef.current = false\n setHasUserEdited(false)\n }}\n placeholder={t('translations.manager.searchRecords', 'Search records...')}\n loadSuggestions={loadRecordSuggestions}\n resolveLabel={resolveRecordLabel}\n allowCustomValues\n disabled={!entityType}\n />\n </div>\n )\n }\n\n const renderLocaleTabs = () => (\n <Tabs variant=\"underline\" value={activeLocale} onValueChange={setActiveLocale}>\n <TabsList>\n {locales.map((locale) => (\n <TabsTrigger key={locale} value={locale}>\n {locale.toUpperCase()}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n )\n\n const renderFieldTable = () => {\n if (!entityType || !recordId) {\n return (\n <div className=\"rounded border bg-background/80 p-4 text-sm text-muted-foreground\">\n {t('translations.manager.selectFirst', 'Select an entity and record to manage translations.')}\n </div>\n )\n }\n if (loadingTranslation || loadingFieldDefs) {\n return (\n <LoadingMessage\n label={t('translations.manager.loadingTranslations', 'Loading translations...')}\n className=\"border-0 bg-transparent p-4\"\n />\n )\n }\n if (translationError) {\n return (\n <ErrorMessage\n label={t('translations.manager.errors.loadTranslation', 'Failed to load translations')}\n action={(\n <Button variant=\"outline\" size=\"sm\" onClick={() => void refetchTranslation()}>\n {t('translations.manager.actions.retry', 'Retry')}\n </Button>\n )}\n />\n )\n }\n if (!fieldList.length) {\n return (\n <div className=\"rounded border bg-background/80 p-4 text-sm text-muted-foreground\">\n {t('translations.manager.noFields', 'No translatable fields found for this entity type.')}\n </div>\n )\n }\n\n const localeTranslations = editedTranslations[activeLocale] ?? {}\n\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[480px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left w-[140px]\">\n {t('translations.manager.fields.field', 'Field')}\n </th>\n {!compact && (\n <th className=\"px-3 py-2 text-left\">\n {t('translations.manager.fields.baseValue', 'Base value')}\n </th>\n )}\n <th className=\"px-3 py-2 text-left\">\n {t('translations.manager.fields.translation', 'Translation')} ({activeLocale.toUpperCase()})\n </th>\n </tr>\n </thead>\n <tbody>\n {fieldList.map((field) => {\n const baseVal = getBaseValue(field.key)\n const translatedVal = localeTranslations[field.key] ?? ''\n\n return (\n <tr key={field.key} className=\"border-t\">\n <td className=\"px-3 py-2 align-top text-xs font-medium text-muted-foreground\">\n {field.label}\n </td>\n {!compact && (\n <td className=\"px-3 py-2 align-top text-xs text-muted-foreground max-w-[200px]\">\n {baseVal ? (\n <span className=\"line-clamp-3\">{baseVal}</span>\n ) : (\n <span className=\"text-muted-foreground/50\">-</span>\n )}\n </td>\n )}\n <td className=\"px-3 py-2 align-top\">\n {field.multiline ? (\n <textarea\n className=\"flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\"\n rows={3}\n value={translatedVal}\n onChange={(e) => updateFieldValue(activeLocale, field.key, e.target.value)}\n placeholder={baseVal || field.label}\n />\n ) : (\n <Input\n value={translatedVal}\n onChange={(e) => updateFieldValue(activeLocale, field.key, e.target.value)}\n placeholder={baseVal || field.label}\n />\n )}\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n }\n\n React.useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault()\n if (entityType && recordId && !mutation.isPending) mutation.mutate()\n }\n }\n document.addEventListener('keydown', handler)\n return () => document.removeEventListener('keydown', handler)\n }, [entityType, recordId, mutation])\n\n if (compact) {\n return (\n <div className=\"space-y-3\">\n {renderLocaleTabs()}\n {renderFieldTable()}\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || !entityType || !recordId}\n data-testid=\"translations-save\"\n >\n <Save className=\"mr-2 h-3 w-3\" />\n {mutation.isPending\n ? t('translations.manager.actions.saving', 'Saving...')\n : t('translations.manager.actions.save', 'Save translations')}\n </Button>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('translations.manager.title', 'Translations')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('translations.manager.description', 'Manage translations for entity records across supported locales.')}\n </p>\n </div>\n\n {!isEmbedded && (\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start\">\n <div className=\"flex-1 space-y-3\">\n <div>\n <label className=\"text-xs text-muted-foreground\">\n {t('translations.manager.selectEntity', 'Choose entity')}\n </label>\n <div className=\"mt-1\">\n <ComboboxInput\n value={selectedEntityType}\n onChange={(next) => {\n setSelectedEntityType(next)\n setSelectedRecordId('')\n hasUserEditedRef.current = false\n setHasUserEdited(false)\n }}\n placeholder={t('translations.manager.placeholder', 'Select an entity')}\n suggestions={entitySuggestions}\n resolveLabel={resolveEntityLabel}\n disabled={loadingEntities || !!entitiesError}\n />\n </div>\n {entitiesError && (\n <p className=\"mt-1 text-xs text-destructive\">\n {t('translations.manager.errors.loadEntities', 'Failed to load entities')}\n </p>\n )}\n </div>\n {renderRecordPicker()}\n </div>\n </div>\n )}\n\n <div className=\"rounded-lg border bg-background/80 p-4\">\n {renderLocaleTabs()}\n <div className=\"mt-3\">\n {renderFieldTable()}\n </div>\n </div>\n\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || loadingEntities || !!entitiesError || !entityType || !recordId}\n data-testid=\"translations-save\"\n >\n <Save className=\"mr-2 h-4 w-4\" />\n {mutation.isPending\n ? t('translations.manager.actions.saving', 'Saving...')\n : t('translations.manager.actions.save', 'Save translations')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n\nexport function LocaleManager() {\n const t = useT()\n const queryClient = useQueryClient()\n const { data: locales = [], isLoading } = useTranslationLocales()\n const [newLocale, setNewLocale] = React.useState('')\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({ contextId: SUPPORTED_LOCALES_MUTATION_CONTEXT_ID })\n\n const mutation = useMutation({\n mutationFn: async (updatedLocales: string[]) => {\n // optimistic-lock-exempt: single-row tenant supported-locales settings list \u2014 no per-record version / concurrent record edit\n return runMutation({\n operation: async () => {\n const res = await apiCall<{ locales: string[] }>('/api/translations/locales', {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ locales: updatedLocales }),\n })\n if (!res.ok) throw new Error('Failed to save locales')\n return res.result?.locales ?? updatedLocales\n },\n context: {\n formId: SUPPORTED_LOCALES_MUTATION_CONTEXT_ID,\n resourceKind: 'translation-locales',\n retryLastMutation,\n },\n mutationPayload: { locales: updatedLocales },\n })\n },\n onSuccess: (result) => {\n queryClient.setQueryData(['translation-locales'], result)\n flash(t('translations.locales.flash.saved', 'Locales updated'), 'success')\n },\n onError: () => {\n flash(t('translations.locales.flash.error', 'Failed to update locales'), 'error')\n },\n })\n\n const availableLocales = React.useMemo(\n () => ISO_639_1.filter((entry) => !locales.includes(entry.code)).map((entry) => ({\n value: entry.code,\n label: `${entry.code.toUpperCase()} \u2014 ${entry.label}`,\n })),\n [locales],\n )\n\n const addLocale = () => {\n const code = newLocale.toLowerCase().trim()\n if (!code || !isValidIso639(code) || locales.includes(code)) return\n mutation.mutate([...locales, code])\n setNewLocale('')\n }\n\n const removeLocale = (locale: string) => {\n if (locales.length <= 1) return\n mutation.mutate(locales.filter((l) => l !== locale))\n }\n\n if (isLoading) {\n return <LoadingMessage label={t('translations.locales.loading', 'Loading locales...')} className=\"border-0 bg-transparent p-4\" />\n }\n\n return (\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-1\">\n <h3 className=\"text-lg font-semibold\">{t('translations.locales.title', 'Supported locales')}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {t('translations.locales.description', 'Configure which locales are available for translations. Add ISO language codes (e.g. fr, it, ja, zh).')}\n </p>\n </div>\n\n <div className=\"flex flex-wrap gap-2\">\n {locales.map((locale) => (\n <span\n key={locale}\n className=\"inline-flex items-center gap-1.5 rounded-full border bg-muted/50 px-3 py-1 text-sm font-medium\"\n title={getIso639Label(locale) ?? locale}\n >\n {locale.toUpperCase()}{getIso639Label(locale) ? ` \u2014 ${getIso639Label(locale)}` : ''}\n {locales.length > 1 && (\n <IconButton\n variant=\"ghost\"\n size=\"xs\"\n fullRadius\n aria-label={t('translations.locales.remove', 'Remove {{locale}}', { locale: getIso639Label(locale) ?? locale.toUpperCase() })}\n title={t('translations.locales.remove', 'Remove {{locale}}', { locale: getIso639Label(locale) ?? locale.toUpperCase() })}\n onClick={() => removeLocale(locale)}\n disabled={mutation.isPending}\n >\n <X className=\"h-3 w-3\" />\n </IconButton>\n )}\n </span>\n ))}\n </div>\n\n <div className=\"flex gap-2 items-center\">\n <div className=\"max-w-[240px] flex-1\">\n <ComboboxInput\n value={newLocale}\n onChange={setNewLocale}\n placeholder={t('translations.locales.addPlaceholder', 'Search language...')}\n suggestions={availableLocales}\n resolveLabel={(value) => {\n const label = getIso639Label(value)\n return label ? `${value.toUpperCase()} \u2014 ${label}` : value.toUpperCase()\n }}\n />\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={addLocale}\n disabled={mutation.isPending || !newLocale.trim() || !isValidIso639(newLocale) || locales.includes(newLocale.toLowerCase().trim())}\n >\n <Plus className=\"mr-1 h-3 w-3\" />\n {t('translations.locales.add', 'Add')}\n </Button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { ComboboxInput } from '@open-mercato/ui/backend/inputs'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useCustomFieldDefs } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Save, Plus, X } from 'lucide-react'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { locales as defaultLocales } from '@open-mercato/shared/lib/i18n/config'\nimport { ISO_639_1, isValidIso639, getIso639Label } from '@open-mercato/shared/lib/i18n/iso639'\nimport { formatEntityLabel, buildEntityListUrl, getRecordLabel, resolveBaseValue } from '../lib/helpers'\nimport { resolveFieldList } from '../lib/resolve-field-list'\nimport type { ResolvedField } from '../lib/resolve-field-list'\n\nconst TRANSLATION_MUTATION_CONTEXT_ID = 'translations.entity-translations'\nconst SUPPORTED_LOCALES_MUTATION_CONTEXT_ID = 'translations.supported-locales'\n\ntype TranslationManagerProps = {\n entityType?: string\n recordId?: string\n baseValues?: Record<string, unknown>\n translatableFields?: string[]\n mode?: 'standalone' | 'embedded'\n compact?: boolean\n}\n\ntype EntityOption = { entityId: string; label?: string; source?: string }\n\ntype TranslationsResponse = {\n entityType: string\n entityId: string\n translations: Record<string, Record<string, unknown>>\n createdAt?: string\n updatedAt?: string\n}\n\nfunction useTranslationLocales() {\n return useQuery<string[]>({\n queryKey: ['translation-locales'],\n queryFn: async () => {\n const res = await apiCall<{ locales: string[] }>('/api/translations/locales')\n if (!res.ok) return [...defaultLocales]\n return Array.isArray(res.result?.locales) && res.result.locales.length > 0\n ? res.result.locales\n : [...defaultLocales]\n },\n staleTime: 60_000,\n })\n}\n\nexport function TranslationManager({\n entityType: propEntityType,\n recordId: propRecordId,\n baseValues: propBaseValues,\n translatableFields: propTranslatableFields,\n mode = 'standalone',\n compact = false,\n}: TranslationManagerProps) {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const isEmbedded = mode === 'embedded'\n\n const [selectedEntityType, setSelectedEntityType] = React.useState(propEntityType ?? '')\n const [selectedRecordId, setSelectedRecordId] = React.useState(propRecordId ?? '')\n const [activeLocale, setActiveLocale] = React.useState('')\n const [editedTranslations, setEditedTranslations] = React.useState<Record<string, Record<string, string>>>({})\n const editedTranslationsRef = React.useRef<Record<string, Record<string, string>>>({})\n const [hasUserEdited, setHasUserEdited] = React.useState(false)\n const hasUserEditedRef = React.useRef(false)\n\n const entityType = isEmbedded ? (propEntityType ?? '') : selectedEntityType\n const recordId = isEmbedded ? (propRecordId ?? '') : selectedRecordId\n\n const { data: locales = [...defaultLocales] } = useTranslationLocales()\n\n React.useEffect(() => {\n if (locales.length > 0 && (!activeLocale || !locales.includes(activeLocale))) {\n setActiveLocale(locales[0])\n }\n }, [locales, activeLocale])\n\n React.useEffect(() => {\n if (isEmbedded && propEntityType) setSelectedEntityType(propEntityType)\n }, [isEmbedded, propEntityType])\n\n React.useEffect(() => {\n if (isEmbedded && propRecordId) setSelectedRecordId(propRecordId)\n }, [isEmbedded, propRecordId])\n\n const { data: entities, isLoading: loadingEntities, error: entitiesError } = useQuery<{ items: EntityOption[] }>({\n queryKey: ['entities-list', scopeVersion],\n enabled: !isEmbedded,\n queryFn: async () =>\n readApiResultOrThrow('/api/entities/entities', undefined, {\n errorMessage: t('translations.manager.errors.loadEntities', 'Failed to load entities'),\n }),\n })\n\n const entitySuggestions = React.useMemo(\n () =>\n (entities?.items || []).map((item) => ({\n value: item.entityId,\n label: formatEntityLabel(item.entityId, item.label),\n description: item.entityId,\n })),\n [entities],\n )\n\n const resolveEntityLabel = React.useCallback(\n (value: string) => {\n const match = entities?.items?.find((e) => e.entityId === value)\n return match ? formatEntityLabel(match.entityId, match.label) : formatEntityLabel(value)\n },\n [entities],\n )\n\n const listUrl = React.useMemo(() => entityType ? buildEntityListUrl(entityType) : null, [entityType])\n\n const loadRecordSuggestions = React.useCallback(\n async (query?: string) => {\n if (!entityType || !listUrl) return []\n const url = `${listUrl}?pageSize=20${query ? `&search=${encodeURIComponent(query)}` : ''}`\n const res = await apiCall<{ items: Array<Record<string, unknown>> }>(url)\n if (!res.ok) return []\n const items = res.result?.items ?? []\n return items.map((item) => ({\n value: String(item.id ?? ''),\n label: getRecordLabel(item),\n }))\n },\n [entityType, listUrl],\n )\n\n const { data: recordData } = useQuery<Record<string, unknown> | null>({\n queryKey: ['translation-record-data', entityType, recordId, listUrl, scopeVersion],\n enabled: !isEmbedded && !!entityType && !!recordId && !!listUrl,\n queryFn: async () => {\n const res = await apiCall<{ items: Array<Record<string, unknown>> }>(\n // Some APIs filter by `id` (catalog), others by `ids` (resources) \u2014 send both so the one recognized by the target route's buildFilters is applied\n `${listUrl}?id=${encodeURIComponent(recordId)}&ids=${encodeURIComponent(recordId)}&pageSize=1`,\n )\n if (!res.ok) return null\n const items = res.result?.items\n return Array.isArray(items) && items.length > 0 ? items[0] : null\n },\n })\n\n const baseValues = isEmbedded ? (propBaseValues ?? {}) : (recordData ?? {})\n\n const resolveRecordLabel = React.useCallback(\n (value: string) => {\n if (recordData) return getRecordLabel(recordData)\n return value\n },\n [recordData],\n )\n\n const { data: fieldDefs = [], isLoading: loadingFieldDefs } = useCustomFieldDefs(entityType ? [entityType] : [], {\n enabled: !!entityType,\n })\n\n const fieldList = React.useMemo(\n () => resolveFieldList(entityType, propTranslatableFields, fieldDefs as Array<{ key: string; kind: string; label?: string }>),\n [entityType, propTranslatableFields, fieldDefs],\n )\n\n const {\n data: translationData,\n isLoading: loadingTranslation,\n isError: translationError,\n refetch: refetchTranslation,\n } = useQuery<TranslationsResponse | null>({\n queryKey: ['entity-translation', entityType, recordId, scopeVersion],\n enabled: !!entityType && !!recordId,\n queryFn: async () => {\n const res = await apiCall<TranslationsResponse>(\n `/api/translations/${encodeURIComponent(entityType)}/${encodeURIComponent(recordId)}`,\n )\n if (!res.ok) {\n if (res.response?.status === 404) return null\n return null\n }\n return res.result ?? null\n },\n })\n\n // Optimistic lock keys off the TRANSLATION ROW'S OWN version (`updatedAt` from\n // the GET response), not the host entity's: the host's EAV `entityType`\n // (`module:entity`) has no reliable server-side mapping to a registered\n // optimistic-lock reader, so the route enforces against the translation row's\n // own `updated_at`. `null` for a brand-new translation (no existing row \u2192 the\n // header is omitted and the route enforces nothing on insert).\n const translationRowUpdatedAt = React.useMemo(() => {\n const value = translationData?.updatedAt\n return typeof value === 'string' && value.trim().length > 0 ? value : null\n }, [translationData])\n\n const translationSignature = React.useMemo(() => JSON.stringify(translationData ?? null), [translationData])\n const lastTranslationSignatureRef = React.useRef<string | null>(null)\n\n React.useEffect(() => {\n const sig = translationSignature\n if (sig === lastTranslationSignatureRef.current && hasUserEditedRef.current) return\n lastTranslationSignatureRef.current = sig\n\n if (!translationData?.translations) {\n if (!hasUserEditedRef.current) {\n editedTranslationsRef.current = {}\n setEditedTranslations({})\n }\n return\n }\n\n const parsed: Record<string, Record<string, string>> = {}\n for (const [locale, fields] of Object.entries(translationData.translations)) {\n if (!fields || typeof fields !== 'object') continue\n parsed[locale] = {}\n for (const [key, val] of Object.entries(fields)) {\n parsed[locale][key] = typeof val === 'string' ? val : ''\n }\n }\n if (!hasUserEditedRef.current) {\n editedTranslationsRef.current = parsed\n setEditedTranslations(parsed)\n }\n }, [translationSignature, translationData])\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n entityType: string\n recordId: string\n resourceKind: string\n resourceId: string\n data: TranslationsResponse | null\n retryLastMutation: () => Promise<boolean>\n }>({ contextId: TRANSLATION_MUTATION_CONTEXT_ID })\n\n const mutation = useMutation({\n mutationFn: async () => {\n if (!entityType || !recordId) {\n throw new Error(t('translations.manager.errors.selectRecord', 'Select an entity and record before saving'))\n }\n const body: Record<string, Record<string, string | null>> = {}\n for (const [locale, fields] of Object.entries(editedTranslationsRef.current)) {\n const localeFields: Record<string, string | null> = {}\n let hasValues = false\n for (const [key, val] of Object.entries(fields)) {\n if (val && val.trim().length > 0) {\n localeFields[key] = val.trim()\n hasValues = true\n }\n }\n if (hasValues) body[locale] = localeFields\n }\n if (Object.keys(body).length === 0) {\n console.warn('[translations] Save skipped: payload is empty \u2014 no locale contains any non-empty field')\n throw new Error(t('translations.manager.errors.nothingToSave', 'Nothing to save \u2014 enter a translation first'))\n }\n return runMutation({\n operation: async () => {\n const res = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(translationRowUpdatedAt),\n () => apiCall(\n `/api/translations/${encodeURIComponent(entityType)}/${encodeURIComponent(recordId)}`,\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n },\n ),\n )\n if (!res.ok) {\n throw new Error(t('translations.manager.errors.save', 'Failed to save translations'))\n }\n return true\n },\n context: {\n formId: TRANSLATION_MUTATION_CONTEXT_ID,\n entityType,\n recordId,\n resourceKind: 'translation',\n resourceId: recordId,\n data: translationData ?? null,\n retryLastMutation,\n },\n mutationPayload: body,\n })\n },\n onSuccess: () => {\n flash(t('translations.manager.flash.saved', 'Translations saved'), 'success')\n hasUserEditedRef.current = false\n setHasUserEdited(false)\n void refetchTranslation()\n },\n onError: (err: unknown) => {\n if (surfaceRecordConflict(err, t)) return\n const message = err instanceof Error ? err.message : t('translations.manager.errors.save', 'Failed to save translations')\n flash(message, 'error')\n },\n })\n\n const updateFieldValue = (locale: string, fieldKey: string, value: string) => {\n hasUserEditedRef.current = true\n setHasUserEdited(true)\n const next = {\n ...editedTranslationsRef.current,\n [locale]: {\n ...editedTranslationsRef.current[locale],\n [fieldKey]: value,\n },\n }\n editedTranslationsRef.current = next\n setEditedTranslations(next)\n }\n\n const getBaseValue = (fieldKey: string): string => resolveBaseValue(baseValues, fieldKey)\n\n const renderRecordPicker = () => {\n if (isEmbedded) return null\n\n return (\n <div className=\"space-y-2\">\n <label className=\"text-xs text-muted-foreground\">\n {t('translations.manager.selectRecord', 'Select record')}\n </label>\n <ComboboxInput\n value={selectedRecordId}\n onChange={(next) => {\n setSelectedRecordId(next)\n hasUserEditedRef.current = false\n setHasUserEdited(false)\n }}\n placeholder={t('translations.manager.searchRecords', 'Search records...')}\n loadSuggestions={loadRecordSuggestions}\n resolveLabel={resolveRecordLabel}\n allowCustomValues\n disabled={!entityType}\n />\n </div>\n )\n }\n\n const renderLocaleTabs = () => (\n <Tabs variant=\"underline\" value={activeLocale} onValueChange={setActiveLocale}>\n <TabsList>\n {locales.map((locale) => (\n <TabsTrigger key={locale} value={locale}>\n {locale.toUpperCase()}\n </TabsTrigger>\n ))}\n </TabsList>\n </Tabs>\n )\n\n const renderFieldTable = () => {\n if (!entityType || !recordId) {\n return (\n <div className=\"rounded border bg-background/80 p-4 text-sm text-muted-foreground\">\n {t('translations.manager.selectFirst', 'Select an entity and record to manage translations.')}\n </div>\n )\n }\n if (loadingTranslation || loadingFieldDefs) {\n return (\n <LoadingMessage\n label={t('translations.manager.loadingTranslations', 'Loading translations...')}\n className=\"border-0 bg-transparent p-4\"\n />\n )\n }\n if (translationError) {\n return (\n <ErrorMessage\n label={t('translations.manager.errors.loadTranslation', 'Failed to load translations')}\n action={(\n <Button variant=\"outline\" size=\"sm\" onClick={() => void refetchTranslation()}>\n {t('translations.manager.actions.retry', 'Retry')}\n </Button>\n )}\n />\n )\n }\n if (!fieldList.length) {\n return (\n <div className=\"rounded border bg-background/80 p-4 text-sm text-muted-foreground\">\n {t('translations.manager.noFields', 'No translatable fields found for this entity type.')}\n </div>\n )\n }\n\n const localeTranslations = editedTranslations[activeLocale] ?? {}\n\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[480px] text-sm\">\n <thead>\n <tr className=\"text-xs uppercase tracking-wide text-muted-foreground\">\n <th className=\"px-3 py-2 text-left w-[140px]\">\n {t('translations.manager.fields.field', 'Field')}\n </th>\n {!compact && (\n <th className=\"px-3 py-2 text-left\">\n {t('translations.manager.fields.baseValue', 'Base value')}\n </th>\n )}\n <th className=\"px-3 py-2 text-left\">\n {t('translations.manager.fields.translation', 'Translation')} ({activeLocale.toUpperCase()})\n </th>\n </tr>\n </thead>\n <tbody>\n {fieldList.map((field) => {\n const baseVal = getBaseValue(field.key)\n const translatedVal = localeTranslations[field.key] ?? ''\n\n return (\n <tr key={field.key} className=\"border-t\">\n <td className=\"px-3 py-2 align-top text-xs font-medium text-muted-foreground\">\n {field.label}\n </td>\n {!compact && (\n <td className=\"px-3 py-2 align-top text-xs text-muted-foreground max-w-[200px]\">\n {baseVal ? (\n <span className=\"line-clamp-3\">{baseVal}</span>\n ) : (\n <span className=\"text-muted-foreground/50\">-</span>\n )}\n </td>\n )}\n <td className=\"px-3 py-2 align-top\">\n {field.multiline ? (\n <textarea\n className=\"flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\"\n rows={3}\n value={translatedVal}\n onChange={(e) => updateFieldValue(activeLocale, field.key, e.target.value)}\n placeholder={baseVal || field.label}\n />\n ) : (\n <Input\n value={translatedVal}\n onChange={(e) => updateFieldValue(activeLocale, field.key, e.target.value)}\n placeholder={baseVal || field.label}\n />\n )}\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n }\n\n React.useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault()\n if (entityType && recordId && !mutation.isPending) mutation.mutate()\n }\n }\n document.addEventListener('keydown', handler)\n return () => document.removeEventListener('keydown', handler)\n }, [entityType, recordId, mutation])\n\n if (compact) {\n return (\n <div className=\"space-y-3\">\n {renderLocaleTabs()}\n {renderFieldTable()}\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n size=\"sm\"\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || !entityType || !recordId}\n data-testid=\"translations-save\"\n >\n <Save className=\"mr-2 h-3 w-3\" />\n {mutation.isPending\n ? t('translations.manager.actions.saving', 'Saving...')\n : t('translations.manager.actions.save', 'Save translations')}\n </Button>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"space-y-6\">\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('translations.manager.title', 'Translations')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('translations.manager.description', 'Manage translations for entity records across supported locales.')}\n </p>\n </div>\n\n {!isEmbedded && (\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start\">\n <div className=\"flex-1 space-y-3\">\n <div>\n <label className=\"text-xs text-muted-foreground\">\n {t('translations.manager.selectEntity', 'Choose entity')}\n </label>\n <div className=\"mt-1\">\n <ComboboxInput\n value={selectedEntityType}\n onChange={(next) => {\n setSelectedEntityType(next)\n setSelectedRecordId('')\n hasUserEditedRef.current = false\n setHasUserEdited(false)\n }}\n placeholder={t('translations.manager.placeholder', 'Select an entity')}\n suggestions={entitySuggestions}\n resolveLabel={resolveEntityLabel}\n disabled={loadingEntities || !!entitiesError}\n />\n </div>\n {entitiesError && (\n <p className=\"mt-1 text-xs text-destructive\">\n {t('translations.manager.errors.loadEntities', 'Failed to load entities')}\n </p>\n )}\n </div>\n {renderRecordPicker()}\n </div>\n </div>\n )}\n\n <div className=\"rounded-lg border bg-background/80 p-4\">\n {renderLocaleTabs()}\n <div className=\"mt-3\">\n {renderFieldTable()}\n </div>\n </div>\n\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n onClick={() => mutation.mutate()}\n disabled={mutation.isPending || loadingEntities || !!entitiesError || !entityType || !recordId}\n data-testid=\"translations-save\"\n >\n <Save className=\"mr-2 h-4 w-4\" />\n {mutation.isPending\n ? t('translations.manager.actions.saving', 'Saving...')\n : t('translations.manager.actions.save', 'Save translations')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n\nexport function LocaleManager() {\n const t = useT()\n const queryClient = useQueryClient()\n const { data: locales = [], isLoading } = useTranslationLocales()\n const [newLocale, setNewLocale] = React.useState('')\n\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({ contextId: SUPPORTED_LOCALES_MUTATION_CONTEXT_ID })\n\n const mutation = useMutation({\n mutationFn: async (updatedLocales: string[]) => {\n // optimistic-lock-exempt: single-row tenant supported-locales settings list \u2014 no per-record version / concurrent record edit\n return runMutation({\n operation: async () => {\n const res = await apiCall<{ locales: string[] }>('/api/translations/locales', {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ locales: updatedLocales }),\n })\n if (!res.ok) throw new Error('Failed to save locales')\n return res.result?.locales ?? updatedLocales\n },\n context: {\n formId: SUPPORTED_LOCALES_MUTATION_CONTEXT_ID,\n resourceKind: 'translation-locales',\n retryLastMutation,\n },\n mutationPayload: { locales: updatedLocales },\n })\n },\n onSuccess: (result) => {\n queryClient.setQueryData(['translation-locales'], result)\n flash(t('translations.locales.flash.saved', 'Locales updated'), 'success')\n },\n onError: () => {\n flash(t('translations.locales.flash.error', 'Failed to update locales'), 'error')\n },\n })\n\n const availableLocales = React.useMemo(\n () => ISO_639_1.filter((entry) => !locales.includes(entry.code)).map((entry) => ({\n value: entry.code,\n label: `${entry.code.toUpperCase()} \u2014 ${entry.label}`,\n })),\n [locales],\n )\n\n const addLocale = () => {\n const code = newLocale.toLowerCase().trim()\n if (!code || !isValidIso639(code) || locales.includes(code)) return\n mutation.mutate([...locales, code])\n setNewLocale('')\n }\n\n const removeLocale = (locale: string) => {\n if (locales.length <= 1) return\n mutation.mutate(locales.filter((l) => l !== locale))\n }\n\n if (isLoading) {\n return <LoadingMessage label={t('translations.locales.loading', 'Loading locales...')} className=\"border-0 bg-transparent p-4\" />\n }\n\n return (\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 shadow-sm\">\n <div className=\"space-y-1\">\n <h3 className=\"text-lg font-semibold\">{t('translations.locales.title', 'Supported locales')}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {t('translations.locales.description', 'Configure which locales are available for translations. Add ISO language codes (e.g. fr, it, ja, zh).')}\n </p>\n </div>\n\n <div className=\"flex flex-wrap gap-2\">\n {locales.map((locale) => (\n <span\n key={locale}\n className=\"inline-flex items-center gap-1.5 rounded-full border bg-muted/50 px-3 py-1 text-sm font-medium\"\n title={getIso639Label(locale) ?? locale}\n >\n {locale.toUpperCase()}{getIso639Label(locale) ? ` \u2014 ${getIso639Label(locale)}` : ''}\n {locales.length > 1 && (\n <IconButton\n variant=\"ghost\"\n size=\"xs\"\n fullRadius\n aria-label={t('translations.locales.remove', 'Remove {{locale}}', { locale: getIso639Label(locale) ?? locale.toUpperCase() })}\n title={t('translations.locales.remove', 'Remove {{locale}}', { locale: getIso639Label(locale) ?? locale.toUpperCase() })}\n onClick={() => removeLocale(locale)}\n disabled={mutation.isPending}\n >\n <X className=\"h-3 w-3\" />\n </IconButton>\n )}\n </span>\n ))}\n </div>\n\n <div className=\"flex gap-2 items-center\">\n <div className=\"max-w-[240px] flex-1\">\n <ComboboxInput\n value={newLocale}\n onChange={setNewLocale}\n placeholder={t('translations.locales.addPlaceholder', 'Search language...')}\n suggestions={availableLocales}\n resolveLabel={(value) => {\n const label = getIso639Label(value)\n return label ? `${value.toUpperCase()} \u2014 ${label}` : value.toUpperCase()\n }}\n />\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={addLocale}\n disabled={mutation.isPending || !newLocale.trim() || !isValidIso639(newLocale) || locales.includes(newLocale.toLowerCase().trim())}\n >\n <Plus className=\"mr-1 h-3 w-3\" />\n {t('translations.locales.add', 'Add')}\n </Button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,8 +3,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { useParams } from "next/navigation";
|
|
5
5
|
import Link from "next/link";
|
|
6
|
+
import { useQuery } from "@tanstack/react-query";
|
|
6
7
|
import { ExternalLink, Languages } from "lucide-react";
|
|
7
8
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
9
|
+
import { hasAllFeatures } from "@open-mercato/shared/lib/auth/featureMatch";
|
|
10
|
+
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
8
11
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
9
12
|
import {
|
|
10
13
|
Drawer,
|
|
@@ -16,21 +19,23 @@ import {
|
|
|
16
19
|
} from "@open-mercato/ui/primitives/drawer";
|
|
17
20
|
import { TranslationManager } from "../../../components/TranslationManager.js";
|
|
18
21
|
import { extractRecordId } from "../../../lib/extract-record-id.js";
|
|
22
|
+
const TRANSLATION_ACCESS_FEATURES = ["translations.view"];
|
|
19
23
|
function useTranslationAccess() {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
const { data } = useQuery({
|
|
25
|
+
queryKey: ["translations", "widget-access", ...TRANSLATION_ACCESS_FEATURES],
|
|
26
|
+
queryFn: async () => {
|
|
27
|
+
const res = await apiCall("/api/auth/feature-check", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "content-type": "application/json" },
|
|
30
|
+
body: JSON.stringify({ features: TRANSLATION_ACCESS_FEATURES })
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) return false;
|
|
33
|
+
return hasAllFeatures(TRANSLATION_ACCESS_FEATURES, res.result?.granted ?? []);
|
|
34
|
+
},
|
|
35
|
+
staleTime: 5 * 60 * 1e3,
|
|
36
|
+
retry: false
|
|
37
|
+
});
|
|
38
|
+
return data === true;
|
|
34
39
|
}
|
|
35
40
|
function TranslationWidget({ context, data }) {
|
|
36
41
|
const entityType = context?.entityId;
|
package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/translations/widgets/injection/translation-manager/widget.client.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useParams } from 'next/navigation'\nimport Link from 'next/link'\nimport { ExternalLink, Languages } from 'lucide-react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Drawer,\n DrawerBody,\n DrawerContent,\n DrawerDescription,\n DrawerHeader,\n DrawerTitle,\n} from '@open-mercato/ui/primitives/drawer'\nimport { TranslationManager } from '../../../components/TranslationManager'\nimport { extractRecordId } from '../../../lib/extract-record-id'\n\ntype WidgetContext = { entityId?: string; recordId?: string }\ntype WidgetData = Record<string, unknown> & { id?: string | number }\n\
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useParams } from 'next/navigation'\nimport Link from 'next/link'\nimport { useQuery } from '@tanstack/react-query'\nimport { ExternalLink, Languages } from 'lucide-react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Drawer,\n DrawerBody,\n DrawerContent,\n DrawerDescription,\n DrawerHeader,\n DrawerTitle,\n} from '@open-mercato/ui/primitives/drawer'\nimport { TranslationManager } from '../../../components/TranslationManager'\nimport { extractRecordId } from '../../../lib/extract-record-id'\n\ntype WidgetContext = { entityId?: string; recordId?: string }\ntype WidgetData = Record<string, unknown> & { id?: string | number }\n\ntype FeatureCheckResponse = {\n ok: boolean\n granted: string[]\n userId: string\n}\n\nconst TRANSLATION_ACCESS_FEATURES = ['translations.view']\n\n// Probe access through the shared apiCall/React Query path so multiple injected\n// widget instances dedupe a single request via the query key. feature-check\n// always answers 200 with the granted feature list, so a user without\n// translations.view is hidden gracefully \u2014 no login redirect, no forbidden flash.\nfunction useTranslationAccess(): boolean {\n const { data } = useQuery<boolean>({\n queryKey: ['translations', 'widget-access', ...TRANSLATION_ACCESS_FEATURES],\n queryFn: async () => {\n const res = await apiCall<FeatureCheckResponse>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: TRANSLATION_ACCESS_FEATURES }),\n })\n if (!res.ok) return false\n return hasAllFeatures(TRANSLATION_ACCESS_FEATURES, res.result?.granted ?? [])\n },\n staleTime: 5 * 60 * 1000,\n retry: false,\n })\n return data === true\n}\n\nexport default function TranslationWidget({ context, data }: InjectionWidgetComponentProps<WidgetContext, WidgetData>) {\n const entityType = context?.entityId\n const params = useParams()\n const t = useT()\n const [open, setOpen] = React.useState(false)\n const hasAccess = useTranslationAccess()\n\n const contextRecordId = typeof context?.recordId === 'string' && context.recordId.trim().length > 0\n ? context.recordId.trim()\n : undefined\n const dataRecordId = data?.id === undefined || data.id === null ? undefined : String(data.id)\n const routeRecordId = params ? extractRecordId(params as Record<string, string | string[]>) : undefined\n const recordId = contextRecordId ?? dataRecordId ?? routeRecordId\n const canRender = Boolean(entityType && recordId && hasAccess)\n\n if (!canRender) return null\n\n return (\n <Drawer open={open} onOpenChange={setOpen}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setOpen(true)}\n aria-label={t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n title={t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n >\n <Languages className=\"size-4\" />\n </Button>\n <DrawerContent className=\"max-w-4xl\">\n <DrawerHeader>\n <DrawerTitle>\n {t('translations.widgets.translationManager.groupLabel', 'Translations')}\n </DrawerTitle>\n <DrawerDescription>\n {t(\n 'translations.widgets.translationManager.groupDescription',\n 'Manage translations for this record across supported locales.',\n )}\n </DrawerDescription>\n </DrawerHeader>\n <DrawerBody>\n <TranslationManager\n mode=\"embedded\"\n compact\n entityType={entityType}\n recordId={recordId}\n baseValues={data}\n />\n </DrawerBody>\n {/* Custom footer: 2 inline link affordances (not action buttons),\n wrap-friendly + left-aligned. DS DrawerFooter layouts assume\n buttons in the trailing slot, so we hand-roll the row with\n the DrawerFooter padding convention (`px-6 pt-4 pb-5`). */}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 px-6 pt-4 pb-5\">\n <Link\n href={`/backend/entities/system/${encodeURIComponent(entityType!)}`}\n className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n <Languages className=\"size-3\" />\n {t('translations.widgets.translationManager.customFieldLabels', 'Custom fields translations')}\n <ExternalLink className=\"size-2.5\" />\n </Link>\n <Link\n href=\"/backend/config/translations\"\n className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n <Languages className=\"size-3\" />\n {t('translations.widgets.translationManager.fullManager', 'Translation manager')}\n <ExternalLink className=\"size-2.5\" />\n </Link>\n </div>\n </DrawerContent>\n </Drawer>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -14,7 +14,7 @@ import { getCodeWorkflow } from "../../../lib/code-registry.js";
|
|
|
14
14
|
import { createGenericOptimisticLockReader } from "@open-mercato/shared/lib/crud/optimistic-lock";
|
|
15
15
|
import { registerOptimisticLockReaderIfAbsent } from "@open-mercato/shared/lib/crud/optimistic-lock-store";
|
|
16
16
|
import { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
17
|
-
import {
|
|
17
|
+
import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
18
18
|
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
19
19
|
registerOptimisticLockReaderIfAbsent({
|
|
20
20
|
"workflows.definition": createGenericOptimisticLockReader({
|
|
@@ -144,7 +144,7 @@ async function PUT(request, context) {
|
|
|
144
144
|
let savedOverride;
|
|
145
145
|
if (existingOverride) {
|
|
146
146
|
try {
|
|
147
|
-
|
|
147
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
148
148
|
resourceKind: "workflows.definition",
|
|
149
149
|
resourceId: existingOverride.id,
|
|
150
150
|
current: existingOverride.updatedAt ?? null,
|
|
@@ -239,7 +239,7 @@ async function PUT(request, context) {
|
|
|
239
239
|
);
|
|
240
240
|
}
|
|
241
241
|
try {
|
|
242
|
-
|
|
242
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
243
243
|
resourceKind: "workflows.definition",
|
|
244
244
|
resourceId: definition.id,
|
|
245
245
|
current: definition.updatedAt ?? null,
|