[ENG-1113] Toggleable list columns (#1352)

* Toggleable list columns

* type
This commit is contained in:
nikec 2023-09-15 13:03:28 +02:00 committed by GitHub
parent 91f3bad92e
commit 42c6c358c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 230 additions and 126 deletions

View file

@ -64,6 +64,7 @@ pub struct ExplorerSettings<TOrder> {
media_aspect_square: Option<bool>,
open_on_double_click: Option<DoubleClickAction>,
show_bytes_in_grid_view: Option<bool>,
col_visibility: Option<BTreeMap<String, bool>>,
col_sizes: Option<BTreeMap<String, i32>>,
// temporary
#[serde(skip_serializing_if = "Option::is_none")]

View file

@ -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<ExplorerItem>;
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<ColumnSizingState>({});
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>();
const [listOffset, setListOffset] = useState(0);
const [ranges, setRanges] = useState<Range[]>([]);
@ -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
}}
>
<div className="flex">
{table.getHeaderGroups().map((headerGroup) => (
<div
ref={tableHeaderRef}
key={headerGroup.id}
className="flex grow border-b border-app-line/50"
onMouseDown={(e) => e.stopPropagation()}
>
{headerGroup.headers.map((header, i) => {
const size = header.column.getSize();
<ContextMenu.Root
trigger={
<div className="flex">
{table.getHeaderGroups().map((headerGroup) => (
<div
ref={tableHeaderRef}
key={headerGroup.id}
className="flex grow border-b border-app-line/50"
onMouseDown={(e) => 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 (
<div
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;
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 (
<div
className={clsx(
'flex items-center justify-between gap-3',
orderingDirection !== null
? 'text-ink'
: 'text-ink-dull'
)}
>
{typeof cellContent ===
'string' && (
<HeaderColumnName
name={cellContent}
/>
)}
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' && (
<CaretUp className="shrink-0 text-ink-faint" />
)}
{orderingDirection === 'Desc' && (
<CaretDown className="shrink-0 text-ink-faint" />
)}
<div
onClick={(e) =>
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 : (
<div
className={clsx(
'flex items-center justify-between gap-3',
orderingDirection !==
null
? 'text-ink'
: 'text-ink-dull'
)}
>
{typeof cellContent ===
'string' && (
<HeaderColumnName
name={cellContent}
/>
)}
{orderingDirection ===
'Asc' && (
<CaretUp className="shrink-0 text-ink-faint" />
)}
{orderingDirection ===
'Desc' && (
<CaretDown className="shrink-0 text-ink-faint" />
)}
<div
onClick={(e) =>
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"
/>
</div>
)}
</div>
)}
</div>
);
})}
);
})}
</div>
))}
</div>
))}
</div>
}
>
{table.getAllLeafColumns().map((column) => {
if (column.id === 'name') return null;
return (
<ContextMenu.CheckboxItem
key={column.id}
label={
typeof column.columnDef.header === 'string'
? column.columnDef.header
: column.id
}
checked={column.getIsVisible()}
onSelect={column.getToggleVisibilityHandler()}
/>
);
})}
</ContextMenu.Root>
</div>
</ScrollSyncPane>
@ -1196,6 +1249,7 @@ export default () => {
row={row}
paddingX={paddingX}
columnSizing={columnSizing}
columnVisibility={columnVisibility}
selected={selected}
cut={cut}
/>

View file

@ -77,14 +77,25 @@ export const createDefaultExplorerSettings = <TOrder extends Ordering>(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
}

View file

@ -154,7 +154,7 @@ export type ExplorerItem = { type: "Path"; has_local_thumbnail: boolean; thumbna
export type ExplorerLayout = "grid" | "list" | "media"
export type ExplorerSettings<TOrder> = { 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<TOrder> = { 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 }

View file

@ -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<typeof contextMenuItemStyles> {
icon?: Icon;
iconProps?: IconProps;
rightArrow?: boolean;
label?: string;
keybind?: string;
}
export interface ContextMenuItemProps
extends RadixCM.MenuItemProps,
VariantProps<typeof contextMenuItemStyles>,
Pick<ContextMenuInnerItemProps, 'label' | 'keybind' | 'icon' | 'iconProps'> {}
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 (
<RadixCM.Item
className={contextMenuItemClassNames}
onClick={(e) => !props.disabled && onClick?.(e)}
{...props}
>
<ContextMenuDivItem
{...{ icon, iconProps, label, rightArrow, keybind, variant, children }}
/>
<ContextMenuDivItem {...{ icon, iconProps, label, keybind, variant, children }} />
</RadixCM.Item>
);
};
export interface ContextMenuCheckboxItemProps
extends RadixCM.MenuCheckboxItemProps,
VariantProps<typeof contextMenuItemStyles>,
Pick<ContextMenuInnerItemProps, 'label' | 'keybind'> {}
const CheckboxItem = ({
variant,
className,
label,
keybind,
children,
...props
}: ContextMenuCheckboxItemProps) => {
return (
<RadixCM.CheckboxItem className={contextMenuItemClassNames} {...props}>
<ContextMenuDivItem variant={variant} className={className}>
<span className="flex h-3.5 w-3.5 items-center justify-center">
<RadixCM.ItemIndicator>
<Check weight="bold" />
</RadixCM.ItemIndicator>
</span>
<ItemInternals {...{ label, keybind, children }} />
</ContextMenuDivItem>
</RadixCM.CheckboxItem>
);
};
interface ContextMenuInnerItemProps {
icon?: Icon;
iconProps?: IconProps;
label?: string;
keybind?: string;
rightArrow?: boolean;
}
export const ContextMenuDivItem = ({
variant,
children,
className,
...props
}: PropsWithChildren<ContextMenuItemProps & { className?: string }>) => (
}: ContextMenuInnerItemProps &
VariantProps<typeof contextMenuItemStyles> &
PropsWithChildren<{ className?: string }>) => (
<div className={contextMenuItemStyles({ variant, className })}>
{children || <ItemInternals {...props} />}
</div>
);
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
};

View file

@ -118,7 +118,6 @@ const Item = ({
icon,
iconProps,
label,
rightArrow,
children,
keybind,
variant,
@ -135,13 +134,13 @@ const Item = ({
<Link to={to} onClick={() => ref.current?.click()}>
<ContextMenuDivItem
className={clsx(selected && 'bg-accent text-white')}
{...{ icon, iconProps, label, rightArrow, keybind, variant, children }}
{...{ icon, iconProps, label, keybind, variant, children }}
/>
</Link>
) : (
<ContextMenuDivItem
className={clsx(selected && 'bg-accent text-white')}
{...{ icon, iconProps, label, rightArrow, keybind, variant, children }}
{...{ icon, iconProps, label, keybind, variant, children }}
/>
)}
</RadixDM.Item>