diff --git a/app/dashboard/e2e/actions.ts b/app/dashboard/e2e/actions.ts index aaf4830d39cb..96b415d85a3d 100644 --- a/app/dashboard/e2e/actions.ts +++ b/app/dashboard/e2e/actions.ts @@ -347,12 +347,6 @@ export function locateCollapsibleDirectories(page: test.Page) { return locateAssetRows(page).filter({ has: page.locator('[aria-label=Collapse]') }) } -/** Find a "confirm delete" modal (if any) on the current page. */ -export function locateConfirmDeleteModal(page: test.Page) { - // This has no identifying features. - return page.getByTestId('confirm-delete-modal') -} - /** Find a "new label" modal (if any) on the current page. */ export function locateNewLabelModal(page: test.Page) { // This has no identifying features. @@ -365,12 +359,6 @@ export function locateUpsertSecretModal(page: test.Page) { return page.getByTestId('upsert-secret-modal') } -/** Find a "new user group" modal (if any) on the current page. */ -export function locateNewUserGroupModal(page: test.Page) { - // This has no identifying features. - return page.getByTestId('new-user-group-modal') -} - /** Find a user menu (if any) on the current page. */ export function locateUserMenu(page: test.Page) { return page.getByAltText('User Settings').locator('visible=true') diff --git a/app/dashboard/e2e/actions/contextMenuActions.ts b/app/dashboard/e2e/actions/contextMenuActions.ts index e87bd8f50e6c..47de5ea5d854 100644 --- a/app/dashboard/e2e/actions/contextMenuActions.ts +++ b/app/dashboard/e2e/actions/contextMenuActions.ts @@ -1,4 +1,5 @@ /** @file Actions for the context menu. */ +import { TEXT } from '../actions' import type * as baseActions from './BaseActions' import type BaseActions from './BaseActions' import EditorPageActions from './EditorPageActions' @@ -13,7 +14,8 @@ export interface ContextMenuActions { readonly uploadToCloud: () => T readonly rename: () => T readonly snapshot: () => T - readonly moveToTrash: () => T + readonly moveNonFolderToTrash: () => T + readonly moveFolderToTrash: () => T readonly moveAllToTrash: () => T readonly restoreFromTrash: () => T readonly restoreAllFromTrash: () => T @@ -43,97 +45,153 @@ export function contextMenuActions( return { open: () => step('Open (context menu)', (page) => - page.getByRole('button', { name: 'Open' }).getByText('Open').click(), + page.getByRole('button', { name: TEXT.openShortcut }).getByText(TEXT.openShortcut).click(), ), uploadToCloud: () => step('Upload to cloud (context menu)', (page) => - page.getByRole('button', { name: 'Upload To Cloud' }).getByText('Upload To Cloud').click(), + page + .getByRole('button', { name: TEXT.uploadToCloudShortcut }) + .getByText(TEXT.uploadToCloudShortcut) + .click(), ), rename: () => step('Rename (context menu)', (page) => - page.getByRole('button', { name: 'Rename' }).getByText('Rename').click(), + page + .getByRole('button', { name: TEXT.renameShortcut }) + .getByText(TEXT.renameShortcut) + .click(), ), snapshot: () => step('Snapshot (context menu)', (page) => - page.getByRole('button', { name: 'Snapshot' }).getByText('Snapshot').click(), + page + .getByRole('button', { name: TEXT.snapshotShortcut }) + .getByText(TEXT.snapshotShortcut) + .click(), ), - moveToTrash: () => + moveNonFolderToTrash: () => step('Move to trash (context menu)', (page) => - page.getByRole('button', { name: 'Move To Trash' }).getByText('Move To Trash').click(), + page + .getByRole('button', { name: TEXT.moveToTrashShortcut }) + .getByText(TEXT.moveToTrashShortcut) + .click(), ), + moveFolderToTrash: () => + step('Move folder to trash (context menu)', async (page) => { + await page + .getByRole('button', { name: TEXT.moveToTrashShortcut }) + .getByText(TEXT.moveToTrashShortcut) + .click() + await page.getByRole('button', { name: TEXT.delete }).getByText(TEXT.delete).click() + }), moveAllToTrash: () => step('Move all to trash (context menu)', (page) => page - .getByRole('button', { name: 'Move All To Trash' }) - .getByText('Move All To Trash') + .getByRole('button', { name: TEXT.moveAllToTrashShortcut }) + .getByText(TEXT.moveAllToTrashShortcut) .click(), ), restoreFromTrash: () => step('Restore from trash (context menu)', (page) => page - .getByRole('button', { name: 'Restore From Trash' }) - .getByText('Restore From Trash') + .getByRole('button', { name: TEXT.restoreFromTrashShortcut }) + .getByText(TEXT.restoreFromTrashShortcut) .click(), ), restoreAllFromTrash: () => step('Restore all from trash (context menu)', (page) => page - .getByRole('button', { name: 'Restore All From Trash' }) - .getByText('Restore All From Trash') + .getByRole('button', { name: TEXT.restoreAllFromTrashShortcut }) + .getByText(TEXT.restoreAllFromTrashShortcut) .click(), ), share: () => step('Share (context menu)', (page) => - page.getByRole('button', { name: 'Share' }).getByText('Share').click(), + page + .getByRole('button', { name: TEXT.shareShortcut }) + .getByText(TEXT.shareShortcut) + .click(), ), label: () => step('Label (context menu)', (page) => - page.getByRole('button', { name: 'Label' }).getByText('Label').click(), + page + .getByRole('button', { name: TEXT.labelShortcut }) + .getByText(TEXT.labelShortcut) + .click(), ), duplicate: () => step('Duplicate (context menu)', (page) => - page.getByRole('button', { name: 'Duplicate' }).getByText('Duplicate').click(), + page + .getByRole('button', { name: TEXT.duplicateShortcut }) + .getByText(TEXT.duplicateShortcut) + .click(), ), duplicateProject: () => step('Duplicate project (context menu)', (page) => - page.getByRole('button', { name: 'Duplicate' }).getByText('Duplicate').click(), + page + .getByRole('button', { name: TEXT.duplicateShortcut }) + .getByText(TEXT.duplicateShortcut) + .click(), ).into(EditorPageActions), copy: () => step('Copy (context menu)', (page) => - page.getByRole('button', { name: 'Copy' }).getByText('Copy', { exact: true }).click(), + page + .getByRole('button', { name: TEXT.copyShortcut }) + .getByText(TEXT.copyShortcut, { exact: true }) + .click(), ), cut: () => step('Cut (context menu)', (page) => - page.getByRole('button', { name: 'Cut' }).getByText('Cut').click(), + page.getByRole('button', { name: TEXT.cutShortcut }).getByText(TEXT.cutShortcut).click(), ), paste: () => step('Paste (context menu)', (page) => - page.getByRole('button', { name: 'Paste' }).getByText('Paste').click(), + page + .getByRole('button', { name: TEXT.pasteShortcut }) + .getByText(TEXT.pasteShortcut) + .click(), ), copyAsPath: () => step('Copy as path (context menu)', (page) => - page.getByRole('button', { name: 'Copy As Path' }).getByText('Copy As Path').click(), + page + .getByRole('button', { name: TEXT.copyAsPathShortcut }) + .getByText(TEXT.copyAsPathShortcut) + .click(), ), download: () => step('Download (context menu)', (page) => - page.getByRole('button', { name: 'Download' }).getByText('Download').click(), + page + .getByRole('button', { name: TEXT.downloadShortcut }) + .getByText(TEXT.downloadShortcut) + .click(), ), // TODO: Specify the files in parameters. uploadFiles: () => step('Upload files (context menu)', (page) => - page.getByRole('button', { name: 'Upload Files' }).getByText('Upload Files').click(), + page + .getByRole('button', { name: TEXT.uploadFilesShortcut }) + .getByText(TEXT.uploadFilesShortcut) + .click(), ), newFolder: () => step('New folder (context menu)', (page) => - page.getByRole('button', { name: 'New Folder' }).getByText('New Folder').click(), + page + .getByRole('button', { name: TEXT.newFolderShortcut }) + .getByText(TEXT.newFolderShortcut) + .click(), ), newSecret: () => step('New secret (context menu)', (page) => - page.getByRole('button', { name: 'New Secret' }).getByText('New Secret').click(), + page + .getByRole('button', { name: TEXT.newSecretShortcut }) + .getByText(TEXT.newSecretShortcut) + .click(), ), newDataLink: () => step('New Data Link (context menu)', (page) => - page.getByRole('button', { name: 'New Data Link' }).getByText('New Data Link').click(), + page + .getByRole('button', { name: TEXT.newDatalinkShortcut }) + .getByText(TEXT.newDatalinkShortcut) + .click(), ), } } diff --git a/app/dashboard/e2e/api.ts b/app/dashboard/e2e/api.ts index a9ccef283cf8..d527bad76ae6 100644 --- a/app/dashboard/e2e/api.ts +++ b/app/dashboard/e2e/api.ts @@ -704,7 +704,12 @@ async function mockApiInternal({ page, setupAPI }: MockParams) { new URL(request.url()).searchParams.entries(), ) as never - const file = addFile(searchParams.file_name) + const file = addFile( + searchParams.file_name, + searchParams.parent_directory_id == null ? + {} + : { parentId: searchParams.parent_directory_id }, + ) return { path: '', id: file.id, project: null } satisfies backend.FileInfo }) @@ -900,8 +905,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) { const body: backend.CreateDirectoryRequestBody = request.postDataJSON() const title = body.title const id = backend.DirectoryId(`directory-${uniqueString.uniqueString()}`) - const parentId = - body.parentId ?? backend.DirectoryId(`directory-${uniqueString.uniqueString()}`) + const parentId = body.parentId ?? defaultDirectoryId const json: backend.CreatedDirectory = { title, id, parentId } addDirectory(title, { description: null, diff --git a/app/dashboard/e2e/delete.spec.ts b/app/dashboard/e2e/delete.spec.ts index d445f9a186e3..67eec35feaeb 100644 --- a/app/dashboard/e2e/delete.spec.ts +++ b/app/dashboard/e2e/delete.spec.ts @@ -1,17 +1,16 @@ /** @file Test copying, moving, cutting and pasting. */ import * as test from '@playwright/test' -import * as actions from './actions' +import { mockAllAndLogin, TEXT } from './actions' test.test('delete and restore', ({ page }) => - actions - .mockAllAndLogin({ page }) + mockAllAndLogin({ page }) .createFolder() .driveTable.withRows(async (rows) => { await test.expect(rows).toHaveCount(1) }) .driveTable.rightClickRow(0) - .contextMenu.moveToTrash() + .contextMenu.moveFolderToTrash() .driveTable.expectPlaceholderRow() .goToCategory.trash() .driveTable.withRows(async (rows) => { @@ -27,14 +26,16 @@ test.test('delete and restore', ({ page }) => ) test.test('delete and restore (keyboard)', ({ page }) => - actions - .mockAllAndLogin({ page }) + mockAllAndLogin({ page }) .createFolder() .driveTable.withRows(async (rows) => { await test.expect(rows).toHaveCount(1) }) .driveTable.clickRow(0) .press('Delete') + .do(async (thePage) => { + await thePage.getByRole('button', { name: TEXT.delete }).getByText(TEXT.delete).click() + }) .driveTable.expectPlaceholderRow() .goToCategory.trash() .driveTable.withRows(async (rows) => { diff --git a/app/dashboard/e2e/driveView.spec.ts b/app/dashboard/e2e/driveView.spec.ts index bdb80e765f8a..7b65bf794977 100644 --- a/app/dashboard/e2e/driveView.spec.ts +++ b/app/dashboard/e2e/driveView.spec.ts @@ -37,7 +37,7 @@ test.test('drive view', ({ page }) => }) // Project context menu .driveTable.rightClickRow(0) - .contextMenu.moveToTrash() + .contextMenu.moveNonFolderToTrash() .driveTable.withRows(async (rows) => { await test.expect(rows).toHaveCount(1) }), diff --git a/app/dashboard/e2e/labelsPanel.spec.ts b/app/dashboard/e2e/labelsPanel.spec.ts index 24350aa7907b..1249f05d7fb7 100644 --- a/app/dashboard/e2e/labelsPanel.spec.ts +++ b/app/dashboard/e2e/labelsPanel.spec.ts @@ -59,6 +59,6 @@ test.test('labels', async ({ page }) => { const labelsPanel = locateLabelsPanel(page) await labelsPanel.getByRole('button').and(labelsPanel.getByLabel(TEXT.delete)).click() - await page.getByRole('button', { name: 'Delete' }).getByText('Delete').click() + await page.getByRole('button', { name: TEXT.delete }).getByText(TEXT.delete).click() test.expect(await locateLabelsPanelLabels(page).count()).toBeGreaterThanOrEqual(1) }) diff --git a/app/dashboard/src/components/dashboard/column/SharedWithColumn.tsx b/app/dashboard/src/components/dashboard/column/SharedWithColumn.tsx index ccf6a4b2af0a..f50b0eead9db 100644 --- a/app/dashboard/src/components/dashboard/column/SharedWithColumn.tsx +++ b/app/dashboard/src/components/dashboard/column/SharedWithColumn.tsx @@ -48,6 +48,7 @@ export default function SharedWithColumn(props: SharedWithColumnPropsInternal) { const dispatchAssetEvent = eventListProvider.useDispatchAssetEvent() const { isFeatureUnderPaywall } = billingHooks.usePaywall({ plan: user.plan }) const isUnderPaywall = isFeatureUnderPaywall('share') + const assetPermissions = asset.permissions ?? [] const { setModal } = modalProvider.useSetModal() const self = permissions.tryFindSelfPermission(user, asset.permissions) const plusButtonRef = React.useRef(null) @@ -70,7 +71,12 @@ export default function SharedWithColumn(props: SharedWithColumnPropsInternal) { return (
- {(asset.permissions ?? []).map((other, idx) => ( + {(category.type === 'trash' ? + assetPermissions.filter( + (permission) => permission.permission === permissions.PermissionAction.own, + ) + : assetPermissions + ).map((other, idx) => ( > = { // ===================== /** Return the full list of columns given the relevant current state. */ -export function getColumnList(user: backend.User, backendType: backend.BackendType) { +export function getColumnList( + user: backend.User, + backendType: backend.BackendType, + category: Category, +) { const isCloud = backendType === backend.BackendType.remote const isEnterprise = user.plan === backend.Plan.enterprise + const isTrash = category.type === 'trash' const columns = [ Column.name, Column.modified, - isCloud && isEnterprise && Column.sharedWith, + isCloud && (isEnterprise || isTrash) && Column.sharedWith, isCloud && Column.labels, isCloud && Column.accessedByProjects, isCloud && Column.accessedData, diff --git a/app/dashboard/src/components/dashboard/columnHeading/SharedWithColumnHeading.tsx b/app/dashboard/src/components/dashboard/columnHeading/SharedWithColumnHeading.tsx index ca629bc1dd26..e7e8b393c543 100644 --- a/app/dashboard/src/components/dashboard/columnHeading/SharedWithColumnHeading.tsx +++ b/app/dashboard/src/components/dashboard/columnHeading/SharedWithColumnHeading.tsx @@ -11,7 +11,7 @@ import { useText } from '#/providers/TextProvider' /** A heading for the "Shared with" column. */ export default function SharedWithColumnHeading(props: AssetColumnHeadingProps) { const { state } = props - const { hideColumn } = state + const { category, hideColumn } = state const { getText } = useText() const { user } = useFullUserSession() @@ -33,7 +33,11 @@ export default function SharedWithColumnHeading(props: AssetColumnHeadingProps) />
- {getText('sharedWithColumnName')} + + {category.type === 'trash' ? + getText('rootFolderColumnName') + : getText('sharedWithColumnName')} + {isUnderPaywall && ( diff --git a/app/dashboard/src/layouts/AssetContextMenu.tsx b/app/dashboard/src/layouts/AssetContextMenu.tsx index f8198d9199d6..c58c45542368 100644 --- a/app/dashboard/src/layouts/AssetContextMenu.tsx +++ b/app/dashboard/src/layouts/AssetContextMenu.tsx @@ -320,6 +320,7 @@ export default function AssetContextMenu(props: AssetContextMenuProps) { doAction={() => { setModal( { @@ -361,8 +362,18 @@ export default function AssetContextMenu(props: AssetContextMenuProps) { label={isCloud ? getText('moveToTrashShortcut') : getText('deleteShortcut')} doAction={() => { if (isCloud) { - unsetModal() - doDelete() + if (asset.type === backendModule.AssetType.directory) { + setModal( + , + ) + } else { + unsetModal() + doDelete() + } } else { setModal( )} - {!isOtherUserUsingProject && ( + {!isRunningProject && !isOtherUserUsingProject && (