@open-mercato/core 0.6.7-develop.6623.1.01d02d34c3 → 0.6.7-develop.6630.1.c8a615e536

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 (29) hide show
  1. package/dist/modules/audit_logs/api/audit-logs/access/route.js +20 -12
  2. package/dist/modules/audit_logs/api/audit-logs/access/route.js.map +2 -2
  3. package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js +25 -17
  4. package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js.map +2 -2
  5. package/dist/modules/audit_logs/api/audit-logs/actions/route.js +9 -1
  6. package/dist/modules/audit_logs/api/audit-logs/actions/route.js.map +2 -2
  7. package/dist/modules/customers/api/dictionaries/[kind]/route.js +7 -2
  8. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  9. package/dist/modules/customers/components/formConfig.js +22 -9
  10. package/dist/modules/customers/components/formConfig.js.map +2 -2
  11. package/dist/modules/customers/lib/dictionaries.js +2 -0
  12. package/dist/modules/customers/lib/dictionaries.js.map +2 -2
  13. package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +22 -5
  14. package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +2 -2
  15. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +12 -1
  16. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  17. package/package.json +7 -7
  18. package/src/modules/audit_logs/api/audit-logs/access/route.ts +20 -12
  19. package/src/modules/audit_logs/api/audit-logs/actions/export/route.ts +25 -17
  20. package/src/modules/audit_logs/api/audit-logs/actions/route.ts +9 -1
  21. package/src/modules/customers/api/dictionaries/[kind]/route.ts +6 -1
  22. package/src/modules/customers/components/formConfig.tsx +26 -8
  23. package/src/modules/customers/lib/dictionaries.ts +1 -0
  24. package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +25 -4
  25. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +19 -2
  26. package/src/modules/entities/i18n/de.json +1 -0
  27. package/src/modules/entities/i18n/en.json +1 -0
  28. package/src/modules/entities/i18n/es.json +1 -0
  29. package/src/modules/entities/i18n/pl.json +1 -0
@@ -109,18 +109,26 @@ export async function GET(req: Request) {
109
109
  actorUserId = actorQuery
110
110
  }
111
111
 
112
- const list = await accessLogs.list({
113
- tenantId: auth.tenantId ?? undefined,
114
- organizationId: organizationId ?? undefined,
115
- actorUserId,
116
- resourceKind: resourceKind ?? undefined,
117
- accessType: accessType ?? undefined,
118
- page,
119
- pageSize,
120
- limit: url.searchParams.get('limit') ? parseNumber(url.searchParams.get('limit'), { min: 1, max: 200, fallback: pageSize }) : undefined,
121
- before,
122
- after,
123
- })
112
+ let list: Awaited<ReturnType<AccessLogService['list']>>
113
+ try {
114
+ list = await accessLogs.list({
115
+ tenantId: auth.tenantId ?? undefined,
116
+ organizationId: organizationId ?? undefined,
117
+ actorUserId,
118
+ resourceKind: resourceKind ?? undefined,
119
+ accessType: accessType ?? undefined,
120
+ page,
121
+ pageSize,
122
+ limit: url.searchParams.get('limit') ? parseNumber(url.searchParams.get('limit'), { min: 1, max: 200, fallback: pageSize }) : undefined,
123
+ before,
124
+ after,
125
+ })
126
+ } catch (err) {
127
+ if (err instanceof z.ZodError) {
128
+ return NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 })
129
+ }
130
+ throw err
131
+ }
124
132
 
125
133
  const displayMaps = await loadAuditLogDisplayMaps(em, {
126
134
  userIds: list.items.map((entry) => entry.actorUserId).filter((value): value is string => !!value),
@@ -158,23 +158,31 @@ export async function GET(req: Request) {
158
158
  }
159
159
  }
160
160
 
161
- const entriesResult = await actionLogs.list({
162
- tenantId: auth.tenantId ?? undefined,
163
- organizationId: organizationId ?? undefined,
164
- actorUserId,
165
- actorUserIds,
166
- resourceKind,
167
- resourceId,
168
- actionTypes,
169
- fieldNames,
170
- includeRelated,
171
- undoableOnly,
172
- sortField,
173
- sortDir,
174
- limit,
175
- before,
176
- after,
177
- })
161
+ let entriesResult: Awaited<ReturnType<ActionLogService['list']>>
162
+ try {
163
+ entriesResult = await actionLogs.list({
164
+ tenantId: auth.tenantId ?? undefined,
165
+ organizationId: organizationId ?? undefined,
166
+ actorUserId,
167
+ actorUserIds,
168
+ resourceKind,
169
+ resourceId,
170
+ actionTypes,
171
+ fieldNames,
172
+ includeRelated,
173
+ undoableOnly,
174
+ sortField,
175
+ sortDir,
176
+ limit,
177
+ before,
178
+ after,
179
+ })
180
+ } catch (err) {
181
+ if (err instanceof z.ZodError) {
182
+ return NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 })
183
+ }
184
+ throw err
185
+ }
178
186
  const entries = entriesResult.items
179
187
 
180
188
  const displayMaps = await loadAuditLogDisplayMaps(em, {
@@ -226,7 +226,15 @@ export async function GET(req: Request) {
226
226
  // includeTotal flag is retained for backward compatibility but page-based pagination
227
227
  // always returns total/totalPages from the list query's findAndCount.
228
228
  void includeTotal
229
- const list = await actionLogs.list(listQuery)
229
+ let list: Awaited<ReturnType<ActionLogService['list']>>
230
+ try {
231
+ list = await actionLogs.list(listQuery)
232
+ } catch (err) {
233
+ if (err instanceof z.ZodError) {
234
+ return NextResponse.json({ error: 'Validation failed', details: err.issues }, { status: 400 })
235
+ }
236
+ throw err
237
+ }
230
238
 
231
239
  const displayMaps = await loadAuditLogDisplayMaps(em, {
232
240
  userIds: list.items.map((entry: any) => entry.actorUserId).filter((value: any): value is string => !!value),
@@ -22,6 +22,7 @@ import {
22
22
  import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
23
23
  import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
24
24
  import { createLogger } from '@open-mercato/shared/lib/logger'
25
+ import { CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE } from '../../../lib/dictionaries'
25
26
 
26
27
  const logger = createLogger('customers')
27
28
 
@@ -55,7 +56,10 @@ export async function GET(req: Request, ctx: { params?: { kind?: string } }) {
55
56
  })
56
57
  const { kind, mappedKind } = mapDictionaryKind(ctx.params?.kind)
57
58
  if (!organizationId) {
58
- throw new CrudHttpError(400, { error: translate('customers.errors.organization_required', 'Organization context is required') })
59
+ throw new CrudHttpError(400, {
60
+ error: translate('customers.errors.organization_required', 'Organization context is required'),
61
+ code: CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE,
62
+ })
59
63
  }
60
64
  const settings = await loadCustomerSettings(em, { tenantId, organizationId })
61
65
  const sortMode = resolveDictionaryEntrySortMode(settings?.dictionarySortModes?.[kind])
@@ -292,6 +296,7 @@ const dictionaryListResponseSchema = z.object({
292
296
 
293
297
  const dictionaryErrorSchema = z.object({
294
298
  error: z.string(),
299
+ code: z.string().optional(),
295
300
  })
296
301
 
297
302
  export const openApi: OpenApiRouteDoc = {
@@ -40,6 +40,7 @@ import type {
40
40
  } from '@open-mercato/ui/backend/CrudForm'
41
41
  import {
42
42
  DictionaryEntrySelect,
43
+ DictionaryOptionsUnavailableError,
43
44
  type DictionarySelectLabels,
44
45
  } from '@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect'
45
46
  import { RolesSection } from './detail/RolesSection'
@@ -53,6 +54,7 @@ import {
53
54
  } from './detail/hooks/useCustomerDictionary'
54
55
  import {
55
56
  CUSTOMER_DICTIONARIES_MANAGE_HREF,
57
+ CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE,
56
58
  getCustomerDictionaryManageHref,
57
59
  type CustomerDictionaryKind,
58
60
  } from '../lib/dictionaries'
@@ -188,14 +190,30 @@ export function DictionarySelectField({
188
190
  )
189
191
 
190
192
  const fetchOptions = React.useCallback(async () => {
191
- const data = await ensureCustomerDictionary(queryClient, kind, scopeVersion)
192
- return data.entries.map((entry) => ({
193
- value: entry.value,
194
- label: entry.label,
195
- color: entry.color ?? null,
196
- icon: entry.icon ?? null,
197
- }))
198
- }, [kind, queryClient, scopeVersion])
193
+ try {
194
+ const data = await ensureCustomerDictionary(queryClient, kind, scopeVersion)
195
+ return data.entries.map((entry) => ({
196
+ value: entry.value,
197
+ label: entry.label,
198
+ color: entry.color ?? null,
199
+ icon: entry.icon ?? null,
200
+ }))
201
+ } catch (err) {
202
+ const responseError = err as { status?: unknown; code?: unknown } | null
203
+ if (
204
+ responseError?.status === 400 &&
205
+ responseError.code === CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE
206
+ ) {
207
+ const serverMessage = err instanceof Error ? err.message.trim() : ''
208
+ throw new DictionaryOptionsUnavailableError(
209
+ serverMessage.length
210
+ ? serverMessage
211
+ : translate('customers.errors.organization_required', 'Organization context is required'),
212
+ )
213
+ }
214
+ throw err
215
+ }
216
+ }, [kind, queryClient, scopeVersion, translate])
199
217
 
200
218
  const createOption = React.useCallback(
201
219
  async (input: { value: string; label?: string; color?: string | null; icon?: string | null }) => {
@@ -32,6 +32,7 @@ export type CustomerDictionaryDisplayEntry = DictionaryDisplayEntry
32
32
  export type CustomerDictionaryMap = DictionaryMap
33
33
 
34
34
  export const CUSTOMER_DICTIONARIES_MANAGE_HREF = '/backend/config/customers'
35
+ export const CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE = 'customer_dictionary_organization_required'
35
36
 
36
37
  export function getCustomerDictionarySettingsSectionId(kind: CustomerDictionaryKind) {
37
38
  return `customer-dictionary-${kind}`
@@ -31,6 +31,13 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
31
31
 
32
32
  const logger = createLogger('dictionaries').child({ component: 'DictionaryEntrySelect' })
33
33
 
34
+ export class DictionaryOptionsUnavailableError extends Error {
35
+ constructor(message: string) {
36
+ super(message)
37
+ this.name = 'DictionaryOptionsUnavailableError'
38
+ }
39
+ }
40
+
34
41
  const DEFAULT_APPEARANCE_LABELS: AppearanceSelectorLabels = {
35
42
  colorLabel: 'Color',
36
43
  colorHelp: 'Pick a highlight color for this entry.',
@@ -117,6 +124,7 @@ export function DictionaryEntrySelect({
117
124
  sortOptions = 'label_asc',
118
125
  showActiveAppearance = true,
119
126
  }: DictionaryEntrySelectProps) {
127
+ const unavailableMessageId = React.useId()
120
128
  const pathname = usePathname()
121
129
  const searchParams = useSearchParams()
122
130
  const [options, setOptions] = React.useState<DictionaryOption[]>([])
@@ -126,17 +134,24 @@ export function DictionaryEntrySelect({
126
134
  const [newValue, setNewValue] = React.useState('')
127
135
  const [newLabel, setNewLabel] = React.useState('')
128
136
  const [formError, setFormError] = React.useState<string | null>(null)
137
+ const [unavailableMessage, setUnavailableMessage] = React.useState<string | null>(null)
129
138
  const appearance = useAppearanceState(null, null)
130
139
 
131
140
  const loadOptions = React.useCallback(async () => {
132
141
  setLoading(true)
142
+ setUnavailableMessage(null)
133
143
  try {
134
144
  const items = await fetchOptions()
135
145
  setOptions(sortOptions === 'none' ? items : items.slice().sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })))
136
146
  } catch (err) {
137
- logger.error('Failed to fetch options', { err })
138
- flash(labels.errorLoad, 'error')
139
- setOptions([])
147
+ if (err instanceof DictionaryOptionsUnavailableError) {
148
+ setUnavailableMessage(err.message)
149
+ setOptions([])
150
+ } else {
151
+ logger.error('Failed to fetch options', { err })
152
+ flash(labels.errorLoad, 'error')
153
+ setOptions([])
154
+ }
140
155
  } finally {
141
156
  setLoading(false)
142
157
  }
@@ -276,7 +291,7 @@ export function DictionaryEntrySelect({
276
291
  return '⌘/Ctrl + Enter'
277
292
  }, [labels.saveShortcutHint])
278
293
 
279
- const disabled = disabledProp || loading || saving
294
+ const disabled = disabledProp || loading || saving || unavailableMessage !== null
280
295
  const manageLink = manageHref ?? '/backend/config/dictionaries'
281
296
  const returnTo = React.useMemo(() => {
282
297
  const query = searchParams?.toString() ?? ''
@@ -294,6 +309,11 @@ export function DictionaryEntrySelect({
294
309
 
295
310
  return (
296
311
  <div className="space-y-2">
312
+ {unavailableMessage ? (
313
+ <p id={unavailableMessageId} className="text-xs text-muted-foreground">
314
+ {unavailableMessage}
315
+ </p>
316
+ ) : null}
297
317
  <div className="flex items-center gap-2">
298
318
  <Select
299
319
  key={`dictionary-entry:${value ?? ''}:${optionsKey}`}
@@ -306,6 +326,7 @@ export function DictionaryEntrySelect({
306
326
  >
307
327
  <SelectTrigger
308
328
  id={id}
329
+ aria-describedby={unavailableMessage ? unavailableMessageId : undefined}
309
330
  className={selectClassName}
310
331
  title={activeOption?.label ?? undefined}
311
332
  >
@@ -18,7 +18,7 @@ import { loadGeneratedFieldRegistrations } from '@open-mercato/ui/backend/fields
18
18
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
19
19
  import { createCrudFormError, raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
20
20
  import { FieldDefinitionsEditor, type FieldDefinition, type FieldDefinitionError } from '@open-mercato/ui/backend/custom-fields/FieldDefinitionsEditor'
21
- import { useT } from '@open-mercato/shared/lib/i18n/context'
21
+ import { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'
22
22
  import {
23
23
  Dialog,
24
24
  DialogContent,
@@ -506,7 +506,15 @@ export default function EditDefinitionsPage({ params }: { params?: { entityId?:
506
506
  const definitionsGroup: CrudFormGroup = { id: 'definitions', title: t('entities.userEntities.edit.groups.definitions', 'Field Definitions'), column: 1, component: renderFieldDefinitions }
507
507
 
508
508
  const groups: CrudFormGroup[] = [
509
- { id: 'settings', title: t('entities.userEntities.edit.groups.settings', 'Entity Settings'), column: 1, fields: entitySource === 'custom' ? ['label','description','defaultEditor','showInSidebar','accessRestricted'] : ['label','description','defaultEditor'] },
509
+ {
510
+ id: 'settings',
511
+ title: t('entities.userEntities.edit.groups.settings', 'Entity Settings'),
512
+ column: 1,
513
+ description: getEntitySettingsNotice(entitySource, t),
514
+ fields: entitySource === 'custom'
515
+ ? ['label', 'description', 'defaultEditor', 'showInSidebar', 'accessRestricted']
516
+ : ['label', 'description', 'defaultEditor'],
517
+ },
510
518
  definitionsGroup,
511
519
  ]
512
520
 
@@ -657,6 +665,15 @@ export function shouldRegisterEntityMetadata(entitySource: EntitySource): boolea
657
665
  return entitySource === 'custom'
658
666
  }
659
667
 
668
+ const SYSTEM_ENTITY_SETTINGS_NOTICE_FALLBACK =
669
+ 'This is a system entity defined in code. Its label and description are managed in code and cannot be edited here — only the field definitions below are editable.'
670
+
671
+ export function getEntitySettingsNotice(entitySource: EntitySource, t: TranslateFn): string | undefined {
672
+ return shouldRegisterEntityMetadata(entitySource)
673
+ ? undefined
674
+ : t('entities.userEntities.form.systemEntityNotice', SYSTEM_ENTITY_SETTINGS_NOTICE_FALLBACK)
675
+ }
676
+
660
677
  export function buildDefinitionsBatchPayload(options: {
661
678
  entityId: string
662
679
  defs: Array<Pick<Def, 'key' | 'kind' | 'configJson' | 'isActive'>>
@@ -237,6 +237,7 @@
237
237
  "entities.userEntities.form.loading": "Wird geladen…",
238
238
  "entities.userEntities.form.showInSidebar.label": "In der Seitenleiste anzeigen",
239
239
  "entities.userEntities.form.submit": "Erstellen",
240
+ "entities.userEntities.form.systemEntityNotice": "Dies ist eine im Code definierte Systementität. Ihr Label und ihre Beschreibung werden im Code verwaltet und können hier nicht bearbeitet werden — nur die Felddefinitionen unten sind bearbeitbar.",
240
241
  "entities.userEntities.form.title": "Entität erstellen",
241
242
  "entities.userEntities.records.errors.deleteFailed": "Datensatz konnte nicht gelöscht werden",
242
243
  "entities.userEntities.records.errors.entityIdRequired": "Entitätskennung ist erforderlich",
@@ -237,6 +237,7 @@
237
237
  "entities.userEntities.form.loading": "Loading…",
238
238
  "entities.userEntities.form.showInSidebar.label": "Show in sidebar",
239
239
  "entities.userEntities.form.submit": "Create",
240
+ "entities.userEntities.form.systemEntityNotice": "This is a system entity defined in code. Its label and description are managed in code and cannot be edited here — only the field definitions below are editable.",
240
241
  "entities.userEntities.form.title": "Create Entity",
241
242
  "entities.userEntities.records.errors.deleteFailed": "Failed to delete record",
242
243
  "entities.userEntities.records.errors.entityIdRequired": "Entity identifier is required",
@@ -237,6 +237,7 @@
237
237
  "entities.userEntities.form.loading": "Cargando…",
238
238
  "entities.userEntities.form.showInSidebar.label": "Mostrar en la barra lateral",
239
239
  "entities.userEntities.form.submit": "Crear",
240
+ "entities.userEntities.form.systemEntityNotice": "Esta es una entidad de sistema definida en el código. Su etiqueta y descripción se gestionan en el código y no se pueden editar aquí — solo las definiciones de campos siguientes son editables.",
240
241
  "entities.userEntities.form.title": "Crear entidad",
241
242
  "entities.userEntities.records.errors.deleteFailed": "No se pudo eliminar el registro",
242
243
  "entities.userEntities.records.errors.entityIdRequired": "Se requiere el identificador de la entidad",
@@ -237,6 +237,7 @@
237
237
  "entities.userEntities.form.loading": "Ładowanie…",
238
238
  "entities.userEntities.form.showInSidebar.label": "Pokaż w panelu bocznym",
239
239
  "entities.userEntities.form.submit": "Utwórz",
240
+ "entities.userEntities.form.systemEntityNotice": "To jest encja systemowa zdefiniowana w kodzie. Jej etykieta i opis są zarządzane w kodzie i nie można ich tutaj edytować — edytowalne są tylko definicje pól poniżej.",
240
241
  "entities.userEntities.form.title": "Utwórz encję",
241
242
  "entities.userEntities.records.errors.deleteFailed": "Nie udało się usunąć rekordu",
242
243
  "entities.userEntities.records.errors.entityIdRequired": "Wymagany jest identyfikator encji",