@open-mercato/core 0.6.4-develop.3996.1.430e257cfc → 0.6.4-develop.4011.1.4f3ed9ae3e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/api/features.js +24 -2
- package/dist/modules/auth/api/features.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +70 -57
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/components/AclDependencyDiagnosticsPanel.js +135 -0
- package/dist/modules/auth/components/AclDependencyDiagnosticsPanel.js.map +7 -0
- package/dist/modules/auth/components/AclEditor.js +10 -0
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/catalog/acl.js +30 -5
- package/dist/modules/catalog/acl.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +17 -5
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/commands/offers.js +26 -7
- package/dist/modules/catalog/commands/offers.js.map +2 -2
- package/dist/modules/catalog/commands/prices.js +41 -26
- package/dist/modules/catalog/commands/prices.js.map +2 -2
- package/dist/modules/catalog/commands/productUnitConversions.js +7 -1
- package/dist/modules/catalog/commands/productUnitConversions.js.map +2 -2
- package/dist/modules/catalog/commands/products.js +2 -0
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/commands/shared.js +58 -11
- package/dist/modules/catalog/commands/shared.js.map +2 -2
- package/dist/modules/catalog/commands/variants.js +18 -5
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/customers/acl.js +72 -12
- package/dist/modules/customers/acl.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +17 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/sales/acl.js +109 -16
- package/dist/modules/sales/acl.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +20 -1
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/setup.js +2 -0
- package/dist/modules/sales/setup.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/features.ts +34 -4
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +28 -6
- package/src/modules/auth/components/AclDependencyDiagnosticsPanel.tsx +173 -0
- package/src/modules/auth/components/AclEditor.tsx +14 -4
- package/src/modules/auth/i18n/de.json +9 -0
- package/src/modules/auth/i18n/en.json +9 -0
- package/src/modules/auth/i18n/es.json +9 -0
- package/src/modules/auth/i18n/pl.json +9 -0
- package/src/modules/catalog/acl.ts +30 -5
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -5
- package/src/modules/catalog/commands/offers.ts +26 -7
- package/src/modules/catalog/commands/prices.ts +41 -26
- package/src/modules/catalog/commands/productUnitConversions.ts +7 -1
- package/src/modules/catalog/commands/products.ts +2 -0
- package/src/modules/catalog/commands/shared.ts +70 -6
- package/src/modules/catalog/commands/variants.ts +18 -5
- package/src/modules/catalog/i18n/de.json +1 -0
- package/src/modules/catalog/i18n/en.json +1 -0
- package/src/modules/catalog/i18n/es.json +1 -0
- package/src/modules/catalog/i18n/pl.json +1 -0
- package/src/modules/customers/acl.ts +72 -12
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +21 -2
- package/src/modules/sales/acl.ts +109 -16
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +28 -1
- package/src/modules/sales/i18n/de.json +3 -0
- package/src/modules/sales/i18n/en.json +3 -0
- package/src/modules/sales/i18n/es.json +3 -0
- package/src/modules/sales/i18n/pl.json +3 -0
- package/src/modules/sales/setup.ts +2 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/sales/setup.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { SalesSettings, SalesDocumentSequence, SalesTaxRate } from './data/entities'\nimport { DEFAULT_ORDER_NUMBER_FORMAT, DEFAULT_QUOTE_NUMBER_FORMAT } from './lib/documentNumberTokens'\nimport { seedSalesStatusDictionaries, seedSalesAdjustmentKinds } from './lib/dictionaries'\nimport { ensureExampleShippingMethods, ensureExamplePaymentMethods } from './seed/examples-data'\nimport { seedSalesExamples } from './seed/examples'\n\ntype SeedScope = { tenantId: string; organizationId: string }\n\nconst DEFAULT_TAX_RATES = [\n { code: 'vat-23', name: '23% VAT', rate: '23' },\n { code: 'vat-0', name: '0% VAT', rate: '0' },\n] as const\n\nasync function seedSalesTaxRates(em: EntityManager, scope: SeedScope): Promise<void> {\n await em.transactional(async (tem) => {\n const existing = await tem.find(SalesTaxRate, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n const existingCodes = new Set(existing.map((rate) => rate.code))\n const hasDefault = existing.some((rate) => rate.isDefault)\n const now = new Date()\n let isFirst = !hasDefault\n\n for (const seed of DEFAULT_TAX_RATES) {\n if (existingCodes.has(seed.code)) continue\n tem.persist(\n tem.create(SalesTaxRate, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n code: seed.code,\n name: seed.name,\n rate: seed.rate,\n priority: 0,\n isCompound: false,\n isDefault: isFirst,\n createdAt: now,\n updatedAt: now,\n })\n )\n isFirst = false\n }\n })\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: ['sales.*', 'sales.documents.number.edit'],\n employee: [\n 'sales.orders.view',\n 'sales.orders.manage',\n 'sales.orders.approve',\n 'sales.widgets.new-orders',\n 'sales.widgets.new-quotes',\n 'sales.quotes.view',\n 'sales.quotes.manage',\n 'sales.shipments.manage',\n 'sales.payments.manage',\n 'sales.returns.view',\n 'sales.returns.create',\n 'sales.invoices.manage',\n 'sales.credit_memos.manage',\n ],\n },\n\n async onTenantCreated({ em, tenantId, organizationId }) {\n const exists = await em.findOne(SalesSettings, { tenantId, organizationId })\n if (!exists) {\n em.persist(\n em.create(SalesSettings, {\n tenantId,\n organizationId,\n orderNumberFormat: DEFAULT_ORDER_NUMBER_FORMAT,\n quoteNumberFormat: DEFAULT_QUOTE_NUMBER_FORMAT,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n )\n }\n\n for (const kind of ['order', 'quote', 'return', 'invoice', 'credit_memo'] as const) {\n const seq = await em.findOne(SalesDocumentSequence, {\n tenantId,\n organizationId,\n documentKind: kind,\n })\n if (!seq) {\n em.persist(\n em.create(SalesDocumentSequence, {\n tenantId,\n organizationId,\n documentKind: kind,\n currentValue: 0,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n )\n }\n }\n\n await em.flush()\n },\n\n async seedDefaults({ em, tenantId, organizationId }) {\n const scope = { tenantId, organizationId }\n await seedSalesTaxRates(em, scope)\n await seedSalesStatusDictionaries(em, scope)\n await seedSalesAdjustmentKinds(em, scope)\n await ensureExampleShippingMethods(em, scope)\n await ensureExamplePaymentMethods(em, scope)\n },\n\n async seedExamples({ em, container, tenantId, organizationId }) {\n const scope = { tenantId, organizationId }\n await seedSalesExamples(em, container, scope)\n },\n}\n\nexport default setup\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { SalesSettings, SalesDocumentSequence, SalesTaxRate } from './data/entities'\nimport { DEFAULT_ORDER_NUMBER_FORMAT, DEFAULT_QUOTE_NUMBER_FORMAT } from './lib/documentNumberTokens'\nimport { seedSalesStatusDictionaries, seedSalesAdjustmentKinds } from './lib/dictionaries'\nimport { ensureExampleShippingMethods, ensureExamplePaymentMethods } from './seed/examples-data'\nimport { seedSalesExamples } from './seed/examples'\n\ntype SeedScope = { tenantId: string; organizationId: string }\n\nconst DEFAULT_TAX_RATES = [\n { code: 'vat-23', name: '23% VAT', rate: '23' },\n { code: 'vat-0', name: '0% VAT', rate: '0' },\n] as const\n\nasync function seedSalesTaxRates(em: EntityManager, scope: SeedScope): Promise<void> {\n await em.transactional(async (tem) => {\n const existing = await tem.find(SalesTaxRate, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n })\n const existingCodes = new Set(existing.map((rate) => rate.code))\n const hasDefault = existing.some((rate) => rate.isDefault)\n const now = new Date()\n let isFirst = !hasDefault\n\n for (const seed of DEFAULT_TAX_RATES) {\n if (existingCodes.has(seed.code)) continue\n tem.persist(\n tem.create(SalesTaxRate, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n code: seed.code,\n name: seed.name,\n rate: seed.rate,\n priority: 0,\n isCompound: false,\n isDefault: isFirst,\n createdAt: now,\n updatedAt: now,\n })\n )\n isFirst = false\n }\n })\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: ['sales.*', 'sales.documents.number.edit'],\n employee: [\n 'sales.channels.view',\n 'sales.settings.view',\n 'sales.orders.view',\n 'sales.orders.manage',\n 'sales.orders.approve',\n 'sales.widgets.new-orders',\n 'sales.widgets.new-quotes',\n 'sales.quotes.view',\n 'sales.quotes.manage',\n 'sales.shipments.manage',\n 'sales.payments.manage',\n 'sales.returns.view',\n 'sales.returns.create',\n 'sales.invoices.manage',\n 'sales.credit_memos.manage',\n ],\n },\n\n async onTenantCreated({ em, tenantId, organizationId }) {\n const exists = await em.findOne(SalesSettings, { tenantId, organizationId })\n if (!exists) {\n em.persist(\n em.create(SalesSettings, {\n tenantId,\n organizationId,\n orderNumberFormat: DEFAULT_ORDER_NUMBER_FORMAT,\n quoteNumberFormat: DEFAULT_QUOTE_NUMBER_FORMAT,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n )\n }\n\n for (const kind of ['order', 'quote', 'return', 'invoice', 'credit_memo'] as const) {\n const seq = await em.findOne(SalesDocumentSequence, {\n tenantId,\n organizationId,\n documentKind: kind,\n })\n if (!seq) {\n em.persist(\n em.create(SalesDocumentSequence, {\n tenantId,\n organizationId,\n documentKind: kind,\n currentValue: 0,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n )\n }\n }\n\n await em.flush()\n },\n\n async seedDefaults({ em, tenantId, organizationId }) {\n const scope = { tenantId, organizationId }\n await seedSalesTaxRates(em, scope)\n await seedSalesStatusDictionaries(em, scope)\n await seedSalesAdjustmentKinds(em, scope)\n await ensureExampleShippingMethods(em, scope)\n await ensureExamplePaymentMethods(em, scope)\n },\n\n async seedExamples({ em, container, tenantId, organizationId }) {\n const scope = { tenantId, organizationId }\n await seedSalesExamples(em, container, scope)\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.4-develop.
|
|
3
|
+
"version": "0.6.4-develop.4011.1.4f3ed9ae3e",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -243,16 +243,16 @@
|
|
|
243
243
|
"zod": "^4.4.3"
|
|
244
244
|
},
|
|
245
245
|
"peerDependencies": {
|
|
246
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
247
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
248
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
246
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.4011.1.4f3ed9ae3e",
|
|
247
|
+
"@open-mercato/shared": "0.6.4-develop.4011.1.4f3ed9ae3e",
|
|
248
|
+
"@open-mercato/ui": "0.6.4-develop.4011.1.4f3ed9ae3e",
|
|
249
249
|
"react": "^19.0.0",
|
|
250
250
|
"react-dom": "^19.0.0"
|
|
251
251
|
},
|
|
252
252
|
"devDependencies": {
|
|
253
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
254
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
255
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
253
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.4011.1.4f3ed9ae3e",
|
|
254
|
+
"@open-mercato/shared": "0.6.4-develop.4011.1.4f3ed9ae3e",
|
|
255
|
+
"@open-mercato/ui": "0.6.4-develop.4011.1.4f3ed9ae3e",
|
|
256
256
|
"@testing-library/dom": "^10.4.1",
|
|
257
257
|
"@testing-library/jest-dom": "^6.9.1",
|
|
258
258
|
"@testing-library/react": "^16.3.1",
|
|
@@ -8,15 +8,44 @@ export const metadata = {
|
|
|
8
8
|
GET: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
type FeatureItem = {
|
|
12
|
+
id: string
|
|
13
|
+
title: string
|
|
14
|
+
module: string
|
|
15
|
+
dependsOn?: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeDependsOn(value: unknown): string[] | undefined {
|
|
19
|
+
if (!Array.isArray(value)) return undefined
|
|
20
|
+
const out: string[] = []
|
|
21
|
+
for (const entry of value) {
|
|
22
|
+
if (typeof entry !== 'string') continue
|
|
23
|
+
const trimmed = entry.trim()
|
|
24
|
+
if (!trimmed) continue
|
|
25
|
+
out.push(trimmed)
|
|
26
|
+
}
|
|
27
|
+
if (out.length === 0) return undefined
|
|
28
|
+
return Array.from(new Set(out))
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
export async function GET(req: Request) {
|
|
12
32
|
const auth = await getAuthFromRequest(req)
|
|
13
33
|
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
14
34
|
const modules = getModules()
|
|
15
|
-
const items = (modules || []).flatMap((m: any) =>
|
|
16
|
-
(m.features || []).map((f: any) =>
|
|
35
|
+
const items: FeatureItem[] = (modules || []).flatMap((m: any) =>
|
|
36
|
+
(m.features || []).map((f: any) => {
|
|
37
|
+
const deps = normalizeDependsOn(f?.dependsOn)
|
|
38
|
+
const base: FeatureItem = {
|
|
39
|
+
id: String(f.id),
|
|
40
|
+
title: String(f.title || f.id),
|
|
41
|
+
module: String(f.module || m.id),
|
|
42
|
+
}
|
|
43
|
+
if (deps) base.dependsOn = deps
|
|
44
|
+
return base
|
|
45
|
+
})
|
|
17
46
|
)
|
|
18
|
-
// Deduplicate by id
|
|
19
|
-
const byId = new Map<string,
|
|
47
|
+
// Deduplicate by id (keep first occurrence)
|
|
48
|
+
const byId = new Map<string, FeatureItem>()
|
|
20
49
|
for (const it of items) if (!byId.has(it.id)) byId.set(it.id, it)
|
|
21
50
|
const list = Array.from(byId.values()).sort((a, b) => a.module.localeCompare(b.module) || a.id.localeCompare(b.id))
|
|
22
51
|
|
|
@@ -35,6 +64,7 @@ const featureItemSchema = z.object({
|
|
|
35
64
|
id: z.string(),
|
|
36
65
|
title: z.string(),
|
|
37
66
|
module: z.string(),
|
|
67
|
+
dependsOn: z.array(z.string()).optional(),
|
|
38
68
|
})
|
|
39
69
|
|
|
40
70
|
const featureModuleSchema = z.object({
|
|
@@ -17,6 +17,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
17
17
|
import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
|
|
18
18
|
import { formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'
|
|
19
19
|
import { UserConsentsPanel } from '@open-mercato/core/modules/auth/components/UserConsentsPanel'
|
|
20
|
+
import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
20
21
|
import { normalizeDisplayNameInput } from '@open-mercato/core/modules/auth/lib/displayName'
|
|
21
22
|
|
|
22
23
|
type EditUserFormValues = {
|
|
@@ -119,6 +120,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
|
|
|
119
120
|
const [selectedTenantId, setSelectedTenantId] = React.useState<string | null>(null)
|
|
120
121
|
const [loading, setLoading] = React.useState(true)
|
|
121
122
|
const [error, setError] = React.useState<string | null>(null)
|
|
123
|
+
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
122
124
|
const [canEditOrgs, setCanEditOrgs] = React.useState(false)
|
|
123
125
|
const [aclData, setAclData] = React.useState<AclData>({ isSuperAdmin: false, features: [], organizations: null })
|
|
124
126
|
const [customFieldValues, setCustomFieldValues] = React.useState<Record<string, unknown>>({})
|
|
@@ -171,6 +173,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
|
|
|
171
173
|
async function load() {
|
|
172
174
|
setLoading(true)
|
|
173
175
|
setError(null)
|
|
176
|
+
setIsNotFound(false)
|
|
174
177
|
setCustomFieldValues({})
|
|
175
178
|
try {
|
|
176
179
|
const { ok, result } = await apiCall<UserListResponse>(
|
|
@@ -182,7 +185,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
|
|
|
182
185
|
setActorIsSuperAdmin(Boolean(result?.isSuperAdmin))
|
|
183
186
|
setActorResolved(true)
|
|
184
187
|
if (!item) {
|
|
185
|
-
|
|
188
|
+
setIsNotFound(true)
|
|
186
189
|
setCustomFieldValues({})
|
|
187
190
|
setInitialUser(null)
|
|
188
191
|
setSelectedTenantId(null)
|
|
@@ -404,14 +407,33 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
|
|
|
404
407
|
}
|
|
405
408
|
}, [initialUser, customFieldValues, selectedTenantId])
|
|
406
409
|
|
|
410
|
+
if (isNotFound) {
|
|
411
|
+
return (
|
|
412
|
+
<Page>
|
|
413
|
+
<PageBody>
|
|
414
|
+
<RecordNotFoundState
|
|
415
|
+
label={t('auth.users.form.errors.notFound', 'User not found')}
|
|
416
|
+
backHref="/backend/users"
|
|
417
|
+
backLabel={t('auth.users.form.actions.backToList', 'Back to users')}
|
|
418
|
+
/>
|
|
419
|
+
</PageBody>
|
|
420
|
+
</Page>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (error && !loading) {
|
|
425
|
+
return (
|
|
426
|
+
<Page>
|
|
427
|
+
<PageBody>
|
|
428
|
+
<ErrorMessage label={error} />
|
|
429
|
+
</PageBody>
|
|
430
|
+
</Page>
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
407
434
|
return (
|
|
408
435
|
<Page>
|
|
409
436
|
<PageBody>
|
|
410
|
-
{error && (
|
|
411
|
-
<div className="p-4 mb-4 bg-red-50 border border-red-200 rounded text-red-800">
|
|
412
|
-
{error}
|
|
413
|
-
</div>
|
|
414
|
-
)}
|
|
415
437
|
<CrudForm<EditUserFormValues>
|
|
416
438
|
title={t('auth.users.form.title.edit', 'Edit User')}
|
|
417
439
|
backHref="/backend/users"
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
4
|
+
import { Alert } from '@open-mercato/ui/primitives/alert'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import {
|
|
7
|
+
applyAddMissingDependency,
|
|
8
|
+
applyRemoveDependents,
|
|
9
|
+
applyRestoreDependency,
|
|
10
|
+
resolveAclDependencyDiagnostics,
|
|
11
|
+
type FeatureDescriptor,
|
|
12
|
+
} from '@open-mercato/shared/security/aclDependencies'
|
|
13
|
+
|
|
14
|
+
export type AclDependencyDiagnosticsPanelProps = {
|
|
15
|
+
granted: readonly string[]
|
|
16
|
+
catalog: readonly FeatureDescriptor[]
|
|
17
|
+
onGrantedChange: (updater: (prev: string[]) => string[]) => void
|
|
18
|
+
hideUnknownReferences?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AclDependencyDiagnosticsPanel({
|
|
22
|
+
granted,
|
|
23
|
+
catalog,
|
|
24
|
+
onGrantedChange,
|
|
25
|
+
hideUnknownReferences,
|
|
26
|
+
}: AclDependencyDiagnosticsPanelProps) {
|
|
27
|
+
const t = useT()
|
|
28
|
+
const diagnostics = React.useMemo(
|
|
29
|
+
() => resolveAclDependencyDiagnostics(granted, catalog),
|
|
30
|
+
[granted, catalog],
|
|
31
|
+
)
|
|
32
|
+
const titleById = React.useMemo(() => {
|
|
33
|
+
const map = new Map<string, string>()
|
|
34
|
+
for (const entry of catalog) {
|
|
35
|
+
if (entry?.title && !map.has(entry.id)) map.set(entry.id, entry.title)
|
|
36
|
+
}
|
|
37
|
+
return map
|
|
38
|
+
}, [catalog])
|
|
39
|
+
const featureLabel = React.useCallback((id: string) => titleById.get(id) ?? id, [titleById])
|
|
40
|
+
|
|
41
|
+
const hasMissing = diagnostics.missingDependencies.length > 0
|
|
42
|
+
const hasOrphaned = diagnostics.orphanedDependents.length > 0
|
|
43
|
+
const showUnknown = !hideUnknownReferences && diagnostics.unknownReferences.length > 0
|
|
44
|
+
if (!hasMissing && !hasOrphaned && !showUnknown) return null
|
|
45
|
+
|
|
46
|
+
const handleAdd = (dep: string) => onGrantedChange((prev) => applyAddMissingDependency(prev, dep))
|
|
47
|
+
const handleRestore = (dep: string) => onGrantedChange((prev) => applyRestoreDependency(prev, dep))
|
|
48
|
+
const handleDropDependents = (dependents: readonly string[]) =>
|
|
49
|
+
onGrantedChange((prev) => applyRemoveDependents(prev, dependents))
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="space-y-3" data-testid="acl-dependency-diagnostics">
|
|
53
|
+
{hasMissing && (
|
|
54
|
+
<Alert status="warning" style="lighter">
|
|
55
|
+
<div className="space-y-2">
|
|
56
|
+
<div className="text-sm font-medium">
|
|
57
|
+
{t(
|
|
58
|
+
'auth.acl.deps.missing.title',
|
|
59
|
+
'Some granted permissions need other permissions to work:',
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
<ul className="space-y-1 text-sm">
|
|
63
|
+
{diagnostics.missingDependencies.map((row) => (
|
|
64
|
+
<li
|
|
65
|
+
key={row.feature}
|
|
66
|
+
className="flex flex-wrap items-center gap-x-2 gap-y-1"
|
|
67
|
+
data-testid={`missing-${row.feature}`}
|
|
68
|
+
>
|
|
69
|
+
<span>
|
|
70
|
+
{t('auth.acl.deps.missing.item', '"{feature}" needs:', {
|
|
71
|
+
feature: featureLabel(row.feature),
|
|
72
|
+
})}
|
|
73
|
+
</span>
|
|
74
|
+
{row.missing.map((dep) => (
|
|
75
|
+
<span key={dep} className="inline-flex items-center gap-1">
|
|
76
|
+
<span className="font-mono text-xs text-muted-foreground">
|
|
77
|
+
{featureLabel(dep)}
|
|
78
|
+
</span>
|
|
79
|
+
<Button
|
|
80
|
+
type="button"
|
|
81
|
+
size="sm"
|
|
82
|
+
variant="outline"
|
|
83
|
+
onClick={() => handleAdd(dep)}
|
|
84
|
+
data-testid={`add-missing-${row.feature}-${dep}`}
|
|
85
|
+
>
|
|
86
|
+
{t('auth.acl.deps.missing.add', 'Add "{dep}"', {
|
|
87
|
+
dep: featureLabel(dep),
|
|
88
|
+
})}
|
|
89
|
+
</Button>
|
|
90
|
+
</span>
|
|
91
|
+
))}
|
|
92
|
+
</li>
|
|
93
|
+
))}
|
|
94
|
+
</ul>
|
|
95
|
+
</div>
|
|
96
|
+
</Alert>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{hasOrphaned && (
|
|
100
|
+
<Alert status="warning" style="lighter">
|
|
101
|
+
<div className="space-y-2">
|
|
102
|
+
<div className="text-sm font-medium">
|
|
103
|
+
{t(
|
|
104
|
+
'auth.acl.deps.orphaned.title',
|
|
105
|
+
'Removing a permission that other granted permissions need:',
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
<ul className="space-y-1 text-sm">
|
|
109
|
+
{diagnostics.orphanedDependents.map((row) => (
|
|
110
|
+
<li
|
|
111
|
+
key={row.dependency}
|
|
112
|
+
className="flex flex-wrap items-center gap-x-2 gap-y-1"
|
|
113
|
+
data-testid={`orphaned-${row.dependency}`}
|
|
114
|
+
>
|
|
115
|
+
<span>
|
|
116
|
+
{t('auth.acl.deps.orphaned.item', '"{dependency}" is required by:', {
|
|
117
|
+
dependency: featureLabel(row.dependency),
|
|
118
|
+
})}
|
|
119
|
+
</span>
|
|
120
|
+
<span className="font-mono text-xs text-muted-foreground">
|
|
121
|
+
{row.dependents
|
|
122
|
+
.map((dependent) => featureLabel(dependent))
|
|
123
|
+
.join(', ')}
|
|
124
|
+
</span>
|
|
125
|
+
<Button
|
|
126
|
+
type="button"
|
|
127
|
+
size="sm"
|
|
128
|
+
variant="outline"
|
|
129
|
+
onClick={() => handleRestore(row.dependency)}
|
|
130
|
+
data-testid={`restore-${row.dependency}`}
|
|
131
|
+
>
|
|
132
|
+
{t('auth.acl.deps.orphaned.restore', 'Restore "{dependency}"', {
|
|
133
|
+
dependency: featureLabel(row.dependency),
|
|
134
|
+
})}
|
|
135
|
+
</Button>
|
|
136
|
+
<Button
|
|
137
|
+
type="button"
|
|
138
|
+
size="sm"
|
|
139
|
+
variant="outline"
|
|
140
|
+
onClick={() => handleDropDependents(row.dependents)}
|
|
141
|
+
data-testid={`drop-dependents-${row.dependency}`}
|
|
142
|
+
>
|
|
143
|
+
{t('auth.acl.deps.orphaned.drop', 'Drop dependents')}
|
|
144
|
+
</Button>
|
|
145
|
+
</li>
|
|
146
|
+
))}
|
|
147
|
+
</ul>
|
|
148
|
+
</div>
|
|
149
|
+
</Alert>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{showUnknown && (
|
|
153
|
+
<Alert status="warning" style="lighter">
|
|
154
|
+
<div className="space-y-1">
|
|
155
|
+
<div className="text-sm font-medium">
|
|
156
|
+
{t(
|
|
157
|
+
'auth.acl.deps.unknown.title',
|
|
158
|
+
'Some declared dependencies do not match any known permission:',
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
<ul className="space-y-1 text-sm font-mono text-xs text-muted-foreground">
|
|
162
|
+
{diagnostics.unknownReferences.map((row) => (
|
|
163
|
+
<li key={row.feature} data-testid={`unknown-${row.feature}`}>
|
|
164
|
+
{row.feature} → {row.missing.join(', ')}
|
|
165
|
+
</li>
|
|
166
|
+
))}
|
|
167
|
+
</ul>
|
|
168
|
+
</div>
|
|
169
|
+
</Alert>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
@@ -5,6 +5,8 @@ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
|
5
5
|
import Link from 'next/link'
|
|
6
6
|
import { hasFeature, matchFeature } from '@open-mercato/shared/security/features'
|
|
7
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
|
+
import type { FeatureDescriptor } from '@open-mercato/shared/security/aclDependencies'
|
|
9
|
+
import { AclDependencyDiagnosticsPanel } from './AclDependencyDiagnosticsPanel'
|
|
8
10
|
|
|
9
11
|
function toTitleCase(value: string): string {
|
|
10
12
|
return value.replace(/[-_.]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase())
|
|
@@ -36,7 +38,7 @@ function formatWildcardLabel(moduleId: string, wildcard: string): string {
|
|
|
36
38
|
return `All ${suffix.split('.').map(toTitleCase).join(' / ')}`
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
type Feature = { id: string; title: string; module: string }
|
|
41
|
+
type Feature = { id: string; title: string; module: string; dependsOn?: string[] }
|
|
40
42
|
type ModuleInfo = { id: string; title: string }
|
|
41
43
|
type RoleListItem = { id?: string | null; name?: string | null }
|
|
42
44
|
type RoleListResponse = { items?: RoleListItem[] }
|
|
@@ -364,9 +366,9 @@ export function AclEditor({
|
|
|
364
366
|
<div className="rounded border border-blue-200 bg-blue-50 p-3">
|
|
365
367
|
<div className="text-sm font-medium text-blue-900">Global wildcard (*) enabled</div>
|
|
366
368
|
<div className="text-xs text-blue-700 mt-1">This grants access to all features in the system.</div>
|
|
367
|
-
<Button
|
|
368
|
-
variant="outline"
|
|
369
|
-
size="sm"
|
|
369
|
+
<Button
|
|
370
|
+
variant="outline"
|
|
371
|
+
size="sm"
|
|
370
372
|
className="mt-2"
|
|
371
373
|
onClick={() => updateGranted((prev) => prev.filter((x) => x !== '*'))}
|
|
372
374
|
>
|
|
@@ -374,6 +376,14 @@ export function AclEditor({
|
|
|
374
376
|
</Button>
|
|
375
377
|
</div>
|
|
376
378
|
)}
|
|
379
|
+
{!hasGlobalWildcard && (
|
|
380
|
+
<AclDependencyDiagnosticsPanel
|
|
381
|
+
granted={granted}
|
|
382
|
+
catalog={features as readonly FeatureDescriptor[]}
|
|
383
|
+
onGrantedChange={updateGranted}
|
|
384
|
+
hideUnknownReferences={process.env.NODE_ENV === 'production'}
|
|
385
|
+
/>
|
|
386
|
+
)}
|
|
377
387
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
378
388
|
{grouped.map((group) => {
|
|
379
389
|
const moduleWildcard = isModuleWildcardEnabled(group.moduleId)
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
"auth.accessDenied.message": "Du hast keine Berechtigung, diese Seite anzuzeigen. Bitte wende dich an deinen Administrator.",
|
|
5
5
|
"auth.accessDenied.title": "Zugriff verweigert",
|
|
6
6
|
"auth.acl.allowAllOrganizations": "Alle Organisationen zulassen",
|
|
7
|
+
"auth.acl.deps.missing.add": "\"{dep}\" hinzufügen",
|
|
8
|
+
"auth.acl.deps.missing.item": "\"{feature}\" benötigt:",
|
|
9
|
+
"auth.acl.deps.missing.title": "Einige gewährte Berechtigungen benötigen weitere Berechtigungen, um zu funktionieren:",
|
|
10
|
+
"auth.acl.deps.orphaned.drop": "Abhängige entfernen",
|
|
11
|
+
"auth.acl.deps.orphaned.item": "\"{dependency}\" wird benötigt von:",
|
|
12
|
+
"auth.acl.deps.orphaned.restore": "\"{dependency}\" wiederherstellen",
|
|
13
|
+
"auth.acl.deps.orphaned.title": "Entfernung einer Berechtigung, die andere gewährte Berechtigungen benötigen:",
|
|
14
|
+
"auth.acl.deps.unknown.title": "Einige deklarierte Abhängigkeiten passen zu keiner bekannten Berechtigung:",
|
|
7
15
|
"auth.acl.organizationWarning": "Organisationseinschränkungen werden nur gespeichert, wenn mindestens eine Berechtigungsüberschreibung ausgewählt ist. Füge eine Berechtigung hinzu oder aktiviere einen Modul-Wildcard vor dem Speichern.",
|
|
8
16
|
"auth.acl.organizationsScope": "Organisationsbereich",
|
|
9
17
|
"auth.acl.restricted": "Eingeschränkt",
|
|
@@ -164,6 +172,7 @@
|
|
|
164
172
|
"auth.users.form.action.resendInvite": "Einladung erneut senden",
|
|
165
173
|
"auth.users.form.action.resendingInvite": "Wird gesendet...",
|
|
166
174
|
"auth.users.form.action.save": "Speichern",
|
|
175
|
+
"auth.users.form.actions.backToList": "Zurück zu Benutzern",
|
|
167
176
|
"auth.users.form.errors.aclUpdate": "Aktualisierung der Benutzerberechtigungen fehlgeschlagen",
|
|
168
177
|
"auth.users.form.errors.delete": "Benutzer konnte nicht gelöscht werden",
|
|
169
178
|
"auth.users.form.errors.inviteResend": "Einladungs-E-Mail konnte nicht gesendet werden",
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
"auth.accessDenied.message": "You do not have permission to view this page. Please contact your administrator.",
|
|
5
5
|
"auth.accessDenied.title": "Access Denied",
|
|
6
6
|
"auth.acl.allowAllOrganizations": "Allow all organizations",
|
|
7
|
+
"auth.acl.deps.missing.add": "Add \"{dep}\"",
|
|
8
|
+
"auth.acl.deps.missing.item": "\"{feature}\" needs:",
|
|
9
|
+
"auth.acl.deps.missing.title": "Some granted permissions need other permissions to work:",
|
|
10
|
+
"auth.acl.deps.orphaned.drop": "Drop dependents",
|
|
11
|
+
"auth.acl.deps.orphaned.item": "\"{dependency}\" is required by:",
|
|
12
|
+
"auth.acl.deps.orphaned.restore": "Restore \"{dependency}\"",
|
|
13
|
+
"auth.acl.deps.orphaned.title": "Removing a permission that other granted permissions need:",
|
|
14
|
+
"auth.acl.deps.unknown.title": "Some declared dependencies do not match any known permission:",
|
|
7
15
|
"auth.acl.organizationWarning": "Organization restrictions are saved only when at least one feature override is selected. Add a feature or enable a module wildcard before saving.",
|
|
8
16
|
"auth.acl.organizationsScope": "Organizations scope",
|
|
9
17
|
"auth.acl.restricted": "Restricted",
|
|
@@ -164,6 +172,7 @@
|
|
|
164
172
|
"auth.users.form.action.resendInvite": "Resend Invite",
|
|
165
173
|
"auth.users.form.action.resendingInvite": "Sending...",
|
|
166
174
|
"auth.users.form.action.save": "Save",
|
|
175
|
+
"auth.users.form.actions.backToList": "Back to users",
|
|
167
176
|
"auth.users.form.errors.aclUpdate": "Failed to update user access control",
|
|
168
177
|
"auth.users.form.errors.delete": "Failed to delete user",
|
|
169
178
|
"auth.users.form.errors.inviteResend": "Failed to send invitation email",
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
"auth.accessDenied.message": "No tienes permiso para ver esta página. Por favor, contacta con tu administrador.",
|
|
5
5
|
"auth.accessDenied.title": "Acceso denegado",
|
|
6
6
|
"auth.acl.allowAllOrganizations": "Permitir todas las organizaciones",
|
|
7
|
+
"auth.acl.deps.missing.add": "Agregar \"{dep}\"",
|
|
8
|
+
"auth.acl.deps.missing.item": "\"{feature}\" necesita:",
|
|
9
|
+
"auth.acl.deps.missing.title": "Algunos permisos otorgados necesitan otros permisos para funcionar:",
|
|
10
|
+
"auth.acl.deps.orphaned.drop": "Eliminar dependientes",
|
|
11
|
+
"auth.acl.deps.orphaned.item": "\"{dependency}\" es requerido por:",
|
|
12
|
+
"auth.acl.deps.orphaned.restore": "Restaurar \"{dependency}\"",
|
|
13
|
+
"auth.acl.deps.orphaned.title": "Eliminando un permiso que otros permisos otorgados necesitan:",
|
|
14
|
+
"auth.acl.deps.unknown.title": "Algunas dependencias declaradas no coinciden con ningún permiso conocido:",
|
|
7
15
|
"auth.acl.organizationWarning": "Las restricciones de organización se guardan solo cuando se selecciona al menos una anulación de función. Agrega una función o habilita un comodín de módulo antes de guardar.",
|
|
8
16
|
"auth.acl.organizationsScope": "Alcance de organizaciones",
|
|
9
17
|
"auth.acl.restricted": "Restringido",
|
|
@@ -164,6 +172,7 @@
|
|
|
164
172
|
"auth.users.form.action.resendInvite": "Reenviar invitación",
|
|
165
173
|
"auth.users.form.action.resendingInvite": "Enviando...",
|
|
166
174
|
"auth.users.form.action.save": "Guardar",
|
|
175
|
+
"auth.users.form.actions.backToList": "Volver a usuarios",
|
|
167
176
|
"auth.users.form.errors.aclUpdate": "No se pudo actualizar el control de acceso del usuario",
|
|
168
177
|
"auth.users.form.errors.delete": "No se pudo eliminar el usuario",
|
|
169
178
|
"auth.users.form.errors.inviteResend": "No se pudo enviar el correo de invitación",
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
"auth.accessDenied.message": "Nie masz uprawnień do wyświetlenia tej strony. Skontaktuj się z administratorem.",
|
|
5
5
|
"auth.accessDenied.title": "Brak dostępu",
|
|
6
6
|
"auth.acl.allowAllOrganizations": "Zezwól na wszystkie organizacje",
|
|
7
|
+
"auth.acl.deps.missing.add": "Dodaj \"{dep}\"",
|
|
8
|
+
"auth.acl.deps.missing.item": "\"{feature}\" wymaga:",
|
|
9
|
+
"auth.acl.deps.missing.title": "Niektóre przyznane uprawnienia wymagają innych uprawnień do działania:",
|
|
10
|
+
"auth.acl.deps.orphaned.drop": "Usuń zależne",
|
|
11
|
+
"auth.acl.deps.orphaned.item": "\"{dependency}\" jest wymagane przez:",
|
|
12
|
+
"auth.acl.deps.orphaned.restore": "Przywróć \"{dependency}\"",
|
|
13
|
+
"auth.acl.deps.orphaned.title": "Usuwasz uprawnienie wymagane przez inne przyznane uprawnienia:",
|
|
14
|
+
"auth.acl.deps.unknown.title": "Niektóre zadeklarowane zależności nie pasują do żadnego znanego uprawnienia:",
|
|
7
15
|
"auth.acl.organizationWarning": "Ograniczenia organizacyjne są zapisywane tylko wtedy, gdy wybrano co najmniej jedno nadpisanie uprawnień. Dodaj uprawnienie lub włącz wildcard modułu przed zapisaniem.",
|
|
8
16
|
"auth.acl.organizationsScope": "Zakres organizacji",
|
|
9
17
|
"auth.acl.restricted": "Ograniczone",
|
|
@@ -164,6 +172,7 @@
|
|
|
164
172
|
"auth.users.form.action.resendInvite": "Wyślij zaproszenie ponownie",
|
|
165
173
|
"auth.users.form.action.resendingInvite": "Wysyłanie...",
|
|
166
174
|
"auth.users.form.action.save": "Zapisz",
|
|
175
|
+
"auth.users.form.actions.backToList": "Powrót do listy użytkowników",
|
|
167
176
|
"auth.users.form.errors.aclUpdate": "Nie udało się zaktualizować uprawnień użytkownika",
|
|
168
177
|
"auth.users.form.errors.delete": "Nie udało się usunąć użytkownika",
|
|
169
178
|
"auth.users.form.errors.inviteResend": "Nie udało się wysłać e-maila z zaproszeniem",
|
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
export const features = [
|
|
2
|
-
{
|
|
3
|
-
|
|
2
|
+
{
|
|
3
|
+
id: 'catalog.products.view',
|
|
4
|
+
title: 'View catalog products',
|
|
5
|
+
module: 'catalog',
|
|
6
|
+
dependsOn: ['currencies.view', 'dictionaries.view'],
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: 'catalog.products.manage',
|
|
10
|
+
title: 'Manage catalog products',
|
|
11
|
+
module: 'catalog',
|
|
12
|
+
dependsOn: ['catalog.products.view'],
|
|
13
|
+
},
|
|
4
14
|
{ id: 'catalog.categories.view', title: 'View catalog categories', module: 'catalog' },
|
|
5
|
-
{
|
|
6
|
-
|
|
7
|
-
|
|
15
|
+
{
|
|
16
|
+
id: 'catalog.categories.manage',
|
|
17
|
+
title: 'Manage catalog categories',
|
|
18
|
+
module: 'catalog',
|
|
19
|
+
dependsOn: ['catalog.categories.view'],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'catalog.variants.manage',
|
|
23
|
+
title: 'Manage catalog variants',
|
|
24
|
+
module: 'catalog',
|
|
25
|
+
dependsOn: ['catalog.products.view'],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'catalog.pricing.manage',
|
|
29
|
+
title: 'Manage catalog pricing',
|
|
30
|
+
module: 'catalog',
|
|
31
|
+
dependsOn: ['catalog.products.view', 'currencies.view'],
|
|
32
|
+
},
|
|
8
33
|
{ id: 'catalog.settings.manage', title: 'Manage catalog settings', module: 'catalog' },
|
|
9
34
|
]
|
|
10
35
|
|
|
@@ -4,7 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import dynamic from "next/dynamic";
|
|
6
6
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
7
|
-
import { ErrorMessage } from "@open-mercato/ui/backend/detail";
|
|
7
|
+
import { ErrorMessage, RecordNotFoundState } from "@open-mercato/ui/backend/detail";
|
|
8
8
|
import {
|
|
9
9
|
CrudForm,
|
|
10
10
|
type CrudFormGroup,
|
|
@@ -327,6 +327,7 @@ export default function EditCatalogProductPage({
|
|
|
327
327
|
React.useState<Partial<ProductFormValues> | null>(null);
|
|
328
328
|
const [loading, setLoading] = React.useState(true);
|
|
329
329
|
const [error, setError] = React.useState<string | null>(null);
|
|
330
|
+
const [isNotFound, setIsNotFound] = React.useState(false);
|
|
330
331
|
const offerSnapshotsRef = React.useRef<OfferSnapshot[]>([]);
|
|
331
332
|
const initialConversionsRef = React.useRef<ProductUnitConversionDraft[]>([]);
|
|
332
333
|
const [categorizeOptions, setCategorizeOptions] = React.useState<{
|
|
@@ -552,6 +553,7 @@ export default function EditCatalogProductPage({
|
|
|
552
553
|
async function loadProduct() {
|
|
553
554
|
setLoading(true);
|
|
554
555
|
setError(null);
|
|
556
|
+
setIsNotFound(false);
|
|
555
557
|
try {
|
|
556
558
|
const productRes = await apiCall<ProductResponse>(
|
|
557
559
|
`/api/catalog/products?id=${encodeURIComponent(productId!)}&page=1&pageSize=1&withDeleted=false`,
|
|
@@ -567,10 +569,10 @@ export default function EditCatalogProductPage({
|
|
|
567
569
|
const record = Array.isArray(productRes.result?.items)
|
|
568
570
|
? productRes.result?.items?.[0]
|
|
569
571
|
: undefined;
|
|
570
|
-
if (!record)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
572
|
+
if (!record) {
|
|
573
|
+
if (!cancelled) setIsNotFound(true);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
574
576
|
const rawMetadata = isRecord(record.metadata)
|
|
575
577
|
? (record.metadata as Record<string, unknown>)
|
|
576
578
|
: null;
|
|
@@ -1341,6 +1343,20 @@ export default function EditCatalogProductPage({
|
|
|
1341
1343
|
);
|
|
1342
1344
|
}
|
|
1343
1345
|
|
|
1346
|
+
if (isNotFound && !loading) {
|
|
1347
|
+
return (
|
|
1348
|
+
<Page>
|
|
1349
|
+
<PageBody>
|
|
1350
|
+
<RecordNotFoundState
|
|
1351
|
+
label={t("catalog.products.edit.errors.notFound", "Product not found.")}
|
|
1352
|
+
backHref="/backend/catalog/products"
|
|
1353
|
+
backLabel={t("catalog.products.edit.actions.backToList", "Back to products")}
|
|
1354
|
+
/>
|
|
1355
|
+
</PageBody>
|
|
1356
|
+
</Page>
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1344
1360
|
if (error && !loading) {
|
|
1345
1361
|
return (
|
|
1346
1362
|
<Page>
|