@umodeapporg/ui 0.22.1 → 0.22.3
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/components/Form/ControlledDrawer.js +1 -1
- package/dist/components/Form/ControlledDrawer.js.map +1 -1
- package/dist/components/Search/filters/FilterFieldControl.js +3 -0
- package/dist/components/Search/filters/FilterFieldControl.js.map +1 -1
- package/dist/components/Search/filters/FiltersPanel.js +1 -0
- package/dist/components/Search/filters/FiltersPanel.js.map +1 -1
- package/dist/components/UI/DataTable/useDataTableInstance.js +4 -4
- package/dist/components/UI/DataTable/useDataTableInstance.js.map +1 -1
- package/package.json +1 -1
|
@@ -103,7 +103,7 @@ const ControlledDrawer = ({
|
|
|
103
103
|
useEffect(() => {
|
|
104
104
|
if (!isOpen || dismiss?.enabled === false) return void 0;
|
|
105
105
|
const handleKeyDown = (event) => {
|
|
106
|
-
if (event.key !== "Escape" || dismiss?.escapeKey === false) return;
|
|
106
|
+
if (event.key !== "Escape" || event.defaultPrevented || dismiss?.escapeKey === false) return;
|
|
107
107
|
onClose?.();
|
|
108
108
|
};
|
|
109
109
|
const handlePointerDown = (event) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControlledDrawer.js","sources":["../../../src/components/Form/ControlledDrawer.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport * as React from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { createDrawerVariants, scrimVariants } from '@/motion';\n\nconst DEFAULT_SIZE = 300;\nconst DEFAULT_MIN_RESIZE_WIDTH = 280;\nconst DEFAULT_MAX_RESIZE_VIEWPORT_RATIO = 0.8;\nconst KEYBOARD_RESIZE_STEP = 24;\n\nexport type TDrawerPlacement = 'left' | 'right' | 'top' | 'bottom';\n\nexport interface IDrawerDismissConfig {\n enabled?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean | ((event: PointerEvent) => boolean);\n}\n\nexport interface IControlledDrawerProps {\n open?: boolean;\n onClose?: () => void;\n placement?: TDrawerPlacement;\n /** Width for left/right placements, height for top/bottom, in pixels. */\n size?: number;\n className?: string;\n overlayClassName?: string;\n children?: React.ReactNode;\n dismiss?: IDrawerDismissConfig;\n\n /** Defer mounting the subtree until the first time `open` becomes true. */\n lazy?: boolean;\n /** Discard the subtree while closed, so its state resets on the next open. */\n unmountOnClose?: boolean;\n /** Fires once, the first time the drawer opens after this component mounted. */\n onFirstOpen?: () => void;\n /** Fires whenever `open` toggles. Argument is the new value. */\n onOpenChange?: (open: boolean) => void;\n\n resizable?: boolean;\n minResizeWidth?: number;\n maxResizeViewportRatio?: number;\n}\n\nconst PLACEMENT_CLASS: Record<TDrawerPlacement, string> = {\n left: 'inset-y-0 left-0',\n right: 'inset-y-0 right-0',\n top: 'inset-x-0 top-0',\n bottom: 'inset-x-0 bottom-0',\n};\n\n/**\n * The panel is removed from the DOM once its exit transition finishes. The Material\n * Tailwind drawer kept a closed panel mounted off-screen, and its shadow bled back into\n * the viewport: with one drawer that is invisible, with one per list row the bleeds\n * stacked into visible gray lines at the screen edge.\n */\nconst ControlledDrawer: React.FC<IControlledDrawerProps> = ({\n open,\n onClose,\n placement = 'left',\n size = DEFAULT_SIZE,\n className,\n overlayClassName,\n children,\n dismiss,\n lazy,\n unmountOnClose,\n onFirstOpen,\n onOpenChange,\n resizable = true,\n minResizeWidth = DEFAULT_MIN_RESIZE_WIDTH,\n maxResizeViewportRatio = DEFAULT_MAX_RESIZE_VIEWPORT_RATIO,\n}) => {\n const { t } = useTranslation('ui');\n\n const isOpen = Boolean(open);\n const panelRef = useRef<HTMLDivElement>(null);\n const previouslyFocusedRef = useRef<HTMLElement | null>(null);\n\n const [hasEverOpened, setHasEverOpened] = useState(isOpen);\n const [isLeaving, setIsLeaving] = useState(false);\n const prevOpenRef = useRef<boolean | undefined>(open);\n const firstOpenFiredRef = useRef(false);\n\n const isHorizontalPlacement = placement === 'left' || placement === 'right';\n const canResize = resizable && isHorizontalPlacement;\n\n const [resizedWidth, setResizedWidth] = useState<number | null>(null);\n const [isResizing, setIsResizing] = useState(false);\n const resizeStartRef = useRef<{ pointerX: number; width: number } | null>(null);\n const previousBaseSizeRef = useRef(size);\n\n const clampWidth = useCallback(\n (width: number) => {\n const maxWidth = Math.max(minResizeWidth, Math.round(window.innerWidth * maxResizeViewportRatio));\n return Math.min(maxWidth, Math.max(minResizeWidth, Math.round(width)));\n },\n [minResizeWidth, maxResizeViewportRatio],\n );\n\n useEffect(() => {\n if (previousBaseSizeRef.current === size) return;\n previousBaseSizeRef.current = size;\n setResizedWidth(null);\n }, [size]);\n\n useEffect(() => {\n if (!canResize) return undefined;\n\n const handleViewportResize = () => setResizedWidth((current) => (current === null ? null : clampWidth(current)));\n\n window.addEventListener('resize', handleViewportResize);\n return () => window.removeEventListener('resize', handleViewportResize);\n }, [canResize, clampWidth]);\n\n useEffect(() => {\n const previous = prevOpenRef.current;\n if (previous === isOpen) return;\n prevOpenRef.current = isOpen;\n\n if (isOpen && !hasEverOpened) setHasEverOpened(true);\n\n if (isOpen && !firstOpenFiredRef.current) {\n firstOpenFiredRef.current = true;\n onFirstOpen?.();\n }\n\n onOpenChange?.(isOpen);\n }, [isOpen, hasEverOpened, onFirstOpen, onOpenChange]);\n\n useEffect(() => {\n if (isOpen) setIsLeaving(true);\n }, [isOpen]);\n\n /* Held until the exit finishes, not until `open` flips: releasing it early brings the\n page scrollbar back while the panel is still travelling, and the content behind jumps. */\n useEffect(() => {\n if (!isOpen && !isLeaving) return undefined;\n\n const { overflow } = document.body.style;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = overflow;\n };\n }, [isOpen, isLeaving]);\n\n useEffect(() => {\n if (!isOpen) return undefined;\n\n previouslyFocusedRef.current = document.activeElement as HTMLElement | null;\n const frame = requestAnimationFrame(() => panelRef.current?.focus());\n\n return () => {\n cancelAnimationFrame(frame);\n previouslyFocusedRef.current?.focus?.();\n };\n }, [isOpen]);\n\n useEffect(() => {\n if (!isOpen || dismiss?.enabled === false) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || dismiss?.escapeKey === false) return;\n onClose?.();\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (dismiss?.outsidePress === false) return;\n const panel = panelRef.current;\n const target = event.target as Node | null;\n if (!panel || !target || panel.contains(target)) return;\n if (isPressOnLayeredSurface(target, panelRef.current)) return;\n if (typeof dismiss?.outsidePress === 'function' && !dismiss.outsidePress(event)) return;\n onClose?.();\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [isOpen, dismiss, onClose]);\n\n const handleResizeStart = (event: React.PointerEvent<HTMLButtonElement>) => {\n event.preventDefault();\n event.stopPropagation();\n resizeStartRef.current = { pointerX: event.clientX, width: resizedWidth ?? size };\n setIsResizing(true);\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const handleResizeMove = (event: React.PointerEvent<HTMLButtonElement>) => {\n const resizeStart = resizeStartRef.current;\n if (!resizeStart) return;\n const pointerDelta =\n placement === 'right' ? resizeStart.pointerX - event.clientX : event.clientX - resizeStart.pointerX;\n setResizedWidth(clampWidth(resizeStart.width + pointerDelta));\n };\n\n const handleResizeEnd = (event: React.PointerEvent<HTMLButtonElement>) => {\n if (!resizeStartRef.current) return;\n resizeStartRef.current = null;\n setIsResizing(false);\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n };\n\n const handleResizeKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;\n event.preventDefault();\n const pointerDirection = event.key === 'ArrowLeft' ? -1 : 1;\n const widthDirection = placement === 'right' ? -pointerDirection : pointerDirection;\n setResizedWidth((current) => clampWidth((current ?? size) + widthDirection * KEYBOARD_RESIZE_STEP));\n };\n\n if (lazy && !hasEverOpened) return null;\n\n const resolvedSize = isHorizontalPlacement ? (resizedWidth ?? size) : size;\n const shouldRenderChildren = unmountOnClose ? isOpen : !lazy || hasEverOpened;\n const drawerVariants = createDrawerVariants(placement);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence onExitComplete={() => setIsLeaving(false)}>\n {isOpen && (\n <motion.div\n key=\"drawer-scrim\"\n variants={scrimVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n aria-hidden\n onClick={() => {\n if (dismiss?.enabled === false || dismiss?.outsidePress === false) return;\n onClose?.();\n }}\n className={classNames('fixed inset-0 z-[9996] glass-scrim', overlayClassName)}\n />\n )}\n\n {isOpen && (\n <motion.div\n key=\"drawer-panel\"\n ref={panelRef}\n variants={drawerVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n aria-modal={true}\n tabIndex={-1}\n style={isHorizontalPlacement ? { width: resolvedSize } : { height: resolvedSize }}\n className={classNames(\n 'fixed z-[9997] flex max-w-full flex-col overflow-y-auto bg-surface shadow-xl outline-none',\n PLACEMENT_CLASS[placement],\n className,\n )}\n >\n {canResize && isOpen ? (\n <ControlledTooltip\n content={t('controlledDrawer.resizeHandle')}\n placement={placement === 'right' ? 'left' : 'right'}\n className=\"z-[9999]\"\n >\n <ControlledButton\n defaultHtmlButton\n onPointerDown={handleResizeStart}\n onPointerMove={handleResizeMove}\n onPointerUp={handleResizeEnd}\n onPointerCancel={handleResizeEnd}\n onKeyDown={handleResizeKeyDown}\n onDoubleClick={() => setResizedWidth(null)}\n className={classNames(\n 'group/resize absolute inset-y-0 z-10 flex w-3 cursor-col-resize touch-none select-none items-center justify-center focus:outline-none',\n placement === 'right' ? 'left-0' : 'right-0',\n )}\n >\n <span\n className={classNames(\n 'h-full w-0.5 rounded-full transition-colors duration-base',\n isResizing ? 'bg-brand-hover' : 'bg-transparent group-hover/resize:bg-brand-hover',\n )}\n />\n </ControlledButton>\n </ControlledTooltip>\n ) : null}\n\n {shouldRenderChildren ? children : null}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\nexport default ControlledDrawer;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ControlledDrawer.js","sources":["../../../src/components/Form/ControlledDrawer.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport * as React from 'react';\nimport classNames from 'classnames';\nimport { AnimatePresence, motion } from 'motion/react';\nimport { createPortal } from 'react-dom';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { isPressOnLayeredSurface } from '@/helpers/layeredSurfaces';\nimport { getPortalHost } from '@/helpers/portalHost';\nimport { createDrawerVariants, scrimVariants } from '@/motion';\n\nconst DEFAULT_SIZE = 300;\nconst DEFAULT_MIN_RESIZE_WIDTH = 280;\nconst DEFAULT_MAX_RESIZE_VIEWPORT_RATIO = 0.8;\nconst KEYBOARD_RESIZE_STEP = 24;\n\nexport type TDrawerPlacement = 'left' | 'right' | 'top' | 'bottom';\n\nexport interface IDrawerDismissConfig {\n enabled?: boolean;\n escapeKey?: boolean;\n outsidePress?: boolean | ((event: PointerEvent) => boolean);\n}\n\nexport interface IControlledDrawerProps {\n open?: boolean;\n onClose?: () => void;\n placement?: TDrawerPlacement;\n /** Width for left/right placements, height for top/bottom, in pixels. */\n size?: number;\n className?: string;\n overlayClassName?: string;\n children?: React.ReactNode;\n dismiss?: IDrawerDismissConfig;\n\n /** Defer mounting the subtree until the first time `open` becomes true. */\n lazy?: boolean;\n /** Discard the subtree while closed, so its state resets on the next open. */\n unmountOnClose?: boolean;\n /** Fires once, the first time the drawer opens after this component mounted. */\n onFirstOpen?: () => void;\n /** Fires whenever `open` toggles. Argument is the new value. */\n onOpenChange?: (open: boolean) => void;\n\n resizable?: boolean;\n minResizeWidth?: number;\n maxResizeViewportRatio?: number;\n}\n\nconst PLACEMENT_CLASS: Record<TDrawerPlacement, string> = {\n left: 'inset-y-0 left-0',\n right: 'inset-y-0 right-0',\n top: 'inset-x-0 top-0',\n bottom: 'inset-x-0 bottom-0',\n};\n\n/**\n * The panel is removed from the DOM once its exit transition finishes. The Material\n * Tailwind drawer kept a closed panel mounted off-screen, and its shadow bled back into\n * the viewport: with one drawer that is invisible, with one per list row the bleeds\n * stacked into visible gray lines at the screen edge.\n */\nconst ControlledDrawer: React.FC<IControlledDrawerProps> = ({\n open,\n onClose,\n placement = 'left',\n size = DEFAULT_SIZE,\n className,\n overlayClassName,\n children,\n dismiss,\n lazy,\n unmountOnClose,\n onFirstOpen,\n onOpenChange,\n resizable = true,\n minResizeWidth = DEFAULT_MIN_RESIZE_WIDTH,\n maxResizeViewportRatio = DEFAULT_MAX_RESIZE_VIEWPORT_RATIO,\n}) => {\n const { t } = useTranslation('ui');\n\n const isOpen = Boolean(open);\n const panelRef = useRef<HTMLDivElement>(null);\n const previouslyFocusedRef = useRef<HTMLElement | null>(null);\n\n const [hasEverOpened, setHasEverOpened] = useState(isOpen);\n const [isLeaving, setIsLeaving] = useState(false);\n const prevOpenRef = useRef<boolean | undefined>(open);\n const firstOpenFiredRef = useRef(false);\n\n const isHorizontalPlacement = placement === 'left' || placement === 'right';\n const canResize = resizable && isHorizontalPlacement;\n\n const [resizedWidth, setResizedWidth] = useState<number | null>(null);\n const [isResizing, setIsResizing] = useState(false);\n const resizeStartRef = useRef<{ pointerX: number; width: number } | null>(null);\n const previousBaseSizeRef = useRef(size);\n\n const clampWidth = useCallback(\n (width: number) => {\n const maxWidth = Math.max(minResizeWidth, Math.round(window.innerWidth * maxResizeViewportRatio));\n return Math.min(maxWidth, Math.max(minResizeWidth, Math.round(width)));\n },\n [minResizeWidth, maxResizeViewportRatio],\n );\n\n useEffect(() => {\n if (previousBaseSizeRef.current === size) return;\n previousBaseSizeRef.current = size;\n setResizedWidth(null);\n }, [size]);\n\n useEffect(() => {\n if (!canResize) return undefined;\n\n const handleViewportResize = () => setResizedWidth((current) => (current === null ? null : clampWidth(current)));\n\n window.addEventListener('resize', handleViewportResize);\n return () => window.removeEventListener('resize', handleViewportResize);\n }, [canResize, clampWidth]);\n\n useEffect(() => {\n const previous = prevOpenRef.current;\n if (previous === isOpen) return;\n prevOpenRef.current = isOpen;\n\n if (isOpen && !hasEverOpened) setHasEverOpened(true);\n\n if (isOpen && !firstOpenFiredRef.current) {\n firstOpenFiredRef.current = true;\n onFirstOpen?.();\n }\n\n onOpenChange?.(isOpen);\n }, [isOpen, hasEverOpened, onFirstOpen, onOpenChange]);\n\n useEffect(() => {\n if (isOpen) setIsLeaving(true);\n }, [isOpen]);\n\n /* Held until the exit finishes, not until `open` flips: releasing it early brings the\n page scrollbar back while the panel is still travelling, and the content behind jumps. */\n useEffect(() => {\n if (!isOpen && !isLeaving) return undefined;\n\n const { overflow } = document.body.style;\n document.body.style.overflow = 'hidden';\n return () => {\n document.body.style.overflow = overflow;\n };\n }, [isOpen, isLeaving]);\n\n useEffect(() => {\n if (!isOpen) return undefined;\n\n previouslyFocusedRef.current = document.activeElement as HTMLElement | null;\n const frame = requestAnimationFrame(() => panelRef.current?.focus());\n\n return () => {\n cancelAnimationFrame(frame);\n previouslyFocusedRef.current?.focus?.();\n };\n }, [isOpen]);\n\n useEffect(() => {\n if (!isOpen || dismiss?.enabled === false) return undefined;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n // A layer inside the drawer (a select list) that consumed the ESC marks it handled.\n if (event.key !== 'Escape' || event.defaultPrevented || dismiss?.escapeKey === false) return;\n onClose?.();\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (dismiss?.outsidePress === false) return;\n const panel = panelRef.current;\n const target = event.target as Node | null;\n if (!panel || !target || panel.contains(target)) return;\n if (isPressOnLayeredSurface(target, panelRef.current)) return;\n if (typeof dismiss?.outsidePress === 'function' && !dismiss.outsidePress(event)) return;\n onClose?.();\n };\n\n document.addEventListener('keydown', handleKeyDown);\n document.addEventListener('pointerdown', handlePointerDown, true);\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n document.removeEventListener('pointerdown', handlePointerDown, true);\n };\n }, [isOpen, dismiss, onClose]);\n\n const handleResizeStart = (event: React.PointerEvent<HTMLButtonElement>) => {\n event.preventDefault();\n event.stopPropagation();\n resizeStartRef.current = { pointerX: event.clientX, width: resizedWidth ?? size };\n setIsResizing(true);\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const handleResizeMove = (event: React.PointerEvent<HTMLButtonElement>) => {\n const resizeStart = resizeStartRef.current;\n if (!resizeStart) return;\n const pointerDelta =\n placement === 'right' ? resizeStart.pointerX - event.clientX : event.clientX - resizeStart.pointerX;\n setResizedWidth(clampWidth(resizeStart.width + pointerDelta));\n };\n\n const handleResizeEnd = (event: React.PointerEvent<HTMLButtonElement>) => {\n if (!resizeStartRef.current) return;\n resizeStartRef.current = null;\n setIsResizing(false);\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n };\n\n const handleResizeKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;\n event.preventDefault();\n const pointerDirection = event.key === 'ArrowLeft' ? -1 : 1;\n const widthDirection = placement === 'right' ? -pointerDirection : pointerDirection;\n setResizedWidth((current) => clampWidth((current ?? size) + widthDirection * KEYBOARD_RESIZE_STEP));\n };\n\n if (lazy && !hasEverOpened) return null;\n\n const resolvedSize = isHorizontalPlacement ? (resizedWidth ?? size) : size;\n const shouldRenderChildren = unmountOnClose ? isOpen : !lazy || hasEverOpened;\n const drawerVariants = createDrawerVariants(placement);\n\n const portalHost = getPortalHost();\n if (portalHost === null) return null;\n\n return createPortal(\n <AnimatePresence onExitComplete={() => setIsLeaving(false)}>\n {isOpen && (\n <motion.div\n key=\"drawer-scrim\"\n variants={scrimVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n aria-hidden\n onClick={() => {\n if (dismiss?.enabled === false || dismiss?.outsidePress === false) return;\n onClose?.();\n }}\n className={classNames('fixed inset-0 z-[9996] glass-scrim', overlayClassName)}\n />\n )}\n\n {isOpen && (\n <motion.div\n key=\"drawer-panel\"\n ref={panelRef}\n variants={drawerVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n role=\"dialog\"\n aria-modal={true}\n tabIndex={-1}\n style={isHorizontalPlacement ? { width: resolvedSize } : { height: resolvedSize }}\n className={classNames(\n 'fixed z-[9997] flex max-w-full flex-col overflow-y-auto bg-surface shadow-xl outline-none',\n PLACEMENT_CLASS[placement],\n className,\n )}\n >\n {canResize && isOpen ? (\n <ControlledTooltip\n content={t('controlledDrawer.resizeHandle')}\n placement={placement === 'right' ? 'left' : 'right'}\n className=\"z-[9999]\"\n >\n <ControlledButton\n defaultHtmlButton\n onPointerDown={handleResizeStart}\n onPointerMove={handleResizeMove}\n onPointerUp={handleResizeEnd}\n onPointerCancel={handleResizeEnd}\n onKeyDown={handleResizeKeyDown}\n onDoubleClick={() => setResizedWidth(null)}\n className={classNames(\n 'group/resize absolute inset-y-0 z-10 flex w-3 cursor-col-resize touch-none select-none items-center justify-center focus:outline-none',\n placement === 'right' ? 'left-0' : 'right-0',\n )}\n >\n <span\n className={classNames(\n 'h-full w-0.5 rounded-full transition-colors duration-base',\n isResizing ? 'bg-brand-hover' : 'bg-transparent group-hover/resize:bg-brand-hover',\n )}\n />\n </ControlledButton>\n </ControlledTooltip>\n ) : null}\n\n {shouldRenderChildren ? children : null}\n </motion.div>\n )}\n </AnimatePresence>,\n portalHost,\n );\n};\n\nexport default ControlledDrawer;\n"],"names":[],"mappings":"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"}
|
|
@@ -40,6 +40,7 @@ const NumberControl = ({ field, value, onChange, disabled }) => /* @__PURE__ */
|
|
|
40
40
|
const SelectControl = ({ field, value, onChange, disabled }) => /* @__PURE__ */ jsx(
|
|
41
41
|
ControlledSelect,
|
|
42
42
|
{
|
|
43
|
+
preserveLabelCase: true,
|
|
43
44
|
label: field.title,
|
|
44
45
|
options: field.options ?? [],
|
|
45
46
|
selectedValue: asString(value),
|
|
@@ -59,6 +60,7 @@ const MultiSelectControl = ({
|
|
|
59
60
|
{
|
|
60
61
|
multiple: true,
|
|
61
62
|
inFieldChips: true,
|
|
63
|
+
preserveLabelCase: true,
|
|
62
64
|
label: field.title,
|
|
63
65
|
options: field.options ?? [],
|
|
64
66
|
selectedValue: asStringArray(value),
|
|
@@ -143,6 +145,7 @@ const AsyncSelectControl = ({
|
|
|
143
145
|
return /* @__PURE__ */ jsx(
|
|
144
146
|
ControlledSelect,
|
|
145
147
|
{
|
|
148
|
+
preserveLabelCase: true,
|
|
146
149
|
label: field.title,
|
|
147
150
|
options,
|
|
148
151
|
selectedValue,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FilterFieldControl.js","sources":["../../../../src/components/Search/filters/FilterFieldControl.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport * as React from 'react';\nimport { parse } from 'date-fns';\nimport { useTranslation } from 'react-i18next';\n\nimport DatePicker from '@/components/DateTime/DatePicker';\nimport DateRangePicker from '@/components/DateTime/DateRangePicker';\nimport ControlledCheckbox from '@/components/Form/ControlledCheckbox';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport { ControlledSelect, IControlledSelectFetchParams } from '@/components/Form/ControlledSelect';\nimport ControlledSwitch from '@/components/Form/ControlledSwitch';\nimport OutlinedChip from '@/components/UI/Chips/OutlinedChip';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport SkeletonLoading from '@/components/UI/SkeletonLoading';\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { IFilterOption } from '@/interfaces/IFilterOption';\n\nimport {\n IFilterAsyncSelectField,\n IFilterBooleanField,\n IFilterCheckboxGroupField,\n IFilterCustomField,\n IFilterDateField,\n IFilterDateRangeField,\n IFilterDateRangeValue,\n IFilterMultiSelectField,\n IFilterNumberField,\n IFilterRadioField,\n IFilterRangeField,\n IFilterRangeValue,\n IFilterSelectField,\n IFilterTagsField,\n IFilterTextField,\n TFilterField,\n TFilterValues,\n} from './filterTypes';\n\nexport interface IFilterControlProps<TField extends TFilterField> {\n field: TField;\n value: unknown;\n values: TFilterValues;\n onChange: (value: unknown) => void;\n disabled?: boolean;\n}\n\nconst asString = (value: unknown): string => (typeof value === 'string' ? value : '');\nconst asStringArray = (value: unknown): Array<string> => (Array.isArray(value) ? value.map(String) : []);\n\n// ───── text / number ────────────────────────────────────────────────\n\nconst TextControl: React.FC<IFilterControlProps<IFilterTextField>> = ({ field, value, onChange, disabled }) => (\n <ControlledInput\n label={field.title}\n placeholder={field.placeholder}\n value={asString(value)}\n disabled={disabled}\n onChange={(event) => onChange(event.target.value)}\n />\n);\n\nconst NumberControl: React.FC<IFilterControlProps<IFilterNumberField>> = ({ field, value, onChange, disabled }) => (\n <ControlledInput\n type=\"number\"\n label={field.title}\n placeholder={field.placeholder}\n min={field.min}\n max={field.max}\n value={value === undefined || value === null ? '' : String(value)}\n disabled={disabled}\n onChange={(event) => onChange(event.target.value)}\n />\n);\n\n// ───── select family ────────────────────────────────────────────────\n\nconst SelectControl: React.FC<IFilterControlProps<IFilterSelectField>> = ({ field, value, onChange, disabled }) => (\n <ControlledSelect\n label={field.title}\n options={field.options ?? []}\n selectedValue={asString(value)}\n searchable={field.searchable}\n clearable={field.clearable ?? true}\n disabled={disabled}\n onChangeHook={(selected) => onChange(Array.isArray(selected) ? (selected[0] ?? '') : selected)}\n />\n);\n\nconst MultiSelectControl: React.FC<IFilterControlProps<IFilterMultiSelectField>> = ({\n field,\n value,\n onChange,\n disabled,\n}) => (\n <ControlledSelect\n multiple\n inFieldChips\n label={field.title}\n options={field.options ?? []}\n selectedValue={asStringArray(value)}\n searchable={field.searchable}\n clearable={field.clearable ?? true}\n disabled={disabled}\n onChangeHook={(selected) => onChange(Array.isArray(selected) ? selected : [selected])}\n />\n);\n\n/**\n * Owns paging, search and the option cache so consumers only answer\n * \"give me page N\". Previously every screen reimplemented this with its own\n * page counter, metadata state and loaded-once ref.\n */\nconst AsyncSelectControl: React.FC<IFilterControlProps<IFilterAsyncSelectField>> = ({\n field,\n value,\n values,\n onChange,\n disabled,\n}) => {\n const { t } = useTranslation('ui');\n const [options, setOptions] = useState<Array<IFilterOption>>(field.initialOptions ?? []);\n const [hasMore, setHasMore] = useState(false);\n const [loading, setLoading] = useState(false);\n const [failed, setFailed] = useState(false);\n const pageRef = useRef(1);\n // Discards responses that resolve after a newer request started (a search reset racing a\n // page append), so stale pages never overwrite fresher data.\n const requestSeqRef = useRef(0);\n const valuesRef = useRef(values);\n valuesRef.current = values;\n\n const loadPage = useCallback(\n async (page: number, search: string | undefined, append: boolean): Promise<Array<IFilterOption>> => {\n const requestSeq = ++requestSeqRef.current;\n setLoading(true);\n setFailed(false);\n try {\n const result = await field.loadOptions({ page, search, values: valuesRef.current });\n if (requestSeq !== requestSeqRef.current) return [];\n\n const nextOptions = result?.options ?? [];\n pageRef.current = page;\n setHasMore(Boolean(result?.hasMore));\n setOptions((previous) => (append ? [...previous, ...nextOptions] : nextOptions));\n\n return nextOptions;\n } catch (error) {\n if (requestSeq === requestSeqRef.current) {\n uiLogger.error(`[FilterFieldControl] Failed to load options for \"${field.key}\"`, error);\n setFailed(true);\n }\n\n return [];\n } finally {\n if (requestSeq === requestSeqRef.current) setLoading(false);\n }\n },\n [field],\n );\n\n const dependencyKey = (field.dependsOn ?? []).map((key) => JSON.stringify(values?.[key])).join('|');\n\n useEffect(() => {\n let cancelled = false;\n const run = async () => {\n const loaded = await loadPage(1, undefined, false);\n if (cancelled || loaded.length === 0) return;\n };\n void run();\n\n return () => {\n cancelled = true;\n };\n }, [dependencyKey, loadPage]);\n\n const fetchMoreOptions = useCallback(\n async ({ search, reset }: IControlledSelectFetchParams): Promise<void> => {\n const term = search.trim() === '' ? undefined : search;\n await loadPage(reset ? 1 : pageRef.current + 1, term, !reset);\n },\n [loadPage],\n );\n\n const selectedValue = field.multiple ? asStringArray(value) : asString(value);\n\n if (failed) {\n return (\n <NoDataFound\n size=\"inline\"\n variant=\"error\"\n message={t('filters.optionsError', { field: field.title })}\n action={{ label: t('actions.tryAgain'), onClick: () => void loadPage(1, undefined, false), icon: 'refresh' }}\n />\n );\n }\n\n return (\n <ControlledSelect\n label={field.title}\n options={options}\n selectedValue={selectedValue}\n multiple={field.multiple}\n inFieldChips={field.multiple}\n searchable={field.searchable ?? true}\n clearable={field.clearable ?? true}\n disabled={disabled}\n loading={loading}\n infiniteScroll\n hasMoreOptions={hasMore}\n fetchMoreOptions={fetchMoreOptions}\n onChangeHook={(selected) =>\n onChange(\n field.multiple\n ? Array.isArray(selected)\n ? selected\n : [selected]\n : Array.isArray(selected)\n ? (selected[0] ?? '')\n : selected,\n )\n }\n />\n );\n};\n\n// ───── dates ────────────────────────────────────────────────────────\n\nconst parseIsoDate = (value: string | undefined): Date | undefined => {\n if (!value) return undefined;\n const parsed = parse(value, 'yyyy-MM-dd', new Date());\n\n return Number.isNaN(parsed.getTime()) ? undefined : parsed;\n};\n\nconst DateControl: React.FC<IFilterControlProps<IFilterDateField>> = ({ field, value, onChange, disabled }) => (\n <DatePicker\n label={field.title}\n minDate={field.minDate}\n maxDate={field.maxDate}\n disabled={disabled}\n startValue={parseIsoDate(asString(value))}\n onUpdate={(next) => onChange(next ?? '')}\n />\n);\n\nconst DateRangeControl: React.FC<IFilterControlProps<IFilterDateRangeField>> = ({\n field,\n value,\n onChange,\n disabled,\n}) => {\n const range = (value ?? {}) as IFilterDateRangeValue;\n const from = parseIsoDate(range.from);\n const to = parseIsoDate(range.to);\n\n return (\n <DateRangePicker\n label={field.title}\n minDate={field.minDate}\n maxDate={field.maxDate}\n disabled={disabled}\n startValue={from ? { from, to } : undefined}\n onUpdate={(next) => onChange({ from: next.from, to: next.to })}\n />\n );\n};\n\n// ───── range ────────────────────────────────────────────────────────\n\nconst RangeControl: React.FC<IFilterControlProps<IFilterRangeField>> = ({ field, value, onChange, disabled }) => {\n const { t } = useTranslation('ui');\n const range = (value ?? {}) as IFilterRangeValue;\n\n const update = (bound: 'min' | 'max', raw: string) => {\n const parsed = raw === '' ? undefined : Number(raw);\n onChange({ ...range, [bound]: Number.isNaN(parsed as number) ? undefined : parsed });\n };\n\n return (\n <div className=\"flex flex-col gap-y-1.5\">\n <span className=\"text-xs font-medium text-foreground-muted\">{field.title}</span>\n <div className=\"flex items-center gap-x-2\">\n <ControlledInput\n type=\"number\"\n label={t('filters.rangeMin')}\n min={field.min}\n max={field.max}\n disabled={disabled}\n value={range.min === undefined ? '' : String(range.min)}\n onChange={(event) => update('min', event.target.value)}\n />\n <span className=\"shrink-0 text-xs text-foreground-faint\">{t('filters.rangeSeparator')}</span>\n <ControlledInput\n type=\"number\"\n label={t('filters.rangeMax')}\n min={field.min}\n max={field.max}\n disabled={disabled}\n value={range.max === undefined ? '' : String(range.max)}\n onChange={(event) => update('max', event.target.value)}\n />\n </div>\n </div>\n );\n};\n\n// ───── boolean / checkbox / radio / tags ────────────────────────────\n\nconst BooleanControl: React.FC<IFilterControlProps<IFilterBooleanField>> = ({ field, value, onChange, disabled }) => (\n <ControlledSwitch\n checked={value === true}\n disabled={disabled}\n label={field.label ?? field.title}\n onChange={(checked) => onChange(checked)}\n />\n);\n\nconst CheckboxGroupControl: React.FC<IFilterControlProps<IFilterCheckboxGroupField>> = ({\n field,\n value,\n onChange,\n disabled,\n}) => {\n const { t } = useTranslation('ui');\n const [search, setSearch] = useState('');\n const selected = asStringArray(value);\n const options = useMemo(() => {\n const list = field.options ?? [];\n if (!field.searchable || search.trim().length === 0) return list;\n const term = search.trim().toLowerCase();\n\n return list.filter((option) => option.label.toLowerCase().includes(term));\n }, [field.options, field.searchable, search]);\n\n const toggle = (optionValue: string) => {\n onChange(\n selected.includes(optionValue) ? selected.filter((entry) => entry !== optionValue) : [...selected, optionValue],\n );\n };\n\n return (\n <div className=\"flex flex-col gap-y-2\">\n <span className=\"text-xs font-medium text-foreground-muted\">{field.title}</span>\n {field.searchable && (\n <ControlledInput\n label={t('filters.searchOptions')}\n value={search}\n disabled={disabled}\n onChange={(event) => setSearch(event.target.value)}\n />\n )}\n <div className=\"flex max-h-56 flex-col gap-y-1 overflow-y-auto\">\n {options.length === 0 ? (\n <NoDataFound size=\"inline\" variant=\"search\" message={t('filters.noOptions')} />\n ) : (\n options.map((option) => (\n <div key={option.value} className=\"flex min-w-0 items-center gap-x-2\">\n <ControlledCheckbox\n checked={selected.includes(option.value)}\n disabled={disabled}\n label={option.label}\n onChange={() => toggle(option.value)}\n />\n {option.secondaryLabel && (\n <span className=\"shrink-0 text-xs tabular-nums text-foreground-faint\">{option.secondaryLabel}</span>\n )}\n {option.swatch && (\n <span\n className=\"size-3 shrink-0 rounded-full border border-surface shadow-sm ring-1 ring-border\"\n style={{ backgroundColor: option.swatch }}\n />\n )}\n </div>\n ))\n )}\n </div>\n </div>\n );\n};\n\nconst RadioControl: React.FC<IFilterControlProps<IFilterRadioField>> = ({ field, value, onChange, disabled }) => {\n const selected = asString(value);\n\n return (\n <div className=\"flex flex-col gap-y-2\">\n <span className=\"text-xs font-medium text-foreground-muted\">{field.title}</span>\n <div className=\"flex flex-wrap gap-2\">\n {(field.options ?? []).map((option) => (\n <OutlinedChip\n key={option.value}\n label={option.label}\n preserveLabelCase\n tone={selected === option.value ? 'purple' : 'neutral'}\n variant={selected === option.value ? 'solid' : 'outlined'}\n size=\"sm\"\n disabled={disabled}\n onClick={() => onChange(selected === option.value ? '' : option.value)}\n />\n ))}\n </div>\n </div>\n );\n};\n\nconst TagsControl: React.FC<IFilterControlProps<IFilterTagsField>> = ({ field, value, onChange, disabled }) => {\n const selected = asStringArray(value);\n\n return (\n <div className=\"flex flex-col gap-y-2\">\n <span className=\"text-xs font-medium text-foreground-muted\">{field.title}</span>\n <div className=\"flex flex-wrap gap-2\">\n {(field.options ?? []).map((option) => {\n const isSelected = selected.includes(option.value);\n\n return (\n <OutlinedChip\n key={option.value}\n label={option.label}\n preserveLabelCase\n tone={isSelected ? 'purple' : 'neutral'}\n variant={isSelected ? 'solid' : 'outlined'}\n size=\"sm\"\n disabled={disabled}\n onClick={() =>\n onChange(isSelected ? selected.filter((entry) => entry !== option.value) : [...selected, option.value])\n }\n />\n );\n })}\n </div>\n </div>\n );\n};\n\nconst CustomControl: React.FC<IFilterControlProps<IFilterCustomField>> = ({ field, value, onChange, disabled }) => (\n <>{field.render({ value, onChange, disabled })}</>\n);\n\n// ───── registry ─────────────────────────────────────────────────────\n\ntype TFilterControl = React.FC<IFilterControlProps<never>>;\n\nconst FILTER_CONTROL_REGISTRY: Record<TFilterField['kind'], TFilterControl> = {\n text: TextControl as TFilterControl,\n number: NumberControl as TFilterControl,\n select: SelectControl as TFilterControl,\n multiselect: MultiSelectControl as TFilterControl,\n 'async-select': AsyncSelectControl as TFilterControl,\n date: DateControl as TFilterControl,\n 'date-range': DateRangeControl as TFilterControl,\n range: RangeControl as TFilterControl,\n boolean: BooleanControl as TFilterControl,\n 'checkbox-group': CheckboxGroupControl as TFilterControl,\n radio: RadioControl as TFilterControl,\n tags: TagsControl as TFilterControl,\n custom: CustomControl as TFilterControl,\n};\n\ninterface IFilterFieldControlProps {\n field: TFilterField;\n value: unknown;\n values: TFilterValues;\n onChange: (value: unknown) => void;\n disabled?: boolean;\n loading?: boolean;\n}\n\nconst FilterFieldControl: React.FC<IFilterFieldControlProps> = ({\n field,\n value,\n values,\n onChange,\n disabled,\n loading,\n}) => {\n if (loading) return <SkeletonLoading className=\"h-10 w-full rounded-lg\" />;\n\n const Control = FILTER_CONTROL_REGISTRY[field.kind];\n if (!Control) return null;\n\n const control = (\n <Control\n field={field as never}\n value={value}\n values={values}\n onChange={onChange}\n disabled={disabled || field.disabled}\n />\n );\n\n if (!field.description) return control;\n\n return (\n <div className=\"flex flex-col gap-y-1\">\n {control}\n <span className=\"px-1 text-xs text-foreground-faint\">{field.description}</span>\n </div>\n );\n};\n\nexport default FilterFieldControl;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FilterFieldControl.js","sources":["../../../../src/components/Search/filters/FilterFieldControl.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport * as React from 'react';\nimport { parse } from 'date-fns';\nimport { useTranslation } from 'react-i18next';\n\nimport DatePicker from '@/components/DateTime/DatePicker';\nimport DateRangePicker from '@/components/DateTime/DateRangePicker';\nimport ControlledCheckbox from '@/components/Form/ControlledCheckbox';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport { ControlledSelect, IControlledSelectFetchParams } from '@/components/Form/ControlledSelect';\nimport ControlledSwitch from '@/components/Form/ControlledSwitch';\nimport OutlinedChip from '@/components/UI/Chips/OutlinedChip';\nimport NoDataFound from '@/components/UI/NoDataFound';\nimport SkeletonLoading from '@/components/UI/SkeletonLoading';\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { IFilterOption } from '@/interfaces/IFilterOption';\n\nimport {\n IFilterAsyncSelectField,\n IFilterBooleanField,\n IFilterCheckboxGroupField,\n IFilterCustomField,\n IFilterDateField,\n IFilterDateRangeField,\n IFilterDateRangeValue,\n IFilterMultiSelectField,\n IFilterNumberField,\n IFilterRadioField,\n IFilterRangeField,\n IFilterRangeValue,\n IFilterSelectField,\n IFilterTagsField,\n IFilterTextField,\n TFilterField,\n TFilterValues,\n} from './filterTypes';\n\nexport interface IFilterControlProps<TField extends TFilterField> {\n field: TField;\n value: unknown;\n values: TFilterValues;\n onChange: (value: unknown) => void;\n disabled?: boolean;\n}\n\nconst asString = (value: unknown): string => (typeof value === 'string' ? value : '');\nconst asStringArray = (value: unknown): Array<string> => (Array.isArray(value) ? value.map(String) : []);\n\n// ───── text / number ────────────────────────────────────────────────\n\nconst TextControl: React.FC<IFilterControlProps<IFilterTextField>> = ({ field, value, onChange, disabled }) => (\n <ControlledInput\n label={field.title}\n placeholder={field.placeholder}\n value={asString(value)}\n disabled={disabled}\n onChange={(event) => onChange(event.target.value)}\n />\n);\n\nconst NumberControl: React.FC<IFilterControlProps<IFilterNumberField>> = ({ field, value, onChange, disabled }) => (\n <ControlledInput\n type=\"number\"\n label={field.title}\n placeholder={field.placeholder}\n min={field.min}\n max={field.max}\n value={value === undefined || value === null ? '' : String(value)}\n disabled={disabled}\n onChange={(event) => onChange(event.target.value)}\n />\n);\n\n// ───── select family ────────────────────────────────────────────────\n\nconst SelectControl: React.FC<IFilterControlProps<IFilterSelectField>> = ({ field, value, onChange, disabled }) => (\n <ControlledSelect\n preserveLabelCase\n label={field.title}\n options={field.options ?? []}\n selectedValue={asString(value)}\n searchable={field.searchable}\n clearable={field.clearable ?? true}\n disabled={disabled}\n onChangeHook={(selected) => onChange(Array.isArray(selected) ? (selected[0] ?? '') : selected)}\n />\n);\n\nconst MultiSelectControl: React.FC<IFilterControlProps<IFilterMultiSelectField>> = ({\n field,\n value,\n onChange,\n disabled,\n}) => (\n <ControlledSelect\n multiple\n inFieldChips\n preserveLabelCase\n label={field.title}\n options={field.options ?? []}\n selectedValue={asStringArray(value)}\n searchable={field.searchable}\n clearable={field.clearable ?? true}\n disabled={disabled}\n onChangeHook={(selected) => onChange(Array.isArray(selected) ? selected : [selected])}\n />\n);\n\n/**\n * Owns paging, search and the option cache so consumers only answer\n * \"give me page N\". Previously every screen reimplemented this with its own\n * page counter, metadata state and loaded-once ref.\n */\nconst AsyncSelectControl: React.FC<IFilterControlProps<IFilterAsyncSelectField>> = ({\n field,\n value,\n values,\n onChange,\n disabled,\n}) => {\n const { t } = useTranslation('ui');\n const [options, setOptions] = useState<Array<IFilterOption>>(field.initialOptions ?? []);\n const [hasMore, setHasMore] = useState(false);\n const [loading, setLoading] = useState(false);\n const [failed, setFailed] = useState(false);\n const pageRef = useRef(1);\n // Discards responses that resolve after a newer request started (a search reset racing a\n // page append), so stale pages never overwrite fresher data.\n const requestSeqRef = useRef(0);\n const valuesRef = useRef(values);\n valuesRef.current = values;\n\n const loadPage = useCallback(\n async (page: number, search: string | undefined, append: boolean): Promise<Array<IFilterOption>> => {\n const requestSeq = ++requestSeqRef.current;\n setLoading(true);\n setFailed(false);\n try {\n const result = await field.loadOptions({ page, search, values: valuesRef.current });\n if (requestSeq !== requestSeqRef.current) return [];\n\n const nextOptions = result?.options ?? [];\n pageRef.current = page;\n setHasMore(Boolean(result?.hasMore));\n setOptions((previous) => (append ? [...previous, ...nextOptions] : nextOptions));\n\n return nextOptions;\n } catch (error) {\n if (requestSeq === requestSeqRef.current) {\n uiLogger.error(`[FilterFieldControl] Failed to load options for \"${field.key}\"`, error);\n setFailed(true);\n }\n\n return [];\n } finally {\n if (requestSeq === requestSeqRef.current) setLoading(false);\n }\n },\n [field],\n );\n\n const dependencyKey = (field.dependsOn ?? []).map((key) => JSON.stringify(values?.[key])).join('|');\n\n useEffect(() => {\n let cancelled = false;\n const run = async () => {\n const loaded = await loadPage(1, undefined, false);\n if (cancelled || loaded.length === 0) return;\n };\n void run();\n\n return () => {\n cancelled = true;\n };\n }, [dependencyKey, loadPage]);\n\n const fetchMoreOptions = useCallback(\n async ({ search, reset }: IControlledSelectFetchParams): Promise<void> => {\n const term = search.trim() === '' ? undefined : search;\n await loadPage(reset ? 1 : pageRef.current + 1, term, !reset);\n },\n [loadPage],\n );\n\n const selectedValue = field.multiple ? asStringArray(value) : asString(value);\n\n if (failed) {\n return (\n <NoDataFound\n size=\"inline\"\n variant=\"error\"\n message={t('filters.optionsError', { field: field.title })}\n action={{ label: t('actions.tryAgain'), onClick: () => void loadPage(1, undefined, false), icon: 'refresh' }}\n />\n );\n }\n\n return (\n <ControlledSelect\n preserveLabelCase\n label={field.title}\n options={options}\n selectedValue={selectedValue}\n multiple={field.multiple}\n inFieldChips={field.multiple}\n searchable={field.searchable ?? true}\n clearable={field.clearable ?? true}\n disabled={disabled}\n loading={loading}\n infiniteScroll\n hasMoreOptions={hasMore}\n fetchMoreOptions={fetchMoreOptions}\n onChangeHook={(selected) =>\n onChange(\n field.multiple\n ? Array.isArray(selected)\n ? selected\n : [selected]\n : Array.isArray(selected)\n ? (selected[0] ?? '')\n : selected,\n )\n }\n />\n );\n};\n\n// ───── dates ────────────────────────────────────────────────────────\n\nconst parseIsoDate = (value: string | undefined): Date | undefined => {\n if (!value) return undefined;\n const parsed = parse(value, 'yyyy-MM-dd', new Date());\n\n return Number.isNaN(parsed.getTime()) ? undefined : parsed;\n};\n\nconst DateControl: React.FC<IFilterControlProps<IFilterDateField>> = ({ field, value, onChange, disabled }) => (\n <DatePicker\n label={field.title}\n minDate={field.minDate}\n maxDate={field.maxDate}\n disabled={disabled}\n startValue={parseIsoDate(asString(value))}\n onUpdate={(next) => onChange(next ?? '')}\n />\n);\n\nconst DateRangeControl: React.FC<IFilterControlProps<IFilterDateRangeField>> = ({\n field,\n value,\n onChange,\n disabled,\n}) => {\n const range = (value ?? {}) as IFilterDateRangeValue;\n const from = parseIsoDate(range.from);\n const to = parseIsoDate(range.to);\n\n return (\n <DateRangePicker\n label={field.title}\n minDate={field.minDate}\n maxDate={field.maxDate}\n disabled={disabled}\n startValue={from ? { from, to } : undefined}\n onUpdate={(next) => onChange({ from: next.from, to: next.to })}\n />\n );\n};\n\n// ───── range ────────────────────────────────────────────────────────\n\nconst RangeControl: React.FC<IFilterControlProps<IFilterRangeField>> = ({ field, value, onChange, disabled }) => {\n const { t } = useTranslation('ui');\n const range = (value ?? {}) as IFilterRangeValue;\n\n const update = (bound: 'min' | 'max', raw: string) => {\n const parsed = raw === '' ? undefined : Number(raw);\n onChange({ ...range, [bound]: Number.isNaN(parsed as number) ? undefined : parsed });\n };\n\n return (\n <div className=\"flex flex-col gap-y-1.5\">\n <span className=\"text-xs font-medium text-foreground-muted\">{field.title}</span>\n <div className=\"flex items-center gap-x-2\">\n <ControlledInput\n type=\"number\"\n label={t('filters.rangeMin')}\n min={field.min}\n max={field.max}\n disabled={disabled}\n value={range.min === undefined ? '' : String(range.min)}\n onChange={(event) => update('min', event.target.value)}\n />\n <span className=\"shrink-0 text-xs text-foreground-faint\">{t('filters.rangeSeparator')}</span>\n <ControlledInput\n type=\"number\"\n label={t('filters.rangeMax')}\n min={field.min}\n max={field.max}\n disabled={disabled}\n value={range.max === undefined ? '' : String(range.max)}\n onChange={(event) => update('max', event.target.value)}\n />\n </div>\n </div>\n );\n};\n\n// ───── boolean / checkbox / radio / tags ────────────────────────────\n\nconst BooleanControl: React.FC<IFilterControlProps<IFilterBooleanField>> = ({ field, value, onChange, disabled }) => (\n <ControlledSwitch\n checked={value === true}\n disabled={disabled}\n label={field.label ?? field.title}\n onChange={(checked) => onChange(checked)}\n />\n);\n\nconst CheckboxGroupControl: React.FC<IFilterControlProps<IFilterCheckboxGroupField>> = ({\n field,\n value,\n onChange,\n disabled,\n}) => {\n const { t } = useTranslation('ui');\n const [search, setSearch] = useState('');\n const selected = asStringArray(value);\n const options = useMemo(() => {\n const list = field.options ?? [];\n if (!field.searchable || search.trim().length === 0) return list;\n const term = search.trim().toLowerCase();\n\n return list.filter((option) => option.label.toLowerCase().includes(term));\n }, [field.options, field.searchable, search]);\n\n const toggle = (optionValue: string) => {\n onChange(\n selected.includes(optionValue) ? selected.filter((entry) => entry !== optionValue) : [...selected, optionValue],\n );\n };\n\n return (\n <div className=\"flex flex-col gap-y-2\">\n <span className=\"text-xs font-medium text-foreground-muted\">{field.title}</span>\n {field.searchable && (\n <ControlledInput\n label={t('filters.searchOptions')}\n value={search}\n disabled={disabled}\n onChange={(event) => setSearch(event.target.value)}\n />\n )}\n <div className=\"flex max-h-56 flex-col gap-y-1 overflow-y-auto\">\n {options.length === 0 ? (\n <NoDataFound size=\"inline\" variant=\"search\" message={t('filters.noOptions')} />\n ) : (\n options.map((option) => (\n <div key={option.value} className=\"flex min-w-0 items-center gap-x-2\">\n <ControlledCheckbox\n checked={selected.includes(option.value)}\n disabled={disabled}\n label={option.label}\n onChange={() => toggle(option.value)}\n />\n {option.secondaryLabel && (\n <span className=\"shrink-0 text-xs tabular-nums text-foreground-faint\">{option.secondaryLabel}</span>\n )}\n {option.swatch && (\n <span\n className=\"size-3 shrink-0 rounded-full border border-surface shadow-sm ring-1 ring-border\"\n style={{ backgroundColor: option.swatch }}\n />\n )}\n </div>\n ))\n )}\n </div>\n </div>\n );\n};\n\nconst RadioControl: React.FC<IFilterControlProps<IFilterRadioField>> = ({ field, value, onChange, disabled }) => {\n const selected = asString(value);\n\n return (\n <div className=\"flex flex-col gap-y-2\">\n <span className=\"text-xs font-medium text-foreground-muted\">{field.title}</span>\n <div className=\"flex flex-wrap gap-2\">\n {(field.options ?? []).map((option) => (\n <OutlinedChip\n key={option.value}\n label={option.label}\n preserveLabelCase\n tone={selected === option.value ? 'purple' : 'neutral'}\n variant={selected === option.value ? 'solid' : 'outlined'}\n size=\"sm\"\n disabled={disabled}\n onClick={() => onChange(selected === option.value ? '' : option.value)}\n />\n ))}\n </div>\n </div>\n );\n};\n\nconst TagsControl: React.FC<IFilterControlProps<IFilterTagsField>> = ({ field, value, onChange, disabled }) => {\n const selected = asStringArray(value);\n\n return (\n <div className=\"flex flex-col gap-y-2\">\n <span className=\"text-xs font-medium text-foreground-muted\">{field.title}</span>\n <div className=\"flex flex-wrap gap-2\">\n {(field.options ?? []).map((option) => {\n const isSelected = selected.includes(option.value);\n\n return (\n <OutlinedChip\n key={option.value}\n label={option.label}\n preserveLabelCase\n tone={isSelected ? 'purple' : 'neutral'}\n variant={isSelected ? 'solid' : 'outlined'}\n size=\"sm\"\n disabled={disabled}\n onClick={() =>\n onChange(isSelected ? selected.filter((entry) => entry !== option.value) : [...selected, option.value])\n }\n />\n );\n })}\n </div>\n </div>\n );\n};\n\nconst CustomControl: React.FC<IFilterControlProps<IFilterCustomField>> = ({ field, value, onChange, disabled }) => (\n <>{field.render({ value, onChange, disabled })}</>\n);\n\n// ───── registry ─────────────────────────────────────────────────────\n\ntype TFilterControl = React.FC<IFilterControlProps<never>>;\n\nconst FILTER_CONTROL_REGISTRY: Record<TFilterField['kind'], TFilterControl> = {\n text: TextControl as TFilterControl,\n number: NumberControl as TFilterControl,\n select: SelectControl as TFilterControl,\n multiselect: MultiSelectControl as TFilterControl,\n 'async-select': AsyncSelectControl as TFilterControl,\n date: DateControl as TFilterControl,\n 'date-range': DateRangeControl as TFilterControl,\n range: RangeControl as TFilterControl,\n boolean: BooleanControl as TFilterControl,\n 'checkbox-group': CheckboxGroupControl as TFilterControl,\n radio: RadioControl as TFilterControl,\n tags: TagsControl as TFilterControl,\n custom: CustomControl as TFilterControl,\n};\n\ninterface IFilterFieldControlProps {\n field: TFilterField;\n value: unknown;\n values: TFilterValues;\n onChange: (value: unknown) => void;\n disabled?: boolean;\n loading?: boolean;\n}\n\nconst FilterFieldControl: React.FC<IFilterFieldControlProps> = ({\n field,\n value,\n values,\n onChange,\n disabled,\n loading,\n}) => {\n if (loading) return <SkeletonLoading className=\"h-10 w-full rounded-lg\" />;\n\n const Control = FILTER_CONTROL_REGISTRY[field.kind];\n if (!Control) return null;\n\n const control = (\n <Control\n field={field as never}\n value={value}\n values={values}\n onChange={onChange}\n disabled={disabled || field.disabled}\n />\n );\n\n if (!field.description) return control;\n\n return (\n <div className=\"flex flex-col gap-y-1\">\n {control}\n <span className=\"px-1 text-xs text-foreground-faint\">{field.description}</span>\n </div>\n );\n};\n\nexport default FilterFieldControl;\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FiltersPanel.js","sources":["../../../../src/components/Search/filters/FiltersPanel.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';\nimport * as React from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport ControlledDrawer from '@/components/Form/ControlledDrawer';\nimport {\n ControlledPopoverContent,\n ControlledPopoverHandler,\n ControlledPopoverRoot,\n} from '@/components/UI/ControlledPopover';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\n\nimport FilterChipsRow from './FilterChipsRow';\nimport FiltersButton from './FiltersButton';\nimport FiltersForm from './FiltersForm';\nimport { FILTERS_POPOVER_WIDTH_REM, getFiltersPanelWidth, TFiltersSize, TFiltersVariant } from './filtersLayout';\nimport { IFiltersSchema, TFilterValues } from './filterTypes';\nimport { formatFilterValue } from './filterValues';\nimport { useFilters } from './useFilters';\n\nexport interface IFiltersPanelProps {\n schema: IFiltersSchema;\n values?: TFilterValues;\n onChange?: (values: TFilterValues) => void;\n initialValues?: TFilterValues;\n urlKey?: string;\n\n variant?: TFiltersVariant;\n size?: TFiltersSize;\n\n /** Controlled open state. Omit to let the panel own its trigger and state. */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Hides the built-in trigger, for hosts that render their own. */\n hideTrigger?: boolean;\n triggerLabel?: string;\n /** Match the page actions standing next to the trigger. */\n triggerSize?: 'sm' | 'md';\n\n loading?: boolean;\n disabled?: boolean;\n onFirstOpen?: () => void;\n emptyMessage?: string;\n className?: string;\n}\n\n/**\n * The filter surface. `variant` picks the shell (side drawer, anchored popover\n * or inline block); the schema, state and field rendering are identical in all\n * three, so a screen never reimplements filtering to get a different shape.\n */\nconst FiltersPanel: React.FC<IFiltersPanelProps> = ({\n schema,\n values,\n onChange,\n initialValues,\n urlKey,\n variant = 'drawer',\n size = 'md',\n open: controlledOpen,\n onOpenChange,\n hideTrigger,\n triggerLabel,\n triggerSize,\n loading,\n disabled,\n onFirstOpen,\n emptyMessage,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const titleId = useId();\n\n const fields = useMemo(() => schema?.fields ?? [], [schema]);\n const filters = useFilters({ fields, value: values, onChange, initialValues, urlKey });\n\n const [internalOpen, setInternalOpen] = useState(false);\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n\n const [isDiscardOpen, setIsDiscardOpen] = useState(false);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const bodyRef = useRef<HTMLDivElement>(null);\n const hasOpenedOnceRef = useRef(false);\n const prevOpenRef = useRef(open);\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n },\n [isControlled, onOpenChange],\n );\n\n const filtersRef = useRef(filters);\n filtersRef.current = filters;\n\n useEffect(() => {\n if (open === prevOpenRef.current) return;\n prevOpenRef.current = open;\n if (open && !hasOpenedOnceRef.current) {\n hasOpenedOnceRef.current = true;\n onFirstOpen?.();\n }\n if (open) {\n // Reopening always shows what is actually applied, never a stale draft.\n filtersRef.current.resetDraft();\n window.requestAnimationFrame(() => {\n bodyRef.current?.querySelector<HTMLElement>('input, button, [tabindex]')?.focus();\n });\n } else {\n triggerRef.current?.focus();\n }\n }, [open, onFirstOpen]);\n\n const closeRequest = useCallback(() => {\n if (filters.isDirty) {\n setIsDiscardOpen(true);\n return;\n }\n setOpen(false);\n }, [filters.isDirty, setOpen]);\n\n const discardAndClose = useCallback(() => {\n filters.resetDraft();\n setIsDiscardOpen(false);\n setOpen(false);\n }, [filters, setOpen]);\n\n const handleApply = useCallback(() => {\n filters.apply();\n setOpen(false);\n }, [filters, setOpen]);\n\n /** Clearing does not close the panel: the user usually clears to start over. */\n const handleClear = useCallback(() => {\n filters.clear();\n }, [filters]);\n\n const handleKeyDown = (event: React.KeyboardEvent) => {\n if (event.key === 'Escape') {\n event.stopPropagation();\n closeRequest();\n return;\n }\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault();\n handleApply();\n }\n };\n\n const form = (\n <FiltersForm\n fields={fields}\n groups={schema?.groups}\n values={filters.draft}\n onValueChange={filters.setDraftValue}\n disabled={disabled}\n loading={loading}\n emptyMessage={emptyMessage}\n />\n );\n\n if (variant === 'chips') {\n // A chip row applies on pick, so the draft is committed immediately.\n return (\n <FilterChipsRow\n fields={fields}\n values={filters.applied}\n disabled={disabled}\n className={className}\n onValueChange={(key, next) => {\n filters.setDraftValue(key, next);\n window.requestAnimationFrame(() => filters.apply());\n }}\n onClearAll={filters.clear}\n />\n );\n }\n\n if (variant === 'inline') {\n return (\n <div className={classNames('flex flex-col gap-y-4', className)}>\n {form}\n {filters.isDirty && (\n <div className=\"flex items-center justify-end gap-x-2\">\n <ControlledButton variant=\"text\" tone=\"neutral\" size=\"sm\" onClick={filters.resetDraft}>\n {t('actions.cancel')}\n </ControlledButton>\n <ControlledButton size=\"sm\" onClick={() => filters.apply()}>\n {t('actions.apply')}\n </ControlledButton>\n </div>\n )}\n </div>\n );\n }\n\n const activeSummary = filters.activeFields.map((field) => ({\n key: field.key,\n label: field.title,\n value: formatFilterValue(field, filters.applied?.[field.key]),\n }));\n\n const trigger = hideTrigger ? null : (\n <FiltersButton\n ref={triggerRef}\n label={triggerLabel}\n size={triggerSize}\n activeCount={filters.activeCount}\n activeFilters={activeSummary}\n loading={loading}\n disabled={disabled}\n onClick={() => setOpen(!open)}\n />\n );\n\n const header = (\n <div className=\"flex shrink-0 items-start justify-between gap-x-3 border-b border-border p-4\">\n <div className=\"flex items-start gap-x-3\">\n <div className=\"flex size-9 shrink-0 items-center justify-center rounded-xl bg-brand-soft text-brand-emphasis\">\n <MaterialSymbol icon=\"filter_alt\" size={20} />\n </div>\n <div className=\"min-w-0\">\n <h2 id={titleId} className=\"truncate text-base font-semibold text-foreground\">\n {schema?.title ?? t('actions.filter')}\n </h2>\n {schema?.subtitle && <p className=\"text-xs text-foreground-subtle\">{schema.subtitle}</p>}\n </div>\n </div>\n <ControlledTooltip className=\"z-[9999]\" content={t('actions.close')}>\n <ControlledButton\n defaultHtmlButton\n onClick={closeRequest}\n className=\"rounded-full p-1 text-foreground-subtle transition-colors hover:bg-muted-hover hover:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-border\"\n >\n <MaterialSymbol icon=\"close\" size={20} />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n );\n\n const footer = (\n <div className=\"flex shrink-0 items-center justify-between gap-x-3 border-t border-border p-4\">\n <ControlledButton\n variant=\"text\"\n tone=\"neutral\"\n disabled={filters.activeCount === 0}\n className=\"flex items-center gap-x-1\"\n onClick={handleClear}\n >\n <MaterialSymbol icon=\"filter_list_off\" size={18} />\n {t('actions.clear')}\n </ControlledButton>\n\n <ControlledButton disabled={!filters.isDirty} onClick={handleApply}>\n {t('actions.apply')}\n </ControlledButton>\n </div>\n );\n\n const discardDialog = (\n <ControlledDialog\n size=\"sm\"\n open={isDiscardOpen}\n title={t('filtersDrawer.discardChangesTitle')}\n subtitle={t('filtersDrawer.discardChangesMessage')}\n cancelLabel={t('filtersDrawer.keepEditing')}\n confirmLabel={t('filtersDrawer.discard')}\n onCancel={() => setIsDiscardOpen(false)}\n onConfirm={discardAndClose}\n >\n {null}\n </ControlledDialog>\n );\n\n if (variant === 'popover') {\n return (\n <>\n <ControlledPopoverRoot open={open} handler={setOpen} placement=\"bottom-end\">\n <ControlledPopoverHandler>{trigger ?? <span />}</ControlledPopoverHandler>\n <ControlledPopoverContent\n className={classNames('z-[9998] flex max-h-[70vh] flex-col overflow-hidden rounded-xl p-0', className)}\n style={{ width: `${FILTERS_POPOVER_WIDTH_REM}rem` }}\n role=\"dialog\"\n aria-labelledby={titleId}\n onKeyDown={handleKeyDown}\n >\n {header}\n <div ref={bodyRef} className=\"min-h-0 flex-1 overflow-y-auto px-4 py-3\">\n {form}\n </div>\n {footer}\n </ControlledPopoverContent>\n </ControlledPopoverRoot>\n {discardDialog}\n </>\n );\n }\n\n return (\n <>\n {trigger}\n\n <ControlledDrawer\n open={open}\n onClose={closeRequest}\n placement=\"right\"\n size={getFiltersPanelWidth(size)}\n lazy\n className={className}\n >\n <div\n className=\"flex h-full flex-col\"\n role=\"dialog\"\n aria-modal\n aria-labelledby={titleId}\n onKeyDown={handleKeyDown}\n >\n {header}\n <div ref={bodyRef} className=\"min-h-0 flex-1 overflow-y-auto px-4 py-4\">\n {form}\n </div>\n {footer}\n </div>\n </ControlledDrawer>\n\n {discardDialog}\n </>\n );\n};\n\nexport default FiltersPanel;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FiltersPanel.js","sources":["../../../../src/components/Search/filters/FiltersPanel.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';\nimport * as React from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport ControlledDrawer from '@/components/Form/ControlledDrawer';\nimport {\n ControlledPopoverContent,\n ControlledPopoverHandler,\n ControlledPopoverRoot,\n} from '@/components/UI/ControlledPopover';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\n\nimport FilterChipsRow from './FilterChipsRow';\nimport FiltersButton from './FiltersButton';\nimport FiltersForm from './FiltersForm';\nimport { FILTERS_POPOVER_WIDTH_REM, getFiltersPanelWidth, TFiltersSize, TFiltersVariant } from './filtersLayout';\nimport { IFiltersSchema, TFilterValues } from './filterTypes';\nimport { formatFilterValue } from './filterValues';\nimport { useFilters } from './useFilters';\n\nexport interface IFiltersPanelProps {\n schema: IFiltersSchema;\n values?: TFilterValues;\n onChange?: (values: TFilterValues) => void;\n initialValues?: TFilterValues;\n urlKey?: string;\n\n variant?: TFiltersVariant;\n size?: TFiltersSize;\n\n /** Controlled open state. Omit to let the panel own its trigger and state. */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Hides the built-in trigger, for hosts that render their own. */\n hideTrigger?: boolean;\n triggerLabel?: string;\n /** Match the page actions standing next to the trigger. */\n triggerSize?: 'sm' | 'md';\n\n loading?: boolean;\n disabled?: boolean;\n onFirstOpen?: () => void;\n emptyMessage?: string;\n className?: string;\n}\n\n/**\n * The filter surface. `variant` picks the shell (side drawer, anchored popover\n * or inline block); the schema, state and field rendering are identical in all\n * three, so a screen never reimplements filtering to get a different shape.\n */\nconst FiltersPanel: React.FC<IFiltersPanelProps> = ({\n schema,\n values,\n onChange,\n initialValues,\n urlKey,\n variant = 'drawer',\n size = 'md',\n open: controlledOpen,\n onOpenChange,\n hideTrigger,\n triggerLabel,\n triggerSize,\n loading,\n disabled,\n onFirstOpen,\n emptyMessage,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const titleId = useId();\n\n const fields = useMemo(() => schema?.fields ?? [], [schema]);\n const filters = useFilters({ fields, value: values, onChange, initialValues, urlKey });\n\n const [internalOpen, setInternalOpen] = useState(false);\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n\n const [isDiscardOpen, setIsDiscardOpen] = useState(false);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const bodyRef = useRef<HTMLDivElement>(null);\n const hasOpenedOnceRef = useRef(false);\n const prevOpenRef = useRef(open);\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!isControlled) setInternalOpen(next);\n onOpenChange?.(next);\n },\n [isControlled, onOpenChange],\n );\n\n const filtersRef = useRef(filters);\n filtersRef.current = filters;\n\n useEffect(() => {\n if (open === prevOpenRef.current) return;\n prevOpenRef.current = open;\n if (open && !hasOpenedOnceRef.current) {\n hasOpenedOnceRef.current = true;\n onFirstOpen?.();\n }\n if (open) {\n // Reopening always shows what is actually applied, never a stale draft.\n filtersRef.current.resetDraft();\n window.requestAnimationFrame(() => {\n bodyRef.current?.querySelector<HTMLElement>('input, button, [tabindex]')?.focus();\n });\n } else {\n triggerRef.current?.focus();\n }\n }, [open, onFirstOpen]);\n\n const closeRequest = useCallback(() => {\n if (filters.isDirty) {\n setIsDiscardOpen(true);\n return;\n }\n setOpen(false);\n }, [filters.isDirty, setOpen]);\n\n const discardAndClose = useCallback(() => {\n filters.resetDraft();\n setIsDiscardOpen(false);\n setOpen(false);\n }, [filters, setOpen]);\n\n const handleApply = useCallback(() => {\n filters.apply();\n setOpen(false);\n }, [filters, setOpen]);\n\n /** Clearing does not close the panel: the user usually clears to start over. */\n const handleClear = useCallback(() => {\n filters.clear();\n }, [filters]);\n\n const handleKeyDown = (event: React.KeyboardEvent) => {\n if (event.key === 'Escape') {\n // React bubbles a select list's keydown through its portal; an ESC it consumed is not ours.\n if (event.defaultPrevented) return;\n event.stopPropagation();\n closeRequest();\n return;\n }\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault();\n handleApply();\n }\n };\n\n const form = (\n <FiltersForm\n fields={fields}\n groups={schema?.groups}\n values={filters.draft}\n onValueChange={filters.setDraftValue}\n disabled={disabled}\n loading={loading}\n emptyMessage={emptyMessage}\n />\n );\n\n if (variant === 'chips') {\n // A chip row applies on pick, so the draft is committed immediately.\n return (\n <FilterChipsRow\n fields={fields}\n values={filters.applied}\n disabled={disabled}\n className={className}\n onValueChange={(key, next) => {\n filters.setDraftValue(key, next);\n window.requestAnimationFrame(() => filters.apply());\n }}\n onClearAll={filters.clear}\n />\n );\n }\n\n if (variant === 'inline') {\n return (\n <div className={classNames('flex flex-col gap-y-4', className)}>\n {form}\n {filters.isDirty && (\n <div className=\"flex items-center justify-end gap-x-2\">\n <ControlledButton variant=\"text\" tone=\"neutral\" size=\"sm\" onClick={filters.resetDraft}>\n {t('actions.cancel')}\n </ControlledButton>\n <ControlledButton size=\"sm\" onClick={() => filters.apply()}>\n {t('actions.apply')}\n </ControlledButton>\n </div>\n )}\n </div>\n );\n }\n\n const activeSummary = filters.activeFields.map((field) => ({\n key: field.key,\n label: field.title,\n value: formatFilterValue(field, filters.applied?.[field.key]),\n }));\n\n const trigger = hideTrigger ? null : (\n <FiltersButton\n ref={triggerRef}\n label={triggerLabel}\n size={triggerSize}\n activeCount={filters.activeCount}\n activeFilters={activeSummary}\n loading={loading}\n disabled={disabled}\n onClick={() => setOpen(!open)}\n />\n );\n\n const header = (\n <div className=\"flex shrink-0 items-start justify-between gap-x-3 border-b border-border p-4\">\n <div className=\"flex items-start gap-x-3\">\n <div className=\"flex size-9 shrink-0 items-center justify-center rounded-xl bg-brand-soft text-brand-emphasis\">\n <MaterialSymbol icon=\"filter_alt\" size={20} />\n </div>\n <div className=\"min-w-0\">\n <h2 id={titleId} className=\"truncate text-base font-semibold text-foreground\">\n {schema?.title ?? t('actions.filter')}\n </h2>\n {schema?.subtitle && <p className=\"text-xs text-foreground-subtle\">{schema.subtitle}</p>}\n </div>\n </div>\n <ControlledTooltip className=\"z-[9999]\" content={t('actions.close')}>\n <ControlledButton\n defaultHtmlButton\n onClick={closeRequest}\n className=\"rounded-full p-1 text-foreground-subtle transition-colors hover:bg-muted-hover hover:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-border\"\n >\n <MaterialSymbol icon=\"close\" size={20} />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n );\n\n const footer = (\n <div className=\"flex shrink-0 items-center justify-between gap-x-3 border-t border-border p-4\">\n <ControlledButton\n variant=\"text\"\n tone=\"neutral\"\n disabled={filters.activeCount === 0}\n className=\"flex items-center gap-x-1\"\n onClick={handleClear}\n >\n <MaterialSymbol icon=\"filter_list_off\" size={18} />\n {t('actions.clear')}\n </ControlledButton>\n\n <ControlledButton disabled={!filters.isDirty} onClick={handleApply}>\n {t('actions.apply')}\n </ControlledButton>\n </div>\n );\n\n const discardDialog = (\n <ControlledDialog\n size=\"sm\"\n open={isDiscardOpen}\n title={t('filtersDrawer.discardChangesTitle')}\n subtitle={t('filtersDrawer.discardChangesMessage')}\n cancelLabel={t('filtersDrawer.keepEditing')}\n confirmLabel={t('filtersDrawer.discard')}\n onCancel={() => setIsDiscardOpen(false)}\n onConfirm={discardAndClose}\n >\n {null}\n </ControlledDialog>\n );\n\n if (variant === 'popover') {\n return (\n <>\n <ControlledPopoverRoot open={open} handler={setOpen} placement=\"bottom-end\">\n <ControlledPopoverHandler>{trigger ?? <span />}</ControlledPopoverHandler>\n <ControlledPopoverContent\n className={classNames('z-[9998] flex max-h-[70vh] flex-col overflow-hidden rounded-xl p-0', className)}\n style={{ width: `${FILTERS_POPOVER_WIDTH_REM}rem` }}\n role=\"dialog\"\n aria-labelledby={titleId}\n onKeyDown={handleKeyDown}\n >\n {header}\n <div ref={bodyRef} className=\"min-h-0 flex-1 overflow-y-auto px-4 py-3\">\n {form}\n </div>\n {footer}\n </ControlledPopoverContent>\n </ControlledPopoverRoot>\n {discardDialog}\n </>\n );\n }\n\n return (\n <>\n {trigger}\n\n <ControlledDrawer\n open={open}\n onClose={closeRequest}\n placement=\"right\"\n size={getFiltersPanelWidth(size)}\n lazy\n className={className}\n >\n <div\n className=\"flex h-full flex-col\"\n role=\"dialog\"\n aria-modal\n aria-labelledby={titleId}\n onKeyDown={handleKeyDown}\n >\n {header}\n <div ref={bodyRef} className=\"min-h-0 flex-1 overflow-y-auto px-4 py-4\">\n {form}\n </div>\n {footer}\n </div>\n </ControlledDrawer>\n\n {discardDialog}\n </>\n );\n};\n\nexport default FiltersPanel;\n"],"names":[],"mappings":"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"}
|
|
@@ -137,10 +137,10 @@ const useDataTableInstance = ({
|
|
|
137
137
|
const getSearchableText = config.getSearchableText;
|
|
138
138
|
const globalFilterFn = useMemo(
|
|
139
139
|
() => (row, _columnId, filterValue) => {
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
const haystack = getSearchableText ? getSearchableText(row.original) : "";
|
|
143
|
-
return
|
|
140
|
+
const searchWords = normalizeText(String(filterValue ?? "")).split(/\s+/).filter(Boolean);
|
|
141
|
+
if (searchWords.length === 0) return true;
|
|
142
|
+
const haystack = normalizeText(getSearchableText ? getSearchableText(row.original) : "");
|
|
143
|
+
return searchWords.every((searchWord) => haystack.includes(searchWord));
|
|
144
144
|
},
|
|
145
145
|
[getSearchableText]
|
|
146
146
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDataTableInstance.js","sources":["../../../../src/components/UI/DataTable/useDataTableInstance.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { normalizeText } from '@/helpers/stringHelpers';\nimport {\n type ColumnSizingState,\n createColumnHelper,\n type ExpandedState,\n type FilterFn,\n getCoreRowModel,\n getExpandedRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n type PaginationState,\n type SortingState,\n type Updater,\n useReactTable,\n type VisibilityState,\n} from '@tanstack/react-table';\n\nimport DataTableActionsCell from './cells/DataTableActionsCell';\nimport DataTableCell from './cells/DataTableCell';\nimport { computeColumnContentWidths, resolveColumnMinWidthPx } from './dataTableColumnWidths';\nimport {\n DATA_TABLE_CROSSFADE_DIP_OPACITY,\n DATA_TABLE_OPACITY_TRANSITION_MS,\n DATA_TABLE_UPDATING_BAR_DELAY_MS,\n DATA_TABLE_UPDATING_OPACITY,\n dataTableOpacityTransitionClass,\n prefersReducedMotion,\n} from './dataTableMotion';\nimport {\n type IDataTableColumnConfig,\n type IDataTableConfig,\n type IDataTableRow,\n type IDataTableSortState,\n isDataTableDetailTableExpansion,\n isDataTablePanelExpansion,\n isDataTableSubRowsExpansion,\n isDataTableTableExpansion,\n type TDataTableSortDirection,\n} from './types';\nimport { useFlipRows } from './useFlipRows';\n\nconst CLIENT_SEARCH_DEBOUNCE_MS = 150;\n\nexport interface IDataTableClientPaginationState {\n page: number;\n pageSize: number;\n}\n\nexport interface IDataTablePagination {\n pageSize: number;\n numberOfPages: number;\n currentPage: number;\n setCurrentPage: (page: number) => void;\n onPageSizeChange?: (pageSize: number) => void;\n /** Total de registros. Presente, a barra mostra o intervalo em vez do número da página. */\n totalItems?: number;\n}\n\ninterface IUseDataTableInstanceParams<T extends IDataTableRow, C extends IDataTableRow = IDataTableRow> {\n config: IDataTableConfig<T, C>;\n rows: Array<T>;\n loading: boolean;\n sort?: IDataTableSortState | null;\n onSortChange?: (sort: IDataTableSortState | null) => void;\n onSearch?: (value: string) => void;\n initialSearch?: string;\n onSearchTermChange?: (value: string) => void;\n pagination?: IDataTablePagination;\n /** Client mode: seeds the internal page/pageSize (1-based page), e.g. from the URL. */\n initialClientPagination?: IDataTableClientPaginationState;\n /** Client mode: reports every page/pageSize change so the host can persist it (URL, storage). */\n onClientPaginationChange?: (pagination: IDataTableClientPaginationState) => void;\n columnVisibility: VisibilityState;\n onColumnVisibilityChange: (updater: Updater<VisibilityState>) => void;\n columnOrder: Array<string>;\n onColumnOrderChange: (updater: Updater<Array<string>>) => void;\n columnSizing: ColumnSizingState;\n onColumnSizingChange: (updater: Updater<ColumnSizingState>) => void;\n onFilteredCountChange?: (count: number) => void;\n}\n\nexport const useDataTableInstance = <T extends IDataTableRow, C extends IDataTableRow = IDataTableRow>({\n config,\n rows,\n loading,\n sort,\n onSortChange,\n onSearch,\n initialSearch = '',\n onSearchTermChange,\n pagination,\n initialClientPagination,\n onClientPaginationChange,\n columnVisibility,\n onColumnVisibilityChange,\n columnOrder,\n onColumnOrderChange,\n columnSizing,\n onColumnSizingChange,\n onFilteredCountChange,\n}: IUseDataTableInstanceParams<T, C>) => {\n const isClientMode = config.dataMode === 'client';\n\n const tabs = config.tabs;\n const [activeTab, setActiveTab] = useState(() => tabs?.[0]?.value ?? '');\n\n // Active tab predicate filters rows BEFORE search/sort/pagination (client mode).\n const tabFilteredRows = useMemo(() => {\n if (!isClientMode || !tabs?.length) return rows;\n const predicate = tabs.find((tab) => tab.value === activeTab)?.clientPredicate;\n if (!predicate) return rows;\n\n return rows.filter(predicate);\n }, [isClientMode, tabs, activeTab, rows]);\n\n const columnHelper = useMemo(() => createColumnHelper<T>(), []);\n\n // Actions and badge columns are sized from the current rows (visible action\n // count / widest chip) so the actions column scales with its buttons and badge\n // labels are never clipped. Other columns fall back to the config-only seed.\n // Both are derived in one memo so the hook signature stays unchanged.\n // Row-derived widths (actions/badge columns) kept in their own memo and read by the\n // column defs through a ref, so editing a cell (which changes the rows) does NOT\n // rebuild the column defs. Rebuilding them hands TanStack a new columns array every\n // keystroke and tears down / remounts every cell, dropping input focus.\n const columnContentWidths = useMemo(\n () => computeColumnContentWidths(config.columns, tabFilteredRows),\n [config.columns, tabFilteredRows],\n );\n const columnContentWidthsRef = useRef(columnContentWidths);\n columnContentWidthsRef.current = columnContentWidths;\n\n const columns = useMemo(() => {\n const contentWidths = columnContentWidthsRef.current;\n\n const columnDefs = config.columns.map((columnConfig, columnIndex) => {\n const accessorFn = (row: T): string | number | boolean => {\n if (columnConfig.sortAccessor) return columnConfig.sortAccessor(row);\n\n const value = columnConfig.accessor?.(row);\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n\n return '';\n };\n\n const isFirstColumn = columnIndex === 0;\n const columnWidth =\n contentWidths.get(columnConfig.id) ??\n resolveColumnMinWidthPx(columnConfig, {\n isFirstColumn,\n hasColumnToggle: isFirstColumn,\n });\n\n return columnHelper.accessor(accessorFn, {\n id: columnConfig.id,\n header: columnConfig.header,\n enableSorting: !!columnConfig.sortable,\n size: columnWidth,\n minSize: columnWidth,\n enableResizing: columnConfig.type !== 'actions' && columnConfig.resizable !== false,\n cell: ({ row }) =>\n columnConfig.type === 'actions' && columnConfig.actions ? (\n <DataTableActionsCell row={row.original} actions={columnConfig.actions} />\n ) : (\n <DataTableCell\n column={columnConfig}\n row={row.original}\n gridRowId={row.id}\n onCellChange={config.editing?.onCellChange}\n keyboardNav={config.editing?.keyboardNav}\n clickToEdit={config.editing?.clickToEdit}\n />\n ),\n });\n });\n\n return columnDefs;\n }, [config.columns, config.editing, columnHelper]);\n\n // ----- Client-mode internal state -----\n const [clientSorting, setClientSorting] = useState<SortingState>([]);\n const [globalFilter, setGlobalFilter] = useState(initialSearch);\n const [clientPagination, setClientPagination] = useState<PaginationState>(() => ({\n pageIndex: Math.max(0, (initialClientPagination?.page ?? 1) - 1),\n pageSize: initialClientPagination?.pageSize ?? 20,\n }));\n\n const onClientPaginationChangeRef = useRef(onClientPaginationChange);\n onClientPaginationChangeRef.current = onClientPaginationChange;\n\n useEffect(() => {\n onClientPaginationChangeRef.current?.({\n page: clientPagination.pageIndex + 1,\n pageSize: clientPagination.pageSize,\n });\n }, [clientPagination.pageIndex, clientPagination.pageSize]);\n const [expanded, setExpanded] = useState<ExpandedState>({});\n\n const expansion = config.expansion;\n const hasExpansion = !!expansion;\n const defaultExpanded = expansion?.defaultExpanded;\n\n // Rows the user opened or closed by hand keep that choice; `defaultExpanded` only drives the\n // rest, so a default that resolves later (a count that arrives async) never undoes a click.\n const userToggledRowIdsRef = useRef<Set<string>>(new Set());\n\n const handleExpandedChange = useCallback((updater: Updater<ExpandedState>) => {\n setExpanded((previous) => {\n const next = typeof updater === 'function' ? updater(previous) : updater;\n\n if (previous !== true && next !== true) {\n for (const rowId of new Set([...Object.keys(previous), ...Object.keys(next)])) {\n if (Boolean(previous[rowId]) !== Boolean(next[rowId])) userToggledRowIdsRef.current.add(rowId);\n }\n }\n\n return next;\n });\n }, []);\n\n useEffect(() => {\n if (!hasExpansion || defaultExpanded === undefined) return;\n\n setExpanded((previous) => {\n if (previous === true) return previous;\n\n let next: Record<string, boolean> | null = null;\n\n for (const row of tabFilteredRows) {\n const rowId = row.id;\n if (!rowId || userToggledRowIdsRef.current.has(rowId)) continue;\n\n const shouldExpand = typeof defaultExpanded === 'function' ? defaultExpanded(row) : defaultExpanded;\n if (Boolean(previous[rowId]) === shouldExpand) continue;\n\n next = next ?? { ...previous };\n if (shouldExpand) next[rowId] = true;\n else delete next[rowId];\n }\n\n return next ?? previous;\n });\n }, [hasExpansion, defaultExpanded, tabFilteredRows]);\n\n const isPanelExpansion = !!expansion && isDataTablePanelExpansion(expansion);\n const isDetailTableExpansion = !!expansion && isDataTableDetailTableExpansion(expansion);\n const isTableExpansion = !!expansion && isDataTableTableExpansion(expansion);\n const subRowsExpansion = expansion && isDataTableSubRowsExpansion(expansion) ? expansion : null;\n\n const getSearchableText = config.getSearchableText;\n\n const globalFilterFn: FilterFn<T> = useMemo(\n () => (row, _columnId, filterValue) => {\n const term = normalizeText(String(filterValue ?? '')).trim();\n if (!term) return true;\n\n const haystack = getSearchableText ? getSearchableText(row.original) : '';\n\n return normalizeText(haystack).includes(term);\n },\n [getSearchableText],\n );\n\n // ----- Server-mode external sort mapping -----\n const serverSortingState: SortingState = useMemo(() => {\n if (!sort) return [];\n const columnForField = config.columns.find((column) => column.sortField === sort.sortBy);\n if (!columnForField) return [];\n\n return [{ id: columnForField.id, desc: sort.sortDir === 'desc' }];\n }, [sort, config.columns]);\n\n const table = useReactTable({\n data: tabFilteredRows,\n columns,\n // The row id IS the entity id (prefixed by the parent for nested rows), not the\n // default positional index: the spreadsheet layer and the commit queue address\n // cells by it, so it has to survive sorting, filtering and pagination.\n getRowId: (row, index, parent) => {\n const ownId = row.id ?? String(index);\n return parent ? `${parent.id}.${ownId}` : ownId;\n },\n state: isClientMode\n ? {\n sorting: clientSorting,\n globalFilter,\n pagination: clientPagination,\n columnVisibility,\n columnOrder,\n columnSizing,\n expanded,\n }\n : { sorting: serverSortingState, columnVisibility, columnOrder, columnSizing, expanded },\n manualSorting: !isClientMode,\n manualFiltering: !isClientMode,\n manualPagination: !isClientMode,\n // A host that persists the page (URL) expects an in-place row update to keep it;\n // TanStack would otherwise jump back to page 1 whenever `rows` changes reference.\n autoResetPageIndex: onClientPaginationChange ? false : undefined,\n enableColumnResizing: true,\n // `onChange`: columns follow the cursor live during the drag (fluid, ag-grid\n // style) rather than snapping only on release.\n columnResizeMode: 'onChange',\n onColumnVisibilityChange,\n onColumnOrderChange,\n onColumnSizingChange,\n onSortingChange: isClientMode ? setClientSorting : undefined,\n onGlobalFilterChange: isClientMode ? setGlobalFilter : undefined,\n onPaginationChange: isClientMode ? setClientPagination : undefined,\n onExpandedChange: hasExpansion ? handleExpandedChange : undefined,\n globalFilterFn: isClientMode ? globalFilterFn : undefined,\n // Panel and detailTable modes have no nested rows in the row model. Omit\n // getSubRows so TanStack never looks for children. SubRows mode keeps the\n // existing lazy-children path.\n getSubRows: subRowsExpansion?.getSubRows,\n getRowCanExpand: expansion\n ? (row) => {\n if (expansion.canExpand) return expansion.canExpand(row.original);\n // Panel/detailTable/table: every row is expandable unless canExpand says otherwise.\n if (isPanelExpansion || isDetailTableExpansion || isTableExpansion) return true;\n return row.subRows.length > 0;\n }\n : undefined,\n // Client mode keeps children glued to their parent (not counted toward the\n // page size). The pagination row model re-inserts them. Server mode has NO\n // pagination row model, so `false` would leave `getExpandedRowModel` without\n // anything to flatten the children and they would never render; `true` makes\n // the expanded model flatten them itself (server already paginates parents).\n // Panel and detailTable modes have no child rows, so the flag is irrelevant.\n paginateExpandedRows: !isClientMode,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: isClientMode ? getFilteredRowModel() : undefined,\n getSortedRowModel: isClientMode ? getSortedRowModel() : undefined,\n getPaginationRowModel: isClientMode ? getPaginationRowModel() : undefined,\n getExpandedRowModel: hasExpansion ? getExpandedRowModel() : undefined,\n });\n\n const visibleRows = table.getRowModel().rows;\n const orderedRowIds = useMemo(() => visibleRows.map((row) => row.id).join('|'), [visibleRows]);\n // SubRows expansion uses a slower slide so the subgroup open/close reads clearly.\n // Panel and detailTable expansion only toggle a content row. Keep the default\n // FLIP timing for those.\n const flipDurationMs = subRowsExpansion ? 420 : undefined;\n const { registerRow, beginExit: beginRowsExit } = useFlipRows(\n useMemo(() => orderedRowIds.split('|'), [orderedRowIds]),\n flipDurationMs,\n );\n\n const filteredCount = isClientMode ? table.getFilteredRowModel().rows.length : rows.length;\n\n useEffect(() => {\n onFilteredCountChange?.(filteredCount);\n }, [filteredCount, onFilteredCountChange]);\n\n // In server mode, header click round-trips through the host. In client mode\n // TanStack drives the sorting state directly (cycle: asc -> desc -> none).\n const handleHeaderSortClick = (columnConfig: IDataTableColumnConfig<T>) => {\n if (!columnConfig.sortable) return;\n\n if (isClientMode) {\n table.getColumn(columnConfig.id)?.toggleSorting(undefined, false);\n return;\n }\n\n if (!columnConfig.sortField || !onSortChange) return;\n\n const isActive = sort?.sortBy === columnConfig.sortField;\n let nextDir: TDataTableSortDirection = 'asc';\n\n if (isActive && sort?.sortDir === 'asc') nextDir = 'desc';\n else if (isActive && sort?.sortDir === 'desc') {\n onSortChange(null);\n return;\n }\n\n onSortChange({ sortBy: columnConfig.sortField, sortDir: nextDir });\n };\n\n const resolveSortDirection = (columnConfig: IDataTableColumnConfig<T>): false | 'asc' | 'desc' => {\n if (isClientMode) {\n const direction = table.getColumn(columnConfig.id)?.getIsSorted();\n return direction ?? false;\n }\n\n if (sort?.sortBy !== columnConfig.sortField) return false;\n return sort?.sortDir ?? 'asc';\n };\n\n const handleSearchInput = (value: string) => {\n onSearchTermChange?.(value);\n\n if (isClientMode) {\n setGlobalFilter(value);\n table.setPageIndex(0);\n return;\n }\n\n onSearch?.(value);\n };\n\n const handleTabChange = (value: string) => {\n setActiveTab(value);\n if (isClientMode) table.setPageIndex(0);\n };\n\n // Skeleton is shown ONLY on the very first load (no rows yet). Filter/sort/\n // pagination never swap the loaded table for skeletons. That is the \"blink\".\n const showSkeleton = loading && rows.length === 0;\n // Server mode keeps rows visible during a refetch with a subtle updating state.\n const isUpdating = !isClientMode && loading && rows.length > 0;\n\n const [showUpdatingBar, setShowUpdatingBar] = useState(false);\n const [contentOpacity, setContentOpacity] = useState(1);\n const wasUpdatingRef = useRef(false);\n\n useEffect(() => {\n if (!isUpdating) {\n setShowUpdatingBar(false);\n return;\n }\n\n if (prefersReducedMotion()) return;\n\n const timer = window.setTimeout(() => setShowUpdatingBar(true), DATA_TABLE_UPDATING_BAR_DELAY_MS);\n return () => window.clearTimeout(timer);\n }, [isUpdating]);\n\n useEffect(() => {\n const reducedMotion = prefersReducedMotion();\n\n if (wasUpdatingRef.current && !isUpdating && !isClientMode) {\n if (reducedMotion) {\n setContentOpacity(1);\n } else {\n setContentOpacity(DATA_TABLE_CROSSFADE_DIP_OPACITY);\n const frame = window.requestAnimationFrame(() => setContentOpacity(1));\n wasUpdatingRef.current = isUpdating;\n return () => window.cancelAnimationFrame(frame);\n }\n }\n\n wasUpdatingRef.current = isUpdating;\n }, [isUpdating, isClientMode]);\n\n const contentOpacityStyle = useMemo(() => {\n if (prefersReducedMotion()) return undefined;\n\n const opacity = isUpdating ? DATA_TABLE_UPDATING_OPACITY : contentOpacity;\n\n return {\n opacity,\n transition: `opacity ${DATA_TABLE_OPACITY_TRANSITION_MS}ms ease-enter`,\n };\n }, [isUpdating, contentOpacity]);\n\n const clientPaginationProps: IDataTablePagination | undefined = isClientMode\n ? {\n pageSize: clientPagination.pageSize,\n numberOfPages: table.getPageCount(),\n currentPage: clientPagination.pageIndex + 1,\n totalItems: table.getFilteredRowModel().rows.length,\n setCurrentPage: (page: number) => table.setPageIndex(page - 1),\n onPageSizeChange: (size: number) => {\n table.setPageSize(size);\n table.setPageIndex(0);\n },\n }\n : pagination;\n\n const hasRows = visibleRows.length > 0;\n\n return {\n table,\n isClientMode,\n columnContentWidths,\n registerRow,\n beginRowsExit,\n visibleRows,\n orderedRowIds,\n filteredCount,\n activeTab,\n tabs,\n handleTabChange,\n handleSearchInput,\n handleHeaderSortClick,\n resolveSortDirection,\n clientPaginationProps,\n isUpdating,\n showUpdatingBar,\n contentOpacityStyle,\n dataTableOpacityTransitionClass,\n showSkeleton,\n hasRows,\n clientSearchDebounceMs: CLIENT_SEARCH_DEBOUNCE_MS,\n };\n};\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useDataTableInstance.js","sources":["../../../../src/components/UI/DataTable/useDataTableInstance.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { normalizeText } from '@/helpers/stringHelpers';\nimport {\n type ColumnSizingState,\n createColumnHelper,\n type ExpandedState,\n type FilterFn,\n getCoreRowModel,\n getExpandedRowModel,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n type PaginationState,\n type SortingState,\n type Updater,\n useReactTable,\n type VisibilityState,\n} from '@tanstack/react-table';\n\nimport DataTableActionsCell from './cells/DataTableActionsCell';\nimport DataTableCell from './cells/DataTableCell';\nimport { computeColumnContentWidths, resolveColumnMinWidthPx } from './dataTableColumnWidths';\nimport {\n DATA_TABLE_CROSSFADE_DIP_OPACITY,\n DATA_TABLE_OPACITY_TRANSITION_MS,\n DATA_TABLE_UPDATING_BAR_DELAY_MS,\n DATA_TABLE_UPDATING_OPACITY,\n dataTableOpacityTransitionClass,\n prefersReducedMotion,\n} from './dataTableMotion';\nimport {\n type IDataTableColumnConfig,\n type IDataTableConfig,\n type IDataTableRow,\n type IDataTableSortState,\n isDataTableDetailTableExpansion,\n isDataTablePanelExpansion,\n isDataTableSubRowsExpansion,\n isDataTableTableExpansion,\n type TDataTableSortDirection,\n} from './types';\nimport { useFlipRows } from './useFlipRows';\n\nconst CLIENT_SEARCH_DEBOUNCE_MS = 150;\n\nexport interface IDataTableClientPaginationState {\n page: number;\n pageSize: number;\n}\n\nexport interface IDataTablePagination {\n pageSize: number;\n numberOfPages: number;\n currentPage: number;\n setCurrentPage: (page: number) => void;\n onPageSizeChange?: (pageSize: number) => void;\n /** Total de registros. Presente, a barra mostra o intervalo em vez do número da página. */\n totalItems?: number;\n}\n\ninterface IUseDataTableInstanceParams<T extends IDataTableRow, C extends IDataTableRow = IDataTableRow> {\n config: IDataTableConfig<T, C>;\n rows: Array<T>;\n loading: boolean;\n sort?: IDataTableSortState | null;\n onSortChange?: (sort: IDataTableSortState | null) => void;\n onSearch?: (value: string) => void;\n initialSearch?: string;\n onSearchTermChange?: (value: string) => void;\n pagination?: IDataTablePagination;\n /** Client mode: seeds the internal page/pageSize (1-based page), e.g. from the URL. */\n initialClientPagination?: IDataTableClientPaginationState;\n /** Client mode: reports every page/pageSize change so the host can persist it (URL, storage). */\n onClientPaginationChange?: (pagination: IDataTableClientPaginationState) => void;\n columnVisibility: VisibilityState;\n onColumnVisibilityChange: (updater: Updater<VisibilityState>) => void;\n columnOrder: Array<string>;\n onColumnOrderChange: (updater: Updater<Array<string>>) => void;\n columnSizing: ColumnSizingState;\n onColumnSizingChange: (updater: Updater<ColumnSizingState>) => void;\n onFilteredCountChange?: (count: number) => void;\n}\n\nexport const useDataTableInstance = <T extends IDataTableRow, C extends IDataTableRow = IDataTableRow>({\n config,\n rows,\n loading,\n sort,\n onSortChange,\n onSearch,\n initialSearch = '',\n onSearchTermChange,\n pagination,\n initialClientPagination,\n onClientPaginationChange,\n columnVisibility,\n onColumnVisibilityChange,\n columnOrder,\n onColumnOrderChange,\n columnSizing,\n onColumnSizingChange,\n onFilteredCountChange,\n}: IUseDataTableInstanceParams<T, C>) => {\n const isClientMode = config.dataMode === 'client';\n\n const tabs = config.tabs;\n const [activeTab, setActiveTab] = useState(() => tabs?.[0]?.value ?? '');\n\n // Active tab predicate filters rows BEFORE search/sort/pagination (client mode).\n const tabFilteredRows = useMemo(() => {\n if (!isClientMode || !tabs?.length) return rows;\n const predicate = tabs.find((tab) => tab.value === activeTab)?.clientPredicate;\n if (!predicate) return rows;\n\n return rows.filter(predicate);\n }, [isClientMode, tabs, activeTab, rows]);\n\n const columnHelper = useMemo(() => createColumnHelper<T>(), []);\n\n // Actions and badge columns are sized from the current rows (visible action\n // count / widest chip) so the actions column scales with its buttons and badge\n // labels are never clipped. Other columns fall back to the config-only seed.\n // Both are derived in one memo so the hook signature stays unchanged.\n // Row-derived widths (actions/badge columns) kept in their own memo and read by the\n // column defs through a ref, so editing a cell (which changes the rows) does NOT\n // rebuild the column defs. Rebuilding them hands TanStack a new columns array every\n // keystroke and tears down / remounts every cell, dropping input focus.\n const columnContentWidths = useMemo(\n () => computeColumnContentWidths(config.columns, tabFilteredRows),\n [config.columns, tabFilteredRows],\n );\n const columnContentWidthsRef = useRef(columnContentWidths);\n columnContentWidthsRef.current = columnContentWidths;\n\n const columns = useMemo(() => {\n const contentWidths = columnContentWidthsRef.current;\n\n const columnDefs = config.columns.map((columnConfig, columnIndex) => {\n const accessorFn = (row: T): string | number | boolean => {\n if (columnConfig.sortAccessor) return columnConfig.sortAccessor(row);\n\n const value = columnConfig.accessor?.(row);\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n\n return '';\n };\n\n const isFirstColumn = columnIndex === 0;\n const columnWidth =\n contentWidths.get(columnConfig.id) ??\n resolveColumnMinWidthPx(columnConfig, {\n isFirstColumn,\n hasColumnToggle: isFirstColumn,\n });\n\n return columnHelper.accessor(accessorFn, {\n id: columnConfig.id,\n header: columnConfig.header,\n enableSorting: !!columnConfig.sortable,\n size: columnWidth,\n minSize: columnWidth,\n enableResizing: columnConfig.type !== 'actions' && columnConfig.resizable !== false,\n cell: ({ row }) =>\n columnConfig.type === 'actions' && columnConfig.actions ? (\n <DataTableActionsCell row={row.original} actions={columnConfig.actions} />\n ) : (\n <DataTableCell\n column={columnConfig}\n row={row.original}\n gridRowId={row.id}\n onCellChange={config.editing?.onCellChange}\n keyboardNav={config.editing?.keyboardNav}\n clickToEdit={config.editing?.clickToEdit}\n />\n ),\n });\n });\n\n return columnDefs;\n }, [config.columns, config.editing, columnHelper]);\n\n // ----- Client-mode internal state -----\n const [clientSorting, setClientSorting] = useState<SortingState>([]);\n const [globalFilter, setGlobalFilter] = useState(initialSearch);\n const [clientPagination, setClientPagination] = useState<PaginationState>(() => ({\n pageIndex: Math.max(0, (initialClientPagination?.page ?? 1) - 1),\n pageSize: initialClientPagination?.pageSize ?? 20,\n }));\n\n const onClientPaginationChangeRef = useRef(onClientPaginationChange);\n onClientPaginationChangeRef.current = onClientPaginationChange;\n\n useEffect(() => {\n onClientPaginationChangeRef.current?.({\n page: clientPagination.pageIndex + 1,\n pageSize: clientPagination.pageSize,\n });\n }, [clientPagination.pageIndex, clientPagination.pageSize]);\n const [expanded, setExpanded] = useState<ExpandedState>({});\n\n const expansion = config.expansion;\n const hasExpansion = !!expansion;\n const defaultExpanded = expansion?.defaultExpanded;\n\n // Rows the user opened or closed by hand keep that choice; `defaultExpanded` only drives the\n // rest, so a default that resolves later (a count that arrives async) never undoes a click.\n const userToggledRowIdsRef = useRef<Set<string>>(new Set());\n\n const handleExpandedChange = useCallback((updater: Updater<ExpandedState>) => {\n setExpanded((previous) => {\n const next = typeof updater === 'function' ? updater(previous) : updater;\n\n if (previous !== true && next !== true) {\n for (const rowId of new Set([...Object.keys(previous), ...Object.keys(next)])) {\n if (Boolean(previous[rowId]) !== Boolean(next[rowId])) userToggledRowIdsRef.current.add(rowId);\n }\n }\n\n return next;\n });\n }, []);\n\n useEffect(() => {\n if (!hasExpansion || defaultExpanded === undefined) return;\n\n setExpanded((previous) => {\n if (previous === true) return previous;\n\n let next: Record<string, boolean> | null = null;\n\n for (const row of tabFilteredRows) {\n const rowId = row.id;\n if (!rowId || userToggledRowIdsRef.current.has(rowId)) continue;\n\n const shouldExpand = typeof defaultExpanded === 'function' ? defaultExpanded(row) : defaultExpanded;\n if (Boolean(previous[rowId]) === shouldExpand) continue;\n\n next = next ?? { ...previous };\n if (shouldExpand) next[rowId] = true;\n else delete next[rowId];\n }\n\n return next ?? previous;\n });\n }, [hasExpansion, defaultExpanded, tabFilteredRows]);\n\n const isPanelExpansion = !!expansion && isDataTablePanelExpansion(expansion);\n const isDetailTableExpansion = !!expansion && isDataTableDetailTableExpansion(expansion);\n const isTableExpansion = !!expansion && isDataTableTableExpansion(expansion);\n const subRowsExpansion = expansion && isDataTableSubRowsExpansion(expansion) ? expansion : null;\n\n const getSearchableText = config.getSearchableText;\n\n const globalFilterFn: FilterFn<T> = useMemo(\n () => (row, _columnId, filterValue) => {\n const searchWords = normalizeText(String(filterValue ?? ''))\n .split(/\\s+/)\n .filter(Boolean);\n if (searchWords.length === 0) return true;\n\n const haystack = normalizeText(getSearchableText ? getSearchableText(row.original) : '');\n\n return searchWords.every((searchWord) => haystack.includes(searchWord));\n },\n [getSearchableText],\n );\n\n // ----- Server-mode external sort mapping -----\n const serverSortingState: SortingState = useMemo(() => {\n if (!sort) return [];\n const columnForField = config.columns.find((column) => column.sortField === sort.sortBy);\n if (!columnForField) return [];\n\n return [{ id: columnForField.id, desc: sort.sortDir === 'desc' }];\n }, [sort, config.columns]);\n\n const table = useReactTable({\n data: tabFilteredRows,\n columns,\n // The row id IS the entity id (prefixed by the parent for nested rows), not the\n // default positional index: the spreadsheet layer and the commit queue address\n // cells by it, so it has to survive sorting, filtering and pagination.\n getRowId: (row, index, parent) => {\n const ownId = row.id ?? String(index);\n return parent ? `${parent.id}.${ownId}` : ownId;\n },\n state: isClientMode\n ? {\n sorting: clientSorting,\n globalFilter,\n pagination: clientPagination,\n columnVisibility,\n columnOrder,\n columnSizing,\n expanded,\n }\n : { sorting: serverSortingState, columnVisibility, columnOrder, columnSizing, expanded },\n manualSorting: !isClientMode,\n manualFiltering: !isClientMode,\n manualPagination: !isClientMode,\n // A host that persists the page (URL) expects an in-place row update to keep it;\n // TanStack would otherwise jump back to page 1 whenever `rows` changes reference.\n autoResetPageIndex: onClientPaginationChange ? false : undefined,\n enableColumnResizing: true,\n // `onChange`: columns follow the cursor live during the drag (fluid, ag-grid\n // style) rather than snapping only on release.\n columnResizeMode: 'onChange',\n onColumnVisibilityChange,\n onColumnOrderChange,\n onColumnSizingChange,\n onSortingChange: isClientMode ? setClientSorting : undefined,\n onGlobalFilterChange: isClientMode ? setGlobalFilter : undefined,\n onPaginationChange: isClientMode ? setClientPagination : undefined,\n onExpandedChange: hasExpansion ? handleExpandedChange : undefined,\n globalFilterFn: isClientMode ? globalFilterFn : undefined,\n // Panel and detailTable modes have no nested rows in the row model. Omit\n // getSubRows so TanStack never looks for children. SubRows mode keeps the\n // existing lazy-children path.\n getSubRows: subRowsExpansion?.getSubRows,\n getRowCanExpand: expansion\n ? (row) => {\n if (expansion.canExpand) return expansion.canExpand(row.original);\n // Panel/detailTable/table: every row is expandable unless canExpand says otherwise.\n if (isPanelExpansion || isDetailTableExpansion || isTableExpansion) return true;\n return row.subRows.length > 0;\n }\n : undefined,\n // Client mode keeps children glued to their parent (not counted toward the\n // page size). The pagination row model re-inserts them. Server mode has NO\n // pagination row model, so `false` would leave `getExpandedRowModel` without\n // anything to flatten the children and they would never render; `true` makes\n // the expanded model flatten them itself (server already paginates parents).\n // Panel and detailTable modes have no child rows, so the flag is irrelevant.\n paginateExpandedRows: !isClientMode,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: isClientMode ? getFilteredRowModel() : undefined,\n getSortedRowModel: isClientMode ? getSortedRowModel() : undefined,\n getPaginationRowModel: isClientMode ? getPaginationRowModel() : undefined,\n getExpandedRowModel: hasExpansion ? getExpandedRowModel() : undefined,\n });\n\n const visibleRows = table.getRowModel().rows;\n const orderedRowIds = useMemo(() => visibleRows.map((row) => row.id).join('|'), [visibleRows]);\n // SubRows expansion uses a slower slide so the subgroup open/close reads clearly.\n // Panel and detailTable expansion only toggle a content row. Keep the default\n // FLIP timing for those.\n const flipDurationMs = subRowsExpansion ? 420 : undefined;\n const { registerRow, beginExit: beginRowsExit } = useFlipRows(\n useMemo(() => orderedRowIds.split('|'), [orderedRowIds]),\n flipDurationMs,\n );\n\n const filteredCount = isClientMode ? table.getFilteredRowModel().rows.length : rows.length;\n\n useEffect(() => {\n onFilteredCountChange?.(filteredCount);\n }, [filteredCount, onFilteredCountChange]);\n\n // In server mode, header click round-trips through the host. In client mode\n // TanStack drives the sorting state directly (cycle: asc -> desc -> none).\n const handleHeaderSortClick = (columnConfig: IDataTableColumnConfig<T>) => {\n if (!columnConfig.sortable) return;\n\n if (isClientMode) {\n table.getColumn(columnConfig.id)?.toggleSorting(undefined, false);\n return;\n }\n\n if (!columnConfig.sortField || !onSortChange) return;\n\n const isActive = sort?.sortBy === columnConfig.sortField;\n let nextDir: TDataTableSortDirection = 'asc';\n\n if (isActive && sort?.sortDir === 'asc') nextDir = 'desc';\n else if (isActive && sort?.sortDir === 'desc') {\n onSortChange(null);\n return;\n }\n\n onSortChange({ sortBy: columnConfig.sortField, sortDir: nextDir });\n };\n\n const resolveSortDirection = (columnConfig: IDataTableColumnConfig<T>): false | 'asc' | 'desc' => {\n if (isClientMode) {\n const direction = table.getColumn(columnConfig.id)?.getIsSorted();\n return direction ?? false;\n }\n\n if (sort?.sortBy !== columnConfig.sortField) return false;\n return sort?.sortDir ?? 'asc';\n };\n\n const handleSearchInput = (value: string) => {\n onSearchTermChange?.(value);\n\n if (isClientMode) {\n setGlobalFilter(value);\n table.setPageIndex(0);\n return;\n }\n\n onSearch?.(value);\n };\n\n const handleTabChange = (value: string) => {\n setActiveTab(value);\n if (isClientMode) table.setPageIndex(0);\n };\n\n // Skeleton is shown ONLY on the very first load (no rows yet). Filter/sort/\n // pagination never swap the loaded table for skeletons. That is the \"blink\".\n const showSkeleton = loading && rows.length === 0;\n // Server mode keeps rows visible during a refetch with a subtle updating state.\n const isUpdating = !isClientMode && loading && rows.length > 0;\n\n const [showUpdatingBar, setShowUpdatingBar] = useState(false);\n const [contentOpacity, setContentOpacity] = useState(1);\n const wasUpdatingRef = useRef(false);\n\n useEffect(() => {\n if (!isUpdating) {\n setShowUpdatingBar(false);\n return;\n }\n\n if (prefersReducedMotion()) return;\n\n const timer = window.setTimeout(() => setShowUpdatingBar(true), DATA_TABLE_UPDATING_BAR_DELAY_MS);\n return () => window.clearTimeout(timer);\n }, [isUpdating]);\n\n useEffect(() => {\n const reducedMotion = prefersReducedMotion();\n\n if (wasUpdatingRef.current && !isUpdating && !isClientMode) {\n if (reducedMotion) {\n setContentOpacity(1);\n } else {\n setContentOpacity(DATA_TABLE_CROSSFADE_DIP_OPACITY);\n const frame = window.requestAnimationFrame(() => setContentOpacity(1));\n wasUpdatingRef.current = isUpdating;\n return () => window.cancelAnimationFrame(frame);\n }\n }\n\n wasUpdatingRef.current = isUpdating;\n }, [isUpdating, isClientMode]);\n\n const contentOpacityStyle = useMemo(() => {\n if (prefersReducedMotion()) return undefined;\n\n const opacity = isUpdating ? DATA_TABLE_UPDATING_OPACITY : contentOpacity;\n\n return {\n opacity,\n transition: `opacity ${DATA_TABLE_OPACITY_TRANSITION_MS}ms ease-enter`,\n };\n }, [isUpdating, contentOpacity]);\n\n const clientPaginationProps: IDataTablePagination | undefined = isClientMode\n ? {\n pageSize: clientPagination.pageSize,\n numberOfPages: table.getPageCount(),\n currentPage: clientPagination.pageIndex + 1,\n totalItems: table.getFilteredRowModel().rows.length,\n setCurrentPage: (page: number) => table.setPageIndex(page - 1),\n onPageSizeChange: (size: number) => {\n table.setPageSize(size);\n table.setPageIndex(0);\n },\n }\n : pagination;\n\n const hasRows = visibleRows.length > 0;\n\n return {\n table,\n isClientMode,\n columnContentWidths,\n registerRow,\n beginRowsExit,\n visibleRows,\n orderedRowIds,\n filteredCount,\n activeTab,\n tabs,\n handleTabChange,\n handleSearchInput,\n handleHeaderSortClick,\n resolveSortDirection,\n clientPaginationProps,\n isUpdating,\n showUpdatingBar,\n contentOpacityStyle,\n dataTableOpacityTransitionClass,\n showSkeleton,\n hasRows,\n clientSearchDebounceMs: CLIENT_SEARCH_DEBOUNCE_MS,\n };\n};\n"],"names":[],"mappings":"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"}
|