Skip to content

Commit

Permalink
Dashboard fixes (#11059)
Browse files Browse the repository at this point in the history
- Fix enso-org/cloud-v2#1482
- Fix "clear trash" not doing anything
- Show "shared with" column as "root folder", but only owners (TODO: actually it should only show the first item in the permissions list)
- Fix enso-org/cloud-v2#1480
- Fix importing .enso-projects not doing anything
- Could not repro crash. Also note that we do want to be able to upload files
- Partly address enso-org/cloud-v2#1479
- Remove "invite" and "share" buttons on top right
- Hide "upgrade" button on top right if on Team or Enterprise plan
- Warn when deleting folder (for now warns for all folders otherwise we may need to expand the folder to know whether it is empty)
- Fix importing projects via upload button
- Fix importing projects via drag
- Hide cut on running projects
- Partly address enso-org/cloud-v2#1481
- Fix "edit secret" not showing dialog
- Fix multiple selection context menu in Team space

# Important Notes
None

(cherry picked from commit cad50a5)
  • Loading branch information
somebody1234 authored and jdunkerley committed Sep 16, 2024
1 parent c67a7d9 commit 573bae6
Show file tree
Hide file tree
Showing 21 changed files with 175 additions and 102 deletions.
12 changes: 0 additions & 12 deletions app/dashboard/e2e/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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')
Expand Down
110 changes: 84 additions & 26 deletions app/dashboard/e2e/actions/contextMenuActions.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -13,7 +14,8 @@ export interface ContextMenuActions<T extends BaseActions> {
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
Expand Down Expand Up @@ -43,97 +45,153 @@ export function contextMenuActions<T extends BaseActions>(
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(),
),
}
}
10 changes: 7 additions & 3 deletions app/dashboard/e2e/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 7 additions & 6 deletions app/dashboard/e2e/delete.spec.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion app/dashboard/e2e/driveView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}),
Expand Down
2 changes: 1 addition & 1 deletion app/dashboard/e2e/labelsPanel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLButtonElement>(null)
Expand All @@ -70,7 +71,12 @@ export default function SharedWithColumn(props: SharedWithColumnPropsInternal) {

return (
<div className="group flex items-center gap-column-items">
{(asset.permissions ?? []).map((other, idx) => (
{(category.type === 'trash' ?
assetPermissions.filter(
(permission) => permission.permission === permissions.PermissionAction.own,
)
: assetPermissions
).map((other, idx) => (
<PermissionDisplay
key={backendModule.getAssetPermissionId(other) + idx}
action={other.permission}
Expand Down
10 changes: 8 additions & 2 deletions app/dashboard/src/components/dashboard/column/columnUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import PeopleIcon from '#/assets/people.svg'
import TagIcon from '#/assets/tag.svg'
import TimeIcon from '#/assets/time.svg'

import type { Category } from '#/layouts/CategorySwitcher/Category'
import * as backend from '#/services/Backend'

// =============
Expand Down Expand Up @@ -82,13 +83,18 @@ export const COLUMN_CSS_CLASS: Readonly<Record<Column, string>> = {
// =====================

/** 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -33,7 +33,11 @@ export default function SharedWithColumnHeading(props: AssetColumnHeadingProps)
/>

<div className="flex items-center gap-1">
<Text className="text-sm font-semibold">{getText('sharedWithColumnName')}</Text>
<Text className="text-sm font-semibold">
{category.type === 'trash' ?
getText('rootFolderColumnName')
: getText('sharedWithColumnName')}
</Text>

{isUnderPaywall && (
<PaywallDialogButton feature="share" variant="icon" children={false} size="medium" />
Expand Down
Loading

0 comments on commit 573bae6

Please sign in to comment.