mirror of
https://github.com/enso-org/enso.git
synced 2025-01-03 12:34:06 +03:00
Move selected rows state of Data Catalog to zustand store (#10637)
- Eliminates lag when using drag-to-select (the `SelectionBrush`) by moving the state into a zustand store. - This avoids the lag because now the entire Data Catalog no longer has to rerender, because the state is no longer stored in the `AssetsTable` component that contains all the rows (and would therefore rerender all the rows when its state changes) # Important Notes - The lag is present on Chromium, but any lag in general is generally more visible on Firefox, so it's highly recommended to test on Firefox as well as Electron - On current develop, *any* drag selection should be enough to trigger the lag (typically 200ms JS + 200ms rendering). If it's not reproducible, then you may need to create more assets.
This commit is contained in:
parent
42ba5ee8a2
commit
636d0d11bf
@ -49,6 +49,7 @@ import * as backendHooks from '#/hooks/backendHooks'
|
||||
|
||||
import AuthProvider, * as authProvider from '#/providers/AuthProvider'
|
||||
import BackendProvider, { useLocalBackend, useRemoteBackend } from '#/providers/BackendProvider'
|
||||
import DriveProvider from '#/providers/DriveProvider'
|
||||
import DevtoolsProvider from '#/providers/EnsoDevtoolsProvider'
|
||||
import * as httpClientProvider from '#/providers/HttpClientProvider'
|
||||
import InputBindingsProvider from '#/providers/InputBindingsProvider'
|
||||
@ -549,6 +550,9 @@ function AppRouter(props: AppRouterProps) {
|
||||
{result}
|
||||
</rootComponent.Root>
|
||||
)
|
||||
// Ideally this would be in `Drive.tsx`, but it currently must be all the way out here
|
||||
// due to modals being in `TheModal`.
|
||||
result = <DriveProvider>{result}</DriveProvider>
|
||||
result = (
|
||||
<offlineNotificationManager.OfflineNotificationManager>
|
||||
{result}
|
||||
|
@ -1,14 +1,18 @@
|
||||
/** @file A table row for an arbitrary asset. */
|
||||
import * as React from 'react'
|
||||
|
||||
import { useStore } from 'zustand'
|
||||
|
||||
import BlankIcon from '#/assets/blank.svg'
|
||||
|
||||
import * as backendHooks from '#/hooks/backendHooks'
|
||||
import * as dragAndDropHooks from '#/hooks/dragAndDropHooks'
|
||||
import { useEventCallback } from '#/hooks/eventCallbackHooks'
|
||||
import * as setAssetHooks from '#/hooks/setAssetHooks'
|
||||
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
||||
|
||||
import * as authProvider from '#/providers/AuthProvider'
|
||||
import { useDriveStore, useSetSelectedKeys } from '#/providers/DriveProvider'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
@ -34,7 +38,6 @@ import * as localBackend from '#/services/LocalBackend'
|
||||
import * as projectManager from '#/services/ProjectManager'
|
||||
|
||||
import type * as assetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import * as dateTime from '#/utilities/dateTime'
|
||||
import * as download from '#/utilities/download'
|
||||
import * as drag from '#/utilities/drag'
|
||||
@ -79,29 +82,35 @@ export interface AssetRowProps
|
||||
readonly state: assetsTable.AssetsTableState
|
||||
readonly hidden: boolean
|
||||
readonly columns: columnUtils.Column[]
|
||||
readonly selected: boolean
|
||||
readonly setSelected: (selected: boolean) => void
|
||||
readonly isSoleSelected: boolean
|
||||
readonly isKeyboardSelected: boolean
|
||||
readonly grabKeyboardFocus: () => void
|
||||
readonly allowContextMenu: boolean
|
||||
readonly onClick: (props: AssetRowInnerProps, event: React.MouseEvent) => void
|
||||
readonly onContextMenu?: (
|
||||
props: AssetRowInnerProps,
|
||||
event: React.MouseEvent<HTMLTableRowElement>,
|
||||
) => void
|
||||
readonly select: () => void
|
||||
readonly updateAssetRef: React.Ref<(asset: backendModule.AnyAsset) => void>
|
||||
}
|
||||
|
||||
/** A row containing an {@link backendModule.AnyAsset}. */
|
||||
export default function AssetRow(props: AssetRowProps) {
|
||||
const { selected, isSoleSelected, isKeyboardSelected, isOpened } = props
|
||||
const { setSelected, allowContextMenu, onContextMenu, state, columns, onClick } = props
|
||||
const { isKeyboardSelected, isOpened, select, state, columns, onClick } = props
|
||||
const { item: rawItem, hidden: hiddenRaw, updateAssetRef, grabKeyboardFocus } = props
|
||||
const { nodeMap, setAssetPanelProps, doToggleDirectoryExpansion, doCopy, doCut, doPaste } = state
|
||||
const { setIsAssetPanelTemporarilyVisible, scrollContainerRef, rootDirectoryId, backend } = state
|
||||
const { visibilities } = state
|
||||
|
||||
const [item, setItem] = React.useState(rawItem)
|
||||
const driveStore = useDriveStore()
|
||||
const setSelectedKeys = useSetSelectedKeys()
|
||||
const selected = useStore(driveStore, ({ visuallySelectedKeys, selectedKeys }) =>
|
||||
(visuallySelectedKeys ?? selectedKeys).has(item.key),
|
||||
)
|
||||
const isSoleSelected = useStore(
|
||||
driveStore,
|
||||
({ selectedKeys }) => selected && selectedKeys.size === 1,
|
||||
)
|
||||
const allowContextMenu = useStore(
|
||||
driveStore,
|
||||
({ selectedKeys }) => selectedKeys.size === 0 || !selected || isSoleSelected,
|
||||
)
|
||||
const draggableProps = dragAndDropHooks.useDraggable()
|
||||
const { user } = authProvider.useNonPartialUserSession()
|
||||
const { setModal, unsetModal } = modalProvider.useSetModal()
|
||||
@ -110,7 +119,6 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
const dispatchAssetEvent = eventListProvider.useDispatchAssetEvent()
|
||||
const dispatchAssetListEvent = eventListProvider.useDispatchAssetListEvent()
|
||||
const [isDraggedOver, setIsDraggedOver] = React.useState(false)
|
||||
const [item, setItem] = React.useState(rawItem)
|
||||
const rootRef = React.useRef<HTMLElement | null>(null)
|
||||
const dragOverTimeoutHandle = React.useRef<number | null>(null)
|
||||
const grabKeyboardFocusRef = React.useRef(grabKeyboardFocus)
|
||||
@ -120,9 +128,8 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
const [rowState, setRowState] = React.useState<assetsTable.AssetRowState>(() =>
|
||||
object.merge(assetRowUtils.INITIAL_ROW_STATE, { setVisibility: setInsertionVisibility }),
|
||||
)
|
||||
const key = AssetTreeNode.getKey(item)
|
||||
const isCloud = backend.type === backendModule.BackendType.remote
|
||||
const outerVisibility = visibilities.get(key)
|
||||
const outerVisibility = visibilities.get(item.key)
|
||||
const visibility =
|
||||
outerVisibility == null || outerVisibility === Visibility.visible ?
|
||||
insertionVisibility
|
||||
@ -147,6 +154,11 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
const openProjectMutate = openProjectMutation.mutateAsync
|
||||
const closeProjectMutate = closeProjectMutation.mutateAsync
|
||||
|
||||
const setSelected = useEventCallback((newSelected: boolean) => {
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
setSelectedKeys(set.withPresence(selectedKeys, item.key, newSelected))
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
setItem(rawItem)
|
||||
}, [rawItem])
|
||||
@ -570,30 +582,30 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
break
|
||||
}
|
||||
case AssetEventType.temporarilyAddLabels: {
|
||||
const labels = event.ids.has(item.key) ? event.labelNames : set.EMPTY
|
||||
const labels = event.ids.has(item.key) ? event.labelNames : set.EMPTY_SET
|
||||
setRowState((oldRowState) =>
|
||||
(
|
||||
oldRowState.temporarilyAddedLabels === labels &&
|
||||
oldRowState.temporarilyRemovedLabels === set.EMPTY
|
||||
oldRowState.temporarilyRemovedLabels === set.EMPTY_SET
|
||||
) ?
|
||||
oldRowState
|
||||
: object.merge(oldRowState, {
|
||||
temporarilyAddedLabels: labels,
|
||||
temporarilyRemovedLabels: set.EMPTY,
|
||||
temporarilyRemovedLabels: set.EMPTY_SET,
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case AssetEventType.temporarilyRemoveLabels: {
|
||||
const labels = event.ids.has(item.key) ? event.labelNames : set.EMPTY
|
||||
const labels = event.ids.has(item.key) ? event.labelNames : set.EMPTY_SET
|
||||
setRowState((oldRowState) =>
|
||||
(
|
||||
oldRowState.temporarilyAddedLabels === set.EMPTY &&
|
||||
oldRowState.temporarilyAddedLabels === set.EMPTY_SET &&
|
||||
oldRowState.temporarilyRemovedLabels === labels
|
||||
) ?
|
||||
oldRowState
|
||||
: object.merge(oldRowState, {
|
||||
temporarilyAddedLabels: set.EMPTY,
|
||||
temporarilyAddedLabels: set.EMPTY_SET,
|
||||
temporarilyRemovedLabels: labels,
|
||||
}),
|
||||
)
|
||||
@ -601,9 +613,9 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
}
|
||||
case AssetEventType.addLabels: {
|
||||
setRowState((oldRowState) =>
|
||||
oldRowState.temporarilyAddedLabels === set.EMPTY ?
|
||||
oldRowState.temporarilyAddedLabels === set.EMPTY_SET ?
|
||||
oldRowState
|
||||
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY }),
|
||||
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY_SET }),
|
||||
)
|
||||
const labels = asset.labels
|
||||
if (
|
||||
@ -626,9 +638,9 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
}
|
||||
case AssetEventType.removeLabels: {
|
||||
setRowState((oldRowState) =>
|
||||
oldRowState.temporarilyAddedLabels === set.EMPTY ?
|
||||
oldRowState.temporarilyAddedLabels === set.EMPTY_SET ?
|
||||
oldRowState
|
||||
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY }),
|
||||
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY_SET }),
|
||||
)
|
||||
const labels = asset.labels
|
||||
if (
|
||||
@ -677,9 +689,9 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
const clearDragState = React.useCallback(() => {
|
||||
setIsDraggedOver(false)
|
||||
setRowState((oldRowState) =>
|
||||
oldRowState.temporarilyAddedLabels === set.EMPTY ?
|
||||
oldRowState.temporarilyAddedLabels === set.EMPTY_SET ?
|
||||
oldRowState
|
||||
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY }),
|
||||
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY_SET }),
|
||||
)
|
||||
}, [])
|
||||
|
||||
@ -707,7 +719,14 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
case backendModule.AssetType.file:
|
||||
case backendModule.AssetType.datalink:
|
||||
case backendModule.AssetType.secret: {
|
||||
const innerProps: AssetRowInnerProps = { key, item, setItem, state, rowState, setRowState }
|
||||
const innerProps: AssetRowInnerProps = {
|
||||
key: item.key,
|
||||
item,
|
||||
setItem,
|
||||
state,
|
||||
rowState,
|
||||
setRowState,
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{!hidden && (
|
||||
@ -758,7 +777,9 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
if (allowContextMenu) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
onContextMenu?.(innerProps, event)
|
||||
if (!selected) {
|
||||
select()
|
||||
}
|
||||
setModal(
|
||||
<AssetContextMenu
|
||||
innerProps={innerProps}
|
||||
@ -774,8 +795,6 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
doTriggerDescriptionEdit={doTriggerDescriptionEdit}
|
||||
/>,
|
||||
)
|
||||
} else {
|
||||
onContextMenu?.(innerProps, event)
|
||||
}
|
||||
}}
|
||||
onDragStart={(event) => {
|
||||
@ -872,7 +891,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
return (
|
||||
<td key={column} className={columnUtils.COLUMN_CSS_CLASS[column]}>
|
||||
<Render
|
||||
keyProp={key}
|
||||
keyProp={item.key}
|
||||
isOpened={isOpened}
|
||||
backendType={backend.type}
|
||||
item={item}
|
||||
@ -898,7 +917,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
<AssetContextMenu
|
||||
hidden
|
||||
innerProps={{
|
||||
key,
|
||||
key: item.key,
|
||||
item,
|
||||
setItem,
|
||||
state,
|
||||
|
@ -9,6 +9,6 @@ export const INITIAL_ROW_STATE: assetsTable.AssetRowState = Object.freeze({
|
||||
// Ignored. This MUST be replaced by the row component. It should also update `visibility`.
|
||||
},
|
||||
isEditingName: false,
|
||||
temporarilyAddedLabels: set.EMPTY,
|
||||
temporarilyRemovedLabels: set.EMPTY,
|
||||
temporarilyAddedLabels: set.EMPTY_SET,
|
||||
temporarilyRemovedLabels: set.EMPTY_SET,
|
||||
})
|
||||
|
@ -8,6 +8,7 @@ import * as backendHooks from '#/hooks/backendHooks'
|
||||
import * as setAssetHooks from '#/hooks/setAssetHooks'
|
||||
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
||||
|
||||
import { useDriveStore } from '#/providers/DriveProvider'
|
||||
import * as inputBindingsProvider from '#/providers/InputBindingsProvider'
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
@ -43,11 +44,12 @@ export interface DirectoryNameColumnProps extends column.AssetColumnProps {}
|
||||
* This should never happen. */
|
||||
export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
||||
const { item, setItem, selected, state, rowState, setRowState, isEditable } = props
|
||||
const { backend, selectedKeys, nodeMap } = state
|
||||
const { backend, nodeMap } = state
|
||||
const { doToggleDirectoryExpansion } = state
|
||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||
const { getText } = textProvider.useText()
|
||||
const inputBindings = inputBindingsProvider.useInputBindings()
|
||||
const driveStore = useDriveStore()
|
||||
const dispatchAssetListEvent = eventListProvider.useDispatchAssetListEvent()
|
||||
if (item.type !== backendModule.AssetType.directory) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
@ -165,7 +167,7 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
||||
} else if (
|
||||
eventModule.isSingleClick(event) &&
|
||||
selected &&
|
||||
selectedKeys.current.size === 1
|
||||
driveStore.getState().selectedKeys.size === 1
|
||||
) {
|
||||
event.stopPropagation()
|
||||
setIsEditing(true)
|
||||
|
@ -11,6 +11,7 @@ import * as setAssetHooks from '#/hooks/setAssetHooks'
|
||||
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
||||
|
||||
import * as authProvider from '#/providers/AuthProvider'
|
||||
import { useDriveStore } from '#/providers/DriveProvider'
|
||||
import * as inputBindingsProvider from '#/providers/InputBindingsProvider'
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
@ -59,13 +60,14 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
backendType,
|
||||
isOpened,
|
||||
} = props
|
||||
const { backend, selectedKeys, nodeMap } = state
|
||||
const { backend, nodeMap } = state
|
||||
const client = reactQuery.useQueryClient()
|
||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||
const { user } = authProvider.useNonPartialUserSession()
|
||||
const { getText } = textProvider.useText()
|
||||
const inputBindings = inputBindingsProvider.useInputBindings()
|
||||
const dispatchAssetListEvent = eventListProvider.useDispatchAssetListEvent()
|
||||
const driveStore = useDriveStore()
|
||||
const doOpenProject = projectHooks.useOpenProject()
|
||||
|
||||
if (item.type !== backendModule.AssetType.project) {
|
||||
@ -321,7 +323,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
!isRunning &&
|
||||
eventModule.isSingleClick(event) &&
|
||||
selected &&
|
||||
selectedKeys.current.size === 1
|
||||
driveStore.getState().selectedKeys.size === 1
|
||||
) {
|
||||
setIsEditing(true)
|
||||
} else if (eventModule.isDoubleClick(event)) {
|
||||
|
@ -16,6 +16,12 @@ import useOnScroll from '#/hooks/useOnScroll'
|
||||
|
||||
import * as authProvider from '#/providers/AuthProvider'
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
import {
|
||||
useDriveStore,
|
||||
useSetCanDownload,
|
||||
useSetSelectedKeys,
|
||||
useSetVisuallySelectedKeys,
|
||||
} from '#/providers/DriveProvider'
|
||||
import * as inputBindingsProvider from '#/providers/InputBindingsProvider'
|
||||
import * as localStorageProvider from '#/providers/LocalStorageProvider'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
@ -310,7 +316,6 @@ const CATEGORY_TO_FILTER_BY: Readonly<Record<Category, backendModule.FilterBy |
|
||||
export interface AssetsTableState {
|
||||
readonly backend: Backend
|
||||
readonly rootDirectoryId: backendModule.DirectoryId
|
||||
readonly selectedKeys: React.MutableRefObject<ReadonlySet<backendModule.AssetId>>
|
||||
readonly scrollContainerRef: React.RefObject<HTMLElement>
|
||||
readonly visibilities: ReadonlyMap<backendModule.AssetId, Visibility>
|
||||
readonly category: Category
|
||||
@ -356,7 +361,6 @@ export interface AssetsTableProps {
|
||||
readonly setSuggestions: React.Dispatch<
|
||||
React.SetStateAction<readonly assetSearchBar.Suggestion[]>
|
||||
>
|
||||
readonly setCanDownload: (canDownload: boolean) => void
|
||||
readonly category: Category
|
||||
readonly initialProjectName: string | null
|
||||
readonly setAssetPanelProps: (props: assetPanel.AssetPanelRequiredProps | null) => void
|
||||
@ -375,12 +379,13 @@ export interface AssetManagementApi {
|
||||
|
||||
/** The table of project assets. */
|
||||
export default function AssetsTable(props: AssetsTableProps) {
|
||||
const { hidden, query, setQuery, setCanDownload, category, assetManagementApiRef } = props
|
||||
const { hidden, query, setQuery, category, assetManagementApiRef } = props
|
||||
const { setSuggestions, initialProjectName } = props
|
||||
const { setAssetPanelProps, targetDirectoryNodeRef, setIsAssetPanelTemporarilyVisible } = props
|
||||
|
||||
const openedProjects = projectsProvider.useLaunchedProjects()
|
||||
const doOpenProject = projectHooks.useOpenProject()
|
||||
const setCanDownload = useSetCanDownload()
|
||||
|
||||
const { user } = authProvider.useNonPartialUserSession()
|
||||
const backend = backendProvider.useBackend(category)
|
||||
@ -400,10 +405,9 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const [enabledColumns, setEnabledColumns] = React.useState(columnUtils.DEFAULT_ENABLED_COLUMNS)
|
||||
const [sortInfo, setSortInfo] =
|
||||
React.useState<sorting.SortInfo<columnUtils.SortableColumn> | null>(null)
|
||||
const [selectedKeys, setSelectedKeysRaw] = React.useState<ReadonlySet<backendModule.AssetId>>(
|
||||
() => new Set(),
|
||||
)
|
||||
const selectedKeysRef = React.useRef(selectedKeys)
|
||||
const driveStore = useDriveStore()
|
||||
const setSelectedKeys = useSetSelectedKeys()
|
||||
const setVisuallySelectedKeys = useSetVisuallySelectedKeys()
|
||||
const updateAssetRef = React.useRef<
|
||||
Record<backendModule.AnyAsset['id'], (asset: backendModule.AnyAsset) => void>
|
||||
>({})
|
||||
@ -637,6 +641,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
if (selectedKeys.size === 0) {
|
||||
targetDirectoryNodeRef.current = null
|
||||
} else if (selectedKeys.size === 1) {
|
||||
@ -676,7 +681,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
targetDirectoryNodeRef.current = node
|
||||
}
|
||||
}
|
||||
}, [targetDirectoryNodeRef, selectedKeys])
|
||||
}, [driveStore, targetDirectoryNodeRef])
|
||||
|
||||
React.useEffect(() => {
|
||||
const nodeToSuggestion = (
|
||||
@ -881,39 +886,37 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
}
|
||||
}, [hidden, inputBindings, dispatchAssetEvent])
|
||||
|
||||
const setSelectedKeys = React.useCallback(
|
||||
(newSelectedKeys: ReadonlySet<backendModule.AssetId>) => {
|
||||
selectedKeysRef.current = newSelectedKeys
|
||||
setSelectedKeysRaw(newSelectedKeys)
|
||||
if (!isCloud) {
|
||||
setCanDownload(
|
||||
newSelectedKeys.size !== 0 &&
|
||||
Array.from(newSelectedKeys).every((key) => {
|
||||
React.useEffect(
|
||||
() =>
|
||||
driveStore.subscribe(({ selectedKeys }) => {
|
||||
let newCanDownload: boolean
|
||||
if (!isCloud) {
|
||||
newCanDownload =
|
||||
selectedKeys.size !== 0 &&
|
||||
Array.from(selectedKeys).every((key) => {
|
||||
const node = nodeMapRef.current.get(key)
|
||||
return node?.item.type === backendModule.AssetType.project
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
setCanDownload(
|
||||
newSelectedKeys.size !== 0 &&
|
||||
Array.from(newSelectedKeys).every((key) => {
|
||||
})
|
||||
} else {
|
||||
newCanDownload =
|
||||
selectedKeys.size !== 0 &&
|
||||
Array.from(selectedKeys).every((key) => {
|
||||
const node = nodeMapRef.current.get(key)
|
||||
return (
|
||||
node?.item.type === backendModule.AssetType.project ||
|
||||
node?.item.type === backendModule.AssetType.file ||
|
||||
node?.item.type === backendModule.AssetType.datalink
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
[isCloud, setCanDownload],
|
||||
})
|
||||
}
|
||||
const currentCanDownload = driveStore.getState().canDownload
|
||||
if (currentCanDownload !== newCanDownload) {
|
||||
setCanDownload(newCanDownload)
|
||||
}
|
||||
}),
|
||||
[driveStore, isCloud, setCanDownload],
|
||||
)
|
||||
|
||||
const clearSelectedKeys = React.useCallback(() => {
|
||||
setSelectedKeys(new Set())
|
||||
}, [setSelectedKeys])
|
||||
|
||||
const overwriteNodes = React.useCallback(
|
||||
(newAssets: backendModule.AnyAsset[]) => {
|
||||
setInitialized(true)
|
||||
@ -1050,12 +1053,16 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
}
|
||||
}, [enabledColumns, initialized, localStorage])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedKeysRef.current.size !== 1) {
|
||||
setAssetPanelProps(null)
|
||||
setIsAssetPanelTemporarilyVisible(false)
|
||||
}
|
||||
}, [selectedKeysRef.current.size, setAssetPanelProps, setIsAssetPanelTemporarilyVisible])
|
||||
React.useEffect(
|
||||
() =>
|
||||
driveStore.subscribe(({ selectedKeys }) => {
|
||||
if (selectedKeys.size !== 1) {
|
||||
setAssetPanelProps(null)
|
||||
setIsAssetPanelTemporarilyVisible(false)
|
||||
}
|
||||
}),
|
||||
[driveStore, setAssetPanelProps, setIsAssetPanelTemporarilyVisible],
|
||||
)
|
||||
|
||||
const directoryListAbortControllersRef = React.useRef(
|
||||
new Map<backendModule.DirectoryId, AbortController>(),
|
||||
@ -1211,14 +1218,15 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
// This is not a React component, even though it contains JSX.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const onKeyDown = (event: React.KeyboardEvent) => {
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
const prevIndex = mostRecentlySelectedIndexRef.current
|
||||
const item = prevIndex == null ? null : visibleItems[prevIndex]
|
||||
if (selectedKeysRef.current.size === 1 && item != null) {
|
||||
if (selectedKeys.size === 1 && item != null) {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
case ' ': {
|
||||
if (event.key === ' ' && event.ctrlKey) {
|
||||
const keys = selectedKeysRef.current
|
||||
const keys = selectedKeys
|
||||
setSelectedKeys(set.withPresence(keys, item.key, !keys.has(item.key)))
|
||||
} else {
|
||||
switch (item.type) {
|
||||
@ -1311,7 +1319,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
switch (event.key) {
|
||||
case ' ': {
|
||||
if (event.ctrlKey && item != null) {
|
||||
const keys = selectedKeysRef.current
|
||||
const keys = selectedKeys
|
||||
setSelectedKeys(set.withPresence(keys, item.key, !keys.has(item.key)))
|
||||
}
|
||||
break
|
||||
@ -1754,8 +1762,9 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
break
|
||||
}
|
||||
case AssetListEventType.willDelete: {
|
||||
if (selectedKeysRef.current.has(event.key)) {
|
||||
const newSelectedKeys = new Set(selectedKeysRef.current)
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
if (selectedKeys.has(event.key)) {
|
||||
const newSelectedKeys = new Set(selectedKeys)
|
||||
newSelectedKeys.delete(event.key)
|
||||
setSelectedKeys(newSelectedKeys)
|
||||
}
|
||||
@ -1828,18 +1837,20 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
|
||||
const doCopy = React.useCallback(() => {
|
||||
unsetModal()
|
||||
setPasteData({ type: PasteType.copy, data: selectedKeysRef.current })
|
||||
}, [unsetModal])
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
setPasteData({ type: PasteType.copy, data: selectedKeys })
|
||||
}, [driveStore, unsetModal])
|
||||
|
||||
const doCut = React.useCallback(() => {
|
||||
unsetModal()
|
||||
if (pasteData != null) {
|
||||
dispatchAssetEvent({ type: AssetEventType.cancelCut, ids: pasteData.data })
|
||||
}
|
||||
setPasteData({ type: PasteType.move, data: selectedKeysRef.current })
|
||||
dispatchAssetEvent({ type: AssetEventType.cut, ids: selectedKeysRef.current })
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
setPasteData({ type: PasteType.move, data: selectedKeys })
|
||||
dispatchAssetEvent({ type: AssetEventType.cut, ids: selectedKeys })
|
||||
setSelectedKeys(new Set())
|
||||
}, [pasteData, setSelectedKeys, unsetModal, dispatchAssetEvent])
|
||||
}, [unsetModal, pasteData, driveStore, dispatchAssetEvent, setSelectedKeys])
|
||||
|
||||
const doPaste = React.useCallback(
|
||||
(newParentKey: backendModule.DirectoryId, newParentId: backendModule.DirectoryId) => {
|
||||
@ -1885,8 +1896,6 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
backend={backend}
|
||||
category={category}
|
||||
pasteData={pasteData}
|
||||
selectedKeys={selectedKeys}
|
||||
clearSelectedKeys={clearSelectedKeys}
|
||||
nodeMapRef={nodeMapRef}
|
||||
rootDirectoryId={rootDirectoryId}
|
||||
event={{ pageX: 0, pageY: 0 }}
|
||||
@ -1895,17 +1904,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
doPaste={doPaste}
|
||||
/>
|
||||
),
|
||||
[
|
||||
backend,
|
||||
rootDirectoryId,
|
||||
category,
|
||||
selectedKeys,
|
||||
pasteData,
|
||||
doCopy,
|
||||
doCut,
|
||||
doPaste,
|
||||
clearSelectedKeys,
|
||||
],
|
||||
[backend, rootDirectoryId, category, pasteData, doCopy, doCut, doPaste],
|
||||
)
|
||||
|
||||
const onDropzoneDragOver = (event: React.DragEvent<Element>) => {
|
||||
@ -1944,7 +1943,6 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
backend,
|
||||
rootDirectoryId,
|
||||
visibilities,
|
||||
selectedKeys: selectedKeysRef,
|
||||
scrollContainerRef: rootRef,
|
||||
category,
|
||||
hasPasteData: pasteData != null,
|
||||
@ -2014,15 +2012,16 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
selectAdditional: () => {},
|
||||
selectAdditionalRange: () => {},
|
||||
[inputBindingsModule.DEFAULT_HANDLER]: () => {
|
||||
if (selectedKeysRef.current.size !== 0) {
|
||||
setSelectedKeys(new Set())
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
if (selectedKeys.size !== 0) {
|
||||
setSelectedKeys(set.EMPTY_SET)
|
||||
setMostRecentlySelectedIndex(null)
|
||||
}
|
||||
},
|
||||
},
|
||||
false,
|
||||
),
|
||||
[setSelectedKeys, inputBindings, setMostRecentlySelectedIndex],
|
||||
[setSelectedKeys, inputBindings, setMostRecentlySelectedIndex, driveStore],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -2057,13 +2056,15 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
result = new Set(getRange())
|
||||
},
|
||||
selectAdditionalRange: () => {
|
||||
result = new Set([...selectedKeysRef.current, ...getRange()])
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
result = new Set([...selectedKeys, ...getRange()])
|
||||
},
|
||||
selectAdditional: () => {
|
||||
const newSelectedKeys = new Set(selectedKeysRef.current)
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
const newSelectedKeys = new Set(selectedKeys)
|
||||
let count = 0
|
||||
for (const key of keys) {
|
||||
if (selectedKeysRef.current.has(key)) {
|
||||
if (selectedKeys.has(key)) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
@ -2079,13 +2080,9 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
})(event, false)
|
||||
return result
|
||||
},
|
||||
[inputBindings],
|
||||
[driveStore, inputBindings],
|
||||
)
|
||||
|
||||
// Only non-`null` when it is different to`selectedKeys`.
|
||||
const [visuallySelectedKeysOverride, setVisuallySelectedKeysOverride] =
|
||||
React.useState<ReadonlySet<backendModule.AssetId> | null>(null)
|
||||
|
||||
const { startAutoScroll, endAutoScroll, onMouseEvent } = autoScrollHooks.useAutoScroll(rootRef)
|
||||
|
||||
const dragSelectionChangeLoopHandle = React.useRef(0)
|
||||
@ -2129,14 +2126,14 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
}
|
||||
}
|
||||
if (range == null) {
|
||||
setVisuallySelectedKeysOverride(null)
|
||||
setVisuallySelectedKeys(null)
|
||||
} else {
|
||||
const keys = displayItems.slice(range.start, range.end).map((node) => node.key)
|
||||
setVisuallySelectedKeysOverride(calculateNewKeys(event, keys, () => []))
|
||||
setVisuallySelectedKeys(calculateNewKeys(event, keys, () => []))
|
||||
}
|
||||
}
|
||||
},
|
||||
[startAutoScroll, onMouseEvent, displayItems, calculateNewKeys],
|
||||
[startAutoScroll, onMouseEvent, setVisuallySelectedKeys, displayItems, calculateNewKeys],
|
||||
)
|
||||
|
||||
const onSelectionDragEnd = React.useCallback(
|
||||
@ -2148,24 +2145,29 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const keys = displayItems.slice(range.start, range.end).map((node) => node.key)
|
||||
setSelectedKeys(calculateNewKeys(event, keys, () => []))
|
||||
}
|
||||
setVisuallySelectedKeysOverride(null)
|
||||
setVisuallySelectedKeys(null)
|
||||
dragSelectionRangeRef.current = null
|
||||
},
|
||||
[endAutoScroll, onMouseEvent, displayItems, setSelectedKeys, calculateNewKeys],
|
||||
[
|
||||
endAutoScroll,
|
||||
onMouseEvent,
|
||||
setVisuallySelectedKeys,
|
||||
displayItems,
|
||||
setSelectedKeys,
|
||||
calculateNewKeys,
|
||||
],
|
||||
)
|
||||
|
||||
const onSelectionDragCancel = React.useCallback(() => {
|
||||
setVisuallySelectedKeysOverride(null)
|
||||
setVisuallySelectedKeys(null)
|
||||
dragSelectionRangeRef.current = null
|
||||
}, [])
|
||||
}, [setVisuallySelectedKeys])
|
||||
|
||||
const onRowClick = React.useCallback(
|
||||
(innerRowProps: assetRow.AssetRowInnerProps, event: React.MouseEvent) => {
|
||||
const { key } = innerRowProps
|
||||
event.stopPropagation()
|
||||
const newIndex = visibleItems.findIndex(
|
||||
(innerItem) => AssetTreeNode.getKey(innerItem) === key,
|
||||
)
|
||||
const newIndex = visibleItems.findIndex((innerItem) => innerItem.key === key)
|
||||
const getRange = () => {
|
||||
if (mostRecentlySelectedIndexRef.current == null) {
|
||||
return [key]
|
||||
@ -2174,7 +2176,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const index2 = newIndex
|
||||
const startIndex = Math.min(index1, index2)
|
||||
const endIndex = Math.max(index1, index2) + 1
|
||||
return visibleItems.slice(startIndex, endIndex).map(AssetTreeNode.getKey)
|
||||
return visibleItems.slice(startIndex, endIndex).map((innerItem) => innerItem.key)
|
||||
}
|
||||
}
|
||||
setSelectedKeys(calculateNewKeys(event, [key], getRange))
|
||||
@ -2233,13 +2235,9 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
</td>
|
||||
</tr>
|
||||
: displayItems.map((item, i) => {
|
||||
const key = AssetTreeNode.getKey(item)
|
||||
const isSelected = (visuallySelectedKeysOverride ?? selectedKeys).has(key)
|
||||
const isSoleSelected = isSelected && selectedKeys.size === 1
|
||||
|
||||
return (
|
||||
<AssetRow
|
||||
key={key}
|
||||
key={item.key}
|
||||
updateAssetRef={(instance) => {
|
||||
if (instance != null) {
|
||||
updateAssetRef.current[item.item.id] = instance
|
||||
@ -2255,37 +2253,27 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
item={item}
|
||||
state={state}
|
||||
hidden={hidden || visibilities.get(item.key) === Visibility.hidden}
|
||||
selected={isSelected}
|
||||
setSelected={(selected) => {
|
||||
setSelectedKeys(set.withPresence(selectedKeysRef.current, key, selected))
|
||||
}}
|
||||
isSoleSelected={isSoleSelected}
|
||||
isKeyboardSelected={
|
||||
keyboardSelectedIndex != null && item === visibleItems[keyboardSelectedIndex]
|
||||
}
|
||||
grabKeyboardFocus={() => {
|
||||
setSelectedKeys(new Set([key]))
|
||||
setSelectedKeys(new Set([item.key]))
|
||||
setMostRecentlySelectedIndex(i, true)
|
||||
}}
|
||||
allowContextMenu={selectedKeysRef.current.size === 0 || !isSelected || isSoleSelected}
|
||||
onClick={onRowClick}
|
||||
onContextMenu={(_innerProps, event) => {
|
||||
if (!isSelected) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setMostRecentlySelectedIndex(visibleItems.indexOf(item))
|
||||
selectionStartIndexRef.current = null
|
||||
setSelectedKeys(new Set([key]))
|
||||
}
|
||||
select={() => {
|
||||
setMostRecentlySelectedIndex(visibleItems.indexOf(item))
|
||||
selectionStartIndexRef.current = null
|
||||
setSelectedKeys(new Set([item.key]))
|
||||
}}
|
||||
onDragStart={(event) => {
|
||||
startAutoScroll()
|
||||
onMouseEvent(event)
|
||||
let newSelectedKeys = selectedKeysRef.current
|
||||
if (!newSelectedKeys.has(key)) {
|
||||
let newSelectedKeys = driveStore.getState().selectedKeys
|
||||
if (!newSelectedKeys.has(item.key)) {
|
||||
setMostRecentlySelectedIndex(visibleItems.indexOf(item))
|
||||
selectionStartIndexRef.current = null
|
||||
newSelectedKeys = new Set([key])
|
||||
newSelectedKeys = new Set([item.key])
|
||||
setSelectedKeys(newSelectedKeys)
|
||||
}
|
||||
const nodes = assetTree
|
||||
@ -2337,8 +2325,8 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
if (payload != null) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const idsReference =
|
||||
selectedKeysRef.current.has(key) ? selectedKeysRef.current : key
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
const idsReference = selectedKeys.has(item.key) ? selectedKeys : item.key
|
||||
// This optimization is required in order to avoid severe lag on Firefox.
|
||||
if (idsReference !== lastSelectedIdsRef.current) {
|
||||
lastSelectedIdsRef.current = idsReference
|
||||
@ -2372,17 +2360,17 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
onDragEnd={() => {
|
||||
endAutoScroll()
|
||||
lastSelectedIdsRef.current = null
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.temporarilyAddLabels,
|
||||
ids: selectedKeysRef.current,
|
||||
labelNames: set.EMPTY,
|
||||
ids: selectedKeys,
|
||||
labelNames: set.EMPTY_SET,
|
||||
})
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
endAutoScroll()
|
||||
const ids = new Set(
|
||||
selectedKeysRef.current.has(key) ? selectedKeysRef.current : [key],
|
||||
)
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
const ids = new Set(selectedKeys.has(item.key) ? selectedKeys : [item.key])
|
||||
const payload = drag.LABELS.lookup(event)
|
||||
if (payload != null) {
|
||||
event.preventDefault()
|
||||
@ -2408,7 +2396,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.temporarilyAddLabels,
|
||||
ids,
|
||||
labelNames: set.EMPTY,
|
||||
labelNames: set.EMPTY_SET,
|
||||
})
|
||||
}
|
||||
}}
|
||||
@ -2434,8 +2422,6 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
backend={backend}
|
||||
category={category}
|
||||
pasteData={pasteData}
|
||||
selectedKeys={selectedKeys}
|
||||
clearSelectedKeys={clearSelectedKeys}
|
||||
nodeMapRef={nodeMapRef}
|
||||
event={event}
|
||||
rootDirectoryId={rootDirectoryId}
|
||||
@ -2453,10 +2439,11 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
!event.currentTarget.contains(event.relatedTarget)
|
||||
) {
|
||||
lastSelectedIdsRef.current = null
|
||||
const { selectedKeys } = driveStore.getState()
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.temporarilyAddLabels,
|
||||
ids: selectedKeysRef.current,
|
||||
labelNames: set.EMPTY,
|
||||
ids: selectedKeys,
|
||||
labelNames: set.EMPTY_SET,
|
||||
})
|
||||
}
|
||||
}}
|
||||
|
@ -3,6 +3,7 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import * as authProvider from '#/providers/AuthProvider'
|
||||
import { useSelectedKeys, useSetSelectedKeys } from '#/providers/DriveProvider'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
@ -24,6 +25,7 @@ import * as backendModule from '#/services/Backend'
|
||||
import type * as assetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import type * as pasteDataModule from '#/utilities/pasteData'
|
||||
import * as permissions from '#/utilities/permissions'
|
||||
import { EMPTY_SET } from '#/utilities/set'
|
||||
import * as uniqueString from '#/utilities/uniqueString'
|
||||
|
||||
// =================
|
||||
@ -37,8 +39,6 @@ export interface AssetsTableContextMenuProps {
|
||||
readonly category: Category
|
||||
readonly rootDirectoryId: backendModule.DirectoryId
|
||||
readonly pasteData: pasteDataModule.PasteData<ReadonlySet<backendModule.AssetId>> | null
|
||||
readonly selectedKeys: ReadonlySet<backendModule.AssetId>
|
||||
readonly clearSelectedKeys: () => void
|
||||
readonly nodeMapRef: React.MutableRefObject<
|
||||
ReadonlyMap<backendModule.AssetId, assetTreeNode.AnyAssetTreeNode>
|
||||
>
|
||||
@ -54,7 +54,7 @@ export interface AssetsTableContextMenuProps {
|
||||
/** A context menu for an `AssetsTable`, when no row is selected, or multiple rows
|
||||
* are selected. */
|
||||
export default function AssetsTableContextMenu(props: AssetsTableContextMenuProps) {
|
||||
const { hidden = false, backend, category, pasteData, selectedKeys, clearSelectedKeys } = props
|
||||
const { hidden = false, backend, category, pasteData } = props
|
||||
const { nodeMapRef, event, rootDirectoryId } = props
|
||||
const { doCopy, doCut, doPaste } = props
|
||||
const { user } = authProvider.useNonPartialUserSession()
|
||||
@ -62,6 +62,8 @@ export default function AssetsTableContextMenu(props: AssetsTableContextMenuProp
|
||||
const { getText } = textProvider.useText()
|
||||
const dispatchAssetEvent = eventListProvider.useDispatchAssetEvent()
|
||||
const isCloud = categoryModule.isCloud(category)
|
||||
const selectedKeys = useSelectedKeys()
|
||||
const setSelectedKeys = useSetSelectedKeys()
|
||||
|
||||
// This works because all items are mutated, ensuring their value stays
|
||||
// up to date.
|
||||
@ -93,7 +95,7 @@ export default function AssetsTableContextMenu(props: AssetsTableContextMenuProp
|
||||
: getText('deleteSelectedAssetsActionText', selectedKeys.size)
|
||||
}
|
||||
doDelete={() => {
|
||||
clearSelectedKeys()
|
||||
setSelectedKeys(EMPTY_SET)
|
||||
dispatchAssetEvent({ type: AssetEventType.delete, ids: selectedKeys })
|
||||
}}
|
||||
/>,
|
||||
@ -134,7 +136,7 @@ export default function AssetsTableContextMenu(props: AssetsTableContextMenuProp
|
||||
: getText('deleteSelectedAssetsForeverActionText', selectedKeys.size)
|
||||
}
|
||||
doDelete={() => {
|
||||
clearSelectedKeys()
|
||||
setSelectedKeys(EMPTY_SET)
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.deleteForever,
|
||||
ids: selectedKeys,
|
||||
|
@ -81,7 +81,6 @@ export default function Drive(props: DriveProps) {
|
||||
const dispatchAssetListEvent = eventListProvider.useDispatchAssetListEvent()
|
||||
const [query, setQuery] = React.useState(() => AssetQuery.fromString(''))
|
||||
const [suggestions, setSuggestions] = React.useState<readonly assetSearchBar.Suggestion[]>([])
|
||||
const [canDownload, setCanDownload] = React.useState(false)
|
||||
const [didLoadingProjectManagerFail, setDidLoadingProjectManagerFail] = React.useState(false)
|
||||
const [assetPanelPropsRaw, setAssetPanelProps] =
|
||||
React.useState<assetPanel.AssetPanelRequiredProps | null>(null)
|
||||
@ -262,7 +261,6 @@ export default function Drive(props: DriveProps) {
|
||||
setQuery={setQuery}
|
||||
suggestions={suggestions}
|
||||
category={category}
|
||||
canDownload={canDownload}
|
||||
isAssetPanelOpen={isAssetPanelVisible}
|
||||
setIsAssetPanelOpen={(valueOrUpdater) => {
|
||||
const newValue =
|
||||
@ -318,7 +316,6 @@ export default function Drive(props: DriveProps) {
|
||||
hidden={hidden}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
setCanDownload={setCanDownload}
|
||||
category={category}
|
||||
setSuggestions={setSuggestions}
|
||||
initialProjectName={initialProjectName}
|
||||
|
@ -15,6 +15,7 @@ import RightPanelIcon from '#/assets/right_panel.svg'
|
||||
import * as offlineHooks from '#/hooks/offlineHooks'
|
||||
import { createGetProjectDetailsQuery } from '#/hooks/projectHooks'
|
||||
|
||||
import { useCanDownload } from '#/providers/DriveProvider'
|
||||
import * as inputBindingsProvider from '#/providers/InputBindingsProvider'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
@ -51,7 +52,6 @@ export interface DriveBarProps {
|
||||
readonly setQuery: React.Dispatch<React.SetStateAction<AssetQuery>>
|
||||
readonly suggestions: readonly assetSearchBar.Suggestion[]
|
||||
readonly category: Category
|
||||
readonly canDownload: boolean
|
||||
readonly isAssetPanelOpen: boolean
|
||||
readonly setIsAssetPanelOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
readonly doEmptyTrash: () => void
|
||||
@ -70,7 +70,7 @@ export interface DriveBarProps {
|
||||
/** Displays the current directory path and permissions, upload and download buttons,
|
||||
* and a column display mode switcher. */
|
||||
export default function DriveBar(props: DriveBarProps) {
|
||||
const { backend, query, setQuery, suggestions, category, canDownload } = props
|
||||
const { backend, query, setQuery, suggestions, category } = props
|
||||
const { doEmptyTrash, doCreateProject, doCreateDirectory } = props
|
||||
const { doCreateSecret, doCreateDatalink, doUploadFiles } = props
|
||||
const { isAssetPanelOpen, setIsAssetPanelOpen } = props
|
||||
@ -81,6 +81,7 @@ export default function DriveBar(props: DriveBarProps) {
|
||||
const uploadFilesRef = React.useRef<HTMLInputElement>(null)
|
||||
const isCloud = categoryModule.isCloud(category)
|
||||
const { isOffline } = offlineHooks.useOffline()
|
||||
const canDownload = useCanDownload()
|
||||
const [isCreatingProjectFromTemplate, setIsCreatingProjectFromTemplate] = React.useState(false)
|
||||
const [isCreatingProject, setIsCreatingProject] = React.useState(false)
|
||||
const [createdProjectId, setCreatedProjectId] = React.useState<ProjectId | null>(null)
|
||||
|
134
app/dashboard/src/providers/DriveProvider.tsx
Normal file
134
app/dashboard/src/providers/DriveProvider.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
/** @file The React provider (and associated hooks) for Data Catalog state. */
|
||||
import * as React from 'react'
|
||||
|
||||
import invariant from 'tiny-invariant'
|
||||
import * as zustand from 'zustand'
|
||||
|
||||
import type { AssetId } from 'enso-common/src/services/Backend'
|
||||
|
||||
// ==================
|
||||
// === DriveStore ===
|
||||
// ==================
|
||||
|
||||
/** The state of this zustand store. */
|
||||
interface DriveStore {
|
||||
readonly canDownload: boolean
|
||||
readonly setCanDownload: (canDownload: boolean) => void
|
||||
readonly selectedKeys: ReadonlySet<AssetId>
|
||||
readonly setSelectedKeys: (selectedKeys: ReadonlySet<AssetId>) => void
|
||||
readonly visuallySelectedKeys: ReadonlySet<AssetId> | null
|
||||
readonly setVisuallySelectedKeys: (visuallySelectedKeys: ReadonlySet<AssetId> | null) => void
|
||||
}
|
||||
|
||||
// =======================
|
||||
// === ProjectsContext ===
|
||||
// =======================
|
||||
|
||||
/** State contained in a `ProjectsContext`. */
|
||||
export interface ProjectsContextType extends zustand.StoreApi<DriveStore> {}
|
||||
|
||||
const DriveContext = React.createContext<ProjectsContextType | null>(null)
|
||||
|
||||
/** Props for a {@link DriveProvider}. */
|
||||
export interface ProjectsProviderProps extends Readonly<React.PropsWithChildren> {}
|
||||
|
||||
// ========================
|
||||
// === ProjectsProvider ===
|
||||
// ========================
|
||||
|
||||
/** A React provider (and associated hooks) for determining whether the current area
|
||||
* containing the current element is focused. */
|
||||
export default function DriveProvider(props: ProjectsProviderProps) {
|
||||
const { children } = props
|
||||
const [store] = React.useState(() => {
|
||||
return zustand.createStore<DriveStore>((set) => ({
|
||||
canDownload: false,
|
||||
setCanDownload: (canDownload) => {
|
||||
set({ canDownload })
|
||||
},
|
||||
selectedKeys: new Set(),
|
||||
setSelectedKeys: (selectedKeys) => {
|
||||
set({ selectedKeys })
|
||||
},
|
||||
visuallySelectedKeys: null,
|
||||
setVisuallySelectedKeys: (visuallySelectedKeys) => {
|
||||
set({ visuallySelectedKeys })
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
return <DriveContext.Provider value={store}>{children}</DriveContext.Provider>
|
||||
}
|
||||
|
||||
// =====================
|
||||
// === useDriveStore ===
|
||||
// =====================
|
||||
|
||||
/** The drive store. */
|
||||
export function useDriveStore() {
|
||||
const store = React.useContext(DriveContext)
|
||||
|
||||
invariant(store, 'Drive store can only be used inside an `DriveProvider`.')
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// ======================
|
||||
// === useCanDownload ===
|
||||
// ======================
|
||||
|
||||
/** Whether the current Asset Table selection is downloadble. */
|
||||
export function useCanDownload() {
|
||||
const store = useDriveStore()
|
||||
return zustand.useStore(store, (state) => state.canDownload)
|
||||
}
|
||||
|
||||
// =========================
|
||||
// === useSetCanDownload ===
|
||||
// =========================
|
||||
|
||||
/** A function to set whether the current Asset Table selection is downloadble. */
|
||||
export function useSetCanDownload() {
|
||||
const store = useDriveStore()
|
||||
return zustand.useStore(store, (state) => state.setCanDownload)
|
||||
}
|
||||
|
||||
// =======================
|
||||
// === useSelectedKeys ===
|
||||
// =======================
|
||||
|
||||
/** The selected keys in the Asset Table. */
|
||||
export function useSelectedKeys() {
|
||||
const store = useDriveStore()
|
||||
return zustand.useStore(store, (state) => state.selectedKeys)
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// === useSetSelectedKeys ===
|
||||
// ==========================
|
||||
|
||||
/** A function to set the selected keys of the Asset Table selection. */
|
||||
export function useSetSelectedKeys() {
|
||||
const store = useDriveStore()
|
||||
return zustand.useStore(store, (state) => state.setSelectedKeys)
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// === useVisuallySelectedKeys ===
|
||||
// ===============================
|
||||
|
||||
/** The visually selected keys in the Asset Table. */
|
||||
export function useVisuallySelectedKeys() {
|
||||
const store = useDriveStore()
|
||||
return zustand.useStore(store, (state) => state.selectedKeys)
|
||||
}
|
||||
|
||||
// ==================================
|
||||
// === useSetVisuallySelectedKeys ===
|
||||
// ==================================
|
||||
|
||||
/** A function to set the visually selected keys in the Asset Table. */
|
||||
export function useSetVisuallySelectedKeys() {
|
||||
const store = useDriveStore()
|
||||
return zustand.useStore(store, (state) => state.setVisuallySelectedKeys)
|
||||
}
|
@ -58,12 +58,6 @@ export default class AssetTreeNode<Item extends backendModule.AnyAsset = backend
|
||||
this.type = item.type
|
||||
}
|
||||
|
||||
/** Get an {@link AssetTreeNode.key} from an {@link AssetTreeNode}. Useful for React,
|
||||
* becausse references of static functions do not change. */
|
||||
static getKey(this: void, node: AssetTreeNode) {
|
||||
return node.key
|
||||
}
|
||||
|
||||
/** Return a positive number if `a > b`, a negative number if `a < b`, and zero if `a === b`.
|
||||
* Uses {@link backendModule.compareAssets} internally. */
|
||||
static compare(this: void, a: AssetTreeNode, b: AssetTreeNode) {
|
||||
|
@ -5,7 +5,7 @@
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
export const EMPTY: ReadonlySet<never> = new Set<never>()
|
||||
export const EMPTY_SET: ReadonlySet<never> = new Set<never>()
|
||||
|
||||
/** Adds the value if `presence` is `true`; deletes the value if `presence` is `false`.
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user