mirror of
https://github.com/enso-org/enso.git
synced 2024-11-25 10:43:02 +03:00
Virtualization draft
This commit is contained in:
parent
ab9e7ef9c2
commit
bf098cdc54
@ -67,10 +67,21 @@ export function DialogTrigger(props: DialogTriggerProps) {
|
|||||||
} satisfies DialogTriggerRenderProps
|
} satisfies DialogTriggerRenderProps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aria.DialogTrigger {...state} onOpenChange={onOpenChangeInternal}>
|
<DialogTriggerInner {...state} onOpenChange={onOpenChangeInternal}>
|
||||||
{trigger}
|
{trigger}
|
||||||
|
|
||||||
{typeof dialog === 'function' ? dialog(renderProps) : dialog}
|
{state.isOpen ?
|
||||||
</aria.DialogTrigger>
|
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} />
|
||||||
|
}
|
||||||
|
@ -113,15 +113,12 @@ export interface AssetRowProps {
|
|||||||
event: React.DragEvent<HTMLTableRowElement>,
|
event: React.DragEvent<HTMLTableRowElement>,
|
||||||
item: backendModule.AnyAsset,
|
item: backendModule.AnyAsset,
|
||||||
) => void
|
) => void
|
||||||
readonly rowHeight: number
|
|
||||||
readonly rowOffset: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A row containing an {@link backendModule.AnyAsset}. */
|
/** A row containing an {@link backendModule.AnyAsset}. */
|
||||||
export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
||||||
const { id, parentId, isKeyboardSelected, isOpened, select, state, columns, onClick } = props
|
const { id, parentId, isKeyboardSelected, isOpened, select, state, columns, onClick } = props
|
||||||
const { path, hidden: hiddenRaw, grabKeyboardFocus, visibility: visibilityRaw, depth } = props
|
const { path, hidden: hiddenRaw, grabKeyboardFocus, visibility: visibilityRaw, depth } = props
|
||||||
const { rowHeight, rowOffset } = props
|
|
||||||
const { initialAssetEvents } = props
|
const { initialAssetEvents } = props
|
||||||
const { nodeMap, doCopy, doCut, doPaste, doDelete: doDeleteRaw } = state
|
const { nodeMap, doCopy, doCut, doPaste, doDelete: doDeleteRaw } = state
|
||||||
const { doRestore, doMove, category, scrollContainerRef, rootDirectoryId, backend } = state
|
const { doRestore, doMove, category, scrollContainerRef, rootDirectoryId, backend } = state
|
||||||
@ -509,10 +506,9 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
|||||||
<>
|
<>
|
||||||
{!hidden && (
|
{!hidden && (
|
||||||
<FocusRing>
|
<FocusRing>
|
||||||
<tr
|
<div
|
||||||
data-testid="asset-row"
|
data-testid="asset-row"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={{ maxHeight: rowHeight, transform: `translateY(${rowOffset}px)` }}
|
|
||||||
ref={(element) => {
|
ref={(element) => {
|
||||||
rootRef.current = element
|
rootRef.current = element
|
||||||
|
|
||||||
@ -699,7 +695,7 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
|||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</tr>
|
</div>
|
||||||
</FocusRing>
|
</FocusRing>
|
||||||
)}
|
)}
|
||||||
{selected && allowContextMenu && !hidden && (
|
{selected && allowContextMenu && !hidden && (
|
||||||
@ -724,7 +720,7 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
|||||||
}
|
}
|
||||||
case backendModule.AssetType.specialLoading: {
|
case backendModule.AssetType.specialLoading: {
|
||||||
return hidden ? null : (
|
return hidden ? null : (
|
||||||
<tr>
|
<div>
|
||||||
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
|
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
|
||||||
<div
|
<div
|
||||||
className={tailwindMerge.twMerge(
|
className={tailwindMerge.twMerge(
|
||||||
@ -735,12 +731,12 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
|||||||
<StatelessSpinner size={24} state={statelessSpinner.SpinnerState.loadingMedium} />
|
<StatelessSpinner size={24} state={statelessSpinner.SpinnerState.loadingMedium} />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case backendModule.AssetType.specialEmpty: {
|
case backendModule.AssetType.specialEmpty: {
|
||||||
return hidden ? null : (
|
return hidden ? null : (
|
||||||
<tr>
|
<div>
|
||||||
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
|
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
|
||||||
<div
|
<div
|
||||||
className={tailwindMerge.twMerge(
|
className={tailwindMerge.twMerge(
|
||||||
@ -754,12 +750,12 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case backendModule.AssetType.specialError: {
|
case backendModule.AssetType.specialError: {
|
||||||
return hidden ? null : (
|
return hidden ? null : (
|
||||||
<tr>
|
<div>
|
||||||
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
|
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
|
||||||
<div
|
<div
|
||||||
className={tailwindMerge.twMerge(
|
className={tailwindMerge.twMerge(
|
||||||
@ -776,7 +772,7 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import SvgMask from '#/components/SvgMask'
|
|||||||
|
|
||||||
import * as backendModule from '#/services/Backend'
|
import * as backendModule from '#/services/Backend'
|
||||||
|
|
||||||
|
import { useEventCallback } from '#/hooks/eventCallbackHooks'
|
||||||
import * as eventModule from '#/utilities/event'
|
import * as eventModule from '#/utilities/event'
|
||||||
import * as indent from '#/utilities/indent'
|
import * as indent from '#/utilities/indent'
|
||||||
import * as object from '#/utilities/object'
|
import * as object from '#/utilities/object'
|
||||||
@ -46,7 +47,7 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
|||||||
|
|
||||||
const updateDirectoryMutation = useMutation(backendMutationOptions(backend, 'updateDirectory'))
|
const updateDirectoryMutation = useMutation(backendMutationOptions(backend, 'updateDirectory'))
|
||||||
|
|
||||||
const setIsEditing = (isEditingName: boolean) => {
|
const setIsEditing = useEventCallback((isEditingName: boolean) => {
|
||||||
if (isEditable) {
|
if (isEditable) {
|
||||||
setRowState(object.merger({ isEditingName }))
|
setRowState(object.merger({ isEditingName }))
|
||||||
}
|
}
|
||||||
@ -54,20 +55,38 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
|||||||
if (!isEditingName) {
|
if (!isEditingName) {
|
||||||
driveStore.setState({ newestFolderId: null })
|
driveStore.setState({ newestFolderId: null })
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
const doRename = async (newTitle: string) => {
|
const doRename = useEventCallback(async (newTitle: string) => {
|
||||||
if (isEditable) {
|
if (isEditable) {
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
if (!string.isWhitespaceOnly(newTitle) && newTitle !== item.title) {
|
if (!string.isWhitespaceOnly(newTitle) && newTitle !== item.title) {
|
||||||
await updateDirectoryMutation.mutateAsync([item.id, { title: 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 (
|
return (
|
||||||
<div
|
<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',
|
'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),
|
indent.indentClass(depth),
|
||||||
)}
|
)}
|
||||||
@ -93,34 +112,23 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
|||||||
variant="custom"
|
variant="custom"
|
||||||
aria-label={isExpanded ? getText('collapse') : getText('expand')}
|
aria-label={isExpanded ? getText('collapse') : getText('expand')}
|
||||||
tooltipPlacement="left"
|
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',
|
'm-0 hidden cursor-pointer border-0 transition-transform duration-arrow group-hover:m-name-column-icon group-hover:inline-block',
|
||||||
isExpanded && 'rotate-90',
|
isExpanded && 'rotate-90',
|
||||||
)}
|
)}
|
||||||
onPress={() => {
|
onPress={toggleExpanded}
|
||||||
doToggleDirectoryExpansion(item.id, item.id)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<SvgMask src={FolderIcon} className="m-name-column-icon size-4 group-hover:hidden" />
|
<SvgMask src={FolderIcon} className="m-name-column-icon size-4 group-hover:hidden" />
|
||||||
<EditableSpan
|
<EditableSpan
|
||||||
data-testid="asset-row-name"
|
data-testid="asset-row-name"
|
||||||
editable={rowState.isEditingName}
|
editable={rowState.isEditingName}
|
||||||
className={tailwindMerge.twMerge(
|
className={tailwindMerge.twJoin(
|
||||||
'grow cursor-pointer bg-transparent font-naming',
|
'grow cursor-pointer bg-transparent font-naming',
|
||||||
rowState.isEditingName ? 'cursor-text' : 'cursor-pointer',
|
rowState.isEditingName ? 'cursor-text' : 'cursor-pointer',
|
||||||
)}
|
)}
|
||||||
checkSubmittable={(newTitle) =>
|
checkSubmittable={checkSubmittable}
|
||||||
validation.DIRECTORY_NAME_REGEX.test(newTitle) &&
|
|
||||||
backendModule.isNewTitleValid(
|
|
||||||
item,
|
|
||||||
newTitle,
|
|
||||||
nodeMap.current.get(item.parentId)?.children?.map((child) => child.item),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onSubmit={doRename}
|
onSubmit={doRename}
|
||||||
onCancel={() => {
|
onCancel={onCancel}
|
||||||
setIsEditing(false)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</EditableSpan>
|
</EditableSpan>
|
||||||
|
@ -20,6 +20,7 @@ import * as backendModule from '#/services/Backend'
|
|||||||
import { useBackendQuery } from '#/hooks/backendHooks'
|
import { useBackendQuery } from '#/hooks/backendHooks'
|
||||||
import * as tailwindMerge from '#/utilities/tailwindMerge'
|
import * as tailwindMerge from '#/utilities/tailwindMerge'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import { useEventCallback } from '#/hooks/eventCallbackHooks'
|
||||||
|
|
||||||
// =================
|
// =================
|
||||||
// === Constants ===
|
// === Constants ===
|
||||||
@ -137,15 +138,15 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const doOpenProject = () => {
|
const doOpenProject = useEventCallback(() => {
|
||||||
openProject({ ...item, type: backend.type })
|
openProject({ ...item, type: backend.type })
|
||||||
}
|
})
|
||||||
const doCloseProject = () => {
|
const doCloseProject = useEventCallback(() => {
|
||||||
closeProject({ ...item, type: backend.type })
|
closeProject({ ...item, type: backend.type })
|
||||||
}
|
})
|
||||||
const doOpenProjectTab = () => {
|
const doOpenProjectTab = useEventCallback(() => {
|
||||||
openProjectTab(item.id)
|
openProjectTab(item.id)
|
||||||
}
|
})
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case backendModule.ProjectState.new:
|
case backendModule.ProjectState.new:
|
||||||
|
@ -20,6 +20,7 @@ import ManageLabelsModal from '#/modals/ManageLabelsModal'
|
|||||||
|
|
||||||
import * as backendModule from '#/services/Backend'
|
import * as backendModule from '#/services/Backend'
|
||||||
|
|
||||||
|
import { useEventCallback } from '#/hooks/eventCallbackHooks'
|
||||||
import * as permissions from '#/utilities/permissions'
|
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.own ||
|
||||||
self?.permission === permissions.PermissionAction.admin)
|
self?.permission === permissions.PermissionAction.admin)
|
||||||
|
|
||||||
|
const onPress = useEventCallback(() => {})
|
||||||
|
|
||||||
|
const renderManageLabelsModal = useEventCallback(() => (
|
||||||
|
<ManageLabelsModal backend={backend} item={item} />
|
||||||
|
))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex items-center gap-column-items">
|
<div className="group flex items-center gap-column-items">
|
||||||
{(item.labels ?? [])
|
{(item.labels ?? [])
|
||||||
@ -93,7 +100,7 @@ export default function LabelsColumn(props: column.AssetColumnProps) {
|
|||||||
isDisabled
|
isDisabled
|
||||||
key={label}
|
key={label}
|
||||||
color={labelsByName.get(label)?.color ?? backendModule.COLORS[0]}
|
color={labelsByName.get(label)?.color ?? backendModule.COLORS[0]}
|
||||||
onPress={() => {}}
|
onPress={onPress}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
@ -101,7 +108,7 @@ export default function LabelsColumn(props: column.AssetColumnProps) {
|
|||||||
{managesThisAsset && (
|
{managesThisAsset && (
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Button variant="ghost" showIconOnHover icon={Plus2Icon} />
|
<Button variant="ghost" showIconOnHover icon={Plus2Icon} />
|
||||||
<ManageLabelsModal backend={backend} item={item} />
|
{renderManageLabelsModal}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -2574,7 +2574,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
|||||||
count: displayItems.length,
|
count: displayItems.length,
|
||||||
getScrollElement: () => rootRef.current,
|
getScrollElement: () => rootRef.current,
|
||||||
estimateSize: () => ROW_HEIGHT_PX,
|
estimateSize: () => ROW_HEIGHT_PX,
|
||||||
overscan: 20,
|
overscan: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
@ -2614,31 +2614,38 @@ export default function AssetsTable(props: AssetsTableProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AssetRow
|
<tr
|
||||||
key={item.key + item.path}
|
key={index}
|
||||||
rowHeight={virtualRow.size}
|
data-testid="asset-row"
|
||||||
rowOffset={virtualRow.start - index * virtualRow.size}
|
tabIndex={0}
|
||||||
isOpened={openedProjects.some(({ id }) => item.item.id === id)}
|
style={{
|
||||||
visibility={visibilities.get(item.key)}
|
maxHeight: virtualRow.size,
|
||||||
columns={columns}
|
transform: `translateY(${virtualRow.start - index * virtualRow.size}px)`,
|
||||||
id={item.item.id}
|
}}
|
||||||
parentId={item.directoryId}
|
>
|
||||||
path={item.path}
|
<AssetRow
|
||||||
initialAssetEvents={item.initialAssetEvents}
|
isOpened={openedProjects.some(({ id }) => item.item.id === id)}
|
||||||
depth={item.depth}
|
visibility={visibilities.get(item.key)}
|
||||||
state={state}
|
columns={columns}
|
||||||
hidden={hidden || visibilities.get(item.key) === Visibility.hidden}
|
id={item.item.id}
|
||||||
isKeyboardSelected={
|
parentId={item.directoryId}
|
||||||
keyboardSelectedIndex != null && item === visibleItems[keyboardSelectedIndex]
|
path={item.path}
|
||||||
}
|
initialAssetEvents={item.initialAssetEvents}
|
||||||
grabKeyboardFocus={grabRowKeyboardFocus}
|
depth={item.depth}
|
||||||
onClick={onRowClick}
|
state={state}
|
||||||
select={selectRow}
|
hidden={hidden || visibilities.get(item.key) === Visibility.hidden}
|
||||||
onDragStart={onRowDragStart}
|
isKeyboardSelected={
|
||||||
onDragOver={onRowDragOver}
|
keyboardSelectedIndex != null && item === visibleItems[keyboardSelectedIndex]
|
||||||
onDragEnd={onRowDragEnd}
|
}
|
||||||
onDrop={onRowDrop}
|
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">
|
<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}>
|
<tbody ref={bodyRef}>
|
||||||
{itemRows}
|
{itemRows}
|
||||||
<tr className="hidden h-row first:table-row">
|
<tr className="hidden h-row first:table-row">
|
||||||
@ -2792,6 +2799,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
|||||||
{...mergeProps<JSX.IntrinsicElements['div']>()(innerProps, {
|
{...mergeProps<JSX.IntrinsicElements['div']>()(innerProps, {
|
||||||
className: 'flex-1 overflow-auto container-size w-full h-full',
|
className: 'flex-1 overflow-auto container-size w-full h-full',
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
ref: rootRef,
|
||||||
onBlur: (event) => {
|
onBlur: (event) => {
|
||||||
if (
|
if (
|
||||||
event.relatedTarget instanceof HTMLElement &&
|
event.relatedTarget instanceof HTMLElement &&
|
||||||
|
Loading…
Reference in New Issue
Block a user