diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index 4c1ba249..33216362 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -739,6 +739,7 @@ body { .simplebar-scrollbar:before { background: #78787854 !important; + transition: opacity 0.3s ease; } .printer-alerts-display-popover .ant-popover-inner { @@ -1612,23 +1613,23 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { top: 0; right: -11px; bottom: 0; - width: 11px; + width: 6px; z-index: 3; cursor: col-resize; touch-action: none; user-select: none; pointer-events: auto; + opacity: 0.6; } .objectKanbanColumnResizeHandle::before { content: ''; position: absolute; - top: 11px; - bottom: 11px; + top: 11.15px; + bottom: 10.85px; right: 1px; width: 2px; transform: translateX(-50%); - opacity: 0.6; transition: opacity 0.15s ease; } @@ -1640,16 +1641,9 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { width: 2px; height: 20px; transform: translate(-50%, -50%); - opacity: 0.6; transition: opacity 0.15s ease; } -.objectKanbanColumnResizeHandle.active::before, -.objectKanbanColumnResizeHandle.active::after { - background: #0e3a5b; - opacity: 1; -} - .dark-mode .objectKanbanColumnResizeHandle::after { background: rgba(255, 255, 255, 0.18); } @@ -1658,16 +1652,30 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { background: rgba(255, 255, 255, 0.08); } -.dark-mode .objectKanbanColumnResizeHandle.active::before, -.dark-mode .objectKanbanColumnResizeHandle.active::after { - background: #0b4c7e; - opacity: 1; +.light-mode .objectKanbanColumnResizeHandle::after { + background: rgba(0, 0, 0, 0.15); } -.objectKanbanColumnResizeHandle:hover::before { +.light-mode .objectKanbanColumnResizeHandle::before { + background: rgba(0, 0, 0, 0.04); +} + +.dark-mode .objectKanbanColumnResizeHandle:hover::before { background: #0e3a5b; } +.light-mode .objectKanbanColumnResizeHandle:hover::before { + background: #e6f8ff; +} + +.dark-mode .objectKanbanColumnResizeHandle.active::before { + background: #0b4c7e; +} + +.light-mode .objectKanbanColumnResizeHandle.active::before { + background: #a3e3ff; +} + body.objectKanbanColumnResizing, body.objectKanbanColumnResizing * { cursor: col-resize !important; @@ -2313,6 +2321,15 @@ body.objectKanbanColumnResizing * { content: none; } +.farmcontrol-splitter .ant-splitter-bar-dragger { + transition: opacity 0.3s ease; + opacity: 1; +} + +.farmcontrol-splitter.vertical-scroll-visible .ant-splitter-bar-dragger { + opacity: 0; +} + .object-display-tag .elipsis-text-wrapper.ant-typography code.elipsis-text-measure, diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx index 47dc2eee..30f8d537 100644 --- a/src/components/Dashboard/common/ObjectTable.jsx +++ b/src/components/Dashboard/common/ObjectTable.jsx @@ -69,6 +69,9 @@ import { import { areValuesEqual } from '../utils/Utils' import MissingPlaceholder from './MissingPlaceholder' import LoadingPlaceholder from './LoadingPlaceholder' +import useSidebarWidth from '../hooks/useSidebarWidth' + +import classNames from 'classnames' const logger = loglevel.getLogger('DasboardTable') logger.setLevel(config.logLevel) @@ -412,6 +415,10 @@ const ObjectTable = forwardRef( const [sidebarFilter, setSidebarFilter] = useState({}) const [tableSorter, setTableSorter] = useState({}) const [initialized, setInitialized] = useState(false) + const [sidebarWidth, setSidebarWidth] = useSidebarWidth(type) + + const [verticalScrollVisible, setVerticalScrollVisible] = useState(false) + const tableContentRef = useRef(null) // Keep refs in sync with props. applyViewState may update them earlier in // the same tick; props remain the source of truth after commit. @@ -1929,7 +1936,11 @@ const ObjectTable = forwardRef( const cardsContainerNodeRef = useRef(null) const cardsResizeObserverRef = useRef(null) - const measureCardColSpan = useCallback(() => { + const measureCardColSpan = useCallback((sizes) => { + if (sizes?.[1]) { + setSidebarWidth(sizes[1]) + } + const node = cardsContainerNodeRef.current if (!node) return const panel = node.closest('.ant-splitter-panel') @@ -2107,17 +2118,53 @@ const ObjectTable = forwardRef( ) useEffect(() => { - console.log('loading', loading) - }, [loading]) + const container = tableContentRef.current + + if (!container) return + + const updateVisibility = () => { + const scrollableY = container.querySelector('.simplebar-scrollable-y') + + const scrollableX = container.querySelector('.simplebar-scrollable-x') + + const scrollbar = container.querySelector( + '.simplebar-vertical .simplebar-scrollbar' + ) + + setVerticalScrollVisible( + scrollbar?.classList.contains('simplebar-visible') || + scrollableY?.classList.contains('simplebar-mouse-entered') || + scrollableX?.classList.contains('simplebar-mouse-entered') + ) + } + + // Initial state + updateVisibility() + + const observer = new MutationObserver(updateVisibility) + + observer.observe(container, { + subtree: true, + attributes: true, + attributeFilter: ['class'] + }) + + return () => { + observer.disconnect() + } + }, []) const tableContent = ( @@ -2214,8 +2261,8 @@ const ObjectTable = forwardRef( )} - {(showSortSidebar || showFilterSidebar) && ( - + {(showSortSidebar || showFilterSidebar) && initialized && ( + { + const { userSettings, updateUserSettings } = useContext(ApiServerContext) + + const savedSidebarWidth = + userSettings?.sidebarWidths?.[componentName] ?? defaultValue + + const [localSidebarWidth, setLocalSidebarWidth] = useState(savedSidebarWidth) + + const pendingValueRef = useRef(null) + const lastUpdateRef = useRef(0) + const timeoutRef = useRef(null) + + /* + * Keep the latest updateUserSettings arguments in refs so the + * cleanup function always has the correct component name/value. + */ + const componentNameRef = useRef(componentName) + const updateUserSettingsRef = useRef(updateUserSettings) + + componentNameRef.current = componentName + updateUserSettingsRef.current = updateUserSettings + + const callUpdateUserSettings = useCallback((name, value) => { + lastUpdateRef.current = Date.now() + + updateUserSettingsRef.current('sidebarWidths', name, value) + }, []) + + const flush = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + + const pending = pendingValueRef.current + + if (!pending) return + + pendingValueRef.current = null + + callUpdateUserSettings(pending.componentName, pending.value) + }, [callUpdateUserSettings]) + + /* + * Flush the previous component's value when the component is + * unmounted OR componentName changes. + */ + useEffect(() => { + const name = componentName + + return () => { + const pending = pendingValueRef.current + + if (pending && pending.componentName === name) { + flush() + } + } + }, [componentName, flush]) + + /* + * Load the saved width when the component name changes. + */ + useEffect(() => { + setLocalSidebarWidth(savedSidebarWidth) + }, [componentName, savedSidebarWidth]) + + const setSidebarWidth = useCallback( + (nextValue) => { + const value = + typeof nextValue === 'function' + ? nextValue(localSidebarWidth) + : nextValue + + if (value === localSidebarWidth) return + + // Always update the UI immediately. + setLocalSidebarWidth(value) + + // Keep only the latest value waiting to be persisted. + pendingValueRef.current = { + componentName, + value + } + + const elapsed = Date.now() - lastUpdateRef.current + + /* + * We haven't called updateUserSettings recently, so call it + * immediately. + */ + if (elapsed >= 1000) { + const pending = pendingValueRef.current + pendingValueRef.current = null + + callUpdateUserSettings(pending.componentName, pending.value) + + return + } + + /* + * Already rate limited. Schedule the next call for exactly + * when the 1 second window expires. + */ + if (!timeoutRef.current) { + timeoutRef.current = setTimeout(() => { + timeoutRef.current = null + + const pending = pendingValueRef.current + + if (!pending) return + + pendingValueRef.current = null + + callUpdateUserSettings(pending.componentName, pending.value) + }, 1000 - elapsed) + } + }, + [componentName, localSidebarWidth, callUpdateUserSettings] + ) + + return [localSidebarWidth, setSidebarWidth] +} + +export default useSidebarWidth