@bsol-oss/react-datatable5 13.0.1-beta.38 → 13.0.1-beta.39

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
@@ -743,7 +743,7 @@ type MediaLibraryBrowserPropsMultiple = MediaLibraryBrowserPropsBase & {
743
743
  onSelectedFileChange?: (files: FilePickerMediaFile[]) => void;
744
744
  };
745
745
  type MediaLibraryBrowserProps = MediaLibraryBrowserPropsSingle | MediaLibraryBrowserPropsMultiple;
746
- declare const MediaLibraryBrowser: ({ onFetchFiles, filterImageOnly, labels, enabled, multiple, onFileSelect, selectedFile: controlledSelectedFile, onSelectedFileChange, }: MediaLibraryBrowserProps) => react_jsx_runtime.JSX.Element | null;
746
+ declare const MediaLibraryBrowser: ({ onFetchFiles, filterImageOnly, labels, enabled, multiple, onFileSelect, selectedFile: controlledSelectedFile, onSelectedFileChange, }: MediaLibraryBrowserProps) => react_jsx_runtime.JSX.Element;
747
747
 
748
748
  interface UseFormProps<T> {
749
749
  preLoadedValues?: T | undefined;
package/dist/index.js CHANGED
@@ -5381,108 +5381,81 @@ function formatBytes(bytes) {
5381
5381
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
5382
5382
  }
5383
5383
 
5384
+ const IMAGE_EXT = /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i;
5385
+ function filterImageFiles(files) {
5386
+ return files.filter((f) => IMAGE_EXT.test(f.name));
5387
+ }
5384
5388
  const MediaLibraryBrowser = ({ onFetchFiles, filterImageOnly = false, labels, enabled = true, multiple = false, onFileSelect, selectedFile: controlledSelectedFile, onSelectedFileChange, }) => {
5385
- const [searchTerm, setSearchTerm] = React.useState('');
5386
- const [internalSelectedFile, setInternalSelectedFile] = React.useState(multiple ? [] : undefined);
5387
- const [failedImageIds, setFailedImageIds] = React.useState(new Set());
5388
- // Use controlled or internal state for selectedFile
5389
- const selectedFile = controlledSelectedFile ?? internalSelectedFile;
5390
- const setSelectedFile = onSelectedFileChange ?? setInternalSelectedFile;
5391
- const { data: filesData, isLoading, isError, } = reactQuery.useQuery({
5392
- queryKey: ['file-picker-library', searchTerm],
5389
+ const [search, setSearch] = React.useState('');
5390
+ const query = reactQuery.useQuery({
5391
+ queryKey: ['media-library', search, filterImageOnly],
5393
5392
  queryFn: async () => {
5394
5393
  if (!onFetchFiles)
5395
- return { data: [] };
5396
- const files = await onFetchFiles(searchTerm.trim() || '');
5397
- return { data: files };
5394
+ return [];
5395
+ const list = await onFetchFiles(search);
5396
+ return filterImageOnly ? filterImageFiles(list) : list;
5398
5397
  },
5399
5398
  enabled: enabled && !!onFetchFiles,
5400
5399
  });
5401
- const files = (filesData?.data || []);
5402
- const filteredFiles = filterImageOnly
5403
- ? files.filter((file) => /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name))
5404
- : files;
5405
- const handleFileClick = (file) => {
5406
- if (multiple) {
5407
- const currentSelection = Array.isArray(selectedFile) ? selectedFile : [];
5408
- const isAlreadySelected = currentSelection.some((f) => f.id === file.id);
5409
- const newSelection = isAlreadySelected
5410
- ? currentSelection.filter((f) => f.id !== file.id)
5411
- : [...currentSelection, file];
5412
- setSelectedFile(newSelection);
5413
- if (onFileSelect) {
5414
- onFileSelect(newSelection);
5415
- }
5416
- }
5417
- else {
5418
- const newFile = selectedFile === file ? undefined : file;
5419
- setSelectedFile(newFile);
5420
- if (onFileSelect && newFile) {
5421
- onFileSelect(newFile);
5422
- }
5400
+ const files = React.useMemo(() => (query.data ?? []), [query.data]);
5401
+ const selectedIds = React.useMemo(() => {
5402
+ if (multiple && Array.isArray(controlledSelectedFile)) {
5403
+ return new Set(controlledSelectedFile.map((f) => f.id));
5404
+ }
5405
+ if (!multiple &&
5406
+ controlledSelectedFile &&
5407
+ !Array.isArray(controlledSelectedFile)) {
5408
+ return new Set([controlledSelectedFile.id]);
5409
+ }
5410
+ return new Set();
5411
+ }, [multiple, controlledSelectedFile]);
5412
+ const handleSingleSelect = (file) => {
5413
+ if (!multiple) {
5414
+ onSelectedFileChange?.(file);
5415
+ onFileSelect?.(file);
5423
5416
  }
5424
5417
  };
5425
- const handleImageError = (fileId) => {
5426
- setFailedImageIds((prev) => new Set(prev).add(fileId));
5418
+ const handleMultipleToggle = (file, checked) => {
5419
+ const current = multiple && Array.isArray(controlledSelectedFile)
5420
+ ? [...controlledSelectedFile]
5421
+ : [];
5422
+ const next = checked
5423
+ ? [...current, file]
5424
+ : current.filter((f) => f.id !== file.id);
5425
+ onSelectedFileChange?.(next);
5426
+ onFileSelect?.(next);
5427
5427
  };
5428
- if (!onFetchFiles)
5429
- return null;
5430
- return (jsxRuntime.jsxs(react.VStack, { align: "stretch", gap: 4, children: [jsxRuntime.jsxs(react.Box, { position: "relative", children: [jsxRuntime.jsx(react.Input, { placeholder: labels?.searchPlaceholder ?? 'Search files...', value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), bg: "bg.panel", border: "1px solid", borderColor: "border.default", colorPalette: "blue", _focus: {
5431
- borderColor: 'colorPalette.500',
5432
- _dark: {
5433
- borderColor: 'colorPalette.400',
5434
- },
5435
- boxShadow: {
5436
- base: '0 0 0 1px var(--chakra-colors-blue-500)',
5437
- _dark: '0 0 0 1px var(--chakra-colors-blue-400)',
5438
- },
5439
- }, 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 ?? 'Loading files...' })] })), isError && (jsxRuntime.jsx(react.Box, { bg: { base: 'colorPalette.50', _dark: 'colorPalette.900/20' }, border: "1px solid", borderColor: {
5440
- base: 'colorPalette.200',
5441
- _dark: 'colorPalette.800',
5442
- }, colorPalette: "red", borderRadius: "md", p: 4, children: jsxRuntime.jsx(react.Text, { color: {
5443
- base: 'colorPalette.600',
5444
- _dark: 'colorPalette.300',
5445
- }, children: labels?.loadingFailed ?? '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 ?? 'No files found' }) })) : (jsxRuntime.jsx(react.VStack, { align: "stretch", gap: 2, children: filteredFiles.map((file) => {
5446
- const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name);
5447
- const isSelected = multiple
5448
- ? Array.isArray(selectedFile) &&
5449
- selectedFile.some((f) => f.id === file.id)
5450
- : selectedFile?.id ===
5451
- file.id;
5452
- const imageFailed = failedImageIds.has(file.id);
5453
- return (jsxRuntime.jsx(react.Box, { p: 3, border: "2px solid", borderColor: isSelected
5454
- ? {
5455
- base: 'colorPalette.500',
5456
- _dark: 'colorPalette.400',
5457
- }
5458
- : 'border.default', borderRadius: "md", bg: isSelected
5459
- ? {
5460
- base: 'colorPalette.50',
5461
- _dark: 'colorPalette.900/20',
5462
- }
5463
- : 'bg.panel', colorPalette: "blue", cursor: "pointer", onClick: () => handleFileClick(file), _hover: {
5464
- borderColor: isSelected
5465
- ? {
5466
- base: 'colorPalette.600',
5467
- _dark: 'colorPalette.400',
5468
- }
5469
- : {
5470
- base: 'colorPalette.300',
5471
- _dark: 'colorPalette.400',
5472
- },
5473
- bg: isSelected
5474
- ? {
5475
- base: 'colorPalette.100',
5476
- _dark: 'colorPalette.800/30',
5477
- }
5478
- : 'bg.muted',
5479
- }, 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 && !imageFailed ? (jsxRuntime.jsx(react.Image, { src: file.url, alt: file.name, boxSize: "60px", objectFit: "cover", borderRadius: "md", onError: () => handleImageError(file.id) })) : isImage && (imageFailed || !file.url) ? (jsxRuntime.jsx(react.Icon, { as: lu.LuImage, boxSize: 6, color: "fg.muted" })) : (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'
5480
- ? formatBytes(file.size)
5481
- : 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: {
5482
- base: 'colorPalette.500',
5483
- _dark: 'colorPalette.400',
5484
- }, colorPalette: "blue", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, children: jsxRuntime.jsx(react.Text, { color: "white", fontSize: "xs", fontWeight: "bold", children: "\u2713" }) }))] }) }, file.id));
5485
- }) })) }))] }));
5428
+ const isLoading = query.isPending;
5429
+ const isError = query.isError;
5430
+ const searchPlaceholder = labels?.searchPlaceholder ?? 'Search files...';
5431
+ const loadingText = labels?.loading ?? 'Loading...';
5432
+ const errorText = labels?.loadingFailed ?? 'Failed to load files';
5433
+ const emptyText = labels?.noFilesFound ?? 'No files found';
5434
+ return (jsxRuntime.jsxs(react.VStack, { align: "stretch", gap: 4, children: [jsxRuntime.jsx(InputGroup, { startElement: jsxRuntime.jsx(react.Icon, { as: lu.LuSearch, color: "fg.muted" }), children: jsxRuntime.jsx(react.Input, { placeholder: searchPlaceholder, value: search, onChange: (e) => setSearch(e.target.value), bg: "bg.panel", borderColor: "border.default" }) }), isLoading && (jsxRuntime.jsxs(react.HStack, { gap: 2, py: 6, justify: "center", children: [jsxRuntime.jsx(react.Spinner, { size: "sm", colorPalette: "blue" }), jsxRuntime.jsx(react.Text, { fontSize: "sm", color: "fg.muted", children: loadingText })] })), isError && (jsxRuntime.jsx(react.Box, { py: 4, px: 3, borderRadius: "md", bg: { base: 'red.50', _dark: 'red.900/20' }, borderWidth: "1px", borderColor: { base: 'red.200', _dark: 'red.800' }, children: jsxRuntime.jsx(react.Text, { fontSize: "sm", color: { base: 'red.600', _dark: 'red.300' }, children: errorText }) })), !isLoading && !isError && files.length === 0 && (jsxRuntime.jsx(react.Box, { py: 6, textAlign: "center", children: jsxRuntime.jsx(react.Text, { fontSize: "sm", color: "fg.muted", children: emptyText }) })), !isLoading && !isError && files.length > 0 && (jsxRuntime.jsx(react.SimpleGrid, { columns: { base: 2, sm: 3, md: 4 }, gap: 3, children: files.map((file) => {
5435
+ const isImage = IMAGE_EXT.test(file.name);
5436
+ const isSelected = selectedIds.has(file.id);
5437
+ const fileSize = typeof file.size === 'number'
5438
+ ? formatBytes(file.size)
5439
+ : file.size ?? null;
5440
+ if (multiple) {
5441
+ return (jsxRuntime.jsxs(CheckboxCard, { checked: isSelected, onCheckedChange: (e) => handleMultipleToggle(file, e.checked === true), variant: "outline", borderColor: "border.default", _hover: { borderColor: 'border.emphasized', bg: 'bg.muted' }, cursor: "pointer", children: [jsxRuntime.jsx(react.Box, { width: "100%", aspectRatio: 1, bg: "bg.muted", borderRadius: "md", overflow: "hidden", mb: 2, display: "flex", alignItems: "center", justifyContent: "center", children: isImage && file.url ? (jsxRuntime.jsx(react.Image, { src: file.url, alt: file.name, width: "100%", height: "100%", objectFit: "cover" })) : isImage ? (jsxRuntime.jsx(react.Icon, { as: lu.LuImage, boxSize: 8, color: "fg.muted" })) : (jsxRuntime.jsx(react.Icon, { as: lu.LuFile, boxSize: 8, color: "fg.muted" })) }), jsxRuntime.jsx(react.Text, { fontSize: "xs", fontWeight: "medium", color: "fg.default", lineClamp: 2, children: file.name }), fileSize && (jsxRuntime.jsx(react.Text, { fontSize: "xs", color: "fg.muted", children: fileSize }))] }, file.id));
5442
+ }
5443
+ return (jsxRuntime.jsxs(react.Box, { role: "button", tabIndex: 0, onClick: () => handleSingleSelect(file), onKeyDown: (e) => {
5444
+ if (e.key === 'Enter' || e.key === ' ') {
5445
+ e.preventDefault();
5446
+ handleSingleSelect(file);
5447
+ }
5448
+ }, padding: 3, borderRadius: "md", borderWidth: "2px", borderColor: isSelected ? 'colorPalette.500' : 'border.default', bg: isSelected
5449
+ ? { base: 'colorPalette.50', _dark: 'colorPalette.900/20' }
5450
+ : 'bg.panel', _hover: {
5451
+ borderColor: isSelected
5452
+ ? 'colorPalette.500'
5453
+ : 'border.emphasized',
5454
+ bg: isSelected
5455
+ ? { base: 'colorPalette.50', _dark: 'colorPalette.900/20' }
5456
+ : 'bg.muted',
5457
+ }, cursor: "pointer", transition: "all 0.2s", children: [jsxRuntime.jsx(react.Box, { width: "100%", aspectRatio: 1, bg: "bg.muted", borderRadius: "md", overflow: "hidden", mb: 2, display: "flex", alignItems: "center", justifyContent: "center", children: isImage && file.url ? (jsxRuntime.jsx(react.Image, { src: file.url, alt: file.name, width: "100%", height: "100%", objectFit: "cover" })) : isImage ? (jsxRuntime.jsx(react.Icon, { as: lu.LuImage, boxSize: 8, color: "fg.muted" })) : (jsxRuntime.jsx(react.Icon, { as: lu.LuFile, boxSize: 8, color: "fg.muted" })) }), jsxRuntime.jsx(react.Text, { fontSize: "xs", fontWeight: "medium", color: "fg.default", lineClamp: 2, children: file.name }), fileSize && (jsxRuntime.jsx(react.Text, { fontSize: "xs", color: "fg.muted", children: fileSize }))] }, file.id));
5458
+ }) }))] }));
5486
5459
  };
5487
5460
 
5488
5461
  function MediaBrowserDialog({ open, onClose, onSelect, title, filterImageOnly = false, onFetchFiles, onUploadFile, enableUpload = false, labels, }) {
@@ -5650,7 +5623,8 @@ const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
5650
5623
  const { required, gridColumn = 'span 12', gridRow = 'span 1', filePicker, type, } = schema;
5651
5624
  const isRequired = required?.some((columnId) => columnId === column);
5652
5625
  const isSingleSelect = type === 'string';
5653
- const currentValue = watch(column) ?? (isSingleSelect ? '' : []);
5626
+ const colLabel = formI18n.colLabel;
5627
+ const currentValue = watch(colLabel) ?? (isSingleSelect ? '' : []);
5654
5628
  // Handle string IDs only
5655
5629
  const currentFileIds = isSingleSelect
5656
5630
  ? currentValue
@@ -5659,7 +5633,6 @@ const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
5659
5633
  : Array.isArray(currentValue)
5660
5634
  ? currentValue
5661
5635
  : [];
5662
- const colLabel = formI18n.colLabel;
5663
5636
  const fieldError = getNestedError(errors, colLabel);
5664
5637
  const [dialogOpen, setDialogOpen] = React.useState(false);
5665
5638
  const [failedImageIds, setFailedImageIds] = React.useState(new Set());
@@ -5747,7 +5720,7 @@ const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
5747
5720
  : /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(fileId);
5748
5721
  const imageFailed = failedImageIds.has(fileId);
5749
5722
  const displayName = file?.name ?? fileId;
5750
- return (jsxRuntime.jsx(react.Card.Root, { variant: 'subtle', colorPalette: "blue", 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: {
5723
+ 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: {
5751
5724
  borderColor: 'colorPalette.300',
5752
5725
  bg: 'bg.muted',
5753
5726
  }, 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", overflow: "hidden", children: isImage && file?.url && !imageFailed ? (jsxRuntime.jsx(react.Image, { src: file.url, alt: displayName, boxSize: "60px", objectFit: "cover", onError: () => handleImageError(fileId) })) : isImage && !imageFailed ? (jsxRuntime.jsx(react.Icon, { as: lu.LuImage, boxSize: 6, color: "fg.muted" })) : (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: displayName }), file?.size && (jsxRuntime.jsx(react.Text, { fontSize: "xs", color: "fg.muted", children: typeof file.size === 'number'
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { Button as Button$1, AbsoluteCenter, Spinner, Span, IconButton, Portal, Dialog, Flex, Text, useDisclosure, DialogBackdrop, RadioGroup as RadioGroup$1, Grid, Box, Slider as Slider$1, HStack, For, CheckboxCard as CheckboxCard$1, Input, Menu, createRecipeContext, createContext as createContext$1, Pagination as Pagination$1, usePaginationContext, Tooltip as Tooltip$1, Group, InputElement, Tag as Tag$1, Checkbox as Checkbox$1, Icon, VStack, Heading, EmptyState as EmptyState$2, List, Table as Table$1, Card, MenuRoot as MenuRoot$1, MenuTrigger as MenuTrigger$1, Clipboard, Badge, Link, Image, Alert, Field as Field$1, Popover, useFilter, useListCollection, Combobox, Tabs, useCombobox, Show, Skeleton, NumberInput, Textarea as Textarea$1, InputGroup as InputGroup$1, Select, Stack } from '@chakra-ui/react';
2
+ import { Button as Button$1, AbsoluteCenter, Spinner, Span, IconButton, Portal, Dialog, Flex, Text, useDisclosure, DialogBackdrop, RadioGroup as RadioGroup$1, Grid, Box, Slider as Slider$1, HStack, For, CheckboxCard as CheckboxCard$1, Input, Menu, createRecipeContext, createContext as createContext$1, Pagination as Pagination$1, usePaginationContext, Tooltip as Tooltip$1, Group, InputElement, Tag as Tag$1, Checkbox as Checkbox$1, Icon, VStack, Heading, EmptyState as EmptyState$2, List, Table as Table$1, Card, MenuRoot as MenuRoot$1, MenuTrigger as MenuTrigger$1, Clipboard, Badge, Link, Image, Alert, Field as Field$1, Popover, useFilter, useListCollection, Combobox, SimpleGrid, Tabs, useCombobox, Show, Skeleton, NumberInput, Textarea as Textarea$1, InputGroup as InputGroup$1, Select, Stack } from '@chakra-ui/react';
3
3
  import { AiOutlineColumnWidth } from 'react-icons/ai';
4
4
  import * as React from 'react';
5
5
  import { createContext, useContext, useState, useMemo, useCallback, useEffect, useRef } from 'react';
@@ -5361,108 +5361,81 @@ function formatBytes(bytes) {
5361
5361
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
5362
5362
  }
5363
5363
 
5364
+ const IMAGE_EXT = /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i;
5365
+ function filterImageFiles(files) {
5366
+ return files.filter((f) => IMAGE_EXT.test(f.name));
5367
+ }
5364
5368
  const MediaLibraryBrowser = ({ onFetchFiles, filterImageOnly = false, labels, enabled = true, multiple = false, onFileSelect, selectedFile: controlledSelectedFile, onSelectedFileChange, }) => {
5365
- const [searchTerm, setSearchTerm] = useState('');
5366
- const [internalSelectedFile, setInternalSelectedFile] = useState(multiple ? [] : undefined);
5367
- const [failedImageIds, setFailedImageIds] = useState(new Set());
5368
- // Use controlled or internal state for selectedFile
5369
- const selectedFile = controlledSelectedFile ?? internalSelectedFile;
5370
- const setSelectedFile = onSelectedFileChange ?? setInternalSelectedFile;
5371
- const { data: filesData, isLoading, isError, } = useQuery({
5372
- queryKey: ['file-picker-library', searchTerm],
5369
+ const [search, setSearch] = useState('');
5370
+ const query = useQuery({
5371
+ queryKey: ['media-library', search, filterImageOnly],
5373
5372
  queryFn: async () => {
5374
5373
  if (!onFetchFiles)
5375
- return { data: [] };
5376
- const files = await onFetchFiles(searchTerm.trim() || '');
5377
- return { data: files };
5374
+ return [];
5375
+ const list = await onFetchFiles(search);
5376
+ return filterImageOnly ? filterImageFiles(list) : list;
5378
5377
  },
5379
5378
  enabled: enabled && !!onFetchFiles,
5380
5379
  });
5381
- const files = (filesData?.data || []);
5382
- const filteredFiles = filterImageOnly
5383
- ? files.filter((file) => /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name))
5384
- : files;
5385
- const handleFileClick = (file) => {
5386
- if (multiple) {
5387
- const currentSelection = Array.isArray(selectedFile) ? selectedFile : [];
5388
- const isAlreadySelected = currentSelection.some((f) => f.id === file.id);
5389
- const newSelection = isAlreadySelected
5390
- ? currentSelection.filter((f) => f.id !== file.id)
5391
- : [...currentSelection, file];
5392
- setSelectedFile(newSelection);
5393
- if (onFileSelect) {
5394
- onFileSelect(newSelection);
5395
- }
5396
- }
5397
- else {
5398
- const newFile = selectedFile === file ? undefined : file;
5399
- setSelectedFile(newFile);
5400
- if (onFileSelect && newFile) {
5401
- onFileSelect(newFile);
5402
- }
5380
+ const files = useMemo(() => (query.data ?? []), [query.data]);
5381
+ const selectedIds = useMemo(() => {
5382
+ if (multiple && Array.isArray(controlledSelectedFile)) {
5383
+ return new Set(controlledSelectedFile.map((f) => f.id));
5384
+ }
5385
+ if (!multiple &&
5386
+ controlledSelectedFile &&
5387
+ !Array.isArray(controlledSelectedFile)) {
5388
+ return new Set([controlledSelectedFile.id]);
5389
+ }
5390
+ return new Set();
5391
+ }, [multiple, controlledSelectedFile]);
5392
+ const handleSingleSelect = (file) => {
5393
+ if (!multiple) {
5394
+ onSelectedFileChange?.(file);
5395
+ onFileSelect?.(file);
5403
5396
  }
5404
5397
  };
5405
- const handleImageError = (fileId) => {
5406
- setFailedImageIds((prev) => new Set(prev).add(fileId));
5398
+ const handleMultipleToggle = (file, checked) => {
5399
+ const current = multiple && Array.isArray(controlledSelectedFile)
5400
+ ? [...controlledSelectedFile]
5401
+ : [];
5402
+ const next = checked
5403
+ ? [...current, file]
5404
+ : current.filter((f) => f.id !== file.id);
5405
+ onSelectedFileChange?.(next);
5406
+ onFileSelect?.(next);
5407
5407
  };
5408
- if (!onFetchFiles)
5409
- return null;
5410
- return (jsxs(VStack, { align: "stretch", gap: 4, children: [jsxs(Box, { position: "relative", children: [jsx(Input, { placeholder: labels?.searchPlaceholder ?? 'Search files...', value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), bg: "bg.panel", border: "1px solid", borderColor: "border.default", colorPalette: "blue", _focus: {
5411
- borderColor: 'colorPalette.500',
5412
- _dark: {
5413
- borderColor: 'colorPalette.400',
5414
- },
5415
- boxShadow: {
5416
- base: '0 0 0 1px var(--chakra-colors-blue-500)',
5417
- _dark: '0 0 0 1px var(--chakra-colors-blue-400)',
5418
- },
5419
- }, 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 ?? 'Loading files...' })] })), isError && (jsx(Box, { bg: { base: 'colorPalette.50', _dark: 'colorPalette.900/20' }, border: "1px solid", borderColor: {
5420
- base: 'colorPalette.200',
5421
- _dark: 'colorPalette.800',
5422
- }, colorPalette: "red", borderRadius: "md", p: 4, children: jsx(Text, { color: {
5423
- base: 'colorPalette.600',
5424
- _dark: 'colorPalette.300',
5425
- }, children: labels?.loadingFailed ?? '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 ?? 'No files found' }) })) : (jsx(VStack, { align: "stretch", gap: 2, children: filteredFiles.map((file) => {
5426
- const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name);
5427
- const isSelected = multiple
5428
- ? Array.isArray(selectedFile) &&
5429
- selectedFile.some((f) => f.id === file.id)
5430
- : selectedFile?.id ===
5431
- file.id;
5432
- const imageFailed = failedImageIds.has(file.id);
5433
- return (jsx(Box, { p: 3, border: "2px solid", borderColor: isSelected
5434
- ? {
5435
- base: 'colorPalette.500',
5436
- _dark: 'colorPalette.400',
5437
- }
5438
- : 'border.default', borderRadius: "md", bg: isSelected
5439
- ? {
5440
- base: 'colorPalette.50',
5441
- _dark: 'colorPalette.900/20',
5442
- }
5443
- : 'bg.panel', colorPalette: "blue", cursor: "pointer", onClick: () => handleFileClick(file), _hover: {
5444
- borderColor: isSelected
5445
- ? {
5446
- base: 'colorPalette.600',
5447
- _dark: 'colorPalette.400',
5448
- }
5449
- : {
5450
- base: 'colorPalette.300',
5451
- _dark: 'colorPalette.400',
5452
- },
5453
- bg: isSelected
5454
- ? {
5455
- base: 'colorPalette.100',
5456
- _dark: 'colorPalette.800/30',
5457
- }
5458
- : 'bg.muted',
5459
- }, 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 && !imageFailed ? (jsx(Image, { src: file.url, alt: file.name, boxSize: "60px", objectFit: "cover", borderRadius: "md", onError: () => handleImageError(file.id) })) : isImage && (imageFailed || !file.url) ? (jsx(Icon, { as: LuImage, boxSize: 6, color: "fg.muted" })) : (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'
5460
- ? formatBytes(file.size)
5461
- : 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: {
5462
- base: 'colorPalette.500',
5463
- _dark: 'colorPalette.400',
5464
- }, colorPalette: "blue", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, children: jsx(Text, { color: "white", fontSize: "xs", fontWeight: "bold", children: "\u2713" }) }))] }) }, file.id));
5465
- }) })) }))] }));
5408
+ const isLoading = query.isPending;
5409
+ const isError = query.isError;
5410
+ const searchPlaceholder = labels?.searchPlaceholder ?? 'Search files...';
5411
+ const loadingText = labels?.loading ?? 'Loading...';
5412
+ const errorText = labels?.loadingFailed ?? 'Failed to load files';
5413
+ const emptyText = labels?.noFilesFound ?? 'No files found';
5414
+ return (jsxs(VStack, { align: "stretch", gap: 4, children: [jsx(InputGroup, { startElement: jsx(Icon, { as: LuSearch, color: "fg.muted" }), children: jsx(Input, { placeholder: searchPlaceholder, value: search, onChange: (e) => setSearch(e.target.value), bg: "bg.panel", borderColor: "border.default" }) }), isLoading && (jsxs(HStack, { gap: 2, py: 6, justify: "center", children: [jsx(Spinner, { size: "sm", colorPalette: "blue" }), jsx(Text, { fontSize: "sm", color: "fg.muted", children: loadingText })] })), isError && (jsx(Box, { py: 4, px: 3, borderRadius: "md", bg: { base: 'red.50', _dark: 'red.900/20' }, borderWidth: "1px", borderColor: { base: 'red.200', _dark: 'red.800' }, children: jsx(Text, { fontSize: "sm", color: { base: 'red.600', _dark: 'red.300' }, children: errorText }) })), !isLoading && !isError && files.length === 0 && (jsx(Box, { py: 6, textAlign: "center", children: jsx(Text, { fontSize: "sm", color: "fg.muted", children: emptyText }) })), !isLoading && !isError && files.length > 0 && (jsx(SimpleGrid, { columns: { base: 2, sm: 3, md: 4 }, gap: 3, children: files.map((file) => {
5415
+ const isImage = IMAGE_EXT.test(file.name);
5416
+ const isSelected = selectedIds.has(file.id);
5417
+ const fileSize = typeof file.size === 'number'
5418
+ ? formatBytes(file.size)
5419
+ : file.size ?? null;
5420
+ if (multiple) {
5421
+ return (jsxs(CheckboxCard, { checked: isSelected, onCheckedChange: (e) => handleMultipleToggle(file, e.checked === true), variant: "outline", borderColor: "border.default", _hover: { borderColor: 'border.emphasized', bg: 'bg.muted' }, cursor: "pointer", children: [jsx(Box, { width: "100%", aspectRatio: 1, bg: "bg.muted", borderRadius: "md", overflow: "hidden", mb: 2, display: "flex", alignItems: "center", justifyContent: "center", children: isImage && file.url ? (jsx(Image, { src: file.url, alt: file.name, width: "100%", height: "100%", objectFit: "cover" })) : isImage ? (jsx(Icon, { as: LuImage, boxSize: 8, color: "fg.muted" })) : (jsx(Icon, { as: LuFile, boxSize: 8, color: "fg.muted" })) }), jsx(Text, { fontSize: "xs", fontWeight: "medium", color: "fg.default", lineClamp: 2, children: file.name }), fileSize && (jsx(Text, { fontSize: "xs", color: "fg.muted", children: fileSize }))] }, file.id));
5422
+ }
5423
+ return (jsxs(Box, { role: "button", tabIndex: 0, onClick: () => handleSingleSelect(file), onKeyDown: (e) => {
5424
+ if (e.key === 'Enter' || e.key === ' ') {
5425
+ e.preventDefault();
5426
+ handleSingleSelect(file);
5427
+ }
5428
+ }, padding: 3, borderRadius: "md", borderWidth: "2px", borderColor: isSelected ? 'colorPalette.500' : 'border.default', bg: isSelected
5429
+ ? { base: 'colorPalette.50', _dark: 'colorPalette.900/20' }
5430
+ : 'bg.panel', _hover: {
5431
+ borderColor: isSelected
5432
+ ? 'colorPalette.500'
5433
+ : 'border.emphasized',
5434
+ bg: isSelected
5435
+ ? { base: 'colorPalette.50', _dark: 'colorPalette.900/20' }
5436
+ : 'bg.muted',
5437
+ }, cursor: "pointer", transition: "all 0.2s", children: [jsx(Box, { width: "100%", aspectRatio: 1, bg: "bg.muted", borderRadius: "md", overflow: "hidden", mb: 2, display: "flex", alignItems: "center", justifyContent: "center", children: isImage && file.url ? (jsx(Image, { src: file.url, alt: file.name, width: "100%", height: "100%", objectFit: "cover" })) : isImage ? (jsx(Icon, { as: LuImage, boxSize: 8, color: "fg.muted" })) : (jsx(Icon, { as: LuFile, boxSize: 8, color: "fg.muted" })) }), jsx(Text, { fontSize: "xs", fontWeight: "medium", color: "fg.default", lineClamp: 2, children: file.name }), fileSize && (jsx(Text, { fontSize: "xs", color: "fg.muted", children: fileSize }))] }, file.id));
5438
+ }) }))] }));
5466
5439
  };
5467
5440
 
5468
5441
  function MediaBrowserDialog({ open, onClose, onSelect, title, filterImageOnly = false, onFetchFiles, onUploadFile, enableUpload = false, labels, }) {
@@ -5630,7 +5603,8 @@ const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
5630
5603
  const { required, gridColumn = 'span 12', gridRow = 'span 1', filePicker, type, } = schema;
5631
5604
  const isRequired = required?.some((columnId) => columnId === column);
5632
5605
  const isSingleSelect = type === 'string';
5633
- const currentValue = watch(column) ?? (isSingleSelect ? '' : []);
5606
+ const colLabel = formI18n.colLabel;
5607
+ const currentValue = watch(colLabel) ?? (isSingleSelect ? '' : []);
5634
5608
  // Handle string IDs only
5635
5609
  const currentFileIds = isSingleSelect
5636
5610
  ? currentValue
@@ -5639,7 +5613,6 @@ const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
5639
5613
  : Array.isArray(currentValue)
5640
5614
  ? currentValue
5641
5615
  : [];
5642
- const colLabel = formI18n.colLabel;
5643
5616
  const fieldError = getNestedError(errors, colLabel);
5644
5617
  const [dialogOpen, setDialogOpen] = useState(false);
5645
5618
  const [failedImageIds, setFailedImageIds] = useState(new Set());
@@ -5727,7 +5700,7 @@ const FormMediaLibraryBrowser = ({ column, schema, prefix, }) => {
5727
5700
  : /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(fileId);
5728
5701
  const imageFailed = failedImageIds.has(fileId);
5729
5702
  const displayName = file?.name ?? fileId;
5730
- return (jsx(Card.Root, { variant: 'subtle', colorPalette: "blue", 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: {
5703
+ 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: {
5731
5704
  borderColor: 'colorPalette.300',
5732
5705
  bg: 'bg.muted',
5733
5706
  }, 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", overflow: "hidden", children: isImage && file?.url && !imageFailed ? (jsx(Image, { src: file.url, alt: displayName, boxSize: "60px", objectFit: "cover", onError: () => handleImageError(fileId) })) : isImage && !imageFailed ? (jsx(Icon, { as: LuImage, boxSize: 6, color: "fg.muted" })) : (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: displayName }), file?.size && (jsx(Text, { fontSize: "xs", color: "fg.muted", children: typeof file.size === 'number'
@@ -18,5 +18,5 @@ type MediaLibraryBrowserPropsMultiple = MediaLibraryBrowserPropsBase & {
18
18
  onSelectedFileChange?: (files: FilePickerMediaFile[]) => void;
19
19
  };
20
20
  export type MediaLibraryBrowserProps = MediaLibraryBrowserPropsSingle | MediaLibraryBrowserPropsMultiple;
21
- export declare const MediaLibraryBrowser: ({ onFetchFiles, filterImageOnly, labels, enabled, multiple, onFileSelect, selectedFile: controlledSelectedFile, onSelectedFileChange, }: MediaLibraryBrowserProps) => import("react/jsx-runtime").JSX.Element | null;
21
+ export declare const MediaLibraryBrowser: ({ onFetchFiles, filterImageOnly, labels, enabled, multiple, onFileSelect, selectedFile: controlledSelectedFile, onSelectedFileChange, }: MediaLibraryBrowserProps) => import("react/jsx-runtime").JSX.Element;
22
22
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsol-oss/react-datatable5",
3
- "version": "13.0.1-beta.38",
3
+ "version": "13.0.1-beta.39",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",