[ENG-624] Explorer order by (#831)

added order by + fixed explorer padding situation
This commit is contained in:
Jamie Pine 2023-05-19 08:51:17 -07:00 committed by GitHub
parent 0ba5874947
commit 3880428596
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 92 additions and 57 deletions

View file

@ -42,12 +42,20 @@ pub fn mount() -> AlphaRouter<Ctx> {
#[specta(inline)] #[specta(inline)]
enum Ordering { enum Ordering {
Name(bool), Name(bool),
SizeInBytes(bool),
DateCreated(bool),
DateModified(bool),
DateIndexed(bool),
} }
impl Ordering { impl Ordering {
fn get_direction(&self) -> Direction { fn get_direction(&self) -> Direction {
match self { match self {
Self::Name(v) => v, Self::Name(v) => v,
Self::SizeInBytes(v) => v,
Self::DateCreated(v) => v,
Self::DateModified(v) => v,
Self::DateIndexed(v) => v,
} }
.then_some(Direction::Asc) .then_some(Direction::Asc)
.unwrap_or(Direction::Desc) .unwrap_or(Direction::Desc)
@ -57,6 +65,10 @@ pub fn mount() -> AlphaRouter<Ctx> {
use file_path::*; use file_path::*;
match self { match self {
Self::Name(_) => name::order(dir), Self::Name(_) => name::order(dir),
Self::SizeInBytes(_) => size_in_bytes::order(dir),
Self::DateCreated(_) => date_created::order(dir),
Self::DateModified(_) => date_modified::order(dir),
Self::DateIndexed(_) => date_indexed::order(dir),
} }
} }
} }

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import clsx from 'clsx'; import clsx from 'clsx';
import { memo, useEffect, useMemo, useRef, useState } from 'react'; import { memo, useEffect, useMemo, useRef, useState } from 'react';
@ -71,6 +72,8 @@ const GridViewItem = memo(({ data, selected, index, ...props }: GridViewItemProp
); );
}); });
const LEFT_PADDING = 14;
export default () => { export default () => {
const explorerStore = useExplorerStore(); const explorerStore = useExplorerStore();
const { data, scrollRef, onLoadMore, hasNextPage, isFetchingNextPage } = const { data, scrollRef, onLoadMore, hasNextPage, isFetchingNextPage } =
@ -107,7 +110,7 @@ export default () => {
function handleWindowResize() { function handleWindowResize() {
if (scrollRef.current) { if (scrollRef.current) {
setWidth(scrollRef.current.offsetWidth); setWidth(scrollRef.current.offsetWidth - LEFT_PADDING);
} }
} }
@ -186,7 +189,8 @@ export default () => {
<div <div
className="relative" className="relative"
style={{ style={{
height: `${rowVirtualizer.getTotalSize()}px` height: `${rowVirtualizer.getTotalSize()}px`,
marginLeft: `${LEFT_PADDING - 4}px`
}} }}
> >
{virtualRows.map((virtualRow) => ( {virtualRows.map((virtualRow) => (

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
ColumnDef, ColumnDef,
ColumnSizingState, ColumnSizingState,
@ -162,12 +163,12 @@ export default () => {
return aName === bName return aName === bName
? 0 ? 0
: aName > bName : aName > bName
? desc ? desc
? 1 ? 1
: -1 : -1
: desc : desc
? -1 ? -1
: 1; : 1;
} }
return aDate > bDate ? 1 : -1; return aDate > bDate ? 1 : -1;
@ -225,13 +226,13 @@ export default () => {
...sizing, ...sizing,
...(scrollWidth && nameWidth ...(scrollWidth && nameWidth
? { ? {
Name: Name:
nameWidth + nameWidth +
scrollWidth - scrollWidth -
paddingX * 2 - paddingX * 2 -
scrollBarWidth - scrollBarWidth -
tableLength tableLength
} }
: {}) : {})
}; };
}); });
@ -360,8 +361,8 @@ export default () => {
i === 0 i === 0
? size + paddingX ? size + paddingX
: i === headerGroup.headers.length - 1 : i === headerGroup.headers.length - 1
? size - paddingX ? size - paddingX
: size : size
}} }}
onClick={header.column.getToggleSortingHandler()} onClick={header.column.getToggleSortingHandler()}
> >
@ -381,16 +382,16 @@ export default () => {
{(i !== headerGroup.headers.length - 1 || {(i !== headerGroup.headers.length - 1 ||
(i === headerGroup.headers.length - 1 && (i === headerGroup.headers.length - 1 &&
!locked)) && ( !locked)) && (
<div <div
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => { onMouseDown={(e) => {
setLocked(false); setLocked(false);
header.getResizeHandler()(e); header.getResizeHandler()(e);
}} }}
onTouchStart={header.getResizeHandler()} onTouchStart={header.getResizeHandler()}
className="absolute right-0 h-[70%] w-2 cursor-col-resize border-r border-app-line/50" className="absolute right-0 h-[70%] w-2 cursor-col-resize border-r border-app-line/50"
/> />
)} )}
</div> </div>
)} )}
</div> </div>

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import clsx from 'clsx'; import clsx from 'clsx';
import { ArrowsOutSimple } from 'phosphor-react'; import { ArrowsOutSimple } from 'phosphor-react';

View file

@ -1,29 +1,26 @@
import { useState } from 'react'; import { useState } from 'react';
import { RadixCheckbox, Select, SelectOption, Slider, tw } from '@sd/ui'; import { RadixCheckbox, Select, SelectOption, Slider, tw } from '@sd/ui';
import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore'; import { ExplorerDirection, ExplorerOrderByKeys, getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
import { getExplorerConfigStore, useExplorerConfigStore } from '~/hooks/useExplorerConfigStore'; import { getExplorerConfigStore, useExplorerConfigStore } from '~/hooks/useExplorerConfigStore';
const Heading = tw.div`text-ink-dull text-xs font-semibold`; const Heading = tw.div`text-ink-dull text-xs font-semibold`;
const Subheading = tw.div`text-ink-dull mb-1 text-xs font-medium`; const Subheading = tw.div`text-ink-dull mb-1 text-xs font-medium`;
const sortOptions = { const sortOptions: Record<ExplorerOrderByKeys, string> = {
none: 'None',
name: 'Name', name: 'Name',
kind: 'Kind', sizeInBytes: 'Size',
favorite: 'Favorite', dateCreated: 'Date created',
date_created: 'Date Created', dateModified: 'Date modified',
date_modified: 'Date Modified', dateIndexed: 'Date indexed'
date_last_opened: 'Date Last Opened'
}; };
export default () => { export default () => {
const [sortBy, setSortBy] = useState('name');
const [stackBy, setStackBy] = useState('kind');
const explorerStore = useExplorerStore(); const explorerStore = useExplorerStore();
const explorerConfig = useExplorerConfigStore(); const explorerConfig = useExplorerConfigStore();
return ( return (
<div className="p-4 "> <div className="p-4">
<Subheading>Item size</Subheading> <Subheading>Item size</Subheading>
{explorerStore.layoutMode === 'media' ? ( {explorerStore.layoutMode === 'media' ? (
<Slider <Slider
@ -52,7 +49,7 @@ export default () => {
<div className="my-2 mt-4 grid grid-cols-2 gap-2"> <div className="my-2 mt-4 grid grid-cols-2 gap-2">
<div className="flex flex-col"> <div className="flex flex-col">
<Subheading>Sort by</Subheading> <Subheading>Sort by</Subheading>
<Select value={sortBy} size="sm" onChange={setSortBy}> <Select value={explorerStore.orderBy} size="sm" className='w-full' onChange={(value) => getExplorerStore().orderBy = value as ExplorerOrderByKeys}>
{Object.entries(sortOptions).map(([value, text]) => ( {Object.entries(sortOptions).map(([value, text]) => (
<SelectOption key={value} value={value}> <SelectOption key={value} value={value}>
{text} {text}
@ -61,11 +58,10 @@ export default () => {
</Select> </Select>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<Subheading>Stack by</Subheading> <Subheading>Direction</Subheading>
<Select value={stackBy} size="sm" onChange={setStackBy}> <Select value={explorerStore.orderByDirection} size="sm" className='w-full' onChange={(value) => getExplorerStore().orderByDirection = value as ExplorerDirection}>
<SelectOption value="kind">Kind</SelectOption> <SelectOption value="asc">Asc</SelectOption>
<SelectOption value="location">Location</SelectOption> <SelectOption value="desc">Desc</SelectOption>
<SelectOption value="node">Node</SelectOption>
</Select> </Select>
</div> </div>
</div> </div>

View file

@ -101,7 +101,7 @@ export default memo((props: Props) => {
ref={props.scrollRef || scrollRef} ref={props.scrollRef || scrollRef}
className={clsx( className={clsx(
'custom-scroll explorer-scroll h-screen', 'custom-scroll explorer-scroll h-screen',
layoutMode === 'grid' && 'overflow-x-hidden pl-4', layoutMode === 'grid' && 'overflow-x-hidden',
props.viewClassName props.viewClassName
)} )}
style={{ paddingTop: TOP_BAR_HEIGHT }} style={{ paddingTop: TOP_BAR_HEIGHT }}

View file

@ -1,8 +1,8 @@
import { z } from 'zod'; import { z } from 'zod';
import { ExplorerItem, ObjectKind, ObjectKindKey, isObject, isPath } from '@sd/client'; import { ExplorerItem, ObjectKind, ObjectKindKey, Ordering, isObject, isPath } from '@sd/client';
import { useZodSearchParams } from '~/hooks'; import { useExplorerStore, useZodSearchParams } from '~/hooks';
export function getExplorerItemData(data: ExplorerItem, hasNewThumbnail: boolean) { export function getExplorerItemData(data: ExplorerItem, hasNewThumbnail?: boolean) {
const objectData = getItemObject(data); const objectData = getItemObject(data);
const filePath = getItemFilePath(data); const filePath = getItemFilePath(data);
@ -15,6 +15,14 @@ export function getExplorerItemData(data: ExplorerItem, hasNewThumbnail: boolean
}; };
} }
export function useExplorerOrder(): Ordering | undefined {
const explorerStore = useExplorerStore();
if (explorerStore.orderBy === 'none') return undefined;
return { [explorerStore.orderBy]: explorerStore.orderByDirection === 'asc' } as Ordering;
}
export function getItemObject(data: ExplorerItem) { export function getItemObject(data: ExplorerItem) {
return isObject(data) ? data.item : data.item.object; return isObject(data) ? data.item : data.item.object;
} }

View file

@ -15,7 +15,7 @@ export const Component = () => {
style={{ paddingTop: TOP_BAR_HEIGHT }} style={{ paddingTop: TOP_BAR_HEIGHT }}
> >
<PageContext.Provider value={{ ref }}> <PageContext.Provider value={{ ref }}>
<div className="flex h-screen w-full flex-col p-5"> <div className="flex h-screen w-full flex-col">
<Outlet /> <Outlet />
</div> </div>
</PageContext.Provider> </PageContext.Provider>

View file

@ -9,7 +9,7 @@ import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
import { useExplorerTopBarOptions } from '~/hooks/useExplorerTopBarOptions'; import { useExplorerTopBarOptions } from '~/hooks/useExplorerTopBarOptions';
import Explorer from '../Explorer'; import Explorer from '../Explorer';
import DeleteDialog from '../Explorer/File/DeleteDialog'; import DeleteDialog from '../Explorer/File/DeleteDialog';
import { useExplorerSearchParams } from '../Explorer/util'; import { useExplorerOrder, useExplorerSearchParams } from '../Explorer/util';
import TopBarChildren from '../TopBar/TopBarChildren'; import TopBarChildren from '../TopBar/TopBarChildren';
const PARAMS = z.object({ const PARAMS = z.object({
@ -81,6 +81,7 @@ const useItems = () => {
{ {
library_id: library.uuid, library_id: library.uuid,
arg: { arg: {
order: useExplorerOrder(),
locationId, locationId,
take, take,
...(explorerState.layoutMode === 'media' ...(explorerState.layoutMode === 'media'

View file

@ -87,7 +87,7 @@ export default () => {
}); });
mounted = true; mounted = true;
return ( return (
<div className="flex w-full"> <div className="flex w-full px-5 pt-4">
{/* STAT CONTAINER */} {/* STAT CONTAINER */}
<div className="-mb-1 flex h-20 overflow-hidden"> <div className="-mb-1 flex h-20 overflow-hidden">
{Object.entries(stats?.data || []).map(([key, value]) => { {Object.entries(stats?.data || []).map(([key, value]) => {

View file

@ -13,7 +13,7 @@ import { useMemo, useState } from 'react';
import 'react-loading-skeleton/dist/skeleton.css'; import 'react-loading-skeleton/dist/skeleton.css';
import { useExplorerStore, useExplorerTopBarOptions } from '~/hooks'; import { useExplorerStore, useExplorerTopBarOptions } from '~/hooks';
import Explorer from '../Explorer'; import Explorer from '../Explorer';
import { SEARCH_PARAMS } from '../Explorer/util'; import { SEARCH_PARAMS, useExplorerOrder } from '../Explorer/util';
import { usePageLayout } from '../PageLayout'; import { usePageLayout } from '../PageLayout';
import TopBarChildren from '../TopBar/TopBarChildren'; import TopBarChildren from '../TopBar/TopBarChildren';
import CategoryButton from '../overview/CategoryButton'; import CategoryButton from '../overview/CategoryButton';
@ -79,6 +79,7 @@ export const Component = () => {
{ {
library_id: library.uuid, library_id: library.uuid,
arg: { arg: {
order: useExplorerOrder(),
favorite: isFavoritesCategory ? true : undefined, favorite: isFavoritesCategory ? true : undefined,
...(explorerStore.layoutMode === 'media' ...(explorerStore.layoutMode === 'media'
? { kind: [5, 7].includes(kind) ? [kind] : isFavoritesCategory ? [5, 7] : [5, 7, kind] } ? { kind: [5, 7].includes(kind) ? [kind] : isFavoritesCategory ? [5, 7] : [5, 7, kind] }
@ -91,7 +92,7 @@ export const Component = () => {
'search.paths', 'search.paths',
{ {
...queryKey[1].arg, ...queryKey[1].arg,
cursor, cursor
}, },
]), ]),
getNextPageParam: (lastPage) => lastPage.cursor ?? undefined getNextPageParam: (lastPage) => lastPage.cursor ?? undefined
@ -123,8 +124,9 @@ export const Component = () => {
isFetchingNextPage={query.isFetchingNextPage} isFetchingNextPage={query.isFetchingNextPage}
scrollRef={page?.ref} scrollRef={page?.ref}
> >
<Statistics /> <Statistics />
<div className="no-scrollbar sticky top-0 z-50 mt-4 flex space-x-[1px] overflow-x-scroll bg-app/90 py-1.5 backdrop-blur"> <div className="no-scrollbar sticky top-0 z-50 mt-2 flex space-x-[1px] overflow-x-scroll bg-app/90 px-5 py-1.5 backdrop-blur">
{categories.data?.map((category) => { {categories.data?.map((category) => {
const iconString = CategoryToIcon[category.name] || 'Document'; const iconString = CategoryToIcon[category.name] || 'Document';
const icon = icons[iconString as keyof typeof icons]; const icon = icons[iconString as keyof typeof icons];
@ -140,6 +142,7 @@ export const Component = () => {
); );
})} })}
</div> </div>
</Explorer> </Explorer>
</> </>
); );

View file

@ -1,7 +1,9 @@
import { proxy, useSnapshot } from 'valtio'; import { proxy, useSnapshot } from 'valtio';
import { ExplorerItem } from '@sd/client'; import { ExplorerItem, Ordering } from '@sd/client';
import { resetStore } from '@sd/client/src/stores/util'; import { resetStore } from '@sd/client/src/stores/util';
type UnionKeys<T> = T extends any ? keyof T : never;
export type ExplorerLayoutMode = 'rows' | 'grid' | 'columns' | 'media'; export type ExplorerLayoutMode = 'rows' | 'grid' | 'columns' | 'media';
export enum ExplorerKind { export enum ExplorerKind {
@ -12,6 +14,10 @@ export enum ExplorerKind {
export type CutCopyType = 'Cut' | 'Copy'; export type CutCopyType = 'Cut' | 'Copy';
export type ExplorerOrderByKeys = UnionKeys<Ordering> | 'none';
export type ExplorerDirection = 'asc' | 'desc';
const state = { const state = {
locationId: null as number | null, locationId: null as number | null,
layoutMode: 'grid' as ExplorerLayoutMode, layoutMode: 'grid' as ExplorerLayoutMode,
@ -35,7 +41,10 @@ const state = {
quickViewObject: null as ExplorerItem | null, quickViewObject: null as ExplorerItem | null,
isRenaming: false, isRenaming: false,
mediaColumns: 8, mediaColumns: 8,
mediaAspectSquare: true mediaAspectSquare: true,
orderBy: 'dateCreated' as ExplorerOrderByKeys,
orderByDirection: 'desc' as ExplorerDirection,
groupBy: 'none',
}; };
// Keep the private and use `useExplorerState` or `getExplorerStore` or you will get production build issues. // Keep the private and use `useExplorerState` or `getExplorerStore` or you will get production build issues.

View file

@ -200,7 +200,7 @@ export type CRDTOperation = { node: string; timestamp: number; id: string; typ:
*/ */
export type Salt = number[] export type Salt = number[]
export type Ordering = { name: boolean } export type Ordering = { name: boolean } | { sizeInBytes: boolean } | { dateCreated: boolean } | { dateModified: boolean } | { dateIndexed: boolean }
export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string } export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string }