mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 13:02:07 +03:00
Option to use Data Link in new project (#9644)
- Close https://github.com/enso-org/cloud-v2/issues/1161 - Add context menu option (and associated shortcut) to open a Data Link in a new project # Important Notes - The corresponding backend endpoints do not yet exist. The frontend currently passes an additional parameter `dataLinkId: string | null` to the backend's `create_project` endpoint. Note that this is inconsistent with the rest of the backend's terminology which calls Data Links `connector`s, so the parameter name might want to be changed.
This commit is contained in:
parent
e3afa5561d
commit
3ecc3aebd0
@ -46,6 +46,7 @@ const ACTION_TO_TEXT_ID: Readonly<Record<inputBindings.DashboardBindingKey, text
|
||||
newFolder: 'newFolderShortcut',
|
||||
newDataLink: 'newDataLinkShortcut',
|
||||
newSecret: 'newSecretShortcut',
|
||||
useInNewProject: 'useInNewProjectShortcut',
|
||||
closeModal: 'closeModalShortcut',
|
||||
cancelEditName: 'cancelEditNameShortcut',
|
||||
signIn: 'signInShortcut',
|
||||
|
@ -32,6 +32,7 @@ import * as backendModule from '#/services/Backend'
|
||||
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'
|
||||
@ -61,8 +62,8 @@ const DRAG_EXPAND_DELAY_MS = 500
|
||||
/** Common properties for state and setters passed to event handlers on an {@link AssetRow}. */
|
||||
export interface AssetRowInnerProps {
|
||||
readonly key: backendModule.AssetId
|
||||
readonly item: AssetTreeNode
|
||||
readonly setItem: React.Dispatch<React.SetStateAction<AssetTreeNode>>
|
||||
readonly item: assetTreeNode.AnyAssetTreeNode
|
||||
readonly setItem: React.Dispatch<React.SetStateAction<assetTreeNode.AnyAssetTreeNode>>
|
||||
readonly state: assetsTable.AssetsTableState
|
||||
readonly rowState: assetsTable.AssetRowState
|
||||
readonly setRowState: React.Dispatch<React.SetStateAction<assetsTable.AssetRowState>>
|
||||
@ -71,7 +72,7 @@ export interface AssetRowInnerProps {
|
||||
/** Props for an {@link AssetRow}. */
|
||||
export interface AssetRowProps
|
||||
extends Readonly<Omit<JSX.IntrinsicElements['tr'], 'onClick' | 'onContextMenu'>> {
|
||||
readonly item: AssetTreeNode
|
||||
readonly item: assetTreeNode.AnyAssetTreeNode
|
||||
readonly state: assetsTable.AssetsTableState
|
||||
readonly hidden: boolean
|
||||
readonly columns: columnUtils.Column[]
|
||||
@ -189,7 +190,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
|
||||
const doMove = React.useCallback(
|
||||
async (
|
||||
newParentKey: backendModule.AssetId | null,
|
||||
newParentKey: backendModule.DirectoryId | null,
|
||||
newParentId: backendModule.DirectoryId | null
|
||||
) => {
|
||||
const rootDirectoryId = user?.rootDirectoryId ?? backendModule.DirectoryId('')
|
||||
@ -726,7 +727,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
unsetModal()
|
||||
onClick(innerProps, event)
|
||||
if (
|
||||
asset.type === backendModule.AssetType.directory &&
|
||||
item.type === backendModule.AssetType.directory &&
|
||||
eventModule.isDoubleClick(event) &&
|
||||
!rowState.isEditingName
|
||||
) {
|
||||
@ -735,7 +736,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
window.setTimeout(() => {
|
||||
setSelected(false)
|
||||
})
|
||||
doToggleDirectoryExpansion(asset.id, item.key, asset.title)
|
||||
doToggleDirectoryExpansion(item.item.id, item.key, asset.title)
|
||||
}
|
||||
}}
|
||||
onContextMenu={event => {
|
||||
@ -772,9 +773,9 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
if (dragOverTimeoutHandle.current != null) {
|
||||
window.clearTimeout(dragOverTimeoutHandle.current)
|
||||
}
|
||||
if (backendModule.assetIsDirectory(asset)) {
|
||||
if (item.type === backendModule.AssetType.directory) {
|
||||
dragOverTimeoutHandle.current = window.setTimeout(() => {
|
||||
doToggleDirectoryExpansion(asset.id, item.key, asset.title, true)
|
||||
doToggleDirectoryExpansion(item.item.id, item.key, asset.title, true)
|
||||
}, DRAG_EXPAND_DELAY_MS)
|
||||
}
|
||||
// Required because `dragover` does not fire on `mouseenter`.
|
||||
@ -814,7 +815,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
props.onDrop?.(event)
|
||||
clearDragState()
|
||||
const [directoryKey, directoryId, directoryTitle] =
|
||||
item.item.type === backendModule.AssetType.directory
|
||||
item.type === backendModule.AssetType.directory
|
||||
? [item.key, item.item.id, asset.title]
|
||||
: [item.directoryKey, item.directoryId, null]
|
||||
const payload = drag.ASSET_ROWS.lookup(event)
|
||||
@ -841,10 +842,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
doToggleDirectoryExpansion(directoryId, directoryKey, directoryTitle, true)
|
||||
dispatchAssetListEvent({
|
||||
type: AssetListEventType.uploadFiles,
|
||||
// This is SAFE, as it is guarded by the condition above:
|
||||
// `item.item.type === backendModule.AssetType.directory`
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
parentKey: directoryKey as backendModule.DirectoryId,
|
||||
parentKey: directoryKey,
|
||||
parentId: directoryId,
|
||||
files: Array.from(event.dataTransfer.files),
|
||||
})
|
||||
|
@ -39,11 +39,11 @@ export default function DataLinkNameColumn(props: DataLinkNameColumnProps) {
|
||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const inputBindings = inputBindingsProvider.useInputBindings()
|
||||
const asset = item.item
|
||||
if (asset.type !== backendModule.AssetType.dataLink) {
|
||||
if (item.type !== backendModule.AssetType.dataLink) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
throw new Error('`DataLinkNameColumn` can only display Data Links.')
|
||||
}
|
||||
const asset = item.item
|
||||
const setAsset = setAssetHooks.useSetAsset(asset, setItem)
|
||||
|
||||
const setIsEditing = (isEditingName: boolean) => {
|
||||
|
@ -45,11 +45,11 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const { getText } = textProvider.useText()
|
||||
const inputBindings = inputBindingsProvider.useInputBindings()
|
||||
const asset = item.item
|
||||
if (asset.type !== backendModule.AssetType.directory) {
|
||||
if (item.type !== backendModule.AssetType.directory) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
throw new Error('`DirectoryNameColumn` can only display folders.')
|
||||
}
|
||||
const asset = item.item
|
||||
const setAsset = setAssetHooks.useSetAsset(asset, setItem)
|
||||
const isCloud = backend.type === backendModule.BackendType.remote
|
||||
|
||||
|
@ -39,11 +39,11 @@ export default function FileNameColumn(props: FileNameColumnProps) {
|
||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const inputBindings = inputBindingsProvider.useInputBindings()
|
||||
const asset = item.item
|
||||
if (asset.type !== backendModule.AssetType.file) {
|
||||
if (item.type !== backendModule.AssetType.file) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
throw new Error('`FileNameColumn` can only display files.')
|
||||
}
|
||||
const asset = item.item
|
||||
const setAsset = setAssetHooks.useSetAsset(asset, setItem)
|
||||
|
||||
const setIsEditing = (isEditingName: boolean) => {
|
||||
|
@ -51,11 +51,11 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
const { user } = authProvider.useNonPartialUserSession()
|
||||
const { getText } = textProvider.useText()
|
||||
const inputBindings = inputBindingsProvider.useInputBindings()
|
||||
const asset = item.item
|
||||
if (asset.type !== backendModule.AssetType.project) {
|
||||
if (item.type !== backendModule.AssetType.project) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
throw new Error('`ProjectNameColumn` can only display projects.')
|
||||
}
|
||||
const asset = item.item
|
||||
const setAsset = setAssetHooks.useSetAsset(asset, setItem)
|
||||
const ownPermission =
|
||||
asset.permissions?.find(permission => permission.user.userId === user?.userId) ?? null
|
||||
@ -142,7 +142,8 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
const createdProject = await backend.createProject({
|
||||
parentDirectoryId: asset.parentId,
|
||||
projectName: asset.title,
|
||||
projectTemplateName: event.templateId,
|
||||
...(event.templateId == null ? {} : { projectTemplateName: event.templateId }),
|
||||
...(event.datalinkId == null ? {} : { datalinkId: event.datalinkId }),
|
||||
})
|
||||
rowState.setVisibility(Visibility.visible)
|
||||
setAsset(
|
||||
|
@ -44,11 +44,11 @@ export default function SecretNameColumn(props: SecretNameColumnProps) {
|
||||
const { setModal } = modalProvider.useSetModal()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const inputBindings = inputBindingsProvider.useInputBindings()
|
||||
const asset = item.item
|
||||
if (asset.type !== backendModule.AssetType.secret) {
|
||||
if (item.type !== backendModule.AssetType.secret) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
throw new Error('`SecretNameColumn` can only display secrets.')
|
||||
}
|
||||
const asset = item.item
|
||||
|
||||
const setIsEditing = (isEditingName: boolean) => {
|
||||
if (isEditable) {
|
||||
|
@ -13,7 +13,7 @@ import SharedWithColumn from '#/components/dashboard/column/SharedWithColumn'
|
||||
|
||||
import type * as backendModule from '#/services/Backend'
|
||||
|
||||
import type AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import type * as assetTreeNode from '#/utilities/AssetTreeNode'
|
||||
|
||||
// ===================
|
||||
// === AssetColumn ===
|
||||
@ -22,8 +22,8 @@ import type AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||
/** Props for an arbitrary variant of {@link backendModule.Asset}. */
|
||||
export interface AssetColumnProps {
|
||||
readonly keyProp: backendModule.AssetId
|
||||
readonly item: AssetTreeNode
|
||||
readonly setItem: React.Dispatch<React.SetStateAction<AssetTreeNode>>
|
||||
readonly item: assetTreeNode.AnyAssetTreeNode
|
||||
readonly setItem: React.Dispatch<React.SetStateAction<assetTreeNode.AnyAssetTreeNode>>
|
||||
readonly selected: boolean
|
||||
readonly setSelected: (selected: boolean) => void
|
||||
readonly isSoleSelected: boolean
|
||||
|
@ -86,6 +86,11 @@ export const BINDINGS = inputBindings.defineBindings({
|
||||
bindings: !detect.isOnMacOS() ? ['Mod+Alt+Shift+N'] : ['Mod+Alt+Shift+N', 'Mod+Alt+Shift+~'],
|
||||
icon: AddConnectorIcon,
|
||||
},
|
||||
useInNewProject: {
|
||||
name: 'Use In New Project',
|
||||
bindings: ['Mod+P'],
|
||||
icon: AddNetworkIcon,
|
||||
},
|
||||
signIn: { name: 'Login', bindings: [], icon: SignInIcon },
|
||||
signOut: { name: 'Logout', bindings: [], icon: SignOutIcon, color: 'rgb(243 24 10 / 0.87)' },
|
||||
// These should not appear in any menus.
|
||||
|
@ -3,7 +3,7 @@ import type AssetEventType from '#/events/AssetEventType'
|
||||
|
||||
import type * as spinner from '#/components/Spinner'
|
||||
|
||||
import type * as backendModule from '#/services/Backend'
|
||||
import type * as backend from '#/services/Backend'
|
||||
|
||||
// This is required, to whitelist this event.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
@ -61,93 +61,94 @@ type SanityCheck<
|
||||
|
||||
/** A signal to create a project. */
|
||||
export interface AssetNewProjectEvent extends AssetBaseEvent<AssetEventType.newProject> {
|
||||
readonly placeholderId: backendModule.ProjectId
|
||||
readonly placeholderId: backend.ProjectId
|
||||
readonly templateId: string | null
|
||||
readonly datalinkId: backend.ConnectorId | null
|
||||
readonly onSpinnerStateChange: ((state: spinner.SpinnerState) => void) | null
|
||||
}
|
||||
|
||||
/** A signal to create a directory. */
|
||||
export interface AssetNewFolderEvent extends AssetBaseEvent<AssetEventType.newFolder> {
|
||||
readonly placeholderId: backendModule.DirectoryId
|
||||
readonly placeholderId: backend.DirectoryId
|
||||
}
|
||||
|
||||
/** A signal to upload files. */
|
||||
export interface AssetUploadFilesEvent extends AssetBaseEvent<AssetEventType.uploadFiles> {
|
||||
readonly files: ReadonlyMap<backendModule.AssetId, File>
|
||||
readonly files: ReadonlyMap<backend.AssetId, File>
|
||||
}
|
||||
|
||||
/** A signal to update files with new versions. */
|
||||
export interface AssetUpdateFilesEvent extends AssetBaseEvent<AssetEventType.updateFiles> {
|
||||
readonly files: ReadonlyMap<backendModule.AssetId, File>
|
||||
readonly files: ReadonlyMap<backend.AssetId, File>
|
||||
}
|
||||
|
||||
/** A signal to create a Data Link. */
|
||||
export interface AssetNewDataLinkEvent extends AssetBaseEvent<AssetEventType.newDataLink> {
|
||||
readonly placeholderId: backendModule.ConnectorId
|
||||
readonly placeholderId: backend.ConnectorId
|
||||
readonly value: unknown
|
||||
}
|
||||
|
||||
/** A signal to create a secret. */
|
||||
export interface AssetNewSecretEvent extends AssetBaseEvent<AssetEventType.newSecret> {
|
||||
readonly placeholderId: backendModule.SecretId
|
||||
readonly placeholderId: backend.SecretId
|
||||
readonly value: string
|
||||
}
|
||||
|
||||
/** A signal to open the specified project. */
|
||||
export interface AssetOpenProjectEvent extends AssetBaseEvent<AssetEventType.openProject> {
|
||||
readonly id: backendModule.ProjectId
|
||||
readonly id: backend.ProjectId
|
||||
readonly shouldAutomaticallySwitchPage: boolean
|
||||
readonly runInBackground: boolean
|
||||
}
|
||||
|
||||
/** A signal to close the specified project. */
|
||||
export interface AssetCloseProjectEvent extends AssetBaseEvent<AssetEventType.closeProject> {
|
||||
readonly id: backendModule.ProjectId
|
||||
readonly id: backend.ProjectId
|
||||
}
|
||||
|
||||
/** A signal that multiple assets should be copied. `ids` are the `Id`s of the newly created
|
||||
* placeholder items. */
|
||||
export interface AssetCopyEvent extends AssetBaseEvent<AssetEventType.copy> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly newParentKey: backendModule.AssetId
|
||||
readonly newParentId: backendModule.DirectoryId
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
readonly newParentKey: backend.AssetId
|
||||
readonly newParentId: backend.DirectoryId
|
||||
}
|
||||
|
||||
/** A signal to cut multiple assets. */
|
||||
export interface AssetCutEvent extends AssetBaseEvent<AssetEventType.cut> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
}
|
||||
|
||||
/** A signal that a cut operation has been cancelled. */
|
||||
export interface AssetCancelCutEvent extends AssetBaseEvent<AssetEventType.cancelCut> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
}
|
||||
|
||||
/** A signal to move multiple assets. */
|
||||
export interface AssetMoveEvent extends AssetBaseEvent<AssetEventType.move> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly newParentKey: backendModule.AssetId
|
||||
readonly newParentId: backendModule.DirectoryId
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
readonly newParentKey: backend.DirectoryId
|
||||
readonly newParentId: backend.DirectoryId
|
||||
}
|
||||
|
||||
/** A signal to delete assets. */
|
||||
export interface AssetDeleteEvent extends AssetBaseEvent<AssetEventType.delete> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
}
|
||||
|
||||
/** A signal to delete assets forever. */
|
||||
export interface AssetDeleteForeverEvent extends AssetBaseEvent<AssetEventType.deleteForever> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
}
|
||||
|
||||
/** A signal to restore assets from trash. */
|
||||
export interface AssetRestoreEvent extends AssetBaseEvent<AssetEventType.restore> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
}
|
||||
|
||||
/** A signal to download assets. */
|
||||
export interface AssetDownloadEvent extends AssetBaseEvent<AssetEventType.download> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
}
|
||||
|
||||
/** A signal to download the currently selected assets. */
|
||||
@ -156,38 +157,38 @@ export interface AssetDownloadSelectedEvent
|
||||
|
||||
/** A signal to remove the current user's permissions for an asset. */
|
||||
export interface AssetRemoveSelfEvent extends AssetBaseEvent<AssetEventType.removeSelf> {
|
||||
readonly id: backendModule.AssetId
|
||||
readonly id: backend.AssetId
|
||||
}
|
||||
|
||||
/** A signal to temporarily add labels to the selected assetss. */
|
||||
export interface AssetTemporarilyAddLabelsEvent
|
||||
extends AssetBaseEvent<AssetEventType.temporarilyAddLabels> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly labelNames: ReadonlySet<backendModule.LabelName>
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
readonly labelNames: ReadonlySet<backend.LabelName>
|
||||
}
|
||||
|
||||
/** A signal to temporarily remove labels from the selected assetss. */
|
||||
export interface AssetTemporarilyRemoveLabelsEvent
|
||||
extends AssetBaseEvent<AssetEventType.temporarilyRemoveLabels> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly labelNames: ReadonlySet<backendModule.LabelName>
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
readonly labelNames: ReadonlySet<backend.LabelName>
|
||||
}
|
||||
|
||||
/** A signal to add labels to the selected assetss. */
|
||||
export interface AssetAddLabelsEvent extends AssetBaseEvent<AssetEventType.addLabels> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly labelNames: ReadonlySet<backendModule.LabelName>
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
readonly labelNames: ReadonlySet<backend.LabelName>
|
||||
}
|
||||
|
||||
/** A signal to remove labels from the selected assetss. */
|
||||
export interface AssetRemoveLabelsEvent extends AssetBaseEvent<AssetEventType.removeLabels> {
|
||||
readonly ids: ReadonlySet<backendModule.AssetId>
|
||||
readonly labelNames: ReadonlySet<backendModule.LabelName>
|
||||
readonly ids: ReadonlySet<backend.AssetId>
|
||||
readonly labelNames: ReadonlySet<backend.LabelName>
|
||||
}
|
||||
|
||||
/** A signal to remove a label from all assets. */
|
||||
export interface AssetDeleteLabelEvent extends AssetBaseEvent<AssetEventType.deleteLabel> {
|
||||
readonly labelName: backendModule.LabelName
|
||||
readonly labelName: backend.LabelName
|
||||
}
|
||||
|
||||
/** Every possible type of asset event. */
|
||||
|
@ -62,7 +62,8 @@ interface AssetListNewProjectEvent extends AssetListBaseEvent<AssetListEventType
|
||||
readonly parentKey: backend.DirectoryId
|
||||
readonly parentId: backend.DirectoryId
|
||||
readonly templateId: string | null
|
||||
readonly templateName: string | null
|
||||
readonly datalinkId: backend.ConnectorId | null
|
||||
readonly preferredName: string | null
|
||||
readonly onSpinnerStateChange: ((state: spinner.SpinnerState) => void) | null
|
||||
}
|
||||
|
||||
@ -104,7 +105,7 @@ interface AssetListCloseFolderEvent extends AssetListBaseEvent<AssetListEventTyp
|
||||
|
||||
/** A signal that files should be copied. */
|
||||
interface AssetListCopyEvent extends AssetListBaseEvent<AssetListEventType.copy> {
|
||||
readonly newParentKey: backend.AssetId
|
||||
readonly newParentKey: backend.DirectoryId
|
||||
readonly newParentId: backend.DirectoryId
|
||||
readonly items: backend.AnyAsset[]
|
||||
}
|
||||
@ -112,7 +113,7 @@ interface AssetListCopyEvent extends AssetListBaseEvent<AssetListEventType.copy>
|
||||
/** A signal that a file has been moved. */
|
||||
interface AssetListMoveEvent extends AssetListBaseEvent<AssetListEventType.move> {
|
||||
readonly key: backend.AssetId
|
||||
readonly newParentKey: backend.AssetId
|
||||
readonly newParentKey: backend.DirectoryId
|
||||
readonly newParentId: backend.DirectoryId
|
||||
readonly item: backend.AnyAsset
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import * as React from 'react'
|
||||
|
||||
import type * as backend from '#/services/Backend'
|
||||
|
||||
import type * as assetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||
|
||||
// ===================
|
||||
@ -17,7 +18,7 @@ import AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||
* has been done. */
|
||||
export function useSetAsset<T extends backend.AnyAsset>(
|
||||
_value: T,
|
||||
setNode: React.Dispatch<React.SetStateAction<AssetTreeNode>>
|
||||
setNode: React.Dispatch<React.SetStateAction<assetTreeNode.AnyAssetTreeNode>>
|
||||
) {
|
||||
return React.useCallback(
|
||||
(valueOrUpdater: React.SetStateAction<T>) => {
|
||||
|
@ -52,7 +52,7 @@ export interface AssetContextMenuProps {
|
||||
readonly doCut: () => void
|
||||
readonly doTriggerDescriptionEdit: () => void
|
||||
readonly doPaste: (
|
||||
newParentKey: backendModule.AssetId,
|
||||
newParentKey: backendModule.DirectoryId,
|
||||
newParentId: backendModule.DirectoryId
|
||||
) => void
|
||||
}
|
||||
@ -135,6 +135,24 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
|
||||
) : (
|
||||
<ContextMenus hidden={hidden} key={asset.id} event={event}>
|
||||
<ContextMenu aria-label={getText('assetContextMenuLabel')} hidden={hidden}>
|
||||
{asset.type === backendModule.AssetType.dataLink && (
|
||||
<ContextMenuEntry
|
||||
hidden={hidden}
|
||||
action="useInNewProject"
|
||||
doAction={() => {
|
||||
unsetModal()
|
||||
dispatchAssetListEvent({
|
||||
type: AssetListEventType.newProject,
|
||||
parentId: item.directoryId,
|
||||
parentKey: item.directoryKey,
|
||||
templateId: null,
|
||||
datalinkId: asset.id,
|
||||
preferredName: asset.title,
|
||||
onSpinnerStateChange: null,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{asset.type === backendModule.AssetType.project &&
|
||||
canExecute &&
|
||||
!isRunningProject &&
|
||||
@ -381,7 +399,7 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
|
||||
action="paste"
|
||||
doAction={() => {
|
||||
const [directoryKey, directoryId] =
|
||||
item.item.type === backendModule.AssetType.directory
|
||||
item.type === backendModule.AssetType.directory
|
||||
? [item.key, item.item.id]
|
||||
: [item.directoryKey, item.directoryId]
|
||||
doPaste(directoryKey, directoryId)
|
||||
|
@ -16,7 +16,7 @@ import * as backend from '#/services/Backend'
|
||||
|
||||
import * as array from '#/utilities/array'
|
||||
import type AssetQuery from '#/utilities/AssetQuery'
|
||||
import type AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import type * as assetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import LocalStorage from '#/utilities/LocalStorage'
|
||||
|
||||
// =====================
|
||||
@ -51,8 +51,8 @@ LocalStorage.registerKey('assetPanelTab', {
|
||||
|
||||
/** The subset of {@link AssetPanelProps} that are required to be supplied by the row. */
|
||||
export interface AssetPanelRequiredProps {
|
||||
readonly item: AssetTreeNode | null
|
||||
readonly setItem: React.Dispatch<React.SetStateAction<AssetTreeNode>> | null
|
||||
readonly item: assetTreeNode.AnyAssetTreeNode | null
|
||||
readonly setItem: React.Dispatch<React.SetStateAction<assetTreeNode.AnyAssetTreeNode>> | null
|
||||
}
|
||||
|
||||
/** Props for an {@link AssetPanel}. */
|
||||
|
@ -26,7 +26,7 @@ import UnstyledButton from '#/components/UnstyledButton'
|
||||
import * as backendModule from '#/services/Backend'
|
||||
|
||||
import type AssetQuery from '#/utilities/AssetQuery'
|
||||
import type AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import type * as assetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import * as object from '#/utilities/object'
|
||||
import * as permissions from '#/utilities/permissions'
|
||||
|
||||
@ -36,8 +36,8 @@ import * as permissions from '#/utilities/permissions'
|
||||
|
||||
/** Props for an {@link AssetPropertiesProps}. */
|
||||
export interface AssetPropertiesProps {
|
||||
readonly item: AssetTreeNode
|
||||
readonly setItem: React.Dispatch<React.SetStateAction<AssetTreeNode>>
|
||||
readonly item: assetTreeNode.AnyAssetTreeNode
|
||||
readonly setItem: React.Dispatch<React.SetStateAction<assetTreeNode.AnyAssetTreeNode>>
|
||||
readonly category: Category
|
||||
readonly labels: backendModule.Label[]
|
||||
readonly setQuery: React.Dispatch<React.SetStateAction<AssetQuery>>
|
||||
@ -75,7 +75,7 @@ export default function AssetProperties(props: AssetPropertiesProps) {
|
||||
[dataLinkValue]
|
||||
)
|
||||
const setItem = React.useCallback(
|
||||
(valueOrUpdater: React.SetStateAction<AssetTreeNode>) => {
|
||||
(valueOrUpdater: React.SetStateAction<assetTreeNode.AnyAssetTreeNode>) => {
|
||||
setItemInner(valueOrUpdater)
|
||||
setItemRaw(valueOrUpdater)
|
||||
},
|
||||
|
@ -49,6 +49,7 @@ import LocalBackend from '#/services/LocalBackend'
|
||||
import * as array from '#/utilities/array'
|
||||
import type * as assetQuery from '#/utilities/AssetQuery'
|
||||
import AssetQuery from '#/utilities/AssetQuery'
|
||||
import type * as assetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import * as dateTime from '#/utilities/dateTime'
|
||||
import * as drag from '#/utilities/drag'
|
||||
@ -191,11 +192,11 @@ const SUGGESTIONS_FOR_NEGATIVE_TYPE: assetSearchBar.Suggestion[] = [
|
||||
/** Return a directory, with new children added into its list of children.
|
||||
* All children MUST have the same asset type. */
|
||||
function insertAssetTreeNodeChildren(
|
||||
item: AssetTreeNode,
|
||||
item: assetTreeNode.AnyAssetTreeNode,
|
||||
children: backendModule.AnyAsset[],
|
||||
directoryKey: backendModule.AssetId,
|
||||
directoryKey: backendModule.DirectoryId,
|
||||
directoryId: backendModule.DirectoryId
|
||||
): AssetTreeNode {
|
||||
): assetTreeNode.AnyAssetTreeNode {
|
||||
const depth = item.depth + 1
|
||||
const typeOrder = children[0] != null ? backendModule.ASSET_TYPE_ORDER[children[0].type] : 0
|
||||
const nodes = (item.children ?? []).filter(
|
||||
@ -215,12 +216,12 @@ function insertAssetTreeNodeChildren(
|
||||
/** Return a directory, with new children added into its list of children.
|
||||
* The children MAY be of different asset types. */
|
||||
function insertArbitraryAssetTreeNodeChildren(
|
||||
item: AssetTreeNode,
|
||||
item: assetTreeNode.AnyAssetTreeNode,
|
||||
children: backendModule.AnyAsset[],
|
||||
directoryKey: backendModule.AssetId,
|
||||
directoryKey: backendModule.DirectoryId,
|
||||
directoryId: backendModule.DirectoryId,
|
||||
getKey: ((asset: backendModule.AnyAsset) => backendModule.AssetId) | null = null
|
||||
): AssetTreeNode {
|
||||
): assetTreeNode.AnyAssetTreeNode {
|
||||
const depth = item.depth + 1
|
||||
const nodes = (item.children ?? []).filter(
|
||||
node => node.item.type !== backendModule.AssetType.specialEmpty
|
||||
@ -243,7 +244,13 @@ function insertArbitraryAssetTreeNodeChildren(
|
||||
if (firstChild) {
|
||||
const typeOrder = backendModule.ASSET_TYPE_ORDER[firstChild.type]
|
||||
const nodesToInsert = childrenOfSpecificType.map(asset =>
|
||||
AssetTreeNode.fromAsset(asset, directoryKey, directoryId, depth, getKey)
|
||||
AssetTreeNode.fromAsset(
|
||||
asset,
|
||||
directoryKey,
|
||||
directoryId,
|
||||
depth,
|
||||
getKey?.(asset) ?? asset.id
|
||||
)
|
||||
)
|
||||
newNodes = array.splicedBefore(
|
||||
newNodes,
|
||||
@ -300,12 +307,12 @@ export interface AssetsTableState {
|
||||
readonly setAssetPanelProps: (props: assetPanel.AssetPanelRequiredProps | null) => void
|
||||
readonly setIsAssetPanelTemporarilyVisible: (visible: boolean) => void
|
||||
readonly nodeMap: Readonly<
|
||||
React.MutableRefObject<ReadonlyMap<backendModule.AssetId, AssetTreeNode>>
|
||||
React.MutableRefObject<ReadonlyMap<backendModule.AssetId, assetTreeNode.AnyAssetTreeNode>>
|
||||
>
|
||||
readonly hideColumn: (column: columnUtils.Column) => void
|
||||
readonly doToggleDirectoryExpansion: (
|
||||
directoryId: backendModule.DirectoryId,
|
||||
key: backendModule.AssetId,
|
||||
key: backendModule.DirectoryId,
|
||||
title?: string | null,
|
||||
override?: boolean
|
||||
) => void
|
||||
@ -321,7 +328,7 @@ export interface AssetsTableState {
|
||||
readonly doCopy: () => void
|
||||
readonly doCut: () => void
|
||||
readonly doPaste: (
|
||||
newParentKey: backendModule.AssetId,
|
||||
newParentKey: backendModule.DirectoryId,
|
||||
newParentId: backendModule.DirectoryId
|
||||
) => void
|
||||
}
|
||||
@ -356,7 +363,7 @@ export interface AssetsTableProps {
|
||||
readonly dispatchAssetEvent: (event: assetEvent.AssetEvent) => void
|
||||
readonly setAssetPanelProps: (props: assetPanel.AssetPanelRequiredProps | null) => void
|
||||
readonly setIsAssetPanelTemporarilyVisible: (visible: boolean) => void
|
||||
readonly targetDirectoryNodeRef: React.MutableRefObject<AssetTreeNode<backendModule.DirectoryAsset> | null>
|
||||
readonly targetDirectoryNodeRef: React.MutableRefObject<assetTreeNode.AnyAssetTreeNode<backendModule.DirectoryAsset> | null>
|
||||
readonly doOpenEditor: (
|
||||
project: backendModule.ProjectAsset,
|
||||
setProject: React.Dispatch<React.SetStateAction<backendModule.ProjectAsset>>,
|
||||
@ -401,7 +408,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
() => backend.rootDirectoryId(user) ?? backendModule.DirectoryId(''),
|
||||
[backend, user]
|
||||
)
|
||||
const [assetTree, setAssetTree] = React.useState<AssetTreeNode>(() => {
|
||||
const [assetTree, setAssetTree] = React.useState<assetTreeNode.AnyAssetTreeNode>(() => {
|
||||
const rootParentDirectoryId = backendModule.DirectoryId('')
|
||||
return AssetTreeNode.fromAsset(
|
||||
backendModule.createRootDirectoryAsset(rootDirectoryId),
|
||||
@ -415,19 +422,19 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const queuedAssetListEventsRef = React.useRef<assetListEvent.AssetListEvent[]>([])
|
||||
const rootRef = React.useRef<HTMLDivElement | null>(null)
|
||||
const headerRowRef = React.useRef<HTMLTableRowElement>(null)
|
||||
const assetTreeRef = React.useRef<AssetTreeNode>(assetTree)
|
||||
const assetTreeRef = React.useRef<assetTreeNode.AnyAssetTreeNode>(assetTree)
|
||||
const pasteDataRef = React.useRef<pasteDataModule.PasteData<
|
||||
ReadonlySet<backendModule.AssetId>
|
||||
> | null>(null)
|
||||
const nodeMapRef = React.useRef<ReadonlyMap<backendModule.AssetId, AssetTreeNode>>(
|
||||
new Map<backendModule.AssetId, AssetTreeNode>()
|
||||
)
|
||||
const nodeMapRef = React.useRef<
|
||||
ReadonlyMap<backendModule.AssetId, assetTreeNode.AnyAssetTreeNode>
|
||||
>(new Map<backendModule.AssetId, assetTreeNode.AnyAssetTreeNode>())
|
||||
const filter = React.useMemo(() => {
|
||||
const globCache: Record<string, RegExp> = {}
|
||||
if (/^\s*$/.test(query.query)) {
|
||||
return null
|
||||
} else {
|
||||
return (node: AssetTreeNode) => {
|
||||
return (node: assetTreeNode.AnyAssetTreeNode) => {
|
||||
if (
|
||||
node.item.type === backendModule.AssetType.specialEmpty ||
|
||||
node.item.type === backendModule.AssetType.specialLoading
|
||||
@ -538,7 +545,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
return assetTree.preorderTraversal()
|
||||
} else {
|
||||
const multiplier = sortInfo.direction === sorting.SortDirection.ascending ? 1 : -1
|
||||
let compare: (a: AssetTreeNode, b: AssetTreeNode) => number
|
||||
let compare: (a: assetTreeNode.AnyAssetTreeNode, b: assetTreeNode.AnyAssetTreeNode) => number
|
||||
switch (sortInfo.field) {
|
||||
case columnUtils.Column.name: {
|
||||
compare = (a, b) => {
|
||||
@ -568,7 +575,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
}, [assetTree, sortInfo])
|
||||
const visibilities = React.useMemo(() => {
|
||||
const map = new Map<backendModule.AssetId, Visibility>()
|
||||
const processNode = (node: AssetTreeNode) => {
|
||||
const processNode = (node: assetTreeNode.AnyAssetTreeNode) => {
|
||||
let displayState = Visibility.hidden
|
||||
const visible = filter?.(node) ?? true
|
||||
for (const child of node.children ?? []) {
|
||||
@ -639,7 +646,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
|
||||
React.useEffect(() => {
|
||||
const nodeToSuggestion = (
|
||||
node: AssetTreeNode,
|
||||
node: assetTreeNode.AnyAssetTreeNode,
|
||||
key: assetQuery.AssetQueryKey = 'names'
|
||||
): assetSearchBar.Suggestion => ({
|
||||
render: () => `${key === 'names' ? '' : '-:'}${node.item.title}`,
|
||||
@ -932,7 +939,6 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const rootParentDirectoryId = backendModule.DirectoryId('')
|
||||
const rootDirectory = backendModule.createRootDirectoryAsset(rootDirectoryId)
|
||||
const newRootNode = new AssetTreeNode(
|
||||
rootDirectoryId,
|
||||
rootDirectory,
|
||||
rootParentDirectoryId,
|
||||
rootParentDirectoryId,
|
||||
@ -1058,7 +1064,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const doToggleDirectoryExpansion = React.useCallback(
|
||||
(
|
||||
directoryId: backendModule.DirectoryId,
|
||||
key: backendModule.AssetId,
|
||||
key: backendModule.DirectoryId,
|
||||
title?: string | null,
|
||||
override?: boolean
|
||||
) => {
|
||||
@ -1203,7 +1209,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const keys = selectedKeysRef.current
|
||||
setSelectedKeys(set.withPresence(keys, item.key, !keys.has(item.key)))
|
||||
} else {
|
||||
switch (item.item.type) {
|
||||
switch (item.type) {
|
||||
case backendModule.AssetType.directory: {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@ -1254,7 +1260,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
break
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
if (item.item.type === backendModule.AssetType.directory) {
|
||||
if (item.type === backendModule.AssetType.directory) {
|
||||
if (item.children != null) {
|
||||
// The folder is expanded; collapse it.
|
||||
event.preventDefault()
|
||||
@ -1280,7 +1286,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
break
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
if (item.item.type === backendModule.AssetType.directory && item.children == null) {
|
||||
if (item.type === backendModule.AssetType.directory && item.children == null) {
|
||||
// The folder is collapsed; expand it.
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@ -1404,7 +1410,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const insertAssets = React.useCallback(
|
||||
(
|
||||
assets: backendModule.AnyAsset[],
|
||||
parentKey: backendModule.AssetId | null,
|
||||
parentKey: backendModule.DirectoryId | null,
|
||||
parentId: backendModule.DirectoryId | null
|
||||
) => {
|
||||
const actualParentKey = parentKey ?? rootDirectoryId
|
||||
@ -1423,7 +1429,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const insertArbitraryAssets = React.useCallback(
|
||||
(
|
||||
assets: backendModule.AnyAsset[],
|
||||
parentKey: backendModule.AssetId | null,
|
||||
parentKey: backendModule.DirectoryId | null,
|
||||
parentId: backendModule.DirectoryId | null,
|
||||
getKey: ((asset: backendModule.AnyAsset) => backendModule.AssetId) | null = null
|
||||
) => {
|
||||
@ -1479,7 +1485,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
break
|
||||
}
|
||||
case AssetListEventType.newProject: {
|
||||
const projectName = getNewProjectName(event.templateName, event.parentId)
|
||||
const projectName = getNewProjectName(event.preferredName, event.parentId)
|
||||
const dummyId = backendModule.ProjectId(uniqueString.uniqueString())
|
||||
const path =
|
||||
backend instanceof LocalBackend ? backend.joinPath(event.parentId, projectName) : null
|
||||
@ -1507,6 +1513,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
type: AssetEventType.newProject,
|
||||
placeholderId: dummyId,
|
||||
templateId: event.templateId,
|
||||
datalinkId: event.datalinkId,
|
||||
onSpinnerStateChange: event.onSpinnerStateChange,
|
||||
})
|
||||
break
|
||||
@ -1798,7 +1805,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
])
|
||||
|
||||
const doPaste = React.useCallback(
|
||||
(newParentKey: backendModule.AssetId, newParentId: backendModule.DirectoryId) => {
|
||||
(newParentKey: backendModule.DirectoryId, newParentId: backendModule.DirectoryId) => {
|
||||
unsetModal()
|
||||
if (pasteData != null) {
|
||||
if (pasteData.data.has(newParentKey)) {
|
||||
|
@ -22,7 +22,7 @@ import ConfirmDeleteModal from '#/modals/ConfirmDeleteModal'
|
||||
|
||||
import * as backendModule from '#/services/Backend'
|
||||
|
||||
import type AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import type * as assetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import type * as pasteDataModule from '#/utilities/pasteData'
|
||||
import * as permissions from '#/utilities/permissions'
|
||||
import * as uniqueString from '#/utilities/uniqueString'
|
||||
@ -38,14 +38,16 @@ export interface AssetsTableContextMenuProps {
|
||||
readonly pasteData: pasteDataModule.PasteData<ReadonlySet<backendModule.AssetId>> | null
|
||||
readonly selectedKeys: ReadonlySet<backendModule.AssetId>
|
||||
readonly clearSelectedKeys: () => void
|
||||
readonly nodeMapRef: React.MutableRefObject<ReadonlyMap<backendModule.AssetId, AssetTreeNode>>
|
||||
readonly nodeMapRef: React.MutableRefObject<
|
||||
ReadonlyMap<backendModule.AssetId, assetTreeNode.AnyAssetTreeNode>
|
||||
>
|
||||
readonly event: Pick<React.MouseEvent<Element, MouseEvent>, 'pageX' | 'pageY'>
|
||||
readonly dispatchAssetEvent: (event: assetEvent.AssetEvent) => void
|
||||
readonly dispatchAssetListEvent: (event: assetListEvent.AssetListEvent) => void
|
||||
readonly doCopy: () => void
|
||||
readonly doCut: () => void
|
||||
readonly doPaste: (
|
||||
newParentKey: backendModule.AssetId,
|
||||
newParentKey: backendModule.DirectoryId,
|
||||
newParentId: backendModule.DirectoryId
|
||||
) => void
|
||||
}
|
||||
@ -192,7 +194,7 @@ export default function AssetsTableContextMenu(props: AssetsTableContextMenuProp
|
||||
selectedKeys.size === 1 && firstKey != null
|
||||
? nodeMapRef.current.get(firstKey)
|
||||
: null
|
||||
if (selectedNode?.item.type === backendModule.AssetType.directory) {
|
||||
if (selectedNode?.type === backendModule.AssetType.directory) {
|
||||
doPaste(selectedNode.key, selectedNode.item.id)
|
||||
} else {
|
||||
doPaste(rootDirectoryId, rootDirectoryId)
|
||||
|
@ -201,7 +201,8 @@ export default function Drive(props: DriveProps) {
|
||||
parentKey: targetDirectoryNodeRef.current?.key ?? rootDirectoryId,
|
||||
parentId: targetDirectoryNodeRef.current?.item.id ?? rootDirectoryId,
|
||||
templateId,
|
||||
templateName,
|
||||
datalinkId: null,
|
||||
preferredName: templateName,
|
||||
onSpinnerStateChange,
|
||||
})
|
||||
},
|
||||
|
@ -26,7 +26,7 @@ export interface GlobalContextMenuProps {
|
||||
readonly directoryId: backendModule.DirectoryId | null
|
||||
readonly dispatchAssetListEvent: (event: assetListEventModule.AssetListEvent) => void
|
||||
readonly doPaste: (
|
||||
newParentKey: backendModule.AssetId,
|
||||
newParentKey: backendModule.DirectoryId,
|
||||
newParentId: backendModule.DirectoryId
|
||||
) => void
|
||||
}
|
||||
@ -106,7 +106,8 @@ export default function GlobalContextMenu(props: GlobalContextMenuProps) {
|
||||
parentKey: directoryKey ?? rootDirectoryId,
|
||||
parentId: directoryId ?? rootDirectoryId,
|
||||
templateId: null,
|
||||
templateName: null,
|
||||
datalinkId: null,
|
||||
preferredName: null,
|
||||
onSpinnerStateChange: null,
|
||||
})
|
||||
}}
|
||||
|
@ -417,7 +417,8 @@ export default function Dashboard(props: DashboardProps) {
|
||||
parentKey: rootDirectoryId,
|
||||
parentId: rootDirectoryId,
|
||||
templateId: templateId,
|
||||
templateName: templateName,
|
||||
datalinkId: null,
|
||||
preferredName: templateName,
|
||||
onSpinnerStateChange: onSpinnerStateChange,
|
||||
})
|
||||
},
|
||||
|
@ -951,8 +951,9 @@ export interface DeleteAssetRequestBody {
|
||||
/** HTTP request body for the "create project" endpoint. */
|
||||
export interface CreateProjectRequestBody {
|
||||
readonly projectName: string
|
||||
readonly projectTemplateName: string | null
|
||||
readonly parentDirectoryId: DirectoryId | null
|
||||
readonly projectTemplateName?: string
|
||||
readonly parentDirectoryId?: DirectoryId
|
||||
readonly datalinkId?: ConnectorId
|
||||
}
|
||||
|
||||
/** HTTP request body for the "update project" endpoint.
|
||||
|
@ -437,6 +437,7 @@
|
||||
"newFolderShortcut": "New Folder",
|
||||
"newDataLinkShortcut": "New Data Link",
|
||||
"newSecretShortcut": "New Secret",
|
||||
"useInNewProjectShortcut": "Use In New Project",
|
||||
"closeModalShortcut": "Close",
|
||||
"cancelEditNameShortcut": "Cancel Editing",
|
||||
"signInShortcut": "Login",
|
||||
|
@ -12,25 +12,36 @@ export interface AssetTreeNodeData
|
||||
'children' | 'depth' | 'directoryId' | 'directoryKey' | 'item' | 'key'
|
||||
> {}
|
||||
|
||||
/** All possible variants of {@link AssetTreeNode}s. */
|
||||
// The `Item extends Item` is required to trigger distributive conditional types:
|
||||
// https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
|
||||
export type AnyAssetTreeNode<Item extends backendModule.AnyAsset = backendModule.AnyAsset> =
|
||||
Item extends Item ? AssetTreeNode<Item> : never
|
||||
|
||||
/** A node in the drive's item tree. */
|
||||
export default class AssetTreeNode<Item extends backendModule.AnyAsset = backendModule.AnyAsset> {
|
||||
readonly type: Item['type']
|
||||
/** Create a {@link AssetTreeNode}. */
|
||||
constructor(
|
||||
/** The id of the asset (or the placeholder id for new assets). This must never change. */
|
||||
public readonly key: Item['id'],
|
||||
/** The actual asset. This MAY change if this is initially a placeholder item, but rows MAY
|
||||
* keep updated values within the row itself as well. */
|
||||
public item: Item,
|
||||
/** The id of the asset's parent directory (or the placeholder id for new assets).
|
||||
* This must never change. */
|
||||
public readonly directoryKey: backendModule.AssetId,
|
||||
public readonly directoryKey: backendModule.DirectoryId,
|
||||
/** The actual id of the asset's parent directory (or the placeholder id for new assets). */
|
||||
public readonly directoryId: backendModule.DirectoryId,
|
||||
/** This is `null` if the asset is not a directory asset, OR if it is a collapsed directory
|
||||
* asset. */
|
||||
public readonly children: AssetTreeNode[] | null,
|
||||
public readonly depth: number
|
||||
) {}
|
||||
public readonly children: AnyAssetTreeNode[] | null,
|
||||
public readonly depth: number,
|
||||
/** The internal (to the frontend) id of the asset (or the placeholder id for new assets).
|
||||
* This must never change, otherwise the component's state is lost when receiving the real id
|
||||
* from the backend. */
|
||||
public readonly key: Item['id'] = item.id
|
||||
) {
|
||||
this.type = item.type
|
||||
}
|
||||
|
||||
/** Get an {@link AssetTreeNode.key} from an {@link AssetTreeNode}. Useful for React,
|
||||
* becausse references of static functions do not change. */
|
||||
@ -45,16 +56,23 @@ export default class AssetTreeNode<Item extends backendModule.AnyAsset = backend
|
||||
}
|
||||
|
||||
/** Creates an {@link AssetTreeNode} from a {@link backendModule.AnyAsset}. */
|
||||
static fromAsset(
|
||||
static fromAsset<Asset extends backendModule.AnyAsset>(
|
||||
this: void,
|
||||
asset: backendModule.AnyAsset,
|
||||
directoryKey: backendModule.AssetId,
|
||||
asset: Asset,
|
||||
directoryKey: backendModule.DirectoryId,
|
||||
directoryId: backendModule.DirectoryId,
|
||||
depth: number,
|
||||
getKey: ((asset: backendModule.AnyAsset) => backendModule.AssetId) | null = null
|
||||
): AssetTreeNode {
|
||||
getKey ??= oldAsset => oldAsset.id
|
||||
return new AssetTreeNode(getKey(asset), asset, directoryKey, directoryId, null, depth)
|
||||
key: Asset['id'] = asset.id
|
||||
): AnyAssetTreeNode {
|
||||
return new AssetTreeNode(asset, directoryKey, directoryId, null, depth, key).asUnion()
|
||||
}
|
||||
|
||||
/** Return `this`, coerced into an {@link AnyAssetTreeNode}. */
|
||||
asUnion() {
|
||||
// This is SAFE, as an `AssetTreeNode` cannot change types, and `AnyAssetTreeNode` is an
|
||||
// exhaustive list of variants.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return this as AnyAssetTreeNode
|
||||
}
|
||||
|
||||
/** Whether this node contains a specific type of asset. */
|
||||
@ -65,24 +83,25 @@ export default class AssetTreeNode<Item extends backendModule.AnyAsset = backend
|
||||
}
|
||||
|
||||
/** Create a new {@link AssetTreeNode} with the specified properties updated. */
|
||||
with(update: Partial<AssetTreeNodeData>) {
|
||||
with(update: Partial<AssetTreeNodeData>): AnyAssetTreeNode {
|
||||
return new AssetTreeNode(
|
||||
update.key ?? this.key,
|
||||
update.item ?? this.item,
|
||||
update.directoryKey ?? this.directoryKey,
|
||||
update.directoryId ?? this.directoryId,
|
||||
// `null` MUST be special-cases in the following line.
|
||||
// eslint-disable-next-line eqeqeq
|
||||
update.children === null ? update.children : update.children ?? this.children,
|
||||
update.depth ?? this.depth
|
||||
)
|
||||
update.depth ?? this.depth,
|
||||
update.key ?? this.key
|
||||
).asUnion()
|
||||
}
|
||||
|
||||
/** Return a new {@link AssetTreeNode} array if any children would be changed by the transformation
|
||||
* function, otherwise return the original {@link AssetTreeNode} array. */
|
||||
map(transform: (node: AssetTreeNode) => AssetTreeNode) {
|
||||
map(transform: (node: AnyAssetTreeNode) => AnyAssetTreeNode): AnyAssetTreeNode {
|
||||
const children = this.children ?? []
|
||||
let result: AssetTreeNode = transform(this)
|
||||
|
||||
let result: AnyAssetTreeNode = transform(this.asUnion())
|
||||
for (let i = 0; i < children.length; i += 1) {
|
||||
const node = children[i]
|
||||
if (node == null) {
|
||||
@ -105,9 +124,9 @@ export default class AssetTreeNode<Item extends backendModule.AnyAsset = backend
|
||||
/** Return a new {@link AssetTreeNode} array if any children would be changed by the transformation
|
||||
* function, otherwise return the original {@link AssetTreeNode} array. The predicate is applied to
|
||||
* a parent node before it is applied to its children. The root node is never removed. */
|
||||
filter(predicate: (node: AssetTreeNode) => boolean) {
|
||||
filter(predicate: (node: AnyAssetTreeNode) => boolean): AnyAssetTreeNode {
|
||||
const children = this.children ?? []
|
||||
let result: AssetTreeNode | null = null
|
||||
let result: AnyAssetTreeNode | null = null
|
||||
for (let i = 0; i < children.length; i += 1) {
|
||||
const node = children[i]
|
||||
if (node == null) {
|
||||
@ -132,13 +151,15 @@ export default class AssetTreeNode<Item extends backendModule.AnyAsset = backend
|
||||
}
|
||||
}
|
||||
}
|
||||
return result?.children?.length === 0 ? result.with({ children: null }) : result ?? this
|
||||
return result?.children?.length === 0
|
||||
? result.with({ children: null })
|
||||
: result ?? this.asUnion()
|
||||
}
|
||||
|
||||
/** Returns all items in the tree, flattened into an array using pre-order traversal. */
|
||||
preorderTraversal(
|
||||
preprocess: ((tree: AssetTreeNode[]) => AssetTreeNode[]) | null = null
|
||||
): AssetTreeNode[] {
|
||||
preprocess: ((tree: AnyAssetTreeNode[]) => AnyAssetTreeNode[]) | null = null
|
||||
): AnyAssetTreeNode[] {
|
||||
return (preprocess?.(this.children ?? []) ?? this.children ?? []).flatMap(node =>
|
||||
node.children == null ? [node] : [node, ...node.preorderTraversal(preprocess)]
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user