@open-mercato/core 0.7.1-develop.7105.1.0d2f4c6651 → 0.7.1-develop.7111.1.c0d5d7286f
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/users/acl/route.js +2 -2
- package/dist/modules/auth/api/users/acl/route.js.map +1 -1
- package/dist/modules/business_rules/backend/logs/[id]/page.js +3 -9
- package/dist/modules/business_rules/backend/logs/[id]/page.js.map +2 -2
- package/dist/modules/business_rules/backend/rules/[id]/page.js +3 -9
- package/dist/modules/business_rules/backend/rules/[id]/page.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/[id]/page.js +3 -9
- package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js +6 -4
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +4 -12
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/authorUserLookup.js +46 -0
- package/dist/modules/customers/components/detail/authorUserLookup.js.map +7 -0
- package/dist/modules/query_index/lib/engine.js +2 -1
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +3 -9
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/events/[id]/page.js +3 -9
- package/dist/modules/workflows/backend/events/[id]/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/users/acl/route.ts +2 -2
- package/src/modules/auth/i18n/de.json +1 -1
- package/src/modules/auth/i18n/en.json +1 -1
- package/src/modules/auth/i18n/es.json +1 -1
- package/src/modules/auth/i18n/ko.json +1 -1
- package/src/modules/auth/i18n/pl.json +1 -1
- package/src/modules/business_rules/backend/logs/[id]/page.tsx +3 -11
- package/src/modules/business_rules/backend/rules/[id]/page.tsx +3 -11
- package/src/modules/business_rules/backend/sets/[id]/page.tsx +3 -11
- package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.tsx +7 -5
- package/src/modules/communication_channels/i18n/de.json +1 -0
- package/src/modules/communication_channels/i18n/en.json +1 -0
- package/src/modules/communication_channels/i18n/es.json +1 -0
- package/src/modules/communication_channels/i18n/ko.json +1 -0
- package/src/modules/communication_channels/i18n/pl.json +1 -0
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +1 -1
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +8 -18
- package/src/modules/customers/components/detail/authorUserLookup.ts +85 -0
- package/src/modules/query_index/lib/engine.ts +9 -0
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +3 -11
- package/src/modules/workflows/backend/events/[id]/page.tsx +3 -12
package/.turbo/turbo-build.log
CHANGED
|
@@ -245,7 +245,7 @@ async function PUT(req) {
|
|
|
245
245
|
return NextResponse.json({
|
|
246
246
|
error: translate(
|
|
247
247
|
"auth.acl.organizationWarning",
|
|
248
|
-
"Organization restrictions
|
|
248
|
+
"Organization restrictions require at least one feature override. Add a feature or module wildcard, or clear the organization scope before saving."
|
|
249
249
|
)
|
|
250
250
|
}, { status: 400 });
|
|
251
251
|
}
|
|
@@ -328,7 +328,7 @@ const openApi = {
|
|
|
328
328
|
},
|
|
329
329
|
PUT: {
|
|
330
330
|
summary: "Update user ACL",
|
|
331
|
-
description: "Updates a per-user ACL override. Omitted super admin, feature, and organization fields preserve their stored values. An organization-scoped non-super-admin override requires at least one feature grant.",
|
|
331
|
+
description: "Updates a per-user ACL override. Omitted super admin, feature, and organization fields preserve their stored values. Authorization evaluates the merged ACL, so a partial request returns 403 when a preserved grant is outside the actor's grantable ACL. An organization-scoped non-super-admin override requires at least one feature grant.",
|
|
332
332
|
requestBody: {
|
|
333
333
|
contentType: "application/json",
|
|
334
334
|
schema: putSchema
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/auth/api/users/acl/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'\nimport { forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { User, UserAcl } from '@open-mercato/core/modules/auth/data/entities'\nimport {\n assertActorCanAccessUserTarget,\n assertActorCanGrantAcl,\n assertActorCanModifySuperAdminUserTarget,\n normalizeGrantFeatureList,\n} from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n AUTH_USER_ACL_UPDATE_COMMAND_ID,\n type AclUpdateResult,\n type UserAclUpdateInput,\n} from '@open-mercato/core/modules/auth/commands/acl'\n\nconst getSchema = z.object({\n userId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n})\nconst putSchema = z.object({\n userId: z.string().uuid(),\n isSuperAdmin: z.boolean().optional(),\n features: z.array(z.string()).optional(),\n organizations: z.array(z.string()).nullable().optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n}\n\nconst userAclResponseSchema = z.object({\n hasCustomAcl: z.boolean(),\n isSuperAdmin: z.boolean(),\n features: z.array(z.string()),\n organizations: z.array(z.string()).nullable(),\n updatedAt: z.string().nullable(),\n})\n\nconst userAclUpdateResponseSchema = z.object({\n ok: z.literal(true),\n sanitized: z.boolean(),\n})\n\nconst userAclErrorSchema = z.object({ error: z.string() })\n\ntype TenantResolution = { tenantId: string | null } | { error: NextResponse }\n\n/**\n * The single tenant scope both handlers work in. GET and PUT MUST resolve it the\n * same way: the admin user form PUTs the ACL panel's state on every save, so a\n * GET that reads a narrower scope than the PUT writes hands the operator an\n * empty form and turns the next name-only edit into a silent revocation, with no\n * `updatedAt` to arm the optimistic lock against it.\n *\n * `user_acls.tenant_id` is NOT NULL while `users.tenant_id` is nullable, so a\n * global account legitimately signs in with `auth.tenantId === null` and the\n * scope has to come from the target user instead. That fallback is reserved for\n * a super admin: for anyone else it would let the target pick the scope its own\n * access is then checked against, and it decrypts a possibly foreign user ahead\n * of every guard. A tenant-less non-super-admin therefore resolves to `null`,\n * which reads as \"no override\" and refuses to write \u2014 the behaviour that held\n * before this route resolved a scope at all. The read is also skipped whenever a\n * tenant is already known.\n *\n * An explicit `tenantId` wins, mirroring the role ACL route (additive there and\n * here; no caller sends it yet) \u2014 but only for a super admin or for the actor's\n * own tenant, so it cannot widen anyone's reach. The result is that for a\n * non-super-admin the resolved scope is always the actor's own.\n */\nasync function resolveAclTenantId(args: {\n em: EntityManager\n auth: NonNullable<AuthContext>\n actorIsSuperAdmin: boolean\n userId: string\n requestedTenantId?: string\n}): Promise<TenantResolution> {\n const authTenantId = args.auth.tenantId ?? null\n if (args.requestedTenantId && args.requestedTenantId !== authTenantId && !args.actorIsSuperAdmin) {\n return { error: NextResponse.json({ error: 'Forbidden' }, { status: 403 }) }\n }\n const known = args.requestedTenantId ?? authTenantId\n if (known) return { tenantId: known }\n if (!args.actorIsSuperAdmin) return { tenantId: null }\n const targetUser = await findOneWithDecryption(\n args.em,\n User,\n { id: args.userId } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: args.auth.orgId ?? null },\n )\n return { tenantId: targetUser?.tenantId ? String(targetUser.tenantId) : null }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsed = getSchema.safeParse({\n userId: url.searchParams.get('userId'),\n tenantId: url.searchParams.get('tenantId') || undefined,\n })\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbacService = container.resolve('rbacService') as any\n // Every grant check is answered in the actor's own scope, never in the scope\n // resolved for the record: passing the resolved one would ask a foreign tenant\n // whether this caller may act there, and for a tenant-less actor \u2014 whose scope\n // comes from the target \u2014 it would compare the target against itself.\n const actorTenantId = auth.tenantId ?? null\n const actorAcl = auth.sub\n ? await rbacService.loadAcl(auth.sub, { tenantId: actorTenantId, organizationId: auth.orgId ?? null })\n : null\n const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin\n\n const resolution = await resolveAclTenantId({\n em: em as EntityManager,\n auth,\n actorIsSuperAdmin,\n userId: parsed.data.userId,\n requestedTenantId: parsed.data.tenantId,\n })\n if ('error' in resolution) return resolution.error\n // An unresolvable scope reads as \"no override\", which is what PUT then refuses\n // to write (`Tenant required`) \u2014 so the pair still cannot destroy a row.\n const tenantId = resolution.tenantId\n\n if (!actorIsSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: actorTenantId,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n })\n await assertActorCanAccessUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: actorTenantId,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n organizationScope: await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: actorTenantId,\n }),\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const acl = tenantId\n ? await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId })\n : null\n const response = acl\n ? {\n hasCustomAcl: true,\n isSuperAdmin: !!acl.isSuperAdmin,\n features: Array.isArray(acl.featuresJson) ? acl.featuresJson : [],\n organizations: Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null,\n updatedAt: acl.updatedAt instanceof Date ? acl.updatedAt.toISOString() : null,\n }\n : { hasCustomAcl: false, isSuperAdmin: false, features: [], organizations: null, updatedAt: null }\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items: [{ id: parsed.data.userId, ...response }],\n idField: 'id',\n resourceKind: 'auth.user_acl',\n organizationId: auth.orgId ?? null,\n tenantId,\n query: { userId: parsed.data.userId, tenantId },\n accessType: 'read:item',\n })\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const body = await req.json().catch(() => ({}))\n const parsed = putSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbacService = container.resolve('rbacService') as any\n\n // Every grant check is answered in the actor's own scope, never in the scope\n // resolved for the record: passing the resolved one would ask a foreign tenant\n // whether this caller may act there, and for a tenant-less actor \u2014 whose scope\n // comes from the target \u2014 it would compare the target against itself.\n const actorTenantId = auth.tenantId ?? null\n const actorAcl = auth.sub\n ? await rbacService.loadAcl(auth.sub, { tenantId: actorTenantId, organizationId: auth.orgId ?? null })\n : null\n const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin\n\n // The scope the row is read and written in. `auth.tenantId` is `string | null`\n // \u2014 never `undefined`, since every producer normalizes with `?? null` \u2014 so the\n // pre-fix lookup ran `tenant_id IS NULL` against a NOT NULL column: it matched\n // no row, leaving create/update to fail the NOT NULL constraint and clear to\n // no-op silently. Nothing cross-tenant was ever matched; the fix turns a 500\n // into a working, correctly scoped write.\n const resolution = await resolveAclTenantId({\n em: em as EntityManager,\n auth,\n actorIsSuperAdmin,\n userId: parsed.data.userId,\n requestedTenantId: parsed.data.tenantId,\n })\n if ('error' in resolution) return resolution.error\n const tenantId = resolution.tenantId\n if (!tenantId) return NextResponse.json({ error: 'Tenant required' }, { status: 400 })\n\n if (!actorIsSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: actorTenantId,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n })\n await assertActorCanAccessUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: actorTenantId,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n organizationScope: await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: actorTenantId,\n }),\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId })\n // Optimistic lock: refuse a stale per-user ACL overwrite so concurrent edits\n // cannot silently clobber each other (#2055). Strictly additive \u2014 a no-op when\n // the client sends no expected-version header; skipped when no ACL row exists.\n if (acl) {\n try {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: 'auth.user_acl',\n resourceId: acl.id,\n current: acl.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const existingIsSuperAdmin = acl ? !!acl.isSuperAdmin : false\n const existingFeatures = acl ? normalizeGrantFeatureList(acl.featuresJson) : []\n const existingOrganizations = acl ? normalizeOrganizations(acl.organizationsJson) : null\n\n // A per-user ACL is an absolute override, so an omitted dimension must keep\n // its stored value. Normalizing an omitted `features` to `[]` or an omitted\n // `organizations` to `null` turned a single-dimension edit into a silent\n // clear, deleting the row and widening the user back to their full role.\n const featuresWereProvided = parsed.data.features !== undefined\n const requestedFeatures = featuresWereProvided\n ? normalizeGrantFeatureList(parsed.data.features)\n : existingFeatures\n const requestedOrganizations = parsed.data.organizations === undefined\n ? existingOrganizations\n : normalizeOrganizations(parsed.data.organizations)\n\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? existingIsSuperAdmin\n\n try {\n await assertActorCanGrantAcl({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: actorTenantId,\n organizationId: auth.orgId ?? null,\n isSuperAdmin: requestedIsSuperAdmin,\n features: requestedFeatures,\n organizations: requestedOrganizations,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n // An omitted feature list is already stored and in effect. Re-sanitizing it\n // during an unrelated organization edit would silently revoke grants the\n // actor did not touch, so only an explicitly submitted list is sanitized.\n const effectiveFeatures = actorIsSuperAdmin || !featuresWereProvided\n ? requestedFeatures\n : sanitizeTenantFeatures(requestedFeatures)\n\n let effectiveIsSuperAdmin = requestedIsSuperAdmin\n\n if (!actorIsSuperAdmin) {\n if (requestedIsSuperAdmin && !existingIsSuperAdmin) {\n throw forbidden('Only super administrators can grant super admin access.')\n }\n if (existingIsSuperAdmin && requestedIsSuperAdmin === false) {\n effectiveIsSuperAdmin = false\n } else {\n effectiveIsSuperAdmin = existingIsSuperAdmin\n }\n }\n\n // Retaining an organization-only override with no features would revoke every\n // role-granted feature instead of narrowing the role. Refuse that state rather\n // than persisting it or silently dropping the organization scope.\n if (!effectiveIsSuperAdmin && effectiveFeatures.length === 0 && hasOrganizationRestriction(requestedOrganizations)) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({\n error: translate(\n 'auth.acl.organizationWarning',\n 'Organization restrictions are saved only when at least one feature override is selected. Add a feature or enable a module wildcard before saving.',\n ),\n }, { status: 400 })\n }\n\n // An unrestricted organization list carries no override on its own, and the guard\n // above already refused the restricted-but-featureless case, so the override is\n // custom exactly when it grants super admin or at least one feature.\n const hasCustomAcl = effectiveIsSuperAdmin || effectiveFeatures.length > 0\n\n // What the caller asked for, handed to the command only when it exceeds what\n // is about to be written. `assertActorCanGrantAcl` above refuses the blatant\n // escalations; what reaches the strip is quieter \u2014 a restricted grant the\n // actor does hold \u2014 and it leaves before/after identical, so the audit entry\n // would otherwise be indistinguishable from a no-op and skipped as one.\n //\n // Features are the only axis that can be trimmed. A super-admin request is\n // either honoured or refused outright above, never silently downgraded, so\n // comparing the two flags here would be dead weight.\n //\n // Deliberately independent of the `sanitized` response flag below:\n // `hasRestrictedChanges` stays false when the trimmed result equals the\n // existing ACL, to avoid nagging the user about a save that changed nothing \u2014\n // but that is precisely an attempt the trail must keep.\n const strippedFeatures = requestedFeatures.filter((feature) => !effectiveFeatures.includes(feature))\n\n // Route the write through the command bus so the permission change lands in\n // the action log. The command owns the transactional write (or removal) and\n // the RBAC cache invalidation that used to live here.\n const commandBus = container.resolve('commandBus') as CommandBus\n // A tenant-less actor edits an override in the target user's tenant, so their\n // organization belongs to a tenant the entry is not about. The bus resolves\n // the log row's organization as `metadata.organizationId ??\n // ctx.selectedOrganizationId ?? ctx.auth.orgId` \u2014 a `??` chain, so the\n // handler's `resolveOrganizationId` cannot express \"explicitly no\n // organization\" and the actor's would be restored. Strip it from the context\n // instead, exactly as the roles route does: otherwise the entry carries a\n // (target tenant, foreign organization) pair that `ActionLogService` \u2014 which\n // filters organization with strict equality \u2014 can never surface for anyone.\n const isForeignTenant = tenantId !== (auth.tenantId ?? null)\n const actorOrgId = isForeignTenant ? null : auth.orgId ?? null\n const commandCtx: CommandRuntimeContext = {\n container,\n auth: isForeignTenant ? { ...auth, orgId: null } : auth,\n organizationScope: null,\n selectedOrganizationId: actorOrgId,\n organizationIds: actorOrgId ? [actorOrgId] : null,\n request: req,\n }\n await commandBus.execute<UserAclUpdateInput, AclUpdateResult>(AUTH_USER_ACL_UPDATE_COMMAND_ID, {\n input: {\n userId: parsed.data.userId,\n tenantId,\n isSuperAdmin: effectiveIsSuperAdmin,\n features: effectiveFeatures,\n organizations: requestedOrganizations,\n clear: !hasCustomAcl,\n requested: strippedFeatures.length ? { features: requestedFeatures } : null,\n },\n ctx: commandCtx,\n })\n\n return NextResponse.json({\n ok: true,\n sanitized: !actorIsSuperAdmin && (hasRestrictedChanges(requestedFeatures, effectiveFeatures, existingFeatures) || requestedIsSuperAdmin !== effectiveIsSuperAdmin),\n })\n}\n\nfunction normalizeOrganizations(organizations: unknown): string[] | null {\n if (!Array.isArray(organizations)) return null\n return normalizeGrantFeatureList(organizations)\n}\n\n// Whether the caller expressed an intentional narrowing. `null` and `__all__`\n// are the two documented ways to say \"every organization\"; an empty list is the\n// editor's \"no organization picked\" state (\"Empty = all organizations\"), which\n// is not a restriction an administrator chose. Only a concrete list narrows, so\n// only a concrete list has to justify itself against the feature grant below.\nfunction hasOrganizationRestriction(organizations: string[] | null): boolean {\n if (!organizations || organizations.length === 0) return false\n return !organizations.includes('__all__')\n}\n\nfunction sanitizeTenantFeatures(features: string[]): string[] {\n return features.filter((feature) => !isTenantRestrictedFeature(feature))\n}\n\nfunction isTenantRestrictedFeature(feature: string): boolean {\n if (feature === '*' || feature === 'directory.*') return true\n if (feature.startsWith('directory.tenants')) return true\n return false\n}\n\nfunction hasRestrictedChanges(requested: string[], effective: string[], existing: string[]): boolean {\n if (requested.length === effective.length) return false\n const effectiveSet = new Set(effective)\n const existingSet = new Set(existing)\n // If the effective set matches existing, we only trimmed restricted duplicates and should not report\n if (effectiveSet.size === existingSet.size) {\n let identical = true\n for (const value of effectiveSet) {\n if (!existingSet.has(value)) {\n identical = false\n break\n }\n }\n if (identical) return false\n }\n return true\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'User ACL management',\n methods: {\n GET: {\n summary: 'Fetch user ACL',\n description: 'Returns custom ACL overrides for a user, scoped to the requested tenant when supplied and to the actor or target user tenant otherwise.',\n query: getSchema,\n responses: [\n { status: 200, description: 'User ACL entry', schema: userAclResponseSchema },\n { status: 400, description: 'Invalid user id', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n { status: 403, description: 'Insufficient privileges for the requested tenant scope', schema: userAclErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update user ACL',\n description: 'Updates a per-user ACL override. Omitted super admin, feature, and organization fields preserve their stored values. An organization-scoped non-super-admin override requires at least one feature grant.',\n requestBody: {\n contentType: 'application/json',\n schema: putSchema,\n },\n responses: [\n { status: 200, description: 'User ACL updated', schema: userAclUpdateResponseSchema },\n { status: 400, description: 'Invalid payload or unresolved tenant scope', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n { status: 403, description: 'Insufficient privileges to modify ACL', schema: userAclErrorSchema },\n ],\n },\n },\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'\nimport { forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { User, UserAcl } from '@open-mercato/core/modules/auth/data/entities'\nimport {\n assertActorCanAccessUserTarget,\n assertActorCanGrantAcl,\n assertActorCanModifySuperAdminUserTarget,\n normalizeGrantFeatureList,\n} from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n AUTH_USER_ACL_UPDATE_COMMAND_ID,\n type AclUpdateResult,\n type UserAclUpdateInput,\n} from '@open-mercato/core/modules/auth/commands/acl'\n\nconst getSchema = z.object({\n userId: z.string().uuid(),\n tenantId: z.string().uuid().optional(),\n})\nconst putSchema = z.object({\n userId: z.string().uuid(),\n isSuperAdmin: z.boolean().optional(),\n features: z.array(z.string()).optional(),\n organizations: z.array(z.string()).nullable().optional(),\n tenantId: z.string().uuid().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n}\n\nconst userAclResponseSchema = z.object({\n hasCustomAcl: z.boolean(),\n isSuperAdmin: z.boolean(),\n features: z.array(z.string()),\n organizations: z.array(z.string()).nullable(),\n updatedAt: z.string().nullable(),\n})\n\nconst userAclUpdateResponseSchema = z.object({\n ok: z.literal(true),\n sanitized: z.boolean(),\n})\n\nconst userAclErrorSchema = z.object({ error: z.string() })\n\ntype TenantResolution = { tenantId: string | null } | { error: NextResponse }\n\n/**\n * The single tenant scope both handlers work in. GET and PUT MUST resolve it the\n * same way: the admin user form PUTs the ACL panel's state on every save, so a\n * GET that reads a narrower scope than the PUT writes hands the operator an\n * empty form and turns the next name-only edit into a silent revocation, with no\n * `updatedAt` to arm the optimistic lock against it.\n *\n * `user_acls.tenant_id` is NOT NULL while `users.tenant_id` is nullable, so a\n * global account legitimately signs in with `auth.tenantId === null` and the\n * scope has to come from the target user instead. That fallback is reserved for\n * a super admin: for anyone else it would let the target pick the scope its own\n * access is then checked against, and it decrypts a possibly foreign user ahead\n * of every guard. A tenant-less non-super-admin therefore resolves to `null`,\n * which reads as \"no override\" and refuses to write \u2014 the behaviour that held\n * before this route resolved a scope at all. The read is also skipped whenever a\n * tenant is already known.\n *\n * An explicit `tenantId` wins, mirroring the role ACL route (additive there and\n * here; no caller sends it yet) \u2014 but only for a super admin or for the actor's\n * own tenant, so it cannot widen anyone's reach. The result is that for a\n * non-super-admin the resolved scope is always the actor's own.\n */\nasync function resolveAclTenantId(args: {\n em: EntityManager\n auth: NonNullable<AuthContext>\n actorIsSuperAdmin: boolean\n userId: string\n requestedTenantId?: string\n}): Promise<TenantResolution> {\n const authTenantId = args.auth.tenantId ?? null\n if (args.requestedTenantId && args.requestedTenantId !== authTenantId && !args.actorIsSuperAdmin) {\n return { error: NextResponse.json({ error: 'Forbidden' }, { status: 403 }) }\n }\n const known = args.requestedTenantId ?? authTenantId\n if (known) return { tenantId: known }\n if (!args.actorIsSuperAdmin) return { tenantId: null }\n const targetUser = await findOneWithDecryption(\n args.em,\n User,\n { id: args.userId } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: args.auth.orgId ?? null },\n )\n return { tenantId: targetUser?.tenantId ? String(targetUser.tenantId) : null }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsed = getSchema.safeParse({\n userId: url.searchParams.get('userId'),\n tenantId: url.searchParams.get('tenantId') || undefined,\n })\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbacService = container.resolve('rbacService') as any\n // Every grant check is answered in the actor's own scope, never in the scope\n // resolved for the record: passing the resolved one would ask a foreign tenant\n // whether this caller may act there, and for a tenant-less actor \u2014 whose scope\n // comes from the target \u2014 it would compare the target against itself.\n const actorTenantId = auth.tenantId ?? null\n const actorAcl = auth.sub\n ? await rbacService.loadAcl(auth.sub, { tenantId: actorTenantId, organizationId: auth.orgId ?? null })\n : null\n const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin\n\n const resolution = await resolveAclTenantId({\n em: em as EntityManager,\n auth,\n actorIsSuperAdmin,\n userId: parsed.data.userId,\n requestedTenantId: parsed.data.tenantId,\n })\n if ('error' in resolution) return resolution.error\n // An unresolvable scope reads as \"no override\", which is what PUT then refuses\n // to write (`Tenant required`) \u2014 so the pair still cannot destroy a row.\n const tenantId = resolution.tenantId\n\n if (!actorIsSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: actorTenantId,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n })\n await assertActorCanAccessUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: actorTenantId,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n organizationScope: await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: actorTenantId,\n }),\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const acl = tenantId\n ? await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId })\n : null\n const response = acl\n ? {\n hasCustomAcl: true,\n isSuperAdmin: !!acl.isSuperAdmin,\n features: Array.isArray(acl.featuresJson) ? acl.featuresJson : [],\n organizations: Array.isArray(acl.organizationsJson) ? acl.organizationsJson : null,\n updatedAt: acl.updatedAt instanceof Date ? acl.updatedAt.toISOString() : null,\n }\n : { hasCustomAcl: false, isSuperAdmin: false, features: [], organizations: null, updatedAt: null }\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items: [{ id: parsed.data.userId, ...response }],\n idField: 'id',\n resourceKind: 'auth.user_acl',\n organizationId: auth.orgId ?? null,\n tenantId,\n query: { userId: parsed.data.userId, tenantId },\n accessType: 'read:item',\n })\n\n return NextResponse.json(response)\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const body = await req.json().catch(() => ({}))\n const parsed = putSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as any\n const rbacService = container.resolve('rbacService') as any\n\n // Every grant check is answered in the actor's own scope, never in the scope\n // resolved for the record: passing the resolved one would ask a foreign tenant\n // whether this caller may act there, and for a tenant-less actor \u2014 whose scope\n // comes from the target \u2014 it would compare the target against itself.\n const actorTenantId = auth.tenantId ?? null\n const actorAcl = auth.sub\n ? await rbacService.loadAcl(auth.sub, { tenantId: actorTenantId, organizationId: auth.orgId ?? null })\n : null\n const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin\n\n // The scope the row is read and written in. `auth.tenantId` is `string | null`\n // \u2014 never `undefined`, since every producer normalizes with `?? null` \u2014 so the\n // pre-fix lookup ran `tenant_id IS NULL` against a NOT NULL column: it matched\n // no row, leaving create/update to fail the NOT NULL constraint and clear to\n // no-op silently. Nothing cross-tenant was ever matched; the fix turns a 500\n // into a working, correctly scoped write.\n const resolution = await resolveAclTenantId({\n em: em as EntityManager,\n auth,\n actorIsSuperAdmin,\n userId: parsed.data.userId,\n requestedTenantId: parsed.data.tenantId,\n })\n if ('error' in resolution) return resolution.error\n const tenantId = resolution.tenantId\n if (!tenantId) return NextResponse.json({ error: 'Tenant required' }, { status: 400 })\n\n if (!actorIsSuperAdmin && auth.sub) {\n try {\n await assertActorCanModifySuperAdminUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: actorTenantId,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n })\n await assertActorCanAccessUserTarget({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: actorTenantId,\n organizationId: auth.orgId ?? null,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin: false,\n organizationScope: await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId: actorTenantId,\n }),\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId })\n // Optimistic lock: refuse a stale per-user ACL overwrite so concurrent edits\n // cannot silently clobber each other (#2055). Strictly additive \u2014 a no-op when\n // the client sends no expected-version header; skipped when no ACL row exists.\n if (acl) {\n try {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: 'auth.user_acl',\n resourceId: acl.id,\n current: acl.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n const existingIsSuperAdmin = acl ? !!acl.isSuperAdmin : false\n const existingFeatures = acl ? normalizeGrantFeatureList(acl.featuresJson) : []\n const existingOrganizations = acl ? normalizeOrganizations(acl.organizationsJson) : null\n\n // A per-user ACL is an absolute override, so an omitted dimension must keep\n // its stored value. Normalizing an omitted `features` to `[]` or an omitted\n // `organizations` to `null` turned a single-dimension edit into a silent\n // clear, deleting the row and widening the user back to their full role.\n const featuresWereProvided = parsed.data.features !== undefined\n const requestedFeatures = featuresWereProvided\n ? normalizeGrantFeatureList(parsed.data.features)\n : existingFeatures\n const requestedOrganizations = parsed.data.organizations === undefined\n ? existingOrganizations\n : normalizeOrganizations(parsed.data.organizations)\n\n const requestedIsSuperAdmin = parsed.data.isSuperAdmin ?? existingIsSuperAdmin\n\n try {\n await assertActorCanGrantAcl({\n em: em as EntityManager,\n rbacService: rbacService as RbacService,\n actorUserId: auth.sub,\n tenantId: actorTenantId,\n organizationId: auth.orgId ?? null,\n isSuperAdmin: requestedIsSuperAdmin,\n features: requestedFeatures,\n organizations: requestedOrganizations,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n // An omitted feature list is already stored and in effect. Re-sanitizing it\n // during an unrelated organization edit would silently revoke grants the\n // actor did not touch, so only an explicitly submitted list is sanitized.\n const effectiveFeatures = actorIsSuperAdmin || !featuresWereProvided\n ? requestedFeatures\n : sanitizeTenantFeatures(requestedFeatures)\n\n let effectiveIsSuperAdmin = requestedIsSuperAdmin\n\n if (!actorIsSuperAdmin) {\n if (requestedIsSuperAdmin && !existingIsSuperAdmin) {\n throw forbidden('Only super administrators can grant super admin access.')\n }\n if (existingIsSuperAdmin && requestedIsSuperAdmin === false) {\n effectiveIsSuperAdmin = false\n } else {\n effectiveIsSuperAdmin = existingIsSuperAdmin\n }\n }\n\n // Retaining an organization-only override with no features would revoke every\n // role-granted feature instead of narrowing the role. Refuse that state rather\n // than persisting it or silently dropping the organization scope.\n if (!effectiveIsSuperAdmin && effectiveFeatures.length === 0 && hasOrganizationRestriction(requestedOrganizations)) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({\n error: translate(\n 'auth.acl.organizationWarning',\n 'Organization restrictions require at least one feature override. Add a feature or module wildcard, or clear the organization scope before saving.',\n ),\n }, { status: 400 })\n }\n\n // An unrestricted organization list carries no override on its own, and the guard\n // above already refused the restricted-but-featureless case, so the override is\n // custom exactly when it grants super admin or at least one feature.\n const hasCustomAcl = effectiveIsSuperAdmin || effectiveFeatures.length > 0\n\n // What the caller asked for, handed to the command only when it exceeds what\n // is about to be written. `assertActorCanGrantAcl` above refuses the blatant\n // escalations; what reaches the strip is quieter \u2014 a restricted grant the\n // actor does hold \u2014 and it leaves before/after identical, so the audit entry\n // would otherwise be indistinguishable from a no-op and skipped as one.\n //\n // Features are the only axis that can be trimmed. A super-admin request is\n // either honoured or refused outright above, never silently downgraded, so\n // comparing the two flags here would be dead weight.\n //\n // Deliberately independent of the `sanitized` response flag below:\n // `hasRestrictedChanges` stays false when the trimmed result equals the\n // existing ACL, to avoid nagging the user about a save that changed nothing \u2014\n // but that is precisely an attempt the trail must keep.\n const strippedFeatures = requestedFeatures.filter((feature) => !effectiveFeatures.includes(feature))\n\n // Route the write through the command bus so the permission change lands in\n // the action log. The command owns the transactional write (or removal) and\n // the RBAC cache invalidation that used to live here.\n const commandBus = container.resolve('commandBus') as CommandBus\n // A tenant-less actor edits an override in the target user's tenant, so their\n // organization belongs to a tenant the entry is not about. The bus resolves\n // the log row's organization as `metadata.organizationId ??\n // ctx.selectedOrganizationId ?? ctx.auth.orgId` \u2014 a `??` chain, so the\n // handler's `resolveOrganizationId` cannot express \"explicitly no\n // organization\" and the actor's would be restored. Strip it from the context\n // instead, exactly as the roles route does: otherwise the entry carries a\n // (target tenant, foreign organization) pair that `ActionLogService` \u2014 which\n // filters organization with strict equality \u2014 can never surface for anyone.\n const isForeignTenant = tenantId !== (auth.tenantId ?? null)\n const actorOrgId = isForeignTenant ? null : auth.orgId ?? null\n const commandCtx: CommandRuntimeContext = {\n container,\n auth: isForeignTenant ? { ...auth, orgId: null } : auth,\n organizationScope: null,\n selectedOrganizationId: actorOrgId,\n organizationIds: actorOrgId ? [actorOrgId] : null,\n request: req,\n }\n await commandBus.execute<UserAclUpdateInput, AclUpdateResult>(AUTH_USER_ACL_UPDATE_COMMAND_ID, {\n input: {\n userId: parsed.data.userId,\n tenantId,\n isSuperAdmin: effectiveIsSuperAdmin,\n features: effectiveFeatures,\n organizations: requestedOrganizations,\n clear: !hasCustomAcl,\n requested: strippedFeatures.length ? { features: requestedFeatures } : null,\n },\n ctx: commandCtx,\n })\n\n return NextResponse.json({\n ok: true,\n sanitized: !actorIsSuperAdmin && (hasRestrictedChanges(requestedFeatures, effectiveFeatures, existingFeatures) || requestedIsSuperAdmin !== effectiveIsSuperAdmin),\n })\n}\n\nfunction normalizeOrganizations(organizations: unknown): string[] | null {\n if (!Array.isArray(organizations)) return null\n return normalizeGrantFeatureList(organizations)\n}\n\n// Whether the caller expressed an intentional narrowing. `null` and `__all__`\n// are the two documented ways to say \"every organization\"; an empty list is the\n// editor's \"no organization picked\" state (\"Empty = all organizations\"), which\n// is not a restriction an administrator chose. Only a concrete list narrows, so\n// only a concrete list has to justify itself against the feature grant below.\nfunction hasOrganizationRestriction(organizations: string[] | null): boolean {\n if (!organizations || organizations.length === 0) return false\n return !organizations.includes('__all__')\n}\n\nfunction sanitizeTenantFeatures(features: string[]): string[] {\n return features.filter((feature) => !isTenantRestrictedFeature(feature))\n}\n\nfunction isTenantRestrictedFeature(feature: string): boolean {\n if (feature === '*' || feature === 'directory.*') return true\n if (feature.startsWith('directory.tenants')) return true\n return false\n}\n\nfunction hasRestrictedChanges(requested: string[], effective: string[], existing: string[]): boolean {\n if (requested.length === effective.length) return false\n const effectiveSet = new Set(effective)\n const existingSet = new Set(existing)\n // If the effective set matches existing, we only trimmed restricted duplicates and should not report\n if (effectiveSet.size === existingSet.size) {\n let identical = true\n for (const value of effectiveSet) {\n if (!existingSet.has(value)) {\n identical = false\n break\n }\n }\n if (identical) return false\n }\n return true\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'User ACL management',\n methods: {\n GET: {\n summary: 'Fetch user ACL',\n description: 'Returns custom ACL overrides for a user, scoped to the requested tenant when supplied and to the actor or target user tenant otherwise.',\n query: getSchema,\n responses: [\n { status: 200, description: 'User ACL entry', schema: userAclResponseSchema },\n { status: 400, description: 'Invalid user id', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n { status: 403, description: 'Insufficient privileges for the requested tenant scope', schema: userAclErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update user ACL',\n description: 'Updates a per-user ACL override. Omitted super admin, feature, and organization fields preserve their stored values. Authorization evaluates the merged ACL, so a partial request returns 403 when a preserved grant is outside the actor\\'s grantable ACL. An organization-scoped non-super-admin override requires at least one feature grant.',\n requestBody: {\n contentType: 'application/json',\n schema: putSchema,\n },\n responses: [\n { status: 200, description: 'User ACL updated', schema: userAclUpdateResponseSchema },\n { status: 400, description: 'Invalid payload or unresolved tenant scope', schema: userAclErrorSchema },\n { status: 401, description: 'Unauthorized', schema: userAclErrorSchema },\n { status: 403, description: 'Insufficient privileges to modify ACL', schema: userAclErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import {
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
5
|
import { useQuery } from "@tanstack/react-query";
|
|
6
6
|
import { apiFetch } from "@open-mercato/ui/backend/utils/api";
|
|
7
7
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
@@ -10,15 +10,9 @@ import { Button } from "@open-mercato/ui/primitives/button";
|
|
|
10
10
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
11
11
|
import { JsonDisplay } from "@open-mercato/ui/backend/JsonDisplay";
|
|
12
12
|
import { RecordNotFoundState, ErrorMessage } from "@open-mercato/ui/backend/detail";
|
|
13
|
-
function ExecutionLogDetailPage() {
|
|
13
|
+
function ExecutionLogDetailPage({ params }) {
|
|
14
14
|
const router = useRouter();
|
|
15
|
-
const
|
|
16
|
-
let logId;
|
|
17
|
-
if (params?.slug && Array.isArray(params.slug)) {
|
|
18
|
-
logId = params.slug[1];
|
|
19
|
-
} else if (params?.id) {
|
|
20
|
-
logId = Array.isArray(params.id) ? params.id[0] : params.id;
|
|
21
|
-
}
|
|
15
|
+
const logId = params?.id;
|
|
22
16
|
const t = useT();
|
|
23
17
|
const { data: log, isLoading, error } = useQuery({
|
|
24
18
|
queryKey: ["business-rules", "logs", logId],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/business_rules/backend/logs/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useParams, useRouter } from 'next/navigation'\nimport { useQuery } from '@tanstack/react-query'\nimport { apiFetch } from '@open-mercato/ui/backend/utils/api'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'\nimport { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'\n\ntype RuleExecutionLog = {\n id: string\n ruleId: string\n rule?: {\n id: string\n ruleId: string\n ruleName: string\n ruleType: string\n } | null\n entityType: string\n entityId: string | null\n eventType: string | null\n executedAt: string\n executionTimeMs: number\n executionResult: 'SUCCESS' | 'FAILURE' | 'ERROR'\n resultValue: any | null\n errorMessage: string | null\n inputContext: any | null\n outputContext: any | null\n executedBy: string | null\n tenantId: string | null\n organizationId: string | null\n}\n\nexport default function ExecutionLogDetailPage() {\n const router = useRouter()\n const params = useParams()\n\n // Handle catch-all route: params.slug = ['logs', 'id']\n let logId: string | undefined\n if (params?.slug && Array.isArray(params.slug)) {\n logId = params.slug[1] // Second element is the ID\n } else if (params?.id) {\n logId = Array.isArray(params.id) ? params.id[0] : params.id\n }\n\n const t = useT()\n\n const { data: log, isLoading, error } = useQuery({\n queryKey: ['business-rules', 'logs', logId],\n queryFn: async () => {\n const response = await apiFetch(`/api/business_rules/logs/${logId}`)\n if (!response.ok) {\n const httpErr = new Error(\n response.status === 404\n ? t('business_rules.logs.errors.notFound', 'Execution log not found.')\n : t('business_rules.logs.errors.fetchFailed')\n ) as Error & { status: number }\n httpErr.status = response.status\n throw httpErr\n }\n const result = await response.json()\n return result as RuleExecutionLog\n },\n enabled: !!logId,\n })\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground\">\n <Spinner className=\"h-6 w-6\" />\n <span>{t('business_rules.logs.detail.loading')}</span>\n </div>\n </PageBody>\n </Page>\n )\n }\n\n const isNotFound = !isLoading && (error as (Error & { status?: number }) | null)?.status === 404\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('business_rules.logs.errors.notFound', 'Execution log not found.')}\n backHref=\"/backend/logs\"\n backLabel={t('business_rules.logs.backToList', 'Back to logs')}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !log) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={(error as Error | null)?.message ?? t('business_rules.logs.errors.loadFailed')}\n action={\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href=\"/backend/logs\">{t('business_rules.logs.backToList', 'Back to logs')}</Link>\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n const getResultBadgeClass = (result: string) => {\n switch (result) {\n case 'SUCCESS':\n return 'bg-green-100 text-green-800'\n case 'FAILURE':\n return 'bg-yellow-100 text-yellow-800'\n case 'ERROR':\n return 'bg-red-100 text-red-800'\n default:\n return 'bg-muted text-foreground'\n }\n }\n\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-bold text-foreground\">\n {t('business_rules.logs.detail.title')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {t('business_rules.logs.detail.logId')}: {log.id}\n </p>\n </div>\n <Button onClick={() => router.push('/backend/logs')} variant=\"outline\">\n {t('business_rules.logs.backToList')}\n </Button>\n </div>\n\n {/* Execution Summary */}\n <div className=\"rounded-lg border bg-card p-6\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {t('business_rules.logs.detail.summary')}\n </h2>\n <dl className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.executedAt')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">\n {new Date(log.executedAt).toLocaleString()}\n </dd>\n </div>\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.result')}\n </dt>\n <dd className=\"mt-1\">\n <span\n className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${getResultBadgeClass(\n log.executionResult\n )}`}\n >\n {t(`business_rules.logs.result.${log.executionResult.toLowerCase()}`)}\n </span>\n </dd>\n </div>\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.executionTime')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">{log.executionTimeMs}ms</dd>\n </div>\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.executedBy')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">\n {log.executedBy || t('common.unknown')}\n </dd>\n </div>\n </dl>\n </div>\n\n {/* Rule Information */}\n <div className=\"rounded-lg border bg-card p-6\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {t('business_rules.logs.detail.ruleInfo')}\n </h2>\n {log.rule ? (\n <dl className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.ruleName')}\n </dt>\n <dd className=\"mt-1\">\n <Link\n href={`/backend/rules/${log.rule.id}`}\n className=\"text-sm text-primary hover:underline\"\n >\n {log.rule.ruleName}\n </Link>\n </dd>\n </div>\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.ruleType')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">{log.rule.ruleType}</dd>\n </div>\n <div className=\"md:col-span-2\">\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.ruleId')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground font-mono\">{log.rule.ruleId}</dd>\n </div>\n </dl>\n ) : (\n <p className=\"text-sm text-muted-foreground\">{t('business_rules.logs.ruleDeleted')}</p>\n )}\n </div>\n\n {/* Entity Information */}\n <div className=\"rounded-lg border bg-card p-6\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {t('business_rules.logs.detail.entityInfo')}\n </h2>\n <dl className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.entityType')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">{log.entityType}</dd>\n </div>\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.eventType')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">\n {log.eventType || t('common.none')}\n </dd>\n </div>\n {log.entityId && (\n <div className=\"md:col-span-2\">\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.entityId')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground font-mono break-all\">\n {log.entityId}\n </dd>\n </div>\n )}\n </dl>\n </div>\n\n {/* Error Message (if present) */}\n {log.errorMessage && (\n <div className=\"rounded-lg border border-destructive bg-destructive/5 p-6\">\n <h2 className=\"text-lg font-semibold mb-4 text-destructive\">\n {t('business_rules.logs.detail.errorMessage')}\n </h2>\n <pre className=\"text-sm text-destructive whitespace-pre-wrap font-mono\">\n {log.errorMessage}\n </pre>\n </div>\n )}\n\n {/* Input Context */}\n {log.inputContext && (\n <JsonDisplay\n data={log.inputContext}\n title={t('business_rules.logs.detail.inputContext')}\n />\n )}\n\n {/* Output Context */}\n {log.outputContext && (\n <JsonDisplay\n data={log.outputContext}\n title={t('business_rules.logs.detail.outputContext')}\n />\n )}\n\n {/* Result Value */}\n {log.resultValue && (\n <JsonDisplay\n data={log.resultValue}\n title={t('business_rules.logs.detail.resultValue')}\n />\n )}\n </div>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useQuery } from '@tanstack/react-query'\nimport { apiFetch } from '@open-mercato/ui/backend/utils/api'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'\nimport { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'\n\ntype RuleExecutionLog = {\n id: string\n ruleId: string\n rule?: {\n id: string\n ruleId: string\n ruleName: string\n ruleType: string\n } | null\n entityType: string\n entityId: string | null\n eventType: string | null\n executedAt: string\n executionTimeMs: number\n executionResult: 'SUCCESS' | 'FAILURE' | 'ERROR'\n resultValue: any | null\n errorMessage: string | null\n inputContext: any | null\n outputContext: any | null\n executedBy: string | null\n tenantId: string | null\n organizationId: string | null\n}\n\nexport default function ExecutionLogDetailPage({ params }: { params?: { id?: string } }) {\n const router = useRouter()\n const logId = params?.id\n\n const t = useT()\n\n const { data: log, isLoading, error } = useQuery({\n queryKey: ['business-rules', 'logs', logId],\n queryFn: async () => {\n const response = await apiFetch(`/api/business_rules/logs/${logId}`)\n if (!response.ok) {\n const httpErr = new Error(\n response.status === 404\n ? t('business_rules.logs.errors.notFound', 'Execution log not found.')\n : t('business_rules.logs.errors.fetchFailed')\n ) as Error & { status: number }\n httpErr.status = response.status\n throw httpErr\n }\n const result = await response.json()\n return result as RuleExecutionLog\n },\n enabled: !!logId,\n })\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground\">\n <Spinner className=\"h-6 w-6\" />\n <span>{t('business_rules.logs.detail.loading')}</span>\n </div>\n </PageBody>\n </Page>\n )\n }\n\n const isNotFound = !isLoading && (error as (Error & { status?: number }) | null)?.status === 404\n\n if (isNotFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('business_rules.logs.errors.notFound', 'Execution log not found.')}\n backHref=\"/backend/logs\"\n backLabel={t('business_rules.logs.backToList', 'Back to logs')}\n />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !log) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={(error as Error | null)?.message ?? t('business_rules.logs.errors.loadFailed')}\n action={\n <Button asChild variant=\"outline\" size=\"sm\">\n <Link href=\"/backend/logs\">{t('business_rules.logs.backToList', 'Back to logs')}</Link>\n </Button>\n }\n />\n </PageBody>\n </Page>\n )\n }\n\n const getResultBadgeClass = (result: string) => {\n switch (result) {\n case 'SUCCESS':\n return 'bg-green-100 text-green-800'\n case 'FAILURE':\n return 'bg-yellow-100 text-yellow-800'\n case 'ERROR':\n return 'bg-red-100 text-red-800'\n default:\n return 'bg-muted text-foreground'\n }\n }\n\n return (\n <Page>\n <PageBody>\n <div className=\"space-y-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-bold text-foreground\">\n {t('business_rules.logs.detail.title')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {t('business_rules.logs.detail.logId')}: {log.id}\n </p>\n </div>\n <Button onClick={() => router.push('/backend/logs')} variant=\"outline\">\n {t('business_rules.logs.backToList')}\n </Button>\n </div>\n\n {/* Execution Summary */}\n <div className=\"rounded-lg border bg-card p-6\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {t('business_rules.logs.detail.summary')}\n </h2>\n <dl className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.executedAt')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">\n {new Date(log.executedAt).toLocaleString()}\n </dd>\n </div>\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.result')}\n </dt>\n <dd className=\"mt-1\">\n <span\n className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${getResultBadgeClass(\n log.executionResult\n )}`}\n >\n {t(`business_rules.logs.result.${log.executionResult.toLowerCase()}`)}\n </span>\n </dd>\n </div>\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.executionTime')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">{log.executionTimeMs}ms</dd>\n </div>\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.executedBy')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">\n {log.executedBy || t('common.unknown')}\n </dd>\n </div>\n </dl>\n </div>\n\n {/* Rule Information */}\n <div className=\"rounded-lg border bg-card p-6\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {t('business_rules.logs.detail.ruleInfo')}\n </h2>\n {log.rule ? (\n <dl className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.ruleName')}\n </dt>\n <dd className=\"mt-1\">\n <Link\n href={`/backend/rules/${log.rule.id}`}\n className=\"text-sm text-primary hover:underline\"\n >\n {log.rule.ruleName}\n </Link>\n </dd>\n </div>\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.ruleType')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">{log.rule.ruleType}</dd>\n </div>\n <div className=\"md:col-span-2\">\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.ruleId')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground font-mono\">{log.rule.ruleId}</dd>\n </div>\n </dl>\n ) : (\n <p className=\"text-sm text-muted-foreground\">{t('business_rules.logs.ruleDeleted')}</p>\n )}\n </div>\n\n {/* Entity Information */}\n <div className=\"rounded-lg border bg-card p-6\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {t('business_rules.logs.detail.entityInfo')}\n </h2>\n <dl className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.entityType')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">{log.entityType}</dd>\n </div>\n <div>\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.eventType')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground\">\n {log.eventType || t('common.none')}\n </dd>\n </div>\n {log.entityId && (\n <div className=\"md:col-span-2\">\n <dt className=\"text-sm font-medium text-muted-foreground\">\n {t('business_rules.logs.fields.entityId')}\n </dt>\n <dd className=\"mt-1 text-sm text-foreground font-mono break-all\">\n {log.entityId}\n </dd>\n </div>\n )}\n </dl>\n </div>\n\n {/* Error Message (if present) */}\n {log.errorMessage && (\n <div className=\"rounded-lg border border-destructive bg-destructive/5 p-6\">\n <h2 className=\"text-lg font-semibold mb-4 text-destructive\">\n {t('business_rules.logs.detail.errorMessage')}\n </h2>\n <pre className=\"text-sm text-destructive whitespace-pre-wrap font-mono\">\n {log.errorMessage}\n </pre>\n </div>\n )}\n\n {/* Input Context */}\n {log.inputContext && (\n <JsonDisplay\n data={log.inputContext}\n title={t('business_rules.logs.detail.inputContext')}\n />\n )}\n\n {/* Output Context */}\n {log.outputContext && (\n <JsonDisplay\n data={log.outputContext}\n title={t('business_rules.logs.detail.outputContext')}\n />\n )}\n\n {/* Result Value */}\n {log.resultValue && (\n <JsonDisplay\n data={log.resultValue}\n title={t('business_rules.logs.detail.resultValue')}\n />\n )}\n </div>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { useRouter,
|
|
5
|
+
import { useRouter, usePathname } from "next/navigation";
|
|
6
6
|
import { useQuery } from "@tanstack/react-query";
|
|
7
7
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
8
8
|
import { CrudForm } from "@open-mercato/ui/backend/CrudForm";
|
|
@@ -23,16 +23,10 @@ import {
|
|
|
23
23
|
import { ConditionBuilder } from "../../../components/ConditionBuilder.js";
|
|
24
24
|
import { ActionBuilder } from "../../../components/ActionBuilder.js";
|
|
25
25
|
import { buildRulePayload, parseRuleToFormValues } from "../../../components/utils/formHelpers.js";
|
|
26
|
-
function EditBusinessRulePage() {
|
|
26
|
+
function EditBusinessRulePage({ params }) {
|
|
27
27
|
const router = useRouter();
|
|
28
|
-
const params = useParams();
|
|
29
28
|
const pathname = usePathname();
|
|
30
|
-
|
|
31
|
-
if (params?.slug && Array.isArray(params.slug)) {
|
|
32
|
-
ruleId = params.slug[1];
|
|
33
|
-
} else if (params?.id) {
|
|
34
|
-
ruleId = Array.isArray(params.id) ? params.id[0] : params.id;
|
|
35
|
-
}
|
|
29
|
+
const ruleId = params?.id;
|
|
36
30
|
const t = useT();
|
|
37
31
|
const { organizationId, tenantId } = useOrganizationScopeDetail();
|
|
38
32
|
const { data: rule, isLoading, error } = useQuery({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/business_rules/backend/rules/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter,
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter, usePathname } from 'next/navigation'\nimport { useQuery } from '@tanstack/react-query'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm } from '@open-mercato/ui/backend/CrudForm'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiFetch, withScopedApiHeaders } from '@open-mercato/ui/backend/utils/api'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'\nimport { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport {\n businessRuleFormSchema,\n createFormGroups,\n createFieldDefinitions,\n type BusinessRuleFormValues,\n} from '../../../components/formConfig'\nimport { ConditionBuilder } from '../../../components/ConditionBuilder'\nimport { ActionBuilder } from '../../../components/ActionBuilder'\nimport { buildRulePayload, parseRuleToFormValues } from '../../../components/utils/formHelpers'\n\nexport default function EditBusinessRulePage({ params }: { params?: { id?: string } }) {\n const router = useRouter()\n const pathname = usePathname()\n const ruleId = params?.id\n\n const t = useT()\n const { organizationId, tenantId } = useOrganizationScopeDetail()\n\n const { data: rule, isLoading, error } = useQuery({\n queryKey: ['business_rule', ruleId],\n queryFn: async () => {\n const response = await apiFetch(`/api/business_rules/rules/${ruleId}`)\n if (!response.ok) {\n throw new Error(t('business_rules.errors.fetchFailed'))\n }\n const result = await response.json()\n return result\n },\n enabled: !!ruleId,\n })\n\n const initialValues = React.useMemo(() => {\n if (rule) {\n return parseRuleToFormValues(rule)\n }\n return null\n }, [rule])\n\n const handleSubmit = async (values: BusinessRuleFormValues) => {\n // Use tenant/org from the loaded rule if available, otherwise from context\n const effectiveTenantId = rule?.tenantId || tenantId\n const effectiveOrgId = rule?.organizationId || organizationId\n\n if (!effectiveTenantId || !effectiveOrgId) {\n throw new Error(t('business_rules.errors.missingTenantOrOrg'))\n }\n\n const payload = buildRulePayload(values, effectiveTenantId, effectiveOrgId, undefined)\n\n const updateRule = () => apiFetch('/api/business_rules/rules', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n ...payload,\n id: ruleId,\n }),\n })\n const headers = buildOptimisticLockHeader(rule?.updatedAt ?? rule?.updated_at ?? null)\n const response = Object.keys(headers).length\n ? await withScopedApiHeaders(headers, updateRule)\n : await updateRule()\n\n if (!response.ok) {\n const body = (await readJsonSafe<Record<string, unknown>>(response)) ?? {}\n const message =\n (typeof body.error === 'string' && body.error) ||\n (typeof body.message === 'string' && body.message) ||\n t('business_rules.errors.updateFailed')\n throw new CrudHttpError(response.status, { ...body, error: message })\n }\n\n router.push('/backend/rules')\n router.refresh()\n }\n\n const fields = React.useMemo(() => createFieldDefinitions(t), [t])\n\n const formGroups = React.useMemo(\n () => createFormGroups(t, ConditionBuilder, ActionBuilder),\n [t]\n )\n\n // Publish page-load record context to the AppShell-owned `backend:record:current`\n // mount so the enterprise record_locks widget resolves `business_rules.rule` + id\n // explicitly. The resourceKind mirrors the route's `enforceCommandOptimisticLock`\n // call so the held lock matches the save-time conflict surface for the same rule.\n useSetCurrentRecordInjectionContext(\n buildRecordInjectionContext({\n resourceKind: 'business_rules.rule',\n resourceId: ruleId ?? null,\n updatedAt: rule?.updatedAt ?? rule?.updated_at ?? null,\n data: (rule ?? null) as Record<string, unknown> | null,\n path: pathname,\n }),\n )\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground\">\n <Spinner className=\"h-6 w-6\" />\n <span>{t('business_rules.rules.edit.loading')}</span>\n </div>\n </PageBody>\n </Page>\n )\n }\n\n if (error || !rule) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground\">\n <p>{t('business_rules.errors.loadFailed')}</p>\n <Button asChild variant=\"outline\">\n <Link href=\"/backend/rules\">{t('business_rules.rules.backToList')}</Link>\n </Button>\n </div>\n </PageBody>\n </Page>\n )\n }\n\n if (!initialValues) {\n return null\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm\n key={ruleId}\n title={t('business_rules.rules.edit.title')}\n backHref=\"/backend/rules\"\n schema={businessRuleFormSchema}\n fields={fields}\n initialValues={initialValues}\n optimisticLockUpdatedAt={rule?.updatedAt ?? rule?.updated_at ?? null}\n onSubmit={handleSubmit}\n cancelHref=\"/backend/rules\"\n groups={formGroups}\n submitLabel={t('business_rules.rules.form.update')}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { useRouter,
|
|
5
|
+
import { useRouter, usePathname } from "next/navigation";
|
|
6
6
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
7
7
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
8
8
|
import { CrudForm } from "@open-mercato/ui/backend/CrudForm";
|
|
@@ -26,16 +26,10 @@ const ruleSetFormSchema = z.object({
|
|
|
26
26
|
description: z.string().max(5e3).optional().nullable(),
|
|
27
27
|
enabled: z.boolean().optional()
|
|
28
28
|
});
|
|
29
|
-
function EditRuleSetPage() {
|
|
29
|
+
function EditRuleSetPage({ params }) {
|
|
30
30
|
const router = useRouter();
|
|
31
|
-
const params = useParams();
|
|
32
31
|
const pathname = usePathname();
|
|
33
|
-
|
|
34
|
-
if (params?.slug && Array.isArray(params.slug)) {
|
|
35
|
-
setId = params.slug[1];
|
|
36
|
-
} else if (params?.id) {
|
|
37
|
-
setId = Array.isArray(params.id) ? params.id[0] : params.id;
|
|
38
|
-
}
|
|
32
|
+
const setId = params?.id;
|
|
39
33
|
const t = useT();
|
|
40
34
|
const { organizationId, tenantId } = useOrganizationScopeDetail();
|
|
41
35
|
const queryClient = useQueryClient();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/business_rules/backend/sets/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter, useParams, usePathname } from 'next/navigation'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm } from '@open-mercato/ui/backend/CrudForm'\nimport type { CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiFetch, withScopedApiHeaders } from '@open-mercato/ui/backend/utils/api'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'\nimport { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { z } from 'zod'\nimport { RuleSetMembers } from '@open-mercato/core/modules/business_rules/components/RuleSetMembers'\n\nconst ruleSetFormSchema = z.object({\n setId: z.string().min(1).max(50),\n setName: z.string().min(1).max(200),\n description: z.string().max(5000).optional().nullable(),\n enabled: z.boolean().optional(),\n})\n\ntype RuleSetFormValues = z.infer<typeof ruleSetFormSchema>\n\ntype RuleSetDetail = {\n id: string\n setId: string\n setName: string\n description: string | null\n enabled: boolean\n tenantId: string\n organizationId: string\n createdBy: string | null\n updatedBy: string | null\n createdAt: string\n updatedAt: string\n members: Array<{\n id: string\n ruleId: string\n ruleName: string\n ruleType: string\n sequence: number\n enabled: boolean\n }>\n}\n\nexport default function EditRuleSetPage() {\n const router = useRouter()\n const params = useParams()\n const pathname = usePathname()\n\n // Handle catch-all route: params.slug = ['sets', 'uuid']\n let setId: string | undefined\n if (params?.slug && Array.isArray(params.slug)) {\n setId = params.slug[1] // Second element is the ID\n } else if (params?.id) {\n setId = Array.isArray(params.id) ? params.id[0] : params.id\n }\n\n const t = useT()\n const { organizationId, tenantId } = useOrganizationScopeDetail()\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const { data: ruleSet, isLoading, error } = useQuery({\n queryKey: ['business-rules', 'sets', setId],\n queryFn: async () => {\n const response = await apiFetch(`/api/business_rules/sets/${setId}`)\n if (!response.ok) {\n throw new Error(t('business_rules.sets.errors.fetchFailed'))\n }\n const result = await response.json()\n return result as RuleSetDetail\n },\n enabled: !!setId,\n })\n\n const handleSubmit = async (values: RuleSetFormValues) => {\n const payload = {\n id: setId,\n ...values,\n }\n\n const updateSet = () => apiFetch('/api/business_rules/sets', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n const headers = buildOptimisticLockHeader(ruleSet?.updatedAt ?? null)\n const response = Object.keys(headers).length\n ? await withScopedApiHeaders(headers, updateSet)\n : await updateSet()\n\n if (!response.ok) {\n const body = (await readJsonSafe<Record<string, unknown>>(response)) ?? {}\n const message =\n (typeof body.error === 'string' && body.error) ||\n (typeof body.message === 'string' && body.message) ||\n t('business_rules.sets.errors.updateFailed')\n throw new CrudHttpError(response.status, { ...body, error: message })\n }\n\n flash(t('business_rules.sets.messages.updated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['business-rules', 'sets', setId] })\n }\n\n const handleAddMember = async (ruleId: string, sequence: number) => {\n const result = await apiCall(`/api/business_rules/sets/${setId}/members`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n ruleId,\n sequence,\n enabled: true,\n }),\n })\n\n if (result.ok) {\n flash(t('business_rules.sets.members.messages.added'), 'success')\n queryClient.invalidateQueries({ queryKey: ['business-rules', 'sets', setId] })\n } else {\n const error = result.result as { error?: string; message?: string } | null\n if (error?.error?.includes('duplicate') || error?.message?.includes('exists')) {\n flash(t('business_rules.sets.members.messages.alreadyExists'), 'error')\n } else {\n flash(t('business_rules.sets.members.messages.addFailed'), 'error')\n }\n }\n }\n\n const handleUpdateMember = async (memberId: string, updates: { sequence?: number; enabled?: boolean }) => {\n const result = await apiCall(`/api/business_rules/sets/${setId}/members`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n memberId,\n ...updates,\n }),\n })\n\n if (result.ok) {\n flash(t('business_rules.sets.members.messages.updated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['business-rules', 'sets', setId] })\n } else {\n flash(t('business_rules.sets.members.messages.updateFailed'), 'error')\n }\n }\n\n const handleRemoveMember = async (memberId: string, ruleName: string) => {\n const confirmed = await confirm({\n title: t('business_rules.sets.members.confirm.remove', { name: ruleName }),\n variant: 'destructive',\n })\n if (!confirmed) return\n\n const result = await apiCall(`/api/business_rules/sets/${setId}/members?memberId=${memberId}`, {\n method: 'DELETE',\n })\n\n if (result.ok) {\n flash(t('business_rules.sets.members.messages.removed'), 'success')\n queryClient.invalidateQueries({ queryKey: ['business-rules', 'sets', setId] })\n } else {\n flash(t('business_rules.sets.members.messages.removeFailed'), 'error')\n }\n }\n\n const fields: CrudField[] = React.useMemo(() => [\n {\n id: 'setId',\n label: t('business_rules.sets.form.setId'),\n type: 'text',\n required: true,\n placeholder: t('business_rules.sets.form.placeholders.setId'),\n description: t('business_rules.sets.form.descriptions.setId'),\n },\n {\n id: 'setName',\n label: t('business_rules.sets.form.setName'),\n type: 'text',\n required: true,\n placeholder: t('business_rules.sets.form.placeholders.setName'),\n },\n {\n id: 'description',\n label: t('business_rules.sets.form.description'),\n type: 'textarea',\n placeholder: t('business_rules.sets.form.placeholders.description'),\n },\n {\n id: 'enabled',\n label: t('business_rules.sets.form.enabled'),\n type: 'checkbox',\n description: t('business_rules.sets.form.descriptions.enabled'),\n },\n ], [t])\n\n const initialValues: Partial<RuleSetFormValues> = React.useMemo(() => {\n if (!ruleSet) return {}\n return {\n setId: ruleSet.setId,\n setName: ruleSet.setName,\n description: ruleSet.description,\n enabled: ruleSet.enabled,\n }\n }, [ruleSet])\n\n const formGroups = React.useMemo(() => {\n if (!ruleSet) return []\n return [\n {\n id: 'details',\n title: t('business_rules.sets.edit.detailsSection'),\n column: 1 as const,\n fields: ['setId', 'setName', 'description', 'enabled'],\n },\n {\n id: 'members',\n title: t('business_rules.sets.edit.membersSection'),\n column: 1 as const,\n component: () => (\n <RuleSetMembers\n members={ruleSet.members}\n onAdd={handleAddMember}\n onUpdate={handleUpdateMember}\n onRemove={handleRemoveMember}\n />\n ),\n },\n ]\n }, [t, ruleSet, handleAddMember, handleUpdateMember, handleRemoveMember])\n\n // Publish page-load record context to the AppShell-owned `backend:record:current`\n // mount so the enterprise record_locks widget resolves the rule set + id\n // explicitly. The resourceKind mirrors the route's `enforceCommandOptimisticLock`\n // call (`business_rules.ruleSet`) so the held lock matches the save-time conflict\n // surface for the same rule set.\n useSetCurrentRecordInjectionContext(\n buildRecordInjectionContext({\n resourceKind: 'business_rules.ruleSet',\n resourceId: setId ?? null,\n updatedAt: ruleSet?.updatedAt ?? null,\n data: (ruleSet ?? null) as Record<string, unknown> | null,\n path: pathname,\n }),\n )\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground\">\n <Spinner className=\"h-6 w-6\" />\n <span>{t('business_rules.sets.edit.loading')}</span>\n </div>\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n }\n\n if (error || !ruleSet) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground\">\n <p>{t('business_rules.sets.messages.loadFailed')}</p>\n <Button asChild variant=\"outline\">\n <Link href=\"/backend/sets\">{t('business_rules.sets.backToList')}</Link>\n </Button>\n </div>\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm\n title={t('business_rules.sets.edit.title')}\n backHref=\"/backend/sets\"\n schema={ruleSetFormSchema}\n fields={fields}\n initialValues={initialValues}\n optimisticLockUpdatedAt={ruleSet.updatedAt}\n onSubmit={handleSubmit}\n cancelHref=\"/backend/sets\"\n submitLabel={t('business_rules.sets.form.update')}\n groups={formGroups}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter, usePathname } from 'next/navigation'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm } from '@open-mercato/ui/backend/CrudForm'\nimport type { CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiFetch, withScopedApiHeaders } from '@open-mercato/ui/backend/utils/api'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'\nimport { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { z } from 'zod'\nimport { RuleSetMembers } from '@open-mercato/core/modules/business_rules/components/RuleSetMembers'\n\nconst ruleSetFormSchema = z.object({\n setId: z.string().min(1).max(50),\n setName: z.string().min(1).max(200),\n description: z.string().max(5000).optional().nullable(),\n enabled: z.boolean().optional(),\n})\n\ntype RuleSetFormValues = z.infer<typeof ruleSetFormSchema>\n\ntype RuleSetDetail = {\n id: string\n setId: string\n setName: string\n description: string | null\n enabled: boolean\n tenantId: string\n organizationId: string\n createdBy: string | null\n updatedBy: string | null\n createdAt: string\n updatedAt: string\n members: Array<{\n id: string\n ruleId: string\n ruleName: string\n ruleType: string\n sequence: number\n enabled: boolean\n }>\n}\n\nexport default function EditRuleSetPage({ params }: { params?: { id?: string } }) {\n const router = useRouter()\n const pathname = usePathname()\n const setId = params?.id\n\n const t = useT()\n const { organizationId, tenantId } = useOrganizationScopeDetail()\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const { data: ruleSet, isLoading, error } = useQuery({\n queryKey: ['business-rules', 'sets', setId],\n queryFn: async () => {\n const response = await apiFetch(`/api/business_rules/sets/${setId}`)\n if (!response.ok) {\n throw new Error(t('business_rules.sets.errors.fetchFailed'))\n }\n const result = await response.json()\n return result as RuleSetDetail\n },\n enabled: !!setId,\n })\n\n const handleSubmit = async (values: RuleSetFormValues) => {\n const payload = {\n id: setId,\n ...values,\n }\n\n const updateSet = () => apiFetch('/api/business_rules/sets', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n const headers = buildOptimisticLockHeader(ruleSet?.updatedAt ?? null)\n const response = Object.keys(headers).length\n ? await withScopedApiHeaders(headers, updateSet)\n : await updateSet()\n\n if (!response.ok) {\n const body = (await readJsonSafe<Record<string, unknown>>(response)) ?? {}\n const message =\n (typeof body.error === 'string' && body.error) ||\n (typeof body.message === 'string' && body.message) ||\n t('business_rules.sets.errors.updateFailed')\n throw new CrudHttpError(response.status, { ...body, error: message })\n }\n\n flash(t('business_rules.sets.messages.updated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['business-rules', 'sets', setId] })\n }\n\n const handleAddMember = async (ruleId: string, sequence: number) => {\n const result = await apiCall(`/api/business_rules/sets/${setId}/members`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n ruleId,\n sequence,\n enabled: true,\n }),\n })\n\n if (result.ok) {\n flash(t('business_rules.sets.members.messages.added'), 'success')\n queryClient.invalidateQueries({ queryKey: ['business-rules', 'sets', setId] })\n } else {\n const error = result.result as { error?: string; message?: string } | null\n if (error?.error?.includes('duplicate') || error?.message?.includes('exists')) {\n flash(t('business_rules.sets.members.messages.alreadyExists'), 'error')\n } else {\n flash(t('business_rules.sets.members.messages.addFailed'), 'error')\n }\n }\n }\n\n const handleUpdateMember = async (memberId: string, updates: { sequence?: number; enabled?: boolean }) => {\n const result = await apiCall(`/api/business_rules/sets/${setId}/members`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n memberId,\n ...updates,\n }),\n })\n\n if (result.ok) {\n flash(t('business_rules.sets.members.messages.updated'), 'success')\n queryClient.invalidateQueries({ queryKey: ['business-rules', 'sets', setId] })\n } else {\n flash(t('business_rules.sets.members.messages.updateFailed'), 'error')\n }\n }\n\n const handleRemoveMember = async (memberId: string, ruleName: string) => {\n const confirmed = await confirm({\n title: t('business_rules.sets.members.confirm.remove', { name: ruleName }),\n variant: 'destructive',\n })\n if (!confirmed) return\n\n const result = await apiCall(`/api/business_rules/sets/${setId}/members?memberId=${memberId}`, {\n method: 'DELETE',\n })\n\n if (result.ok) {\n flash(t('business_rules.sets.members.messages.removed'), 'success')\n queryClient.invalidateQueries({ queryKey: ['business-rules', 'sets', setId] })\n } else {\n flash(t('business_rules.sets.members.messages.removeFailed'), 'error')\n }\n }\n\n const fields: CrudField[] = React.useMemo(() => [\n {\n id: 'setId',\n label: t('business_rules.sets.form.setId'),\n type: 'text',\n required: true,\n placeholder: t('business_rules.sets.form.placeholders.setId'),\n description: t('business_rules.sets.form.descriptions.setId'),\n },\n {\n id: 'setName',\n label: t('business_rules.sets.form.setName'),\n type: 'text',\n required: true,\n placeholder: t('business_rules.sets.form.placeholders.setName'),\n },\n {\n id: 'description',\n label: t('business_rules.sets.form.description'),\n type: 'textarea',\n placeholder: t('business_rules.sets.form.placeholders.description'),\n },\n {\n id: 'enabled',\n label: t('business_rules.sets.form.enabled'),\n type: 'checkbox',\n description: t('business_rules.sets.form.descriptions.enabled'),\n },\n ], [t])\n\n const initialValues: Partial<RuleSetFormValues> = React.useMemo(() => {\n if (!ruleSet) return {}\n return {\n setId: ruleSet.setId,\n setName: ruleSet.setName,\n description: ruleSet.description,\n enabled: ruleSet.enabled,\n }\n }, [ruleSet])\n\n const formGroups = React.useMemo(() => {\n if (!ruleSet) return []\n return [\n {\n id: 'details',\n title: t('business_rules.sets.edit.detailsSection'),\n column: 1 as const,\n fields: ['setId', 'setName', 'description', 'enabled'],\n },\n {\n id: 'members',\n title: t('business_rules.sets.edit.membersSection'),\n column: 1 as const,\n component: () => (\n <RuleSetMembers\n members={ruleSet.members}\n onAdd={handleAddMember}\n onUpdate={handleUpdateMember}\n onRemove={handleRemoveMember}\n />\n ),\n },\n ]\n }, [t, ruleSet, handleAddMember, handleUpdateMember, handleRemoveMember])\n\n // Publish page-load record context to the AppShell-owned `backend:record:current`\n // mount so the enterprise record_locks widget resolves the rule set + id\n // explicitly. The resourceKind mirrors the route's `enforceCommandOptimisticLock`\n // call (`business_rules.ruleSet`) so the held lock matches the save-time conflict\n // surface for the same rule set.\n useSetCurrentRecordInjectionContext(\n buildRecordInjectionContext({\n resourceKind: 'business_rules.ruleSet',\n resourceId: setId ?? null,\n updatedAt: ruleSet?.updatedAt ?? null,\n data: (ruleSet ?? null) as Record<string, unknown> | null,\n path: pathname,\n }),\n )\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground\">\n <Spinner className=\"h-6 w-6\" />\n <span>{t('business_rules.sets.edit.loading')}</span>\n </div>\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n }\n\n if (error || !ruleSet) {\n return (\n <Page>\n <PageBody>\n <div className=\"flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground\">\n <p>{t('business_rules.sets.messages.loadFailed')}</p>\n <Button asChild variant=\"outline\">\n <Link href=\"/backend/sets\">{t('business_rules.sets.backToList')}</Link>\n </Button>\n </div>\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm\n title={t('business_rules.sets.edit.title')}\n backHref=\"/backend/sets\"\n schema={ruleSetFormSchema}\n fields={fields}\n initialValues={initialValues}\n optimisticLockUpdatedAt={ruleSet.updatedAt}\n onSubmit={handleSubmit}\n cancelHref=\"/backend/sets\"\n submitLabel={t('business_rules.sets.form.update')}\n groups={formGroups}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["error"]
|
|
7
7
|
}
|
package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { useParams } from "next/navigation";
|
|
5
4
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
6
5
|
import { LoadingMessage, ErrorMessage, RecordNotFoundState } from "@open-mercato/ui/backend/detail";
|
|
7
6
|
import { Tag } from "@open-mercato/ui/primitives/tag";
|
|
8
7
|
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
9
8
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
10
|
-
function ChannelDetailPage() {
|
|
9
|
+
function ChannelDetailPage({ params }) {
|
|
11
10
|
const t = useT();
|
|
12
|
-
const params = useParams();
|
|
13
11
|
const id = params?.id ?? "";
|
|
14
12
|
const [detail, setDetail] = React.useState(null);
|
|
15
13
|
const [health, setHealth] = React.useState(null);
|
|
@@ -17,7 +15,11 @@ function ChannelDetailPage() {
|
|
|
17
15
|
const [errorMessage, setErrorMessage] = React.useState(null);
|
|
18
16
|
const [notFound, setNotFound] = React.useState(false);
|
|
19
17
|
React.useEffect(() => {
|
|
20
|
-
if (!id)
|
|
18
|
+
if (!id) {
|
|
19
|
+
setIsLoading(false);
|
|
20
|
+
setErrorMessage(t("communication_channels.errors.missingChannelId", "No channel selected."));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
21
23
|
let cancelled = false;
|
|
22
24
|
async function load() {
|
|
23
25
|
setIsLoading(true);
|
package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/communication_channels/backend/communication_channels/channels/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype ChannelDetail = {\n id: string\n providerKey: string\n channelType: string\n displayName: string\n externalIdentifier: string | null\n capabilities: Record<string, unknown> | null\n isActive: boolean\n}\n\ntype ChannelHealth = {\n channelId: string\n providerKey: string\n channelType: string\n windowHours: number\n totalsLast24h: number\n counts: Record<string, number>\n recentFailures: Array<{\n id: string\n messageId: string\n direction: string\n createdAt: string | null\n lastError: string | null\n transient: boolean | null\n }>\n}\n\nexport default function ChannelDetailPage({ params }: { params?: { id?: string } }) {\n const t = useT()\n const id = params?.id ?? ''\n\n const [detail, setDetail] = React.useState<ChannelDetail | null>(null)\n const [health, setHealth] = React.useState<ChannelHealth | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n const [notFound, setNotFound] = React.useState(false)\n\n React.useEffect(() => {\n if (!id) {\n setIsLoading(false)\n setErrorMessage(t('communication_channels.errors.missingChannelId', 'No channel selected.'))\n return\n }\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setErrorMessage(null)\n setNotFound(false)\n const [detailRes, healthRes] = await Promise.all([\n apiCall<ChannelDetail>(`/api/communication_channels/channels/${encodeURIComponent(id)}`),\n apiCall<ChannelHealth>(`/api/communication_channels/channels/${encodeURIComponent(id)}/health`),\n ]).catch((err: unknown) => {\n return [\n { ok: false, result: { error: err instanceof Error ? err.message : 'load failed' } },\n { ok: false, result: { error: err instanceof Error ? err.message : 'load failed' } },\n ] as const\n })\n if (cancelled) return\n if (!detailRes.ok) {\n const status = (detailRes as { status?: number }).status\n if (status === 404) {\n setNotFound(true)\n } else {\n const body = detailRes.result as { error?: string } | undefined\n setErrorMessage(\n body?.error ?? t('communication_channels.errors.loadDetail', 'Failed to load channel'),\n )\n }\n setDetail(null)\n setHealth(null)\n } else {\n setDetail(detailRes.result ?? null)\n setHealth(healthRes.ok ? (healthRes.result ?? null) : null)\n }\n setIsLoading(false)\n }\n void load()\n return () => {\n cancelled = true\n }\n }, [id, t])\n\n if (isLoading) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('communication_channels.detail.loading', 'Loading channel...')} />\n </PageBody>\n </Page>\n )\n }\n if (notFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('communication_channels.detail.notFound', 'Channel not found')}\n backHref=\"/backend/communication_channels/channels\"\n />\n </PageBody>\n </Page>\n )\n }\n if (errorMessage || !detail) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage\n label={errorMessage ?? t('communication_channels.errors.loadDetail', 'Failed to load channel')}\n />\n </PageBody>\n </Page>\n )\n }\n\n const capabilities = (detail.capabilities ?? {}) as Record<string, unknown>\n const capabilityKeys = Object.keys(capabilities)\n\n return (\n <Page>\n <PageBody>\n <header className=\"mb-4 flex flex-wrap items-baseline justify-between gap-4\">\n <div>\n <h2 className=\"text-2xl font-semibold\">{detail.displayName}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {detail.providerKey} \u00B7 {detail.channelType}\n {detail.externalIdentifier ? ` \u00B7 ${detail.externalIdentifier}` : ''}\n </p>\n </div>\n {detail.isActive ? (\n <Tag variant=\"success\" dot>\n {t('communication_channels.status.active', 'Active')}\n </Tag>\n ) : (\n <Tag variant=\"neutral\">{t('communication_channels.status.inactive', 'Inactive')}</Tag>\n )}\n </header>\n\n <section\n className=\"mb-4 rounded-md border bg-card p-4\"\n aria-label={t('communication_channels.detail.capabilities', 'Capabilities')}\n >\n <header className=\"mb-2 text-overline text-muted-foreground\">\n {t('communication_channels.detail.capabilities', 'Capabilities')}\n </header>\n {capabilityKeys.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'communication_channels.detail.noCapabilities',\n 'No capabilities snapshot recorded for this channel yet.',\n )}\n </p>\n ) : (\n <dl className=\"grid grid-cols-2 gap-x-6 gap-y-1 text-sm md:grid-cols-3\">\n {capabilityKeys.sort((a, b) => a.localeCompare(b)).map((key) => (\n <React.Fragment key={key}>\n <dt className=\"text-muted-foreground\">{key}</dt>\n <dd className=\"md:col-span-2\">{renderCapabilityValue(capabilities[key])}</dd>\n </React.Fragment>\n ))}\n </dl>\n )}\n </section>\n\n <section\n className=\"rounded-md border bg-card p-4\"\n aria-label={t('communication_channels.detail.health', 'Delivery health (last 24h)')}\n >\n <header className=\"mb-2 flex items-baseline justify-between text-overline text-muted-foreground\">\n <span>{t('communication_channels.detail.health', 'Delivery health (last 24h)')}</span>\n {health ? (\n <span className=\"text-xs\">\n {health.totalsLast24h}{' '}\n {t('communication_channels.detail.messages', 'messages')}\n </span>\n ) : null}\n </header>\n {health ? (\n <>\n <ul className=\"mb-3 flex flex-wrap gap-2 text-xs\">\n {Object.entries(health.counts).map(([status, count]) => (\n <li key={status}>\n <Tag variant={tagVariantForStatus(status)}>\n {status}: {count}\n </Tag>\n </li>\n ))}\n </ul>\n {health.recentFailures.length > 0 ? (\n <details>\n <summary className=\"cursor-pointer text-sm text-muted-foreground\">\n {t('communication_channels.detail.recentFailures', 'Recent failures')} (\n {health.recentFailures.length})\n </summary>\n <ul className=\"mt-2 space-y-1 text-xs\">\n {health.recentFailures.map((failure) => (\n <li key={failure.id} className=\"rounded border bg-muted p-2\">\n <div className=\"font-mono text-xs text-muted-foreground\">\n {failure.messageId} \u00B7 {failure.direction} \u00B7{' '}\n {failure.createdAt ?? '\u2014'}\n </div>\n {failure.lastError ? (\n <div className=\"mt-1\">{failure.lastError}</div>\n ) : null}\n </li>\n ))}\n </ul>\n </details>\n ) : null}\n </>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t('communication_channels.detail.noHealth', 'No health data available.')}\n </p>\n )}\n </section>\n </PageBody>\n </Page>\n )\n}\n\nfunction renderCapabilityValue(value: unknown): string {\n if (value === null || value === undefined) return '\u2014'\n if (Array.isArray(value)) return value.join(', ')\n if (typeof value === 'object') return JSON.stringify(value)\n return String(value)\n}\n\nfunction tagVariantForStatus(status: string): 'success' | 'warning' | 'error' | 'info' | 'neutral' {\n switch (status) {\n case 'sent':\n case 'delivered':\n case 'read':\n return 'success'\n case 'failed':\n return 'error'\n case 'pending':\n case 'queued':\n return 'warning'\n default:\n return 'neutral'\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|