Virtualization draft

This commit is contained in:
Sergei Garin 2024-11-01 14:14:35 +03:00
parent ab9e7ef9c2
commit bf098cdc54
No known key found for this signature in database
6 changed files with 102 additions and 71 deletions

View File

@ -67,10 +67,21 @@ export function DialogTrigger(props: DialogTriggerProps) {
} satisfies DialogTriggerRenderProps
return (
<aria.DialogTrigger {...state} onOpenChange={onOpenChangeInternal}>
<DialogTriggerInner {...state} onOpenChange={onOpenChangeInternal}>
{trigger}
{typeof dialog === 'function' ? dialog(renderProps) : dialog}
</aria.DialogTrigger>
{state.isOpen ?
typeof dialog === 'function' ?
dialog(renderProps)
: dialog
: null}
</DialogTriggerInner>
)
}
/**
* A DialogTriggerInner is a DialogTrigger that has a stable reference.
*/
function DialogTriggerInner(props: aria.DialogTriggerProps) {
return <aria.DialogTrigger {...props} />
}

View File

@ -113,15 +113,12 @@ export interface AssetRowProps {
event: React.DragEvent<HTMLTableRowElement>,
item: backendModule.AnyAsset,
) => void
readonly rowHeight: number
readonly rowOffset: number
}
/** A row containing an {@link backendModule.AnyAsset}. */
export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
const { id, parentId, isKeyboardSelected, isOpened, select, state, columns, onClick } = props
const { path, hidden: hiddenRaw, grabKeyboardFocus, visibility: visibilityRaw, depth } = props
const { rowHeight, rowOffset } = props
const { initialAssetEvents } = props
const { nodeMap, doCopy, doCut, doPaste, doDelete: doDeleteRaw } = state
const { doRestore, doMove, category, scrollContainerRef, rootDirectoryId, backend } = state
@ -509,10 +506,9 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
<>
{!hidden && (
<FocusRing>
<tr
<div
data-testid="asset-row"
tabIndex={0}
style={{ maxHeight: rowHeight, transform: `translateY(${rowOffset}px)` }}
ref={(element) => {
rootRef.current = element
@ -699,7 +695,7 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
</td>
)
})}
</tr>
</div>
</FocusRing>
)}
{selected && allowContextMenu && !hidden && (
@ -724,7 +720,7 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
}
case backendModule.AssetType.specialLoading: {
return hidden ? null : (
<tr>
<div>
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
<div
className={tailwindMerge.twMerge(
@ -735,12 +731,12 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
<StatelessSpinner size={24} state={statelessSpinner.SpinnerState.loadingMedium} />
</div>
</td>
</tr>
</div>
)
}
case backendModule.AssetType.specialEmpty: {
return hidden ? null : (
<tr>
<div>
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
<div
className={tailwindMerge.twMerge(
@ -754,12 +750,12 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
</Text>
</div>
</td>
</tr>
</div>
)
}
case backendModule.AssetType.specialError: {
return hidden ? null : (
<tr>
<div>
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
<div
className={tailwindMerge.twMerge(
@ -776,7 +772,7 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
</Text>
</div>
</td>
</tr>
</div>
)
}
}

View File

@ -16,6 +16,7 @@ import SvgMask from '#/components/SvgMask'
import * as backendModule from '#/services/Backend'
import { useEventCallback } from '#/hooks/eventCallbackHooks'
import * as eventModule from '#/utilities/event'
import * as indent from '#/utilities/indent'
import * as object from '#/utilities/object'
@ -46,7 +47,7 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
const updateDirectoryMutation = useMutation(backendMutationOptions(backend, 'updateDirectory'))
const setIsEditing = (isEditingName: boolean) => {
const setIsEditing = useEventCallback((isEditingName: boolean) => {
if (isEditable) {
setRowState(object.merger({ isEditingName }))
}
@ -54,20 +55,38 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
if (!isEditingName) {
driveStore.setState({ newestFolderId: null })
}
}
})
const doRename = async (newTitle: string) => {
const doRename = useEventCallback(async (newTitle: string) => {
if (isEditable) {
setIsEditing(false)
if (!string.isWhitespaceOnly(newTitle) && newTitle !== item.title) {
await updateDirectoryMutation.mutateAsync([item.id, { title: newTitle }, item.title])
}
}
}
})
const toggleExpanded = useEventCallback(() => {
doToggleDirectoryExpansion(item.id, item.id)
})
const checkSubmittable = useEventCallback(
(newTitle: string) =>
validation.DIRECTORY_NAME_REGEX.test(newTitle) &&
backendModule.isNewTitleValid(
item,
newTitle,
nodeMap.current.get(item.parentId)?.children?.map((child) => child.item),
),
)
const onCancel = useEventCallback(() => {
setIsEditing(false)
})
return (
<div
className={tailwindMerge.twMerge(
className={tailwindMerge.twJoin(
'group flex h-table-row min-w-max items-center gap-name-column-icon whitespace-nowrap rounded-l-full px-name-column-x py-name-column-y',
indent.indentClass(depth),
)}
@ -93,34 +112,23 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
variant="custom"
aria-label={isExpanded ? getText('collapse') : getText('expand')}
tooltipPlacement="left"
className={tailwindMerge.twMerge(
className={tailwindMerge.twJoin(
'm-0 hidden cursor-pointer border-0 transition-transform duration-arrow group-hover:m-name-column-icon group-hover:inline-block',
isExpanded && 'rotate-90',
)}
onPress={() => {
doToggleDirectoryExpansion(item.id, item.id)
}}
onPress={toggleExpanded}
/>
<SvgMask src={FolderIcon} className="m-name-column-icon size-4 group-hover:hidden" />
<EditableSpan
data-testid="asset-row-name"
editable={rowState.isEditingName}
className={tailwindMerge.twMerge(
className={tailwindMerge.twJoin(
'grow cursor-pointer bg-transparent font-naming',
rowState.isEditingName ? 'cursor-text' : 'cursor-pointer',
)}
checkSubmittable={(newTitle) =>
validation.DIRECTORY_NAME_REGEX.test(newTitle) &&
backendModule.isNewTitleValid(
item,
newTitle,
nodeMap.current.get(item.parentId)?.children?.map((child) => child.item),
)
}
checkSubmittable={checkSubmittable}
onSubmit={doRename}
onCancel={() => {
setIsEditing(false)
}}
onCancel={onCancel}
>
{item.title}
</EditableSpan>

View File

@ -20,6 +20,7 @@ import * as backendModule from '#/services/Backend'
import { useBackendQuery } from '#/hooks/backendHooks'
import * as tailwindMerge from '#/utilities/tailwindMerge'
import { useMemo } from 'react'
import { useEventCallback } from '#/hooks/eventCallbackHooks'
// =================
// === Constants ===
@ -137,15 +138,15 @@ export default function ProjectIcon(props: ProjectIconProps) {
}
})()
const doOpenProject = () => {
const doOpenProject = useEventCallback(() => {
openProject({ ...item, type: backend.type })
}
const doCloseProject = () => {
})
const doCloseProject = useEventCallback(() => {
closeProject({ ...item, type: backend.type })
}
const doOpenProjectTab = () => {
})
const doOpenProjectTab = useEventCallback(() => {
openProjectTab(item.id)
}
})
switch (state) {
case backendModule.ProjectState.new:

View File

@ -20,6 +20,7 @@ import ManageLabelsModal from '#/modals/ManageLabelsModal'
import * as backendModule from '#/services/Backend'
import { useEventCallback } from '#/hooks/eventCallbackHooks'
import * as permissions from '#/utilities/permissions'
// ====================
@ -44,6 +45,12 @@ export default function LabelsColumn(props: column.AssetColumnProps) {
(self?.permission === permissions.PermissionAction.own ||
self?.permission === permissions.PermissionAction.admin)
const onPress = useEventCallback(() => {})
const renderManageLabelsModal = useEventCallback(() => (
<ManageLabelsModal backend={backend} item={item} />
))
return (
<div className="group flex items-center gap-column-items">
{(item.labels ?? [])
@ -93,7 +100,7 @@ export default function LabelsColumn(props: column.AssetColumnProps) {
isDisabled
key={label}
color={labelsByName.get(label)?.color ?? backendModule.COLORS[0]}
onPress={() => {}}
onPress={onPress}
>
{label}
</Label>
@ -101,7 +108,7 @@ export default function LabelsColumn(props: column.AssetColumnProps) {
{managesThisAsset && (
<DialogTrigger>
<Button variant="ghost" showIconOnHover icon={Plus2Icon} />
<ManageLabelsModal backend={backend} item={item} />
{renderManageLabelsModal}
</DialogTrigger>
)}
</div>

View File

@ -2574,7 +2574,7 @@ export default function AssetsTable(props: AssetsTableProps) {
count: displayItems.length,
getScrollElement: () => rootRef.current,
estimateSize: () => ROW_HEIGHT_PX,
overscan: 20,
overscan: 0,
})
const columns = useMemo(
@ -2614,31 +2614,38 @@ export default function AssetsTable(props: AssetsTableProps) {
}
return (
<AssetRow
key={item.key + item.path}
rowHeight={virtualRow.size}
rowOffset={virtualRow.start - index * virtualRow.size}
isOpened={openedProjects.some(({ id }) => item.item.id === id)}
visibility={visibilities.get(item.key)}
columns={columns}
id={item.item.id}
parentId={item.directoryId}
path={item.path}
initialAssetEvents={item.initialAssetEvents}
depth={item.depth}
state={state}
hidden={hidden || visibilities.get(item.key) === Visibility.hidden}
isKeyboardSelected={
keyboardSelectedIndex != null && item === visibleItems[keyboardSelectedIndex]
}
grabKeyboardFocus={grabRowKeyboardFocus}
onClick={onRowClick}
select={selectRow}
onDragStart={onRowDragStart}
onDragOver={onRowDragOver}
onDragEnd={onRowDragEnd}
onDrop={onRowDrop}
/>
<tr
key={index}
data-testid="asset-row"
tabIndex={0}
style={{
maxHeight: virtualRow.size,
transform: `translateY(${virtualRow.start - index * virtualRow.size}px)`,
}}
>
<AssetRow
isOpened={openedProjects.some(({ id }) => item.item.id === id)}
visibility={visibilities.get(item.key)}
columns={columns}
id={item.item.id}
parentId={item.directoryId}
path={item.path}
initialAssetEvents={item.initialAssetEvents}
depth={item.depth}
state={state}
hidden={hidden || visibilities.get(item.key) === Visibility.hidden}
isKeyboardSelected={
keyboardSelectedIndex != null && item === visibleItems[keyboardSelectedIndex]
}
grabKeyboardFocus={grabRowKeyboardFocus}
onClick={onRowClick}
select={selectRow}
onDragStart={onRowDragStart}
onDragOver={onRowDragOver}
onDragEnd={onRowDragEnd}
onDrop={onRowDrop}
/>
</tr>
)
})
@ -2690,7 +2697,7 @@ export default function AssetsTable(props: AssetsTableProps) {
}}
>
<table className="isolate table-fixed border-collapse rounded-rows">
<thead className="sticky top-0 z-1 bg-dashboard">{headerRow}</thead>
<thead className="bg-dashboard sticky top-0 z-1">{headerRow}</thead>
<tbody ref={bodyRef}>
{itemRows}
<tr className="hidden h-row first:table-row">
@ -2792,6 +2799,7 @@ export default function AssetsTable(props: AssetsTableProps) {
{...mergeProps<JSX.IntrinsicElements['div']>()(innerProps, {
className: 'flex-1 overflow-auto container-size w-full h-full',
onKeyDown,
ref: rootRef,
onBlur: (event) => {
if (
event.relatedTarget instanceof HTMLElement &&