@k3-tech/react-kit 0.0.59 → 0.0.61
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/index.js +978 -851
- package/dist/kit/builder/data-table/components/DataTable.d.ts +7 -1
- package/dist/kit/builder/data-table/components/DataTable.d.ts.map +1 -1
- package/dist/kit/builder/form/components/FormBuilderField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/ArrayField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/CheckboxField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/DateField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/DatePickerField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/DateRangePickerField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/DateTimePickerField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/DateTimeRangePickerField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/FileField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/MonthPickerField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/MonthRangePickerField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/NumberField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/RadioField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/SelectField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/SwitchField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/TextField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/TextareaField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/TimePickerField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/TimeRangePickerField.d.ts.map +1 -1
- package/dist/kit/builder/form/types.d.ts +6 -1
- package/dist/kit/builder/form/types.d.ts.map +1 -1
- package/dist/kit/components/autocomplete/Autocomplete.d.ts +2 -1
- package/dist/kit/components/autocomplete/Autocomplete.d.ts.map +1 -1
- package/dist/kit/components/fileuploader/FileUploader.d.ts +1 -1
- package/dist/kit/components/fileuploader/FileUploader.d.ts.map +1 -1
- package/dist/kit/components/fileuploader/types.d.ts +1 -0
- package/dist/kit/components/fileuploader/types.d.ts.map +1 -1
- package/dist/kit/components/info-tooltip/InfoTooltip.d.ts +6 -0
- package/dist/kit/components/info-tooltip/InfoTooltip.d.ts.map +1 -0
- package/dist/kit/themes/clean-slate.css +6 -2
- package/dist/kit/themes/default.css +6 -2
- package/dist/kit/themes/minimal-modern.css +6 -2
- package/dist/kit/themes/spotify.css +6 -2
- package/package.json +1 -1
- package/src/kit/builder/data-table/components/DataTable.tsx +110 -97
- package/src/kit/builder/form/components/FormBuilderField.tsx +17 -2
- package/src/kit/builder/form/components/fields/ArrayField.tsx +1 -0
- package/src/kit/builder/form/components/fields/CheckboxField.tsx +21 -8
- package/src/kit/builder/form/components/fields/DateField.tsx +1 -0
- package/src/kit/builder/form/components/fields/DatePickerField.tsx +1 -0
- package/src/kit/builder/form/components/fields/DateRangePickerField.tsx +3 -2
- package/src/kit/builder/form/components/fields/DateTimePickerField.tsx +1 -0
- package/src/kit/builder/form/components/fields/DateTimeRangePickerField.tsx +1 -0
- package/src/kit/builder/form/components/fields/FileField.tsx +1 -0
- package/src/kit/builder/form/components/fields/MonthPickerField.tsx +1 -0
- package/src/kit/builder/form/components/fields/MonthRangePickerField.tsx +1 -0
- package/src/kit/builder/form/components/fields/NumberField.tsx +70 -2
- package/src/kit/builder/form/components/fields/RadioField.tsx +1 -0
- package/src/kit/builder/form/components/fields/SelectField.tsx +1 -0
- package/src/kit/builder/form/components/fields/SwitchField.tsx +11 -0
- package/src/kit/builder/form/components/fields/TextField.tsx +1 -0
- package/src/kit/builder/form/components/fields/TextareaField.tsx +2 -1
- package/src/kit/builder/form/components/fields/TimePickerField.tsx +1 -0
- package/src/kit/builder/form/components/fields/TimeRangePickerField.tsx +1 -0
- package/src/kit/builder/form/types.ts +6 -1
- package/src/kit/components/autocomplete/Autocomplete.tsx +6 -3
- package/src/kit/components/fileuploader/FileUploader.tsx +2 -0
- package/src/kit/components/fileuploader/types.ts +2 -0
- package/src/kit/components/info-tooltip/InfoTooltip.tsx +18 -0
|
@@ -82,6 +82,12 @@ export interface DataTableProps<TData, TValue> {
|
|
|
82
82
|
filterWrapper?: 'accordion' | 'card' | 'none';
|
|
83
83
|
filterTitle?: string;
|
|
84
84
|
filterShowActionsSeparator?: boolean;
|
|
85
|
+
// Custom header component
|
|
86
|
+
/**
|
|
87
|
+
* Should only be used if you intend to replace the default filter.
|
|
88
|
+
* Will hide default filter and actions, and also disable some props.
|
|
89
|
+
*/
|
|
90
|
+
CustomHeaderComponent?: () => React.ReactNode;
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
export function DataTable<TData, TValue>({
|
|
@@ -117,6 +123,7 @@ export function DataTable<TData, TValue>({
|
|
|
117
123
|
filterWrapper,
|
|
118
124
|
filterTitle,
|
|
119
125
|
filterShowActionsSeparator,
|
|
126
|
+
CustomHeaderComponent,
|
|
120
127
|
}: DataTableProps<TData, TValue>) {
|
|
121
128
|
const [rowSelection, setRowSelection] = React.useState<
|
|
122
129
|
Record<string, boolean>
|
|
@@ -359,105 +366,111 @@ export function DataTable<TData, TValue>({
|
|
|
359
366
|
|
|
360
367
|
return (
|
|
361
368
|
<div className={cn('space-y-3', className)}>
|
|
362
|
-
{
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
className="
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
</AccordionTrigger>
|
|
375
|
-
<AccordionContent className="px-4 pb-4 pt-5">
|
|
376
|
-
<FormBuilder
|
|
377
|
-
key={JSON.stringify(formFilterValues ?? {})}
|
|
378
|
-
sections={formFilters as FormBuilderSectionConfig[]}
|
|
379
|
-
defaultValues={formFilterValues}
|
|
380
|
-
onSubmit={(data) =>
|
|
381
|
-
onFormFilterChange?.(data as Record<string, unknown>)
|
|
382
|
-
}
|
|
383
|
-
onReset={() => onFormFilterChange?.({})}
|
|
384
|
-
showActions
|
|
385
|
-
submitLabel="Apply"
|
|
386
|
-
resetLabel="Clear"
|
|
387
|
-
showActionsSeparator={filterShowActionsSeparator}
|
|
388
|
-
/>
|
|
389
|
-
</AccordionContent>
|
|
390
|
-
</AccordionItem>
|
|
391
|
-
</Accordion>
|
|
392
|
-
</div>
|
|
393
|
-
) : effectiveFilterWrapper === 'card' ? (
|
|
394
|
-
<div className="rounded-md border border-border p-4">
|
|
395
|
-
<FormBuilder
|
|
396
|
-
key={JSON.stringify(formFilterValues ?? {})}
|
|
397
|
-
sections={formFilters as FormBuilderSectionConfig[]}
|
|
398
|
-
defaultValues={formFilterValues}
|
|
399
|
-
onSubmit={(data) =>
|
|
400
|
-
onFormFilterChange?.(data as Record<string, unknown>)
|
|
401
|
-
}
|
|
402
|
-
onReset={() => onFormFilterChange?.({})}
|
|
403
|
-
showActions
|
|
404
|
-
submitLabel="Apply"
|
|
405
|
-
resetLabel="Clear"
|
|
406
|
-
showActionsSeparator={filterShowActionsSeparator}
|
|
407
|
-
/>
|
|
408
|
-
</div>
|
|
409
|
-
) : (
|
|
410
|
-
<FormBuilder
|
|
411
|
-
key={JSON.stringify(formFilterValues ?? {})}
|
|
412
|
-
sections={formFilters as FormBuilderSectionConfig[]}
|
|
413
|
-
defaultValues={formFilterValues}
|
|
414
|
-
onSubmit={(data) =>
|
|
415
|
-
onFormFilterChange?.(data as Record<string, unknown>)
|
|
416
|
-
}
|
|
417
|
-
onReset={() => onFormFilterChange?.({})}
|
|
418
|
-
showActions
|
|
419
|
-
submitLabel="Apply"
|
|
420
|
-
resetLabel="Clear"
|
|
421
|
-
showActionsSeparator={filterShowActionsSeparator}
|
|
422
|
-
/>
|
|
423
|
-
)
|
|
424
|
-
) : null}
|
|
425
|
-
{/* Actions Bar */}
|
|
426
|
-
{(safeActions.length ||
|
|
427
|
-
showStandardActions ||
|
|
428
|
-
(selectable &&
|
|
429
|
-
table.getSelectedRowModel().rows.length > 0 &&
|
|
430
|
-
safeBatchActions.length)) && (
|
|
431
|
-
<div className="rounded-md p-2 mt-6">
|
|
432
|
-
<div className="flex items-center justify-between">
|
|
433
|
-
<div className="flex items-center gap-2">
|
|
434
|
-
{selectable &&
|
|
435
|
-
table.getSelectedRowModel().rows.length > 0 &&
|
|
436
|
-
safeBatchActions.map((a) => renderBatchButton(a, a.key))}
|
|
437
|
-
</div>
|
|
438
|
-
<div className="flex items-center gap-2">
|
|
439
|
-
{showStandardActions && onRefresh && (
|
|
440
|
-
<Button
|
|
441
|
-
variant="outline"
|
|
442
|
-
size="sm"
|
|
443
|
-
onClick={handleRefresh}
|
|
444
|
-
disabled={loading}
|
|
445
|
-
className="h-8"
|
|
446
|
-
title="Refresh"
|
|
369
|
+
{CustomHeaderComponent ? (
|
|
370
|
+
CustomHeaderComponent()
|
|
371
|
+
) : (
|
|
372
|
+
<>
|
|
373
|
+
{formFilters?.length ? (
|
|
374
|
+
effectiveFilterWrapper === 'accordion' ? (
|
|
375
|
+
<div className="rounded-md border border-border">
|
|
376
|
+
<Accordion
|
|
377
|
+
type="single"
|
|
378
|
+
collapsible
|
|
379
|
+
className="w-full"
|
|
380
|
+
defaultValue="filters"
|
|
447
381
|
>
|
|
448
|
-
<
|
|
449
|
-
className=
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
382
|
+
<AccordionItem value="filters">
|
|
383
|
+
<AccordionTrigger className="px-4 py-3 border-b border-border text-md">
|
|
384
|
+
{effectiveFilterTitle}
|
|
385
|
+
</AccordionTrigger>
|
|
386
|
+
<AccordionContent className="px-4 pb-4 pt-5">
|
|
387
|
+
<FormBuilder
|
|
388
|
+
key={JSON.stringify(formFilterValues ?? {})}
|
|
389
|
+
sections={formFilters as FormBuilderSectionConfig[]}
|
|
390
|
+
defaultValues={formFilterValues}
|
|
391
|
+
onSubmit={(data) =>
|
|
392
|
+
onFormFilterChange?.(data as Record<string, unknown>)
|
|
393
|
+
}
|
|
394
|
+
onReset={() => onFormFilterChange?.({})}
|
|
395
|
+
showActions
|
|
396
|
+
submitLabel="Apply"
|
|
397
|
+
resetLabel="Clear"
|
|
398
|
+
showActionsSeparator={filterShowActionsSeparator}
|
|
399
|
+
/>
|
|
400
|
+
</AccordionContent>
|
|
401
|
+
</AccordionItem>
|
|
402
|
+
</Accordion>
|
|
403
|
+
</div>
|
|
404
|
+
) : effectiveFilterWrapper === 'card' ? (
|
|
405
|
+
<div className="rounded-md border border-border p-4">
|
|
406
|
+
<FormBuilder
|
|
407
|
+
key={JSON.stringify(formFilterValues ?? {})}
|
|
408
|
+
sections={formFilters as FormBuilderSectionConfig[]}
|
|
409
|
+
defaultValues={formFilterValues}
|
|
410
|
+
onSubmit={(data) =>
|
|
411
|
+
onFormFilterChange?.(data as Record<string, unknown>)
|
|
412
|
+
}
|
|
413
|
+
onReset={() => onFormFilterChange?.({})}
|
|
414
|
+
showActions
|
|
415
|
+
submitLabel="Apply"
|
|
416
|
+
resetLabel="Clear"
|
|
417
|
+
showActionsSeparator={filterShowActionsSeparator}
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
420
|
+
) : (
|
|
421
|
+
<FormBuilder
|
|
422
|
+
key={JSON.stringify(formFilterValues ?? {})}
|
|
423
|
+
sections={formFilters as FormBuilderSectionConfig[]}
|
|
424
|
+
defaultValues={formFilterValues}
|
|
425
|
+
onSubmit={(data) =>
|
|
426
|
+
onFormFilterChange?.(data as Record<string, unknown>)
|
|
427
|
+
}
|
|
428
|
+
onReset={() => onFormFilterChange?.({})}
|
|
429
|
+
showActions
|
|
430
|
+
submitLabel="Apply"
|
|
431
|
+
resetLabel="Clear"
|
|
432
|
+
showActionsSeparator={filterShowActionsSeparator}
|
|
433
|
+
/>
|
|
434
|
+
)
|
|
435
|
+
) : null}
|
|
436
|
+
{/* Actions Bar */}
|
|
437
|
+
{(safeActions.length ||
|
|
438
|
+
showStandardActions ||
|
|
439
|
+
(selectable &&
|
|
440
|
+
table.getSelectedRowModel().rows.length > 0 &&
|
|
441
|
+
safeBatchActions.length)) && (
|
|
442
|
+
<div className="rounded-md p-2 mt-6">
|
|
443
|
+
<div className="flex items-center justify-between">
|
|
444
|
+
<div className="flex items-center gap-2">
|
|
445
|
+
{selectable &&
|
|
446
|
+
table.getSelectedRowModel().rows.length > 0 &&
|
|
447
|
+
safeBatchActions.map((a) => renderBatchButton(a, a.key))}
|
|
448
|
+
</div>
|
|
449
|
+
<div className="flex items-center gap-2">
|
|
450
|
+
{showStandardActions && onRefresh && (
|
|
451
|
+
<Button
|
|
452
|
+
variant="outline"
|
|
453
|
+
size="sm"
|
|
454
|
+
onClick={handleRefresh}
|
|
455
|
+
disabled={loading}
|
|
456
|
+
className="h-8"
|
|
457
|
+
title="Refresh"
|
|
458
|
+
>
|
|
459
|
+
<RefreshCw
|
|
460
|
+
className={cn('h-4 w-4', loading && 'animate-spin')}
|
|
461
|
+
/>{' '}
|
|
462
|
+
Refresh
|
|
463
|
+
</Button>
|
|
464
|
+
)}
|
|
465
|
+
{isColumnVisibilityEnabled ? (
|
|
466
|
+
<DataTableViewOptions table={table} />
|
|
467
|
+
) : null}
|
|
468
|
+
{safeActions.map((a) => renderActionButton(a, a.key))}
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
458
471
|
</div>
|
|
459
|
-
|
|
460
|
-
|
|
472
|
+
)}
|
|
473
|
+
</>
|
|
461
474
|
)}
|
|
462
475
|
<div
|
|
463
476
|
className="relative overflow-hidden rounded-md border border-border"
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
ObjectField,
|
|
27
27
|
ArrayField,
|
|
28
28
|
} from './fields';
|
|
29
|
+
import InfoTooltip from '@/kit/components/info-tooltip/InfoTooltip';
|
|
29
30
|
|
|
30
31
|
export interface FormBuilderFieldProps<
|
|
31
32
|
TFieldValues extends FieldValues = FieldValues,
|
|
@@ -68,8 +69,10 @@ export function FormBuilderField<
|
|
|
68
69
|
(value: unknown, ...extras: unknown[]) => {
|
|
69
70
|
// Check for onChangeOverride, block any change to RHF
|
|
70
71
|
// if onChangeOverride returns exactly false
|
|
71
|
-
if (
|
|
72
|
-
|
|
72
|
+
if (
|
|
73
|
+
typeof field.onChangeOverride === 'function' &&
|
|
74
|
+
field.onChangeOverride(value, ...extras) === false
|
|
75
|
+
)
|
|
73
76
|
return;
|
|
74
77
|
// Only patch the RHF value with the first argument (the canonical value)
|
|
75
78
|
controllerField.onChange(value);
|
|
@@ -410,6 +413,12 @@ export function FormBuilderField<
|
|
|
410
413
|
<Label htmlFor={fieldPath} className="text-sm font-medium">
|
|
411
414
|
{field.label}
|
|
412
415
|
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
416
|
+
{field.infoTooltip && (
|
|
417
|
+
<InfoTooltip
|
|
418
|
+
iconSize={field.infoTooltipIconSize}
|
|
419
|
+
content={field.infoTooltip}
|
|
420
|
+
/>
|
|
421
|
+
)}
|
|
413
422
|
</Label>
|
|
414
423
|
{renderField()}
|
|
415
424
|
</div>
|
|
@@ -440,6 +449,12 @@ export function FormBuilderField<
|
|
|
440
449
|
<Label htmlFor={fieldPath} className="text-sm font-medium">
|
|
441
450
|
{field.label}
|
|
442
451
|
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
452
|
+
{field.infoTooltip && (
|
|
453
|
+
<InfoTooltip
|
|
454
|
+
iconSize={field.infoTooltipIconSize}
|
|
455
|
+
content={field.infoTooltip}
|
|
456
|
+
/>
|
|
457
|
+
)}
|
|
443
458
|
</Label>
|
|
444
459
|
{renderField()}
|
|
445
460
|
{field.description && (
|
|
@@ -2,6 +2,7 @@ import { Checkbox } from '../../../../../shadcn/ui/checkbox';
|
|
|
2
2
|
import { Label } from '../../../../../shadcn/ui/label';
|
|
3
3
|
import { cn } from '../../../../../shadcn/lib/utils';
|
|
4
4
|
import type { FieldRenderProps } from './types';
|
|
5
|
+
import InfoTooltip from '@/kit/components/info-tooltip/InfoTooltip';
|
|
5
6
|
|
|
6
7
|
export function CheckboxField({
|
|
7
8
|
field,
|
|
@@ -19,15 +20,25 @@ export function CheckboxField({
|
|
|
19
20
|
<Label id={labelId} className="text-sm font-medium">
|
|
20
21
|
{field.label}
|
|
21
22
|
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
23
|
+
{field.infoTooltip && (
|
|
24
|
+
<InfoTooltip
|
|
25
|
+
iconSize={field.infoTooltipIconSize}
|
|
26
|
+
content={field.infoTooltip}
|
|
27
|
+
/>
|
|
28
|
+
)}
|
|
22
29
|
</Label>
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
<div className="flex items-center space-x-4">
|
|
31
|
+
<Checkbox
|
|
32
|
+
aria-labelledby={labelId}
|
|
33
|
+
id={fieldPath}
|
|
34
|
+
checked={(value as boolean) || false}
|
|
35
|
+
onCheckedChange={onChange as (val: boolean) => void}
|
|
36
|
+
disabled={field.disabled || field.readOnly}
|
|
37
|
+
className={cn(className)}
|
|
38
|
+
data-testid={field.dataTestID}
|
|
39
|
+
/>
|
|
40
|
+
{field.subLabel}
|
|
41
|
+
</div>
|
|
31
42
|
</div>
|
|
32
43
|
);
|
|
33
44
|
}
|
|
@@ -40,6 +51,7 @@ export function CheckboxField({
|
|
|
40
51
|
onCheckedChange={onChange as (val: boolean) => void}
|
|
41
52
|
disabled={field.disabled}
|
|
42
53
|
className={cn(className)}
|
|
54
|
+
data-testid={field.dataTestID}
|
|
43
55
|
/>
|
|
44
56
|
);
|
|
45
57
|
}
|
|
@@ -52,6 +64,7 @@ export function CheckboxField({
|
|
|
52
64
|
onCheckedChange={onChange as (val: boolean) => void}
|
|
53
65
|
disabled={field.disabled}
|
|
54
66
|
className={cn(className)}
|
|
67
|
+
data-testid={field.dataTestID}
|
|
55
68
|
/>
|
|
56
69
|
<Label
|
|
57
70
|
htmlFor={fieldPath}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import type { FieldRenderProps } from './types';
|
|
3
|
-
import { DateRangePicker } from '../../../../components/datepicker/DateRangePicker';
|
|
4
2
|
import type { DateRange } from 'react-day-picker';
|
|
5
3
|
import { useWatch } from 'react-hook-form';
|
|
4
|
+
import { DateRangePicker } from '../../../../components/datepicker/DateRangePicker';
|
|
5
|
+
import type { FieldRenderProps } from './types';
|
|
6
6
|
|
|
7
7
|
function coerceDate(input: unknown): Date | undefined {
|
|
8
8
|
if (!input) return undefined;
|
|
@@ -66,6 +66,7 @@ export function DateRangePickerField({
|
|
|
66
66
|
showFooter={field.showFooter}
|
|
67
67
|
cancelLabel={field.cancelLabel}
|
|
68
68
|
applyLabel={field.applyLabel}
|
|
69
|
+
data-testid={field.dataTestID}
|
|
69
70
|
/>
|
|
70
71
|
);
|
|
71
72
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import { Input } from '../../../../../shadcn/ui/input';
|
|
2
3
|
import type { FieldRenderProps } from './types';
|
|
3
4
|
|
|
@@ -7,14 +8,81 @@ export function NumberField({
|
|
|
7
8
|
onChange,
|
|
8
9
|
className,
|
|
9
10
|
}: FieldRenderProps) {
|
|
11
|
+
const innerValue = value as number;
|
|
12
|
+
const [displayValue, setDisplayValue] = useState(
|
|
13
|
+
(Number(innerValue) || "").toLocaleString('id-ID'),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
if (field.numberMode === 'currency') {
|
|
17
|
+
const parseAndFormatIDRInput = (input: string) => {
|
|
18
|
+
const cleaned = input.replace(/[^\d.,]/g, '');
|
|
19
|
+
if (!cleaned) {
|
|
20
|
+
return { display: '', numeric: 0 };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// IDR uses "," as decimal separator and "." for thousands.
|
|
24
|
+
const hasDecimalSeparator = cleaned.includes(',');
|
|
25
|
+
const sepIndex = cleaned.lastIndexOf(',');
|
|
26
|
+
const rawInteger = hasDecimalSeparator
|
|
27
|
+
? cleaned.slice(0, Math.max(0, sepIndex))
|
|
28
|
+
: cleaned;
|
|
29
|
+
const rawFraction = hasDecimalSeparator
|
|
30
|
+
? cleaned.slice(sepIndex + 1)
|
|
31
|
+
: '';
|
|
32
|
+
|
|
33
|
+
const integerDigits = rawInteger.replace(/\D/g, '');
|
|
34
|
+
const fractionDigits = rawFraction.replace(/\D/g, '');
|
|
35
|
+
const safeInteger = integerDigits || '0';
|
|
36
|
+
|
|
37
|
+
const groupedInteger = Number(safeInteger).toLocaleString('id-ID');
|
|
38
|
+
const display = hasDecimalSeparator
|
|
39
|
+
? `${groupedInteger},${fractionDigits}`
|
|
40
|
+
: groupedInteger;
|
|
41
|
+
const numeric = Number(
|
|
42
|
+
fractionDigits ? `${safeInteger}.${fractionDigits}` : safeInteger,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
display,
|
|
47
|
+
numeric: Number.isFinite(numeric) ? numeric : 0,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
52
|
+
const { display, numeric } = parseAndFormatIDRInput(
|
|
53
|
+
e.currentTarget.value,
|
|
54
|
+
);
|
|
55
|
+
setDisplayValue(display);
|
|
56
|
+
onChange(numeric);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Input
|
|
61
|
+
type="text"
|
|
62
|
+
inputMode="decimal"
|
|
63
|
+
value={displayValue}
|
|
64
|
+
onChange={handleInputChange}
|
|
65
|
+
className="min-w-[120px] text-right bg-background text-foreground"
|
|
66
|
+
data-testid={field.dataTestID}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
10
71
|
return (
|
|
11
72
|
<Input
|
|
73
|
+
data-testid={field.dataTestID}
|
|
12
74
|
className={className}
|
|
13
75
|
disabled={field.disabled || field.readOnly}
|
|
14
76
|
placeholder={field.placeholder}
|
|
15
77
|
type="number"
|
|
16
|
-
value={
|
|
17
|
-
onChange={(e) =>
|
|
78
|
+
value={innerValue ?? ''}
|
|
79
|
+
onChange={(e) =>
|
|
80
|
+
onChange(
|
|
81
|
+
typeof e.currentTarget.valueAsNumber === 'number'
|
|
82
|
+
? e.currentTarget.valueAsNumber
|
|
83
|
+
: null,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
18
86
|
/>
|
|
19
87
|
);
|
|
20
88
|
}
|
|
@@ -27,6 +27,7 @@ export function SelectField({
|
|
|
27
27
|
value={toUiValue(value)}
|
|
28
28
|
onValueChange={(val) => onChange(fromUiValue(val))}
|
|
29
29
|
disabled={field.disabled || field.readOnly}
|
|
30
|
+
data-testid={field.dataTestID}
|
|
30
31
|
>
|
|
31
32
|
<SelectTrigger className={className}>
|
|
32
33
|
<SelectValue placeholder={field.placeholder} />
|
|
@@ -2,6 +2,7 @@ import { Switch } from '../../../../../shadcn/ui/switch';
|
|
|
2
2
|
import { Label } from '../../../../../shadcn/ui/label';
|
|
3
3
|
import { cn } from '../../../../../shadcn/lib/utils';
|
|
4
4
|
import type { FieldRenderProps } from './types';
|
|
5
|
+
import InfoTooltip from '@/kit/components/info-tooltip/InfoTooltip';
|
|
5
6
|
|
|
6
7
|
export function SwitchField({
|
|
7
8
|
field,
|
|
@@ -19,6 +20,12 @@ export function SwitchField({
|
|
|
19
20
|
<Label id={labelId} className="text-sm font-medium">
|
|
20
21
|
{field.label}
|
|
21
22
|
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
23
|
+
{field.infoTooltip && (
|
|
24
|
+
<InfoTooltip
|
|
25
|
+
iconSize={field.infoTooltipIconSize}
|
|
26
|
+
content={field.infoTooltip}
|
|
27
|
+
/>
|
|
28
|
+
)}
|
|
22
29
|
</Label>
|
|
23
30
|
<Switch
|
|
24
31
|
aria-labelledby={labelId}
|
|
@@ -27,7 +34,9 @@ export function SwitchField({
|
|
|
27
34
|
onCheckedChange={onChange as (val: boolean) => void}
|
|
28
35
|
disabled={field.disabled || field.readOnly}
|
|
29
36
|
className={cn(className)}
|
|
37
|
+
data-testid={field.dataTestID}
|
|
30
38
|
/>
|
|
39
|
+
{field.subLabel}
|
|
31
40
|
</div>
|
|
32
41
|
);
|
|
33
42
|
}
|
|
@@ -40,6 +49,7 @@ export function SwitchField({
|
|
|
40
49
|
onCheckedChange={onChange as (val: boolean) => void}
|
|
41
50
|
disabled={field.disabled}
|
|
42
51
|
className={cn(className)}
|
|
52
|
+
data-testid={field.dataTestID}
|
|
43
53
|
/>
|
|
44
54
|
);
|
|
45
55
|
}
|
|
@@ -52,6 +62,7 @@ export function SwitchField({
|
|
|
52
62
|
onCheckedChange={onChange as (val: boolean) => void}
|
|
53
63
|
disabled={field.disabled}
|
|
54
64
|
className={cn(className)}
|
|
65
|
+
data-testid={field.dataTestID}
|
|
55
66
|
/>
|
|
56
67
|
<Label
|
|
57
68
|
htmlFor={fieldPath}
|
|
@@ -73,13 +73,18 @@ export interface FormBuilderFieldConfig<
|
|
|
73
73
|
> {
|
|
74
74
|
id?: string;
|
|
75
75
|
name: TName;
|
|
76
|
-
label: string;
|
|
77
76
|
type: FieldType;
|
|
77
|
+
label: React.ReactNode;
|
|
78
78
|
placeholder?: string;
|
|
79
79
|
description?: string;
|
|
80
|
+
subLabel?: React.ReactNode;
|
|
81
|
+
infoTooltip?: React.ReactNode;
|
|
82
|
+
infoTooltipIconSize?: string | number;
|
|
80
83
|
required?: boolean;
|
|
81
84
|
disabled?: boolean;
|
|
82
85
|
readOnly?: boolean;
|
|
86
|
+
dataTestID?: string;
|
|
87
|
+
numberMode?: "currency" | undefined;
|
|
83
88
|
options?: { label: string; value: string | number | boolean | null }[];
|
|
84
89
|
autocompleteMode?: 'client' | 'server';
|
|
85
90
|
fetcher?: AutocompleteFetcher;
|