Skip to content

Commit

Permalink
Merge branch 'develop' into wip/frizi/bazel
Browse files Browse the repository at this point in the history
  • Loading branch information
vitvakatu committed Jan 3, 2025
2 parents 4a4b4ba + 21531e3 commit 4893370
Show file tree
Hide file tree
Showing 71 changed files with 1,425 additions and 874 deletions.
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# Next Next Release
# Next Release

#### Enso IDE

- [ENSO_IDE_MAPBOX_API_TOKEN environment variable should be provided to enable
GeoMap visualization][11889].
- [Round ‘Add component’ button under the component menu replaced by a small
button protruding from the output port.][11836].
- [Fixed nodes being selected after deleting other nodes or connections.][11902]
- [Redo stack is no longer lost when interacting with text literals][11908].

[11889]: https://github.com/enso-org/enso/pull/11889
[11836]: https://github.com/enso-org/enso/pull/11836
[11902]: https://github.com/enso-org/enso/pull/11902
[11908]: https://github.com/enso-org/enso/pull/11908

#### Enso Language & Runtime

Expand All @@ -24,7 +28,7 @@
[11856]: https://github.com/enso-org/enso/pull/11856
[11897]: https://github.com/enso-org/enso/pull/11897

# Next Release
# Enso 2024.5

#### Enso IDE

Expand Down
3 changes: 2 additions & 1 deletion app/common/src/services/Backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1290,7 +1290,8 @@ export interface CreateSecretRequestBody {

/** HTTP request body for the "update secret" endpoint. */
export interface UpdateSecretRequestBody {
readonly value: string
readonly title: string | null
readonly value: string | null
}

/** HTTP request body for the "create datalink" endpoint. */
Expand Down
5 changes: 1 addition & 4 deletions app/common/src/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
import ENGLISH from './text/english.json' with { type: 'json' }
import { unsafeKeys } from './utilities/data/object'

// =============
// === Types ===
// =============

/** Possible languages in which to display text. */
export enum Language {
english = 'english',
Expand Down Expand Up @@ -46,6 +42,7 @@ interface PlaceholderOverrides {
readonly confirmPrompt: [action: string]
readonly trashTheAssetTypeTitle: [assetType: string, assetName: string]
readonly deleteTheAssetTypeTitle: [assetType: string, assetName: string]
readonly deleteTheAssetTypeTitleForever: [assetType: string, assetName: string]
readonly couldNotInviteUser: [userEmail: string]
readonly filesWithoutConflicts: [fileCount: number]
readonly projectsWithoutConflicts: [projectCount: number]
Expand Down
4 changes: 3 additions & 1 deletion app/common/src/text/english.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@
"thisFolderFailedToFetch": "This folder failed to fetch.",
"yourTrashIsEmpty": "Your trash is empty.",
"deleteTheAssetTypeTitle": "delete the $0 '$1'",
"deleteTheAssetTypeTitleForever": "permanently delete the $0 '$1'",
"trashTheAssetTypeTitle": "move the $0 '$1' to Trash",
"notImplemetedYet": "Not implemented yet.",
"newLabelButtonLabel": "New label",
Expand Down Expand Up @@ -456,7 +457,8 @@
"youHaveNoRecentProjects": "You have no recent projects. Switch to another category to create a project.",
"youHaveNoFiles": "This folder is empty. You can create a project using the buttons above.",
"placeholderChatPrompt": "Login or register to access live chat with our support team.",
"confirmPrompt": "Are you sure you want to $0?",
"confirmPrompt": "Do you really want to $0?",
"thisOperationCannotBeUndone": "This operation is final and cannot be undone.",
"couldNotInviteUser": "Could not invite user $0",
"inviteFormSeatsLeft": "You have $0 seats left on your plan. Upgrade to invite more",
"inviteFormSeatsLeftError": "You have exceed the number of seats on your plan by $0",
Expand Down
73 changes: 63 additions & 10 deletions app/gui/integration-test/dashboard/actions/BaseActions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/** @file The base class from which all `Actions` classes are derived. */
import { expect, test, type Locator, type Page } from '@playwright/test'

import type { AutocompleteKeybind } from '#/utilities/inputBindings'
import type { AutocompleteKeybind, ModifierKey } from '#/utilities/inputBindings'

/** `Meta` (`Cmd`) on macOS, and `Control` on all other platforms. */
async function modModifier(page: Page) {
export async function modModifier(page: Page) {
let userAgent = ''
await test.step('Detect browser OS', async () => {
userAgent = await page.evaluate(() => navigator.userAgent)
Expand Down Expand Up @@ -51,11 +51,17 @@ export default class BaseActions<Context> implements Promise<void> {
}

/**
* Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms.
* Return the appropriate key for a shortcut, replacing the text `Mod` with `Meta` (`Cmd`) on macOS,
* and `Control` on all other platforms. Similarly, replace the text `Delete` with `Backspace`
* on `macOS`, and `Delete` on all other platforms.
*/
static press(page: Page, keyOrShortcut: string): Promise<void> {
return test.step(`Press '${keyOrShortcut}'`, async () => {
static async withNormalizedKey(
page: Page,
keyOrShortcut: string,
callback: (shortcut: string) => Promise<void>,
description = 'Normalize',
): Promise<void> {
return test.step(`${description} '${keyOrShortcut}'`, async () => {
if (/\bMod\b|\bDelete\b/.test(keyOrShortcut)) {
let userAgent = ''
await test.step('Detect browser OS', async () => {
Expand All @@ -65,13 +71,23 @@ export default class BaseActions<Context> implements Promise<void> {
const ctrlKey = isMacOS ? 'Meta' : 'Control'
const deleteKey = isMacOS ? 'Backspace' : 'Delete'
const shortcut = keyOrShortcut.replace(/\bMod\b/, ctrlKey).replace(/\bDelete\b/, deleteKey)
await page.keyboard.press(shortcut)
return await callback(shortcut)
} else {
await page.keyboard.press(keyOrShortcut)
return callback(keyOrShortcut)
}
})
}

/** Press a key or shortcut. */
static async press(page: Page, keyOrShortcut: string) {
await BaseActions.withNormalizedKey(
page,
keyOrShortcut,
(shortcut) => page.keyboard.press(shortcut),
'Press and release',
)
}

/** Proxies the `then` method of the internal {@link Promise}. */
async then<T, E>(
onfulfilled?: (() => PromiseLike<T> | T) | null | undefined,
Expand Down Expand Up @@ -135,8 +151,45 @@ export default class BaseActions<Context> implements Promise<void> {
* Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms.
*/
press<Key extends string>(keyOrShortcut: AutocompleteKeybind<Key>) {
return this.do((page) => BaseActions.press(page, keyOrShortcut))
press<Key extends string>(keyOrShortcut: AutocompleteKeybind<Key> | ModifierKey) {
return this.do((page) =>
BaseActions.withNormalizedKey(
page,
keyOrShortcut,
(shortcut) => page.keyboard.press(shortcut),
'Press and release',
),
)
}

/**
* Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms.
*/
down<Key extends string>(keyOrShortcut: AutocompleteKeybind<Key> | ModifierKey) {
return this.do((page) =>
BaseActions.withNormalizedKey(
page,
keyOrShortcut,
(shortcut) => page.keyboard.down(shortcut),
'Press',
),
)
}

/**
* Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms.
*/
up<Key extends string>(keyOrShortcut: AutocompleteKeybind<Key> | ModifierKey) {
return this.do((page) =>
BaseActions.withNormalizedKey(
page,
keyOrShortcut,
(shortcut) => page.keyboard.up(shortcut),
'Release',
),
)
}

/** Perform actions until a predicate passes. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,14 @@ export default class DrivePageActions<Context> extends PageActions<Context> {
})
}

/** Clear trash. */
clearTrash() {
return this.step('Clear trash', async (page) => {
await page.getByText(TEXT.clearTrash).click()
await page.getByRole('button', { name: TEXT.delete }).getByText(TEXT.delete).click()
})
}

/** Create a new empty project. */
newEmptyProject() {
return this.step(
Expand Down
75 changes: 71 additions & 4 deletions app/gui/integration-test/dashboard/actions/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const INITIAL_CALLS_OBJECT = {
updateDirectory: array<
{ directoryId: backend.DirectoryId } & backend.UpdateDirectoryRequestBody
>(),
deleteAsset: array<{ assetId: backend.AssetId }>(),
deleteAsset: array<{ assetId: backend.AssetId; force: boolean }>(),
undoDeleteAsset: array<{ assetId: backend.AssetId }>(),
createUser: array<backend.CreateUserRequestBody>(),
createUserGroup: array<backend.CreateUserGroupRequestBody>(),
Expand Down Expand Up @@ -283,6 +283,17 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
return !alreadyDeleted
}

const forceDeleteAsset = (assetId: backend.AssetId) => {
const hasAsset = assetMap.has(assetId)
deletedAssets.delete(assetId)
assetMap.delete(assetId)
assets.splice(
assets.findIndex((asset) => asset.id === assetId),
1,
)
return hasAsset
}

const undeleteAsset = (assetId: backend.AssetId) => {
const wasDeleted = deletedAssets.has(assetId)
deletedAssets.delete(assetId)
Expand Down Expand Up @@ -487,7 +498,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
description: rest.description ?? '',
labels: [],
parentId: defaultDirectoryId,
permissions: [],
permissions: [createUserPermission(defaultUser, permissions.PermissionAction.own)],
parentsPath: '',
virtualParentsPath: '',
},
Expand Down Expand Up @@ -517,6 +528,48 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
return secret
}

const createDatalink = (rest: Partial<backend.DatalinkAsset>): backend.DatalinkAsset => {
const datalink = object.merge(
{
type: backend.AssetType.datalink,
id: backend.DatalinkId('datalink-' + uniqueString.uniqueString()),
projectState: null,
extension: null,
title: rest.title ?? '',
modifiedAt: dateTime.toRfc3339(new Date()),
description: rest.description ?? '',
labels: [],
parentId: defaultDirectoryId,
permissions: [createUserPermission(defaultUser, permissions.PermissionAction.own)],
parentsPath: '',
virtualParentsPath: '',
},
rest,
)

Object.defineProperty(datalink, 'toJSON', {
value: function toJSON() {
const { parentsPath: _, virtualParentsPath: __, ...rest } = this

return {
...rest,
parentsPath: this.parentsPath,
virtualParentsPath: this.virtualParentsPath,
}
},
})

Object.defineProperty(datalink, 'parentsPath', {
get: () => getParentPath(datalink.parentId),
})

Object.defineProperty(datalink, 'virtualParentsPath', {
get: () => getVirtualParentPath(datalink.parentId, datalink.title),
})

return datalink
}

const createLabel = (value: string, color: backend.LChColor): backend.Label => ({
id: backend.TagId('tag-' + uniqueString.uniqueString()),
value: backend.LabelName(value),
Expand All @@ -539,6 +592,10 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
return addAsset(createSecret(rest))
}

const addDatalink = (rest: Partial<backend.DatalinkAsset> = {}) => {
return addAsset(createDatalink(rest))
}

const addLabel = (value: string, color: backend.LChColor) => {
const label = createLabel(value, color)
labels.push(label)
Expand Down Expand Up @@ -1109,6 +1166,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
})

await delete_(remoteBackendPaths.deleteAssetPath(GLOB_ASSET_ID), async (route, request) => {
const force = new URL(request.url()).searchParams.get('force') === 'true'
const maybeId = request.url().match(/[/]assets[/]([^?]+)/)?.[1]

if (!maybeId) return
Expand All @@ -1117,9 +1175,13 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
// `DirectoryId` to make TypeScript happy.
const assetId = decodeURIComponent(maybeId) as backend.DirectoryId

called('deleteAsset', { assetId })
called('deleteAsset', { assetId, force })

deleteAsset(assetId)
if (force) {
forceDeleteAsset(assetId)
} else {
deleteAsset(assetId)
}

await route.fulfill({ status: HTTP_STATUS_NO_CONTENT })
})
Expand Down Expand Up @@ -1365,6 +1427,9 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
defaultUser,
defaultUserId,
rootDirectoryId: defaultDirectoryId,
get assetCount() {
return assetMap.size
},
goOffline: () => {
isOnline = false
},
Expand Down Expand Up @@ -1395,10 +1460,12 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
createProject,
createFile,
createSecret,
createDatalink,
addDirectory,
addProject,
addFile,
addSecret,
addDatalink,
createLabel,
addLabel,
setLabels,
Expand Down
15 changes: 9 additions & 6 deletions app/gui/integration-test/dashboard/actions/contextMenuActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface ContextMenuActions<T extends BaseActions<Context>, Context> {
readonly snapshot: () => T
readonly moveNonFolderToTrash: () => T
readonly moveFolderToTrash: () => T
readonly moveAllToTrash: () => T
readonly moveAllToTrash: (confirm?: boolean) => T
readonly restoreFromTrash: () => T
readonly restoreAllFromTrash: () => T
readonly share: () => T
Expand Down Expand Up @@ -77,13 +77,16 @@ export function contextMenuActions<T extends BaseActions<Context>, Context>(
// Confirm the deletion in the dialog
await page.getByRole('button', { name: TEXT.delete }).getByText(TEXT.delete).click()
}),
moveAllToTrash: () =>
step('Move all to trash (context menu)', (page) =>
page
moveAllToTrash: (hasFolder = false) =>
step('Move all to trash (context menu)', async (page) => {
await page
.getByRole('button', { name: TEXT.moveAllToTrashShortcut })
.getByText(TEXT.moveAllToTrashShortcut)
.click(),
),
.click()
if (hasFolder) {
await page.getByRole('button', { name: TEXT.delete }).getByText(TEXT.delete).click()
}
}),
restoreFromTrash: () =>
step('Restore from trash (context menu)', (page) =>
page
Expand Down
Loading

0 comments on commit 4893370

Please sign in to comment.