@open-mercato/ui 0.6.6-develop.6519.1.bdc91fb755 → 0.6.6-develop.6520.1.55df6df792
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/backend/CrudForm.js +7 -1
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js +110 -91
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +2 -2
- package/dist/primitives/date-picker.js +13 -6
- package/dist/primitives/date-picker.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/CrudForm.tsx +17 -1
- package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +122 -96
- package/src/primitives/date-picker.tsx +18 -6
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
SelectValue,
|
|
16
16
|
} from '../../primitives/select'
|
|
17
17
|
import { CUSTOM_FIELD_KINDS } from '@open-mercato/shared/modules/entities/kinds'
|
|
18
|
+
import { useOptionalT } from '@open-mercato/shared/lib/i18n/context'
|
|
18
19
|
import { FieldRegistry } from '../fields/registry'
|
|
19
20
|
import { slugify } from '@open-mercato/shared/lib/slugify'
|
|
20
21
|
import { useConfirmDialog } from '../confirm-dialog'
|
|
@@ -70,11 +71,6 @@ export type FieldDefinitionsEditorProps = {
|
|
|
70
71
|
translate?: (key: string, fallback: string) => string
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
const DEFAULT_KIND_OPTIONS = CUSTOM_FIELD_KINDS.map((k) => ({
|
|
74
|
-
value: k,
|
|
75
|
-
label: k.charAt(0).toUpperCase() + k.slice(1),
|
|
76
|
-
}))
|
|
77
|
-
|
|
78
74
|
const DEFAULT_VALUE_NONE = '__open_mercato_no_default__'
|
|
79
75
|
|
|
80
76
|
function getDefaultValueNoneOptionValue(options: CustomFieldOptionDto[]): string {
|
|
@@ -158,14 +154,10 @@ export function FieldDefinitionsEditor({
|
|
|
158
154
|
definitions,
|
|
159
155
|
errors,
|
|
160
156
|
deletedKeys,
|
|
161
|
-
kindOptions
|
|
157
|
+
kindOptions,
|
|
162
158
|
orderNotice,
|
|
163
|
-
infoNote
|
|
164
|
-
|
|
165
|
-
Supported kinds: text, multiline, integer, float, boolean, select (with options/optionsUrl), currency (fixed currencies list), relation (with related entity and options URL).
|
|
166
|
-
</div>
|
|
167
|
-
),
|
|
168
|
-
addButtonLabel = 'Add Field',
|
|
159
|
+
infoNote,
|
|
160
|
+
addButtonLabel,
|
|
169
161
|
fieldsets = [],
|
|
170
162
|
activeFieldset = null,
|
|
171
163
|
onActiveFieldsetChange,
|
|
@@ -187,7 +179,33 @@ export function FieldDefinitionsEditor({
|
|
|
187
179
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
188
180
|
const dragIndex = React.useRef<number | null>(null)
|
|
189
181
|
const hasFieldsets = fieldsets.length > 0
|
|
190
|
-
const
|
|
182
|
+
const contextT = useOptionalT()
|
|
183
|
+
const t = React.useCallback(
|
|
184
|
+
(key: string, fallback: string) => {
|
|
185
|
+
if (translate) return translate(key, fallback)
|
|
186
|
+
if (contextT) return contextT(key, fallback)
|
|
187
|
+
return fallback
|
|
188
|
+
},
|
|
189
|
+
[translate, contextT],
|
|
190
|
+
)
|
|
191
|
+
const resolvedKindOptions = React.useMemo(
|
|
192
|
+
() =>
|
|
193
|
+
kindOptions ??
|
|
194
|
+
CUSTOM_FIELD_KINDS.map((kind) => ({
|
|
195
|
+
value: kind,
|
|
196
|
+
label: t(`entities.customFields.editor.kindOption.${kind}`, kind.charAt(0).toUpperCase() + kind.slice(1)),
|
|
197
|
+
})),
|
|
198
|
+
[kindOptions, t],
|
|
199
|
+
)
|
|
200
|
+
const resolvedInfoNote = infoNote !== undefined ? infoNote : (
|
|
201
|
+
<div className="text-xs text-muted-foreground mt-2">
|
|
202
|
+
{t(
|
|
203
|
+
'entities.customFields.editor.supportedKindsNote',
|
|
204
|
+
'Supported kinds: text, multiline, integer, float, boolean, select (with options/optionsUrl), currency (fixed currencies list), relation (with related entity and options URL).',
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
)
|
|
208
|
+
const resolvedAddButtonLabel = addButtonLabel ?? t('entities.customFields.editor.addField', 'Add Field')
|
|
191
209
|
const resolvedActiveFieldset = React.useMemo(() => {
|
|
192
210
|
if (!hasFieldsets) return activeFieldset ?? null
|
|
193
211
|
if (activeFieldset === null) return null
|
|
@@ -237,7 +255,7 @@ export function FieldDefinitionsEditor({
|
|
|
237
255
|
const handleAddFieldset = () => {
|
|
238
256
|
if (!onFieldsetsChange) return
|
|
239
257
|
const code = ensureUniqueFieldsetCode(`fieldset_${fieldsets.length + 1}`, fieldsets)
|
|
240
|
-
const nextFieldsets = [...fieldsets, { code, label: 'New fieldset', icon: 'layers' }]
|
|
258
|
+
const nextFieldsets = [...fieldsets, { code, label: t('entities.customFields.editor.newFieldsetLabel', 'New fieldset'), icon: 'layers' }]
|
|
241
259
|
onFieldsetsChange(nextFieldsets)
|
|
242
260
|
onActiveFieldsetChange?.(code)
|
|
243
261
|
}
|
|
@@ -246,8 +264,8 @@ export function FieldDefinitionsEditor({
|
|
|
246
264
|
if (!onFieldsetsChange) return
|
|
247
265
|
if (!resolvedActiveFieldset) return
|
|
248
266
|
const confirmed = await confirm({
|
|
249
|
-
title:
|
|
250
|
-
text: 'This will move its fields to Unassigned.',
|
|
267
|
+
title: t('entities.customFields.editor.deleteFieldsetTitle', 'Delete fieldset "{code}"?').replace('{code}', resolvedActiveFieldset),
|
|
268
|
+
text: t('entities.customFields.editor.deleteFieldsetText', 'This will move its fields to Unassigned.'),
|
|
251
269
|
variant: 'destructive',
|
|
252
270
|
})
|
|
253
271
|
if (!confirmed) return
|
|
@@ -309,13 +327,13 @@ export function FieldDefinitionsEditor({
|
|
|
309
327
|
{hasFieldsets ? (
|
|
310
328
|
<div className="rounded border bg-card p-3 space-y-3">
|
|
311
329
|
<div className="flex flex-wrap items-center gap-2">
|
|
312
|
-
<label className="text-xs font-medium text-muted-foreground">Fieldset</label>
|
|
330
|
+
<label className="text-xs font-medium text-muted-foreground">{t('entities.customFields.fieldsetSelectorLabel', 'Fieldset')}</label>
|
|
313
331
|
<Select
|
|
314
332
|
value={resolvedActiveFieldset || undefined}
|
|
315
333
|
onValueChange={(value) => handleActiveFieldsetChange(value ?? '')}
|
|
316
334
|
>
|
|
317
335
|
<SelectTrigger size="sm" className="w-auto min-w-[10rem]">
|
|
318
|
-
<SelectValue placeholder=
|
|
336
|
+
<SelectValue placeholder={t('entities.customFields.editor.unassignedFields', 'Unassigned fields')} />
|
|
319
337
|
</SelectTrigger>
|
|
320
338
|
<SelectContent>
|
|
321
339
|
{fieldsets.map((fs) => (
|
|
@@ -331,7 +349,7 @@ export function FieldDefinitionsEditor({
|
|
|
331
349
|
onClick={handleAddFieldset}
|
|
332
350
|
className="text-xs"
|
|
333
351
|
>
|
|
334
|
-
<Plus className="h-3.5 w-3.5" /> Add
|
|
352
|
+
<Plus className="h-3.5 w-3.5" /> {t('entities.customFields.editor.add', 'Add')}
|
|
335
353
|
</Button>
|
|
336
354
|
<Button
|
|
337
355
|
variant="outline"
|
|
@@ -340,12 +358,12 @@ export function FieldDefinitionsEditor({
|
|
|
340
358
|
disabled={!resolvedActiveFieldset}
|
|
341
359
|
className="text-xs"
|
|
342
360
|
>
|
|
343
|
-
<Trash2 className="h-3.5 w-3.5" /> Delete
|
|
361
|
+
<Trash2 className="h-3.5 w-3.5" /> {t('entities.customFields.editor.delete', 'Delete')}
|
|
344
362
|
</Button>
|
|
345
363
|
</div>
|
|
346
364
|
{resolvedActiveFieldset && activeFieldsetConfig ? (
|
|
347
365
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
|
348
|
-
<FormField label=
|
|
366
|
+
<FormField label={t('entities.customFields.editor.code', 'Code')}>
|
|
349
367
|
<Input
|
|
350
368
|
size="sm"
|
|
351
369
|
inputClassName="font-mono"
|
|
@@ -353,14 +371,14 @@ export function FieldDefinitionsEditor({
|
|
|
353
371
|
onChange={(event) => handleFieldsetCodeInput(activeFieldsetConfig.code, event.target.value)}
|
|
354
372
|
/>
|
|
355
373
|
</FormField>
|
|
356
|
-
<FormField label=
|
|
374
|
+
<FormField label={t('entities.customFields.editor.label', 'Label')}>
|
|
357
375
|
<Input
|
|
358
376
|
size="sm"
|
|
359
377
|
value={activeFieldsetConfig.label}
|
|
360
378
|
onChange={(event) => handleFieldsetPatch(activeFieldsetConfig.code, { label: event.target.value })}
|
|
361
379
|
/>
|
|
362
380
|
</FormField>
|
|
363
|
-
<FormField label=
|
|
381
|
+
<FormField label={t('entities.customFields.editor.icon', 'Icon')}>
|
|
364
382
|
<Select
|
|
365
383
|
value={activeFieldsetConfig.icon || undefined}
|
|
366
384
|
onValueChange={(value) =>
|
|
@@ -370,18 +388,18 @@ export function FieldDefinitionsEditor({
|
|
|
370
388
|
}
|
|
371
389
|
>
|
|
372
390
|
<SelectTrigger size="sm">
|
|
373
|
-
<SelectValue placeholder=
|
|
391
|
+
<SelectValue placeholder={t('entities.customFields.editor.defaultOption', 'Default')} />
|
|
374
392
|
</SelectTrigger>
|
|
375
393
|
<SelectContent>
|
|
376
394
|
{FIELDSET_ICON_OPTIONS.map((option) => (
|
|
377
395
|
<SelectItem key={option.value} value={option.value}>
|
|
378
|
-
{option.label}
|
|
396
|
+
{t(`entities.customFields.editor.iconOption.${option.value}`, option.label)}
|
|
379
397
|
</SelectItem>
|
|
380
398
|
))}
|
|
381
399
|
</SelectContent>
|
|
382
400
|
</Select>
|
|
383
401
|
</FormField>
|
|
384
|
-
<FormField label=
|
|
402
|
+
<FormField label={t('entities.customFields.editor.description', 'Description')}>
|
|
385
403
|
<Input
|
|
386
404
|
size="sm"
|
|
387
405
|
value={activeFieldsetConfig.description ?? ''}
|
|
@@ -396,18 +414,18 @@ export function FieldDefinitionsEditor({
|
|
|
396
414
|
disabled={!canToggleSingleFieldset}
|
|
397
415
|
checked={singleFieldsetChecked}
|
|
398
416
|
onCheckedChange={(checked) => onSingleFieldsetPerRecordChange?.(checked === true)}
|
|
399
|
-
label=
|
|
417
|
+
label={t('entities.customFields.editor.singleFieldsetPerEntity', 'Single fieldset per entity')}
|
|
400
418
|
containerClassName="text-xs"
|
|
401
419
|
contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
|
|
402
420
|
/>
|
|
403
421
|
{!canToggleSingleFieldset ? (
|
|
404
|
-
<span className="text-xs text-muted-foreground">(add at least two fieldsets to toggle)</span>
|
|
422
|
+
<span className="text-xs text-muted-foreground">{t('entities.customFields.editor.singleFieldsetToggleHint', '(add at least two fieldsets to toggle)')}</span>
|
|
405
423
|
) : null}
|
|
406
424
|
</div>
|
|
407
425
|
</div>
|
|
408
426
|
) : (
|
|
409
427
|
<div className="rounded border border-dashed bg-muted/30 p-4 text-sm text-muted-foreground flex flex-col gap-3">
|
|
410
|
-
<div>No fieldsets defined yet. Fieldsets let you group custom fields for different variants of the same entity (e.g., Fashion vs. Sport products)
|
|
428
|
+
<div>{t('entities.customFields.editor.noFieldsets', 'No fieldsets defined yet. Fieldsets let you group custom fields for different variants of the same entity (e.g., Fashion vs. Sport products).')}</div>
|
|
411
429
|
<div>
|
|
412
430
|
<Button
|
|
413
431
|
variant="outline"
|
|
@@ -415,7 +433,7 @@ export function FieldDefinitionsEditor({
|
|
|
415
433
|
onClick={handleAddFieldset}
|
|
416
434
|
>
|
|
417
435
|
<Plus className="h-4 w-4" />
|
|
418
|
-
Add first fieldset
|
|
436
|
+
{t('entities.customFields.editor.addFirstFieldset', 'Add first fieldset')}
|
|
419
437
|
</Button>
|
|
420
438
|
</div>
|
|
421
439
|
</div>
|
|
@@ -423,7 +441,9 @@ export function FieldDefinitionsEditor({
|
|
|
423
441
|
{orderNotice?.dirty && (
|
|
424
442
|
<div className="sticky top-0 z-sticky -mt-1 -mb-1">
|
|
425
443
|
<div className="inline-flex items-center gap-2 text-xs px-2 py-1 rounded border border-status-warning-border bg-status-warning-bg text-status-warning-text shadow-sm">
|
|
426
|
-
{orderNotice?.saving
|
|
444
|
+
{orderNotice?.saving
|
|
445
|
+
? t('entities.customFields.editor.savingOrder', 'Saving order…')
|
|
446
|
+
: (orderNotice?.message ?? t('entities.customFields.editor.reorderedSavingSoon', 'Reordered — saving soon'))}
|
|
427
447
|
</div>
|
|
428
448
|
</div>
|
|
429
449
|
)}
|
|
@@ -462,7 +482,7 @@ export function FieldDefinitionsEditor({
|
|
|
462
482
|
<FieldDefinitionCard
|
|
463
483
|
definition={definition}
|
|
464
484
|
error={errors?.[index]}
|
|
465
|
-
kindOptions={
|
|
485
|
+
kindOptions={resolvedKindOptions}
|
|
466
486
|
onChange={(next) => onDefinitionChange(index, next)}
|
|
467
487
|
onRemove={() => onRemoveField(index)}
|
|
468
488
|
allowFieldsetSelection={hasFieldsets}
|
|
@@ -482,12 +502,12 @@ export function FieldDefinitionsEditor({
|
|
|
482
502
|
size="sm"
|
|
483
503
|
onClick={onAddField}
|
|
484
504
|
>
|
|
485
|
-
<Plus className="h-4 w-4" /> {
|
|
505
|
+
<Plus className="h-4 w-4" /> {resolvedAddButtonLabel}
|
|
486
506
|
</Button>
|
|
487
|
-
{
|
|
507
|
+
{resolvedInfoNote}
|
|
488
508
|
{deletedKeys && deletedKeys.length > 0 && onRestoreField ? (
|
|
489
509
|
<div className="text-xs text-muted-foreground mt-2">
|
|
490
|
-
Restore deleted fields:{' '}
|
|
510
|
+
{t('entities.customFields.editor.restoreDeletedFields', 'Restore deleted fields:')}{' '}
|
|
491
511
|
{deletedKeys.map((key, idx) => (
|
|
492
512
|
<span key={key}>
|
|
493
513
|
<Button
|
|
@@ -669,7 +689,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
669
689
|
if (!currentFieldsetValue) return
|
|
670
690
|
const code = slugifyFieldsetCode(groupDraft.code || '')
|
|
671
691
|
if (!code) {
|
|
672
|
-
setGroupError('Group code is required.')
|
|
692
|
+
setGroupError(t('entities.customFields.editor.groupCodeRequired', 'Group code is required.'))
|
|
673
693
|
return
|
|
674
694
|
}
|
|
675
695
|
const group: FieldsetGroup = {
|
|
@@ -725,7 +745,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
725
745
|
const value = optionValueDraft.trim()
|
|
726
746
|
const label = optionLabelDraft.trim()
|
|
727
747
|
if (!value) {
|
|
728
|
-
setOptionFormError('Value is required')
|
|
748
|
+
setOptionFormError(t('entities.customFields.editor.valueRequired', 'Value is required'))
|
|
729
749
|
return
|
|
730
750
|
}
|
|
731
751
|
setOptionFormError(null)
|
|
@@ -749,30 +769,30 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
749
769
|
<span className="inline-flex items-center justify-center h-5 w-5 rounded hover:bg-muted cursor-grab active:cursor-grabbing">
|
|
750
770
|
<GripVertical className="h-4 w-4 opacity-70" />
|
|
751
771
|
</span>
|
|
752
|
-
Drag to reorder
|
|
772
|
+
{t('entities.customFields.editor.dragToReorder', 'Drag to reorder')}
|
|
753
773
|
</div>
|
|
754
774
|
<div className="flex items-center gap-3">
|
|
755
775
|
<CheckboxField
|
|
756
776
|
size="sm"
|
|
757
777
|
checked={local.isActive !== false}
|
|
758
778
|
onCheckedChange={(checked) => { apply({ isActive: checked === true }, true) }}
|
|
759
|
-
label=
|
|
779
|
+
label={t('entities.customFields.editor.active', 'Active')}
|
|
760
780
|
containerClassName="text-sm"
|
|
761
781
|
contentClassName="gap-1 [&_label]:text-sm [&_label]:font-normal"
|
|
762
782
|
/>
|
|
763
783
|
{onTranslate && (
|
|
764
|
-
<IconButton variant="outline" size="sm" onClick={onTranslate} aria-label=
|
|
784
|
+
<IconButton variant="outline" size="sm" onClick={onTranslate} aria-label={t('entities.customFields.editor.translateField', 'Translate field')}>
|
|
765
785
|
<Languages className="h-4 w-4" />
|
|
766
786
|
</IconButton>
|
|
767
787
|
)}
|
|
768
|
-
<IconButton variant="outline" size="sm" onClick={onRemove} aria-label=
|
|
788
|
+
<IconButton variant="outline" size="sm" onClick={onRemove} aria-label={t('entities.customFields.editor.removeField', 'Remove field')}>
|
|
769
789
|
<Trash2 className="h-4 w-4" />
|
|
770
790
|
</IconButton>
|
|
771
791
|
</div>
|
|
772
792
|
</div>
|
|
773
793
|
|
|
774
794
|
<div className="mt-3 grid grid-cols-1 md:grid-cols-12 gap-3 items-center">
|
|
775
|
-
<FormField label=
|
|
795
|
+
<FormField label={t('entities.customFields.editor.key', 'Key')} error={error?.key} className="md:col-span-6">
|
|
776
796
|
<Input
|
|
777
797
|
inputClassName="font-mono"
|
|
778
798
|
placeholder="snake_case"
|
|
@@ -781,7 +801,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
781
801
|
onBlur={commit}
|
|
782
802
|
/>
|
|
783
803
|
</FormField>
|
|
784
|
-
<FormField label=
|
|
804
|
+
<FormField label={t('entities.customFields.editor.kind', 'Kind')} error={error?.kind} className="md:col-span-6">
|
|
785
805
|
<Select
|
|
786
806
|
value={local.kind || undefined}
|
|
787
807
|
onValueChange={(value) => { apply({ kind: value ?? '' }, true) }}
|
|
@@ -803,13 +823,13 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
803
823
|
{allowFieldsetSelection ? (
|
|
804
824
|
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
805
825
|
<div>
|
|
806
|
-
<label className="text-xs">Assign to fieldset</label>
|
|
826
|
+
<label className="text-xs">{t('entities.customFields.editor.assignToFieldset', 'Assign to fieldset')}</label>
|
|
807
827
|
<Select
|
|
808
828
|
value={currentFieldsetValue || undefined}
|
|
809
829
|
onValueChange={(value) => handleFieldsetSelect(value ?? '')}
|
|
810
830
|
>
|
|
811
831
|
<SelectTrigger>
|
|
812
|
-
<SelectValue placeholder=
|
|
832
|
+
<SelectValue placeholder={t('entities.customFields.editor.unassigned', 'Unassigned')} />
|
|
813
833
|
</SelectTrigger>
|
|
814
834
|
<SelectContent>
|
|
815
835
|
{(fieldsets || []).map((fs) => (
|
|
@@ -823,7 +843,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
823
843
|
{currentFieldsetValue ? (
|
|
824
844
|
<div>
|
|
825
845
|
<div className="flex items-center justify-between">
|
|
826
|
-
<label className="text-xs">Group</label>
|
|
846
|
+
<label className="text-xs">{t('entities.customFields.editor.group', 'Group')}</label>
|
|
827
847
|
</div>
|
|
828
848
|
<div className="flex items-center gap-2 mt-1">
|
|
829
849
|
<Select
|
|
@@ -831,7 +851,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
831
851
|
onValueChange={(value) => handleGroupSelect(value ?? '')}
|
|
832
852
|
>
|
|
833
853
|
<SelectTrigger className="flex-1">
|
|
834
|
-
<SelectValue placeholder=
|
|
854
|
+
<SelectValue placeholder={t('entities.customFields.editor.noGroup', 'No group')} />
|
|
835
855
|
</SelectTrigger>
|
|
836
856
|
<SelectContent>
|
|
837
857
|
{groupOptions.map((group) => (
|
|
@@ -845,7 +865,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
845
865
|
variant="outline"
|
|
846
866
|
className="text-muted-foreground"
|
|
847
867
|
onClick={() => handleOpenGroupDialog()}
|
|
848
|
-
aria-label=
|
|
868
|
+
aria-label={t('entities.customFields.editor.createGroup', 'Create group')}
|
|
849
869
|
>
|
|
850
870
|
<Plus className="h-4 w-4" />
|
|
851
871
|
</IconButton>
|
|
@@ -853,7 +873,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
853
873
|
variant="outline"
|
|
854
874
|
className="text-muted-foreground"
|
|
855
875
|
onClick={() => handleOpenGroupDialog()}
|
|
856
|
-
aria-label=
|
|
876
|
+
aria-label={t('entities.customFields.editor.editGroups', 'Edit groups')}
|
|
857
877
|
>
|
|
858
878
|
<Cog className="h-4 w-4" />
|
|
859
879
|
</IconButton>
|
|
@@ -864,14 +884,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
864
884
|
) : null}
|
|
865
885
|
|
|
866
886
|
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
867
|
-
<FormField label=
|
|
887
|
+
<FormField label={t('entities.customFields.editor.label', 'Label')}>
|
|
868
888
|
<Input
|
|
869
889
|
value={typeof local.configJson?.label === 'string' ? local.configJson.label : ''}
|
|
870
890
|
onChange={(event) => apply({ configJson: { ...(local.configJson || {}), label: event.target.value } })}
|
|
871
891
|
onBlur={commit}
|
|
872
892
|
/>
|
|
873
893
|
</FormField>
|
|
874
|
-
<FormField label=
|
|
894
|
+
<FormField label={t('entities.customFields.editor.description', 'Description')}>
|
|
875
895
|
<Input
|
|
876
896
|
value={typeof local.configJson?.description === 'string' ? local.configJson.description : ''}
|
|
877
897
|
onChange={(event) => apply({ configJson: { ...(local.configJson || {}), description: event.target.value } })}
|
|
@@ -882,18 +902,18 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
882
902
|
{(local.kind === 'text' || local.kind === 'multiline') && (
|
|
883
903
|
<>
|
|
884
904
|
<div>
|
|
885
|
-
<label className="text-xs">Editor</label>
|
|
905
|
+
<label className="text-xs">{t('entities.customFields.editor.editor', 'Editor')}</label>
|
|
886
906
|
<Select
|
|
887
907
|
value={typeof local.configJson?.editor === 'string' && local.configJson.editor ? local.configJson.editor : undefined}
|
|
888
908
|
onValueChange={(value) => { apply({ configJson: { ...(local.configJson || {}), editor: value || undefined } }, true) }}
|
|
889
909
|
>
|
|
890
910
|
<SelectTrigger>
|
|
891
|
-
<SelectValue placeholder=
|
|
911
|
+
<SelectValue placeholder={t('entities.customFields.editor.defaultOption', 'Default')} />
|
|
892
912
|
</SelectTrigger>
|
|
893
913
|
<SelectContent>
|
|
894
|
-
<SelectItem value="markdown">Markdown (UIW)</SelectItem>
|
|
895
|
-
<SelectItem value="simpleMarkdown">Simple Markdown</SelectItem>
|
|
896
|
-
<SelectItem value="htmlRichText">HTML Rich Text</SelectItem>
|
|
914
|
+
<SelectItem value="markdown">{t('entities.customFields.editor.editorMarkdown', 'Markdown (UIW)')}</SelectItem>
|
|
915
|
+
<SelectItem value="simpleMarkdown">{t('entities.customFields.editor.editorSimpleMarkdown', 'Simple Markdown')}</SelectItem>
|
|
916
|
+
<SelectItem value="htmlRichText">{t('entities.customFields.editor.editorHtmlRichText', 'HTML Rich Text')}</SelectItem>
|
|
897
917
|
</SelectContent>
|
|
898
918
|
</Select>
|
|
899
919
|
</div>
|
|
@@ -903,13 +923,13 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
903
923
|
size="sm"
|
|
904
924
|
checked={!!local.configJson?.multi}
|
|
905
925
|
onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), multi: checked === true } }, true) }}
|
|
906
|
-
label=
|
|
926
|
+
label={t('entities.customFields.editor.multiple', 'Multiple')}
|
|
907
927
|
containerClassName="md:col-span-2"
|
|
908
928
|
contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
|
|
909
929
|
/>
|
|
910
930
|
{!!local.configJson?.multi && (
|
|
911
931
|
<div className="md:col-span-2">
|
|
912
|
-
<label className="text-xs">Multi-select input style</label>
|
|
932
|
+
<label className="text-xs">{t('entities.customFields.editor.multiSelectInputStyle', 'Multi-select input style')}</label>
|
|
913
933
|
<Select
|
|
914
934
|
value={local.configJson?.input === 'listbox' ? 'listbox' : 'default'}
|
|
915
935
|
onValueChange={(value) => {
|
|
@@ -923,8 +943,8 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
923
943
|
<SelectValue />
|
|
924
944
|
</SelectTrigger>
|
|
925
945
|
<SelectContent>
|
|
926
|
-
<SelectItem value="default">Default</SelectItem>
|
|
927
|
-
<SelectItem value="listbox">Listbox (searchable)</SelectItem>
|
|
946
|
+
<SelectItem value="default">{t('entities.customFields.editor.defaultOption', 'Default')}</SelectItem>
|
|
947
|
+
<SelectItem value="listbox">{t('entities.customFields.editor.listboxSearchable', 'Listbox (searchable)')}</SelectItem>
|
|
928
948
|
</SelectContent>
|
|
929
949
|
</Select>
|
|
930
950
|
</div>
|
|
@@ -936,7 +956,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
936
956
|
|
|
937
957
|
{local.kind === 'select' && (
|
|
938
958
|
<div className="md:col-span-6 space-y-3">
|
|
939
|
-
<label className="text-xs">Options</label>
|
|
959
|
+
<label className="text-xs">{t('entities.customFields.editor.options', 'Options')}</label>
|
|
940
960
|
<div className="space-y-2">
|
|
941
961
|
{resolvedOptions.length > 0 ? (
|
|
942
962
|
resolvedOptions.map((option, idx) => (
|
|
@@ -953,14 +973,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
953
973
|
size="xs"
|
|
954
974
|
className="text-destructive hover:text-destructive/80"
|
|
955
975
|
onClick={() => handleRemoveOption(idx)}
|
|
956
|
-
aria-label=
|
|
976
|
+
aria-label={t('entities.customFields.editor.removeOption', 'Remove option')}
|
|
957
977
|
>
|
|
958
978
|
<Trash2 className="h-3.5 w-3.5" />
|
|
959
979
|
</IconButton>
|
|
960
980
|
</div>
|
|
961
981
|
))
|
|
962
982
|
) : (
|
|
963
|
-
<span className="text-xs text-muted-foreground">No options defined
|
|
983
|
+
<span className="text-xs text-muted-foreground">{t('entities.customFields.editor.noOptionsDefined', 'No options defined.')}</span>
|
|
964
984
|
)}
|
|
965
985
|
</div>
|
|
966
986
|
<div className="flex justify-end">
|
|
@@ -971,7 +991,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
971
991
|
onClick={handleOpenOptionDialog}
|
|
972
992
|
>
|
|
973
993
|
<Plus className="h-3.5 w-3.5" />
|
|
974
|
-
Add option
|
|
994
|
+
{t('entities.customFields.editor.addOption', 'Add option')}
|
|
975
995
|
</Button>
|
|
976
996
|
</div>
|
|
977
997
|
</div>
|
|
@@ -979,7 +999,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
979
999
|
|
|
980
1000
|
{(local.kind === 'select' || local.kind === 'relation') && (
|
|
981
1001
|
<>
|
|
982
|
-
<FormField label=
|
|
1002
|
+
<FormField label={t('entities.customFields.editor.optionsUrl', 'Options URL')}>
|
|
983
1003
|
<Input
|
|
984
1004
|
placeholder="/api/..."
|
|
985
1005
|
value={typeof local.configJson?.optionsUrl === 'string' ? local.configJson.optionsUrl : ''}
|
|
@@ -988,7 +1008,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
988
1008
|
/>
|
|
989
1009
|
</FormField>
|
|
990
1010
|
{local.kind === 'relation' && (
|
|
991
|
-
<FormField label=
|
|
1011
|
+
<FormField label={t('entities.customFields.editor.relatedEntityId', 'Related Entity ID')}>
|
|
992
1012
|
<Input
|
|
993
1013
|
inputClassName="font-mono"
|
|
994
1014
|
placeholder="module:entity"
|
|
@@ -1015,7 +1035,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1015
1035
|
|
|
1016
1036
|
{local.kind === 'currency' && (
|
|
1017
1037
|
<div className="md:col-span-2">
|
|
1018
|
-
<label className="text-xs">Options source</label>
|
|
1038
|
+
<label className="text-xs">{t('entities.customFields.editor.optionsSource', 'Options source')}</label>
|
|
1019
1039
|
<div className="rounded border bg-muted px-2 py-1 text-xs text-muted-foreground">
|
|
1020
1040
|
/api/currencies/options
|
|
1021
1041
|
</div>
|
|
@@ -1023,9 +1043,9 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1023
1043
|
)}
|
|
1024
1044
|
|
|
1025
1045
|
{(local.kind === 'integer' || local.kind === 'float') && (
|
|
1026
|
-
<FormField label=
|
|
1046
|
+
<FormField label={t('entities.customFields.editor.unitsOptional', 'Units (optional)')} className="md:col-span-2">
|
|
1027
1047
|
<Input
|
|
1028
|
-
placeholder=
|
|
1048
|
+
placeholder={t('entities.customFields.editor.unitsPlaceholder', 'kg, cm, etc.')}
|
|
1029
1049
|
value={typeof local.configJson?.unit === 'string' ? local.configJson.unit : ''}
|
|
1030
1050
|
onChange={(event) => apply({ configJson: { ...(local.configJson || {}), unit: event.target.value } })}
|
|
1031
1051
|
onBlur={commit}
|
|
@@ -1036,7 +1056,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1036
1056
|
|
|
1037
1057
|
<div className="mt-3 pt-3 border-t">
|
|
1038
1058
|
<div className="flex items-center justify-between mb-2">
|
|
1039
|
-
<label className="text-sm font-medium">Validation rules</label>
|
|
1059
|
+
<label className="text-sm font-medium">{t('entities.customFields.editor.validationRules', 'Validation rules')}</label>
|
|
1040
1060
|
<Button
|
|
1041
1061
|
variant="outline"
|
|
1042
1062
|
size="sm"
|
|
@@ -1044,13 +1064,13 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1044
1064
|
onClick={() => {
|
|
1045
1065
|
apply((current) => {
|
|
1046
1066
|
const list = Array.isArray(current.configJson?.validation) ? [...current.configJson.validation] : []
|
|
1047
|
-
list.push({ rule: 'required', message: 'This field is required' } as any)
|
|
1067
|
+
list.push({ rule: 'required', message: t('entities.customFields.editor.ruleRequiredMessage', 'This field is required') } as any)
|
|
1048
1068
|
return { configJson: { ...(current.configJson || {}), validation: list } }
|
|
1049
1069
|
}, true)
|
|
1050
1070
|
}}
|
|
1051
1071
|
>
|
|
1052
1072
|
<Plus className="h-3.5 w-3.5" />
|
|
1053
|
-
Add rule
|
|
1073
|
+
{t('entities.customFields.editor.addRule', 'Add rule')}
|
|
1054
1074
|
</Button>
|
|
1055
1075
|
</div>
|
|
1056
1076
|
<div className="space-y-2">
|
|
@@ -1089,7 +1109,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1089
1109
|
</div>
|
|
1090
1110
|
<div className="md:col-span-4">
|
|
1091
1111
|
<Input
|
|
1092
|
-
placeholder={rule?.rule === 'regex' ? 'Pattern (e.g. ^[a-z]+$)' : (['lt','lte','gt','gte'].includes(rule?.rule) ? 'Number' : '—')}
|
|
1112
|
+
placeholder={rule?.rule === 'regex' ? t('entities.customFields.editor.rulePatternPlaceholder', 'Pattern (e.g. ^[a-z]+$)') : (['lt','lte','gt','gte'].includes(rule?.rule) ? t('entities.customFields.editor.ruleNumberPlaceholder', 'Number') : '—')}
|
|
1093
1113
|
value={rule?.param ?? ''}
|
|
1094
1114
|
onChange={(event) => {
|
|
1095
1115
|
const value = ['lt','lte','gt','gte'].includes(rule?.rule) ? Number(event.target.value) : event.target.value
|
|
@@ -1106,7 +1126,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1106
1126
|
</div>
|
|
1107
1127
|
<div className="md:col-span-4">
|
|
1108
1128
|
<Input
|
|
1109
|
-
placeholder=
|
|
1129
|
+
placeholder={t('entities.customFields.editor.ruleErrorMessagePlaceholder', 'Error message')}
|
|
1110
1130
|
value={rule?.message || ''}
|
|
1111
1131
|
onChange={(event) => {
|
|
1112
1132
|
const message = event.target.value
|
|
@@ -1124,7 +1144,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1124
1144
|
<IconButton
|
|
1125
1145
|
variant="outline"
|
|
1126
1146
|
size="sm"
|
|
1127
|
-
aria-label=
|
|
1147
|
+
aria-label={t('entities.customFields.editor.removeRule', 'Remove rule')}
|
|
1128
1148
|
onClick={() => {
|
|
1129
1149
|
apply((current) => {
|
|
1130
1150
|
const list = Array.isArray(current.configJson?.validation) ? [...current.configJson.validation] : []
|
|
@@ -1139,7 +1159,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1139
1159
|
</div>
|
|
1140
1160
|
))}
|
|
1141
1161
|
{(!Array.isArray(local.configJson?.validation) || local.configJson!.validation.length === 0) && (
|
|
1142
|
-
<div className="text-xs text-muted-foreground">No validation rules defined
|
|
1162
|
+
<div className="text-xs text-muted-foreground">{t('entities.customFields.editor.noValidationRules', 'No validation rules defined.')}</div>
|
|
1143
1163
|
)}
|
|
1144
1164
|
</div>
|
|
1145
1165
|
</div>
|
|
@@ -1303,11 +1323,11 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1303
1323
|
>
|
|
1304
1324
|
<DialogContent className="max-w-sm">
|
|
1305
1325
|
<DialogHeader>
|
|
1306
|
-
<DialogTitle>Add option</DialogTitle>
|
|
1307
|
-
<DialogDescription>Provide the stored value and optional label shown to users
|
|
1326
|
+
<DialogTitle>{t('entities.customFields.editor.addOption', 'Add option')}</DialogTitle>
|
|
1327
|
+
<DialogDescription>{t('entities.customFields.editor.addOptionDescription', 'Provide the stored value and optional label shown to users.')}</DialogDescription>
|
|
1308
1328
|
</DialogHeader>
|
|
1309
1329
|
<div className="space-y-4 py-2">
|
|
1310
|
-
<FormField label=
|
|
1330
|
+
<FormField label={t('entities.customFields.editor.value', 'Value')} error={optionFormError ?? undefined}>
|
|
1311
1331
|
<Input
|
|
1312
1332
|
inputClassName="font-mono"
|
|
1313
1333
|
placeholder="unique_value"
|
|
@@ -1318,9 +1338,9 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1318
1338
|
}}
|
|
1319
1339
|
/>
|
|
1320
1340
|
</FormField>
|
|
1321
|
-
<FormField label=
|
|
1341
|
+
<FormField label={t('entities.customFields.editor.label', 'Label')}>
|
|
1322
1342
|
<Input
|
|
1323
|
-
placeholder=
|
|
1343
|
+
placeholder={t('entities.customFields.editor.optionLabelPlaceholder', 'Label shown to users (optional)')}
|
|
1324
1344
|
value={optionLabelDraft}
|
|
1325
1345
|
onChange={(event) => setOptionLabelDraft(event.target.value)}
|
|
1326
1346
|
/>
|
|
@@ -1328,10 +1348,10 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1328
1348
|
</div>
|
|
1329
1349
|
<DialogFooter>
|
|
1330
1350
|
<Button variant="outline" size="sm" onClick={handleCloseOptionDialog}>
|
|
1331
|
-
Cancel
|
|
1351
|
+
{t('entities.customFields.editor.cancel', 'Cancel')}
|
|
1332
1352
|
</Button>
|
|
1333
1353
|
<Button size="sm" onClick={handleAddOption}>
|
|
1334
|
-
Add option
|
|
1354
|
+
{t('entities.customFields.editor.addOption', 'Add option')}
|
|
1335
1355
|
</Button>
|
|
1336
1356
|
</DialogFooter>
|
|
1337
1357
|
</DialogContent>
|
|
@@ -1349,13 +1369,19 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1349
1369
|
>
|
|
1350
1370
|
<DialogContent className="max-w-md">
|
|
1351
1371
|
<DialogHeader>
|
|
1352
|
-
<DialogTitle>
|
|
1372
|
+
<DialogTitle>
|
|
1373
|
+
{editingGroupCode
|
|
1374
|
+
? t('entities.customFields.editor.editGroup', 'Edit group')
|
|
1375
|
+
: t('entities.customFields.editor.newGroup', 'New group')}
|
|
1376
|
+
</DialogTitle>
|
|
1353
1377
|
<DialogDescription>
|
|
1354
|
-
{editingGroupCode
|
|
1378
|
+
{editingGroupCode
|
|
1379
|
+
? t('entities.customFields.editor.editGroupDescription', 'Update the selected group for this fieldset.')
|
|
1380
|
+
: t('entities.customFields.editor.newGroupDescription', 'Add a reusable group for this fieldset.')}
|
|
1355
1381
|
</DialogDescription>
|
|
1356
1382
|
</DialogHeader>
|
|
1357
1383
|
<div className="space-y-3">
|
|
1358
|
-
<FormField label=
|
|
1384
|
+
<FormField label={t('entities.customFields.editor.groupCode', 'Group code')} error={groupError ?? undefined} disabled={!!editingGroupCode}>
|
|
1359
1385
|
<Input
|
|
1360
1386
|
inputClassName="font-mono"
|
|
1361
1387
|
value={groupDraft.code}
|
|
@@ -1364,26 +1390,26 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1364
1390
|
if (groupError) setGroupError(null)
|
|
1365
1391
|
}}
|
|
1366
1392
|
disabled={!!editingGroupCode}
|
|
1367
|
-
placeholder=
|
|
1393
|
+
placeholder={t('entities.customFields.editor.groupCodePlaceholder', 'e.g. buying_committee')}
|
|
1368
1394
|
/>
|
|
1369
1395
|
</FormField>
|
|
1370
|
-
<FormField label=
|
|
1396
|
+
<FormField label={t('entities.customFields.editor.label', 'Label')}>
|
|
1371
1397
|
<Input
|
|
1372
1398
|
value={groupDraft.title}
|
|
1373
1399
|
onChange={(event) => setGroupDraft((prev) => ({ ...prev, title: event.target.value }))}
|
|
1374
|
-
placeholder=
|
|
1400
|
+
placeholder={t('entities.customFields.editor.groupLabelPlaceholder', 'Buying committee')}
|
|
1375
1401
|
/>
|
|
1376
1402
|
</FormField>
|
|
1377
|
-
<FormField label=
|
|
1403
|
+
<FormField label={t('entities.customFields.editor.hint', 'Hint')}>
|
|
1378
1404
|
<Input
|
|
1379
1405
|
value={groupDraft.hint}
|
|
1380
1406
|
onChange={(event) => setGroupDraft((prev) => ({ ...prev, hint: event.target.value }))}
|
|
1381
|
-
placeholder=
|
|
1407
|
+
placeholder={t('entities.customFields.editor.groupHintPlaceholder', 'Visible to merchandisers')}
|
|
1382
1408
|
/>
|
|
1383
1409
|
</FormField>
|
|
1384
1410
|
{currentFieldsetValue && groupOptions.length > 0 ? (
|
|
1385
1411
|
<div>
|
|
1386
|
-
<div className="text-xs font-medium mb-1">Existing groups</div>
|
|
1412
|
+
<div className="text-xs font-medium mb-1">{t('entities.customFields.editor.existingGroups', 'Existing groups')}</div>
|
|
1387
1413
|
<div className="space-y-2 max-h-40 overflow-y-auto">
|
|
1388
1414
|
{groupOptions.map((group) => (
|
|
1389
1415
|
<div key={group.code} className="flex items-center justify-between rounded border px-3 py-2 text-sm">
|
|
@@ -1400,7 +1426,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1400
1426
|
size="xs"
|
|
1401
1427
|
className="text-muted-foreground hover:text-foreground"
|
|
1402
1428
|
onClick={() => handleEditGroupEntry(group)}
|
|
1403
|
-
aria-label={
|
|
1429
|
+
aria-label={t('entities.customFields.editor.editGroupAria', 'Edit {code}').replace('{code}', group.code)}
|
|
1404
1430
|
>
|
|
1405
1431
|
<Pencil className="h-4 w-4" />
|
|
1406
1432
|
</IconButton>
|
|
@@ -1409,7 +1435,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1409
1435
|
size="xs"
|
|
1410
1436
|
className="text-destructive hover:text-destructive/80"
|
|
1411
1437
|
onClick={() => handleRemoveGroupEntry(group.code)}
|
|
1412
|
-
aria-label={
|
|
1438
|
+
aria-label={t('entities.customFields.editor.deleteGroupAria', 'Delete {code}').replace('{code}', group.code)}
|
|
1413
1439
|
>
|
|
1414
1440
|
<Trash2 className="h-4 w-4" />
|
|
1415
1441
|
</IconButton>
|
|
@@ -1422,10 +1448,10 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1422
1448
|
</div>
|
|
1423
1449
|
<DialogFooter>
|
|
1424
1450
|
<Button variant="outline" size="sm" onClick={() => setGroupDialogOpen(false)}>
|
|
1425
|
-
Cancel
|
|
1451
|
+
{t('entities.customFields.editor.cancel', 'Cancel')}
|
|
1426
1452
|
</Button>
|
|
1427
1453
|
<Button size="sm" onClick={handleGroupDialogSubmit}>
|
|
1428
|
-
Save group
|
|
1454
|
+
{t('entities.customFields.editor.saveGroup', 'Save group')}
|
|
1429
1455
|
</Button>
|
|
1430
1456
|
</DialogFooter>
|
|
1431
1457
|
</DialogContent>
|