@bsol-oss/react-datatable5 12.0.0-beta.75 → 12.0.0-beta.76

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.d.ts CHANGED
@@ -823,6 +823,17 @@ interface EnumPickerLabels {
823
823
  emptySearchResult?: string;
824
824
  initialResults?: string;
825
825
  }
826
+ interface FilePickerLabels {
827
+ fileDropzone?: string;
828
+ browseLibrary?: string;
829
+ dialogTitle?: string;
830
+ searchPlaceholder?: string;
831
+ loading?: string;
832
+ loadingFailed?: string;
833
+ noFilesFound?: string;
834
+ cancel?: string;
835
+ select?: string;
836
+ }
826
837
  interface CustomJSONSchema7 extends JSONSchema7 {
827
838
  gridColumn?: string;
828
839
  gridRow?: string;
@@ -849,12 +860,26 @@ interface CustomJSONSchema7 extends JSONSchema7 {
849
860
  formatOptions?: Intl.NumberFormatOptions;
850
861
  numberStorageType?: 'string' | 'number';
851
862
  errorMessages?: Partial<Record<ValidationErrorType | string, string>>;
863
+ filePicker?: FilePickerProps;
852
864
  }
853
865
  interface TagPickerProps {
854
866
  column: string;
855
867
  schema: CustomJSONSchema7;
856
868
  prefix: string;
857
869
  }
870
+ interface FilePickerMediaFile {
871
+ id: string;
872
+ name: string;
873
+ url?: string;
874
+ size?: string | number;
875
+ comment?: string;
876
+ type?: string;
877
+ }
878
+ interface FilePickerProps {
879
+ onFetchFiles?: (search: string) => Promise<FilePickerMediaFile[]>;
880
+ enableMediaLibrary?: boolean;
881
+ filterImageOnly?: boolean;
882
+ }
858
883
 
859
884
  interface FormRootProps<TData extends FieldValues> {
860
885
  schema: CustomJSONSchema7;
@@ -883,6 +908,7 @@ interface FormRootProps<TData extends FieldValues> {
883
908
  dateTimePickerLabels?: DateTimePickerLabels;
884
909
  idPickerLabels?: IdPickerLabels;
885
910
  enumPickerLabels?: EnumPickerLabels;
911
+ filePickerLabels?: FilePickerLabels;
886
912
  }
887
913
  interface CustomJSONSchema7Definition extends JSONSchema7 {
888
914
  variant: string;
@@ -899,7 +925,7 @@ declare const idPickerSanityCheck: (column: string, foreign_key?: {
899
925
  column?: string | undefined;
900
926
  display_column?: string | undefined;
901
927
  } | undefined) => void;
902
- declare const FormRoot: <TData extends FieldValues>({ schema, idMap, setIdMap, form, serverUrl, translate, children, order, ignore, include, onSubmit, rowNumber, requestOptions, getUpdatedData, customErrorRenderer, customSuccessRenderer, displayConfig, requireConfirmation, dateTimePickerLabels, idPickerLabels, enumPickerLabels, }: FormRootProps<TData>) => react_jsx_runtime.JSX.Element;
928
+ declare const FormRoot: <TData extends FieldValues>({ schema, idMap, setIdMap, form, serverUrl, translate, children, order, ignore, include, onSubmit, rowNumber, requestOptions, getUpdatedData, customErrorRenderer, customSuccessRenderer, displayConfig, requireConfirmation, dateTimePickerLabels, idPickerLabels, enumPickerLabels, filePickerLabels, }: FormRootProps<TData>) => react_jsx_runtime.JSX.Element;
903
929
 
904
930
  interface DefaultFormProps<TData extends FieldValues> {
905
931
  formConfig: Omit<FormRootProps<TData>, "children">;
@@ -1071,4 +1097,4 @@ declare module "@tanstack/react-table" {
1071
1097
  }
1072
1098
  }
1073
1099
 
1074
- export { type CalendarProps, CardHeader, type CardHeaderProps, type CustomJSONSchema7, type CustomJSONSchema7Definition, DataDisplay, type DataDisplayProps, type DataResponse, DataTable, type DataTableDefaultState, type DataTableProps, DataTableServer, type DataTableServerProps, type DatePickerLabels, type DatePickerProps, type DateTimePickerLabels, DefaultCardTitle, DefaultForm, type DefaultFormProps, DefaultTable, type DefaultTableProps, DensityToggleButton, type DensityToggleButtonProps, type EditFilterButtonProps, EditSortingButton, type EditSortingButtonProps, type EditViewButtonProps, EmptyState, type EmptyStateProps, type EnumPickerLabels, ErrorAlert, type ErrorAlertProps, type ErrorMessageConfig, type ErrorMessageResult, type FieldErrorConfig, FilterDialog, FormBody, FormRoot, type FormRootProps, FormTitle, type GetColumnsConfigs, type GetDateColorProps, type GetMultiDatesProps, type GetRangeDatesProps, type GetStyleProps, type GetVariantProps, GlobalFilter, type IdPickerLabels, PageSizeControl, type PageSizeControlProps, Pagination, type QueryParams, type RangeCalendarProps, type RangeDatePickerProps, RecordDisplay, type RecordDisplayProps, ReloadButton, type ReloadButtonProps, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, type Result, RowCountText, Table, TableBody, type TableBodyProps, TableCardContainer, type TableCardContainerProps, TableCards, type TableCardsProps, TableComponent, TableControls, type TableControlsProps, TableDataDisplay, type TableDataDisplayProps, TableFilter, TableFilterTags, TableFooter, type TableFooterProps, TableHeader, type TableHeaderProps, type TableHeaderTexts, TableLoadingComponent, type TableLoadingComponentProps, type TableProps, type TableRendererProps, type TableRowSelectorProps, TableSelector, TableSorter, TableViewer, type TagPickerProps, TextCell, type TextCellProps, type UseDataTableProps, type UseDataTableReturn, type UseDataTableServerProps, type UseDataTableServerReturn, type UseFormProps, type ValidationErrorType, ViewDialog, buildErrorMessages, buildFieldErrors, buildRequiredErrors, convertToAjvErrorsFormat, createErrorMessage, getColumns, getMultiDates, getRangeDates, idPickerSanityCheck, useDataTable, useDataTableContext, useDataTableServer, useForm, widthSanityCheck };
1100
+ export { type CalendarProps, CardHeader, type CardHeaderProps, type CustomJSONSchema7, type CustomJSONSchema7Definition, DataDisplay, type DataDisplayProps, type DataResponse, DataTable, type DataTableDefaultState, type DataTableProps, DataTableServer, type DataTableServerProps, type DatePickerLabels, type DatePickerProps, type DateTimePickerLabels, DefaultCardTitle, DefaultForm, type DefaultFormProps, DefaultTable, type DefaultTableProps, DensityToggleButton, type DensityToggleButtonProps, type EditFilterButtonProps, EditSortingButton, type EditSortingButtonProps, type EditViewButtonProps, EmptyState, type EmptyStateProps, type EnumPickerLabels, ErrorAlert, type ErrorAlertProps, type ErrorMessageConfig, type ErrorMessageResult, type FieldErrorConfig, type FilePickerLabels, type FilePickerMediaFile, type FilePickerProps, FilterDialog, FormBody, FormRoot, type FormRootProps, FormTitle, type GetColumnsConfigs, type GetDateColorProps, type GetMultiDatesProps, type GetRangeDatesProps, type GetStyleProps, type GetVariantProps, GlobalFilter, type IdPickerLabels, PageSizeControl, type PageSizeControlProps, Pagination, type QueryParams, type RangeCalendarProps, type RangeDatePickerProps, RecordDisplay, type RecordDisplayProps, ReloadButton, type ReloadButtonProps, ResetFilteringButton, ResetSelectionButton, ResetSortingButton, type Result, RowCountText, Table, TableBody, type TableBodyProps, TableCardContainer, type TableCardContainerProps, TableCards, type TableCardsProps, TableComponent, TableControls, type TableControlsProps, TableDataDisplay, type TableDataDisplayProps, TableFilter, TableFilterTags, TableFooter, type TableFooterProps, TableHeader, type TableHeaderProps, type TableHeaderTexts, TableLoadingComponent, type TableLoadingComponentProps, type TableProps, type TableRendererProps, type TableRowSelectorProps, TableSelector, TableSorter, TableViewer, type TagPickerProps, TextCell, type TextCellProps, type UseDataTableProps, type UseDataTableReturn, type UseDataTableServerProps, type UseDataTableServerReturn, type UseFormProps, type ValidationErrorType, ViewDialog, buildErrorMessages, buildFieldErrors, buildRequiredErrors, convertToAjvErrorsFormat, createErrorMessage, getColumns, getMultiDates, getRangeDates, idPickerSanityCheck, useDataTable, useDataTableContext, useDataTableServer, useForm, widthSanityCheck };
package/dist/index.js CHANGED
@@ -3908,7 +3908,7 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
3908
3908
  showSubmitButton: true,
3909
3909
  showResetButton: true,
3910
3910
  showTitle: true,
3911
- }, requireConfirmation = false, dateTimePickerLabels, idPickerLabels, enumPickerLabels, }) => {
3911
+ }, requireConfirmation = false, dateTimePickerLabels, idPickerLabels, enumPickerLabels, filePickerLabels, }) => {
3912
3912
  const [isSuccess, setIsSuccess] = React.useState(false);
3913
3913
  const [isError, setIsError] = React.useState(false);
3914
3914
  const [isSubmiting, setIsSubmiting] = React.useState(false);
@@ -3997,6 +3997,7 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
3997
3997
  dateTimePickerLabels,
3998
3998
  idPickerLabels,
3999
3999
  enumPickerLabels,
4000
+ filePickerLabels,
4000
4001
  ajvResolver: ajvResolver(schema),
4001
4002
  }, children: jsxRuntime.jsx(reactHookForm.FormProvider, { ...form, children: children }) }));
4002
4003
  };
@@ -4370,6 +4371,92 @@ const DatePicker = ({ column, schema, prefix }) => {
4370
4371
  } })] }) })] }) }));
4371
4372
  };
4372
4373
 
4374
+ dayjs.extend(utc);
4375
+ dayjs.extend(timezone);
4376
+ const DateRangePicker = ({ column, schema, prefix, }) => {
4377
+ const { watch, formState: { errors }, setValue, } = reactHookForm.useFormContext();
4378
+ const { timezone, dateTimePickerLabels } = useSchemaContext();
4379
+ const formI18n = useFormI18n(column, prefix);
4380
+ const { required, gridColumn = 'span 12', gridRow = 'span 1', displayDateFormat = 'YYYY-MM-DD', dateFormat = 'YYYY-MM-DD', } = schema;
4381
+ const isRequired = required?.some((columnId) => columnId === column);
4382
+ const colLabel = formI18n.colLabel;
4383
+ const [open, setOpen] = React.useState(false);
4384
+ const selectedDateRange = watch(colLabel);
4385
+ // Convert string[] to Date[] for the picker
4386
+ const selectedDates = (selectedDateRange ?? [])
4387
+ .map((dateStr) => {
4388
+ if (!dateStr)
4389
+ return null;
4390
+ const parsed = dayjs(dateStr).tz(timezone);
4391
+ return parsed.isValid() ? parsed.toDate() : null;
4392
+ })
4393
+ .filter((date) => date !== null);
4394
+ // Format display string
4395
+ const getDisplayText = () => {
4396
+ if (!selectedDateRange || selectedDateRange.length === 0) {
4397
+ return '';
4398
+ }
4399
+ if (selectedDateRange.length === 1) {
4400
+ const date = dayjs(selectedDateRange[0]).tz(timezone);
4401
+ return date.isValid() ? date.format(displayDateFormat) : '';
4402
+ }
4403
+ if (selectedDateRange.length === 2) {
4404
+ const startDate = dayjs(selectedDateRange[0]).tz(timezone);
4405
+ const endDate = dayjs(selectedDateRange[1]).tz(timezone);
4406
+ if (startDate.isValid() && endDate.isValid()) {
4407
+ return `${startDate.format(displayDateFormat)} - ${endDate.format(displayDateFormat)}`;
4408
+ }
4409
+ }
4410
+ return '';
4411
+ };
4412
+ React.useEffect(() => {
4413
+ try {
4414
+ if (selectedDateRange && selectedDateRange.length > 0) {
4415
+ // Format dates according to dateFormat from schema
4416
+ const formatted = selectedDateRange
4417
+ .map((dateStr) => {
4418
+ if (!dateStr)
4419
+ return null;
4420
+ const parsed = dayjs(dateStr).tz(timezone);
4421
+ return parsed.isValid() ? parsed.format(dateFormat) : null;
4422
+ })
4423
+ .filter((date) => date !== null);
4424
+ // Update the form value only if different to avoid loops
4425
+ // Compare arrays element by element
4426
+ const needsUpdate = formatted.length !== selectedDateRange.length ||
4427
+ formatted.some((val, idx) => val !== selectedDateRange[idx]);
4428
+ if (needsUpdate && formatted.length > 0) {
4429
+ setValue(colLabel, formatted, {
4430
+ shouldValidate: true,
4431
+ shouldDirty: true,
4432
+ });
4433
+ }
4434
+ }
4435
+ }
4436
+ catch (e) {
4437
+ console.error(e);
4438
+ }
4439
+ }, [selectedDateRange, dateFormat, colLabel, setValue, timezone]);
4440
+ return (jsxRuntime.jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
4441
+ gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxRuntime.jsxs(PopoverRoot, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, children: [jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: jsxRuntime.jsxs(Button, { size: "sm", variant: "outline", onClick: () => {
4442
+ setOpen(true);
4443
+ }, justifyContent: 'start', children: [jsxRuntime.jsx(md.MdDateRange, {}), getDisplayText()] }) }), jsxRuntime.jsx(PopoverContent, { minW: '600px', children: jsxRuntime.jsxs(PopoverBody, { children: [jsxRuntime.jsx(PopoverTitle, {}), jsxRuntime.jsx(RangeDatePicker, { selected: selectedDates, onDateSelected: ({ selected, selectable, date }) => {
4444
+ const newDates = getRangeDates({
4445
+ selectable,
4446
+ date,
4447
+ selectedDates,
4448
+ }) ?? [];
4449
+ // Convert Date[] to string[]
4450
+ const formattedDates = newDates
4451
+ .map((dateObj) => dayjs(dateObj).tz(timezone).format(dateFormat))
4452
+ .filter((dateStr) => dateStr);
4453
+ setValue(colLabel, formattedDates, {
4454
+ shouldValidate: true,
4455
+ shouldDirty: true,
4456
+ });
4457
+ }, monthsToDisplay: 2 })] }) })] }) }));
4458
+ };
4459
+
4373
4460
  function filterArray(array, searchTerm) {
4374
4461
  // Convert the search term to lower case for case-insensitive comparison
4375
4462
  const lowerCaseSearchTerm = searchTerm.toLowerCase();
@@ -4831,24 +4918,161 @@ const FileDropzone = ({ children = undefined, gridProps = {}, onDrop = () => { }
4831
4918
  return (jsxRuntime.jsxs(react.Grid, { ...getColor(isDraggedOver), ref: ref, cursor: "pointer", onClick: handleClick, borderStyle: "dashed", borderColor: "colorPalette.400", alignContent: "center", justifyContent: "center", borderWidth: 1, borderRadius: 4, ...gridProps, children: [children, !!children === false && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(react.Flex, { children: placeholder }), jsxRuntime.jsx(react.Input, { type: "file", multiple: true, style: { display: "none" }, ref: fileInput, onChange: handleChange })] }))] }));
4832
4919
  };
4833
4920
 
4921
+ /**
4922
+ * Format bytes to human-readable string
4923
+ * @param bytes - The number of bytes to format
4924
+ * @returns Formatted string (e.g., "1.5 KB", "2.3 MB")
4925
+ */
4926
+ function formatBytes(bytes) {
4927
+ if (bytes === 0)
4928
+ return '0 Bytes';
4929
+ const k = 1024;
4930
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
4931
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
4932
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
4933
+ }
4934
+
4935
+ function FilePickerDialog({ open, onClose, onSelect, title, filterImageOnly = false, onFetchFiles, labels, translate, colLabel, }) {
4936
+ const [searchTerm, setSearchTerm] = React.useState('');
4937
+ const [selectedFileId, setSelectedFileId] = React.useState('');
4938
+ const { data: filesData, isLoading, isError, } = reactQuery.useQuery({
4939
+ queryKey: ['file-picker-library', searchTerm],
4940
+ queryFn: async () => {
4941
+ if (!onFetchFiles)
4942
+ return { data: [] };
4943
+ const files = await onFetchFiles(searchTerm.trim() || '');
4944
+ return { data: files };
4945
+ },
4946
+ enabled: open && !!onFetchFiles,
4947
+ });
4948
+ const files = (filesData?.data || []);
4949
+ const filteredFiles = filterImageOnly
4950
+ ? files.filter((file) => /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name))
4951
+ : files;
4952
+ const handleSelect = () => {
4953
+ if (selectedFileId) {
4954
+ onSelect(selectedFileId);
4955
+ onClose();
4956
+ setSelectedFileId('');
4957
+ setSearchTerm('');
4958
+ }
4959
+ };
4960
+ const handleClose = () => {
4961
+ onClose();
4962
+ setSelectedFileId('');
4963
+ setSearchTerm('');
4964
+ };
4965
+ if (!onFetchFiles)
4966
+ return null;
4967
+ return (jsxRuntime.jsx(DialogRoot, { open: open, onOpenChange: (e) => !e.open && handleClose(), children: jsxRuntime.jsxs(DialogContent, { maxWidth: "800px", maxHeight: "90vh", children: [jsxRuntime.jsxs(DialogHeader, { children: [jsxRuntime.jsx(DialogTitle, { fontSize: "lg", fontWeight: "bold", children: title }), jsxRuntime.jsx(DialogCloseTrigger, {})] }), jsxRuntime.jsx(DialogBody, { children: jsxRuntime.jsxs(react.VStack, { align: "stretch", gap: 4, children: [jsxRuntime.jsxs(react.Box, { position: "relative", children: [jsxRuntime.jsx(react.Input, { placeholder: labels?.searchPlaceholder ??
4968
+ translate(removeIndex(`${colLabel}.search_placeholder`)) ??
4969
+ 'Search files...', value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), bg: "bg.panel", border: "1px solid", borderColor: "border.default", _focus: {
4970
+ borderColor: 'blue.500',
4971
+ boxShadow: '0 0 0 1px var(--chakra-colors-blue-500)',
4972
+ }, pl: 10 }), jsxRuntime.jsx(react.Icon, { as: lu.LuSearch, position: "absolute", left: 3, top: "50%", transform: "translateY(-50%)", color: "fg.muted", boxSize: 4 })] }), isLoading && (jsxRuntime.jsxs(react.Box, { textAlign: "center", py: 8, children: [jsxRuntime.jsx(react.Spinner, { size: "lg", colorPalette: "blue" }), jsxRuntime.jsx(react.Text, { mt: 4, color: "fg.muted", children: labels?.loading ??
4973
+ translate(removeIndex(`${colLabel}.loading`)) ??
4974
+ 'Loading files...' })] })), isError && (jsxRuntime.jsx(react.Box, { bg: "red.50", _dark: { bg: 'red.900/20' }, border: "1px solid", borderColor: "red.200", borderRadius: "md", p: 4, children: jsxRuntime.jsx(react.Text, { color: "red.600", _dark: { color: 'red.300' }, children: labels?.loadingFailed ??
4975
+ translate(removeIndex(`${colLabel}.error.loading_failed`)) ??
4976
+ 'Failed to load files' }) })), !isLoading && !isError && (jsxRuntime.jsx(react.Box, { maxHeight: "400px", overflowY: "auto", children: filteredFiles.length === 0 ? (jsxRuntime.jsx(react.Box, { textAlign: "center", py: 8, children: jsxRuntime.jsx(react.Text, { color: "fg.muted", children: labels?.noFilesFound ??
4977
+ translate(removeIndex(`${colLabel}.no_files_found`)) ??
4978
+ 'No files found' }) })) : (jsxRuntime.jsx(react.VStack, { align: "stretch", gap: 2, children: filteredFiles.map((file) => {
4979
+ const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name);
4980
+ const isSelected = selectedFileId === file.id;
4981
+ return (jsxRuntime.jsx(react.Box, { p: 3, border: "2px solid", borderColor: isSelected ? 'blue.500' : 'border.default', borderRadius: "md", bg: isSelected ? 'blue.50' : 'bg.panel', _dark: {
4982
+ bg: isSelected ? 'blue.900/20' : 'bg.panel',
4983
+ }, cursor: "pointer", onClick: () => setSelectedFileId(file.id), _hover: {
4984
+ borderColor: isSelected ? 'blue.600' : 'blue.300',
4985
+ bg: isSelected ? 'blue.100' : 'bg.muted',
4986
+ }, transition: "all 0.2s", children: jsxRuntime.jsxs(react.HStack, { gap: 3, children: [jsxRuntime.jsx(react.Box, { width: "60px", height: "60px", display: "flex", alignItems: "center", justifyContent: "center", bg: "bg.muted", borderRadius: "md", flexShrink: 0, children: isImage && file.url ? (jsxRuntime.jsx(react.Image, { src: file.url, alt: file.name, boxSize: "60px", objectFit: "cover", borderRadius: "md" })) : (jsxRuntime.jsx(react.Icon, { as: lu.LuFile, boxSize: 6, color: "fg.muted" })) }), jsxRuntime.jsxs(react.VStack, { align: "start", flex: 1, gap: 1, children: [jsxRuntime.jsx(react.Text, { fontSize: "sm", fontWeight: "medium", color: "fg.default", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: file.name }), jsxRuntime.jsxs(react.HStack, { gap: 2, children: [file.size && (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(react.Text, { fontSize: "xs", color: "fg.muted", children: typeof file.size === 'number'
4987
+ ? formatBytes(file.size)
4988
+ : file.size }) })), file.comment && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [file.size && (jsxRuntime.jsx(react.Text, { fontSize: "xs", color: "fg.muted", children: "\u2022" })), jsxRuntime.jsx(react.Text, { fontSize: "xs", color: "fg.muted", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: file.comment })] }))] })] }), isSelected && (jsxRuntime.jsx(react.Box, { width: "24px", height: "24px", borderRadius: "full", bg: "blue.500", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, children: jsxRuntime.jsx(react.Text, { color: "white", fontSize: "xs", fontWeight: "bold", children: "\u2713" }) }))] }) }, file.id));
4989
+ }) })) }))] }) }), jsxRuntime.jsx(DialogFooter, { children: jsxRuntime.jsxs(react.HStack, { gap: 3, justify: "end", children: [jsxRuntime.jsx(react.Button, { variant: "outline", onClick: handleClose, borderColor: "border.default", bg: "bg.panel", _hover: { bg: 'bg.muted' }, children: labels?.cancel ??
4990
+ translate(removeIndex(`${colLabel}.cancel`)) ??
4991
+ 'Cancel' }), jsxRuntime.jsx(react.Button, { colorPalette: "blue", onClick: handleSelect, disabled: !selectedFileId, children: labels?.select ??
4992
+ translate(removeIndex(`${colLabel}.select`)) ??
4993
+ 'Select' })] }) })] }) }));
4994
+ }
4834
4995
  const FilePicker = ({ column, schema, prefix }) => {
4835
4996
  const { setValue, formState: { errors }, watch, } = reactHookForm.useFormContext();
4836
- const { translate } = useSchemaContext();
4837
- const { required, gridColumn = 'span 12', gridRow = 'span 1', } = schema;
4997
+ const { filePickerLabels } = useSchemaContext();
4998
+ const formI18n = useFormI18n(column, prefix);
4999
+ const { required, gridColumn = 'span 12', gridRow = 'span 1', filePicker, } = schema;
4838
5000
  const isRequired = required?.some((columnId) => columnId === column);
4839
- const currentFiles = (watch(column) ?? []);
4840
- const colLabel = `${prefix}${column}`;
4841
- return (jsxRuntime.jsxs(Field, { label: `${translate.t(`${colLabel}.field_label`)}`, required: isRequired, gridColumn: gridColumn ?? 'span 4', gridRow: gridRow ?? 'span 1', display: 'grid', gridTemplateRows: 'auto 1fr auto', alignItems: 'stretch', errorText: errors[`${colLabel}`]
4842
- ? translate.t(removeIndex(`${colLabel}.field_required`))
4843
- : undefined, invalid: !!errors[colLabel], children: [jsxRuntime.jsx(FileDropzone, { onDrop: ({ files }) => {
4844
- const newFiles = files.filter(({ name }) => !currentFiles.some((cur) => cur.name === name));
4845
- setValue(colLabel, [...currentFiles, ...newFiles]);
4846
- }, placeholder: translate.t(removeIndex(`${colLabel}.fileDropzone`)) }), jsxRuntime.jsx(react.Flex, { flexFlow: 'column', gap: 1, children: currentFiles.map((file) => {
4847
- return (jsxRuntime.jsx(react.Card.Root, { variant: 'subtle', children: jsxRuntime.jsxs(react.Card.Body, { gap: "2", cursor: 'pointer', onClick: () => {
4848
- setValue(column, currentFiles.filter(({ name }) => {
4849
- return name !== file.name;
4850
- }));
4851
- }, display: 'flex', flexFlow: 'row', alignItems: 'center', padding: '2', children: [file.type.startsWith('image/') && (jsxRuntime.jsx(react.Image, { src: URL.createObjectURL(file), alt: file.name, boxSize: "50px", objectFit: "cover", borderRadius: "md", marginRight: "2" })), jsxRuntime.jsx(react.Box, { children: file.name }), jsxRuntime.jsx(ti.TiDeleteOutline, {})] }) }, file.name));
5001
+ const currentValue = watch(column) ?? [];
5002
+ const currentFiles = Array.isArray(currentValue)
5003
+ ? currentValue
5004
+ : [];
5005
+ const colLabel = formI18n.colLabel;
5006
+ const [dialogOpen, setDialogOpen] = React.useState(false);
5007
+ const { onFetchFiles, enableMediaLibrary = false, filterImageOnly = false, } = filePicker || {};
5008
+ const showMediaLibrary = enableMediaLibrary && !!onFetchFiles;
5009
+ const handleMediaLibrarySelect = (fileId) => {
5010
+ const newFiles = [...currentFiles, fileId];
5011
+ setValue(colLabel, newFiles);
5012
+ };
5013
+ const handleRemove = (index) => {
5014
+ const newFiles = currentFiles.filter((_, i) => i !== index);
5015
+ setValue(colLabel, newFiles);
5016
+ };
5017
+ const isFileObject = (value) => {
5018
+ return value instanceof File;
5019
+ };
5020
+ const getFileIdentifier = (file, index) => {
5021
+ if (isFileObject(file)) {
5022
+ return `${file.name}-${file.size}-${index}`;
5023
+ }
5024
+ return file;
5025
+ };
5026
+ const getFileName = (file) => {
5027
+ if (isFileObject(file)) {
5028
+ return file.name;
5029
+ }
5030
+ return typeof file === 'string' ? file : 'Unknown file';
5031
+ };
5032
+ const getFileSize = (file) => {
5033
+ if (isFileObject(file)) {
5034
+ return file.size;
5035
+ }
5036
+ return undefined;
5037
+ };
5038
+ const isImageFile = (file) => {
5039
+ if (isFileObject(file)) {
5040
+ return file.type.startsWith('image/');
5041
+ }
5042
+ if (typeof file === 'string') {
5043
+ return /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file);
5044
+ }
5045
+ return false;
5046
+ };
5047
+ const getImageUrl = (file) => {
5048
+ if (isFileObject(file)) {
5049
+ return URL.createObjectURL(file);
5050
+ }
5051
+ return undefined;
5052
+ };
5053
+ return (jsxRuntime.jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
5054
+ gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [jsxRuntime.jsxs(react.VStack, { align: "stretch", gap: 2, children: [jsxRuntime.jsx(FileDropzone, { onDrop: ({ files }) => {
5055
+ const newFiles = files.filter(({ name }) => !currentFiles.some((cur) => {
5056
+ if (isFileObject(cur)) {
5057
+ return cur.name === name;
5058
+ }
5059
+ return false;
5060
+ }));
5061
+ setValue(colLabel, [...currentFiles, ...newFiles]);
5062
+ }, placeholder: filePickerLabels?.fileDropzone ?? formI18n.t('fileDropzone') }), showMediaLibrary && (jsxRuntime.jsx(react.Button, { variant: "outline", onClick: () => setDialogOpen(true), borderColor: "border.default", bg: "bg.panel", _hover: { bg: 'bg.muted' }, children: filePickerLabels?.browseLibrary ??
5063
+ formI18n.t('browse_library') ??
5064
+ 'Browse from Library' }))] }), showMediaLibrary && (jsxRuntime.jsx(FilePickerDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSelect: handleMediaLibrarySelect, title: filePickerLabels?.dialogTitle ??
5065
+ formI18n.t('dialog_title') ??
5066
+ 'Select File', filterImageOnly: filterImageOnly, onFetchFiles: onFetchFiles, labels: filePickerLabels, translate: formI18n.t, colLabel: colLabel })), jsxRuntime.jsx(react.Flex, { flexFlow: 'column', gap: 1, children: currentFiles.map((file, index) => {
5067
+ const fileIdentifier = getFileIdentifier(file, index);
5068
+ const fileName = getFileName(file);
5069
+ const fileSize = getFileSize(file);
5070
+ const isImage = isImageFile(file);
5071
+ const imageUrl = getImageUrl(file);
5072
+ return (jsxRuntime.jsx(react.Card.Root, { variant: 'subtle', children: jsxRuntime.jsxs(react.Card.Body, { gap: "2", cursor: 'pointer', onClick: () => handleRemove(index), display: 'flex', flexFlow: 'row', alignItems: 'center', padding: '2', border: "2px solid", borderColor: "border.default", borderRadius: "md", _hover: {
5073
+ borderColor: 'blue.300',
5074
+ bg: 'bg.muted',
5075
+ }, transition: "all 0.2s", children: [jsxRuntime.jsx(react.Box, { width: "60px", height: "60px", display: "flex", alignItems: "center", justifyContent: "center", bg: "bg.muted", borderRadius: "md", flexShrink: 0, marginRight: "2", children: isImage && imageUrl ? (jsxRuntime.jsx(react.Image, { src: imageUrl, alt: fileName, boxSize: "60px", objectFit: "cover", borderRadius: "md" })) : (jsxRuntime.jsx(react.Icon, { as: lu.LuFile, boxSize: 6, color: "fg.muted" })) }), jsxRuntime.jsxs(react.VStack, { align: "start", flex: 1, gap: 1, children: [jsxRuntime.jsx(react.Text, { fontSize: "sm", fontWeight: "medium", color: "fg.default", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: fileName }), fileSize !== undefined && (jsxRuntime.jsx(react.Text, { fontSize: "xs", color: "fg.muted", children: formatBytes(fileSize) }))] }), jsxRuntime.jsx(react.Icon, { as: ti.TiDeleteOutline, boxSize: 5, color: "fg.muted" })] }) }, fileIdentifier));
4852
5076
  }) })] }));
4853
5077
  };
4854
5078
 
@@ -5972,59 +6196,62 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
5972
6196
  const SchemaRenderer = ({ schema, prefix, column, }) => {
5973
6197
  const colSchema = schema;
5974
6198
  const { type, variant, properties: innerProperties, foreign_key, format, items, } = schema;
5975
- if (variant === "custom-input") {
6199
+ if (variant === 'custom-input') {
5976
6200
  return jsxRuntime.jsx(CustomInput, { schema: colSchema, prefix, column });
5977
6201
  }
5978
- if (type === "string") {
6202
+ if (type === 'string') {
5979
6203
  if ((schema.enum ?? []).length > 0) {
5980
6204
  return jsxRuntime.jsx(EnumPicker, { schema: colSchema, prefix, column });
5981
6205
  }
5982
- if (variant === "id-picker") {
6206
+ if (variant === 'id-picker') {
5983
6207
  idPickerSanityCheck(column, foreign_key);
5984
6208
  return jsxRuntime.jsx(IdPicker, { schema: colSchema, prefix, column });
5985
6209
  }
5986
- if (format === "date") {
6210
+ if (format === 'date') {
5987
6211
  return jsxRuntime.jsx(DatePicker, { schema: colSchema, prefix, column });
5988
6212
  }
5989
- if (format === "time") {
6213
+ if (format === 'time') {
5990
6214
  return jsxRuntime.jsx(TimePicker, { schema: colSchema, prefix, column });
5991
6215
  }
5992
- if (format === "date-time") {
6216
+ if (format === 'date-time') {
5993
6217
  return jsxRuntime.jsx(DateTimePicker, { schema: colSchema, prefix, column });
5994
6218
  }
5995
- if (variant === "text-area") {
6219
+ if (variant === 'text-area') {
5996
6220
  return jsxRuntime.jsx(TextAreaInput, { schema: colSchema, prefix, column });
5997
6221
  }
5998
6222
  return jsxRuntime.jsx(StringInputField, { schema: colSchema, prefix, column });
5999
6223
  }
6000
- if (type === "number" || type === "integer") {
6224
+ if (type === 'number' || type === 'integer') {
6001
6225
  return jsxRuntime.jsx(NumberInputField, { schema: colSchema, prefix, column });
6002
6226
  }
6003
- if (type === "boolean") {
6227
+ if (type === 'boolean') {
6004
6228
  return jsxRuntime.jsx(BooleanPicker, { schema: colSchema, prefix, column });
6005
6229
  }
6006
- if (type === "object") {
6230
+ if (type === 'object') {
6007
6231
  if (innerProperties) {
6008
6232
  return jsxRuntime.jsx(ObjectInput, { schema: colSchema, prefix, column });
6009
6233
  }
6010
6234
  return jsxRuntime.jsx(RecordInput$1, { schema: colSchema, prefix, column });
6011
6235
  }
6012
- if (type === "array") {
6013
- if (variant === "id-picker") {
6236
+ if (type === 'array') {
6237
+ if (variant === 'id-picker') {
6014
6238
  idPickerSanityCheck(column, foreign_key);
6015
6239
  return (jsxRuntime.jsx(IdPicker, { schema: colSchema, prefix, column, isMultiple: true }));
6016
6240
  }
6017
- if (variant === "tag-picker") {
6241
+ if (variant === 'tag-picker') {
6018
6242
  return jsxRuntime.jsx(TagPicker, { schema: colSchema, prefix, column });
6019
6243
  }
6020
- if (variant === "file-picker") {
6244
+ if (variant === 'file-picker') {
6021
6245
  return jsxRuntime.jsx(FilePicker, { schema: colSchema, prefix, column });
6022
6246
  }
6023
- if (variant === "enum-picker") {
6247
+ if (variant === 'date-range') {
6248
+ return jsxRuntime.jsx(DateRangePicker, { schema: colSchema, prefix, column });
6249
+ }
6250
+ if (variant === 'enum-picker') {
6024
6251
  const { items } = colSchema;
6025
6252
  const { enum: enumItems } = items;
6026
6253
  const enumSchema = {
6027
- type: "string",
6254
+ type: 'string',
6028
6255
  enum: enumItems,
6029
6256
  };
6030
6257
  return (jsxRuntime.jsx(EnumPicker, { isMultiple: true, schema: enumSchema, prefix, column }));
@@ -6034,7 +6261,7 @@ const SchemaRenderer = ({ schema, prefix, column, }) => {
6034
6261
  }
6035
6262
  return jsxRuntime.jsx(react.Text, { children: `array ${column}` });
6036
6263
  }
6037
- if (type === "null") {
6264
+ if (type === 'null') {
6038
6265
  return jsxRuntime.jsx(react.Text, { children: `null ${column}` });
6039
6266
  }
6040
6267
  return jsxRuntime.jsx(react.Text, { children: "missing type" });
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import { Button as Button$1, AbsoluteCenter, Spinner, Span, IconButton, Portal,
3
3
  import { AiOutlineColumnWidth } from 'react-icons/ai';
4
4
  import * as React from 'react';
5
5
  import React__default, { createContext, useContext, useState, useEffect, useRef, forwardRef } from 'react';
6
- import { LuX, LuCheck, LuChevronRight } from 'react-icons/lu';
6
+ import { LuX, LuCheck, LuChevronRight, LuFile, LuSearch } from 'react-icons/lu';
7
7
  import { MdOutlineSort, MdFilterAlt, MdSearch, MdOutlineViewColumn, MdFilterListAlt, MdPushPin, MdCancel, MdClear, MdOutlineChecklist, MdDateRange } from 'react-icons/md';
8
8
  import { FaUpDown, FaGripLinesVertical, FaTrash } from 'react-icons/fa6';
9
9
  import { BiDownArrow, BiUpArrow, BiError } from 'react-icons/bi';
@@ -3888,7 +3888,7 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
3888
3888
  showSubmitButton: true,
3889
3889
  showResetButton: true,
3890
3890
  showTitle: true,
3891
- }, requireConfirmation = false, dateTimePickerLabels, idPickerLabels, enumPickerLabels, }) => {
3891
+ }, requireConfirmation = false, dateTimePickerLabels, idPickerLabels, enumPickerLabels, filePickerLabels, }) => {
3892
3892
  const [isSuccess, setIsSuccess] = useState(false);
3893
3893
  const [isError, setIsError] = useState(false);
3894
3894
  const [isSubmiting, setIsSubmiting] = useState(false);
@@ -3977,6 +3977,7 @@ const FormRoot = ({ schema, idMap, setIdMap, form, serverUrl, translate, childre
3977
3977
  dateTimePickerLabels,
3978
3978
  idPickerLabels,
3979
3979
  enumPickerLabels,
3980
+ filePickerLabels,
3980
3981
  ajvResolver: ajvResolver(schema),
3981
3982
  }, children: jsx(FormProvider, { ...form, children: children }) }));
3982
3983
  };
@@ -4350,6 +4351,92 @@ const DatePicker = ({ column, schema, prefix }) => {
4350
4351
  } })] }) })] }) }));
4351
4352
  };
4352
4353
 
4354
+ dayjs.extend(utc);
4355
+ dayjs.extend(timezone);
4356
+ const DateRangePicker = ({ column, schema, prefix, }) => {
4357
+ const { watch, formState: { errors }, setValue, } = useFormContext();
4358
+ const { timezone, dateTimePickerLabels } = useSchemaContext();
4359
+ const formI18n = useFormI18n(column, prefix);
4360
+ const { required, gridColumn = 'span 12', gridRow = 'span 1', displayDateFormat = 'YYYY-MM-DD', dateFormat = 'YYYY-MM-DD', } = schema;
4361
+ const isRequired = required?.some((columnId) => columnId === column);
4362
+ const colLabel = formI18n.colLabel;
4363
+ const [open, setOpen] = useState(false);
4364
+ const selectedDateRange = watch(colLabel);
4365
+ // Convert string[] to Date[] for the picker
4366
+ const selectedDates = (selectedDateRange ?? [])
4367
+ .map((dateStr) => {
4368
+ if (!dateStr)
4369
+ return null;
4370
+ const parsed = dayjs(dateStr).tz(timezone);
4371
+ return parsed.isValid() ? parsed.toDate() : null;
4372
+ })
4373
+ .filter((date) => date !== null);
4374
+ // Format display string
4375
+ const getDisplayText = () => {
4376
+ if (!selectedDateRange || selectedDateRange.length === 0) {
4377
+ return '';
4378
+ }
4379
+ if (selectedDateRange.length === 1) {
4380
+ const date = dayjs(selectedDateRange[0]).tz(timezone);
4381
+ return date.isValid() ? date.format(displayDateFormat) : '';
4382
+ }
4383
+ if (selectedDateRange.length === 2) {
4384
+ const startDate = dayjs(selectedDateRange[0]).tz(timezone);
4385
+ const endDate = dayjs(selectedDateRange[1]).tz(timezone);
4386
+ if (startDate.isValid() && endDate.isValid()) {
4387
+ return `${startDate.format(displayDateFormat)} - ${endDate.format(displayDateFormat)}`;
4388
+ }
4389
+ }
4390
+ return '';
4391
+ };
4392
+ useEffect(() => {
4393
+ try {
4394
+ if (selectedDateRange && selectedDateRange.length > 0) {
4395
+ // Format dates according to dateFormat from schema
4396
+ const formatted = selectedDateRange
4397
+ .map((dateStr) => {
4398
+ if (!dateStr)
4399
+ return null;
4400
+ const parsed = dayjs(dateStr).tz(timezone);
4401
+ return parsed.isValid() ? parsed.format(dateFormat) : null;
4402
+ })
4403
+ .filter((date) => date !== null);
4404
+ // Update the form value only if different to avoid loops
4405
+ // Compare arrays element by element
4406
+ const needsUpdate = formatted.length !== selectedDateRange.length ||
4407
+ formatted.some((val, idx) => val !== selectedDateRange[idx]);
4408
+ if (needsUpdate && formatted.length > 0) {
4409
+ setValue(colLabel, formatted, {
4410
+ shouldValidate: true,
4411
+ shouldDirty: true,
4412
+ });
4413
+ }
4414
+ }
4415
+ }
4416
+ catch (e) {
4417
+ console.error(e);
4418
+ }
4419
+ }, [selectedDateRange, dateFormat, colLabel, setValue, timezone]);
4420
+ return (jsx(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
4421
+ gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: jsxs(PopoverRoot, { open: open, onOpenChange: (e) => setOpen(e.open), closeOnInteractOutside: true, children: [jsx(PopoverTrigger, { asChild: true, children: jsxs(Button, { size: "sm", variant: "outline", onClick: () => {
4422
+ setOpen(true);
4423
+ }, justifyContent: 'start', children: [jsx(MdDateRange, {}), getDisplayText()] }) }), jsx(PopoverContent, { minW: '600px', children: jsxs(PopoverBody, { children: [jsx(PopoverTitle, {}), jsx(RangeDatePicker, { selected: selectedDates, onDateSelected: ({ selected, selectable, date }) => {
4424
+ const newDates = getRangeDates({
4425
+ selectable,
4426
+ date,
4427
+ selectedDates,
4428
+ }) ?? [];
4429
+ // Convert Date[] to string[]
4430
+ const formattedDates = newDates
4431
+ .map((dateObj) => dayjs(dateObj).tz(timezone).format(dateFormat))
4432
+ .filter((dateStr) => dateStr);
4433
+ setValue(colLabel, formattedDates, {
4434
+ shouldValidate: true,
4435
+ shouldDirty: true,
4436
+ });
4437
+ }, monthsToDisplay: 2 })] }) })] }) }));
4438
+ };
4439
+
4353
4440
  function filterArray(array, searchTerm) {
4354
4441
  // Convert the search term to lower case for case-insensitive comparison
4355
4442
  const lowerCaseSearchTerm = searchTerm.toLowerCase();
@@ -4811,24 +4898,161 @@ const FileDropzone = ({ children = undefined, gridProps = {}, onDrop = () => { }
4811
4898
  return (jsxs(Grid, { ...getColor(isDraggedOver), ref: ref, cursor: "pointer", onClick: handleClick, borderStyle: "dashed", borderColor: "colorPalette.400", alignContent: "center", justifyContent: "center", borderWidth: 1, borderRadius: 4, ...gridProps, children: [children, !!children === false && (jsxs(Fragment, { children: [jsx(Flex, { children: placeholder }), jsx(Input, { type: "file", multiple: true, style: { display: "none" }, ref: fileInput, onChange: handleChange })] }))] }));
4812
4899
  };
4813
4900
 
4901
+ /**
4902
+ * Format bytes to human-readable string
4903
+ * @param bytes - The number of bytes to format
4904
+ * @returns Formatted string (e.g., "1.5 KB", "2.3 MB")
4905
+ */
4906
+ function formatBytes(bytes) {
4907
+ if (bytes === 0)
4908
+ return '0 Bytes';
4909
+ const k = 1024;
4910
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
4911
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
4912
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
4913
+ }
4914
+
4915
+ function FilePickerDialog({ open, onClose, onSelect, title, filterImageOnly = false, onFetchFiles, labels, translate, colLabel, }) {
4916
+ const [searchTerm, setSearchTerm] = useState('');
4917
+ const [selectedFileId, setSelectedFileId] = useState('');
4918
+ const { data: filesData, isLoading, isError, } = useQuery({
4919
+ queryKey: ['file-picker-library', searchTerm],
4920
+ queryFn: async () => {
4921
+ if (!onFetchFiles)
4922
+ return { data: [] };
4923
+ const files = await onFetchFiles(searchTerm.trim() || '');
4924
+ return { data: files };
4925
+ },
4926
+ enabled: open && !!onFetchFiles,
4927
+ });
4928
+ const files = (filesData?.data || []);
4929
+ const filteredFiles = filterImageOnly
4930
+ ? files.filter((file) => /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name))
4931
+ : files;
4932
+ const handleSelect = () => {
4933
+ if (selectedFileId) {
4934
+ onSelect(selectedFileId);
4935
+ onClose();
4936
+ setSelectedFileId('');
4937
+ setSearchTerm('');
4938
+ }
4939
+ };
4940
+ const handleClose = () => {
4941
+ onClose();
4942
+ setSelectedFileId('');
4943
+ setSearchTerm('');
4944
+ };
4945
+ if (!onFetchFiles)
4946
+ return null;
4947
+ return (jsx(DialogRoot, { open: open, onOpenChange: (e) => !e.open && handleClose(), children: jsxs(DialogContent, { maxWidth: "800px", maxHeight: "90vh", children: [jsxs(DialogHeader, { children: [jsx(DialogTitle, { fontSize: "lg", fontWeight: "bold", children: title }), jsx(DialogCloseTrigger, {})] }), jsx(DialogBody, { children: jsxs(VStack, { align: "stretch", gap: 4, children: [jsxs(Box, { position: "relative", children: [jsx(Input, { placeholder: labels?.searchPlaceholder ??
4948
+ translate(removeIndex(`${colLabel}.search_placeholder`)) ??
4949
+ 'Search files...', value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), bg: "bg.panel", border: "1px solid", borderColor: "border.default", _focus: {
4950
+ borderColor: 'blue.500',
4951
+ boxShadow: '0 0 0 1px var(--chakra-colors-blue-500)',
4952
+ }, pl: 10 }), jsx(Icon, { as: LuSearch, position: "absolute", left: 3, top: "50%", transform: "translateY(-50%)", color: "fg.muted", boxSize: 4 })] }), isLoading && (jsxs(Box, { textAlign: "center", py: 8, children: [jsx(Spinner, { size: "lg", colorPalette: "blue" }), jsx(Text, { mt: 4, color: "fg.muted", children: labels?.loading ??
4953
+ translate(removeIndex(`${colLabel}.loading`)) ??
4954
+ 'Loading files...' })] })), isError && (jsx(Box, { bg: "red.50", _dark: { bg: 'red.900/20' }, border: "1px solid", borderColor: "red.200", borderRadius: "md", p: 4, children: jsx(Text, { color: "red.600", _dark: { color: 'red.300' }, children: labels?.loadingFailed ??
4955
+ translate(removeIndex(`${colLabel}.error.loading_failed`)) ??
4956
+ 'Failed to load files' }) })), !isLoading && !isError && (jsx(Box, { maxHeight: "400px", overflowY: "auto", children: filteredFiles.length === 0 ? (jsx(Box, { textAlign: "center", py: 8, children: jsx(Text, { color: "fg.muted", children: labels?.noFilesFound ??
4957
+ translate(removeIndex(`${colLabel}.no_files_found`)) ??
4958
+ 'No files found' }) })) : (jsx(VStack, { align: "stretch", gap: 2, children: filteredFiles.map((file) => {
4959
+ const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name);
4960
+ const isSelected = selectedFileId === file.id;
4961
+ return (jsx(Box, { p: 3, border: "2px solid", borderColor: isSelected ? 'blue.500' : 'border.default', borderRadius: "md", bg: isSelected ? 'blue.50' : 'bg.panel', _dark: {
4962
+ bg: isSelected ? 'blue.900/20' : 'bg.panel',
4963
+ }, cursor: "pointer", onClick: () => setSelectedFileId(file.id), _hover: {
4964
+ borderColor: isSelected ? 'blue.600' : 'blue.300',
4965
+ bg: isSelected ? 'blue.100' : 'bg.muted',
4966
+ }, transition: "all 0.2s", children: jsxs(HStack, { gap: 3, children: [jsx(Box, { width: "60px", height: "60px", display: "flex", alignItems: "center", justifyContent: "center", bg: "bg.muted", borderRadius: "md", flexShrink: 0, children: isImage && file.url ? (jsx(Image, { src: file.url, alt: file.name, boxSize: "60px", objectFit: "cover", borderRadius: "md" })) : (jsx(Icon, { as: LuFile, boxSize: 6, color: "fg.muted" })) }), jsxs(VStack, { align: "start", flex: 1, gap: 1, children: [jsx(Text, { fontSize: "sm", fontWeight: "medium", color: "fg.default", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: file.name }), jsxs(HStack, { gap: 2, children: [file.size && (jsx(Fragment, { children: jsx(Text, { fontSize: "xs", color: "fg.muted", children: typeof file.size === 'number'
4967
+ ? formatBytes(file.size)
4968
+ : file.size }) })), file.comment && (jsxs(Fragment, { children: [file.size && (jsx(Text, { fontSize: "xs", color: "fg.muted", children: "\u2022" })), jsx(Text, { fontSize: "xs", color: "fg.muted", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: file.comment })] }))] })] }), isSelected && (jsx(Box, { width: "24px", height: "24px", borderRadius: "full", bg: "blue.500", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, children: jsx(Text, { color: "white", fontSize: "xs", fontWeight: "bold", children: "\u2713" }) }))] }) }, file.id));
4969
+ }) })) }))] }) }), jsx(DialogFooter, { children: jsxs(HStack, { gap: 3, justify: "end", children: [jsx(Button$1, { variant: "outline", onClick: handleClose, borderColor: "border.default", bg: "bg.panel", _hover: { bg: 'bg.muted' }, children: labels?.cancel ??
4970
+ translate(removeIndex(`${colLabel}.cancel`)) ??
4971
+ 'Cancel' }), jsx(Button$1, { colorPalette: "blue", onClick: handleSelect, disabled: !selectedFileId, children: labels?.select ??
4972
+ translate(removeIndex(`${colLabel}.select`)) ??
4973
+ 'Select' })] }) })] }) }));
4974
+ }
4814
4975
  const FilePicker = ({ column, schema, prefix }) => {
4815
4976
  const { setValue, formState: { errors }, watch, } = useFormContext();
4816
- const { translate } = useSchemaContext();
4817
- const { required, gridColumn = 'span 12', gridRow = 'span 1', } = schema;
4977
+ const { filePickerLabels } = useSchemaContext();
4978
+ const formI18n = useFormI18n(column, prefix);
4979
+ const { required, gridColumn = 'span 12', gridRow = 'span 1', filePicker, } = schema;
4818
4980
  const isRequired = required?.some((columnId) => columnId === column);
4819
- const currentFiles = (watch(column) ?? []);
4820
- const colLabel = `${prefix}${column}`;
4821
- return (jsxs(Field, { label: `${translate.t(`${colLabel}.field_label`)}`, required: isRequired, gridColumn: gridColumn ?? 'span 4', gridRow: gridRow ?? 'span 1', display: 'grid', gridTemplateRows: 'auto 1fr auto', alignItems: 'stretch', errorText: errors[`${colLabel}`]
4822
- ? translate.t(removeIndex(`${colLabel}.field_required`))
4823
- : undefined, invalid: !!errors[colLabel], children: [jsx(FileDropzone, { onDrop: ({ files }) => {
4824
- const newFiles = files.filter(({ name }) => !currentFiles.some((cur) => cur.name === name));
4825
- setValue(colLabel, [...currentFiles, ...newFiles]);
4826
- }, placeholder: translate.t(removeIndex(`${colLabel}.fileDropzone`)) }), jsx(Flex, { flexFlow: 'column', gap: 1, children: currentFiles.map((file) => {
4827
- return (jsx(Card.Root, { variant: 'subtle', children: jsxs(Card.Body, { gap: "2", cursor: 'pointer', onClick: () => {
4828
- setValue(column, currentFiles.filter(({ name }) => {
4829
- return name !== file.name;
4830
- }));
4831
- }, display: 'flex', flexFlow: 'row', alignItems: 'center', padding: '2', children: [file.type.startsWith('image/') && (jsx(Image, { src: URL.createObjectURL(file), alt: file.name, boxSize: "50px", objectFit: "cover", borderRadius: "md", marginRight: "2" })), jsx(Box, { children: file.name }), jsx(TiDeleteOutline, {})] }) }, file.name));
4981
+ const currentValue = watch(column) ?? [];
4982
+ const currentFiles = Array.isArray(currentValue)
4983
+ ? currentValue
4984
+ : [];
4985
+ const colLabel = formI18n.colLabel;
4986
+ const [dialogOpen, setDialogOpen] = useState(false);
4987
+ const { onFetchFiles, enableMediaLibrary = false, filterImageOnly = false, } = filePicker || {};
4988
+ const showMediaLibrary = enableMediaLibrary && !!onFetchFiles;
4989
+ const handleMediaLibrarySelect = (fileId) => {
4990
+ const newFiles = [...currentFiles, fileId];
4991
+ setValue(colLabel, newFiles);
4992
+ };
4993
+ const handleRemove = (index) => {
4994
+ const newFiles = currentFiles.filter((_, i) => i !== index);
4995
+ setValue(colLabel, newFiles);
4996
+ };
4997
+ const isFileObject = (value) => {
4998
+ return value instanceof File;
4999
+ };
5000
+ const getFileIdentifier = (file, index) => {
5001
+ if (isFileObject(file)) {
5002
+ return `${file.name}-${file.size}-${index}`;
5003
+ }
5004
+ return file;
5005
+ };
5006
+ const getFileName = (file) => {
5007
+ if (isFileObject(file)) {
5008
+ return file.name;
5009
+ }
5010
+ return typeof file === 'string' ? file : 'Unknown file';
5011
+ };
5012
+ const getFileSize = (file) => {
5013
+ if (isFileObject(file)) {
5014
+ return file.size;
5015
+ }
5016
+ return undefined;
5017
+ };
5018
+ const isImageFile = (file) => {
5019
+ if (isFileObject(file)) {
5020
+ return file.type.startsWith('image/');
5021
+ }
5022
+ if (typeof file === 'string') {
5023
+ return /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file);
5024
+ }
5025
+ return false;
5026
+ };
5027
+ const getImageUrl = (file) => {
5028
+ if (isFileObject(file)) {
5029
+ return URL.createObjectURL(file);
5030
+ }
5031
+ return undefined;
5032
+ };
5033
+ return (jsxs(Field, { label: formI18n.label(), required: isRequired, alignItems: 'stretch', gridColumn,
5034
+ gridRow, errorText: errors[`${colLabel}`] ? formI18n.required() : undefined, invalid: !!errors[colLabel], children: [jsxs(VStack, { align: "stretch", gap: 2, children: [jsx(FileDropzone, { onDrop: ({ files }) => {
5035
+ const newFiles = files.filter(({ name }) => !currentFiles.some((cur) => {
5036
+ if (isFileObject(cur)) {
5037
+ return cur.name === name;
5038
+ }
5039
+ return false;
5040
+ }));
5041
+ setValue(colLabel, [...currentFiles, ...newFiles]);
5042
+ }, placeholder: filePickerLabels?.fileDropzone ?? formI18n.t('fileDropzone') }), showMediaLibrary && (jsx(Button$1, { variant: "outline", onClick: () => setDialogOpen(true), borderColor: "border.default", bg: "bg.panel", _hover: { bg: 'bg.muted' }, children: filePickerLabels?.browseLibrary ??
5043
+ formI18n.t('browse_library') ??
5044
+ 'Browse from Library' }))] }), showMediaLibrary && (jsx(FilePickerDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSelect: handleMediaLibrarySelect, title: filePickerLabels?.dialogTitle ??
5045
+ formI18n.t('dialog_title') ??
5046
+ 'Select File', filterImageOnly: filterImageOnly, onFetchFiles: onFetchFiles, labels: filePickerLabels, translate: formI18n.t, colLabel: colLabel })), jsx(Flex, { flexFlow: 'column', gap: 1, children: currentFiles.map((file, index) => {
5047
+ const fileIdentifier = getFileIdentifier(file, index);
5048
+ const fileName = getFileName(file);
5049
+ const fileSize = getFileSize(file);
5050
+ const isImage = isImageFile(file);
5051
+ const imageUrl = getImageUrl(file);
5052
+ return (jsx(Card.Root, { variant: 'subtle', children: jsxs(Card.Body, { gap: "2", cursor: 'pointer', onClick: () => handleRemove(index), display: 'flex', flexFlow: 'row', alignItems: 'center', padding: '2', border: "2px solid", borderColor: "border.default", borderRadius: "md", _hover: {
5053
+ borderColor: 'blue.300',
5054
+ bg: 'bg.muted',
5055
+ }, transition: "all 0.2s", children: [jsx(Box, { width: "60px", height: "60px", display: "flex", alignItems: "center", justifyContent: "center", bg: "bg.muted", borderRadius: "md", flexShrink: 0, marginRight: "2", children: isImage && imageUrl ? (jsx(Image, { src: imageUrl, alt: fileName, boxSize: "60px", objectFit: "cover", borderRadius: "md" })) : (jsx(Icon, { as: LuFile, boxSize: 6, color: "fg.muted" })) }), jsxs(VStack, { align: "start", flex: 1, gap: 1, children: [jsx(Text, { fontSize: "sm", fontWeight: "medium", color: "fg.default", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", children: fileName }), fileSize !== undefined && (jsx(Text, { fontSize: "xs", color: "fg.muted", children: formatBytes(fileSize) }))] }), jsx(Icon, { as: TiDeleteOutline, boxSize: 5, color: "fg.muted" })] }) }, fileIdentifier));
4832
5056
  }) })] }));
4833
5057
  };
4834
5058
 
@@ -5952,59 +6176,62 @@ const DateTimePicker = ({ column, schema, prefix, }) => {
5952
6176
  const SchemaRenderer = ({ schema, prefix, column, }) => {
5953
6177
  const colSchema = schema;
5954
6178
  const { type, variant, properties: innerProperties, foreign_key, format, items, } = schema;
5955
- if (variant === "custom-input") {
6179
+ if (variant === 'custom-input') {
5956
6180
  return jsx(CustomInput, { schema: colSchema, prefix, column });
5957
6181
  }
5958
- if (type === "string") {
6182
+ if (type === 'string') {
5959
6183
  if ((schema.enum ?? []).length > 0) {
5960
6184
  return jsx(EnumPicker, { schema: colSchema, prefix, column });
5961
6185
  }
5962
- if (variant === "id-picker") {
6186
+ if (variant === 'id-picker') {
5963
6187
  idPickerSanityCheck(column, foreign_key);
5964
6188
  return jsx(IdPicker, { schema: colSchema, prefix, column });
5965
6189
  }
5966
- if (format === "date") {
6190
+ if (format === 'date') {
5967
6191
  return jsx(DatePicker, { schema: colSchema, prefix, column });
5968
6192
  }
5969
- if (format === "time") {
6193
+ if (format === 'time') {
5970
6194
  return jsx(TimePicker, { schema: colSchema, prefix, column });
5971
6195
  }
5972
- if (format === "date-time") {
6196
+ if (format === 'date-time') {
5973
6197
  return jsx(DateTimePicker, { schema: colSchema, prefix, column });
5974
6198
  }
5975
- if (variant === "text-area") {
6199
+ if (variant === 'text-area') {
5976
6200
  return jsx(TextAreaInput, { schema: colSchema, prefix, column });
5977
6201
  }
5978
6202
  return jsx(StringInputField, { schema: colSchema, prefix, column });
5979
6203
  }
5980
- if (type === "number" || type === "integer") {
6204
+ if (type === 'number' || type === 'integer') {
5981
6205
  return jsx(NumberInputField, { schema: colSchema, prefix, column });
5982
6206
  }
5983
- if (type === "boolean") {
6207
+ if (type === 'boolean') {
5984
6208
  return jsx(BooleanPicker, { schema: colSchema, prefix, column });
5985
6209
  }
5986
- if (type === "object") {
6210
+ if (type === 'object') {
5987
6211
  if (innerProperties) {
5988
6212
  return jsx(ObjectInput, { schema: colSchema, prefix, column });
5989
6213
  }
5990
6214
  return jsx(RecordInput$1, { schema: colSchema, prefix, column });
5991
6215
  }
5992
- if (type === "array") {
5993
- if (variant === "id-picker") {
6216
+ if (type === 'array') {
6217
+ if (variant === 'id-picker') {
5994
6218
  idPickerSanityCheck(column, foreign_key);
5995
6219
  return (jsx(IdPicker, { schema: colSchema, prefix, column, isMultiple: true }));
5996
6220
  }
5997
- if (variant === "tag-picker") {
6221
+ if (variant === 'tag-picker') {
5998
6222
  return jsx(TagPicker, { schema: colSchema, prefix, column });
5999
6223
  }
6000
- if (variant === "file-picker") {
6224
+ if (variant === 'file-picker') {
6001
6225
  return jsx(FilePicker, { schema: colSchema, prefix, column });
6002
6226
  }
6003
- if (variant === "enum-picker") {
6227
+ if (variant === 'date-range') {
6228
+ return jsx(DateRangePicker, { schema: colSchema, prefix, column });
6229
+ }
6230
+ if (variant === 'enum-picker') {
6004
6231
  const { items } = colSchema;
6005
6232
  const { enum: enumItems } = items;
6006
6233
  const enumSchema = {
6007
- type: "string",
6234
+ type: 'string',
6008
6235
  enum: enumItems,
6009
6236
  };
6010
6237
  return (jsx(EnumPicker, { isMultiple: true, schema: enumSchema, prefix, column }));
@@ -6014,7 +6241,7 @@ const SchemaRenderer = ({ schema, prefix, column, }) => {
6014
6241
  }
6015
6242
  return jsx(Text, { children: `array ${column}` });
6016
6243
  }
6017
- if (type === "null") {
6244
+ if (type === 'null') {
6018
6245
  return jsx(Text, { children: `null ${column}` });
6019
6246
  }
6020
6247
  return jsx(Text, { children: "missing type" });
@@ -3,7 +3,7 @@ import { JSONSchema7 } from 'json-schema';
3
3
  import { Dispatch, ReactNode, SetStateAction } from 'react';
4
4
  import { FieldValues, Resolver } from 'react-hook-form';
5
5
  import { UseTranslationResponse } from 'react-i18next';
6
- import { DateTimePickerLabels, IdPickerLabels, EnumPickerLabels } from './components/types/CustomJSONSchema7';
6
+ import { DateTimePickerLabels, IdPickerLabels, EnumPickerLabels, FilePickerLabels } from './components/types/CustomJSONSchema7';
7
7
  export interface SchemaFormContext<TData extends FieldValues> {
8
8
  schema: JSONSchema7;
9
9
  serverUrl: string;
@@ -43,6 +43,7 @@ export interface SchemaFormContext<TData extends FieldValues> {
43
43
  dateTimePickerLabels?: DateTimePickerLabels;
44
44
  idPickerLabels?: IdPickerLabels;
45
45
  enumPickerLabels?: EnumPickerLabels;
46
+ filePickerLabels?: FilePickerLabels;
46
47
  ajvResolver: Resolver<FieldValues>;
47
48
  }
48
49
  export declare const SchemaFormContext: import("react").Context<SchemaFormContext<unknown>>;
@@ -4,7 +4,7 @@ import { JSONSchema7 } from 'json-schema';
4
4
  import { Dispatch, ReactNode, SetStateAction } from 'react';
5
5
  import { FieldValues, SubmitHandler, UseFormReturn } from 'react-hook-form';
6
6
  import { UseTranslationResponse } from 'react-i18next';
7
- import { CustomJSONSchema7, DateTimePickerLabels, IdPickerLabels, EnumPickerLabels } from '../types/CustomJSONSchema7';
7
+ import { CustomJSONSchema7, DateTimePickerLabels, IdPickerLabels, EnumPickerLabels, FilePickerLabels } from '../types/CustomJSONSchema7';
8
8
  export interface FormRootProps<TData extends FieldValues> {
9
9
  schema: CustomJSONSchema7;
10
10
  serverUrl: string;
@@ -32,6 +32,7 @@ export interface FormRootProps<TData extends FieldValues> {
32
32
  dateTimePickerLabels?: DateTimePickerLabels;
33
33
  idPickerLabels?: IdPickerLabels;
34
34
  enumPickerLabels?: EnumPickerLabels;
35
+ filePickerLabels?: FilePickerLabels;
35
36
  }
36
37
  export interface CustomJSONSchema7Definition extends JSONSchema7 {
37
38
  variant: string;
@@ -48,4 +49,4 @@ export declare const idPickerSanityCheck: (column: string, foreign_key?: {
48
49
  column?: string | undefined;
49
50
  display_column?: string | undefined;
50
51
  } | undefined) => void;
51
- export declare const FormRoot: <TData extends FieldValues>({ schema, idMap, setIdMap, form, serverUrl, translate, children, order, ignore, include, onSubmit, rowNumber, requestOptions, getUpdatedData, customErrorRenderer, customSuccessRenderer, displayConfig, requireConfirmation, dateTimePickerLabels, idPickerLabels, enumPickerLabels, }: FormRootProps<TData>) => import("react/jsx-runtime").JSX.Element;
52
+ export declare const FormRoot: <TData extends FieldValues>({ schema, idMap, setIdMap, form, serverUrl, translate, children, order, ignore, include, onSubmit, rowNumber, requestOptions, getUpdatedData, customErrorRenderer, customSuccessRenderer, displayConfig, requireConfirmation, dateTimePickerLabels, idPickerLabels, enumPickerLabels, filePickerLabels, }: FormRootProps<TData>) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import { InputDefaultProps } from './types';
2
+ export declare const DateRangePicker: ({ column, schema, prefix, }: InputDefaultProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,4 @@
1
- import { CustomJSONSchema7 } from "../types/CustomJSONSchema7";
1
+ import { CustomJSONSchema7 } from '../types/CustomJSONSchema7';
2
2
  export interface SchemaRendererProps {
3
3
  column: string;
4
4
  schema: CustomJSONSchema7;
@@ -29,6 +29,17 @@ export interface EnumPickerLabels {
29
29
  emptySearchResult?: string;
30
30
  initialResults?: string;
31
31
  }
32
+ export interface FilePickerLabels {
33
+ fileDropzone?: string;
34
+ browseLibrary?: string;
35
+ dialogTitle?: string;
36
+ searchPlaceholder?: string;
37
+ loading?: string;
38
+ loadingFailed?: string;
39
+ noFilesFound?: string;
40
+ cancel?: string;
41
+ select?: string;
42
+ }
32
43
  export interface CustomJSONSchema7 extends JSONSchema7 {
33
44
  gridColumn?: string;
34
45
  gridRow?: string;
@@ -55,9 +66,23 @@ export interface CustomJSONSchema7 extends JSONSchema7 {
55
66
  formatOptions?: Intl.NumberFormatOptions;
56
67
  numberStorageType?: 'string' | 'number';
57
68
  errorMessages?: Partial<Record<ValidationErrorType | string, string>>;
69
+ filePicker?: FilePickerProps;
58
70
  }
59
71
  export interface TagPickerProps {
60
72
  column: string;
61
73
  schema: CustomJSONSchema7;
62
74
  prefix: string;
63
75
  }
76
+ export interface FilePickerMediaFile {
77
+ id: string;
78
+ name: string;
79
+ url?: string;
80
+ size?: string | number;
81
+ comment?: string;
82
+ type?: string;
83
+ }
84
+ export interface FilePickerProps {
85
+ onFetchFiles?: (search: string) => Promise<FilePickerMediaFile[]>;
86
+ enableMediaLibrary?: boolean;
87
+ filterImageOnly?: boolean;
88
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Format bytes to human-readable string
3
+ * @param bytes - The number of bytes to format
4
+ * @returns Formatted string (e.g., "1.5 KB", "2.3 MB")
5
+ */
6
+ export declare function formatBytes(bytes: number): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsol-oss/react-datatable5",
3
- "version": "12.0.0-beta.75",
3
+ "version": "12.0.0-beta.76",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",