mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 22:10:15 +03:00
Local dashboard fixes (#11066)
- Fix https://github.com/enso-org/cloud-v2/issues/1479 - See #11059 for rest of fixes. - When you add a new folder it is now selected and in rename mode - Double click opening no longer adds (2) to the name when there are no duplicates - When dragging a file to upload, the "Drop here to upload box" now properly disappears after dropping - Fix removing folder from favorites list Other changes: - Switch "delete user" modal to new "Dialog" and "Form" components instead of old "Modal" and HTML "form" components # Important Notes None
This commit is contained in:
parent
bdadedbde5
commit
066d4ea609
@ -249,9 +249,12 @@ export default class DrivePageActions extends PageActions {
|
||||
|
||||
/** Create a new folder using the icon in the Drive Bar. */
|
||||
createFolder() {
|
||||
return this.step('Create folder', (page) =>
|
||||
page.getByRole('button', { name: TEXT.newFolder, exact: true }).click(),
|
||||
)
|
||||
return this.step('Create folder', async (page) => {
|
||||
await page.getByRole('button', { name: TEXT.newFolder, exact: true }).click()
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
await test.expect(page.locator('input:focus')).toBeVisible()
|
||||
await page.keyboard.press('Escape')
|
||||
})
|
||||
}
|
||||
|
||||
/** Upload a file using the icon in the Drive Bar. */
|
||||
|
@ -161,7 +161,10 @@ export const AssetRow = React.memo(function AssetRow(props: AssetRowProps) {
|
||||
const asset = item.item
|
||||
const [insertionVisibility, setInsertionVisibility] = React.useState(Visibility.visible)
|
||||
const [rowState, setRowState] = React.useState<assetsTable.AssetRowState>(() =>
|
||||
object.merge(assetRowUtils.INITIAL_ROW_STATE, { setVisibility: setInsertionVisibility }),
|
||||
object.merge(assetRowUtils.INITIAL_ROW_STATE, {
|
||||
setVisibility: setInsertionVisibility,
|
||||
isEditingName: driveStore.getState().newestFolderId === asset.id,
|
||||
}),
|
||||
)
|
||||
const nodeParentKeysRef = React.useRef<{
|
||||
readonly nodeMap: WeakRef<ReadonlyMap<backendModule.AssetId, assetTreeNode.AnyAssetTreeNode>>
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
useSetCanCreateAssets,
|
||||
useSetCanDownload,
|
||||
useSetIsAssetPanelTemporarilyVisible,
|
||||
useSetNewestFolderId,
|
||||
useSetSelectedKeys,
|
||||
useSetSuggestions,
|
||||
useSetTargetDirectory,
|
||||
@ -340,6 +341,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
const [sortInfo, setSortInfo] =
|
||||
React.useState<sorting.SortInfo<columnUtils.SortableColumn> | null>(null)
|
||||
const driveStore = useDriveStore()
|
||||
const setNewestFolderId = useSetNewestFolderId()
|
||||
const setSelectedKeys = useSetSelectedKeys()
|
||||
const setVisuallySelectedKeys = useSetVisuallySelectedKeys()
|
||||
const updateAssetRef = React.useRef<
|
||||
@ -834,6 +836,10 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
},
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
setNewestFolderId(null)
|
||||
}, [category, setNewestFolderId])
|
||||
|
||||
React.useEffect(
|
||||
() =>
|
||||
driveStore.subscribe(({ selectedKeys }, { selectedKeys: oldSelectedKeys }) => {
|
||||
@ -1574,9 +1580,12 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
doToggleDirectoryExpansion(event.parentId, event.parentKey, true)
|
||||
insertAssets([placeholderItem], event.parentId)
|
||||
|
||||
createDirectoryMutation.mutate([
|
||||
{ parentId: placeholderItem.parentId, title: placeholderItem.title },
|
||||
])
|
||||
void createDirectoryMutation
|
||||
.mutateAsync([{ parentId: placeholderItem.parentId, title: placeholderItem.title }])
|
||||
.then(({ id }) => {
|
||||
setNewestFolderId(id)
|
||||
setSelectedKeys(new Set([id]))
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
@ -1666,6 +1675,12 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
userGroups ?? [],
|
||||
)
|
||||
const fileMap = new Map<backendModule.AssetId, File>()
|
||||
const uploadedFileIds: backendModule.AssetId[] = []
|
||||
const addIdToSelection = (id: backendModule.AssetId) => {
|
||||
uploadedFileIds.push(id)
|
||||
const newIds = new Set(uploadedFileIds)
|
||||
setSelectedKeys(newIds)
|
||||
}
|
||||
|
||||
const doUploadFile = async (asset: backendModule.AnyAsset, method: 'new' | 'update') => {
|
||||
const file = fileMap.get(asset.id)
|
||||
@ -1680,13 +1695,10 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
|
||||
const assetNode = nodeMapRef.current.get(asset.id)
|
||||
|
||||
if (assetNode == null) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return
|
||||
}
|
||||
|
||||
if (backend.type === backendModule.BackendType.local && localBackend != null) {
|
||||
const directory = localBackendModule.extractTypeAndId(assetNode.directoryId).id
|
||||
const directory = localBackendModule.extractTypeAndId(
|
||||
assetNode?.directoryId ?? asset.parentId,
|
||||
).id
|
||||
let id: string
|
||||
if (
|
||||
'backendApi' in window &&
|
||||
@ -1714,6 +1726,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
id = await response.text()
|
||||
}
|
||||
const projectId = localBackendModule.newProjectId(projectManager.UUID(id))
|
||||
addIdToSelection(projectId)
|
||||
|
||||
await getProjectDetailsMutation
|
||||
.mutateAsync([projectId, asset.parentId, file.name])
|
||||
@ -1731,6 +1744,9 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
},
|
||||
file,
|
||||
])
|
||||
.then(({ id }) => {
|
||||
addIdToSelection(id)
|
||||
})
|
||||
.catch((error) => {
|
||||
deleteAsset(asset.id)
|
||||
toastAndLog('uploadProjectError', error)
|
||||
@ -1740,10 +1756,14 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
break
|
||||
}
|
||||
case backendModule.assetIsFile(asset): {
|
||||
uploadFileMutation.mutate([
|
||||
{ fileId, fileName: asset.title, parentDirectoryId: asset.parentId },
|
||||
file,
|
||||
])
|
||||
void uploadFileMutation
|
||||
.mutateAsync([
|
||||
{ fileId, fileName: asset.title, parentDirectoryId: asset.parentId },
|
||||
file,
|
||||
])
|
||||
.then(({ id }) => {
|
||||
addIdToSelection(id)
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
@ -2174,6 +2194,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
}
|
||||
|
||||
const handleFileDrop = (event: React.DragEvent) => {
|
||||
setIsDraggingFiles(false)
|
||||
if (event.dataTransfer.types.includes('Files')) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@ -2542,10 +2563,6 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
},
|
||||
)
|
||||
|
||||
const onRowDragLeave = useEventCallback(() => {
|
||||
setIsDraggingFiles(false)
|
||||
})
|
||||
|
||||
const onRowDragEnd = useEventCallback(() => {
|
||||
setIsDraggingFiles(false)
|
||||
endAutoScroll()
|
||||
@ -2669,7 +2686,6 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
select={selectRow}
|
||||
onDragStart={onRowDragStart}
|
||||
onDragOver={onRowDragOver}
|
||||
onDragLeave={onRowDragLeave}
|
||||
onDragEnd={onRowDragEnd}
|
||||
onDrop={onRowDrop}
|
||||
/>
|
||||
@ -2759,11 +2775,11 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
)}
|
||||
onDragEnter={onDropzoneDragOver}
|
||||
onDragOver={onDropzoneDragOver}
|
||||
onDragLeave={(event) => {
|
||||
onDragLeave={() => {
|
||||
lastSelectedIdsRef.current = null
|
||||
if (event.currentTarget === event.target) {
|
||||
setIsDraggingFiles(false)
|
||||
}
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
setIsDraggingFiles(false)
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
const payload = drag.ASSET_ROWS.lookup(event)
|
||||
@ -2852,9 +2868,11 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
!event.currentTarget.contains(event.relatedTarget)
|
||||
) {
|
||||
lastSelectedIdsRef.current = null
|
||||
setIsDraggingFiles(false)
|
||||
}
|
||||
},
|
||||
onDragEnd: () => {
|
||||
setIsDraggingFiles(false)
|
||||
},
|
||||
})}
|
||||
>
|
||||
{!hidden && hiddenContextMenu}
|
||||
@ -2913,12 +2931,9 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
{isDraggingFiles && !isMainDropzoneVisible && (
|
||||
<div className="pointer-events-none absolute bottom-4 left-1/2 -translate-x-1/2">
|
||||
<div
|
||||
className="flex items-center justify-center gap-3 rounded-default bg-selected-frame px-8 py-6 text-primary/50 backdrop-blur-3xl transition-all"
|
||||
className="pointer-events-auto flex items-center justify-center gap-3 rounded-default bg-selected-frame px-8 py-6 text-primary/50 backdrop-blur-3xl transition-all"
|
||||
onDragEnter={onDropzoneDragOver}
|
||||
onDragOver={onDropzoneDragOver}
|
||||
onDragLeave={() => {
|
||||
setIsDraggingFiles(false)
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
setIsDraggingFiles(false)
|
||||
}}
|
||||
|
@ -302,7 +302,6 @@ export default function CategorySwitcher(props: CategorySwitcherProps) {
|
||||
const [localRootDirectories, setLocalRootDirectories] =
|
||||
useLocalStorageState('localRootDirectories')
|
||||
const hasUserAndTeamSpaces = backend.userHasUserAndTeamSpaces(user)
|
||||
const { setModal } = modalProvider.useSetModal()
|
||||
|
||||
const localBackend = backendProvider.useLocalBackend()
|
||||
const itemProps = { currentCategory: category, setCategory, dispatchAssetEvent }
|
||||
@ -501,31 +500,27 @@ export default function CategorySwitcher(props: CategorySwitcherProps) {
|
||||
dropZoneLabel={getText('localCategoryDropZoneLabel')}
|
||||
/>
|
||||
<div className="grow" />
|
||||
<ariaComponents.Button
|
||||
size="medium"
|
||||
variant="icon"
|
||||
icon={Minus2Icon}
|
||||
aria-label={getText('removeDirectoryFromFavorites')}
|
||||
className="hidden group-hover:block"
|
||||
onPress={() => {
|
||||
setModal(
|
||||
<ConfirmDeleteModal
|
||||
actionText={getText(
|
||||
'removeTheLocalDirectoryXFromFavorites',
|
||||
getFileName(directory),
|
||||
)}
|
||||
actionButtonLabel={getText('remove')}
|
||||
doDelete={() => {
|
||||
setLocalRootDirectories(
|
||||
localRootDirectories.filter(
|
||||
(otherDirectory) => otherDirectory !== directory,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<ariaComponents.DialogTrigger>
|
||||
<ariaComponents.Button
|
||||
size="medium"
|
||||
variant="icon"
|
||||
icon={Minus2Icon}
|
||||
aria-label={getText('removeDirectoryFromFavorites')}
|
||||
className="hidden group-hover:block"
|
||||
/>
|
||||
<ConfirmDeleteModal
|
||||
actionText={getText(
|
||||
'removeTheLocalDirectoryXFromFavorites',
|
||||
getFileName(directory),
|
||||
)}
|
||||
actionButtonLabel={getText('remove')}
|
||||
doDelete={() => {
|
||||
setLocalRootDirectories(
|
||||
localRootDirectories.filter((otherDirectory) => otherDirectory !== directory),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</ariaComponents.DialogTrigger>
|
||||
</div>
|
||||
))}
|
||||
{localBackend && window.fileBrowserApi && (
|
||||
|
@ -2,7 +2,6 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import * as authProvider from '#/providers/AuthProvider'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
@ -18,7 +17,6 @@ import ConfirmDeleteUserModal from '#/modals/ConfirmDeleteUserModal'
|
||||
/** Settings tab for deleting the current user. */
|
||||
export default function DeleteUserAccountSettingsSection() {
|
||||
const { signOut, deleteUser } = authProvider.useAuth()
|
||||
const { setModal } = modalProvider.useSetModal()
|
||||
const { getText } = textProvider.useText()
|
||||
|
||||
return (
|
||||
@ -32,22 +30,17 @@ export default function DeleteUserAccountSettingsSection() {
|
||||
{getText('dangerZone')}
|
||||
</aria.Heading>
|
||||
<div className="flex gap-2">
|
||||
<ariaComponents.Button
|
||||
size="medium"
|
||||
variant="delete"
|
||||
onPress={() => {
|
||||
setModal(
|
||||
<ConfirmDeleteUserModal
|
||||
doDelete={async () => {
|
||||
await deleteUser()
|
||||
await signOut()
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
}}
|
||||
>
|
||||
{getText('deleteUserAccountButtonLabel')}
|
||||
</ariaComponents.Button>
|
||||
<ariaComponents.DialogTrigger>
|
||||
<ariaComponents.Button size="medium" variant="delete">
|
||||
{getText('deleteUserAccountButtonLabel')}
|
||||
</ariaComponents.Button>
|
||||
<ConfirmDeleteUserModal
|
||||
doDelete={async () => {
|
||||
await deleteUser()
|
||||
await signOut()
|
||||
}}
|
||||
/>
|
||||
</ariaComponents.DialogTrigger>
|
||||
<aria.Text className="my-auto">{getText('deleteUserAccountWarning')}</aria.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,14 +1,8 @@
|
||||
/** @file Modal for confirming delete of any type of asset. */
|
||||
import * as React from 'react'
|
||||
import * as z from 'zod'
|
||||
|
||||
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
||||
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
import * as aria from '#/components/aria'
|
||||
import * as ariaComponents from '#/components/AriaComponents'
|
||||
import Modal from '#/components/Modal'
|
||||
import { ButtonGroup, Dialog, Form, Text } from '#/components/AriaComponents'
|
||||
import { useText } from '#/providers/TextProvider'
|
||||
|
||||
// ==============================
|
||||
// === ConfirmDeleteUserModal ===
|
||||
@ -22,53 +16,32 @@ export interface ConfirmDeleteUserModalProps {
|
||||
/** A modal for confirming the deletion of a user. */
|
||||
export default function ConfirmDeleteUserModal(props: ConfirmDeleteUserModalProps) {
|
||||
const { doDelete } = props
|
||||
const { unsetModal } = modalProvider.useSetModal()
|
||||
const { getText } = textProvider.useText()
|
||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||
|
||||
const doSubmit = async () => {
|
||||
unsetModal()
|
||||
try {
|
||||
await doDelete()
|
||||
} catch (error) {
|
||||
toastAndLog(null, error)
|
||||
}
|
||||
}
|
||||
const { getText } = useText()
|
||||
|
||||
return (
|
||||
<Modal centered className="bg-dim">
|
||||
<form
|
||||
<Dialog title={getText('areYouSure')} className="items-center">
|
||||
<Form
|
||||
schema={z.object({})}
|
||||
method="dialog"
|
||||
data-testid="confirm-delete-modal"
|
||||
ref={(element) => {
|
||||
element?.focus()
|
||||
}}
|
||||
tabIndex={-1}
|
||||
className="pointer-events-auto relative flex w-confirm-delete-user-modal flex-col items-center gap-modal rounded-default p-modal-wide pt-modal before:absolute before:inset before:h-full before:w-full before:rounded-default before:bg-selected-frame before:backdrop-blur-default"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
void doSubmit()
|
||||
}}
|
||||
onSubmit={doDelete}
|
||||
>
|
||||
<aria.Heading className="py-heading relative h-heading text-xl font-bold">
|
||||
{getText('areYouSure')}
|
||||
</aria.Heading>
|
||||
<aria.Text className="relative mb-2 text-balance text-center">
|
||||
<Text className="text-balance text-center">
|
||||
{getText('confirmDeleteUserAccountWarning')}
|
||||
</aria.Text>
|
||||
<ariaComponents.ButtonGroup className="relative self-center">
|
||||
<ariaComponents.Button
|
||||
size="custom"
|
||||
variant="custom"
|
||||
className="button relative bg-danger text-inversed active"
|
||||
onPress={doSubmit}
|
||||
>
|
||||
<aria.Text className="text">{getText('confirmDeleteUserAccountButtonLabel')}</aria.Text>
|
||||
</ariaComponents.Button>
|
||||
</ariaComponents.ButtonGroup>
|
||||
</form>
|
||||
</Modal>
|
||||
</Text>
|
||||
<ButtonGroup className="w-min self-center">
|
||||
<Form.Submit variant="delete">
|
||||
{getText('confirmDeleteUserAccountButtonLabel')}
|
||||
</Form.Submit>
|
||||
</ButtonGroup>
|
||||
</Form>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import type { Suggestion } from '#/layouts/AssetSearchBar'
|
||||
import { useLocalStorage } from '#/providers/LocalStorageProvider'
|
||||
import type AssetTreeNode from '#/utilities/AssetTreeNode'
|
||||
import { EMPTY_SET } from '#/utilities/set'
|
||||
import type { AssetId, DirectoryAsset } from 'enso-common/src/services/Backend'
|
||||
import type { AssetId, DirectoryAsset, DirectoryId } from 'enso-common/src/services/Backend'
|
||||
import { EMPTY_ARRAY } from 'enso-common/src/utilities/data/array'
|
||||
|
||||
// ==================
|
||||
@ -20,6 +20,8 @@ import { EMPTY_ARRAY } from 'enso-common/src/utilities/data/array'
|
||||
interface DriveStore {
|
||||
readonly targetDirectory: AssetTreeNode<DirectoryAsset> | null
|
||||
readonly setTargetDirectory: (targetDirectory: AssetTreeNode<DirectoryAsset> | null) => void
|
||||
readonly newestFolderId: DirectoryId | null
|
||||
readonly setNewestFolderId: (newestFolderId: DirectoryId | null) => void
|
||||
readonly canCreateAssets: boolean
|
||||
readonly setCanCreateAssets: (canCreateAssets: boolean) => void
|
||||
readonly canDownload: boolean
|
||||
@ -61,11 +63,15 @@ export default function DriveProvider(props: ProjectsProviderProps) {
|
||||
const { localStorage } = useLocalStorage()
|
||||
const [store] = React.useState(() =>
|
||||
zustand.createStore<DriveStore>((set, get) => ({
|
||||
canCreateAssets: true,
|
||||
targetDirectory: null,
|
||||
setTargetDirectory: (targetDirectory) => {
|
||||
set({ targetDirectory })
|
||||
},
|
||||
newestFolderId: null,
|
||||
setNewestFolderId: (newestFolderId) => {
|
||||
set({ newestFolderId })
|
||||
},
|
||||
canCreateAssets: true,
|
||||
setCanCreateAssets: (canCreateAssets) => {
|
||||
if (get().canCreateAssets !== canCreateAssets) {
|
||||
set({ canCreateAssets })
|
||||
@ -138,26 +144,30 @@ export function useDriveStore() {
|
||||
return store
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// === useTargetDirectory ===
|
||||
// ==========================
|
||||
|
||||
/** A function to get the target directory of the Asset Table selection. */
|
||||
/** Get the target directory of the Asset Table selection. */
|
||||
export function useTargetDirectory() {
|
||||
const store = useDriveStore()
|
||||
return zustand.useStore(store, (state) => state.targetDirectory)
|
||||
}
|
||||
|
||||
// =============================
|
||||
// === useSetTargetDirectory ===
|
||||
// =============================
|
||||
|
||||
/** A function to set the target directory of the Asset Table selection. */
|
||||
export function useSetTargetDirectory() {
|
||||
const store = useDriveStore()
|
||||
return zustand.useStore(store, (state) => state.setTargetDirectory)
|
||||
}
|
||||
|
||||
/** Get the ID of the most newly created folder. */
|
||||
export function useNewestFolderId() {
|
||||
const store = useDriveStore()
|
||||
return zustand.useStore(store, (state) => state.newestFolderId)
|
||||
}
|
||||
|
||||
/** A function to set the ID of the most newly created folder. */
|
||||
export function useSetNewestFolderId() {
|
||||
const store = useDriveStore()
|
||||
return zustand.useStore(store, (state) => state.setNewestFolderId)
|
||||
}
|
||||
|
||||
/** Whether assets can be created in the current directory. */
|
||||
export function useCanCreateAssets() {
|
||||
const store = useDriveStore()
|
||||
|
@ -422,9 +422,11 @@ export function bumpMetadata(
|
||||
let index: number | null = null
|
||||
const prefix = `${currentName} `
|
||||
for (const sibling of fs.readdirSync(parentDirectory, { withFileTypes: true })) {
|
||||
if (sibling.isDirectory()) {
|
||||
const siblingPath = pathModule.join(parentDirectory, sibling.name)
|
||||
if (siblingPath === projectRoot) {
|
||||
continue
|
||||
} else if (sibling.isDirectory()) {
|
||||
try {
|
||||
const siblingPath = pathModule.join(parentDirectory, sibling.name)
|
||||
const siblingName = getPackageName(siblingPath)
|
||||
if (siblingName === currentName) {
|
||||
index = index ?? 2
|
||||
|
Loading…
Reference in New Issue
Block a user