From 42c6c358c6838be0eba19ed24c8f78a17b473188 Mon Sep 17 00:00:00 2001 From: nikec <43032218+niikeec@users.noreply.github.com> Date: Fri, 15 Sep 2023 13:03:28 +0200 Subject: [PATCH] [ENG-1113] Toggleable list columns (#1352) * Toggleable list columns * type --- core/src/preferences/library.rs | 1 + .../app/$libraryId/Explorer/View/ListView.tsx | 262 +++++++++++------- interface/app/$libraryId/Explorer/store.ts | 17 +- packages/client/src/core.ts | 2 +- packages/ui/src/ContextMenu.tsx | 69 ++++- packages/ui/src/DropdownMenu.tsx | 5 +- 6 files changed, 230 insertions(+), 126 deletions(-) diff --git a/core/src/preferences/library.rs b/core/src/preferences/library.rs index 571f36eac..781cfc473 100644 --- a/core/src/preferences/library.rs +++ b/core/src/preferences/library.rs @@ -64,6 +64,7 @@ pub struct ExplorerSettings { media_aspect_square: Option, open_on_double_click: Option, show_bytes_in_grid_view: Option, + col_visibility: Option>, col_sizes: Option>, // temporary #[serde(skip_serializing_if = "Option::is_none")] diff --git a/interface/app/$libraryId/Explorer/View/ListView.tsx b/interface/app/$libraryId/Explorer/View/ListView.tsx index 365d58c94..1d447b3b6 100644 --- a/interface/app/$libraryId/Explorer/View/ListView.tsx +++ b/interface/app/$libraryId/Explorer/View/ListView.tsx @@ -1,8 +1,10 @@ import { CaretDown, CaretUp } from '@phosphor-icons/react'; import { flexRender, + functionalUpdate, getCoreRowModel, useReactTable, + VisibilityState, type ColumnDef, type ColumnSizingState, type Row @@ -24,7 +26,7 @@ import { type FilePath, type NonIndexedPathItem } from '@sd/client'; -import { Tooltip } from '@sd/ui'; +import { ContextMenu, Tooltip } from '@sd/ui'; import { useIsTextTruncated, useScrolled } from '~/hooks'; import { stringify } from '~/util/uuid'; @@ -48,6 +50,7 @@ import RenamableItemText from './RenamableItemText'; interface ListViewItemProps { row: Row; columnSizing: ColumnSizingState; + columnVisibility?: VisibilityState; paddingX: number; selected: boolean; cut: boolean; @@ -112,6 +115,7 @@ export default () => { const [locked, setLocked] = useState(false); const [resizing, setResizing] = useState(false); const [columnSizing, setColumnSizing] = useState({}); + const [columnVisibility, setColumnVisibility] = useState(); const [listOffset, setListOffset] = useState(0); const [ranges, setRanges] = useState([]); @@ -281,7 +285,10 @@ export default () => { data: useMemo(() => explorer.items ?? [], [explorer.items]), columns, defaultColumn: { minSize: 100, maxSize: 250 }, - state: { columnSizing }, + state: { columnSizing, columnVisibility }, + onColumnVisibilityChange: (updater) => { + setColumnVisibility(functionalUpdate(updater, columnVisibility ?? {})); + }, onColumnSizingChange: setColumnSizing, columnResizeMode: 'onChange', getCoreRowModel: useMemo(() => getCoreRowModel(), []), @@ -792,6 +799,19 @@ export default () => { if (lastRow.index >= loadMoreFromRow - 1) explorer.loadMore.call(undefined); }, [virtualRows, rows.length, explorer.loadMore]); + // Initialize column visibility from explorer settings + useEffect(() => { + !columnVisibility && setColumnVisibility(explorer.settingsStore.colVisibility); + }, [columnVisibility, explorer.settingsStore.colVisibility]); + + // Update column visibility in explorer settings + // We don't update directly because it takes too long to get the updated values + useEffect(() => { + if (!columnVisibility) return; + explorer.settingsStore.colVisibility = + columnVisibility as typeof explorer.settingsStore.colVisibility; + }, [columnVisibility, explorer]); + useKey(['ArrowUp', 'ArrowDown', 'Escape'], (e) => { if (!explorerView.selectable) return; @@ -1014,116 +1034,149 @@ export default () => { width: isScrolled ? tableWidth : undefined }} > -
- {table.getHeaderGroups().map((headerGroup) => ( -
e.stopPropagation()} - > - {headerGroup.headers.map((header, i) => { - const size = header.column.getSize(); + + {table.getHeaderGroups().map((headerGroup) => ( +
e.stopPropagation()} + > + {headerGroup.headers.map((header, i) => { + const size = header.column.getSize(); - const orderingDirection = - settings.order && - orderingKey(settings.order) === header.id - ? getOrderingDirection(settings.order) - : null; + const orderingDirection = + settings.order && + orderingKey(settings.order) === + header.id + ? getOrderingDirection( + settings.order + ) + : null; - const cellContent = flexRender( - header.column.columnDef.header, - header.getContext() - ); + const cellContent = flexRender( + header.column.columnDef.header, + header.getContext() + ); - return ( -
{ - if (resizing) return; - - if (header.column.getCanSort()) { - if (orderingDirection) { - explorer.settingsStore.order = - createOrdering( - header.id, - orderingDirection === - 'Asc' - ? 'Desc' - : 'Asc' - ); - } else { - explorer.settingsStore.order = - createOrdering( - header.id, - 'Asc' - ); - } - } - }} - > - {header.isPlaceholder ? null : ( + return (
- {typeof cellContent === - 'string' && ( - - )} + key={header.id} + className="relative shrink-0 px-4 py-2 text-xs first:pl-24" + style={{ + width: + i === 0 + ? size + paddingX + : i === + headerGroup.headers + .length - + 1 + ? size + + paddingX + + scrollBarWidth + : size + }} + onClick={() => { + if (resizing) return; - {orderingDirection === 'Asc' && ( - - )} - {orderingDirection === 'Desc' && ( - - )} - -
- e.stopPropagation() - } - onMouseDown={(e) => { - header.getResizeHandler()( - e - ); - setResizing(true); - setLocked(false); - - if (layout?.ref.current) { - layout.ref.current.style.cursor = - 'col-resize'; + if ( + header.column.getCanSort() + ) { + if (orderingDirection) { + explorer.settingsStore.order = + createOrdering( + header.id, + orderingDirection === + 'Asc' + ? 'Desc' + : 'Asc' + ); + } else { + explorer.settingsStore.order = + createOrdering( + header.id, + 'Asc' + ); } - }} - onTouchStart={header.getResizeHandler()} - className="absolute right-0 h-[70%] w-2 cursor-col-resize border-r border-app-line/50" - /> + } + }} + > + {header.isPlaceholder ? null : ( +
+ {typeof cellContent === + 'string' && ( + + )} + + {orderingDirection === + 'Asc' && ( + + )} + {orderingDirection === + 'Desc' && ( + + )} + +
+ e.stopPropagation() + } + onMouseDown={(e) => { + header.getResizeHandler()( + e + ); + setResizing(true); + setLocked(false); + + if ( + layout?.ref + .current + ) { + layout.ref.current.style.cursor = + 'col-resize'; + } + }} + onTouchStart={header.getResizeHandler()} + className="absolute right-0 h-[70%] w-2 cursor-col-resize border-r border-app-line/50" + /> +
+ )}
- )} -
- ); - })} + ); + })} +
+ ))}
- ))} -
+ } + > + {table.getAllLeafColumns().map((column) => { + if (column.id === 'name') return null; + return ( + + ); + })} +
@@ -1196,6 +1249,7 @@ export default () => { row={row} paddingX={paddingX} columnSizing={columnSizing} + columnVisibility={columnVisibility} selected={selected} cut={cut} /> diff --git a/interface/app/$libraryId/Explorer/store.ts b/interface/app/$libraryId/Explorer/store.ts index 93cf72729..6db25745f 100644 --- a/interface/app/$libraryId/Explorer/store.ts +++ b/interface/app/$libraryId/Explorer/store.ts @@ -77,14 +77,25 @@ export const createDefaultExplorerSettings = (args?: { mediaColumns: 8 as number, mediaAspectSquare: false as boolean, openOnDoubleClick: 'openFile' as DoubleClickAction, + colVisibility: { + name: true, + kind: true, + sizeInBytes: true, + dateCreated: true, + dateModified: true, + dateAccessed: false, + dateIndexed: false, + contentId: false, + objectId: false + }, colSizes: { - kind: 150, name: 350, + kind: 150, sizeInBytes: 100, - dateModified: 150, - dateIndexed: 150, dateCreated: 150, + dateModified: 150, dateAccessed: 150, + dateIndexed: 150, contentId: 180, objectId: 180 } diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index b939e1804..9824200be 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -154,7 +154,7 @@ export type ExplorerItem = { type: "Path"; has_local_thumbnail: boolean; thumbna export type ExplorerLayout = "grid" | "list" | "media" -export type ExplorerSettings = { layoutMode: ExplorerLayout | null; gridItemSize: number | null; mediaColumns: number | null; mediaAspectSquare: boolean | null; openOnDoubleClick: DoubleClickAction | null; showBytesInGridView: boolean | null; colSizes: { [key: string]: number } | null; order?: TOrder | null; showHiddenFiles?: boolean } +export type ExplorerSettings = { layoutMode: ExplorerLayout | null; gridItemSize: number | null; mediaColumns: number | null; mediaAspectSquare: boolean | null; openOnDoubleClick: DoubleClickAction | null; showBytesInGridView: boolean | null; colVisibility: { [key: string]: boolean } | null; colSizes: { [key: string]: number } | null; order?: TOrder | null; showHiddenFiles?: boolean } export type FileCopierJobInit = { source_location_id: number; target_location_id: number; sources_file_path_ids: number[]; target_location_relative_directory_path: string; target_file_name_suffix: string | null } diff --git a/packages/ui/src/ContextMenu.tsx b/packages/ui/src/ContextMenu.tsx index 87cae989a..d8d3ffa3e 100644 --- a/packages/ui/src/ContextMenu.tsx +++ b/packages/ui/src/ContextMenu.tsx @@ -1,7 +1,7 @@ +import { CaretRight, Check, Icon, IconProps } from '@phosphor-icons/react'; import * as RadixCM from '@radix-ui/react-context-menu'; import { cva, VariantProps } from 'class-variance-authority'; import clsx from 'clsx'; -import { CaretRight, Icon, IconProps } from '@phosphor-icons/react'; import { ContextType, createContext, PropsWithChildren, Suspense, useContext } from 'react'; interface ContextMenuProps extends RadixCM.MenuContentProps { @@ -109,52 +109,90 @@ const contextMenuItemStyles = cva( } ); -export interface ContextMenuItemProps extends VariantProps { - icon?: Icon; - iconProps?: IconProps; - rightArrow?: boolean; - label?: string; - keybind?: string; -} +export interface ContextMenuItemProps + extends RadixCM.MenuItemProps, + VariantProps, + Pick {} export const contextMenuItemClassNames = 'group py-0.5 outline-none px-1'; const Item = ({ icon, label, - rightArrow, children, keybind, variant, iconProps, onClick, ...props -}: ContextMenuItemProps & RadixCM.MenuItemProps) => { +}: ContextMenuItemProps) => { return ( !props.disabled && onClick?.(e)} {...props} > - + ); }; +export interface ContextMenuCheckboxItemProps + extends RadixCM.MenuCheckboxItemProps, + VariantProps, + Pick {} + +const CheckboxItem = ({ + variant, + className, + label, + keybind, + children, + ...props +}: ContextMenuCheckboxItemProps) => { + return ( + + + + + + + + + + + + ); +}; + +interface ContextMenuInnerItemProps { + icon?: Icon; + iconProps?: IconProps; + label?: string; + keybind?: string; + rightArrow?: boolean; +} + export const ContextMenuDivItem = ({ variant, children, className, ...props -}: PropsWithChildren) => ( +}: ContextMenuInnerItemProps & + VariantProps & + PropsWithChildren<{ className?: string }>) => (
{children || }
); -const ItemInternals = ({ icon, label, rightArrow, keybind, iconProps }: ContextMenuItemProps) => { +const ItemInternals = ({ + icon, + label, + rightArrow, + keybind, + iconProps +}: ContextMenuInnerItemProps) => { const ItemIcon = icon; return ( @@ -181,6 +219,7 @@ const ItemInternals = ({ icon, label, rightArrow, keybind, iconProps }: ContextM export const ContextMenu = { Root, Item, + CheckboxItem, Separator, SubMenu }; diff --git a/packages/ui/src/DropdownMenu.tsx b/packages/ui/src/DropdownMenu.tsx index cb1151d6f..ab90d8ce8 100644 --- a/packages/ui/src/DropdownMenu.tsx +++ b/packages/ui/src/DropdownMenu.tsx @@ -118,7 +118,6 @@ const Item = ({ icon, iconProps, label, - rightArrow, children, keybind, variant, @@ -135,13 +134,13 @@ const Item = ({ ref.current?.click()}> ) : ( )}