@open-mercato/core 0.10.1-develop.7841.1.af87ac614d → 0.10.1-develop.7847.1.7d83dc5794

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 (36) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/api_keys/api/keys/route.js +15 -1
  3. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  4. package/dist/modules/auth/api/users/route.js +16 -1
  5. package/dist/modules/auth/api/users/route.js.map +2 -2
  6. package/dist/modules/auth/backend/users/page.js +13 -2
  7. package/dist/modules/auth/backend/users/page.js.map +2 -2
  8. package/dist/modules/catalog/commands/products.js +4 -2
  9. package/dist/modules/catalog/commands/products.js.map +2 -2
  10. package/dist/modules/directory/api/organization-branding/route.js +6 -9
  11. package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
  12. package/dist/modules/directory/backend/directory/branding/page.js +15 -3
  13. package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
  14. package/dist/modules/wms/api/listEnrichers.js +3 -1
  15. package/dist/modules/wms/api/listEnrichers.js.map +2 -2
  16. package/dist/modules/wms/index.js +1 -1
  17. package/dist/modules/wms/index.js.map +2 -2
  18. package/package.json +7 -7
  19. package/src/modules/api_keys/api/keys/route.ts +15 -1
  20. package/src/modules/api_keys/i18n/de.json +1 -0
  21. package/src/modules/api_keys/i18n/en.json +1 -0
  22. package/src/modules/api_keys/i18n/es.json +1 -0
  23. package/src/modules/api_keys/i18n/ko.json +1 -0
  24. package/src/modules/api_keys/i18n/pl.json +1 -0
  25. package/src/modules/auth/api/users/route.ts +18 -1
  26. package/src/modules/auth/backend/users/page.tsx +14 -2
  27. package/src/modules/auth/i18n/de.json +1 -0
  28. package/src/modules/auth/i18n/en.json +1 -0
  29. package/src/modules/auth/i18n/es.json +1 -0
  30. package/src/modules/auth/i18n/ko.json +1 -0
  31. package/src/modules/auth/i18n/pl.json +1 -0
  32. package/src/modules/catalog/commands/products.ts +4 -1
  33. package/src/modules/directory/api/organization-branding/route.ts +5 -8
  34. package/src/modules/directory/backend/directory/branding/page.tsx +15 -3
  35. package/src/modules/wms/api/listEnrichers.ts +5 -2
  36. package/src/modules/wms/index.ts +1 -1
@@ -21,6 +21,7 @@ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/u
21
21
  import { useT } from '@open-mercato/shared/lib/i18n/context'
22
22
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
23
23
  import { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'
24
+ import { ErrorMessage } from '@open-mercato/ui/backend/detail'
24
25
 
25
26
  type Row = {
26
27
  id: string
@@ -348,7 +349,7 @@ export default function UsersListPage() {
348
349
  return p.toString()
349
350
  }, [filterValues.name, page, search, organizationId, normalizedRoleIds])
350
351
 
351
- const { data: usersData, isLoading } = useQuery({
352
+ const { data: usersData, isLoading, isPending, isFetching, isError, errorUpdatedAt, refetch } = useQuery({
352
353
  queryKey: ['users', params, scopeVersion],
353
354
  queryFn: async () => {
354
355
  const call = await apiCall<{ items: Row[]; total: number; totalPages: number; totalIsCapped?: boolean; isSuperAdmin?: boolean }>(
@@ -361,6 +362,7 @@ export default function UsersListPage() {
361
362
  },
362
363
  })
363
364
 
365
+ const hasLoadError = isError || (isPending && errorUpdatedAt > 0)
364
366
  const rows = usersData?.items || []
365
367
  const total = usersData?.total || 0
366
368
  const totalPages = usersData?.totalPages || 1
@@ -459,7 +461,17 @@ export default function UsersListPage() {
459
461
  ]} />
460
462
  )}
461
463
  pagination={{ page, pageSize: 50, total, totalPages, totalIsCapped, onPageChange: setPage }}
462
- isLoading={isLoading}
464
+ isLoading={isLoading && !hasLoadError}
465
+ error={hasLoadError ? (
466
+ <ErrorMessage
467
+ label={t('auth.users.list.error.load', 'Failed to load users')}
468
+ action={(
469
+ <Button type="button" variant="outline" disabled={isFetching} onClick={() => { void refetch() }}>
470
+ {t('auth.users.list.actions.retry', 'Retry')}
471
+ </Button>
472
+ )}
473
+ />
474
+ ) : null}
463
475
  />
464
476
  </PageBody>
465
477
  {ConfirmDialogElement}
@@ -710,6 +710,7 @@
710
710
  "auth.users.form.loading": "Benutzerdaten werden geladen...",
711
711
  "auth.users.form.title.create": "Benutzer erstellen",
712
712
  "auth.users.form.title.edit": "Benutzer bearbeiten",
713
+ "auth.users.list.actions.retry": "Erneut versuchen",
713
714
  "auth.users.list.columns.email": "E-Mail",
714
715
  "auth.users.list.columns.name": "Anzeigename",
715
716
  "auth.users.list.columns.organization": "Organisation",
@@ -710,6 +710,7 @@
710
710
  "auth.users.form.loading": "Loading user data...",
711
711
  "auth.users.form.title.create": "Create User",
712
712
  "auth.users.form.title.edit": "Edit User",
713
+ "auth.users.list.actions.retry": "Retry",
713
714
  "auth.users.list.columns.email": "Email",
714
715
  "auth.users.list.columns.name": "Display name",
715
716
  "auth.users.list.columns.organization": "Organization",
@@ -710,6 +710,7 @@
710
710
  "auth.users.form.loading": "Cargando datos del usuario...",
711
711
  "auth.users.form.title.create": "Crear usuario",
712
712
  "auth.users.form.title.edit": "Editar usuario",
713
+ "auth.users.list.actions.retry": "Reintentar",
713
714
  "auth.users.list.columns.email": "Correo electrónico",
714
715
  "auth.users.list.columns.name": "Nombre visible",
715
716
  "auth.users.list.columns.organization": "Organización",
@@ -710,6 +710,7 @@
710
710
  "auth.users.form.loading": "사용자 데이터를 불러오는 중...",
711
711
  "auth.users.form.title.create": "사용자 생성",
712
712
  "auth.users.form.title.edit": "사용자 편집",
713
+ "auth.users.list.actions.retry": "다시 시도",
713
714
  "auth.users.list.columns.email": "이메일",
714
715
  "auth.users.list.columns.name": "표시 이름",
715
716
  "auth.users.list.columns.organization": "조직",
@@ -710,6 +710,7 @@
710
710
  "auth.users.form.loading": "Ładowanie danych użytkownika...",
711
711
  "auth.users.form.title.create": "Utwórz użytkownika",
712
712
  "auth.users.form.title.edit": "Edytuj użytkownika",
713
+ "auth.users.list.actions.retry": "Spróbuj ponownie",
713
714
  "auth.users.list.columns.email": "Email",
714
715
  "auth.users.list.columns.name": "Nazwa wyświetlana",
715
716
  "auth.users.list.columns.organization": "Organizacja",
@@ -688,6 +688,7 @@ function ensureSchemaName(
688
688
 
689
689
  async function assignOptionSchemaTemplate(
690
690
  em: EntityManager,
691
+ lookupEm: EntityManager,
691
692
  product: CatalogProduct,
692
693
  schema: CatalogProductOptionSchema,
693
694
  preferredName?: string | null,
@@ -703,7 +704,7 @@ async function assignOptionSchemaTemplate(
703
704
  });
704
705
  let template = product.optionSchemaTemplate ?? null;
705
706
  if (!template) {
706
- template = await em.findOne(CatalogOptionSchemaTemplate, {
707
+ template = await lookupEm.findOne(CatalogOptionSchemaTemplate, {
707
708
  organizationId: product.organizationId,
708
709
  tenantId: product.tenantId,
709
710
  code: templateCode,
@@ -1606,6 +1607,7 @@ const createProductCommand: CommandHandler<
1606
1607
  record.optionSchemaTemplate = optionSchemaTemplate;
1607
1608
  } else if (optionSchemaDefinition) {
1608
1609
  optionSchemaTemplate = await assignOptionSchemaTemplate(
1610
+ em,
1609
1611
  em,
1610
1612
  record,
1611
1613
  optionSchemaDefinition,
@@ -2029,6 +2031,7 @@ const updateProductCommand: CommandHandler<
2029
2031
  if (optionSchemaDefinition) {
2030
2032
  await assignOptionSchemaTemplate(
2031
2033
  em,
2034
+ lookupEm,
2032
2035
  record,
2033
2036
  optionSchemaDefinition,
2034
2037
  optionSchemaDefinition.name ?? parsed.title ?? record.title,
@@ -17,7 +17,7 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
17
17
  import { Organization } from '@open-mercato/core/modules/directory/data/entities'
18
18
  import { organizationUpdateSchema } from '@open-mercato/core/modules/directory/data/validators'
19
19
  import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
20
- import { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'
20
+ import { isExplicitlyEmptyOrganizationScope } from '@open-mercato/shared/lib/auth/organizationScope'
21
21
  import '@open-mercato/core/modules/directory/commands/organizations'
22
22
  import { createLogger } from '@open-mercato/shared/lib/logger'
23
23
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
@@ -82,15 +82,12 @@ async function resolveCurrentOrganization(req: Request) {
82
82
 
83
83
  const container = await createRequestContainer()
84
84
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
85
- let organizationId: string | null
86
- try {
87
- organizationId = resolveSingleOrganizationIdOrDeny(scope, auth)
88
- } catch (error) {
89
- if (isCrudHttpError(error)) {
90
- return { response: NextResponse.json(error.body, { status: error.status }) }
85
+ if (isExplicitlyEmptyOrganizationScope(scope)) {
86
+ return {
87
+ response: NextResponse.json({ error: translate('api.errors.forbidden', 'Forbidden') }, { status: 403 }),
91
88
  }
92
- throw error
93
89
  }
90
+ const organizationId = scope.selectedId
94
91
  const tenantId = scope.tenantId ?? auth.tenantId ?? null
95
92
  if (!organizationId || !tenantId) {
96
93
  return {
@@ -15,7 +15,8 @@ import { Button } from '@open-mercato/ui/primitives/button'
15
15
  import { Input } from '@open-mercato/ui/primitives/input'
16
16
  import { SwitchField } from '@open-mercato/ui/primitives/switch-field'
17
17
  import { useT } from '@open-mercato/shared/lib/i18n/context'
18
- import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
18
+ import { useOrganizationScopeDetail, useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
19
+ import { getCurrentOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/organizationEvents'
19
20
 
20
21
  type BrandingPayload = {
21
22
  organizationId: string
@@ -42,6 +43,7 @@ export default function OrganizationBrandingPage() {
42
43
  const t = useT()
43
44
  const queryClient = useQueryClient()
44
45
  const { organizationId, tenantId } = useOrganizationScopeDetail()
46
+ const scopeVersion = useOrganizationScopeVersion()
45
47
  const [logoUrl, setLogoUrl] = React.useState('')
46
48
  const [logoPreserveAspectRatio, setLogoPreserveAspectRatio] = React.useState(false)
47
49
  const [selectedFile, setSelectedFile] = React.useState<File | null>(null)
@@ -67,7 +69,8 @@ export default function OrganizationBrandingPage() {
67
69
  setLogoUrl(data?.logoUrl ?? '')
68
70
  setLogoPreserveAspectRatio(data?.logoPreserveAspectRatio ?? false)
69
71
  setSelectedFile(null)
70
- }, [data?.logoPreserveAspectRatio, data?.logoUrl, organizationId, tenantId])
72
+ if (fileInputRef.current) fileInputRef.current.value = ''
73
+ }, [scopeVersion, organizationId, tenantId, data?.organizationId, data?.logoPreserveAspectRatio, data?.logoUrl])
71
74
 
72
75
  React.useEffect(() => {
73
76
  if (!selectedFile || typeof URL === 'undefined') {
@@ -111,7 +114,13 @@ export default function OrganizationBrandingPage() {
111
114
  try {
112
115
  await runMutation({
113
116
  operation: async () => {
117
+ if (getCurrentOrganizationScopeVersion() !== scopeVersion) {
118
+ throw new Error('[internal] Organization scope changed during branding save')
119
+ }
114
120
  const uploadedLogoUrl = shouldUpload ? await uploadLogo(data.organizationId) : null
121
+ if (getCurrentOrganizationScopeVersion() !== scopeVersion) {
122
+ throw new Error('[internal] Organization scope changed during branding save')
123
+ }
115
124
  const resolvedLogoUrl = uploadedLogoUrl ?? nextLogoUrl ?? logoUrl.trim()
116
125
  const response = await withScopedApiRequestHeaders(
117
126
  buildOptimisticLockHeader(data.updatedAt),
@@ -142,12 +151,15 @@ export default function OrganizationBrandingPage() {
142
151
  hasUpload: shouldUpload,
143
152
  },
144
153
  })
154
+ if (getCurrentOrganizationScopeVersion() !== scopeVersion) return
145
155
  await queryClient.invalidateQueries({ queryKey: ['directory-organization-branding'] })
156
+ if (getCurrentOrganizationScopeVersion() !== scopeVersion) return
146
157
  window.dispatchEvent(new Event('om:refresh-sidebar'))
147
158
  setSelectedFile(null)
148
159
  if (fileInputRef.current) fileInputRef.current.value = ''
149
160
  flash(t('directory.branding.flash.saved', 'Organization branding updated'), 'success')
150
161
  } catch (err: unknown) {
162
+ if (getCurrentOrganizationScopeVersion() !== scopeVersion) return
151
163
  if (surfaceRecordConflict(err, t)) return
152
164
  const fallback = t('directory.branding.errors.save', 'Failed to update organization branding')
153
165
  const message = err instanceof Error ? err.message : fallback
@@ -155,7 +167,7 @@ export default function OrganizationBrandingPage() {
155
167
  } finally {
156
168
  setSaving(false)
157
169
  }
158
- }, [data, logoPreserveAspectRatio, logoUrl, queryClient, runMutation, selectedFile, t, uploadLogo])
170
+ }, [data, logoPreserveAspectRatio, logoUrl, queryClient, runMutation, scopeVersion, selectedFile, t, uploadLogo])
159
171
 
160
172
  const handleSubmit = React.useCallback((event: React.FormEvent<HTMLFormElement>) => {
161
173
  event.preventDefault()
@@ -277,7 +277,8 @@ const CUSTOMER_FIELDS = ['id', 'display_name']
277
277
  /**
278
278
  * Decorate list items with `vendor_name` when they carry a `vendor_id` column
279
279
  * referencing `customers.customer_entity` by UUID only (no ORM cross-module
280
- * relations). Mutates `payload.items` in place.
280
+ * relations). No-op when the optional `customers` module is not enabled.
281
+ * Mutates `payload.items` in place.
281
282
  */
282
283
  export async function attachVendorLabelsToListItems(
283
284
  payload: AnyListPayload,
@@ -285,11 +286,13 @@ export async function attachVendorLabelsToListItems(
285
286
  ): Promise<void> {
286
287
  const items = Array.isArray(payload?.items) ? payload.items : []
287
288
  if (items.length === 0) return
289
+ const customerEntityId = E.customers?.customer_entity
290
+ if (!customerEntityId) return
288
291
  const ids = uniqueIdsFromKeys(items, ['vendor_id'])
289
292
  if (ids.length === 0) return
290
293
  const map = (await batchLoadById(
291
294
  ctx,
292
- E.customers.customer_entity,
295
+ customerEntityId,
293
296
  ids,
294
297
  CUSTOMER_FIELDS,
295
298
  'vendor',
@@ -9,7 +9,7 @@ export const metadata: ModuleInfo = {
9
9
  author: 'Open Mercato Team',
10
10
  license: 'MIT',
11
11
  ejectable: true,
12
- requires: ['catalog', 'sales', 'feature_toggles'],
12
+ requires: ['catalog', 'customers', 'sales', 'feature_toggles'],
13
13
  }
14
14
 
15
15
  export { features } from './acl'