@open-mercato/ui 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6245.1.2be3b151f8
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/DataTable.js +68 -38
- package/dist/backend/DataTable.js.map +3 -3
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js +336 -385
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +2 -2
- package/dist/primitives/icon-button.js +2 -0
- package/dist/primitives/icon-button.js.map +2 -2
- package/package.json +3 -3
- package/src/backend/DataTable.tsx +79 -39
- package/src/backend/__tests__/DataTable.perspectiveGuardedMutation.test.tsx +263 -0
- package/src/backend/__tests__/FieldDefinitionsEditor.test.tsx +17 -0
- package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +172 -181
- package/src/primitives/__tests__/icon-button.test.tsx +40 -0
- package/src/primitives/icon-button.tsx +4 -0
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Cog, GripVertical, Languages, Pencil, Plus, Trash2, X } from 'lucide-react'
|
|
5
5
|
import { Button } from '../../primitives/button'
|
|
6
|
+
import { CheckboxField } from '../../primitives/checkbox-field'
|
|
7
|
+
import { FormField } from '../../primitives/form-field'
|
|
6
8
|
import { IconButton } from '../../primitives/icon-button'
|
|
9
|
+
import { Input } from '../../primitives/input'
|
|
7
10
|
import {
|
|
8
11
|
Select,
|
|
9
12
|
SelectContent,
|
|
@@ -72,6 +75,17 @@ const DEFAULT_KIND_OPTIONS = CUSTOM_FIELD_KINDS.map((k) => ({
|
|
|
72
75
|
label: k.charAt(0).toUpperCase() + k.slice(1),
|
|
73
76
|
}))
|
|
74
77
|
|
|
78
|
+
const DEFAULT_VALUE_NONE = '__open_mercato_no_default__'
|
|
79
|
+
|
|
80
|
+
function getDefaultValueNoneOptionValue(options: CustomFieldOptionDto[]): string {
|
|
81
|
+
const optionValues = new Set(options.map((option) => String(option.value)))
|
|
82
|
+
let candidate = DEFAULT_VALUE_NONE
|
|
83
|
+
while (optionValues.has(candidate)) {
|
|
84
|
+
candidate = `${candidate}_`
|
|
85
|
+
}
|
|
86
|
+
return candidate
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
const FIELDSET_ICON_OPTIONS = [
|
|
76
90
|
{ value: 'layers', label: 'Layers' },
|
|
77
91
|
{ value: 'tag', label: 'Tag' },
|
|
@@ -331,24 +345,22 @@ export function FieldDefinitionsEditor({
|
|
|
331
345
|
</div>
|
|
332
346
|
{resolvedActiveFieldset && activeFieldsetConfig ? (
|
|
333
347
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
|
334
|
-
<
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
|
|
348
|
+
<FormField label="Code">
|
|
349
|
+
<Input
|
|
350
|
+
size="sm"
|
|
351
|
+
inputClassName="font-mono"
|
|
338
352
|
value={activeFieldsetConfig.code}
|
|
339
353
|
onChange={(event) => handleFieldsetCodeInput(activeFieldsetConfig.code, event.target.value)}
|
|
340
354
|
/>
|
|
341
|
-
</
|
|
342
|
-
<
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
className="border rounded w-full px-2 py-1 text-sm"
|
|
355
|
+
</FormField>
|
|
356
|
+
<FormField label="Label">
|
|
357
|
+
<Input
|
|
358
|
+
size="sm"
|
|
346
359
|
value={activeFieldsetConfig.label}
|
|
347
360
|
onChange={(event) => handleFieldsetPatch(activeFieldsetConfig.code, { label: event.target.value })}
|
|
348
361
|
/>
|
|
349
|
-
</
|
|
350
|
-
<
|
|
351
|
-
<label className="text-xs">Icon</label>
|
|
362
|
+
</FormField>
|
|
363
|
+
<FormField label="Icon">
|
|
352
364
|
<Select
|
|
353
365
|
value={activeFieldsetConfig.icon || undefined}
|
|
354
366
|
onValueChange={(value) =>
|
|
@@ -368,29 +380,28 @@ export function FieldDefinitionsEditor({
|
|
|
368
380
|
))}
|
|
369
381
|
</SelectContent>
|
|
370
382
|
</Select>
|
|
371
|
-
</
|
|
372
|
-
<
|
|
373
|
-
<
|
|
374
|
-
|
|
375
|
-
className="border rounded w-full px-2 py-1 text-sm"
|
|
383
|
+
</FormField>
|
|
384
|
+
<FormField label="Description">
|
|
385
|
+
<Input
|
|
386
|
+
size="sm"
|
|
376
387
|
value={activeFieldsetConfig.description ?? ''}
|
|
377
388
|
onChange={(event) => handleFieldsetPatch(activeFieldsetConfig.code, { description: event.target.value })}
|
|
378
389
|
/>
|
|
379
|
-
</
|
|
390
|
+
</FormField>
|
|
380
391
|
</div>
|
|
381
392
|
) : null}
|
|
382
|
-
<div className="flex items-center gap-2
|
|
383
|
-
<
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
393
|
+
<div className="flex items-center gap-2">
|
|
394
|
+
<CheckboxField
|
|
395
|
+
size="sm"
|
|
396
|
+
disabled={!canToggleSingleFieldset}
|
|
397
|
+
checked={singleFieldsetChecked}
|
|
398
|
+
onCheckedChange={(checked) => onSingleFieldsetPerRecordChange?.(checked === true)}
|
|
399
|
+
label="Single fieldset per entity"
|
|
400
|
+
containerClassName="text-xs"
|
|
401
|
+
contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
|
|
402
|
+
/>
|
|
392
403
|
{!canToggleSingleFieldset ? (
|
|
393
|
-
<span className="text-muted-foreground">(add at least two fieldsets to toggle)</span>
|
|
404
|
+
<span className="text-xs text-muted-foreground">(add at least two fieldsets to toggle)</span>
|
|
394
405
|
) : null}
|
|
395
406
|
</div>
|
|
396
407
|
</div>
|
|
@@ -411,7 +422,7 @@ export function FieldDefinitionsEditor({
|
|
|
411
422
|
)}
|
|
412
423
|
{orderNotice?.dirty && (
|
|
413
424
|
<div className="sticky top-0 z-sticky -mt-1 -mb-1">
|
|
414
|
-
<div className="inline-flex items-center gap-2 text-xs px-2 py-1 rounded border bg-
|
|
425
|
+
<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">
|
|
415
426
|
{orderNotice?.saving ? 'Saving order…' : (orderNotice?.message ?? 'Reordered — saving soon')}
|
|
416
427
|
</div>
|
|
417
428
|
</div>
|
|
@@ -481,7 +492,7 @@ export function FieldDefinitionsEditor({
|
|
|
481
492
|
<span key={key}>
|
|
482
493
|
<Button
|
|
483
494
|
variant="link"
|
|
484
|
-
className="h-auto p-0
|
|
495
|
+
className="h-auto p-0"
|
|
485
496
|
onClick={() => onRestoreField(key)}
|
|
486
497
|
>
|
|
487
498
|
{key}
|
|
@@ -741,9 +752,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
741
752
|
Drag to reorder
|
|
742
753
|
</div>
|
|
743
754
|
<div className="flex items-center gap-3">
|
|
744
|
-
<
|
|
745
|
-
|
|
746
|
-
|
|
755
|
+
<CheckboxField
|
|
756
|
+
size="sm"
|
|
757
|
+
checked={local.isActive !== false}
|
|
758
|
+
onCheckedChange={(checked) => { apply({ isActive: checked === true }, true) }}
|
|
759
|
+
label="Active"
|
|
760
|
+
containerClassName="text-sm"
|
|
761
|
+
contentClassName="gap-1 [&_label]:text-sm [&_label]:font-normal"
|
|
762
|
+
/>
|
|
747
763
|
{onTranslate && (
|
|
748
764
|
<IconButton variant="outline" size="sm" onClick={onTranslate} aria-label="Translate field">
|
|
749
765
|
<Languages className="h-4 w-4" />
|
|
@@ -756,24 +772,21 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
756
772
|
</div>
|
|
757
773
|
|
|
758
774
|
<div className="mt-3 grid grid-cols-1 md:grid-cols-12 gap-3 items-center">
|
|
759
|
-
<
|
|
760
|
-
<
|
|
761
|
-
|
|
762
|
-
className={`rounded w-full px-2 py-1 text-sm font-mono ${error?.key ? 'border-red-500 border' : 'border'}`}
|
|
775
|
+
<FormField label="Key" error={error?.key} className="md:col-span-6">
|
|
776
|
+
<Input
|
|
777
|
+
inputClassName="font-mono"
|
|
763
778
|
placeholder="snake_case"
|
|
764
779
|
value={local.key}
|
|
765
780
|
onChange={(event) => apply({ key: event.target.value })}
|
|
766
781
|
onBlur={commit}
|
|
767
782
|
/>
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
<div className="md:col-span-6">
|
|
771
|
-
<label className="text-xs">Kind</label>
|
|
783
|
+
</FormField>
|
|
784
|
+
<FormField label="Kind" error={error?.kind} className="md:col-span-6">
|
|
772
785
|
<Select
|
|
773
786
|
value={local.kind || undefined}
|
|
774
787
|
onValueChange={(value) => { apply({ kind: value ?? '' }, true) }}
|
|
775
788
|
>
|
|
776
|
-
<SelectTrigger
|
|
789
|
+
<SelectTrigger aria-invalid={!!error?.kind}>
|
|
777
790
|
<SelectValue />
|
|
778
791
|
</SelectTrigger>
|
|
779
792
|
<SelectContent>
|
|
@@ -784,9 +797,8 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
784
797
|
))}
|
|
785
798
|
</SelectContent>
|
|
786
799
|
</Select>
|
|
787
|
-
|
|
800
|
+
</FormField>
|
|
788
801
|
</div>
|
|
789
|
-
</div>
|
|
790
802
|
|
|
791
803
|
{allowFieldsetSelection ? (
|
|
792
804
|
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
@@ -852,24 +864,20 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
852
864
|
) : null}
|
|
853
865
|
|
|
854
866
|
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
855
|
-
<
|
|
856
|
-
<
|
|
857
|
-
<input
|
|
858
|
-
className="border rounded w-full px-2 py-1 text-sm"
|
|
867
|
+
<FormField label="Label">
|
|
868
|
+
<Input
|
|
859
869
|
value={typeof local.configJson?.label === 'string' ? local.configJson.label : ''}
|
|
860
870
|
onChange={(event) => apply({ configJson: { ...(local.configJson || {}), label: event.target.value } })}
|
|
861
871
|
onBlur={commit}
|
|
862
872
|
/>
|
|
863
|
-
</
|
|
864
|
-
<
|
|
865
|
-
<
|
|
866
|
-
<input
|
|
867
|
-
className="border rounded w-full px-2 py-1 text-sm"
|
|
873
|
+
</FormField>
|
|
874
|
+
<FormField label="Description">
|
|
875
|
+
<Input
|
|
868
876
|
value={typeof local.configJson?.description === 'string' ? local.configJson.description : ''}
|
|
869
877
|
onChange={(event) => apply({ configJson: { ...(local.configJson || {}), description: event.target.value } })}
|
|
870
878
|
onBlur={commit}
|
|
871
879
|
/>
|
|
872
|
-
</
|
|
880
|
+
</FormField>
|
|
873
881
|
|
|
874
882
|
{(local.kind === 'text' || local.kind === 'multiline') && (
|
|
875
883
|
<>
|
|
@@ -891,11 +899,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
891
899
|
</div>
|
|
892
900
|
{local.kind === 'text' && (
|
|
893
901
|
<>
|
|
894
|
-
<
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
902
|
+
<CheckboxField
|
|
903
|
+
size="sm"
|
|
904
|
+
checked={!!local.configJson?.multi}
|
|
905
|
+
onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), multi: checked === true } }, true) }}
|
|
906
|
+
label="Multiple"
|
|
907
|
+
containerClassName="md:col-span-2"
|
|
908
|
+
contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
|
|
909
|
+
/>
|
|
899
910
|
{!!local.configJson?.multi && (
|
|
900
911
|
<div className="md:col-span-2">
|
|
901
912
|
<label className="text-xs">Multi-select input style</label>
|
|
@@ -940,7 +951,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
940
951
|
<IconButton
|
|
941
952
|
variant="ghost"
|
|
942
953
|
size="xs"
|
|
943
|
-
className="text-
|
|
954
|
+
className="text-destructive hover:text-destructive/80"
|
|
944
955
|
onClick={() => handleRemoveOption(idx)}
|
|
945
956
|
aria-label="Remove option"
|
|
946
957
|
>
|
|
@@ -968,25 +979,22 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
968
979
|
|
|
969
980
|
{(local.kind === 'select' || local.kind === 'relation') && (
|
|
970
981
|
<>
|
|
971
|
-
<
|
|
972
|
-
<
|
|
973
|
-
<input
|
|
974
|
-
className="border rounded w-full px-2 py-1 text-sm"
|
|
982
|
+
<FormField label="Options URL">
|
|
983
|
+
<Input
|
|
975
984
|
placeholder="/api/..."
|
|
976
985
|
value={typeof local.configJson?.optionsUrl === 'string' ? local.configJson.optionsUrl : ''}
|
|
977
986
|
onChange={(event) => apply({ configJson: { ...(local.configJson || {}), optionsUrl: event.target.value } })}
|
|
978
987
|
onBlur={commit}
|
|
979
988
|
/>
|
|
980
|
-
</
|
|
989
|
+
</FormField>
|
|
981
990
|
{local.kind === 'relation' && (
|
|
982
|
-
<
|
|
983
|
-
<
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
const relatedEntityId = event.target.value
|
|
991
|
+
<FormField label="Related Entity ID">
|
|
992
|
+
<Input
|
|
993
|
+
inputClassName="font-mono"
|
|
994
|
+
placeholder="module:entity"
|
|
995
|
+
value={typeof local.configJson?.relatedEntityId === 'string' ? local.configJson.relatedEntityId : ''}
|
|
996
|
+
onChange={(event) => {
|
|
997
|
+
const relatedEntityId = event.target.value
|
|
990
998
|
const defOptionsUrl = relatedEntityId
|
|
991
999
|
? `/api/entities/relations/options?entityId=${encodeURIComponent(relatedEntityId)}`
|
|
992
1000
|
: ''
|
|
@@ -1000,7 +1008,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1000
1008
|
}}
|
|
1001
1009
|
onBlur={commit}
|
|
1002
1010
|
/>
|
|
1003
|
-
</
|
|
1011
|
+
</FormField>
|
|
1004
1012
|
)}
|
|
1005
1013
|
</>
|
|
1006
1014
|
)}
|
|
@@ -1015,16 +1023,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1015
1023
|
)}
|
|
1016
1024
|
|
|
1017
1025
|
{(local.kind === 'integer' || local.kind === 'float') && (
|
|
1018
|
-
<
|
|
1019
|
-
<
|
|
1020
|
-
<input
|
|
1021
|
-
className="border rounded w-full px-2 py-1 text-sm"
|
|
1026
|
+
<FormField label="Units (optional)" className="md:col-span-2">
|
|
1027
|
+
<Input
|
|
1022
1028
|
placeholder="kg, cm, etc."
|
|
1023
1029
|
value={typeof local.configJson?.unit === 'string' ? local.configJson.unit : ''}
|
|
1024
1030
|
onChange={(event) => apply({ configJson: { ...(local.configJson || {}), unit: event.target.value } })}
|
|
1025
1031
|
onBlur={commit}
|
|
1026
1032
|
/>
|
|
1027
|
-
</
|
|
1033
|
+
</FormField>
|
|
1028
1034
|
)}
|
|
1029
1035
|
</div>
|
|
1030
1036
|
|
|
@@ -1082,8 +1088,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1082
1088
|
</Select>
|
|
1083
1089
|
</div>
|
|
1084
1090
|
<div className="md:col-span-4">
|
|
1085
|
-
<
|
|
1086
|
-
className="border rounded w-full px-2 py-1 text-sm"
|
|
1091
|
+
<Input
|
|
1087
1092
|
placeholder={rule?.rule === 'regex' ? 'Pattern (e.g. ^[a-z]+$)' : (['lt','lte','gt','gte'].includes(rule?.rule) ? 'Number' : '—')}
|
|
1088
1093
|
value={rule?.param ?? ''}
|
|
1089
1094
|
onChange={(event) => {
|
|
@@ -1100,8 +1105,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1100
1105
|
/>
|
|
1101
1106
|
</div>
|
|
1102
1107
|
<div className="md:col-span-4">
|
|
1103
|
-
<
|
|
1104
|
-
className="border rounded w-full px-2 py-1 text-sm"
|
|
1108
|
+
<Input
|
|
1105
1109
|
placeholder="Error message"
|
|
1106
1110
|
value={rule?.message || ''}
|
|
1107
1111
|
onChange={(event) => {
|
|
@@ -1163,37 +1167,37 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1163
1167
|
if (local.kind === 'boolean') {
|
|
1164
1168
|
const boolDefault = currentDefault === true ? 'true' : currentDefault === false ? 'false' : ''
|
|
1165
1169
|
return (
|
|
1166
|
-
<
|
|
1167
|
-
<
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
value={boolDefault}
|
|
1171
|
-
onChange={(event) => {
|
|
1172
|
-
const raw = event.target.value
|
|
1170
|
+
<FormField label={t('entities.customFields.fields.defaultValue', 'Default value')} className="mt-3">
|
|
1171
|
+
<Select
|
|
1172
|
+
value={boolDefault || DEFAULT_VALUE_NONE}
|
|
1173
|
+
onValueChange={(raw) => {
|
|
1173
1174
|
const value = raw === 'true' ? true : raw === 'false' ? false : undefined
|
|
1174
1175
|
apply({ configJson: { ...(local.configJson || {}), defaultValue: value } }, true)
|
|
1175
1176
|
}}
|
|
1176
1177
|
>
|
|
1177
|
-
<
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1178
|
+
<SelectTrigger>
|
|
1179
|
+
<SelectValue />
|
|
1180
|
+
</SelectTrigger>
|
|
1181
|
+
<SelectContent>
|
|
1182
|
+
<SelectItem value={DEFAULT_VALUE_NONE}>{t('entities.customFields.fields.defaultValueNone', 'No default')}</SelectItem>
|
|
1183
|
+
<SelectItem value="true">{t('entities.customFields.fields.defaultValueTrue', 'Checked (true)')}</SelectItem>
|
|
1184
|
+
<SelectItem value="false">{t('entities.customFields.fields.defaultValueFalse', 'Unchecked (false)')}</SelectItem>
|
|
1185
|
+
</SelectContent>
|
|
1186
|
+
</Select>
|
|
1187
|
+
</FormField>
|
|
1182
1188
|
)
|
|
1183
1189
|
}
|
|
1184
1190
|
// select with static options — picker
|
|
1185
1191
|
if (local.kind === 'select' && !local.configJson?.multi) {
|
|
1186
1192
|
const opts = normalizeCustomFieldOptions(local.configJson?.options || [])
|
|
1187
1193
|
if (opts.length > 0) {
|
|
1194
|
+
const noneValue = getDefaultValueNoneOptionValue(opts)
|
|
1188
1195
|
return (
|
|
1189
|
-
<
|
|
1190
|
-
<
|
|
1191
|
-
<select
|
|
1192
|
-
className="border rounded w-full px-2 py-1 text-sm"
|
|
1196
|
+
<FormField label={t('entities.customFields.fields.defaultValue', 'Default value')} className="mt-3">
|
|
1197
|
+
<Select
|
|
1193
1198
|
value={typeof currentDefault === 'string' || typeof currentDefault === 'number' ? String(currentDefault) : ''}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
if (!raw) {
|
|
1199
|
+
onValueChange={(raw) => {
|
|
1200
|
+
if (raw === noneValue) {
|
|
1197
1201
|
apply({ configJson: { ...(local.configJson || {}), defaultValue: undefined } }, true)
|
|
1198
1202
|
return
|
|
1199
1203
|
}
|
|
@@ -1203,26 +1207,29 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1203
1207
|
apply({ configJson: { ...(local.configJson || {}), defaultValue: typed } }, true)
|
|
1204
1208
|
}}
|
|
1205
1209
|
>
|
|
1206
|
-
<
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
</
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1210
|
+
<SelectTrigger>
|
|
1211
|
+
<SelectValue placeholder={t('entities.customFields.fields.defaultValueNone', 'No default')} />
|
|
1212
|
+
</SelectTrigger>
|
|
1213
|
+
<SelectContent>
|
|
1214
|
+
<SelectItem value={noneValue}>{t('entities.customFields.fields.defaultValueNone', 'No default')}</SelectItem>
|
|
1215
|
+
{opts.map((option) => (
|
|
1216
|
+
<SelectItem key={String(option.value)} value={String(option.value)}>
|
|
1217
|
+
{option.label}
|
|
1218
|
+
</SelectItem>
|
|
1219
|
+
))}
|
|
1220
|
+
</SelectContent>
|
|
1221
|
+
</Select>
|
|
1222
|
+
</FormField>
|
|
1214
1223
|
)
|
|
1215
1224
|
}
|
|
1216
1225
|
}
|
|
1217
1226
|
// Numeric kinds
|
|
1218
1227
|
if (local.kind === 'integer' || local.kind === 'float') {
|
|
1219
1228
|
return (
|
|
1220
|
-
<
|
|
1221
|
-
<
|
|
1222
|
-
<input
|
|
1229
|
+
<FormField label={t('entities.customFields.fields.defaultValue', 'Default value')} className="mt-3">
|
|
1230
|
+
<Input
|
|
1223
1231
|
type="number"
|
|
1224
1232
|
step={local.kind === 'integer' ? '1' : 'any'}
|
|
1225
|
-
className="border rounded w-full px-2 py-1 text-sm"
|
|
1226
1233
|
value={typeof currentDefault === 'number' ? String(currentDefault) : ''}
|
|
1227
1234
|
onChange={(event) => {
|
|
1228
1235
|
const raw = event.target.value
|
|
@@ -1233,16 +1240,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1233
1240
|
onBlur={commit}
|
|
1234
1241
|
placeholder={t('entities.customFields.fields.defaultValuePlaceholder', 'No default')}
|
|
1235
1242
|
/>
|
|
1236
|
-
</
|
|
1243
|
+
</FormField>
|
|
1237
1244
|
)
|
|
1238
1245
|
}
|
|
1239
1246
|
// Text-like kinds (text, multiline, date, datetime, currency)
|
|
1240
1247
|
if (['text', 'multiline', 'date', 'datetime', 'currency'].includes(local.kind)) {
|
|
1241
1248
|
return (
|
|
1242
|
-
<
|
|
1243
|
-
<
|
|
1244
|
-
<input
|
|
1245
|
-
className="border rounded w-full px-2 py-1 text-sm"
|
|
1249
|
+
<FormField label={t('entities.customFields.fields.defaultValue', 'Default value')} className="mt-3">
|
|
1250
|
+
<Input
|
|
1246
1251
|
value={typeof currentDefault === 'string' ? currentDefault : ''}
|
|
1247
1252
|
onChange={(event) => {
|
|
1248
1253
|
const value = event.target.value
|
|
@@ -1251,7 +1256,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1251
1256
|
onBlur={commit}
|
|
1252
1257
|
placeholder={t('entities.customFields.fields.defaultValuePlaceholder', 'No default')}
|
|
1253
1258
|
/>
|
|
1254
|
-
</
|
|
1259
|
+
</FormField>
|
|
1255
1260
|
)
|
|
1256
1261
|
}
|
|
1257
1262
|
return null
|
|
@@ -1259,38 +1264,34 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1259
1264
|
|
|
1260
1265
|
<div className="mt-3 pt-2 border-t flex flex-wrap items-center gap-4">
|
|
1261
1266
|
<span className="text-xs text-muted-foreground">{t('entities.customFields.fields.visibility', 'Visibility:')}</span>
|
|
1262
|
-
<
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
onChange={(event) => { apply({ configJson: { ...(local.configJson || {}), encrypted: event.target.checked } }, true) }}
|
|
1291
|
-
/>
|
|
1292
|
-
{t('entities.customFields.fields.encrypted', 'Encrypted')}
|
|
1293
|
-
</label>
|
|
1267
|
+
<CheckboxField
|
|
1268
|
+
size="sm"
|
|
1269
|
+
checked={local.configJson?.listVisible !== false}
|
|
1270
|
+
onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), listVisible: checked === true } }, true) }}
|
|
1271
|
+
label={t('entities.customFields.fields.listVisible', 'List')}
|
|
1272
|
+
contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
|
|
1273
|
+
/>
|
|
1274
|
+
<CheckboxField
|
|
1275
|
+
size="sm"
|
|
1276
|
+
checked={!!local.configJson?.filterable}
|
|
1277
|
+
onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), filterable: checked === true } }, true) }}
|
|
1278
|
+
label={t('entities.customFields.fields.filterable', 'Filter')}
|
|
1279
|
+
contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
|
|
1280
|
+
/>
|
|
1281
|
+
<CheckboxField
|
|
1282
|
+
size="sm"
|
|
1283
|
+
checked={local.configJson?.formEditable !== false}
|
|
1284
|
+
onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), formEditable: checked === true } }, true) }}
|
|
1285
|
+
label={t('entities.customFields.fields.formEditable', 'Form')}
|
|
1286
|
+
contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
|
|
1287
|
+
/>
|
|
1288
|
+
<CheckboxField
|
|
1289
|
+
size="sm"
|
|
1290
|
+
checked={!!local.configJson?.encrypted}
|
|
1291
|
+
onCheckedChange={(checked) => { apply({ configJson: { ...(local.configJson || {}), encrypted: checked === true } }, true) }}
|
|
1292
|
+
label={t('entities.customFields.fields.encrypted', 'Encrypted')}
|
|
1293
|
+
contentClassName="gap-1 [&_label]:text-xs [&_label]:font-normal"
|
|
1294
|
+
/>
|
|
1294
1295
|
</div>
|
|
1295
1296
|
</div>
|
|
1296
1297
|
<Dialog
|
|
@@ -1306,10 +1307,9 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1306
1307
|
<DialogDescription>Provide the stored value and optional label shown to users.</DialogDescription>
|
|
1307
1308
|
</DialogHeader>
|
|
1308
1309
|
<div className="space-y-4 py-2">
|
|
1309
|
-
<
|
|
1310
|
-
<
|
|
1311
|
-
|
|
1312
|
-
className="mt-1 w-full rounded border px-2 py-1 text-sm font-mono"
|
|
1310
|
+
<FormField label="Value" error={optionFormError ?? undefined}>
|
|
1311
|
+
<Input
|
|
1312
|
+
inputClassName="font-mono"
|
|
1313
1313
|
placeholder="unique_value"
|
|
1314
1314
|
value={optionValueDraft}
|
|
1315
1315
|
onChange={(event) => {
|
|
@@ -1317,17 +1317,14 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1317
1317
|
setOptionValueDraft(event.target.value)
|
|
1318
1318
|
}}
|
|
1319
1319
|
/>
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
<label className="text-xs">Label</label>
|
|
1324
|
-
<input
|
|
1325
|
-
className="mt-1 w-full rounded border px-2 py-1 text-sm"
|
|
1320
|
+
</FormField>
|
|
1321
|
+
<FormField label="Label">
|
|
1322
|
+
<Input
|
|
1326
1323
|
placeholder="Label shown to users (optional)"
|
|
1327
1324
|
value={optionLabelDraft}
|
|
1328
1325
|
onChange={(event) => setOptionLabelDraft(event.target.value)}
|
|
1329
1326
|
/>
|
|
1330
|
-
</
|
|
1327
|
+
</FormField>
|
|
1331
1328
|
</div>
|
|
1332
1329
|
<DialogFooter>
|
|
1333
1330
|
<Button variant="outline" size="sm" onClick={handleCloseOptionDialog}>
|
|
@@ -1358,10 +1355,9 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1358
1355
|
</DialogDescription>
|
|
1359
1356
|
</DialogHeader>
|
|
1360
1357
|
<div className="space-y-3">
|
|
1361
|
-
<
|
|
1362
|
-
<
|
|
1363
|
-
|
|
1364
|
-
className="mt-1 w-full rounded border px-2 py-1 text-sm font-mono disabled:cursor-not-allowed disabled:bg-muted/50 disabled:text-muted-foreground"
|
|
1358
|
+
<FormField label="Group code" error={groupError ?? undefined} disabled={!!editingGroupCode}>
|
|
1359
|
+
<Input
|
|
1360
|
+
inputClassName="font-mono"
|
|
1365
1361
|
value={groupDraft.code}
|
|
1366
1362
|
onChange={(event) => {
|
|
1367
1363
|
setGroupDraft((prev) => ({ ...prev, code: event.target.value }))
|
|
@@ -1370,26 +1366,21 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1370
1366
|
disabled={!!editingGroupCode}
|
|
1371
1367
|
placeholder="e.g. buying_committee"
|
|
1372
1368
|
/>
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
<label className="text-xs font-medium">Label</label>
|
|
1377
|
-
<input
|
|
1378
|
-
className="mt-1 w-full rounded border px-2 py-1 text-sm"
|
|
1369
|
+
</FormField>
|
|
1370
|
+
<FormField label="Label">
|
|
1371
|
+
<Input
|
|
1379
1372
|
value={groupDraft.title}
|
|
1380
1373
|
onChange={(event) => setGroupDraft((prev) => ({ ...prev, title: event.target.value }))}
|
|
1381
1374
|
placeholder="Buying committee"
|
|
1382
1375
|
/>
|
|
1383
|
-
</
|
|
1384
|
-
<
|
|
1385
|
-
<
|
|
1386
|
-
<input
|
|
1387
|
-
className="mt-1 w-full rounded border px-2 py-1 text-sm"
|
|
1376
|
+
</FormField>
|
|
1377
|
+
<FormField label="Hint">
|
|
1378
|
+
<Input
|
|
1388
1379
|
value={groupDraft.hint}
|
|
1389
1380
|
onChange={(event) => setGroupDraft((prev) => ({ ...prev, hint: event.target.value }))}
|
|
1390
1381
|
placeholder="Visible to merchandisers"
|
|
1391
1382
|
/>
|
|
1392
|
-
</
|
|
1383
|
+
</FormField>
|
|
1393
1384
|
{currentFieldsetValue && groupOptions.length > 0 ? (
|
|
1394
1385
|
<div>
|
|
1395
1386
|
<div className="text-xs font-medium mb-1">Existing groups</div>
|
|
@@ -1416,7 +1407,7 @@ const FieldDefinitionCard = React.memo(function FieldDefinitionCard({
|
|
|
1416
1407
|
<IconButton
|
|
1417
1408
|
variant="ghost"
|
|
1418
1409
|
size="xs"
|
|
1419
|
-
className="text-
|
|
1410
|
+
className="text-destructive hover:text-destructive/80"
|
|
1420
1411
|
onClick={() => handleRemoveGroupEntry(group.code)}
|
|
1421
1412
|
aria-label={`Delete ${group.code}`}
|
|
1422
1413
|
>
|