@open-mercato/core 0.6.6-develop.6543.1.49fadb4c31 → 0.6.6

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.
Files changed (166) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/api_keys/api/keys/route.js +9 -23
  3. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  4. package/dist/modules/api_keys/cli.js +1 -2
  5. package/dist/modules/api_keys/cli.js.map +2 -2
  6. package/dist/modules/attachments/api/file/[id]/route.js +7 -9
  7. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  8. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -9
  9. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  10. package/dist/modules/attachments/api/route.js +17 -21
  11. package/dist/modules/attachments/api/route.js.map +2 -2
  12. package/dist/modules/auth/api/users/route.js +2 -6
  13. package/dist/modules/auth/api/users/route.js.map +2 -2
  14. package/dist/modules/auth/cli.js +2 -3
  15. package/dist/modules/auth/cli.js.map +2 -2
  16. package/dist/modules/configs/lib/upgrade-actions.js +0 -22
  17. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  18. package/dist/modules/customer_accounts/widgets/injection-table.js +2 -0
  19. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  20. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +1 -7
  21. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  22. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +7 -11
  23. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  24. package/dist/modules/dashboards/api/roles/widgets/route.js +3 -22
  25. package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
  26. package/dist/modules/dashboards/api/users/widgets/route.js +3 -22
  27. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  28. package/dist/modules/dashboards/cli.js +3 -4
  29. package/dist/modules/dashboards/cli.js.map +2 -2
  30. package/dist/modules/dashboards/lib/widgetAssignmentScope.js +1 -9
  31. package/dist/modules/dashboards/lib/widgetAssignmentScope.js.map +2 -2
  32. package/dist/modules/dictionaries/commands/entries.js +14 -7
  33. package/dist/modules/dictionaries/commands/entries.js.map +2 -2
  34. package/dist/modules/dictionaries/commands/entry-operations.js +16 -7
  35. package/dist/modules/dictionaries/commands/entry-operations.js.map +2 -2
  36. package/dist/modules/dictionaries/commands/factory.js +3 -9
  37. package/dist/modules/dictionaries/commands/factory.js.map +2 -2
  38. package/dist/modules/dictionaries/fields/dictionary.js +41 -56
  39. package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
  40. package/dist/modules/directory/utils/organizationScope.js +3 -10
  41. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  42. package/dist/modules/entities/api/records.js +4 -5
  43. package/dist/modules/entities/api/records.js.map +2 -2
  44. package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js +0 -1
  45. package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js.map +2 -2
  46. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +49 -49
  47. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  48. package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js +0 -1
  49. package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js.map +2 -2
  50. package/dist/modules/entities/cli.js +1 -2
  51. package/dist/modules/entities/cli.js.map +2 -2
  52. package/dist/modules/query_index/api/status.js +8 -2
  53. package/dist/modules/query_index/api/status.js.map +2 -2
  54. package/dist/modules/sales/commands/documents.js +0 -44
  55. package/dist/modules/sales/commands/documents.js.map +2 -2
  56. package/dist/modules/sales/commands/returns.js +27 -2
  57. package/dist/modules/sales/commands/returns.js.map +2 -2
  58. package/dist/modules/sales/components/documents/AddressesSection.js +2 -6
  59. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  60. package/dist/modules/sales/components/documents/ItemsSection.js +0 -1
  61. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  62. package/dist/modules/sales/components/documents/LineItemDialog.js +0 -9
  63. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  64. package/dist/modules/sales/components/documents/SalesDocumentForm.js +0 -44
  65. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  66. package/dist/modules/sales/components/documents/ShipmentDialog.js +38 -59
  67. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  68. package/dist/modules/sales/lib/shipments/snapshots.js +1 -27
  69. package/dist/modules/sales/lib/shipments/snapshots.js.map +2 -2
  70. package/dist/modules/workflows/frontend/checkout-demo/page.js +8 -1
  71. package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
  72. package/dist/modules/workflows/lib/activity-executor.js +1 -1
  73. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  74. package/dist/modules/workflows/lib/find-definition.js +18 -2
  75. package/dist/modules/workflows/lib/find-definition.js.map +2 -2
  76. package/dist/modules/workflows/lib/signal-handler.js +3 -2
  77. package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
  78. package/dist/modules/workflows/lib/step-handler.js +4 -8
  79. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  80. package/dist/modules/workflows/lib/task-handler.js +3 -5
  81. package/dist/modules/workflows/lib/task-handler.js.map +2 -2
  82. package/dist/modules/workflows/lib/timer-handler.js +2 -1
  83. package/dist/modules/workflows/lib/timer-handler.js.map +2 -2
  84. package/dist/modules/workflows/lib/transition-handler.js +5 -13
  85. package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
  86. package/dist/modules/workflows/lib/workflow-executor.js +11 -9
  87. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  88. package/dist/modules/workflows/migrations/Migration20260715120000.js +68 -0
  89. package/dist/modules/workflows/migrations/Migration20260715120000.js.map +7 -0
  90. package/dist/modules/workflows/migrations/Migration20260716120000.js +24 -0
  91. package/dist/modules/workflows/migrations/Migration20260716120000.js.map +7 -0
  92. package/dist/modules/workflows/workflows.js +2 -0
  93. package/dist/modules/workflows/workflows.js.map +2 -2
  94. package/package.json +8 -9
  95. package/src/modules/api_keys/api/keys/route.ts +9 -27
  96. package/src/modules/api_keys/cli.ts +3 -2
  97. package/src/modules/attachments/api/file/[id]/route.ts +7 -11
  98. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -11
  99. package/src/modules/attachments/api/route.ts +21 -26
  100. package/src/modules/auth/api/users/route.ts +2 -11
  101. package/src/modules/auth/cli.ts +2 -3
  102. package/src/modules/configs/i18n/de.json +1 -5
  103. package/src/modules/configs/i18n/en.json +1 -5
  104. package/src/modules/configs/i18n/es.json +1 -5
  105. package/src/modules/configs/i18n/pl.json +1 -5
  106. package/src/modules/configs/lib/upgrade-actions.ts +0 -24
  107. package/src/modules/customer_accounts/AGENTS.md +6 -8
  108. package/src/modules/customer_accounts/widgets/injection-table.ts +2 -0
  109. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +1 -11
  110. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +7 -13
  111. package/src/modules/dashboards/api/roles/widgets/route.ts +3 -22
  112. package/src/modules/dashboards/api/users/widgets/route.ts +3 -22
  113. package/src/modules/dashboards/cli.ts +9 -4
  114. package/src/modules/dashboards/lib/widgetAssignmentScope.ts +0 -18
  115. package/src/modules/dictionaries/commands/entries.ts +16 -7
  116. package/src/modules/dictionaries/commands/entry-operations.ts +18 -8
  117. package/src/modules/dictionaries/commands/factory.ts +2 -8
  118. package/src/modules/dictionaries/fields/dictionary.tsx +32 -55
  119. package/src/modules/dictionaries/i18n/de.json +0 -2
  120. package/src/modules/dictionaries/i18n/en.json +0 -2
  121. package/src/modules/dictionaries/i18n/es.json +0 -2
  122. package/src/modules/dictionaries/i18n/pl.json +0 -2
  123. package/src/modules/directory/utils/organizationScope.ts +2 -36
  124. package/src/modules/entities/api/records.ts +7 -5
  125. package/src/modules/entities/backend/entities/system/[entityId]/page.meta.ts +0 -1
  126. package/src/modules/entities/backend/entities/user/[entityId]/page.meta.ts +0 -1
  127. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +49 -51
  128. package/src/modules/entities/cli.ts +1 -2
  129. package/src/modules/entities/i18n/de.json +0 -34
  130. package/src/modules/entities/i18n/en.json +0 -34
  131. package/src/modules/entities/i18n/es.json +0 -34
  132. package/src/modules/entities/i18n/pl.json +0 -34
  133. package/src/modules/query_index/api/status.ts +8 -2
  134. package/src/modules/sales/commands/documents.ts +0 -65
  135. package/src/modules/sales/commands/returns.ts +32 -2
  136. package/src/modules/sales/components/documents/AddressesSection.tsx +2 -6
  137. package/src/modules/sales/components/documents/ItemsSection.tsx +0 -5
  138. package/src/modules/sales/components/documents/LineItemDialog.tsx +0 -10
  139. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +0 -46
  140. package/src/modules/sales/components/documents/ShipmentDialog.tsx +40 -65
  141. package/src/modules/sales/i18n/de.json +0 -4
  142. package/src/modules/sales/i18n/en.json +0 -4
  143. package/src/modules/sales/i18n/es.json +0 -4
  144. package/src/modules/sales/i18n/pl.json +0 -4
  145. package/src/modules/sales/lib/shipments/snapshots.ts +0 -30
  146. package/src/modules/workflows/frontend/checkout-demo/page.tsx +12 -1
  147. package/src/modules/workflows/lib/activity-executor.ts +22 -2
  148. package/src/modules/workflows/lib/find-definition.ts +53 -1
  149. package/src/modules/workflows/lib/signal-handler.ts +5 -4
  150. package/src/modules/workflows/lib/step-handler.ts +4 -8
  151. package/src/modules/workflows/lib/task-handler.ts +2 -4
  152. package/src/modules/workflows/lib/timer-handler.ts +3 -2
  153. package/src/modules/workflows/lib/transition-handler.ts +5 -13
  154. package/src/modules/workflows/lib/workflow-executor.ts +11 -8
  155. package/src/modules/workflows/migrations/Migration20260715120000.ts +114 -0
  156. package/src/modules/workflows/migrations/Migration20260716120000.ts +57 -0
  157. package/src/modules/workflows/workflows.ts +2 -0
  158. package/dist/modules/attachments/lib/reconcileOrganization.js +0 -110
  159. package/dist/modules/attachments/lib/reconcileOrganization.js.map +0 -7
  160. package/dist/modules/attachments/lib/requestScope.js +0 -10
  161. package/dist/modules/attachments/lib/requestScope.js.map +0 -7
  162. package/dist/modules/customers/components/detail/useDealsAccess.js +0 -16
  163. package/dist/modules/customers/components/detail/useDealsAccess.js.map +0 -7
  164. package/src/modules/attachments/lib/reconcileOrganization.ts +0 -188
  165. package/src/modules/attachments/lib/requestScope.ts +0 -27
  166. package/src/modules/customers/components/detail/useDealsAccess.ts +0 -19
@@ -19,8 +19,10 @@ const companyUsersWidget = {
19
19
  export const injectionTable: ModuleInjectionTable = {
20
20
  'customers.person': [accountStatusWidget],
21
21
  'crud-form:customers.person': [accountStatusWidget],
22
+ 'crud-form:customers:customer_person_profile:fields': [accountStatusWidget],
22
23
  'customers.company': [companyUsersWidget],
23
24
  'crud-form:customers.company': [companyUsersWidget],
25
+ 'crud-form:customers:customer_company_profile:fields': [companyUsersWidget],
24
26
  // Step 4.10 — Portal AiChat injection example.
25
27
  // Mapped to the portal profile page's `pageAfter('profile')` spot;
26
28
  // third-party modules targeting other portal pages can copy this entry.
@@ -31,7 +31,6 @@ import type { TagsSectionController } from '@open-mercato/ui/backend/detail'
31
31
  import { coerceDisplayName } from '../../../../lib/displayName'
32
32
  import { CompanyDetailHeader } from '../../../../components/detail/CompanyDetailHeader'
33
33
  import { CompanyDetailTabs, resolveLegacyTab, type CompanyTabId } from '../../../../components/detail/CompanyDetailTabs'
34
- import { useDealsAccess } from '../../../../components/detail/useDealsAccess'
35
34
  import { CompanyKpiBar } from '../../../../components/detail/CompanyKpiBar'
36
35
  import { ScheduleActivityDialog, type ScheduleActivityEditData } from '../../../../components/detail/ScheduleActivityDialog'
37
36
  import { ChangelogTab } from '../../../../components/detail/ChangelogTab'
@@ -70,7 +69,6 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
70
69
  }, [searchParams])
71
70
  const [activeTab, setActiveTab] = React.useState<CompanyTabId>(initialTab)
72
71
  const [sectionAction, setSectionAction] = React.useState<SectionAction | null>(null)
73
- const { canViewDeals, isReady: isDealsAccessReady } = useDealsAccess()
74
72
 
75
73
  // Form state
76
74
  const [isDirty, setIsDirty] = React.useState(false)
@@ -330,14 +328,6 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
330
328
  setSectionAction(null)
331
329
  }, [activeTab])
332
330
 
333
- // A `?tab=deals` deep link must not strand users without `customers.deals.view`
334
- // on a tab that no longer exists for them. Wait for the granted features to load
335
- // so a permitted user is never bounced off the tab mid-fetch.
336
- React.useEffect(() => {
337
- if (!isDealsAccessReady || canViewDeals) return
338
- setActiveTab((current) => (current === 'deals' ? 'people' : current))
339
- }, [isDealsAccessReady, canViewDeals])
340
-
341
331
  // Deals scope
342
332
  const dealsScope = React.useMemo(
343
333
  () => (currentCompanyId ? ({ kind: 'company', entityId: currentCompanyId } as const) : null),
@@ -554,7 +544,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
554
544
  />
555
545
  )}
556
546
 
557
- {activeTab === 'deals' && canViewDeals && (
547
+ {activeTab === 'deals' && (
558
548
  <DealsSection
559
549
  scope={dealsScope}
560
550
  emptyLabel={t('customers.companies.detail.empty.deals', 'No deals linked to this company.')}
@@ -14,7 +14,6 @@ import {
14
14
  Plus,
15
15
  } from 'lucide-react'
16
16
  import type { SectionAction } from '@open-mercato/ui/backend/detail'
17
- import { useDealsAccess } from './useDealsAccess'
18
17
 
19
18
  export type CompanyTabId =
20
19
  | 'people'
@@ -88,7 +87,6 @@ export function CompanyDetailTabs({
88
87
  children,
89
88
  }: CompanyDetailTabsProps) {
90
89
  const t = useT()
91
- const { canViewDeals } = useDealsAccess()
92
90
 
93
91
  const builtInTabs: TabDef[] = React.useMemo(
94
92
  () => [
@@ -98,16 +96,12 @@ export function CompanyDetailTabs({
98
96
  icon: <Users className="size-4" />,
99
97
  badge: <CountBadge count={peopleCount} />,
100
98
  },
101
- ...(canViewDeals
102
- ? [
103
- {
104
- id: 'deals' as CompanyTabId,
105
- label: t('customers.companies.detail.tabs.deals', 'Deals'),
106
- icon: <Handshake className="size-4" />,
107
- badge: <CountBadge count={dealsCount} />,
108
- },
109
- ]
110
- : []),
99
+ {
100
+ id: 'deals',
101
+ label: t('customers.companies.detail.tabs.deals', 'Deals'),
102
+ icon: <Handshake className="size-4" />,
103
+ badge: <CountBadge count={dealsCount} />,
104
+ },
111
105
  {
112
106
  id: 'activity-log',
113
107
  label: t('customers.companies.detail.tabs.activityLog', 'Activity log'),
@@ -127,7 +121,7 @@ export function CompanyDetailTabs({
127
121
  badge: <CountBadge count={filesCount} />,
128
122
  },
129
123
  ],
130
- [t, canViewDeals, peopleCount, dealsCount, activitiesCount, filesCount],
124
+ [t, peopleCount, dealsCount, activitiesCount, filesCount],
131
125
  )
132
126
 
133
127
  const allTabs: TabDef[] = React.useMemo(
@@ -6,10 +6,7 @@ import { DashboardRoleWidgets } from '@open-mercato/core/modules/dashboards/data
6
6
  import { Role } from '@open-mercato/core/modules/auth/data/entities'
7
7
  import { roleWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'
8
8
  import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
9
- import {
10
- resolveWidgetAssignmentReadScope,
11
- resolveWidgetAssignmentTargetAccess,
12
- } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
9
+ import { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
13
10
  import { hasFeature } from '@open-mercato/shared/security/features'
14
11
  import {
15
12
  runCrudMutationGuardAfterSuccess,
@@ -73,15 +70,7 @@ export async function GET(req: Request) {
73
70
  })
74
71
 
75
72
  const role = await em.findOne(Role, { id: roleId, deletedAt: null })
76
- const access = resolveWidgetAssignmentTargetAccess({
77
- isSuperAdmin: !!acl.isSuperAdmin,
78
- scopeTenantId: tenantId,
79
- target: role,
80
- })
81
- if (access === 'forbidden') {
82
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
83
- }
84
- if (access === 'not-found') {
73
+ if (!role || (tenantId && role.tenantId !== tenantId)) {
85
74
  return NextResponse.json({ error: 'Role not found' }, { status: 404 })
86
75
  }
87
76
 
@@ -132,15 +121,7 @@ export async function PUT(req: Request) {
132
121
  const organizationId = auth.orgId ?? null
133
122
 
134
123
  const role = await em.findOne(Role, { id: parsed.data.roleId, deletedAt: null })
135
- const access = resolveWidgetAssignmentTargetAccess({
136
- isSuperAdmin: !!acl.isSuperAdmin,
137
- scopeTenantId: tenantId,
138
- target: role,
139
- })
140
- if (access === 'forbidden') {
141
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
142
- }
143
- if (access === 'not-found') {
124
+ if (!role || (tenantId && role.tenantId !== tenantId)) {
144
125
  return NextResponse.json({ error: 'Role not found' }, { status: 404 })
145
126
  }
146
127
 
@@ -7,10 +7,7 @@ import { User } from '@open-mercato/core/modules/auth/data/entities'
7
7
  import { userWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'
8
8
  import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
9
9
  import { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'
10
- import {
11
- resolveWidgetAssignmentReadScope,
12
- resolveWidgetAssignmentTargetAccess,
13
- } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
10
+ import { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
14
11
  import { hasFeature } from '@open-mercato/shared/security/features'
15
12
  import {
16
13
  runCrudMutationGuardAfterSuccess,
@@ -56,15 +53,7 @@ export async function GET(req: Request) {
56
53
  })
57
54
 
58
55
  const targetUserForRead = await em.findOne(User, { id: userId, deletedAt: null })
59
- const readAccess = resolveWidgetAssignmentTargetAccess({
60
- isSuperAdmin: !!acl.isSuperAdmin,
61
- scopeTenantId: tenantId,
62
- target: targetUserForRead,
63
- })
64
- if (readAccess === 'forbidden') {
65
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
66
- }
67
- if (readAccess === 'not-found') {
56
+ if (!targetUserForRead || (tenantId && (targetUserForRead.tenantId ?? null) !== tenantId)) {
68
57
  return NextResponse.json({ error: 'User not found' }, { status: 404 })
69
58
  }
70
59
 
@@ -132,15 +121,7 @@ export async function PUT(req: Request) {
132
121
  const organizationId = auth.orgId ?? null
133
122
 
134
123
  const targetUser = await em.findOne(User, { id: parsed.data.userId, deletedAt: null })
135
- const writeAccess = resolveWidgetAssignmentTargetAccess({
136
- isSuperAdmin: !!acl.isSuperAdmin,
137
- scopeTenantId: tenantId,
138
- target: targetUser,
139
- })
140
- if (writeAccess === 'forbidden') {
141
- return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
142
- }
143
- if (writeAccess === 'not-found') {
124
+ if (!targetUser || (tenantId && (targetUser.tenantId ?? null) !== tenantId)) {
144
125
  return NextResponse.json({ error: 'User not found' }, { status: 404 })
145
126
  }
146
127
 
@@ -6,7 +6,6 @@ import { Role } from '@open-mercato/core/modules/auth/data/entities'
6
6
  import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
7
7
  import { appendWidgetsToRoles, resolveAnalyticsWidgetIds } from '@open-mercato/core/modules/dashboards/lib/role-widgets'
8
8
  import { seedAnalyticsData } from './seed/analytics'
9
- import { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'
10
9
 
11
10
  type Args = Record<string, string>
12
11
 
@@ -109,7 +108,10 @@ const seedDefaults: ModuleCli = {
109
108
  return
110
109
  }
111
110
 
112
- const roleNames = parseCommaSeparatedList(roleCsv)
111
+ const roleNames = roleCsv
112
+ .split(',')
113
+ .map((name) => name.trim())
114
+ .filter(Boolean)
113
115
 
114
116
  if (!roleNames.length) {
115
117
  console.log('No roles provided, nothing to seed.')
@@ -123,7 +125,7 @@ const seedDefaults: ModuleCli = {
123
125
  tenantId,
124
126
  organizationId,
125
127
  roleNames,
126
- widgetIds: widgetCsv ? parseCommaSeparatedList(widgetCsv) : undefined,
128
+ widgetIds: widgetCsv ? widgetCsv.split(',').map((id) => id.trim()).filter(Boolean) : undefined,
127
129
  logger: (message) => console.log(message),
128
130
  })
129
131
  },
@@ -141,7 +143,10 @@ const enableAnalyticsWidgets: ModuleCli = {
141
143
  return
142
144
  }
143
145
 
144
- const roleNames = parseCommaSeparatedList(roleCsv)
146
+ const roleNames = roleCsv
147
+ .split(',')
148
+ .map((name) => name.trim())
149
+ .filter(Boolean)
145
150
 
146
151
  if (!roleNames.length) {
147
152
  console.log('No roles provided, nothing to update.')
@@ -8,24 +8,6 @@ export type ResolvedAssignmentScope = {
8
8
  organizationId: string | null
9
9
  }
10
10
 
11
- export type WidgetAssignmentTarget = {
12
- tenantId?: string | null
13
- }
14
-
15
- export type WidgetAssignmentTargetAccess = 'allowed' | 'forbidden' | 'not-found'
16
-
17
- export function resolveWidgetAssignmentTargetAccess(params: {
18
- isSuperAdmin: boolean
19
- scopeTenantId: string | null
20
- target: WidgetAssignmentTarget | null | undefined
21
- }): WidgetAssignmentTargetAccess {
22
- const { isSuperAdmin, scopeTenantId, target } = params
23
- if (!isSuperAdmin && !scopeTenantId) return 'forbidden'
24
- if (!target) return 'not-found'
25
- if (scopeTenantId && (target.tenantId ?? null) !== scopeTenantId) return 'not-found'
26
- return 'allowed'
27
- }
28
-
29
11
  export function resolveWidgetAssignmentReadScope(params: {
30
12
  auth: AuthScopeInput
31
13
  isSuperAdmin: boolean
@@ -4,13 +4,22 @@ import {
4
4
  dictionaryEntryCommandCreateSchema,
5
5
  dictionaryEntryCommandUpdateSchema,
6
6
  } from '@open-mercato/core/modules/dictionaries/data/validators'
7
- import {
8
- ensureDictionaryEntryScope,
9
- registerDictionaryEntryCommands,
10
- } from '@open-mercato/core/modules/dictionaries/commands/factory'
7
+ import { registerDictionaryEntryCommands } from '@open-mercato/core/modules/dictionaries/commands/factory'
8
+ import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
11
9
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
12
10
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
13
11
 
12
+ function ensureScope(ctx: CommandRuntimeContext, scope: { tenantId: string; organizationId: string }): void {
13
+ const tenantId = ctx.auth?.tenantId ?? null
14
+ if (tenantId && tenantId !== scope.tenantId) {
15
+ throw new CrudHttpError(403, { error: 'Forbidden' })
16
+ }
17
+ const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
18
+ if (organizationId && organizationId !== scope.organizationId) {
19
+ throw new CrudHttpError(403, { error: 'Forbidden' })
20
+ }
21
+ }
22
+
14
23
  async function ensureDictionary(em: EntityManager, id: string): Promise<Dictionary> {
15
24
  const dictionary = await em.findOne(Dictionary, {
16
25
  id,
@@ -28,12 +37,12 @@ registerDictionaryEntryCommands({
28
37
  translationKeyPrefix: 'dictionaries.entries',
29
38
  createSchema: dictionaryEntryCommandCreateSchema,
30
39
  updateSchema: dictionaryEntryCommandUpdateSchema,
31
- ensureScope: ensureDictionaryEntryScope,
40
+ ensureScope,
32
41
  duplicateError: 'An entry with this value already exists.',
33
42
  resolveDictionaryForCreate: async ({ em, ctx, parsed }) => {
34
43
  const dictionary = await ensureDictionary(em, parsed.dictionaryId)
35
44
  const scope = { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId }
36
- ensureDictionaryEntryScope(ctx, scope)
45
+ ensureScope(ctx, scope)
37
46
  return { dictionary, scope }
38
47
  },
39
48
  resolveEntry: async ({ em, ctx, id }) => {
@@ -45,7 +54,7 @@ registerDictionaryEntryCommands({
45
54
  throw new CrudHttpError(404, { error: 'Dictionary not found' })
46
55
  }
47
56
  const scope = { tenantId: entry.tenantId, organizationId: entry.organizationId }
48
- ensureDictionaryEntryScope(ctx, scope)
57
+ ensureScope(ctx, scope)
49
58
  return { entry, dictionary: entry.dictionary, scope }
50
59
  },
51
60
  ensureDictionaryForUndo: async ({ em, snapshot }) => {
@@ -6,8 +6,7 @@ import {
6
6
  type ReorderDictionaryEntriesCommandInput,
7
7
  type SetDefaultDictionaryEntryCommandInput,
8
8
  } from '@open-mercato/core/modules/dictionaries/data/validators'
9
- import { ensureDictionaryEntryScope } from '@open-mercato/core/modules/dictionaries/commands/factory'
10
- import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
9
+ import { registerCommand, type CommandHandler, type CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
11
10
  import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
12
11
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
13
12
  import { emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
@@ -37,6 +36,17 @@ const dictionaryCrudEvents: CrudEventsConfig = {
37
36
  }),
38
37
  }
39
38
 
39
+ function ensureScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {
40
+ const tenantId = ctx.auth?.tenantId ?? null
41
+ if (tenantId && tenantId !== scope.tenantId) {
42
+ throw new CrudHttpError(403, { error: 'Forbidden' })
43
+ }
44
+ const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
45
+ if (organizationId && organizationId !== scope.organizationId) {
46
+ throw new CrudHttpError(403, { error: 'Forbidden' })
47
+ }
48
+ }
49
+
40
50
  async function requireDictionary(
41
51
  em: EntityManager,
42
52
  id: string,
@@ -74,7 +84,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
74
84
  async prepare(rawInput, ctx) {
75
85
  const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)
76
86
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
77
- ensureDictionaryEntryScope(ctx, scope)
87
+ ensureScope(ctx, scope)
78
88
  const em = (ctx.container.resolve('em') as EntityManager).fork()
79
89
  const ids = parsed.entries.map((e) => e.id)
80
90
  const existing = await findWithDecryption(
@@ -98,7 +108,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
98
108
  async execute(rawInput, ctx) {
99
109
  const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)
100
110
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
101
- ensureDictionaryEntryScope(ctx, scope)
111
+ ensureScope(ctx, scope)
102
112
 
103
113
  const em = (ctx.container.resolve('em') as EntityManager).fork()
104
114
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
@@ -191,7 +201,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
191
201
  const undo = extractUndoPayload<ReorderUndoPayload>(logEntry)
192
202
  const before = undo?.before ?? (logEntry?.snapshotBefore as ReorderSnapshot | null | undefined) ?? null
193
203
  if (!before) return
194
- ensureDictionaryEntryScope(ctx, before.scope)
204
+ ensureScope(ctx, before.scope)
195
205
 
196
206
  const em = (ctx.container.resolve('em') as EntityManager).fork()
197
207
  const ids = before.positions.map((p) => p.id)
@@ -255,7 +265,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
255
265
  async prepare(rawInput, ctx) {
256
266
  const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)
257
267
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
258
- ensureDictionaryEntryScope(ctx, scope)
268
+ ensureScope(ctx, scope)
259
269
  const em = (ctx.container.resolve('em') as EntityManager).fork()
260
270
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
261
271
  const previousDefaults = await findWithDecryption(
@@ -281,7 +291,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
281
291
  async execute(rawInput, ctx) {
282
292
  const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)
283
293
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
284
- ensureDictionaryEntryScope(ctx, scope)
294
+ ensureScope(ctx, scope)
285
295
 
286
296
  const em = (ctx.container.resolve('em') as EntityManager).fork()
287
297
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
@@ -389,7 +399,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
389
399
  const undo = extractUndoPayload<DefaultUndoPayload>(logEntry)
390
400
  const before = undo?.before ?? (logEntry?.snapshotBefore as DefaultSnapshot | null | undefined) ?? null
391
401
  if (!before) return
392
- ensureDictionaryEntryScope(ctx, before.scope)
402
+ ensureScope(ctx, before.scope)
393
403
 
394
404
  const em = (ctx.container.resolve('em') as EntityManager).fork()
395
405
  const dictionary = await requireDictionary(em, before.dictionaryId, before.scope)
@@ -12,7 +12,6 @@ import {
12
12
  } from '@open-mercato/shared/lib/commands'
13
13
  import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
14
14
  import { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'
15
- import { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
16
15
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
17
16
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
18
17
  import type { z } from 'zod'
@@ -23,11 +22,6 @@ type DictionaryScope = {
23
22
  organizationId: string
24
23
  }
25
24
 
26
- export function ensureDictionaryEntryScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {
27
- ensureTenantScope(ctx, scope.tenantId)
28
- ensureOrganizationScope(ctx, scope.organizationId)
29
- }
30
-
31
25
  export type DictionaryEntrySnapshot = {
32
26
  id: string
33
27
  dictionaryId: string
@@ -93,8 +87,8 @@ export type DictionaryEntryCommandConfig<TCreate, TUpdate> = {
93
87
  labels?: DictionaryEntryCommandLabels
94
88
  }
95
89
 
96
- export function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {
97
- return typeof fn === 'function' ? fn : ensureDictionaryEntryScope
90
+ function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {
91
+ return typeof fn === 'function' ? fn : () => {}
98
92
  }
99
93
 
100
94
  async function loadSnapshot(em: EntityManager, id: string): Promise<DictionaryEntrySnapshot | null> {
@@ -90,12 +90,9 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
90
90
  const [error, setError] = React.useState<string | null>(null)
91
91
  const multiId = React.useId()
92
92
  const inlineCreateId = React.useId()
93
- const inlineCreateHintId = React.useId()
94
- const defaultValueHintId = React.useId()
95
93
  const selectedId = typeof def?.configJson?.dictionaryId === 'string' ? def?.configJson?.dictionaryId : ''
96
94
  const inlineCreate = def?.configJson?.dictionaryInlineCreate !== false
97
95
  const isMulti = def?.configJson?.multi === true
98
- const inlineCreateDisabled = !selectedId || isMulti
99
96
  const errorLoadLabel = t('dictionaries.customFields.errorLoad', 'Failed to load dictionaries.')
100
97
 
101
98
  React.useEffect(() => {
@@ -184,65 +181,45 @@ function DictionaryFieldDefEditor({ def, onChange }: { def: { configJson?: Dicti
184
181
  </a>
185
182
  </div>
186
183
  ) : null}
187
- <div className="space-y-2">
188
- <div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:gap-x-4">
189
- <div className="inline-flex items-center gap-2 text-xs">
190
- <Checkbox
191
- id={multiId}
192
- checked={isMulti}
193
- onCheckedChange={(checked) => {
194
- const enabled = checked === true
195
- onChange({ multi: enabled, defaultValue: enabled ? undefined : def?.configJson?.defaultValue })
196
- }}
197
- disabled={!selectedId}
198
- />
199
- <label
200
- htmlFor={multiId}
201
- className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
202
- >
203
- {t('dictionaries.customFields.allowMultiple', 'Allow selecting multiple entries')}
204
- </label>
205
- </div>
206
- <div className="inline-flex items-center gap-2 text-xs">
207
- <Checkbox
208
- id={inlineCreateId}
209
- checked={!isMulti && inlineCreate}
210
- onCheckedChange={(checked) => onChange({ dictionaryInlineCreate: checked === true })}
211
- disabled={inlineCreateDisabled}
212
- aria-describedby={isMulti ? inlineCreateHintId : undefined}
213
- />
214
- <label
215
- htmlFor={inlineCreateId}
216
- className={!inlineCreateDisabled ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
217
- >
218
- {t('dictionaries.customFields.allowInlineCreate', 'Allow inline creation inside forms')}
219
- </label>
220
- </div>
221
- </div>
222
- {isMulti ? (
223
- <p id={inlineCreateHintId} className="text-xs text-muted-foreground">
224
- {t(
225
- 'dictionaries.customFields.inlineCreateSingleOnly',
226
- 'Inline creation is available for single-entry dictionary fields only.',
227
- )}
228
- </p>
229
- ) : null}
184
+ <div className="inline-flex items-center gap-2 text-xs">
185
+ <Checkbox
186
+ id={multiId}
187
+ checked={isMulti}
188
+ onCheckedChange={(checked) => {
189
+ const enabled = checked === true
190
+ onChange({ multi: enabled, defaultValue: enabled ? undefined : def?.configJson?.defaultValue })
191
+ }}
192
+ disabled={!selectedId}
193
+ />
194
+ <label
195
+ htmlFor={multiId}
196
+ className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
197
+ >
198
+ {t('dictionaries.customFields.allowMultiple', 'Allow selecting multiple entries')}
199
+ </label>
230
200
  </div>
201
+ {!isMulti ? (
202
+ <div className="inline-flex items-center gap-2 text-xs">
203
+ <Checkbox
204
+ id={inlineCreateId}
205
+ checked={inlineCreate}
206
+ onCheckedChange={(checked) => onChange({ dictionaryInlineCreate: checked === true })}
207
+ disabled={!selectedId}
208
+ />
209
+ <label
210
+ htmlFor={inlineCreateId}
211
+ className={selectedId ? 'cursor-pointer select-none' : 'cursor-not-allowed opacity-60'}
212
+ >
213
+ {t('dictionaries.customFields.allowInlineCreate', 'Allow inline creation inside forms')}
214
+ </label>
215
+ </div>
216
+ ) : null}
231
217
  {selectedId && !isMulti ? (
232
218
  <DictionaryDefaultSelector
233
219
  dictionaryId={selectedId}
234
220
  defaultValue={typeof def?.configJson?.defaultValue === 'string' ? def.configJson.defaultValue : ''}
235
221
  onChange={(value) => onChange({ defaultValue: value || undefined })}
236
222
  />
237
- ) : selectedId && isMulti ? (
238
- <div className="inline-flex items-center gap-2 text-xs">
239
- <p id={defaultValueHintId} className="text-xs text-muted-foreground">
240
- {t(
241
- 'dictionaries.customFields.defaultValueMultiUnavailable',
242
- 'Default values are not available for multi-select dictionary fields.',
243
- )}
244
- </p>
245
- </div>
246
223
  ) : null}
247
224
  </div>
248
225
  )
@@ -82,7 +82,6 @@
82
82
  "dictionaries.customFields.allowInlineCreate": "Inline-Erstellung in Formularen erlauben",
83
83
  "dictionaries.customFields.allowMultiple": "Mehrere Einträge auswählen erlauben",
84
84
  "dictionaries.customFields.defaultValue": "Standardwert",
85
- "dictionaries.customFields.defaultValueMultiUnavailable": "Standardwerte sind für Wörterbuchfelder mit Mehrfachauswahl nicht verfügbar.",
86
85
  "dictionaries.customFields.defaultValueNone": "Kein Standardwert",
87
86
  "dictionaries.customFields.defaultValueStale": "Standardeintrag nicht gefunden — er wurde möglicherweise gelöscht oder umbenannt.",
88
87
  "dictionaries.customFields.dictionaryLabel": "Quelle des Wörterbuchs",
@@ -90,7 +89,6 @@
90
89
  "dictionaries.customFields.empty": "Noch keine Wörterbücher verfügbar. Erstellen Sie zuerst eines.",
91
90
  "dictionaries.customFields.errorLoad": "Wörterbücher konnten nicht geladen werden.",
92
91
  "dictionaries.customFields.inactive": "inaktiv",
93
- "dictionaries.customFields.inlineCreateSingleOnly": "Inline-Erstellung ist nur für Wörterbuchfelder mit Einzelauswahl verfügbar.",
94
92
  "dictionaries.customFields.loading": "Wörterbücher werden geladen…",
95
93
  "dictionaries.customFields.manageLink": "Wörterbücher verwalten",
96
94
  "dictionaries.customFields.selectedHint": "Einträge aus diesem Wörterbuch füllen das Feld.",
@@ -82,7 +82,6 @@
82
82
  "dictionaries.customFields.allowInlineCreate": "Allow inline creation inside forms",
83
83
  "dictionaries.customFields.allowMultiple": "Allow selecting multiple entries",
84
84
  "dictionaries.customFields.defaultValue": "Default value",
85
- "dictionaries.customFields.defaultValueMultiUnavailable": "Default values are not available for multi-select dictionary fields.",
86
85
  "dictionaries.customFields.defaultValueNone": "No default",
87
86
  "dictionaries.customFields.defaultValueStale": "Default entry not found — it may have been deleted or renamed.",
88
87
  "dictionaries.customFields.dictionaryLabel": "Dictionary source",
@@ -90,7 +89,6 @@
90
89
  "dictionaries.customFields.empty": "No dictionaries available yet. Create one first.",
91
90
  "dictionaries.customFields.errorLoad": "Failed to load dictionaries.",
92
91
  "dictionaries.customFields.inactive": "inactive",
93
- "dictionaries.customFields.inlineCreateSingleOnly": "Inline creation is available for single-entry dictionary fields only.",
94
92
  "dictionaries.customFields.loading": "Loading dictionaries…",
95
93
  "dictionaries.customFields.manageLink": "Manage dictionaries",
96
94
  "dictionaries.customFields.selectedHint": "Entries from this dictionary populate the field.",
@@ -82,7 +82,6 @@
82
82
  "dictionaries.customFields.allowInlineCreate": "Permitir creación en línea dentro de los formularios",
83
83
  "dictionaries.customFields.allowMultiple": "Permitir seleccionar varias entradas",
84
84
  "dictionaries.customFields.defaultValue": "Valor predeterminado",
85
- "dictionaries.customFields.defaultValueMultiUnavailable": "Los valores predeterminados no están disponibles para campos de diccionario con selección múltiple.",
86
85
  "dictionaries.customFields.defaultValueNone": "Sin valor predeterminado",
87
86
  "dictionaries.customFields.defaultValueStale": "No se encontró la entrada predeterminada; puede haber sido eliminada o renombrada.",
88
87
  "dictionaries.customFields.dictionaryLabel": "Fuente del diccionario",
@@ -90,7 +89,6 @@
90
89
  "dictionaries.customFields.empty": "Aún no hay diccionarios disponibles. Crea uno primero.",
91
90
  "dictionaries.customFields.errorLoad": "No se pudieron cargar los diccionarios.",
92
91
  "dictionaries.customFields.inactive": "inactivo",
93
- "dictionaries.customFields.inlineCreateSingleOnly": "La creación en línea solo está disponible para campos de diccionario de una sola entrada.",
94
92
  "dictionaries.customFields.loading": "Cargando diccionarios…",
95
93
  "dictionaries.customFields.manageLink": "Administrar diccionarios",
96
94
  "dictionaries.customFields.selectedHint": "Las entradas de este diccionario rellenarán el campo.",
@@ -82,7 +82,6 @@
82
82
  "dictionaries.customFields.allowInlineCreate": "Zezwól na tworzenie w formularzach",
83
83
  "dictionaries.customFields.allowMultiple": "Zezwól na wybór wielu wpisów",
84
84
  "dictionaries.customFields.defaultValue": "Wartość domyślna",
85
- "dictionaries.customFields.defaultValueMultiUnavailable": "Wartości domyślne nie są dostępne dla wielokrotnego wyboru wpisów słownika.",
86
85
  "dictionaries.customFields.defaultValueNone": "Brak wartości domyślnej",
87
86
  "dictionaries.customFields.defaultValueStale": "Nie znaleziono domyślnego wpisu — mógł zostać usunięty albo przemianowany.",
88
87
  "dictionaries.customFields.dictionaryLabel": "Źródło słownika",
@@ -90,7 +89,6 @@
90
89
  "dictionaries.customFields.empty": "Brak dostępnych słowników. Najpierw utwórz jeden.",
91
90
  "dictionaries.customFields.errorLoad": "Nie udało się wczytać słowników.",
92
91
  "dictionaries.customFields.inactive": "nieaktywne",
93
- "dictionaries.customFields.inlineCreateSingleOnly": "Tworzenie wpisów w formularzu jest dostępne tylko dla pól z pojedynczym wyborem.",
94
92
  "dictionaries.customFields.loading": "Ładowanie słowników…",
95
93
  "dictionaries.customFields.manageLink": "Zarządzaj słownikami",
96
94
  "dictionaries.customFields.selectedHint": "Wpisy z tego słownika uzupełnią pole.",