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:
somebody1234 2024-09-14 02:49:14 +10:00 committed by GitHub
parent bdadedbde5
commit 066d4ea609
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 127 additions and 133 deletions

View File

@ -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. */

View File

@ -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>>

View File

@ -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)
}}

View File

@ -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 && (

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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()

View File

@ -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