@open-mercato/core 0.6.7-develop.6622.1.4d29ddaca4 → 0.6.7-develop.6628.1.f9e4ecf70f
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/dist/modules/audit_logs/api/audit-logs/access/route.js +20 -12
- package/dist/modules/audit_logs/api/audit-logs/access/route.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js +25 -17
- package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/route.js +9 -1
- package/dist/modules/audit_logs/api/audit-logs/actions/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +7 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +22 -9
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/lib/dictionaries.js +2 -0
- package/dist/modules/customers/lib/dictionaries.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +22 -5
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js +12 -1
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/api/audit-logs/access/route.ts +20 -12
- package/src/modules/audit_logs/api/audit-logs/actions/export/route.ts +25 -17
- package/src/modules/audit_logs/api/audit-logs/actions/route.ts +9 -1
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +6 -1
- package/src/modules/customers/components/formConfig.tsx +26 -8
- package/src/modules/customers/lib/dictionaries.ts +1 -0
- package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +25 -4
- package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +19 -2
- package/src/modules/entities/i18n/de.json +1 -0
- package/src/modules/entities/i18n/en.json +1 -0
- package/src/modules/entities/i18n/es.json +1 -0
- 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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
{
|
|
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",
|