diff --git a/app/ide-desktop/lib/dashboard/src/components/MenuEntry.tsx b/app/ide-desktop/lib/dashboard/src/components/MenuEntry.tsx index 89b714d7ff4..4fccdc77c60 100644 --- a/app/ide-desktop/lib/dashboard/src/components/MenuEntry.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/MenuEntry.tsx @@ -46,6 +46,7 @@ const ACTION_TO_TEXT_ID: Readonly> + readonly item: assetTreeNode.AnyAssetTreeNode + readonly setItem: React.Dispatch> readonly state: assetsTable.AssetsTableState readonly rowState: assetsTable.AssetRowState readonly setRowState: React.Dispatch> @@ -71,7 +72,7 @@ export interface AssetRowInnerProps { /** Props for an {@link AssetRow}. */ export interface AssetRowProps extends Readonly> { - 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), }) diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/DataLinkNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/DataLinkNameColumn.tsx index fe69e6b7aab..34e705cd225 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/DataLinkNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/DataLinkNameColumn.tsx @@ -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) => { diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx index e80710ea4dc..626c71a91a4 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx @@ -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 diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/FileNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/FileNameColumn.tsx index 178240fefda..601c51e10e7 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/FileNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/FileNameColumn.tsx @@ -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) => { diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectNameColumn.tsx index 2c67dfb0b8f..59e1a84f790 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectNameColumn.tsx @@ -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( diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/SecretNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/SecretNameColumn.tsx index 1d83220f300..1fba4446470 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/SecretNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/SecretNameColumn.tsx @@ -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) { diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/column.ts b/app/ide-desktop/lib/dashboard/src/components/dashboard/column.ts index 0ed2d4c89de..b6c69f94a16 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/column.ts +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/column.ts @@ -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> + readonly item: assetTreeNode.AnyAssetTreeNode + readonly setItem: React.Dispatch> readonly selected: boolean readonly setSelected: (selected: boolean) => void readonly isSoleSelected: boolean diff --git a/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts b/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts index 83b8dd469d0..1351f945972 100644 --- a/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts +++ b/app/ide-desktop/lib/dashboard/src/configurations/inputBindings.ts @@ -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. diff --git a/app/ide-desktop/lib/dashboard/src/events/assetEvent.ts b/app/ide-desktop/lib/dashboard/src/events/assetEvent.ts index 24185ba8cc6..2324f145982 100644 --- a/app/ide-desktop/lib/dashboard/src/events/assetEvent.ts +++ b/app/ide-desktop/lib/dashboard/src/events/assetEvent.ts @@ -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 { - 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 { - readonly placeholderId: backendModule.DirectoryId + readonly placeholderId: backend.DirectoryId } /** A signal to upload files. */ export interface AssetUploadFilesEvent extends AssetBaseEvent { - readonly files: ReadonlyMap + readonly files: ReadonlyMap } /** A signal to update files with new versions. */ export interface AssetUpdateFilesEvent extends AssetBaseEvent { - readonly files: ReadonlyMap + readonly files: ReadonlyMap } /** A signal to create a Data Link. */ export interface AssetNewDataLinkEvent extends AssetBaseEvent { - readonly placeholderId: backendModule.ConnectorId + readonly placeholderId: backend.ConnectorId readonly value: unknown } /** A signal to create a secret. */ export interface AssetNewSecretEvent extends AssetBaseEvent { - readonly placeholderId: backendModule.SecretId + readonly placeholderId: backend.SecretId readonly value: string } /** A signal to open the specified project. */ export interface AssetOpenProjectEvent extends AssetBaseEvent { - 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 { - 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 { - readonly ids: ReadonlySet - readonly newParentKey: backendModule.AssetId - readonly newParentId: backendModule.DirectoryId + readonly ids: ReadonlySet + readonly newParentKey: backend.AssetId + readonly newParentId: backend.DirectoryId } /** A signal to cut multiple assets. */ export interface AssetCutEvent extends AssetBaseEvent { - readonly ids: ReadonlySet + readonly ids: ReadonlySet } /** A signal that a cut operation has been cancelled. */ export interface AssetCancelCutEvent extends AssetBaseEvent { - readonly ids: ReadonlySet + readonly ids: ReadonlySet } /** A signal to move multiple assets. */ export interface AssetMoveEvent extends AssetBaseEvent { - readonly ids: ReadonlySet - readonly newParentKey: backendModule.AssetId - readonly newParentId: backendModule.DirectoryId + readonly ids: ReadonlySet + readonly newParentKey: backend.DirectoryId + readonly newParentId: backend.DirectoryId } /** A signal to delete assets. */ export interface AssetDeleteEvent extends AssetBaseEvent { - readonly ids: ReadonlySet + readonly ids: ReadonlySet } /** A signal to delete assets forever. */ export interface AssetDeleteForeverEvent extends AssetBaseEvent { - readonly ids: ReadonlySet + readonly ids: ReadonlySet } /** A signal to restore assets from trash. */ export interface AssetRestoreEvent extends AssetBaseEvent { - readonly ids: ReadonlySet + readonly ids: ReadonlySet } /** A signal to download assets. */ export interface AssetDownloadEvent extends AssetBaseEvent { - readonly ids: ReadonlySet + readonly ids: ReadonlySet } /** 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 { - readonly id: backendModule.AssetId + readonly id: backend.AssetId } /** A signal to temporarily add labels to the selected assetss. */ export interface AssetTemporarilyAddLabelsEvent extends AssetBaseEvent { - readonly ids: ReadonlySet - readonly labelNames: ReadonlySet + readonly ids: ReadonlySet + readonly labelNames: ReadonlySet } /** A signal to temporarily remove labels from the selected assetss. */ export interface AssetTemporarilyRemoveLabelsEvent extends AssetBaseEvent { - readonly ids: ReadonlySet - readonly labelNames: ReadonlySet + readonly ids: ReadonlySet + readonly labelNames: ReadonlySet } /** A signal to add labels to the selected assetss. */ export interface AssetAddLabelsEvent extends AssetBaseEvent { - readonly ids: ReadonlySet - readonly labelNames: ReadonlySet + readonly ids: ReadonlySet + readonly labelNames: ReadonlySet } /** A signal to remove labels from the selected assetss. */ export interface AssetRemoveLabelsEvent extends AssetBaseEvent { - readonly ids: ReadonlySet - readonly labelNames: ReadonlySet + readonly ids: ReadonlySet + readonly labelNames: ReadonlySet } /** A signal to remove a label from all assets. */ export interface AssetDeleteLabelEvent extends AssetBaseEvent { - readonly labelName: backendModule.LabelName + readonly labelName: backend.LabelName } /** Every possible type of asset event. */ diff --git a/app/ide-desktop/lib/dashboard/src/events/assetListEvent.ts b/app/ide-desktop/lib/dashboard/src/events/assetListEvent.ts index 4262121ff3f..54a3abfe3bc 100644 --- a/app/ide-desktop/lib/dashboard/src/events/assetListEvent.ts +++ b/app/ide-desktop/lib/dashboard/src/events/assetListEvent.ts @@ -62,7 +62,8 @@ interface AssetListNewProjectEvent extends AssetListBaseEvent void) | null } @@ -104,7 +105,7 @@ interface AssetListCloseFolderEvent extends AssetListBaseEvent { - readonly newParentKey: backend.AssetId + readonly newParentKey: backend.DirectoryId readonly newParentId: backend.DirectoryId readonly items: backend.AnyAsset[] } @@ -112,7 +113,7 @@ interface AssetListCopyEvent extends AssetListBaseEvent /** A signal that a file has been moved. */ interface AssetListMoveEvent extends AssetListBaseEvent { readonly key: backend.AssetId - readonly newParentKey: backend.AssetId + readonly newParentKey: backend.DirectoryId readonly newParentId: backend.DirectoryId readonly item: backend.AnyAsset } diff --git a/app/ide-desktop/lib/dashboard/src/hooks/setAssetHooks.ts b/app/ide-desktop/lib/dashboard/src/hooks/setAssetHooks.ts index e0c6d7ef31e..9d1275a3a9f 100644 --- a/app/ide-desktop/lib/dashboard/src/hooks/setAssetHooks.ts +++ b/app/ide-desktop/lib/dashboard/src/hooks/setAssetHooks.ts @@ -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( _value: T, - setNode: React.Dispatch> + setNode: React.Dispatch> ) { return React.useCallback( (valueOrUpdater: React.SetStateAction) => { diff --git a/app/ide-desktop/lib/dashboard/src/layouts/AssetContextMenu.tsx b/app/ide-desktop/lib/dashboard/src/layouts/AssetContextMenu.tsx index 47818855b14..94992931b84 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/AssetContextMenu.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/AssetContextMenu.tsx @@ -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) { ) : (