mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
[ENG-1113] Toggleable list columns (#1352)
* Toggleable list columns * type
This commit is contained in:
parent
91f3bad92e
commit
42c6c358c6
|
@ -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")]
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue