@umodeapporg/ui 0.22.2 → 0.23.0
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/assets/umode-ui.css +105 -0
- 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/Flow/FlowBlockNode.d.ts +35 -0
- package/dist/components/UI/Flow/FlowBlockNode.js +84 -0
- package/dist/components/UI/Flow/FlowBlockNode.js.map +1 -0
- package/dist/components/UI/Flow/FlowCanvas.d.ts +22 -0
- package/dist/components/UI/Flow/FlowCanvas.js +263 -0
- package/dist/components/UI/Flow/FlowCanvas.js.map +1 -0
- package/dist/components/UI/Flow/FlowConnectionEdge.d.ts +10 -0
- package/dist/components/UI/Flow/FlowConnectionEdge.js +54 -0
- package/dist/components/UI/Flow/FlowConnectionEdge.js.map +1 -0
- package/dist/components/UI/Flow/FlowEditorProvider.d.ts +17 -0
- package/dist/components/UI/Flow/FlowEditorProvider.js +27 -0
- package/dist/components/UI/Flow/FlowEditorProvider.js.map +1 -0
- package/dist/components/UI/Flow/FlowInspector.d.ts +11 -0
- package/dist/components/UI/Flow/FlowInspector.js +124 -0
- package/dist/components/UI/Flow/FlowInspector.js.map +1 -0
- package/dist/components/UI/Flow/FlowIssueList.d.ts +8 -0
- package/dist/components/UI/Flow/FlowIssueList.js +55 -0
- package/dist/components/UI/Flow/FlowIssueList.js.map +1 -0
- package/dist/components/UI/Flow/FlowLaneMap.d.ts +30 -0
- package/dist/components/UI/Flow/FlowLaneMap.js +206 -0
- package/dist/components/UI/Flow/FlowLaneMap.js.map +1 -0
- package/dist/components/UI/Flow/FlowNodeShell.d.ts +20 -0
- package/dist/components/UI/Flow/FlowNodeShell.js +81 -0
- package/dist/components/UI/Flow/FlowNodeShell.js.map +1 -0
- package/dist/components/UI/Flow/FlowPalette.d.ts +7 -0
- package/dist/components/UI/Flow/FlowPalette.js +90 -0
- package/dist/components/UI/Flow/FlowPalette.js.map +1 -0
- package/dist/components/UI/Flow/FlowResizablePanel.d.ts +20 -0
- package/dist/components/UI/Flow/FlowResizablePanel.js +117 -0
- package/dist/components/UI/Flow/FlowResizablePanel.js.map +1 -0
- package/dist/components/UI/Flow/FlowZoomControls.d.ts +12 -0
- package/dist/components/UI/Flow/FlowZoomControls.js +53 -0
- package/dist/components/UI/Flow/FlowZoomControls.js.map +1 -0
- package/dist/components/UI/Flow/flowBlockRegistry.d.ts +12 -0
- package/dist/components/UI/Flow/flowBlockRegistry.js +15 -0
- package/dist/components/UI/Flow/flowBlockRegistry.js.map +1 -0
- package/dist/components/UI/Flow/flowConstants.d.ts +2 -0
- package/dist/components/UI/Flow/flowConstants.js +5 -0
- package/dist/components/UI/Flow/flowConstants.js.map +1 -0
- package/dist/components/UI/Flow/flowEditorStore.d.ts +44 -0
- package/dist/components/UI/Flow/flowEditorStore.js +258 -0
- package/dist/components/UI/Flow/flowEditorStore.js.map +1 -0
- package/dist/components/UI/Flow/flowGraph.d.ts +32 -0
- package/dist/components/UI/Flow/flowGraph.js +156 -0
- package/dist/components/UI/Flow/flowGraph.js.map +1 -0
- package/dist/components/UI/Flow/flowTypes.d.ts +74 -0
- package/dist/components/UI/Flow/flowTypes.js +2 -0
- package/dist/components/UI/Flow/flowTypes.js.map +1 -0
- package/dist/components/UI/Flow/index.d.ts +14 -0
- package/dist/components/UI/Flow/index.js +43 -0
- package/dist/components/UI/Flow/index.js.map +1 -0
- package/dist/components/UI/Flow/useFlowLabels.d.ts +10 -0
- package/dist/components/UI/Flow/useFlowLabels.js +51 -0
- package/dist/components/UI/Flow/useFlowLabels.js.map +1 -0
- package/dist/locales/en/ui.json.d.ts +67 -0
- package/dist/locales/en/ui.json.js +3 -0
- package/dist/locales/en/ui.json.js.map +1 -1
- package/dist/locales/index.d.ts +134 -0
- package/dist/locales/pt/ui.json.d.ts +67 -0
- package/dist/locales/pt/ui.json.js +3 -0
- package/dist/locales/pt/ui.json.js.map +1 -1
- package/package.json +10 -1
package/dist/assets/umode-ui.css
CHANGED
|
@@ -338,3 +338,108 @@
|
|
|
338
338
|
height: 1.25rem;
|
|
339
339
|
font-size: 1.25rem;
|
|
340
340
|
}
|
|
341
|
+
/* Theme bridge for @xyflow/react internals the components cannot reach with classes. */
|
|
342
|
+
.umode-flow .react-flow__pane {
|
|
343
|
+
cursor: default;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.umode-flow .react-flow__background {
|
|
347
|
+
--xy-background-pattern-color: rgb(var(--color-border-subtle));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.umode-flow .umode-flow-handle {
|
|
351
|
+
width: 0.75rem;
|
|
352
|
+
height: 0.75rem;
|
|
353
|
+
border: 2px solid rgb(var(--color-surface));
|
|
354
|
+
background: rgb(var(--color-border-strong));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.umode-flow .umode-flow-handle.connectionindicator:hover,
|
|
358
|
+
.umode-flow .umode-flow-handle.connectingto {
|
|
359
|
+
background: rgb(var(--color-primary));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.umode-flow .umode-flow-edge-path {
|
|
363
|
+
stroke: rgb(var(--color-border-strong));
|
|
364
|
+
stroke-width: 1.5;
|
|
365
|
+
transition: stroke 140ms ease-out;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.umode-flow .umode-flow-edge-path--selected {
|
|
369
|
+
stroke: rgb(var(--color-primary));
|
|
370
|
+
stroke-width: 2;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.umode-flow .umode-flow-edge-path--traversed {
|
|
374
|
+
stroke: rgb(var(--color-primary));
|
|
375
|
+
stroke-width: 2.5;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.umode-flow .umode-flow-edge-path--warning {
|
|
379
|
+
stroke: rgb(var(--color-warning));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.umode-flow .umode-flow-edge-path--error {
|
|
383
|
+
stroke: rgb(var(--color-danger));
|
|
384
|
+
stroke-dasharray: 6 4;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.umode-flow .react-flow__minimap {
|
|
388
|
+
background: rgb(var(--color-surface));
|
|
389
|
+
border: 1px solid rgb(var(--color-border));
|
|
390
|
+
border-radius: 0.75rem;
|
|
391
|
+
overflow: hidden;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.umode-flow .react-flow__minimap-mask {
|
|
395
|
+
fill: rgb(var(--color-muted) / 0.7);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.umode-flow .react-flow__minimap-node {
|
|
399
|
+
fill: rgb(var(--color-border-emphasis));
|
|
400
|
+
stroke: none;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.umode-flow .react-flow__attribution {
|
|
404
|
+
background: transparent;
|
|
405
|
+
color: rgb(var(--color-foreground-faint));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.umode-flow .react-flow__connectionline path {
|
|
409
|
+
stroke: rgb(var(--color-primary));
|
|
410
|
+
stroke-width: 2;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.umode-flow .umode-flow-arc {
|
|
414
|
+
stroke: rgb(var(--color-border-strong));
|
|
415
|
+
stroke-width: 1.5;
|
|
416
|
+
fill: none;
|
|
417
|
+
transition: stroke 140ms ease-out;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.umode-flow .umode-flow-arc--warning {
|
|
421
|
+
stroke: rgb(var(--color-warning));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.umode-flow .umode-flow-arc--danger {
|
|
425
|
+
stroke: rgb(var(--color-danger));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.umode-flow .umode-flow-arc--dashed {
|
|
429
|
+
stroke-dasharray: 6 4;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.umode-flow .umode-flow-arc--selected,
|
|
433
|
+
.umode-flow .react-flow__edge:hover .umode-flow-arc {
|
|
434
|
+
stroke: rgb(var(--color-primary));
|
|
435
|
+
stroke-width: 2.5;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.umode-flow .react-flow__edge {
|
|
439
|
+
cursor: pointer;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.umode-flow .react-flow__arrowhead polyline {
|
|
443
|
+
stroke: rgb(var(--color-border-strong));
|
|
444
|
+
fill: rgb(var(--color-border-strong));
|
|
445
|
+
}
|
|
@@ -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":";;;;;;;;;;;;;;AA6CA,MAAM,WAAW,CAAC,UAA4B,OAAO,UAAU,WAAW,QAAQ;AAClF,MAAM,gBAAgB,CAAC,UAAmC,MAAM,QAAQ,KAAK,IAAI,MAAM,IAAI,MAAM,IAAI,CAAA;AAIrG,MAAM,cAA+D,CAAC,EAAE,OAAO,OAAO,UAAU,eAC9F;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,OAAO,MAAM;AAAA,IACb,aAAa,MAAM;AAAA,IACnB,OAAO,SAAS,KAAK;AAAA,IACrB;AAAA,IACA,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,EAAA;AAClD;AAGF,MAAM,gBAAmE,CAAC,EAAE,OAAO,OAAO,UAAU,eAClG;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,MAAK;AAAA,IACL,OAAO,MAAM;AAAA,IACb,aAAa,MAAM;AAAA,IACnB,KAAK,MAAM;AAAA,IACX,KAAK,MAAM;AAAA,IACX,OAAO,UAAU,UAAa,UAAU,OAAO,KAAK,OAAO,KAAK;AAAA,IAChE;AAAA,IACA,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK;AAAA,EAAA;AAClD;AAKF,MAAM,gBAAmE,CAAC,EAAE,OAAO,OAAO,UAAU,eAClG;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,OAAO,MAAM;AAAA,IACb,SAAS,MAAM,WAAW,CAAA;AAAA,IAC1B,eAAe,SAAS,KAAK;AAAA,IAC7B,YAAY,MAAM;AAAA,IAClB,WAAW,MAAM,aAAa;AAAA,IAC9B;AAAA,IACA,cAAc,CAAC,aAAa,SAAS,MAAM,QAAQ,QAAQ,IAAK,SAAS,CAAC,KAAK,KAAM,QAAQ;AAAA,EAAA;AAC/F;AAGF,MAAM,qBAA6E,CAAC;AAAA,EAClF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MACE;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,UAAQ;AAAA,IACR,cAAY;AAAA,IACZ,OAAO,MAAM;AAAA,IACb,SAAS,MAAM,WAAW,CAAA;AAAA,IAC1B,eAAe,cAAc,KAAK;AAAA,IAClC,YAAY,MAAM;AAAA,IAClB,WAAW,MAAM,aAAa;AAAA,IAC9B;AAAA,IACA,cAAc,CAAC,aAAa,SAAS,MAAM,QAAQ,QAAQ,IAAI,WAAW,CAAC,QAAQ,CAAC;AAAA,EAAA;AACtF;AAQF,MAAM,qBAA6E,CAAC;AAAA,EAClF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,CAAC,SAAS,UAAU,IAAI,SAA+B,MAAM,kBAAkB,EAAE;AACvF,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,UAAU,OAAO,CAAC;AAGxB,QAAM,gBAAgB,OAAO,CAAC;AAC9B,QAAM,YAAY,OAAO,MAAM;AAC/B,YAAU,UAAU;AAEpB,QAAM,WAAW;AAAA,IACf,OAAO,MAAc,QAA4B,WAAmD;AAClG,YAAM,aAAa,EAAE,cAAc;AACnC,iBAAW,IAAI;AACf,gBAAU,KAAK;AACf,UAAI;AACF,cAAM,SAAS,MAAM,MAAM,YAAY,EAAE,MAAM,QAAQ,QAAQ,UAAU,SAAS;AAClF,YAAI,eAAe,cAAc,QAAS,QAAO,CAAA;AAEjD,cAAM,cAAc,QAAQ,WAAW,CAAA;AACvC,gBAAQ,UAAU;AAClB,mBAAW,QAAQ,QAAQ,OAAO,CAAC;AACnC,mBAAW,CAAC,aAAc,SAAS,CAAC,GAAG,UAAU,GAAG,WAAW,IAAI,WAAY;AAE/E,eAAO;AAAA,MACT,SAAS,OAAO;AACd,YAAI,eAAe,cAAc,SAAS;AACxC,mBAAS,MAAM,oDAAoD,MAAM,GAAG,KAAK,KAAK;AACtF,oBAAU,IAAI;AAAA,QAChB;AAEA,eAAO,CAAA;AAAA,MACT,UAAA;AACE,YAAI,eAAe,cAAc,QAAS,YAAW,KAAK;AAAA,MAC5D;AAAA,IACF;AAAA,IACA,CAAC,KAAK;AAAA,EAAA;AAGR,QAAM,iBAAiB,MAAM,aAAa,CAAA,GAAI,IAAI,CAAC,QAAQ,KAAK,UAAU,SAAS,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG;AAElG,YAAU,MAAM;AACd,QAAI,YAAY;AAChB,UAAM,MAAM,YAAY;AACtB,YAAM,SAAS,MAAM,SAAS,GAAG,QAAW,KAAK;AACjD,UAAI,aAAa,OAAO,WAAW,EAAG;AAAA,IACxC;AACA,SAAK,IAAA;AAEL,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,eAAe,QAAQ,CAAC;AAE5B,QAAM,mBAAmB;AAAA,IACvB,OAAO,EAAE,QAAQ,YAAyD;AACxE,YAAM,OAAO,OAAO,KAAA,MAAW,KAAK,SAAY;AAChD,YAAM,SAAS,QAAQ,IAAI,QAAQ,UAAU,GAAG,MAAM,CAAC,KAAK;AAAA,IAC9D;AAAA,IACA,CAAC,QAAQ;AAAA,EAAA;AAGX,QAAM,gBAAgB,MAAM,WAAW,cAAc,KAAK,IAAI,SAAS,KAAK;AAE5E,MAAI,QAAQ;AACV,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,SAAS,EAAE,wBAAwB,EAAE,OAAO,MAAM,OAAO;AAAA,QACzD,QAAQ,EAAE,OAAO,EAAE,kBAAkB,GAAG,SAAS,MAAM,KAAK,SAAS,GAAG,QAAW,KAAK,GAAG,MAAM,UAAA;AAAA,MAAU;AAAA,IAAA;AAAA,EAGjH;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,OAAO,MAAM;AAAA,MACb;AAAA,MACA;AAAA,MACA,UAAU,MAAM;AAAA,MAChB,cAAc,MAAM;AAAA,MACpB,YAAY,MAAM,cAAc;AAAA,MAChC,WAAW,MAAM,aAAa;AAAA,MAC9B;AAAA,MACA;AAAA,MACA,gBAAc;AAAA,MACd,gBAAgB;AAAA,MAChB;AAAA,MACA,cAAc,CAAC,aACb;AAAA,QACE,MAAM,WACF,MAAM,QAAQ,QAAQ,IACpB,WACA,CAAC,QAAQ,IACX,MAAM,QAAQ,QAAQ,IACnB,SAAS,CAAC,KAAK,KAChB;AAAA,MAAA;AAAA,IACR;AAAA,EAAA;AAIR;AAIA,MAAM,eAAe,CAAC,UAAgD;AACpE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,MAAM,OAAO,cAAc,oBAAI,MAAM;AAEpD,SAAO,OAAO,MAAM,OAAO,QAAA,CAAS,IAAI,SAAY;AACtD;AAEA,MAAM,cAA+D,CAAC,EAAE,OAAO,OAAO,UAAU,eAC9F;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,OAAO,MAAM;AAAA,IACb,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf;AAAA,IACA,YAAY,aAAa,SAAS,KAAK,CAAC;AAAA,IACxC,UAAU,CAAC,SAAS,SAAS,QAAQ,EAAE;AAAA,EAAA;AACzC;AAGF,MAAM,mBAAyE,CAAC;AAAA,EAC9E;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,QAAS,SAAS,CAAA;AACxB,QAAM,OAAO,aAAa,MAAM,IAAI;AACpC,QAAM,KAAK,aAAa,MAAM,EAAE;AAEhC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,OAAO,MAAM;AAAA,MACb,SAAS,MAAM;AAAA,MACf,SAAS,MAAM;AAAA,MACf;AAAA,MACA,YAAY,OAAO,EAAE,MAAM,OAAO;AAAA,MAClC,UAAU,CAAC,SAAS,SAAS,EAAE,MAAM,KAAK,MAAM,IAAI,KAAK,GAAA,CAAI;AAAA,IAAA;AAAA,EAAA;AAGnE;AAIA,MAAM,eAAiE,CAAC,EAAE,OAAO,OAAO,UAAU,eAAe;AAC/G,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,QAAS,SAAS,CAAA;AAExB,QAAM,SAAS,CAAC,OAAsB,QAAgB;AACpD,UAAM,SAAS,QAAQ,KAAK,SAAY,OAAO,GAAG;AAClD,aAAS,EAAE,GAAG,OAAO,CAAC,KAAK,GAAG,OAAO,MAAM,MAAgB,IAAI,SAAY,QAAQ;AAAA,EACrF;AAEA,SACE,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAK,WAAU,6CAA6C,UAAA,MAAM,OAAM;AAAA,IACzE,qBAAC,OAAA,EAAI,WAAU,6BACb,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO,EAAE,kBAAkB;AAAA,UAC3B,KAAK,MAAM;AAAA,UACX,KAAK,MAAM;AAAA,UACX;AAAA,UACA,OAAO,MAAM,QAAQ,SAAY,KAAK,OAAO,MAAM,GAAG;AAAA,UACtD,UAAU,CAAC,UAAU,OAAO,OAAO,MAAM,OAAO,KAAK;AAAA,QAAA;AAAA,MAAA;AAAA,0BAEtD,QAAA,EAAK,WAAU,0CAA0C,UAAA,EAAE,wBAAwB,GAAE;AAAA,MACtF;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,OAAO,EAAE,kBAAkB;AAAA,UAC3B,KAAK,MAAM;AAAA,UACX,KAAK,MAAM;AAAA,UACX;AAAA,UACA,OAAO,MAAM,QAAQ,SAAY,KAAK,OAAO,MAAM,GAAG;AAAA,UACtD,UAAU,CAAC,UAAU,OAAO,OAAO,MAAM,OAAO,KAAK;AAAA,QAAA;AAAA,MAAA;AAAA,IACvD,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;AAIA,MAAM,iBAAqE,CAAC,EAAE,OAAO,OAAO,UAAU,eACpG;AAAA,EAAC;AAAA,EAAA;AAAA,IACC,SAAS,UAAU;AAAA,IACnB;AAAA,IACA,OAAO,MAAM,SAAS,MAAM;AAAA,IAC5B,UAAU,CAAC,YAAY,SAAS,OAAO;AAAA,EAAA;AACzC;AAGF,MAAM,uBAAiF,CAAC;AAAA,EACtF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,WAAW,cAAc,KAAK;AACpC,QAAM,UAAU,QAAQ,MAAM;AAC5B,UAAM,OAAO,MAAM,WAAW,CAAA;AAC9B,QAAI,CAAC,MAAM,cAAc,OAAO,OAAO,WAAW,EAAG,QAAO;AAC5D,UAAM,OAAO,OAAO,KAAA,EAAO,YAAA;AAE3B,WAAO,KAAK,OAAO,CAAC,WAAW,OAAO,MAAM,YAAA,EAAc,SAAS,IAAI,CAAC;AAAA,EAC1E,GAAG,CAAC,MAAM,SAAS,MAAM,YAAY,MAAM,CAAC;AAE5C,QAAM,SAAS,CAAC,gBAAwB;AACtC;AAAA,MACE,SAAS,SAAS,WAAW,IAAI,SAAS,OAAO,CAAC,UAAU,UAAU,WAAW,IAAI,CAAC,GAAG,UAAU,WAAW;AAAA,IAAA;AAAA,EAElH;AAEA,SACE,qBAAC,OAAA,EAAI,WAAU,yBACb,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAK,WAAU,6CAA6C,UAAA,MAAM,OAAM;AAAA,IACxE,MAAM,cACL;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,OAAO,EAAE,uBAAuB;AAAA,QAChC,OAAO;AAAA,QACP;AAAA,QACA,UAAU,CAAC,UAAU,UAAU,MAAM,OAAO,KAAK;AAAA,MAAA;AAAA,IAAA;AAAA,IAGrD,oBAAC,OAAA,EAAI,WAAU,kDACZ,UAAA,QAAQ,WAAW,IAClB,oBAAC,aAAA,EAAY,MAAK,UAAS,SAAQ,UAAS,SAAS,EAAE,mBAAmB,EAAA,CAAG,IAE7E,QAAQ,IAAI,CAAC,WACX,qBAAC,OAAA,EAAuB,WAAU,qCAChC,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,SAAS,SAAS,SAAS,OAAO,KAAK;AAAA,UACvC;AAAA,UACA,OAAO,OAAO;AAAA,UACd,UAAU,MAAM,OAAO,OAAO,KAAK;AAAA,QAAA;AAAA,MAAA;AAAA,MAEpC,OAAO,kBACN,oBAAC,UAAK,WAAU,uDAAuD,iBAAO,gBAAe;AAAA,MAE9F,OAAO,UACN;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAU;AAAA,UACV,OAAO,EAAE,iBAAiB,OAAO,OAAA;AAAA,QAAO;AAAA,MAAA;AAAA,IAC1C,KAdM,OAAO,KAgBjB,CACD,EAAA,CAEL;AAAA,EAAA,GACF;AAEJ;AAEA,MAAM,eAAiE,CAAC,EAAE,OAAO,OAAO,UAAU,eAAe;AAC/G,QAAM,WAAW,SAAS,KAAK;AAE/B,SACE,qBAAC,OAAA,EAAI,WAAU,yBACb,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAK,WAAU,6CAA6C,UAAA,MAAM,OAAM;AAAA,IACzE,oBAAC,OAAA,EAAI,WAAU,wBACX,WAAA,MAAM,WAAW,CAAA,GAAI,IAAI,CAAC,WAC1B;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,OAAO,OAAO;AAAA,QACd,mBAAiB;AAAA,QACjB,MAAM,aAAa,OAAO,QAAQ,WAAW;AAAA,QAC7C,SAAS,aAAa,OAAO,QAAQ,UAAU;AAAA,QAC/C,MAAK;AAAA,QACL;AAAA,QACA,SAAS,MAAM,SAAS,aAAa,OAAO,QAAQ,KAAK,OAAO,KAAK;AAAA,MAAA;AAAA,MAPhE,OAAO;AAAA,IAAA,CASf,EAAA,CACH;AAAA,EAAA,GACF;AAEJ;AAEA,MAAM,cAA+D,CAAC,EAAE,OAAO,OAAO,UAAU,eAAe;AAC7G,QAAM,WAAW,cAAc,KAAK;AAEpC,SACE,qBAAC,OAAA,EAAI,WAAU,yBACb,UAAA;AAAA,IAAA,oBAAC,QAAA,EAAK,WAAU,6CAA6C,UAAA,MAAM,OAAM;AAAA,IACzE,oBAAC,OAAA,EAAI,WAAU,wBACX,WAAA,MAAM,WAAW,CAAA,GAAI,IAAI,CAAC,WAAW;AACrC,YAAM,aAAa,SAAS,SAAS,OAAO,KAAK;AAEjD,aACE;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,OAAO,OAAO;AAAA,UACd,mBAAiB;AAAA,UACjB,MAAM,aAAa,WAAW;AAAA,UAC9B,SAAS,aAAa,UAAU;AAAA,UAChC,MAAK;AAAA,UACL;AAAA,UACA,SAAS,MACP,SAAS,aAAa,SAAS,OAAO,CAAC,UAAU,UAAU,OAAO,KAAK,IAAI,CAAC,GAAG,UAAU,OAAO,KAAK,CAAC;AAAA,QAAA;AAAA,QARnG,OAAO;AAAA,MAAA;AAAA,IAYlB,CAAC,EAAA,CACH;AAAA,EAAA,GACF;AAEJ;AAEA,MAAM,gBAAmE,CAAC,EAAE,OAAO,OAAO,UAAU,SAAA,MAClG,oBAAA,UAAA,EAAG,UAAA,MAAM,OAAO,EAAE,OAAO,UAAU,SAAA,CAAU,GAAE;AAOjD,MAAM,0BAAwE;AAAA,EAC5E,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,MAAM;AAAA,EACN,cAAc;AAAA,EACd,OAAO;AAAA,EACP,SAAS;AAAA,EACT,kBAAkB;AAAA,EAClB,OAAO;AAAA,EACP,MAAM;AAAA,EACN,QAAQ;AACV;AAWA,MAAM,qBAAyD,CAAC;AAAA,EAC9D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,MAAI,QAAS,QAAO,oBAAC,iBAAA,EAAgB,WAAU,0BAAyB;AAExE,QAAM,UAAU,wBAAwB,MAAM,IAAI;AAClD,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,UACJ;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,YAAY,MAAM;AAAA,IAAA;AAAA,EAAA;AAIhC,MAAI,CAAC,MAAM,YAAa,QAAO;AAE/B,SACE,qBAAC,OAAA,EAAI,WAAU,yBACZ,UAAA;AAAA,IAAA;AAAA,IACD,oBAAC,QAAA,EAAK,WAAU,sCAAsC,gBAAM,YAAA,CAAY;AAAA,EAAA,GAC1E;AAEJ;"}
|
|
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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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { SymbolCodepoints } from '../MaterialSymbol';
|
|
3
|
+
import { Node, NodeProps } from '@xyflow/react';
|
|
4
|
+
import { TFlowIssueSeverity, TFlowPortId, TFlowRunStepStatus } from './flowTypes';
|
|
5
|
+
export declare const FLOW_INPUT_HANDLE_ID = "in";
|
|
6
|
+
export interface IFlowAddableBlock {
|
|
7
|
+
type: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon: SymbolCodepoints;
|
|
10
|
+
}
|
|
11
|
+
export interface IFlowBlockNodeData extends Record<string, unknown> {
|
|
12
|
+
icon: SymbolCodepoints;
|
|
13
|
+
title: string;
|
|
14
|
+
categoryLabel: string;
|
|
15
|
+
summary?: ReactNode;
|
|
16
|
+
hasInput: boolean;
|
|
17
|
+
outputs: Array<TFlowPortId>;
|
|
18
|
+
/** Ports already wired: their edge carries the label, so the port does not repeat it. */
|
|
19
|
+
connectedPorts: Array<TFlowPortId>;
|
|
20
|
+
portLabels: Record<TFlowPortId, string>;
|
|
21
|
+
/** What a free port can grow into; empty in a read-only canvas. */
|
|
22
|
+
addableBlocks: Array<IFlowAddableBlock>;
|
|
23
|
+
onAddFromPort?: (port: TFlowPortId, type: string) => void;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
issueSeverity?: TFlowIssueSeverity;
|
|
26
|
+
issueCount: number;
|
|
27
|
+
runStatus?: TFlowRunStepStatus;
|
|
28
|
+
}
|
|
29
|
+
export type TFlowBlockNode = Node<IFlowBlockNodeData, 'flowBlock'>;
|
|
30
|
+
/**
|
|
31
|
+
* Input on the left edge, output ports spread along the right edge; a lone `next` port needs no
|
|
32
|
+
* label. A free port shows a "+" that adds the next block already wired to it.
|
|
33
|
+
*/
|
|
34
|
+
declare const FlowBlockNode: React.FC<NodeProps<TFlowBlockNode>>;
|
|
35
|
+
export default FlowBlockNode;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import ControlledButton from "../../Form/ControlledButton.js";
|
|
4
|
+
import ControlledMenu from "../ControlledMenu.js";
|
|
5
|
+
import { MaterialSymbol } from "../MaterialSymbol/index.js";
|
|
6
|
+
import { Handle, Position } from "@xyflow/react";
|
|
7
|
+
import FlowNodeShell from "./FlowNodeShell.js";
|
|
8
|
+
const FLOW_INPUT_HANDLE_ID = "in";
|
|
9
|
+
const portTop = (index, count) => `${(index + 1) / (count + 1) * 100}%`;
|
|
10
|
+
const FlowBlockNode = ({ data, selected }) => {
|
|
11
|
+
const { t } = useTranslation("ui");
|
|
12
|
+
const outputs = Array.isArray(data.outputs) ? data.outputs : [];
|
|
13
|
+
const connectedPorts = Array.isArray(data.connectedPorts) ? data.connectedPorts : [];
|
|
14
|
+
const addableBlocks = Array.isArray(data.addableBlocks) ? data.addableBlocks : [];
|
|
15
|
+
const showPortLabels = !(outputs.length === 1 && outputs[0] === "next");
|
|
16
|
+
const canAdd = Boolean(data.onAddFromPort) && addableBlocks.length > 0;
|
|
17
|
+
return /* @__PURE__ */ jsxs(
|
|
18
|
+
FlowNodeShell,
|
|
19
|
+
{
|
|
20
|
+
icon: data.icon,
|
|
21
|
+
title: data.title,
|
|
22
|
+
categoryLabel: data.categoryLabel,
|
|
23
|
+
summary: data.summary,
|
|
24
|
+
selected,
|
|
25
|
+
disabled: data.disabled,
|
|
26
|
+
issueSeverity: data.issueSeverity,
|
|
27
|
+
issueCount: data.issueCount,
|
|
28
|
+
runStatus: data.runStatus,
|
|
29
|
+
className: showPortLabels ? "pr-12" : void 0,
|
|
30
|
+
children: [
|
|
31
|
+
data.hasInput && /* @__PURE__ */ jsx(Handle, { id: FLOW_INPUT_HANDLE_ID, type: "target", position: Position.Left, className: "umode-flow-handle" }),
|
|
32
|
+
outputs.map((port, index) => /* @__PURE__ */ jsx(
|
|
33
|
+
Handle,
|
|
34
|
+
{
|
|
35
|
+
id: port,
|
|
36
|
+
type: "source",
|
|
37
|
+
position: Position.Right,
|
|
38
|
+
className: "umode-flow-handle",
|
|
39
|
+
"data-port": port,
|
|
40
|
+
style: { top: portTop(index, outputs.length) },
|
|
41
|
+
children: showPortLabels && !connectedPorts.includes(port) && /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 whitespace-nowrap text-xxs font-semibold text-foreground-subtle", children: data.portLabels[port] })
|
|
42
|
+
},
|
|
43
|
+
port
|
|
44
|
+
)),
|
|
45
|
+
canAdd && outputs.map(
|
|
46
|
+
(port, index) => connectedPorts.includes(port) ? null : /* @__PURE__ */ jsx(
|
|
47
|
+
"div",
|
|
48
|
+
{
|
|
49
|
+
className: "nodrag nopan absolute -right-10 -translate-y-1/2",
|
|
50
|
+
style: { top: portTop(index, outputs.length) },
|
|
51
|
+
children: /* @__PURE__ */ jsx(
|
|
52
|
+
ControlledMenu,
|
|
53
|
+
{
|
|
54
|
+
placement: "right-start",
|
|
55
|
+
triggerTooltip: t("flow.node.addBlock"),
|
|
56
|
+
items: addableBlocks.map((block) => ({
|
|
57
|
+
key: block.type,
|
|
58
|
+
label: block.label,
|
|
59
|
+
icon: block.icon,
|
|
60
|
+
onClick: () => data.onAddFromPort?.(port, block.type)
|
|
61
|
+
})),
|
|
62
|
+
trigger: /* @__PURE__ */ jsx(
|
|
63
|
+
ControlledButton,
|
|
64
|
+
{
|
|
65
|
+
defaultHtmlButton: true,
|
|
66
|
+
className: "interactive flex size-6 items-center justify-center rounded-full border border-border-emphasis bg-surface text-foreground-muted shadow-sm hover:border-brand-border hover:bg-brand-soft hover:text-brand-emphasis focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
67
|
+
children: /* @__PURE__ */ jsx(MaterialSymbol, { icon: "add", size: 16, "aria-hidden": true })
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
},
|
|
73
|
+
`add|${port}`
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
export {
|
|
81
|
+
FLOW_INPUT_HANDLE_ID,
|
|
82
|
+
FlowBlockNode as default
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=FlowBlockNode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlowBlockNode.js","sources":["../../../../src/components/UI/Flow/FlowBlockNode.tsx"],"sourcesContent":["import { ReactNode } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledMenu from '@/components/UI/ControlledMenu';\nimport { MaterialSymbol, type SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { Handle, type Node, type NodeProps, Position } from '@xyflow/react';\n\nimport FlowNodeShell from './FlowNodeShell';\nimport type { TFlowIssueSeverity, TFlowPortId, TFlowRunStepStatus } from './flowTypes';\n\nexport const FLOW_INPUT_HANDLE_ID = 'in';\n\nexport interface IFlowAddableBlock {\n type: string;\n label: string;\n icon: SymbolCodepoints;\n}\n\nexport interface IFlowBlockNodeData extends Record<string, unknown> {\n icon: SymbolCodepoints;\n title: string;\n categoryLabel: string;\n summary?: ReactNode;\n hasInput: boolean;\n outputs: Array<TFlowPortId>;\n /** Ports already wired: their edge carries the label, so the port does not repeat it. */\n connectedPorts: Array<TFlowPortId>;\n portLabels: Record<TFlowPortId, string>;\n /** What a free port can grow into; empty in a read-only canvas. */\n addableBlocks: Array<IFlowAddableBlock>;\n onAddFromPort?: (port: TFlowPortId, type: string) => void;\n disabled?: boolean;\n issueSeverity?: TFlowIssueSeverity;\n issueCount: number;\n runStatus?: TFlowRunStepStatus;\n}\n\nexport type TFlowBlockNode = Node<IFlowBlockNodeData, 'flowBlock'>;\n\nconst portTop = (index: number, count: number): string => `${((index + 1) / (count + 1)) * 100}%`;\n\n/**\n * Input on the left edge, output ports spread along the right edge; a lone `next` port needs no\n * label. A free port shows a \"+\" that adds the next block already wired to it.\n */\nconst FlowBlockNode: React.FC<NodeProps<TFlowBlockNode>> = ({ data, selected }) => {\n const { t } = useTranslation('ui');\n const outputs = Array.isArray(data.outputs) ? data.outputs : [];\n const connectedPorts = Array.isArray(data.connectedPorts) ? data.connectedPorts : [];\n const addableBlocks = Array.isArray(data.addableBlocks) ? data.addableBlocks : [];\n const showPortLabels = !(outputs.length === 1 && outputs[0] === 'next');\n const canAdd = Boolean(data.onAddFromPort) && addableBlocks.length > 0;\n\n return (\n <FlowNodeShell\n icon={data.icon}\n title={data.title}\n categoryLabel={data.categoryLabel}\n summary={data.summary}\n selected={selected}\n disabled={data.disabled}\n issueSeverity={data.issueSeverity}\n issueCount={data.issueCount}\n runStatus={data.runStatus}\n className={showPortLabels ? 'pr-12' : undefined}\n >\n {data.hasInput && (\n <Handle id={FLOW_INPUT_HANDLE_ID} type=\"target\" position={Position.Left} className=\"umode-flow-handle\" />\n )}\n {outputs.map((port, index) => (\n <Handle\n key={port}\n id={port}\n type=\"source\"\n position={Position.Right}\n className=\"umode-flow-handle\"\n data-port={port}\n style={{ top: portTop(index, outputs.length) }}\n >\n {showPortLabels && !connectedPorts.includes(port) && (\n <span className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 whitespace-nowrap text-xxs font-semibold text-foreground-subtle\">\n {data.portLabels[port]}\n </span>\n )}\n </Handle>\n ))}\n {canAdd &&\n outputs.map((port, index) =>\n connectedPorts.includes(port) ? null : (\n <div\n key={`add|${port}`}\n className=\"nodrag nopan absolute -right-10 -translate-y-1/2\"\n style={{ top: portTop(index, outputs.length) }}\n >\n <ControlledMenu\n placement=\"right-start\"\n triggerTooltip={t('flow.node.addBlock')}\n items={addableBlocks.map((block) => ({\n key: block.type,\n label: block.label,\n icon: block.icon,\n onClick: () => data.onAddFromPort?.(port, block.type),\n }))}\n trigger={\n <ControlledButton\n defaultHtmlButton\n className=\"interactive flex size-6 items-center justify-center rounded-full border border-border-emphasis bg-surface text-foreground-muted shadow-sm hover:border-brand-border hover:bg-brand-soft hover:text-brand-emphasis focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MaterialSymbol icon=\"add\" size={16} aria-hidden />\n </ControlledButton>\n }\n />\n </div>\n ),\n )}\n </FlowNodeShell>\n );\n};\n\nexport default FlowBlockNode;\n"],"names":[],"mappings":";;;;;;;AAWO,MAAM,uBAAuB;AA6BpC,MAAM,UAAU,CAAC,OAAe,UAA0B,IAAK,QAAQ,MAAM,QAAQ,KAAM,GAAG;AAM9F,MAAM,gBAAqD,CAAC,EAAE,MAAM,eAAe;AACjF,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,UAAU,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,UAAU,CAAA;AAC7D,QAAM,iBAAiB,MAAM,QAAQ,KAAK,cAAc,IAAI,KAAK,iBAAiB,CAAA;AAClF,QAAM,gBAAgB,MAAM,QAAQ,KAAK,aAAa,IAAI,KAAK,gBAAgB,CAAA;AAC/E,QAAM,iBAAiB,EAAE,QAAQ,WAAW,KAAK,QAAQ,CAAC,MAAM;AAChE,QAAM,SAAS,QAAQ,KAAK,aAAa,KAAK,cAAc,SAAS;AAErE,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAM,KAAK;AAAA,MACX,OAAO,KAAK;AAAA,MACZ,eAAe,KAAK;AAAA,MACpB,SAAS,KAAK;AAAA,MACd;AAAA,MACA,UAAU,KAAK;AAAA,MACf,eAAe,KAAK;AAAA,MACpB,YAAY,KAAK;AAAA,MACjB,WAAW,KAAK;AAAA,MAChB,WAAW,iBAAiB,UAAU;AAAA,MAErC,UAAA;AAAA,QAAA,KAAK,YACJ,oBAAC,QAAA,EAAO,IAAI,sBAAsB,MAAK,UAAS,UAAU,SAAS,MAAM,WAAU,oBAAA,CAAoB;AAAA,QAExG,QAAQ,IAAI,CAAC,MAAM,UAClB;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,IAAI;AAAA,YACJ,MAAK;AAAA,YACL,UAAU,SAAS;AAAA,YACnB,WAAU;AAAA,YACV,aAAW;AAAA,YACX,OAAO,EAAE,KAAK,QAAQ,OAAO,QAAQ,MAAM,EAAA;AAAA,YAE1C,UAAA,kBAAkB,CAAC,eAAe,SAAS,IAAI,KAC9C,oBAAC,QAAA,EAAK,WAAU,iIACb,UAAA,KAAK,WAAW,IAAI,EAAA,CACvB;AAAA,UAAA;AAAA,UAXG;AAAA,QAAA,CAcR;AAAA,QACA,UACC,QAAQ;AAAA,UAAI,CAAC,MAAM,UACjB,eAAe,SAAS,IAAI,IAAI,OAC9B;AAAA,YAAC;AAAA,YAAA;AAAA,cAEC,WAAU;AAAA,cACV,OAAO,EAAE,KAAK,QAAQ,OAAO,QAAQ,MAAM,EAAA;AAAA,cAE3C,UAAA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,WAAU;AAAA,kBACV,gBAAgB,EAAE,oBAAoB;AAAA,kBACtC,OAAO,cAAc,IAAI,CAAC,WAAW;AAAA,oBACnC,KAAK,MAAM;AAAA,oBACX,OAAO,MAAM;AAAA,oBACb,MAAM,MAAM;AAAA,oBACZ,SAAS,MAAM,KAAK,gBAAgB,MAAM,MAAM,IAAI;AAAA,kBAAA,EACpD;AAAA,kBACF,SACE;AAAA,oBAAC;AAAA,oBAAA;AAAA,sBACC,mBAAiB;AAAA,sBACjB,WAAU;AAAA,sBAEV,8BAAC,gBAAA,EAAe,MAAK,OAAM,MAAM,IAAI,eAAW,KAAA,CAAC;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBACnD;AAAA,cAAA;AAAA,YAEJ;AAAA,YArBK,OAAO,IAAI;AAAA,UAAA;AAAA,QAsBlB;AAAA,MAEJ;AAAA,IAAA;AAAA,EAAA;AAGR;"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { PanelPosition } from '@xyflow/react';
|
|
3
|
+
import { IFlowIssue, IFlowRunOverlay } from './flowTypes';
|
|
4
|
+
export interface IFlowCanvasProps {
|
|
5
|
+
issues?: Array<IFlowIssue>;
|
|
6
|
+
runOverlay?: IFlowRunOverlay | null;
|
|
7
|
+
readOnly?: boolean;
|
|
8
|
+
/** Accessible name of the canvas region, from `t()`. Defaults to the library label. */
|
|
9
|
+
ariaLabel?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
/** Where the zoom and history controls sit; move them when floating chrome takes that corner. */
|
|
12
|
+
controlsPosition?: PanelPosition;
|
|
13
|
+
/** Floating chrome drawn over the canvas (toolbars, panels); position it with absolute classes. */
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The editing surface. Renders the graph held by `FlowEditorProvider` and turns every
|
|
18
|
+
* gesture (drag, connect, select, delete, drop, shortcuts) into a store action, so undo
|
|
19
|
+
* and the dirty flag see all of them.
|
|
20
|
+
*/
|
|
21
|
+
declare const FlowCanvas: React.FC<IFlowCanvasProps>;
|
|
22
|
+
export default FlowCanvas;
|