diff --git a/.github/workflows/gui-tests.yml b/.github/workflows/gui-tests.yml index a7eeebb47c47..e422a03b9031 100644 --- a/.github/workflows/gui-tests.yml +++ b/.github/workflows/gui-tests.yml @@ -1,7 +1,7 @@ # This file is auto-generated. Do not edit it manually! # Edit the enso_build::ci_gen module instead and run `cargo run --package enso-build-ci-gen`. -name: GUI Tests +name: GUI Check on: push: branches: @@ -27,7 +27,7 @@ jobs: access_token: ${{ github.token }} permissions: actions: write - enso-build-ci-gen-job-gui-test-linux-x86_64: + enso-build-ci-gen-job-gui-check-linux-x86_64: name: GUI tests (linux, x86_64) runs-on: - self-hosted @@ -56,7 +56,7 @@ jobs: run: ./run git-clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: ./run gui test + - run: ./run gui check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - if: failure() && runner.os == 'Windows' diff --git a/.github/workflows/gui.yml b/.github/workflows/gui.yml index 20320b417273..cd4e0a35f654 100644 --- a/.github/workflows/gui.yml +++ b/.github/workflows/gui.yml @@ -155,7 +155,7 @@ jobs: access_token: ${{ github.token }} permissions: actions: write - enso-build-ci-gen-job-new-gui-build-linux-x86_64: + enso-build-ci-gen-job-gui-build-linux-x86_64: name: GUI build (linux, x86_64) runs-on: - self-hosted @@ -210,7 +210,7 @@ jobs: run: ./run git-clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - enso-build-ci-gen-job-new-gui-build-macos-x86_64: + enso-build-ci-gen-job-gui-build-macos-x86_64: name: GUI build (macos, x86_64) runs-on: - macos-12 @@ -264,7 +264,7 @@ jobs: run: ./run git-clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - enso-build-ci-gen-job-new-gui-build-windows-x86_64: + enso-build-ci-gen-job-gui-build-windows-x86_64: name: GUI build (windows, x86_64) runs-on: - self-hosted diff --git a/.gitignore b/.gitignore index ce2d01a6bce2..6f6542d25cb9 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,8 @@ node_modules/ .metals tools/performance/engine-benchmarks/generated_site *.tsbuildinfo +vite.config.ts.timestamp-*.mjs +vitest.config.ts.timestamp-*.mjs ############################ ## Rendered Documentation ## diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs new file mode 100644 index 000000000000..a2b91cba5e66 --- /dev/null +++ b/.pnpmfile.cjs @@ -0,0 +1,21 @@ +const IGNORED_DEPS = ['react-native-url-polyfill', 'react-native-get-random-values'] + +const unusedIgnores = new Set(IGNORED_DEPS) +module.exports.hooks = { + readPackage: (pkg, context) => { + for (const ignored of IGNORED_DEPS) { + if (pkg.dependencies[ignored]) { + delete pkg.dependencies[ignored] + context.log(`Ignoring dependency ${ignored} in ${pkg.name}`) + unusedIgnores.delete(ignored) + } + } + return pkg + }, + afterAllResolved(lockfile, context) { + if (unusedIgnores.size > 0) { + context.log(`Unused dependency ignore declarations: ${Array.from(unusedIgnores).join(', ')}`) + } + return lockfile + }, +} diff --git a/.prettierignore b/.prettierignore index 4c63858056d1..0e2f83fa81fc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -38,8 +38,7 @@ app/ide-desktop/lib/dashboard/playwright-report/ app/ide-desktop/lib/dashboard/playwright/.cache/ app/ide-desktop/lib/dashboard/dist/ app/gui/view/documentation/assets/stylesheet.css -app/gui2/rust-ffi/pkg -app/gui2/rust-ffi/node-pkg +app/rust-ffi/pkg app/gui2/src/assets/font-*.css Cargo.lock build.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 32a965890370..f9c6ca98693d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Next Release +#### Enso IDE + +- [Table Editor Widget][10774] displayed in `Table.new` component. + +[10774]: https://github.com/enso-org/enso/pull/10774 + #### Enso Standard Library - [Implemented in-memory and database mixed `Decimal` column @@ -23,14 +29,12 @@ - [Space-precedence does not apply to value-level operators][10597] - [Must specify `--repl` to enable debug server][10709] - [Improved parser error reporting and performance][10734] -- [Import all available libraries in `--repl` mode][10746] [10468]: https://github.com/enso-org/enso/pull/10468 [10535]: https://github.com/enso-org/enso/pull/10535 [10597]: https://github.com/enso-org/enso/pull/10597 [10709]: https://github.com/enso-org/enso/pull/10709 [10734]: https://github.com/enso-org/enso/pull/10734 -[10746]: https://github.com/enso-org/enso/pull/10746 #### Enso IDE diff --git a/Cargo.toml b/Cargo.toml index af211e4b9cee..3184943284ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ resolver = "2" # path, e.g. `lib/rust/ensogl/examples`, or `app/gui/view/examples`; this is used to optimize the application for # loading the IDE. members = [ - "app/gui2/rust-ffi", + "app/rust-ffi", "build/cli", "build/macros/proc-macro", "build/ci-gen", diff --git a/app/.vscode/launch.json b/app/.vscode/launch.json new file mode 100644 index 000000000000..ec7c5ec59fa0 --- /dev/null +++ b/app/.vscode/launch.json @@ -0,0 +1,93 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Dashboard", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-dashboard", "dev"] + }, + { + "type": "node", + "request": "launch", + "name": "GUI", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-gui2", "dev"] + }, + { + "type": "node", + "request": "launch", + "name": "GUI (Storybook)", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-gui2", "story:dev"] + }, + { + "type": "node", + "request": "launch", + "name": "Dashboard (Build)", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-gui2", "build:cloud"] + }, + { + "type": "node", + "request": "launch", + "name": "Dashboard (E2E UI)", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-dashboard", "test:e2e:debug"] + }, + { + "type": "node", + "request": "launch", + "name": "GUI (E2E UI)", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-gui2", "test:e2e", "--", "--ui"] + }, + { + "type": "node", + "request": "launch", + "name": "Dashboard (All tests)", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-dashboard", "test"] + }, + { + "type": "node", + "request": "launch", + "name": "Dashboard (E2E tests)", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-dashboard", "test:e2e"], + "outputCapture": "std" + }, + { + "type": "node", + "request": "launch", + "name": "Dashboard (Unit tests)", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-dashboard", "test:unit"], + "outputCapture": "std" + }, + { + "type": "node", + "request": "launch", + "name": "GUI (All tests)", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-gui2", "test"] + }, + { + "type": "node", + "request": "launch", + "name": "GUI (E2E tests)", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-gui2", "test:e2e"], + "outputCapture": "std" + }, + { + "type": "node", + "request": "launch", + "name": "GUI (Unit tests)", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "--workspace=enso-gui2", "test:unit", "--", "run"], + "outputCapture": "std" + } + ] +} diff --git a/app/.vscode/react.code-snippets b/app/.vscode/react.code-snippets index a6a27e058e65..32127c8d48eb 100644 --- a/app/.vscode/react.code-snippets +++ b/app/.vscode/react.code-snippets @@ -2,9 +2,6 @@ "React Component": { "prefix": ["$c", "component"], "body": [ - "/** @file $2 */", - "import * as React from 'react'", - "", "// ====${1/./=/g}====", "// === $1 ===", "// ====${1/./=/g}====", @@ -18,15 +15,28 @@ "export default function $1(props: $1Props) {", " const { ${3/(.+?):.+/$1, /g} } = props", " return <>$4", - "}" - ] + "}", + ], + }, + "React Hook": { + "prefix": ["$h", "hook"], + "body": [ + "// =======${1/./=/g}====", + "// === use$1 ===", + "// =======${1/./=/g}====", + "", + "/** $2 */", + "export function use$1($3) {", + " $4", + "}", + ], }, "useState": { "prefix": ["$s", "usestate"], - "body": ["const [$1, set${1/(.*)/${1:/pascalcase}/}] = React.useState($2)"] + "body": ["const [$1, set${1/(.*)/${1:/pascalcase}/}] = React.useState($2)"], }, "section": { "prefix": ["$S", "section"], - "body": ["// ====${1/./=/g}====", "// === $1 ===", "// ====${1/./=/g}===="] - } + "body": ["// ====${1/./=/g}====", "// === $1 ===", "// ====${1/./=/g}===="], + }, } diff --git a/app/dashboard/e2e/README.md b/app/dashboard/e2e/README.md index 168e8eddeb12..0917914fe44b 100644 --- a/app/dashboard/e2e/README.md +++ b/app/dashboard/e2e/README.md @@ -6,20 +6,20 @@ Execute all commands from the parent directory. ```sh # Run tests normally -npm run test:e2e +pnpm run test:e2e # Open UI to run tests -npm run test:e2e:debug +pnpm run test:e2e:debug # Run tests in a specific file only -npm run test:e2e -- e2e/file-name-here.spec.ts -npm run test:e2e:debug -- e2e/file-name-here.spec.ts +pnpm run test:e2e -- e2e/file-name-here.spec.ts +pnpm run test:e2e:debug -- e2e/file-name-here.spec.ts # Compile the entire app before running the tests. # DOES NOT hot reload the tests. # Prefer not using this when you are trying to fix a test; # prefer using this when you just want to know which tests are failing (if any). -PROD=1 npm run test:e2e -PROD=1 npm run test:e2e:debug -PROD=1 npm run test:e2e -- e2e/file-name-here.spec.ts -PROD=1 npm run test:e2e:debug -- e2e/file-name-here.spec.ts +PROD=1 pnpm run test:e2e +PROD=1 pnpm run test:e2e:debug +PROD=1 pnpm run test:e2e -- e2e/file-name-here.spec.ts +PROD=1 pnpm run test:e2e:debug -- e2e/file-name-here.spec.ts ``` ## Getting started diff --git a/app/dashboard/e2e/actions.ts b/app/dashboard/e2e/actions.ts index 53cfefdef5f3..b6d558795e03 100644 --- a/app/dashboard/e2e/actions.ts +++ b/app/dashboard/e2e/actions.ts @@ -2,6 +2,8 @@ /** @file Various actions, locators, and constants used in end-to-end tests. */ import * as test from '@playwright/test' +import * as text from 'enso-common/src/text' + import DrivePageActions from './actions/DrivePageActions' import LoginPageActions from './actions/LoginPageActions' import * as apiModule from './api' @@ -18,6 +20,7 @@ export const INVALID_PASSWORD = 'password' export const VALID_PASSWORD = 'Password0!' /** An example valid email address. */ export const VALID_EMAIL = 'email@example.com' +export const TEXT = text.TEXTS.english // ================ // === Locators === @@ -325,7 +328,12 @@ export function locateAssetsTable(page: test.Page) { /** Find assets table rows (if any) on the current page. */ export function locateAssetRows(page: test.Page) { - return locateAssetsTable(page).locator('tbody').getByRole('row') + return locateAssetsTable(page).getByTestId('asset-row') +} + +/** Find assets table placeholder rows (if any) on the current page. */ +export function locateNonAssetRows(page: test.Page) { + return locateAssetsTable(page).locator('tbody tr:not([data-testid="asset-row"])') } /** Find the name column of the given asset row. */ @@ -494,17 +502,21 @@ export namespace settings { /** Find a "current password" input in the "user account" settings section. */ export function locateCurrentPasswordInput(page: test.Page) { - return locate(page).getByLabel('Current password') + return locate(page).getByRole('group', { name: 'Current password' }).getByRole('textbox') } /** Find a "new password" input in the "user account" settings section. */ export function locateNewPasswordInput(page: test.Page) { - return locate(page).getByLabel('New password', { exact: true }) + return locate(page) + .getByRole('group', { name: /^New password/, exact: true }) + .getByRole('textbox') } /** Find a "confirm new password" input in the "user account" settings section. */ export function locateConfirmNewPasswordInput(page: test.Page) { - return locate(page).getByLabel('Confirm new password') + return locate(page) + .getByRole('group', { name: /^Confirm new password/, exact: true }) + .getByRole('textbox') } /** Find a "change" button. */ @@ -701,34 +713,6 @@ export async function expectNotOnScreen(locator: test.Locator) { }) } -// ======================= -// === Mouse utilities === -// ======================= - -// eslint-disable-next-line @typescript-eslint/no-magic-numbers -export const ASSET_ROW_SAFE_POSITION = { x: 300, y: 16 } - -/** Click an asset row. The center must not be clicked as that is the button for adding a label. */ -export async function clickAssetRow(assetRow: test.Locator) { - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - await assetRow.click({ position: ASSET_ROW_SAFE_POSITION }) -} - -/** Drag an asset row. The center must not be clicked as that is the button for adding a label. */ -export async function dragAssetRowToAssetRow(from: test.Locator, to: test.Locator) { - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - await from.dragTo(to, { - sourcePosition: ASSET_ROW_SAFE_POSITION, - targetPosition: ASSET_ROW_SAFE_POSITION, - }) -} - -/** Drag an asset row. The center must not be clicked as that is the button for adding a label. */ -export async function dragAssetRow(from: test.Locator, to: test.Locator) { - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - await from.dragTo(to, { sourcePosition: ASSET_ROW_SAFE_POSITION }) -} - // ========================== // === Keyboard utilities === // ========================== @@ -776,7 +760,6 @@ export async function login( first = true, ) { await test.test.step('Login', async () => { - await page.goto('/') await locateEmailInput(page).fill(email) await locatePasswordInput(page).fill(password) await locateLoginButton(page).click() @@ -885,6 +868,7 @@ export function mockAll({ page, setupAPI }: MockParams) { return new LoginPageActions(page).step('Execute all mocks', async () => { await mockApi({ page, setupAPI }) await mockDate({ page, setupAPI }) + await page.goto('/') }) } @@ -900,6 +884,7 @@ export function mockAllAndLogin({ page, setupAPI }: MockParams) { .step('Execute all mocks', async () => { await mockApi({ page, setupAPI }) await mockDate({ page, setupAPI }) + await page.goto('/') }) .do((thePage) => login({ page: thePage, setupAPI })) } @@ -916,6 +901,7 @@ export async function mockAllAndLoginAndExposeAPI({ page, setupAPI }: MockParams return await test.test.step('Execute all mocks and login', async () => { const api = await mockApi({ page, setupAPI }) await mockDate({ page, setupAPI }) + await page.goto('/') await login({ page, setupAPI }) return api }) diff --git a/app/dashboard/e2e/actions/BaseActions.ts b/app/dashboard/e2e/actions/BaseActions.ts index 424f9648849d..d9362b1837aa 100644 --- a/app/dashboard/e2e/actions/BaseActions.ts +++ b/app/dashboard/e2e/actions/BaseActions.ts @@ -3,7 +3,7 @@ import * as test from '@playwright/test' import type * as inputBindings from '#/utilities/inputBindings' -import * as actions from '../actions' +import { modModifier, TEXT } from '../actions' // ==================== // === PageCallback === @@ -149,10 +149,30 @@ export default class BaseActions implements Promise { withModPressed(callback: (actions: this) => R) { return callback( this.step('Press "Mod"', async (page) => { - await page.keyboard.down(await actions.modModifier(page)) + await page.keyboard.down(await modModifier(page)) }), ).step('Release "Mod"', async (page) => { - await page.keyboard.up(await actions.modModifier(page)) + await page.keyboard.up(await modModifier(page)) }) } + + /** Expect an input to have an error (or no error if the expected value is `null`). + * If the expected value is `undefined`, the assertion is skipped. */ + expectInputError(testId: string, description: string, expected: string | null | undefined) { + if (expected === undefined) { + return this + } else if (expected != null) { + return this.step(`Expect ${description} error to be '${expected}'`, async (page) => { + await test + .expect(page.getByTestId(testId).getByLabel(TEXT.fieldErrorLabel)) + .toHaveText(expected) + }) + } else { + return this.step(`Expect no ${description} error`, async (page) => { + await test + .expect(page.getByTestId(testId).getByLabel(TEXT.fieldErrorLabel)) + .not.toBeVisible() + }) + } + } } diff --git a/app/dashboard/e2e/actions/DrivePageActions.ts b/app/dashboard/e2e/actions/DrivePageActions.ts index b2877749d932..cf76ab4cea38 100644 --- a/app/dashboard/e2e/actions/DrivePageActions.ts +++ b/app/dashboard/e2e/actions/DrivePageActions.ts @@ -1,7 +1,18 @@ /** @file Actions for the "drive" page. */ import * as test from 'playwright/test' -import * as actions from '../actions' +import { + locateAssetPanel, + locateAssetsTable, + locateContextMenus, + locateCreateButton, + locateDriveView, + locateNewSecretIcon, + locateNonAssetRows, + locateSecretNameInput, + locateSecretValueInput, + TEXT, +} from '../actions' import type * as baseActions from './BaseActions' import * as contextMenuActions from './contextMenuActions' import EditorPageActions from './EditorPageActions' @@ -23,7 +34,7 @@ const ASSET_ROW_SAFE_POSITION = { x: 300, y: 16 } /** Find all assets table rows (if any). */ function locateAssetRows(page: test.Page) { - return actions.locateAssetsTable(page).locator('tbody').getByRole('row') + return locateAssetsTable(page).getByTestId('asset-row') } // ======================== @@ -50,25 +61,37 @@ export default class DrivePageActions extends PageActions { /** Switch to the "cloud" category. */ cloud() { return self.step('Go to "Cloud" category', (page) => - page.getByRole('button', { name: 'Cloud' }).getByText('Cloud').click(), + page + .getByRole('button', { name: TEXT.cloudCategory }) + .getByText(TEXT.cloudCategory) + .click(), ) }, /** Switch to the "local" category. */ local() { return self.step('Go to "Local" category', (page) => - page.getByRole('button', { name: 'Local' }).getByText('Local').click(), + page + .getByRole('button', { name: TEXT.localCategory }) + .getByText(TEXT.localCategory) + .click(), ) }, /** Switch to the "recent" category. */ recent() { return self.step('Go to "Recent" category', (page) => - page.getByRole('button', { name: 'Recent' }).getByText('Recent').click(), + page + .getByRole('button', { name: TEXT.recentCategory }) + .getByText(TEXT.recentCategory) + .click(), ) }, /** Switch to the "trash" category. */ trash() { return self.step('Go to "Trash" category', (page) => - page.getByRole('button', { name: 'Trash' }).getByText('Trash').click(), + page + .getByRole('button', { name: TEXT.trashCategory }) + .getByText(TEXT.trashCategory) + .click(), ) }, } @@ -82,22 +105,22 @@ export default class DrivePageActions extends PageActions { /** Click the column heading for the "name" column to change its sort order. */ clickNameColumnHeading() { return self.step('Click "name" column heading', (page) => - page.getByLabel('Sort by name').or(page.getByLabel('Stop sorting by name')).click(), + page.getByLabel(TEXT.sortByName).or(page.getByLabel(TEXT.stopSortingByName)).click(), ) }, /** Click the column heading for the "modified" column to change its sort order. */ clickModifiedColumnHeading() { return self.step('Click "modified" column heading', (page) => page - .getByLabel('Sort by modification date') - .or(page.getByLabel('Stop sorting by modification date')) + .getByLabel(TEXT.sortByModificationDate) + .or(page.getByLabel(TEXT.stopSortingByModificationDate)) .click(), ) }, /** Click to select a specific row. */ clickRow(index: number) { return self.step(`Click drive table row #${index}`, (page) => - locateAssetRows(page).nth(index).click({ position: actions.ASSET_ROW_SAFE_POSITION }), + locateAssetRows(page).nth(index).click({ position: ASSET_ROW_SAFE_POSITION }), ) }, /** Right click a specific row to bring up its context menu, or the context menu for multiple @@ -106,19 +129,21 @@ export default class DrivePageActions extends PageActions { return self.step(`Right click drive table row #${index}`, (page) => locateAssetRows(page) .nth(index) - .click({ button: 'right', position: actions.ASSET_ROW_SAFE_POSITION }), + .click({ button: 'right', position: ASSET_ROW_SAFE_POSITION }), ) }, /** Double click a row. */ doubleClickRow(index: number) { return self.step(`Double dlick drive table row #${index}`, (page) => - locateAssetRows(page).nth(index).dblclick({ position: actions.ASSET_ROW_SAFE_POSITION }), + locateAssetRows(page).nth(index).dblclick({ position: ASSET_ROW_SAFE_POSITION }), ) }, /** Interact with the set of all rows in the Drive table. */ - withRows(callback: baseActions.LocatorCallback) { + withRows( + callback: (assetRows: test.Locator, nonAssetRows: test.Locator) => Promise | void, + ) { return self.step('Interact with drive table rows', async (page) => { - await callback(locateAssetRows(page)) + await callback(locateAssetRows(page), locateNonAssetRows(page)) }) }, /** Drag a row onto another row. */ @@ -146,18 +171,20 @@ export default class DrivePageActions extends PageActions { * placeholder row displayed when there are no assets to show. */ expectPlaceholderRow() { return self.step('Expect placeholder row', async (page) => { - const rows = locateAssetRows(page) - await test.expect(rows).toHaveCount(1) - await test.expect(rows).toHaveText(/You have no files/) + await test.expect(locateAssetRows(page)).toHaveCount(0) + const nonAssetRows = locateNonAssetRows(page) + await test.expect(nonAssetRows).toHaveCount(1) + await test.expect(nonAssetRows).toHaveText(/You have no files/) }) }, /** A test assertion to confirm that there is only one row visible, and that row is the * placeholder row displayed when there are no assets in Trash. */ expectTrashPlaceholderRow() { return self.step('Expect trash placeholder row', async (page) => { - const rows = locateAssetRows(page) - await test.expect(rows).toHaveCount(1) - await test.expect(rows).toHaveText(/Your trash is empty/) + await test.expect(locateAssetRows(page)).toHaveCount(0) + const nonAssetRows = locateNonAssetRows(page) + await test.expect(nonAssetRows).toHaveCount(1) + await test.expect(nonAssetRows).toHaveText(/Your trash is empty/) }) }, /** Toggle a column's visibility. */ @@ -165,38 +192,38 @@ export default class DrivePageActions extends PageActions { return { /** Toggle visibility for the "modified" column. */ modified() { - return self.step('Expect trash placeholder row', (page) => - page.getByAltText('Modified').click(), + return self.step('Toggle "modified" column', (page) => + page.getByAltText(TEXT.modifiedColumnName).click(), ) }, /** Toggle visibility for the "shared with" column. */ sharedWith() { - return self.step('Expect trash placeholder row', (page) => - page.getByAltText('Shared With').click(), + return self.step('Toggle "shared with" column', (page) => + page.getByAltText(TEXT.sharedWithColumnName).click(), ) }, /** Toggle visibility for the "labels" column. */ labels() { - return self.step('Expect trash placeholder row', (page) => - page.getByAltText('Labels').click(), + return self.step('Toggle "labels" column', (page) => + page.getByAltText(TEXT.labelsColumnName).click(), ) }, /** Toggle visibility for the "accessed by projects" column. */ accessedByProjects() { - return self.step('Expect trash placeholder row', (page) => - page.getByAltText('Accessed By Projects').click(), + return self.step('Toggle "accessed by projects" column', (page) => + page.getByAltText(TEXT.accessedByProjectsColumnName).click(), ) }, /** Toggle visibility for the "accessed data" column. */ accessedData() { - return self.step('Expect trash placeholder row', (page) => - page.getByAltText('Accessed Data').click(), + return self.step('Toggle "accessed data" column', (page) => + page.getByAltText(TEXT.accessedDataColumnName).click(), ) }, /** Toggle visibility for the "docs" column. */ docs() { - return self.step('Expect trash placeholder row', (page) => - page.getByAltText('Docs').click(), + return self.step('Toggle "docs" column', (page) => + page.getByAltText(TEXT.docsColumnName).click(), ) }, } @@ -207,26 +234,26 @@ export default class DrivePageActions extends PageActions { /** Open the "start" modal. */ openStartModal() { return this.step('Open "start" modal', (page) => - page.getByText('Start with a template').click(), + page.getByText(TEXT.startWithATemplate).click(), ).into(StartModalActions) } /** Create a new empty project. */ newEmptyProject() { return this.step('Create empty project', (page) => - page.getByText('New Empty Project').click(), + page.getByText(TEXT.newEmptyProject).click(), ).into(EditorPageActions) } /** Interact with the drive view (the main container of this page). */ withDriveView(callback: baseActions.LocatorCallback) { - return this.step('Interact with drive view', (page) => callback(actions.locateDriveView(page))) + return this.step('Interact with drive view', (page) => callback(locateDriveView(page))) } /** Create a new folder using the icon in the Drive Bar. */ createFolder() { return this.step('Create folder', (page) => - page.getByRole('button', { name: 'New Folder', exact: true }).click(), + page.getByRole('button', { name: TEXT.newFolder, exact: true }).click(), ) } @@ -238,7 +265,7 @@ export default class DrivePageActions extends PageActions { ) { return this.step(`Upload file '${name}'`, async (page) => { const fileChooserPromise = page.waitForEvent('filechooser') - await page.getByRole('button', { name: 'Import' }).click() + await page.getByRole('button', { name: TEXT.uploadFiles }).click() const fileChooser = await fileChooserPromise await fileChooser.setFiles([{ name, buffer: Buffer.from(contents), mimeType }]) }) @@ -247,10 +274,10 @@ export default class DrivePageActions extends PageActions { /** Create a new secret using the icon in the Drive Bar. */ createSecret(name: string, value: string) { return this.step(`Create secret '${name}' = '${value}'`, async (page) => { - await actions.locateNewSecretIcon(page).click() - await actions.locateSecretNameInput(page).fill(name) - await actions.locateSecretValueInput(page).fill(value) - await actions.locateCreateButton(page).click() + await locateNewSecretIcon(page).click() + await locateSecretNameInput(page).fill(name) + await locateSecretValueInput(page).fill(value) + await locateCreateButton(page).click() }) } @@ -264,28 +291,28 @@ export default class DrivePageActions extends PageActions { /** Interact with the container element of the assets table. */ withAssetsTable(callback: baseActions.LocatorCallback) { return this.step('Interact with drive table', async (page) => { - await callback(actions.locateAssetsTable(page)) + await callback(locateAssetsTable(page)) }) } /** Interact with the Asset Panel. */ withAssetPanel(callback: baseActions.LocatorCallback) { return this.step('Interact with asset panel', async (page) => { - await callback(actions.locateAssetPanel(page)) + await callback(locateAssetPanel(page)) }) } /** Open the Data Link creation modal by clicking on the Data Link icon. */ openDataLinkModal() { return this.step('Open "new data link" modal', (page) => - page.getByRole('button', { name: 'New Datalink' }).click(), + page.getByRole('button', { name: TEXT.newDatalink }).click(), ).into(NewDataLinkModalActions) } /** Interact with the context menus (the context menus MUST be visible). */ withContextMenus(callback: baseActions.LocatorCallback) { return this.step('Interact with context menus', async (page) => { - await callback(actions.locateContextMenus(page)) + await callback(locateContextMenus(page)) }) } } diff --git a/app/dashboard/e2e/actions/ForgotPasswordPageActions.ts b/app/dashboard/e2e/actions/ForgotPasswordPageActions.ts new file mode 100644 index 000000000000..b3dade2d1129 --- /dev/null +++ b/app/dashboard/e2e/actions/ForgotPasswordPageActions.ts @@ -0,0 +1,54 @@ +/** @file Available actions for the login page. */ +import * as test from '@playwright/test' + +import { TEXT, VALID_EMAIL } from '../actions' +import BaseActions, { type LocatorCallback } from './BaseActions' +import LoginPageActions from './LoginPageActions' + +// ================================= +// === ForgotPasswordPageActions === +// ================================= + +/** Available actions for the login page. */ +export default class ForgotPasswordPageActions extends BaseActions { + /** Actions for navigating to another page. */ + get goToPage() { + return { + login: (): LoginPageActions => + this.step("Go to 'login' page", async (page) => + page.getByRole('link', { name: TEXT.goBackToLogin, exact: true }).click(), + ).into(LoginPageActions), + } + } + + /** Perform a successful login. */ + forgotPassword(email = VALID_EMAIL) { + return this.step('Forgot password', () => this.forgotPasswordInternal(email)).into( + LoginPageActions, + ) + } + + /** Fill the email input. */ + fillEmail(email: string) { + return this.step(`Fill email with '${email}'`, (page) => + page.getByPlaceholder(TEXT.emailPlaceholder).fill(email), + ) + } + + /** Interact with the email input. */ + withEmailInput(callback: LocatorCallback) { + return this.step('Interact with email input', async (page) => { + await callback(page.getByPlaceholder(TEXT.emailPlaceholder)) + }) + } + + /** Internal login logic shared between all public methods. */ + private async forgotPasswordInternal(email: string) { + await this.page.getByPlaceholder(TEXT.emailPlaceholder).fill(email) + await this.page + .getByRole('button', { name: TEXT.login, exact: true }) + .getByText(TEXT.login) + .click() + await test.expect(this.page.getByText(TEXT.loadingAppMessage)).not.toBeVisible() + } +} diff --git a/app/dashboard/e2e/actions/LoginPageActions.ts b/app/dashboard/e2e/actions/LoginPageActions.ts index 935065acb39e..6a73e9dfbc73 100644 --- a/app/dashboard/e2e/actions/LoginPageActions.ts +++ b/app/dashboard/e2e/actions/LoginPageActions.ts @@ -1,10 +1,12 @@ /** @file Available actions for the login page. */ import * as test from '@playwright/test' -import * as actions from '../actions' -import BaseActions from './BaseActions' +import { TEXT, VALID_EMAIL, VALID_PASSWORD } from '../actions' +import BaseActions, { type LocatorCallback } from './BaseActions' import DrivePageActions from './DrivePageActions' -import SetUsernamePageActions from './SetUsernamePageActions' +import ForgotPasswordPageActions from './ForgotPasswordPageActions' +import RegisterPageActions from './RegisterPageActions' +import SetupPageActions from './SetupPageActions' // ======================== // === LoginPageActions === @@ -12,29 +14,85 @@ import SetUsernamePageActions from './SetUsernamePageActions' /** Available actions for the login page. */ export default class LoginPageActions extends BaseActions { + /** Actions for navigating to another page. */ + get goToPage() { + return { + register: (): RegisterPageActions => + this.step("Go to 'register' page", async (page) => + page.getByRole('link', { name: TEXT.dontHaveAnAccount, exact: true }).click(), + ).into(RegisterPageActions), + forgotPassword: (): ForgotPasswordPageActions => + this.step("Go to 'forgot password' page", async (page) => + page.getByRole('link', { name: TEXT.forgotYourPassword, exact: true }).click(), + ).into(ForgotPasswordPageActions), + } + } + /** Perform a successful login. */ - login(email = 'email@example.com', password = actions.VALID_PASSWORD) { + login(email = VALID_EMAIL, password = VALID_PASSWORD) { return this.step('Login', () => this.loginInternal(email, password)).into(DrivePageActions) } /** Perform a login as a new user (a user that does not yet have a username). */ - loginAsNewUser(email = 'email@example.com', password = actions.VALID_PASSWORD) { + loginAsNewUser(email = VALID_EMAIL, password = VALID_PASSWORD) { return this.step('Login (as new user)', () => this.loginInternal(email, password)).into( - SetUsernamePageActions, + SetupPageActions, ) } /** Perform a failing login. */ - loginThatShouldFail(email = 'email@example.com', password = actions.VALID_PASSWORD) { - return this.step('Login (should fail)', () => this.loginInternal(email, password)) + loginThatShouldFail( + email = VALID_EMAIL, + password = VALID_PASSWORD, + { + assert = {}, + }: { + assert?: { + emailError?: string | null + passwordError?: string | null + formError?: string | null + } + } = {}, + ) { + const { emailError, passwordError, formError } = assert + const next = this.step('Login (should fail)', () => this.loginInternal(email, password)) + .expectInputError('email-input', 'email', emailError) + .expectInputError('password-input', 'password', passwordError) + if (formError === undefined) { + return next + } else if (formError != null) { + return next.step(`Expect form error to be '${formError}'`, async (page) => { + await test.expect(page.getByTestId('form-submit-error')).toHaveText(formError) + }) + } else { + return next.step('Expect no form error', async (page) => { + await test.expect(page.getByTestId('form-submit-error')).not.toBeVisible() + }) + } + } + + /** Fill the email input. */ + fillEmail(email: string) { + return this.step(`Fill email with '${email}'`, (page) => + page.getByPlaceholder(TEXT.emailPlaceholder).fill(email), + ) + } + + /** Interact with the email input. */ + withEmailInput(callback: LocatorCallback) { + return this.step('Interact with email input', async (page) => { + await callback(page.getByPlaceholder(TEXT.emailPlaceholder)) + }) } /** Internal login logic shared between all public methods. */ private async loginInternal(email: string, password: string) { - await this.page.goto('/') - await this.page.getByPlaceholder('Enter your email').fill(email) - await this.page.getByPlaceholder('Enter your password').fill(password) - await this.page.getByRole('button', { name: 'Login', exact: true }).getByText('Login').click() - await test.expect(this.page.getByText('Logging in to Enso...')).not.toBeVisible() + await this.page.getByPlaceholder(TEXT.emailPlaceholder).fill(email) + await this.page.getByPlaceholder(TEXT.passwordPlaceholder).fill(password) + await this.page + .getByRole('button', { name: TEXT.login, exact: true }) + .getByText(TEXT.login) + .click() + await test.expect(this.page.getByText(TEXT.loadingAppMessage)).not.toBeVisible() } } diff --git a/app/dashboard/e2e/actions/RegisterPageActions.ts b/app/dashboard/e2e/actions/RegisterPageActions.ts new file mode 100644 index 000000000000..c78fd08b13a8 --- /dev/null +++ b/app/dashboard/e2e/actions/RegisterPageActions.ts @@ -0,0 +1,92 @@ +/** @file Available actions for the login page. */ +import * as test from '@playwright/test' + +import { TEXT, VALID_EMAIL, VALID_PASSWORD } from '../actions' +import BaseActions, { type LocatorCallback } from './BaseActions' +import LoginPageActions from './LoginPageActions' + +// ======================== +// === LoginPageActions === +// ======================== + +/** Available actions for the login page. */ +export default class RegisterPageActions extends BaseActions { + /** Actions for navigating to another page. */ + get goToPage() { + return { + login: (): LoginPageActions => + this.step("Go to 'login' page", async (page) => + page.getByRole('link', { name: TEXT.alreadyHaveAnAccount, exact: true }).click(), + ).into(LoginPageActions), + } + } + + /** Perform a successful login. */ + register(email = VALID_EMAIL, password = VALID_PASSWORD, confirmPassword = password) { + return this.step('Reegister', () => + this.registerInternal(email, password, confirmPassword), + ).into(LoginPageActions) + } + + /** Perform a failing login. */ + registerThatShouldFail( + email = VALID_EMAIL, + password = VALID_PASSWORD, + confirmPassword = password, + { + assert = {}, + }: { + assert?: { + emailError?: string | null + passwordError?: string | null + confirmPasswordError?: string | null + formError?: string | null + } + } = {}, + ) { + const { emailError, passwordError, confirmPasswordError, formError } = assert + const next = this.step('Register (should fail)', () => + this.registerInternal(email, password, confirmPassword), + ) + .expectInputError('email-input', 'email', emailError) + .expectInputError('password-input', 'password', passwordError) + .expectInputError('confirm-password-input', 'confirmPassword', confirmPasswordError) + if (formError === undefined) { + return next + } else if (formError != null) { + return next.step(`Expect form error to be '${formError}'`, async (page) => { + await test.expect(page.getByTestId('form-submit-error')).toHaveText(formError) + }) + } else { + return next.step('Expect no form error', async (page) => { + await test.expect(page.getByTestId('form-submit-error')).not.toBeVisible() + }) + } + } + + /** Fill the email input. */ + fillEmail(email: string) { + return this.step(`Fill email with '${email}'`, (page) => + page.getByPlaceholder(TEXT.emailPlaceholder).fill(email), + ) + } + + /** Interact with the email input. */ + withEmailInput(callback: LocatorCallback) { + return this.step('Interact with email input', async (page) => { + await callback(page.getByPlaceholder(TEXT.emailPlaceholder)) + }) + } + + /** Internal login logic shared between all public methods. */ + private async registerInternal(email: string, password: string, confirmPassword: string) { + await this.page.getByPlaceholder(TEXT.emailPlaceholder).fill(email) + await this.page.getByPlaceholder(TEXT.passwordPlaceholder).fill(password) + await this.page.getByPlaceholder(TEXT.confirmPasswordPlaceholder).fill(confirmPassword) + await this.page + .getByRole('button', { name: TEXT.register, exact: true }) + .getByText(TEXT.register) + .click() + await test.expect(this.page.getByText(TEXT.loadingAppMessage)).not.toBeVisible() + } +} diff --git a/app/dashboard/e2e/actions/SetUsernamePageActions.ts b/app/dashboard/e2e/actions/SetupPageActions.ts similarity index 84% rename from app/dashboard/e2e/actions/SetUsernamePageActions.ts rename to app/dashboard/e2e/actions/SetupPageActions.ts index 2e9e2de6427a..f25b46b1da6c 100644 --- a/app/dashboard/e2e/actions/SetUsernamePageActions.ts +++ b/app/dashboard/e2e/actions/SetupPageActions.ts @@ -1,4 +1,4 @@ -/** @file Actions for the "set username" page. */ +/** @file Actions for the "setup" page. */ import * as actions from '../actions' import BaseActions from './BaseActions' import DrivePageActions from './DrivePageActions' @@ -8,7 +8,7 @@ import DrivePageActions from './DrivePageActions' // ============================== /** Actions for the "set username" page. */ -export default class SetUsernamePageActions extends BaseActions { +export default class SetupPageActions extends BaseActions { /** Set the userame for a new user that does not yet have a username. */ setUsername(username: string) { return this.step(`Set username to '${username}'`, async (page) => { diff --git a/app/dashboard/e2e/api.ts b/app/dashboard/e2e/api.ts index 757f872b9996..c8261eed4bb2 100644 --- a/app/dashboard/e2e/api.ts +++ b/app/dashboard/e2e/api.ts @@ -12,6 +12,8 @@ import * as uniqueString from '#/utilities/uniqueString' import * as actions from './actions' +import LATEST_GITHUB_RELEASES from './latestGithubReleases.json' with { type: 'json' } + // ================= // === Constants === // ================= @@ -357,6 +359,67 @@ async function mockApiInternal({ page, setupAPI }: MockParams) { await page.route('https://www.googletagmanager.com/gtag/js*', (route) => route.fulfill({ contentType: 'text/javascript', body: 'export {};' }), ) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (process.env.MOCK_ALL_URLS === 'true') { + await page.route('https://fonts.googleapis.com/css2*', async (route) => { + await route.fulfill({ contentType: 'text/css', body: '' }) + }) + await page.route('https://ensoanalytics.com/eula.json', async (route) => { + await route.fulfill({ + json: { + path: '/eula.md', + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + size: 9472, + modified: '2024-06-26T10:44:04.939Z', + hash: '1c8a655202e59f0efebf5a83a703662527aa97247052964f959a8488382604b8', + }, + }) + }) + await page.route( + 'https://api.github.com/repos/enso-org/enso/releases/latest', + async (route) => { + await route.fulfill({ json: LATEST_GITHUB_RELEASES }) + }, + ) + await page.route('https://github.com/enso-org/enso/releases/download/**', async (route) => { + await route.fulfill({ + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + status: 302, + headers: { location: 'https://objects.githubusercontent.com/foo/bar' }, + }) + }) + await page.route('https://objects.githubusercontent.com/**', async (route) => { + await route.fulfill({ + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + status: 200, + headers: { + /* eslint-disable @typescript-eslint/naming-convention */ + 'content-type': 'application/octet-stream', + 'last-modified': 'Wed, 24 Jul 2024 17:22:47 GMT', + etag: '"0x8DCAC053D058EA5"', + server: 'Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0', + 'x-ms-request-id': '20ab2b4e-c01e-0068-7dfa-dd87c5000000', + 'x-ms-version': '2020-10-02', + 'x-ms-creation-time': 'Wed, 24 Jul 2024 17:22:47 GMT', + 'x-ms-lease-status': 'unlocked', + 'x-ms-lease-state': 'available', + 'x-ms-blob-type': 'BlockBlob', + 'content-disposition': 'attachment; filename=enso-linux-x86_64-2024.3.1-rc3.AppImage', + 'x-ms-server-encrypted': 'true', + via: '1.1 varnish, 1.1 varnish', + 'accept-ranges': 'bytes', + age: '1217', + date: 'Mon, 29 Jul 2024 09:40:09 GMT', + 'x-served-by': 'cache-iad-kcgs7200163-IAD, cache-bne12520-BNE', + 'x-cache': 'HIT, HIT', + 'x-cache-hits': '48, 0', + 'x-timer': 'S1722246008.269342,VS0,VE895', + 'content-length': '1030383958', + /* eslint-enable @typescript-eslint/naming-convention */ + }, + }) + }) + } const isActuallyOnline = await page.evaluate(() => navigator.onLine) if (!isActuallyOnline) { await page.route('https://fonts.googleapis.com/*', (route) => route.abort()) @@ -776,12 +839,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) { await post(remoteBackendPaths.CREATE_TAG_PATH + '*', (route) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const body: backend.CreateTagRequestBody = route.request().postDataJSON() - const json: backend.Label = { - id: backend.TagId(`tag-${uniqueString.uniqueString()}`), - value: backend.LabelName(body.value), - color: body.color, - } - return json + return addLabel(body.value, body.color) }) await post(remoteBackendPaths.CREATE_PROJECT_PATH + '*', (_route, request) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment diff --git a/app/dashboard/e2e/assetSearchBar.spec.ts b/app/dashboard/e2e/assetSearchBar.spec.ts index 666e7a3d027a..408f50a7773c 100644 --- a/app/dashboard/e2e/assetSearchBar.spec.ts +++ b/app/dashboard/e2e/assetSearchBar.spec.ts @@ -36,12 +36,9 @@ test.test('labels', async ({ page }) => { page, setupAPI: (api) => { api.addLabel('aaaa', backend.COLORS[0]) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - api.addLabel('bbbb', backend.COLORS[1]!) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - api.addLabel('cccc', backend.COLORS[2]!) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - api.addLabel('dddd', backend.COLORS[3]!) + api.addLabel('bbbb', backend.COLORS[1]) + api.addLabel('cccc', backend.COLORS[2]) + api.addLabel('dddd', backend.COLORS[3]) }, }) const searchBarInput = actions.locateSearchBarInput(page) diff --git a/app/dashboard/e2e/assetsTableFeatures.spec.ts b/app/dashboard/e2e/assetsTableFeatures.spec.ts index ab01b960b22d..bd80bd889b08 100644 --- a/app/dashboard/e2e/assetsTableFeatures.spec.ts +++ b/app/dashboard/e2e/assetsTableFeatures.spec.ts @@ -92,17 +92,15 @@ test.test('can drop onto root directory dropzone', ({ page }) => .createFolder() .uploadFile('b', 'testing') .driveTable.doubleClickRow(0) - .driveTable.withRows(async (rows) => { + .driveTable.withRows(async (rows, nonAssetRows) => { const parentLeft = await actions.getAssetRowLeftPx(rows.nth(0)) - const childLeft = await actions.getAssetRowLeftPx(rows.nth(1)) + const childLeft = await actions.getAssetRowLeftPx(nonAssetRows.nth(0)) test.expect(childLeft, 'Child is indented further than parent').toBeGreaterThan(parentLeft) }) .driveTable.dragRow(1, actions.locateRootDirectoryDropzone(page)) .driveTable.withRows(async (rows) => { const firstLeft = await actions.getAssetRowLeftPx(rows.nth(0)) - // The second row is the indented child of the directory - // (the "this folder is empty" row). - const secondLeft = await actions.getAssetRowLeftPx(rows.nth(2)) + const secondLeft = await actions.getAssetRowLeftPx(rows.nth(1)) test.expect(firstLeft, 'Siblings have same indentation').toEqual(secondLeft) }), ) diff --git a/app/dashboard/e2e/authPreserveEmail.spec.ts b/app/dashboard/e2e/authPreserveEmail.spec.ts new file mode 100644 index 000000000000..20df1440ce25 --- /dev/null +++ b/app/dashboard/e2e/authPreserveEmail.spec.ts @@ -0,0 +1,27 @@ +/** @file Test that emails are preserved when navigating between auth pages. */ +import * as test from '@playwright/test' +import { VALID_EMAIL, mockAll } from './actions' + +test.test('preserve email input when changing pages', ({ page }) => + mockAll({ page }) + .fillEmail(VALID_EMAIL) + .goToPage.register() + .withEmailInput(async (emailInput) => { + await test.expect(emailInput).toHaveValue(VALID_EMAIL) + }) + .fillEmail(`2${VALID_EMAIL}`) + .goToPage.login() + .withEmailInput(async (emailInput) => { + await test.expect(emailInput).toHaveValue(`2${VALID_EMAIL}`) + }) + .fillEmail(`3${VALID_EMAIL}`) + .goToPage.forgotPassword() + .withEmailInput(async (emailInput) => { + await test.expect(emailInput).toHaveValue(`3${VALID_EMAIL}`) + }) + .fillEmail(`4${VALID_EMAIL}`) + .goToPage.login() + .withEmailInput(async (emailInput) => { + await test.expect(emailInput).toHaveValue(`4${VALID_EMAIL}`) + }), +) diff --git a/app/dashboard/e2e/labels.spec.ts b/app/dashboard/e2e/labels.spec.ts index 92fb3892edb7..4b0d5a726faf 100644 --- a/app/dashboard/e2e/labels.spec.ts +++ b/app/dashboard/e2e/labels.spec.ts @@ -5,18 +5,24 @@ import * as backend from '#/services/Backend' import * as actions from './actions' +// eslint-disable-next-line @typescript-eslint/no-magic-numbers +export const ASSET_ROW_SAFE_POSITION = { x: 300, y: 16 } + +/** Click an asset row. The center must not be clicked as that is the button for adding a label. */ +export async function clickAssetRow(assetRow: test.Locator) { + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + await assetRow.click({ position: ASSET_ROW_SAFE_POSITION }) +} + test.test('drag labels onto single row', async ({ page }) => { const label = 'aaaa' await actions.mockAllAndLogin({ page, setupAPI: (api) => { api.addLabel(label, backend.COLORS[0]) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - api.addLabel('bbbb', backend.COLORS[1]!) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - api.addLabel('cccc', backend.COLORS[2]!) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - api.addLabel('dddd', backend.COLORS[3]!) + api.addLabel('bbbb', backend.COLORS[1]) + api.addLabel('cccc', backend.COLORS[2]) + api.addLabel('dddd', backend.COLORS[3]) api.addDirectory('foo') api.addSecret('bar') api.addFile('baz') @@ -41,12 +47,9 @@ test.test('drag labels onto multiple rows', async ({ page }) => { page, setupAPI: (api) => { api.addLabel(label, backend.COLORS[0]) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - api.addLabel('bbbb', backend.COLORS[1]!) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - api.addLabel('cccc', backend.COLORS[2]!) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - api.addLabel('dddd', backend.COLORS[3]!) + api.addLabel('bbbb', backend.COLORS[1]) + api.addLabel('cccc', backend.COLORS[2]) + api.addLabel('dddd', backend.COLORS[3]) api.addDirectory('foo') api.addSecret('bar') api.addFile('baz') @@ -57,8 +60,9 @@ test.test('drag labels onto multiple rows', async ({ page }) => { const labelEl = actions.locateLabelsPanelLabels(page, label) await page.keyboard.down(await actions.modModifier(page)) - await actions.clickAssetRow(assetRows.nth(0)) - await actions.clickAssetRow(assetRows.nth(2)) + await test.expect(assetRows).toHaveCount(4) + await clickAssetRow(assetRows.nth(0)) + await clickAssetRow(assetRows.nth(2)) await test.expect(labelEl).toBeVisible() await labelEl.dragTo(assetRows.nth(2)) await page.keyboard.up(await actions.modModifier(page)) diff --git a/app/dashboard/e2e/latestGithubReleases.json b/app/dashboard/e2e/latestGithubReleases.json new file mode 100644 index 000000000000..171185284e3e --- /dev/null +++ b/app/dashboard/e2e/latestGithubReleases.json @@ -0,0 +1,1106 @@ +{ + "url": "https://api.github.com/repos/enso-org/enso/releases/166959527", + "assets_url": "https://api.github.com/repos/enso-org/enso/releases/166959527/assets", + "upload_url": "https://uploads.github.com/repos/enso-org/enso/releases/166959527/assets{?name,label}", + "html_url": "https://github.com/enso-org/enso/releases/tag/2024.3.1-rc3", + "id": 166959527, + "author": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "node_id": "RE_kwDOBJHs384J85mn", + "tag_name": "2024.3.1-rc3", + "target_commitish": "df55f6506071ed89f08e8df1b21d4bca670b8dbe", + "name": "Enso 2024.3.1-rc3", + "draft": false, + "prerelease": false, + "created_at": "2024-07-24T16:40:45Z", + "published_at": "2024-07-24T18:48:34Z", + "assets": [ + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181611371", + "id": 181611371, + "node_id": "RA_kwDOBJHs384K0ytr", + "name": "assets.json", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/json", + "state": "uploaded", + "size": 1428, + "download_count": 1, + "created_at": "2024-07-24T18:48:37Z", + "updated_at": "2024-07-24T18:48:37Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/assets.json" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596166", + "id": 181596166, + "node_id": "RA_kwDOBJHs384K0vAG", + "name": "enso-bundle-2024.3.1-rc3-linux-amd64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 858298795, + "download_count": 3, + "created_at": "2024-07-24T17:08:31Z", + "updated_at": "2024-07-24T17:09:01Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-bundle-2024.3.1-rc3-linux-amd64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596666", + "id": 181596666, + "node_id": "RA_kwDOBJHs384K0vH6", + "name": "enso-bundle-2024.3.1-rc3-macos-aarch64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 862159215, + "download_count": 2, + "created_at": "2024-07-24T17:12:24Z", + "updated_at": "2024-07-24T17:12:52Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-bundle-2024.3.1-rc3-macos-aarch64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181599597", + "id": 181599597, + "node_id": "RA_kwDOBJHs384K0v1t", + "name": "enso-bundle-2024.3.1-rc3-macos-amd64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 849560594, + "download_count": 3, + "created_at": "2024-07-24T17:33:42Z", + "updated_at": "2024-07-24T17:36:05Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-bundle-2024.3.1-rc3-macos-amd64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181597182", + "id": 181597182, + "node_id": "RA_kwDOBJHs384K0vP-", + "name": "enso-bundle-2024.3.1-rc3-windows-amd64.zip", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 856323128, + "download_count": 3, + "created_at": "2024-07-24T17:15:16Z", + "updated_at": "2024-07-24T17:15:44Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-bundle-2024.3.1-rc3-windows-amd64.zip" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596021", + "id": 181596021, + "node_id": "RA_kwDOBJHs384K0u91", + "name": "enso-engine-2024.3.1-rc3-linux-amd64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 549393006, + "download_count": 1, + "created_at": "2024-07-24T17:07:11Z", + "updated_at": "2024-07-24T17:07:31Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-engine-2024.3.1-rc3-linux-amd64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596484", + "id": 181596484, + "node_id": "RA_kwDOBJHs384K0vFE", + "name": "enso-engine-2024.3.1-rc3-macos-aarch64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 547995238, + "download_count": 0, + "created_at": "2024-07-24T17:10:38Z", + "updated_at": "2024-07-24T17:10:56Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-engine-2024.3.1-rc3-macos-aarch64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181599192", + "id": 181599192, + "node_id": "RA_kwDOBJHs384K0vvY", + "name": "enso-engine-2024.3.1-rc3-macos-amd64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 548006738, + "download_count": 0, + "created_at": "2024-07-24T17:30:56Z", + "updated_at": "2024-07-24T17:32:38Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-engine-2024.3.1-rc3-macos-amd64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596857", + "id": 181596857, + "node_id": "RA_kwDOBJHs384K0vK5", + "name": "enso-engine-2024.3.1-rc3-windows-amd64.zip", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 546077818, + "download_count": 0, + "created_at": "2024-07-24T17:13:33Z", + "updated_at": "2024-07-24T17:14:03Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-engine-2024.3.1-rc3-windows-amd64.zip" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596071", + "id": 181596071, + "node_id": "RA_kwDOBJHs384K0u-n", + "name": "enso-launcher-2024.3.1-rc3-linux-amd64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 20113787, + "download_count": 0, + "created_at": "2024-07-24T17:07:34Z", + "updated_at": "2024-07-24T17:07:36Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-launcher-2024.3.1-rc3-linux-amd64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596512", + "id": 181596512, + "node_id": "RA_kwDOBJHs384K0vFg", + "name": "enso-launcher-2024.3.1-rc3-macos-aarch64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 19643459, + "download_count": 0, + "created_at": "2024-07-24T17:10:59Z", + "updated_at": "2024-07-24T17:11:01Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-launcher-2024.3.1-rc3-macos-aarch64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181599494", + "id": 181599494, + "node_id": "RA_kwDOBJHs384K0v0G", + "name": "enso-launcher-2024.3.1-rc3-macos-amd64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 19800200, + "download_count": 0, + "created_at": "2024-07-24T17:32:41Z", + "updated_at": "2024-07-24T17:32:43Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-launcher-2024.3.1-rc3-macos-amd64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181597031", + "id": 181597031, + "node_id": "RA_kwDOBJHs384K0vNn", + "name": "enso-launcher-2024.3.1-rc3-windows-amd64.zip", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 19484737, + "download_count": 0, + "created_at": "2024-07-24T17:14:08Z", + "updated_at": "2024-07-24T17:14:09Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-launcher-2024.3.1-rc3-windows-amd64.zip" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181598141", + "id": 181598141, + "node_id": "RA_kwDOBJHs384K0ve9", + "name": "enso-linux-x86_64-2024.3.1-rc3.AppImage", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 1030383958, + "download_count": 370, + "created_at": "2024-07-24T17:22:11Z", + "updated_at": "2024-07-24T17:22:48Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-linux-x86_64-2024.3.1-rc3.AppImage" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181598234", + "id": 181598234, + "node_id": "RA_kwDOBJHs384K0vga", + "name": "enso-linux-x86_64-2024.3.1-rc3.sha256", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 64, + "download_count": 0, + "created_at": "2024-07-24T17:22:48Z", + "updated_at": "2024-07-24T17:22:48Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-linux-x86_64-2024.3.1-rc3.sha256" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181606533", + "id": 181606533, + "node_id": "RA_kwDOBJHs384K0xiF", + "name": "enso-mac-arm64-2024.3.1-rc3.dmg", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 1022616014, + "download_count": 22, + "created_at": "2024-07-24T18:16:04Z", + "updated_at": "2024-07-24T18:16:41Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-mac-arm64-2024.3.1-rc3.dmg" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181606723", + "id": 181606723, + "node_id": "RA_kwDOBJHs384K0xlD", + "name": "enso-mac-arm64-2024.3.1-rc3.sha256", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 64, + "download_count": 0, + "created_at": "2024-07-24T18:16:41Z", + "updated_at": "2024-07-24T18:16:42Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-mac-arm64-2024.3.1-rc3.sha256" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181611019", + "id": 181611019, + "node_id": "RA_kwDOBJHs384K0yoL", + "name": "enso-mac-x64-2024.3.1-rc3.dmg", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 1018774891, + "download_count": 2, + "created_at": "2024-07-24T18:46:07Z", + "updated_at": "2024-07-24T18:47:37Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-mac-x64-2024.3.1-rc3.dmg" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181611229", + "id": 181611229, + "node_id": "RA_kwDOBJHs384K0yrd", + "name": "enso-mac-x64-2024.3.1-rc3.sha256", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 64, + "download_count": 0, + "created_at": "2024-07-24T18:47:37Z", + "updated_at": "2024-07-24T18:47:37Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-mac-x64-2024.3.1-rc3.sha256" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596080", + "id": 181596080, + "node_id": "RA_kwDOBJHs384K0u-w", + "name": "enso-project-manager-2024.3.1-rc3-linux-amd64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 72941303, + "download_count": 0, + "created_at": "2024-07-24T17:07:43Z", + "updated_at": "2024-07-24T17:07:46Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-project-manager-2024.3.1-rc3-linux-amd64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596537", + "id": 181596537, + "node_id": "RA_kwDOBJHs384K0vF5", + "name": "enso-project-manager-2024.3.1-rc3-macos-aarch64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 71748842, + "download_count": 0, + "created_at": "2024-07-24T17:11:07Z", + "updated_at": "2024-07-24T17:11:11Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-project-manager-2024.3.1-rc3-macos-aarch64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181599514", + "id": 181599514, + "node_id": "RA_kwDOBJHs384K0v0a", + "name": "enso-project-manager-2024.3.1-rc3-macos-amd64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 72474560, + "download_count": 0, + "created_at": "2024-07-24T17:32:52Z", + "updated_at": "2024-07-24T17:32:59Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-project-manager-2024.3.1-rc3-macos-amd64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181597064", + "id": 181597064, + "node_id": "RA_kwDOBJHs384K0vOI", + "name": "enso-project-manager-2024.3.1-rc3-windows-amd64.zip", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 70706423, + "download_count": 0, + "created_at": "2024-07-24T17:14:28Z", + "updated_at": "2024-07-24T17:14:31Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-project-manager-2024.3.1-rc3-windows-amd64.zip" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181599175", + "id": 181599175, + "node_id": "RA_kwDOBJHs384K0vvH", + "name": "enso-win-x64-2024.3.1-rc3.exe", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/vnd.microsoft.portable-executable", + "state": "uploaded", + "size": 1037289696, + "download_count": 35, + "created_at": "2024-07-24T17:30:39Z", + "updated_at": "2024-07-24T17:31:51Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-win-x64-2024.3.1-rc3.exe" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181599394", + "id": 181599394, + "node_id": "RA_kwDOBJHs384K0vyi", + "name": "enso-win-x64-2024.3.1-rc3.sha256", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 64, + "download_count": 0, + "created_at": "2024-07-24T17:31:51Z", + "updated_at": "2024-07-24T17:31:51Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-win-x64-2024.3.1-rc3.sha256" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596446", + "id": 181596446, + "node_id": "RA_kwDOBJHs384K0vEe", + "name": "launcher-manifest.yaml", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "text/yaml", + "state": "uploaded", + "size": 101, + "download_count": 0, + "created_at": "2024-07-24T17:10:30Z", + "updated_at": "2024-07-24T17:10:30Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/launcher-manifest.yaml" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596443", + "id": 181596443, + "node_id": "RA_kwDOBJHs384K0vEb", + "name": "manifest.yaml", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "text/yaml", + "state": "uploaded", + "size": 274, + "download_count": 0, + "created_at": "2024-07-24T17:10:29Z", + "updated_at": "2024-07-24T17:10:29Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/manifest.yaml" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181596336", + "id": 181596336, + "node_id": "RA_kwDOBJHs384K0vCw", + "name": "project-manager-bundle-2024.3.1-rc3-linux-amd64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 911128717, + "download_count": 1, + "created_at": "2024-07-24T17:09:55Z", + "updated_at": "2024-07-24T17:10:29Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/project-manager-bundle-2024.3.1-rc3-linux-amd64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181597048", + "id": 181597048, + "node_id": "RA_kwDOBJHs384K0vN4", + "name": "project-manager-bundle-2024.3.1-rc3-macos-aarch64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 914278251, + "download_count": 1, + "created_at": "2024-07-24T17:14:18Z", + "updated_at": "2024-07-24T17:14:51Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/project-manager-bundle-2024.3.1-rc3-macos-aarch64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181600014", + "id": 181600014, + "node_id": "RA_kwDOBJHs384K0v8O", + "name": "project-manager-bundle-2024.3.1-rc3-macos-amd64.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 902231006, + "download_count": 1, + "created_at": "2024-07-24T17:36:54Z", + "updated_at": "2024-07-24T17:40:15Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/project-manager-bundle-2024.3.1-rc3-macos-amd64.tar.gz" + }, + { + "url": "https://api.github.com/repos/enso-org/enso/releases/assets/181597329", + "id": 181597329, + "node_id": "RA_kwDOBJHs384K0vSR", + "name": "project-manager-bundle-2024.3.1-rc3-windows-amd64.zip", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 907545448, + "download_count": 1, + "created_at": "2024-07-24T17:16:36Z", + "updated_at": "2024-07-24T17:17:09Z", + "browser_download_url": "https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/project-manager-bundle-2024.3.1-rc3-windows-amd64.zip" + } + ], + "tarball_url": "https://api.github.com/repos/enso-org/enso/tarball/2024.3.1-rc3", + "zipball_url": "https://api.github.com/repos/enso-org/enso/zipball/2024.3.1-rc3", + "body": "# Download\r\n\r\n## Enso IDE\r\n\r\nEnso IDE is the main product of the Enso project. The packages are stand-alone, they contain both GUI and the backend.\r\n\r\nDownload links:\r\n- [Windows](https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-win-x64-2024.3.1-rc3.exe)\r\n- [Linux](https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-linux-x86_64-2024.3.1-rc3.AppImage)\r\n- [macOS (Intel)](https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-mac-x64-2024.3.1-rc3.dmg)\r\n- [macOS (Apple silicon)](https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-mac-arm64-2024.3.1-rc3.dmg)\r\n\r\nThis is the recommended download for most users.\r\n\r\n## Enso Engine\r\n\r\nIf you are interested in using Enso Engine command line tools only, download the Enso Engine bundle.\r\n\r\nDownload links:\r\n- [Windows](https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-bundle-2024.3.1-rc3-windows-amd64.zip)\r\n- [Linux](https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-bundle-2024.3.1-rc3-linux-amd64.tar.gz)\r\n- [macOS (Intel)](https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-bundle-2024.3.1-rc3-macos-amd64.tar.gz)\r\n- [macOS (Apple silicon)](https://github.com/enso-org/enso/releases/download/2024.3.1-rc3/enso-bundle-2024.3.1-rc3-macos-aarch64.tar.gz)\r\n\r\nThese are archives containing the [Enso portable distribution](https://enso.org/docs/developer/enso/distribution/distribution.html#portable-enso-distribution-layout). User is responsible for setting up the environment variables and adding the `bin` directory to the `PATH`.\r\n\r\nNote that these distributions do not allow you to use the Enso IDE.\r\n\r\nIt is recommended only for advanced users, who want to just try the compiler CLI.\r\n\r\n# Anonymous Data Collection\r\n\r\nPlease note that this release collects anonymous usage data which will be used to improve Enso and prepare it for a stable release. We will switch to opt-in data collection in stable version releases. The usage data will not contain your code (expressions above nodes), however, reported errors may contain brief snippets of out of context code that specifically leads to the error, like \"the method 'foo' does not exist on Number\". The following data will be collected:\r\n\r\n- Session length.\r\n- Graph editing events (node creation, deletion, position change, connect, disconnect, collapse, edit start, edit end). This will not include any information about node expressions used.\r\n- Navigation events (camera movement, scope change).\r\n- Visualization events (visualization open, close, switch). This will not include any information about the displayed data nor the rendered visualization itself.\r\n- Project management events (project open, close, rename).\r\n- Errors (IDE crashes, WASM panics, Project Manager errors, Language Server errors, Compiler errors).\r\n- Performance statistics (minimum, maximum, average GUI refresh rate).\r\n\r\n# Changelog\r\n\r\n#### Enso Language & Runtime\r\n\r\n- [Enforce conversion method return type][10468]\r\n- [Renaming launcher executable to ensoup][10535]\r\n- [Space-precedence does not apply to value-level operators][10597]\r\n\r\n[10468]: https://github.com/enso-org/enso/pull/10468\r\n[10535]: https://github.com/enso-org/enso/pull/10535\r\n[10597]: https://github.com/enso-org/enso/pull/10597\r\n\r\n#### Enso IDE\r\n\r\n- ["Add node" button is not obscured by output port][10433]\r\n- [Numeric Widget does not accept non-numeric input][10457]. This is to prevent\r\n node being completely altered by accidental code put to the widget.\r\n- [Redesigned "record control" panel][10509]. Now it contains more intuitive\r\n "refresh" and "write all" buttons.\r\n- [Warning messages do not obscure visualization buttons][10546].\r\n- [Output component in collapsed function changed][10577]. It cannot be deleted\r\n anymore, except by directily editing the code.\r\n- [Multiselect drop-down widget visuals are improved][10607].\r\n- [Text displayed in monospace and whitespace rendered as symbols][10563].\r\n\r\n[10433]: https://github.com/enso-org/enso/pull/10443\r\n[10457]: https://github.com/enso-org/enso/pull/10457\r\n[10509]: https://github.com/enso-org/enso/pull/10509\r\n[10546]: https://github.com/enso-org/enso/pull/10546\r\n[10577]: https://github.com/enso-org/enso/pull/10577\r\n[10607]: https://github.com/enso-org/enso/pull/10607\r\n[10563]: https://github.com/enso-org/enso/pull/10563\r\n\r\n#### Enso Enso Standard Library\r\n\r\n- [Renamed `Data.list_directory` to `Data.list`. Removed list support from read\r\n methods.][10434]\r\n- [Renamed `Location.Start` to `Location.Left` and `Location.End` to\r\n `Location.Right`.][10445]\r\n- [Renamed `Postgres_Details.Postgres` to `Postgres.Server`.][10466]\r\n- [Remove `First` and `Last` from namespace, use auto-scoped.][10467]\r\n- [Rename `Map` to `Dictionary` and `Set` to `Hashset`.][10474]\r\n- [Compare two objects with `Ordering.compare` and define comparator with\r\n `Comparable.new`][10468]\r\n- [Added `dec` construction function for creating `Decimal`s.][10517]\r\n\r\n[10434]: https://github.com/enso-org/enso/pull/10434\r\n[10445]: https://github.com/enso-org/enso/pull/10445\r\n[10466]: https://github.com/enso-org/enso/pull/10466\r\n[10467]: https://github.com/enso-org/enso/pull/10467\r\n[10474]: https://github.com/enso-org/enso/pull/10474\r\n[10517]: https://github.com/enso-org/enso/pull/10517\r\n", + "reactions": { + "url": "https://api.github.com/repos/enso-org/enso/releases/166959527/reactions", + "total_count": 1, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 1, + "eyes": 0 + } +} diff --git a/app/dashboard/e2e/loginScreen.spec.ts b/app/dashboard/e2e/loginScreen.spec.ts index 2dbd7be3efc1..61cac698b1ba 100644 --- a/app/dashboard/e2e/loginScreen.spec.ts +++ b/app/dashboard/e2e/loginScreen.spec.ts @@ -1,35 +1,34 @@ /** @file Test the login flow. */ import * as test from '@playwright/test' -import * as actions from './actions' - -test.test.beforeEach(({ page }) => actions.mockAll({ page })) +import { + INVALID_PASSWORD, + mockAll, + passTermsAndConditionsDialog, + TEXT, + VALID_EMAIL, + VALID_PASSWORD, +} from './actions' // ============= // === Tests === // ============= -test.test('login screen', async ({ page }) => { - await page.goto('/') - - // Invalid email - await actions.locateEmailInput(page).fill('invalid email') - test - .expect( - await page.evaluate(() => document.querySelector('form')?.checkValidity()), - 'form should reject invalid email', - ) - .toBe(false) - await actions.locateLoginButton(page).click() - - // Invalid password - await actions.locateEmailInput(page).fill(actions.VALID_EMAIL) - await actions.locatePasswordInput(page).fill(actions.INVALID_PASSWORD) - test - .expect( - await page.evaluate(() => document.querySelector('form')?.checkValidity()), - 'form should accept invalid password', - ) - .toBe(true) - await actions.locateLoginButton(page).click() -}) +test.test('login screen', ({ page }) => + mockAll({ page }) + .loginThatShouldFail('invalid email', VALID_PASSWORD, { + assert: { + emailError: TEXT.invalidEmailValidationError, + passwordError: null, + formError: null, + }, + }) + // Technically it should not be allowed, but + .login(VALID_EMAIL, INVALID_PASSWORD) + .do(async (thePage) => { + await passTermsAndConditionsDialog({ page: thePage }) + }) + .withDriveView(async (driveView) => { + await test.expect(driveView).toBeVisible() + }), +) diff --git a/app/dashboard/e2e/pageSwitcher.spec.ts b/app/dashboard/e2e/pageSwitcher.spec.ts index 115478fc3387..4800b78ab532 100644 --- a/app/dashboard/e2e/pageSwitcher.spec.ts +++ b/app/dashboard/e2e/pageSwitcher.spec.ts @@ -8,6 +8,10 @@ test.test('page switcher', ({ page }) => .mockAllAndLogin({ page }) // Create a new project so that the editor page can be switched to. .newEmptyProject() + .do(async (thePage) => { + await test.expect(actions.locateDriveView(thePage)).not.toBeVisible() + await test.expect(actions.locateEditor(thePage)).toBeVisible() + }) .goToPage.drive() .do(async (thePage) => { await test.expect(actions.locateDriveView(thePage)).toBeVisible() diff --git a/app/dashboard/e2e/signUp.spec.ts b/app/dashboard/e2e/signUp.spec.ts new file mode 100644 index 000000000000..6c3426235084 --- /dev/null +++ b/app/dashboard/e2e/signUp.spec.ts @@ -0,0 +1,38 @@ +/** @file Test the login flow. */ +import * as test from '@playwright/test' + +import { INVALID_PASSWORD, mockAll, TEXT, VALID_EMAIL, VALID_PASSWORD } from './actions' + +// ============= +// === Tests === +// ============= + +test.test('sign up without organization id', ({ page }) => + mockAll({ page }) + .goToPage.register() + .registerThatShouldFail('invalid email', VALID_PASSWORD, VALID_PASSWORD, { + assert: { + emailError: TEXT.invalidEmailValidationError, + passwordError: null, + confirmPasswordError: null, + formError: null, + }, + }) + .registerThatShouldFail(VALID_EMAIL, INVALID_PASSWORD, INVALID_PASSWORD, { + assert: { + emailError: null, + passwordError: TEXT.passwordValidationError, + confirmPasswordError: null, + formError: null, + }, + }) + .registerThatShouldFail(VALID_EMAIL, VALID_PASSWORD, INVALID_PASSWORD, { + assert: { + emailError: null, + passwordError: null, + confirmPasswordError: TEXT.passwordMismatchError, + formError: null, + }, + }) + .register(), +) diff --git a/app/dashboard/e2e/userSettings.spec.ts b/app/dashboard/e2e/userSettings.spec.ts index aa6da128c884..6f91554af1f1 100644 --- a/app/dashboard/e2e/userSettings.spec.ts +++ b/app/dashboard/e2e/userSettings.spec.ts @@ -25,45 +25,37 @@ test.test('change password form', async ({ page }) => { test.expect(api.currentPassword()).toBe(actions.VALID_PASSWORD) await localActions.locateCurrentPasswordInput(page).fill(actions.VALID_PASSWORD) await localActions.locateNewPasswordInput(page).fill(actions.INVALID_PASSWORD) - await test - .expect(localActions.locateChangeButton(page), 'incomplete form should be rejected') - .toBeDisabled() await test.test.step('Invalid new password', async () => { await localActions.locateCurrentPasswordInput(page).fill(actions.VALID_PASSWORD) await localActions.locateNewPasswordInput(page).fill(actions.INVALID_PASSWORD) await localActions.locateConfirmNewPasswordInput(page).fill(actions.INVALID_PASSWORD) - test + await localActions.locateChangeButton(page).click() + await test .expect( - await localActions - .locateNewPasswordInput(page) - .evaluate((element: HTMLInputElement) => element.validity.valid), - 'invalid new password should be rejected', + localActions + .locate(page) + .getByRole('group', { name: /^New password/, exact: true }) + .locator('.text-danger') + .last(), ) - .toBe(false) - await test - .expect(localActions.locateChangeButton(page), 'invalid new password should be rejected') - .toBeDisabled() + .toHaveText(actions.TEXT.passwordValidationError) }) await test.test.step('Invalid new password confirmation', async () => { await localActions.locateCurrentPasswordInput(page).fill(actions.VALID_PASSWORD) await localActions.locateNewPasswordInput(page).fill(actions.VALID_PASSWORD) await localActions.locateConfirmNewPasswordInput(page).fill(actions.VALID_PASSWORD + 'a') - test - .expect( - await localActions - .locateConfirmNewPasswordInput(page) - .evaluate((element: HTMLInputElement) => element.validity.valid), - 'invalid new password confirmation should be rejected', - ) - .toBe(false) + await localActions.locateChangeButton(page).click() await test .expect( - localActions.locateChangeButton(page), - 'invalid new password confirmation should be rejected', + localActions + .locate(page) + .getByRole('group', { name: /^Confirm new password/, exact: true }) + .locator('.text-danger') + .last(), ) - .toBeDisabled() + .toHaveText(actions.TEXT.passwordMismatchError) }) await test.test.step('Successful password change', async () => { diff --git a/app/dashboard/package.json b/app/dashboard/package.json index fc8df720b002..1bd447c99121 100644 --- a/app/dashboard/package.json +++ b/app/dashboard/package.json @@ -15,16 +15,17 @@ }, "scripts": { "compile": "tsc", - "typecheck": "tsc --noEmit", + "typecheck": "tsc", "build": "vite build", + "lint": "eslint .", "dev": "vite", "dev:e2e": "vite -c vite.test.config.ts", "dev:e2e:ci": "vite -c vite.test.config.ts build && vite preview --port 8080 --strictPort", - "test": "npm run test:unit && npm run test:e2e", + "test": "corepack pnpm run /^^^^test:.*/", "test:unit": "vitest run", - "test:unit:debug": "vitest", + "test-dev:unit": "vitest", "test:e2e": "cross-env NODE_ENV=production playwright test", - "test:e2e:debug": "cross-env NODE_ENV=production playwright test --ui" + "test-dev:e2e": "cross-env NODE_ENV=production playwright test --ui" }, "dependencies": { "@aws-amplify/auth": "5.6.5", diff --git a/app/dashboard/playwright.config.ts b/app/dashboard/playwright.config.ts index dd9597fc053e..f0bf92b3fcfc 100644 --- a/app/dashboard/playwright.config.ts +++ b/app/dashboard/playwright.config.ts @@ -57,7 +57,7 @@ export default test.defineConfig({ }, }, webServer: { - command: process.env.CI || process.env.PROD ? 'npm run dev:e2e:ci' : 'npm run dev:e2e', + command: `corepack pnpm run ${process.env.CI || process.env.PROD ? 'dev:e2e:ci' : 'dev:e2e'}`, port: 8080, reuseExistingServer: false, }, diff --git a/app/dashboard/src/App.tsx b/app/dashboard/src/App.tsx index eee830bfa0b8..d4b0f8a72cb9 100644 --- a/app/dashboard/src/App.tsx +++ b/app/dashboard/src/App.tsx @@ -38,6 +38,7 @@ import * as React from 'react' import * as reactQuery from '@tanstack/react-query' import * as router from 'react-router-dom' import * as toastify from 'react-toastify' +import * as z from 'zod' import * as detect from 'enso-common/src/detect' @@ -45,17 +46,14 @@ import * as appUtils from '#/appUtils' import * as inputBindingsModule from '#/configurations/inputBindings' -import * as backendHooks from '#/hooks/backendHooks' - import AuthProvider, * as authProvider from '#/providers/AuthProvider' -import BackendProvider, { useLocalBackend, useRemoteBackend } from '#/providers/BackendProvider' +import BackendProvider from '#/providers/BackendProvider' import DriveProvider from '#/providers/DriveProvider' import DevtoolsProvider from '#/providers/EnsoDevtoolsProvider' -import * as httpClientProvider from '#/providers/HttpClientProvider' +import { useHttpClient } from '#/providers/HttpClientProvider' import InputBindingsProvider from '#/providers/InputBindingsProvider' import LocalStorageProvider, * as localStorageProvider from '#/providers/LocalStorageProvider' -import type * as loggerProvider from '#/providers/LoggerProvider' -import LoggerProvider from '#/providers/LoggerProvider' +import { useLogger } from '#/providers/LoggerProvider' import ModalProvider, * as modalProvider from '#/providers/ModalProvider' import * as navigator2DProvider from '#/providers/Navigator2DProvider' import SessionProvider from '#/providers/SessionProvider' @@ -76,10 +74,9 @@ import type * as editor from '#/layouts/Editor' import * as openAppWatcher from '#/layouts/OpenAppWatcher' import VersionChecker from '#/layouts/VersionChecker' +import { RouterProvider } from '#/components/aria' import * as devtools from '#/components/Devtools' import * as errorBoundary from '#/components/ErrorBoundary' -import * as offlineNotificationManager from '#/components/OfflineNotificationManager' -import * as rootComponent from '#/components/Root' import * as suspense from '#/components/Suspense' import AboutModal from '#/modals/AboutModal' @@ -92,11 +89,10 @@ import RemoteBackend from '#/services/RemoteBackend' import * as appBaseUrl from '#/utilities/appBaseUrl' import * as eventModule from '#/utilities/event' -import type HttpClient from '#/utilities/HttpClient' import LocalStorage from '#/utilities/LocalStorage' import * as object from '#/utilities/object' -import * as authServiceModule from '#/authentication/service' +import { useInitAuthService } from '#/authentication/service' // ============================ // === Global configuration === @@ -110,17 +106,16 @@ declare module '#/utilities/LocalStorage' { } LocalStorage.registerKey('inputBindings', { - tryParse: (value) => - typeof value !== 'object' || value == null ? - null - : Object.fromEntries( - Object.entries({ ...value }).flatMap((kv) => { - const [k, v] = kv - return Array.isArray(v) && v.every((item): item is string => typeof item === 'string') ? - [[k, v]] - : [] - }), - ), + schema: z.record(z.string().array().readonly()).transform((value) => + Object.fromEntries( + Object.entries({ ...value }).flatMap((kv) => { + const [k, v] = kv + return Array.isArray(v) && v.every((item): item is string => typeof item === 'string') ? + [[k, v]] + : [] + }), + ), + ), }) // ====================== @@ -141,7 +136,6 @@ function getMainPageUrl() { /** Global configuration for the `App` component. */ export interface AppProps { readonly vibrancy: boolean - readonly logger: loggerProvider.Logger /** Whether the application may have the local backend running. */ readonly supportsLocalBackend: boolean /** If true, the app can only be used in offline mode. */ @@ -157,8 +151,6 @@ export interface AppProps { readonly projectManagerUrl: string | null readonly ydocUrl: string | null readonly appRunner: editor.GraphEditorRunner | null - readonly portalRoot: Element - readonly httpClient: HttpClient readonly queryClient: reactQuery.QueryClient } @@ -262,12 +254,10 @@ export interface AppRouterProps extends AppProps { * because the {@link AppRouter} relies on React hooks, which can't be used in the same React * component as the component that defines the provider. */ function AppRouter(props: AppRouterProps) { - const { logger, isAuthenticationDisabled, shouldShowDashboard, httpClient } = props + const { isAuthenticationDisabled, shouldShowDashboard } = props const { onAuthenticated, projectManagerInstance } = props - const { portalRoot } = props - // `navigateHooks.useNavigate` cannot be used here as it relies on `AuthProvider`, which has not - // yet been initialized at this point. - // eslint-disable-next-line no-restricted-properties + const httpClient = useHttpClient() + const logger = useLogger() const navigate = router.useNavigate() const { getText } = textProvider.useText() const { localStorage } = localStorageProvider.useLocalStorage() @@ -356,14 +346,8 @@ function AppRouter(props: AppRouterProps) { }, } }, [localStorage, inputBindingsRaw]) - const mainPageUrl = getMainPageUrl() - - const authService = React.useMemo(() => { - const authConfig = { navigate, ...props } - return authServiceModule.initAuthService(authConfig) - }, [props, navigate]) - + const authService = useInitAuthService(props) const userSession = authService?.cognito.userSession.bind(authService.cognito) ?? null const refreshUserSession = authService?.cognito.refreshUserSession.bind(authService.cognito) ?? null @@ -494,90 +478,41 @@ function AppRouter(props: AppRouterProps) { ) - let result = ( - <> - - - {routes} - - ) - - result = ( - <> - {result} - - - - - - - - ) - result = {result} - result = {result} - result = ( - - {result} - - ) - - result = ( - - {result} - - ) - - result = ( - - {result} - - ) - result = {result} - result = ( - - {result} - - ) - // Ideally this would be in `Drive.tsx`, but it currently must be all the way out here - // due to modals being in `TheModal`. - result = {result} - result = ( - - {result} - - ) - result = ( - - {result} - + return ( + + + + + + + {/* Ideally this would be in `Drive.tsx`, but it currently must be all the way out here + * due to modals being in `TheModal`. */} + + + + {routes} + {detect.IS_DEV_MODE && ( + + + + )} + + + + + + + + ) - result = {result} - result = {result} - - return result -} - -// ======================== -// === MutationListener === -// ======================== - -/** A component that applies state updates for successful mutations. */ -function MutationListener() { - const remoteBackend = useRemoteBackend() - const localBackend = useLocalBackend() - - backendHooks.useObserveBackend(remoteBackend) - backendHooks.useObserveBackend(localBackend) - - return null } diff --git a/app/dashboard/src/assets/github_color.svg b/app/dashboard/src/assets/github_color.svg new file mode 100644 index 000000000000..43530dea8077 --- /dev/null +++ b/app/dashboard/src/assets/github_color.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/dashboard/src/assets/google_color.svg b/app/dashboard/src/assets/google_color.svg new file mode 100644 index 000000000000..d0b375f80225 --- /dev/null +++ b/app/dashboard/src/assets/google_color.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/dashboard/src/authentication/service.ts b/app/dashboard/src/authentication/service.ts index 20404e5a957a..a72f2b3d83d5 100644 --- a/app/dashboard/src/authentication/service.ts +++ b/app/dashboard/src/authentication/service.ts @@ -1,14 +1,17 @@ /** @file Provides an {@link AuthService} which consists of an underyling `Cognito` API * wrapper, along with some convenience callbacks to make URL redirects for the authentication flows * work with Electron. */ +import * as React from 'react' + import * as amplify from '@aws-amplify/auth' +import { useNavigate } from 'react-router' import * as common from 'enso-common' import * as detect from 'enso-common/src/detect' import * as appUtils from '#/appUtils' -import type * as loggerProvider from '#/providers/LoggerProvider' +import { useLogger, type Logger } from '#/providers/LoggerProvider' import type * as saveAccessTokenModule from '#/utilities/accessToken' @@ -90,16 +93,9 @@ export function toNestedAmplifyConfig(config: AmplifyConfig): NestedAmplifyConfi /** Configuration for the authentication service. */ export interface AuthConfig { - /** Logger for the authentication service. */ - readonly logger: loggerProvider.Logger /** Whether the application supports deep links. This is only true when using * the installed app on macOS and Windows. */ readonly supportsDeepLinks: boolean - /** Function to navigate to a given (relative) URL. - * - * Used to redirect to pages like the password reset page with the query parameters set in the - * URL (e.g., `?verification_code=...`). */ - readonly navigate: (url: string) => void } // =================== @@ -118,24 +114,28 @@ export interface AuthService { * * # Warning * - * This function should only be called once, and the returned service should be used throughout the - * application. This is because it performs global configuration of the Amplify library. */ -export function initAuthService(authConfig: AuthConfig): AuthService | null { - const { logger, supportsDeepLinks, navigate } = authConfig - const amplifyConfig = loadAmplifyConfig(logger, supportsDeepLinks, navigate) - const cognito = - amplifyConfig == null ? null : ( - new cognitoModule.Cognito(logger, supportsDeepLinks, amplifyConfig) - ) + * This hook should only be called in a single place, as it performs global configuration of the + * Amplify library. */ +export function useInitAuthService(authConfig: AuthConfig): AuthService | null { + const { supportsDeepLinks } = authConfig + const logger = useLogger() + const navigate = useNavigate() + return React.useMemo(() => { + const amplifyConfig = loadAmplifyConfig(logger, supportsDeepLinks, navigate) + const cognito = + amplifyConfig == null ? null : ( + new cognitoModule.Cognito(logger, supportsDeepLinks, amplifyConfig) + ) - return cognito == null ? null : ( - { cognito, registerAuthEventListener: listen.registerAuthEventListener } - ) + return cognito == null ? null : ( + { cognito, registerAuthEventListener: listen.registerAuthEventListener } + ) + }, [logger, navigate, supportsDeepLinks]) } /** Return the appropriate Amplify configuration for the current platform. */ function loadAmplifyConfig( - logger: loggerProvider.Logger, + logger: Logger, supportsDeepLinks: boolean, navigate: (url: string) => void, ): AmplifyConfig | null { @@ -213,7 +213,7 @@ function loadAmplifyConfig( * * All URLs that don't have a pathname that starts with `AUTHENTICATION_PATHNAME_BASE` will be * ignored by this handler. */ -function setDeepLinkHandler(logger: loggerProvider.Logger, navigate: (url: string) => void) { +function setDeepLinkHandler(logger: Logger, navigate: (url: string) => void) { window.authenticationApi.setDeepLinkHandler((urlString: string) => { const url = new URL(urlString) logger.log(`Parsed pathname: ${url.pathname}`) diff --git a/app/dashboard/src/components/AriaComponents/Button/Button.tsx b/app/dashboard/src/components/AriaComponents/Button/Button.tsx index 95e2dc13524d..55421ed0cb83 100644 --- a/app/dashboard/src/components/AriaComponents/Button/Button.tsx +++ b/app/dashboard/src/components/AriaComponents/Button/Button.tsx @@ -100,7 +100,6 @@ export const BUTTON_STYLES = twv.tv({ }, loading: { true: { base: 'cursor-wait' } }, fullWidth: { true: 'w-full' }, - fullWidthText: { true: { text: 'w-full' } }, size: { custom: { base: '', extraClickZone: '', icon: 'h-full' }, hero: { base: 'px-8 py-4 text-lg font-bold', content: 'gap-[0.75em]' }, @@ -112,7 +111,7 @@ export const BUTTON_STYLES = twv.tv({ className: 'flex px-[11px] py-[5.5px]', }), content: 'gap-2', - icon: 'mb-[-0.1cap] h-4.5 w-4.5', + icon: 'mb-[-0.1cap] h-4 w-4', extraClickZone: 'after:inset-[-6px]', }, medium: { @@ -218,7 +217,7 @@ export const BUTTON_STYLES = twv.tv({ extraClickZone: 'flex relative after:absolute after:cursor-pointer', }, false: { - extraClickZone: '', + extraClickZone: 'after:inset-0', }, xxsmall: { extraClickZone: 'after:inset-[-2px]', @@ -295,7 +294,6 @@ export const Button = React.forwardRef(function Button( iconPosition, size, fullWidth, - fullWidthText, rounded, tooltip, tooltipPlacement, @@ -316,8 +314,7 @@ export const Button = React.forwardRef(function Button( const Tag = isLink ? aria.Link : aria.Button const goodDefaults = { - ...(isLink ? { rel: 'noopener noreferrer', ref } : {}), - ...(isLink ? {} : { type: 'button' as const }), + ...(isLink ? { rel: 'noopener noreferrer' } : { type: 'button' as const }), 'data-testid': testId ?? (isLink ? 'link' : 'button'), } @@ -389,7 +386,6 @@ export const Button = React.forwardRef(function Button( isActive, loading: isLoading, fullWidth, - fullWidthText, size, rounded, variant, @@ -443,7 +439,8 @@ export const Button = React.forwardRef(function Button( ()(goodDefaults, ariaProps, focusChildProps, { - isDisabled: isDisabled, + ref, + isDisabled, // we use onPressEnd instead of onPress because for some reason react-aria doesn't trigger // onPress on EXTRA_CLICK_ZONE, but onPress{start,end} are triggered onPressEnd: handlePress, diff --git a/app/dashboard/src/components/AriaComponents/Form/Form.tsx b/app/dashboard/src/components/AriaComponents/Form/Form.tsx index 7698f2939382..f03489fe2f8e 100644 --- a/app/dashboard/src/components/AriaComponents/Form/Form.tsx +++ b/app/dashboard/src/components/AriaComponents/Form/Form.tsx @@ -13,11 +13,23 @@ import * as aria from '#/components/aria' import * as errorUtils from '#/utilities/error' +import type { Mutable } from 'enso-common/src/utilities/data/object' import * as dialog from '../Dialog' import * as components from './components' import * as styles from './styles' import type * as types from './types' +/** + * Maps the value to the event object. + */ +function mapValueOnEvent(value: unknown) { + if (typeof value === 'object' && value != null && 'target' in value && 'type' in value) { + return value + } else { + return { target: { value } } + } +} + /** Form component. It wraps a `form` and provides form context. * It also handles form submission. * Provides better error handling and form state management and better UX out of the box. */ @@ -147,30 +159,13 @@ export const Form = React.forwardRef(function Form< register: (name, options) => { const registered = register(name, options) - /** - * Maps the value to the event object. - */ - function mapValueOnEvent(value: unknown) { - if (typeof value === 'object' && value != null && 'target' in value && 'type' in value) { - return value - } else { - return { target: { value } } - } - } - - const onChange: types.UseFormRegisterReturn['onChange'] = (value) => - registered.onChange(mapValueOnEvent(value)) - - const onBlur: types.UseFormRegisterReturn['onBlur'] = (value) => - registered.onBlur(mapValueOnEvent(value)) - const result: types.UseFormRegisterReturn = { ...registered, - ...(registered.disabled != null ? { isDisabled: registered.disabled } : {}), - ...(registered.required != null ? { isRequired: registered.required } : {}), - isInvalid: !!formState.errors[name], - onChange, - onBlur, + isDisabled: registered.disabled ?? false, + isRequired: registered.required ?? false, + isInvalid: Boolean(formState.errors[name]), + onChange: (value) => registered.onChange(mapValueOnEvent(value)), + onBlur: (value) => registered.onBlur(mapValueOnEvent(value)), } return result @@ -226,26 +221,29 @@ export const Form = React.forwardRef(function Form< ) -}) as unknown as (< - Schema extends components.TSchema, - TFieldValues extends components.FieldValues, - TTransformedValues extends components.FieldValues | undefined = undefined, ->( - props: React.RefAttributes & - types.FormProps, - // eslint-disable-next-line no-restricted-syntax -) => React.JSX.Element) & { - /* eslint-disable @typescript-eslint/naming-convention */ - schema: typeof components.schema - useForm: typeof components.useForm - useField: typeof components.useField - Submit: typeof components.Submit - Reset: typeof components.Reset - Field: typeof components.Field - FormError: typeof components.FormError - useFormSchema: typeof components.useFormSchema - /* eslint-enable @typescript-eslint/naming-convention */ -} +}) as unknown as Mutable< + Pick< + typeof components, + | 'FIELD_STYLES' + | 'Field' + | 'FormError' + | 'Reset' + | 'schema' + | 'Submit' + | 'useField' + | 'useForm' + | 'useFormSchema' + > +> & + (< + Schema extends components.TSchema, + TFieldValues extends components.FieldValues, + TTransformedValues extends components.FieldValues | undefined = undefined, + >( + props: React.RefAttributes & + types.FormProps, + // eslint-disable-next-line no-restricted-syntax + ) => React.JSX.Element) Form.schema = components.schema Form.useForm = components.useForm @@ -255,3 +253,4 @@ Form.Submit = components.Submit Form.Reset = components.Reset Form.FormError = components.FormError Form.Field = components.Field +Form.FIELD_STYLES = components.FIELD_STYLES diff --git a/app/dashboard/src/components/AriaComponents/Form/components/Field.tsx b/app/dashboard/src/components/AriaComponents/Form/components/Field.tsx index a046589dc8bb..31429f77f7d5 100644 --- a/app/dashboard/src/components/AriaComponents/Form/components/Field.tsx +++ b/app/dashboard/src/components/AriaComponents/Form/components/Field.tsx @@ -8,8 +8,8 @@ import * as React from 'react' import * as aria from '#/components/aria' -import * as twv from '#/utilities/tailwindVariants' - +import { useText } from '#/providers/TextProvider' +import { type ExtractFunction, tv, type VariantProps } from '#/utilities/tailwindVariants' import * as text from '../../Text' import type * as types from './types' import * as formContext from './useFormContext' @@ -17,9 +17,8 @@ import * as formContext from './useFormContext' /** * Props for Field component */ -export interface FieldComponentProps - extends twv.VariantProps, - types.FieldProps { +export interface FieldComponentProps extends VariantProps, types.FieldProps { + readonly 'data-testid'?: string | undefined readonly name: string // eslint-disable-next-line @typescript-eslint/no-explicit-any readonly form?: types.FormInstance @@ -27,6 +26,7 @@ export interface FieldComponentProps readonly className?: string | undefined readonly children?: React.ReactNode | ((props: FieldChildrenRenderProps) => React.ReactNode) readonly style?: React.CSSProperties | undefined + readonly variants?: ExtractFunction | undefined } /** @@ -40,11 +40,12 @@ export interface FieldChildrenRenderProps { readonly error?: string | undefined } -export const FIELD_STYLES = twv.tv({ +export const FIELD_STYLES = tv({ base: 'flex flex-col gap-0.5 items-start', variants: { fullWidth: { true: 'w-full' }, isInvalid: { true: { label: 'text-danger' } }, + isHidden: { true: { base: 'hidden' } }, }, slots: { labelContainer: 'contents', @@ -53,7 +54,9 @@ export const FIELD_STYLES = twv.tv({ description: text.TEXT_STYLE({ variant: 'body', color: 'disabled' }), error: text.TEXT_STYLE({ variant: 'body', color: 'danger' }), }, - defaultVariants: { fullWidth: true }, + defaultVariants: { + fullWidth: true, + }, }) /** @@ -73,8 +76,11 @@ export const Field = React.forwardRef(function Field( fullWidth, error, name, + isHidden, isRequired = false, + variants, } = props + const { getText } = useText() const fieldState = form.getFieldState(name) @@ -84,9 +90,10 @@ export const Field = React.forwardRef(function Field( const invalid = isInvalid === true || fieldState.invalid - const classes = FIELD_STYLES({ + const classes = (variants ?? FIELD_STYLES)({ fullWidth, isInvalid: invalid, + isHidden, }) const hasError = (error ?? fieldState.error?.message) != null @@ -95,6 +102,7 @@ export const Field = React.forwardRef(function Field(
+ {error ?? fieldState.error?.message} )} diff --git a/app/dashboard/src/components/AriaComponents/Form/components/FormError.tsx b/app/dashboard/src/components/AriaComponents/Form/components/FormError.tsx index afc3e19d166b..65e73671342c 100644 --- a/app/dashboard/src/components/AriaComponents/Form/components/FormError.tsx +++ b/app/dashboard/src/components/AriaComponents/Form/components/FormError.tsx @@ -65,7 +65,12 @@ export function FormError(props: FormErrorProps) { const submitErrorAlert = errorMessage != null ? - + {errorMessage} diff --git a/app/dashboard/src/components/AriaComponents/Form/components/types.ts b/app/dashboard/src/components/AriaComponents/Form/components/types.ts index 692debeb3eff..46e3ac542d86 100644 --- a/app/dashboard/src/components/AriaComponents/Form/components/types.ts +++ b/app/dashboard/src/components/AriaComponents/Form/components/types.ts @@ -14,7 +14,7 @@ import type * as schemaModule from './schema' */ // eslint-disable-next-line no-restricted-syntax export type FieldValues = - Schema extends z.AnyZodObject ? z.infer : reactHookForm.FieldValues + Schema extends TSchema ? z.infer : reactHookForm.FieldValues /** * Field path type. @@ -28,7 +28,7 @@ export type FieldPath< /** * Schema type */ -export type TSchema = z.AnyZodObject +export type TSchema = z.AnyZodObject | z.ZodEffects /** * Props for the useForm hook. diff --git a/app/dashboard/src/components/AriaComponents/Inputs/Input/Input.tsx b/app/dashboard/src/components/AriaComponents/Inputs/Input/Input.tsx index 2524b7397980..be701975b4c5 100644 --- a/app/dashboard/src/components/AriaComponents/Inputs/Input/Input.tsx +++ b/app/dashboard/src/components/AriaComponents/Inputs/Input/Input.tsx @@ -12,8 +12,10 @@ import * as ariaComponents from '#/components/AriaComponents' import * as mergeRefs from '#/utilities/mergeRefs' +import SvgMask from '#/components/SvgMask' +import type { ExtractFunction } from '#/utilities/tailwindVariants' import { omit } from 'enso-common/src/utilities/data/object' -import * as variants from '../variants' +import { INPUT_STYLES } from '../variants' /** * Props for the Input component. @@ -31,13 +33,18 @@ export interface InputProps< TTransformedValues >, ariaComponents.FieldProps, - Omit, 'disabled' | 'invalid'> { + Omit, 'disabled' | 'invalid'> { + readonly 'data-testid'?: string | undefined readonly className?: string readonly style?: React.CSSProperties readonly inputRef?: React.Ref readonly addonStart?: React.ReactNode readonly addonEnd?: React.ReactNode readonly placeholder?: string + /** The icon to display in the input. */ + readonly icon?: React.ReactElement | string | null + readonly variants?: ExtractFunction | undefined + readonly fieldVariants?: ariaComponents.FieldComponentProps['variants'] } /** @@ -68,7 +75,11 @@ export const Input = React.forwardRef(function Input< isRequired = false, min, max, + icon, type = 'text', + variant, + variants, + fieldVariants, ...inputProps } = props @@ -81,7 +92,8 @@ export const Input = React.forwardRef(function Input< defaultValue, }) - const classes = variants.INPUT_STYLES({ + const classes = (variants ?? INPUT_STYLES)({ + variant, size, rounded, invalid: fieldState.invalid, @@ -116,9 +128,11 @@ export const Input = React.forwardRef(function Input< return (
privateInputRef.current?.focus({ preventScroll: true })} > -
+
{addonStart != null &&
{addonStart}
} + {icon != null && + (typeof icon === 'string' ? : icon)} - ()( - { className: classes.textArea(), type, name, min, max, isRequired, isDisabled }, - inputProps, - omit(field, 'required', 'disabled'), - )} - /> +
+ ()( + { className: classes.textArea(), type, name, min, max, isRequired, isDisabled }, + inputProps, + omit(field, 'required', 'disabled'), + )} + /> +
{addonEnd != null &&
{addonEnd}
}
diff --git a/app/dashboard/src/components/AriaComponents/Inputs/Password/Password.tsx b/app/dashboard/src/components/AriaComponents/Inputs/Password/Password.tsx new file mode 100644 index 000000000000..c0221c76a06b --- /dev/null +++ b/app/dashboard/src/components/AriaComponents/Inputs/Password/Password.tsx @@ -0,0 +1,57 @@ +/** @file A component wrapping {@link Input} with the ability to show and hide password. */ +import { useState } from 'react' +import type { Path } from 'react-hook-form' + +import EyeIcon from '#/assets/eye.svg' +import EyeCrossedIcon from '#/assets/eye_crossed.svg' +import { + Button, + Input, + type FieldValues, + type InputProps, + type TSchema, +} from '#/components/AriaComponents' + +// ================ +// === Password === +// ================ + +/** Props for a {@link Password}. */ +export interface PasswordProps< + Schema extends TSchema, + TFieldValues extends FieldValues, + TFieldName extends Path, + TTransformedValues extends FieldValues | undefined = undefined, +> extends Omit, 'type'> {} + +/** A component wrapping {@link Input} with the ability to show and hide password. */ +export function Password< + Schema extends TSchema, + TFieldValues extends FieldValues, + TFieldName extends Path, + TTransformedValues extends FieldValues | undefined = undefined, +>(props: PasswordProps) { + const [showPassword, setShowPassword] = useState(false) + + return ( + + {props.addonEnd} + + +
-
-
{ - event.preventDefault() - setIsSubmitting(true) - await signInWithPassword(email, password) - shouldReportValidityRef.current = true - setIsSubmitting(false) - }} + + + z.object({ + email: z + .string() + .min(1, getText('arbitraryFieldRequired')) + .email(getText('invalidEmailValidationError')), + password: passwordSchema(getText), + }) + } + gap="medium" + onSubmit={({ email, password }) => signInWithPassword(email, password)} > { + setEmailInput(event.currentTarget.value) + }} /> -
- + - + +
- - + + {getText('login')} + + + + ) } diff --git a/app/dashboard/src/pages/authentication/Registration.tsx b/app/dashboard/src/pages/authentication/Registration.tsx index cc9a0207f969..ff1ae5091b54 100644 --- a/app/dashboard/src/pages/authentication/Registration.tsx +++ b/app/dashboard/src/pages/authentication/Registration.tsx @@ -1,29 +1,24 @@ /** @file Registration container responsible for rendering and interactions in sign up flow. */ -import * as React from 'react' +import { useEffect, useState } from 'react' +import { useLocation } from 'react-router-dom' -import * as router from 'react-router-dom' +import * as z from 'zod' +import { LOGIN_PATH } from '#/appUtils' import AtIcon from '#/assets/at.svg' import CreateAccountIcon from '#/assets/create_account.svg' import GoBackIcon from '#/assets/go_back.svg' import LockIcon from '#/assets/lock.svg' - -import * as appUtils from '#/appUtils' - -import * as authProvider from '#/providers/AuthProvider' -import * as backendProvider from '#/providers/BackendProvider' -import * as localStorageProvider from '#/providers/LocalStorageProvider' -import * as textProvider from '#/providers/TextProvider' - -import AuthenticationPage from '#/pages/authentication/AuthenticationPage' - -import Input from '#/components/Input' +import { Form, Input, Password } from '#/components/AriaComponents' import Link from '#/components/Link' -import SubmitButton from '#/components/SubmitButton' - +import AuthenticationPage from '#/pages/authentication/AuthenticationPage' +import { passwordWithPatternSchema } from '#/pages/authentication/schemas' +import { useAuth } from '#/providers/AuthProvider' +import { useLocalBackend } from '#/providers/BackendProvider' +import { useLocalStorage } from '#/providers/LocalStorageProvider' +import { type GetText, useText } from '#/providers/TextProvider' import LocalStorage from '#/utilities/LocalStorage' -import * as string from '#/utilities/string' -import * as validation from '#/utilities/validation' +import { PASSWORD_REGEX } from '#/utilities/validation' // ============================ // === Global configuration === @@ -38,33 +33,48 @@ declare module '#/utilities/LocalStorage' { LocalStorage.registerKey('loginRedirect', { isUserSpecific: true, - tryParse: (value) => (typeof value === 'string' ? value : null), + schema: z.string(), }) +/** Create the schema for this form. */ +function createRegistrationFormSchema(getText: GetText) { + return z + .object({ + email: z.string().email(getText('invalidEmailValidationError')), + password: passwordWithPatternSchema(getText), + confirmPassword: z.string(), + }) + .superRefine((object, context) => { + if (PASSWORD_REGEX.test(object.password) && object.password !== object.confirmPassword) { + context.addIssue({ + path: ['confirmPassword'], + code: 'custom', + message: getText('passwordMismatchError'), + }) + } + }) +} + // ==================== // === Registration === // ==================== /** A form for users to register an account. */ export default function Registration() { - const auth = authProvider.useAuth() - const location = router.useLocation() - const { localStorage } = localStorageProvider.useLocalStorage() - const { getText } = textProvider.useText() - const localBackend = backendProvider.useLocalBackend() + const { signUp } = useAuth() + const location = useLocation() + const { localStorage } = useLocalStorage() + const { getText } = useText() + const localBackend = useLocalBackend() const supportsOffline = localBackend != null const query = new URLSearchParams(location.search) const initialEmail = query.get('email') const organizationId = query.get('organization_id') const redirectTo = query.get('redirect_to') + const [emailInput, setEmailInput] = useState(initialEmail ?? '') - const [email, setEmail] = React.useState(initialEmail ?? '') - const [password, setPassword] = React.useState('') - const [confirmPassword, setConfirmPassword] = React.useState('') - const [isSubmitting, setIsSubmitting] = React.useState(false) - - React.useEffect(() => { + useEffect(() => { if (redirectTo != null) { localStorage.set('loginRedirect', redirectTo) } else { @@ -74,56 +84,58 @@ export default function Registration() { return ( + } - onSubmit={async (event) => { - event.preventDefault() - setIsSubmitting(true) - await auth.signUp(email, password, organizationId) - setIsSubmitting(false) - }} + onSubmit={({ email, password }) => signUp(email, password, organizationId)} > { + setEmailInput(event.currentTarget.value) + }} /> - - - + + + {getText('register')} + + + ) } diff --git a/app/dashboard/src/pages/authentication/ResetPassword.tsx b/app/dashboard/src/pages/authentication/ResetPassword.tsx index 7bf3a2ae8a12..41a9a927525f 100644 --- a/app/dashboard/src/pages/authentication/ResetPassword.tsx +++ b/app/dashboard/src/pages/authentication/ResetPassword.tsx @@ -1,30 +1,47 @@ /** @file Container responsible for rendering and interactions in second half of forgot password * flow. */ import * as React from 'react' - import * as router from 'react-router-dom' +import isEmail from 'validator/lib/isEmail' +import * as z from 'zod' + +import { LOGIN_PATH } from '#/appUtils' import ArrowRightIcon from '#/assets/arrow_right.svg' import GoBackIcon from '#/assets/go_back.svg' import LockIcon from '#/assets/lock.svg' - -import * as appUtils from '#/appUtils' - -import * as toastAndLogHooks from '#/hooks/toastAndLogHooks' - -import * as authProvider from '#/providers/AuthProvider' -import * as backendProvider from '#/providers/BackendProvider' -import * as textProvider from '#/providers/TextProvider' - -import AuthenticationPage from '#/pages/authentication/AuthenticationPage' - -import * as aria from '#/components/aria' -import Input from '#/components/Input' +import { Form, Input, Password } from '#/components/AriaComponents' import Link from '#/components/Link' -import SubmitButton from '#/components/SubmitButton' +import { useToastAndLog } from '#/hooks/toastAndLogHooks' +import AuthenticationPage from '#/pages/authentication/AuthenticationPage' +import { passwordWithPatternSchema } from '#/pages/authentication/schemas' +import { useAuth } from '#/providers/AuthProvider' +import { useLocalBackend } from '#/providers/BackendProvider' +import { type GetText, useText } from '#/providers/TextProvider' +import { PASSWORD_REGEX } from '#/utilities/validation' -import * as string from '#/utilities/string' -import * as validation from '#/utilities/validation' +/** Create the schema for this form. */ +function createResetPasswordFormSchema(getText: GetText) { + return z + .object({ + email: z.string().refine(isEmail, getText('invalidEmailValidationError')), + verificationCode: z.string(), + newPassword: passwordWithPatternSchema(getText), + confirmNewPassword: z.string(), + }) + .superRefine((object, context) => { + if ( + PASSWORD_REGEX.test(object.newPassword) && + object.newPassword !== object.confirmNewPassword + ) { + context.addIssue({ + path: ['confirmNewPassword'], + code: 'custom', + message: getText('passwordMismatchError'), + }) + } + }) +} // ===================== // === ResetPassword === @@ -32,97 +49,91 @@ import * as validation from '#/utilities/validation' /** A form for users to reset their password. */ export default function ResetPassword() { - const { resetPassword } = authProvider.useAuth() - const { getText } = textProvider.useText() + const { resetPassword } = useAuth() + const { getText } = useText() const location = router.useLocation() const navigate = router.useNavigate() - const toastAndLog = toastAndLogHooks.useToastAndLog() - const localBackend = backendProvider.useLocalBackend() + const toastAndLog = useToastAndLog() + const localBackend = useLocalBackend() const supportsOffline = localBackend != null const query = new URLSearchParams(location.search) - const email = query.get('email') - const verificationCode = query.get('verification_code') - - const [newPassword, setNewPassword] = React.useState('') - const [newPasswordConfirm, setNewPasswordConfirm] = React.useState('') + const defaultEmail = query.get('email') + const defaultVerificationCode = query.get('verification_code') React.useEffect(() => { - if (email == null) { + if (defaultEmail == null) { toastAndLog('missingEmailError') - navigate(appUtils.LOGIN_PATH) - } else if (verificationCode == null) { + navigate(LOGIN_PATH) + } else if (defaultVerificationCode == null) { toastAndLog('missingVerificationCodeError') - navigate(appUtils.LOGIN_PATH) + navigate(LOGIN_PATH) } - }, [email, navigate, verificationCode, getText, toastAndLog]) - - const doSubmit = () => { - if (newPassword !== newPasswordConfirm) { - toastAndLog('passwordMismatchError') - return Promise.resolve() - } else { - // These should never be nullish, as the effect should immediately navigate away. - return resetPassword(email ?? '', verificationCode ?? '', newPassword) - } - } + }, [defaultEmail, navigate, defaultVerificationCode, getText, toastAndLog]) return ( } - onSubmit={async (event) => { - event.preventDefault() - await doSubmit() - }} + schema={createResetPasswordFormSchema(getText)} + footer={ + + } + onSubmit={({ email, verificationCode, newPassword }) => + resetPassword(email, verificationCode, newPassword) + } > - ) } diff --git a/app/dashboard/src/pages/authentication/schemas.ts b/app/dashboard/src/pages/authentication/schemas.ts new file mode 100644 index 000000000000..3e806e9f9d30 --- /dev/null +++ b/app/dashboard/src/pages/authentication/schemas.ts @@ -0,0 +1,32 @@ +/** + * @file + * + * This file contains common schemas for authentication. + */ +import type { GetText } from '#/providers/TextProvider' +import { PASSWORD_REGEX } from '#/utilities/validation' +import { z } from 'zod' + +/** + * A schema for validating passwords. + */ +export function passwordSchema(getText: GetText) { + return ( + z + .string() + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + .min(6, { message: getText('passwordLengthError') }) + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + .max(256, { message: getText('passwordLengthError') }) + ) +} + +/** + * A schema for validating passwords that match the required pattern. + */ +export function passwordWithPatternSchema(getText: GetText) { + return passwordSchema(getText).refine( + (password) => PASSWORD_REGEX.test(password), + getText('passwordValidationError'), + ) +} diff --git a/app/dashboard/src/providers/AuthProvider.tsx b/app/dashboard/src/providers/AuthProvider.tsx index 65d841261763..9fe08c461e59 100644 --- a/app/dashboard/src/providers/AuthProvider.tsx +++ b/app/dashboard/src/providers/AuthProvider.tsx @@ -86,20 +86,16 @@ export type UserSession = FullUserSession | PartialUserSession * * See `Cognito` for details on each of the authentication functions. */ interface AuthContextType { - readonly signUp: ( - email: string, - password: string, - organizationId: string | null, - ) => Promise + readonly signUp: (email: string, password: string, organizationId: string | null) => Promise readonly authQueryKey: reactQuery.QueryKey readonly confirmSignUp: (email: string, code: string) => Promise readonly setUsername: (username: string) => Promise readonly signInWithGoogle: () => Promise readonly signInWithGitHub: () => Promise - readonly signInWithPassword: (email: string, password: string) => Promise - readonly forgotPassword: (email: string) => Promise + readonly signInWithPassword: (email: string, password: string) => Promise + readonly forgotPassword: (email: string) => Promise readonly changePassword: (oldPassword: string, newPassword: string) => Promise - readonly resetPassword: (email: string, code: string, password: string) => Promise + readonly resetPassword: (email: string, code: string, password: string) => Promise readonly signOut: () => Promise /** * @deprecated Never use this function. Prefer particular functions like `setUsername` or `deleteUser`. @@ -293,18 +289,15 @@ export default function AuthProvider(props: AuthProviderProps) { const signUp = useEventCallback( async (username: string, password: string, organizationId: string | null) => { - if (cognito == null) { - return false - } else { + if (cognito != null) { gtagEvent('cloud_sign_up') const result = await cognito.signUp(username, password, organizationId) if (result.ok) { - toastSuccess(getText('signUpSuccess')) navigate(appUtils.LOGIN_PATH) } else { - toastError(result.val.message) + // eslint-disable-next-line no-restricted-syntax + throw new Error(result.val.message) } - return result.ok } }, ) @@ -337,23 +330,16 @@ export default function AuthProvider(props: AuthProviderProps) { }) const signInWithPassword = useEventCallback(async (email: string, password: string) => { - if (cognito == null) { - return false - } else { + if (cognito != null) { gtagEvent('cloud_sign_in', { provider: 'Email' }) const result = await cognito.signInWithPassword(email, password) if (result.ok) { - toastSuccess(getText('signInWithPasswordSuccess')) void queryClient.invalidateQueries({ queryKey: sessionQueryKey }) navigate(appUtils.DASHBOARD_PATH) + return } else { - if (result.val.type === cognitoModule.CognitoErrorType.userNotFound) { - // It may not be safe to pass the user's password in the URL. - navigate(`${appUtils.REGISTRATION_PATH}?${new URLSearchParams({ email }).toString()}`) - } - toastError(result.val.message) + throw new Error(result.val.message) } - return result.ok } }) @@ -422,32 +408,26 @@ export default function AuthProvider(props: AuthProviderProps) { }) const forgotPassword = useEventCallback(async (email: string) => { - if (cognito == null) { - return false - } else { + if (cognito != null) { const result = await cognito.forgotPassword(email) if (result.ok) { - toastSuccess(getText('forgotPasswordSuccess')) navigate(appUtils.LOGIN_PATH) + return } else { - toastError(result.val.message) + throw new Error(result.val.message) } - return result.ok } }) const resetPassword = useEventCallback(async (email: string, code: string, password: string) => { - if (cognito == null) { - return false - } else { + if (cognito != null) { const result = await cognito.forgotPasswordSubmit(email, code, password) if (result.ok) { - toastSuccess(getText('resetPasswordSuccess')) navigate(appUtils.LOGIN_PATH) + return } else { - toastError(result.val.message) + throw new Error(result.val.message) } - return result.ok } }) @@ -521,7 +501,7 @@ export default function AuthProvider(props: AuthProviderProps) { }, [userData, onAuthenticated]) const value: AuthContextType = { - signUp: withLoadingToast(signUp), + signUp, confirmSignUp: withLoadingToast(confirmSignUp), setUsername, isUserMarkedForDeletion, @@ -557,9 +537,9 @@ export default function AuthProvider(props: AuthProviderProps) { ) } }), - signInWithPassword: signInWithPassword, - forgotPassword: withLoadingToast(forgotPassword), - resetPassword: withLoadingToast(resetPassword), + signInWithPassword, + forgotPassword, + resetPassword, changePassword: withLoadingToast(changePassword), refetchSession: usersMeQuery.refetch, session: userData, diff --git a/app/dashboard/src/tailwind.css b/app/dashboard/src/tailwind.css index bae6a2d3dbb7..61ec600b09fe 100644 --- a/app/dashboard/src/tailwind.css +++ b/app/dashboard/src/tailwind.css @@ -290,7 +290,7 @@ /* The gap between the header and contents of a section in a settings page. */ --settings-section-header-gap: 0.625rem; /* The gap between the label and value of a settings entry. */ - --settings-entry-gap: 1.1875rem; + --settings-entry-gap: 1.25rem; --settings-sidebar-width: 12.875rem; /* The gap between each section in the settings sidebar. */ --settings-sidebar-gap: 1rem; diff --git a/app/dashboard/src/utilities/LocalStorage.ts b/app/dashboard/src/utilities/LocalStorage.ts index a3a87c337607..d41ba16a4ee0 100644 --- a/app/dashboard/src/utilities/LocalStorage.ts +++ b/app/dashboard/src/utilities/LocalStorage.ts @@ -5,33 +5,12 @@ import * as common from 'enso-common' import * as object from '#/utilities/object' -// ==================== -// === LocalStorage === -// ==================== +// =============================== +// === LocalStorageKeyMetadata === +// =============================== /** Metadata describing runtime behavior associated with a {@link LocalStorageKey}. */ -export type LocalStorageKeyMetadata = - | LocalStorageKeyMetadataWithParseFunction - | LocalStorageKeyMetadataWithSchema - -/** - * A {@link LocalStorageKeyMetadata} with a `tryParse` function. - */ -interface LocalStorageKeyMetadataWithParseFunction { - readonly isUserSpecific?: boolean - /** - * A function to parse a value from the stored data. - * If this is provided, the value will be parsed using this function. - * If this is not provided, the value will be parsed using the `schema`. - */ - readonly tryParse: (value: unknown) => LocalStorageData[K] | null - readonly schema?: never -} - -/** - * A {@link LocalStorageKeyMetadata} with a `schema`. - */ -interface LocalStorageKeyMetadataWithSchema { +export interface LocalStorageKeyMetadata { readonly isUserSpecific?: boolean /** * The Zod schema to validate the value. @@ -39,16 +18,36 @@ interface LocalStorageKeyMetadataWithSchema { * If this is not provided, the value will be parsed using the `tryParse` function. */ readonly schema: z.ZodType - readonly tryParse?: never } +// ======================== +// === LocalStorageData === +// ======================== + /** The data that can be stored in a {@link LocalStorage}. * Declaration merge into this interface to add a new key. */ export interface LocalStorageData {} +// ======================= +// === LocalStorageKey === +// ======================= + /** All possible keys of a {@link LocalStorage}. */ type LocalStorageKey = keyof LocalStorageData +// ================================= +// === LocalStorageMutateOptions === +// ================================= + +/** Options for methods that mutate `localStorage` state (set, delete, and save). */ +export interface LocalStorageMutateOptions { + readonly triggerRerender?: boolean +} + +// ==================== +// === LocalStorage === +// ==================== + /** A LocalStorage data manager. */ export default class LocalStorage { // This is UNSAFE. It is assumed that `LocalStorage.register` is always called @@ -68,10 +67,7 @@ export default class LocalStorage { // This is SAFE, as it is guarded by the `key in savedValues` check. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-restricted-syntax, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment const savedValue = (savedValues as any)[key] - const value = - metadata.schema ? - metadata.schema.safeParse(savedValue).data - : metadata.tryParse(savedValue) + const value = metadata.schema.safeParse(savedValue).data if (value != null) { newValues[key] = value } @@ -95,18 +91,22 @@ export default class LocalStorage { } /** Write an entry to the stored data, and save. */ - set(key: K, value: LocalStorageData[K]) { + set( + key: K, + value: LocalStorageData[K], + options?: LocalStorageMutateOptions, + ) { this.values[key] = value - this.save() + this.save(options) } /** Delete an entry from the stored data, and save. */ - delete(key: K) { + delete(key: K, options?: LocalStorageMutateOptions) { const oldValue = this.values[key] // The key being deleted is one of a statically known set of keys. // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.values[key] - this.save() + this.save(options) return oldValue } @@ -120,8 +120,11 @@ export default class LocalStorage { } /** Save the current value of the stored data.. */ - protected save() { + protected save(options: LocalStorageMutateOptions = {}) { + const { triggerRerender = false } = options localStorage.setItem(this.localStorageKey, JSON.stringify(this.values)) - this.triggerRerender() + if (triggerRerender) { + this.triggerRerender() + } } } diff --git a/app/dashboard/src/utilities/tailwindVariants.ts b/app/dashboard/src/utilities/tailwindVariants.ts index 8215c0910f7f..c3aa3736c12b 100644 --- a/app/dashboard/src/utilities/tailwindVariants.ts +++ b/app/dashboard/src/utilities/tailwindVariants.ts @@ -1,7 +1,7 @@ /** @file `tailwind-variants` with a custom configuration. */ -import * as tailwindVariants from 'tailwind-variants' +import { createTV } from 'tailwind-variants' -import * as tailwindMerge from '#/utilities/tailwindMerge' +import { TAILWIND_MERGE_CONFIG } from '#/utilities/tailwindMerge' export * from 'tailwind-variants' @@ -11,4 +11,11 @@ export * from 'tailwind-variants' // This is a function, even though it does not contain function syntax. // eslint-disable-next-line no-restricted-syntax -export const tv = tailwindVariants.createTV({ twMergeConfig: tailwindMerge.TAILWIND_MERGE_CONFIG }) +export const tv = createTV({ twMergeConfig: TAILWIND_MERGE_CONFIG }) + +/** Extract function signatures from a type. */ +export type ExtractFunction = + T extends (...args: infer Args) => infer Ret ? (...args: Args) => Ret : never + +/** A `tailwind-variants` type, without restrictions onn the `extends` key. */ +export type TVWithoutExtends = ExtractFunction & Omit diff --git a/app/dashboard/tsconfig.json b/app/dashboard/tsconfig.json index a50e793d18f3..3ed52dcedc51 100644 --- a/app/dashboard/tsconfig.json +++ b/app/dashboard/tsconfig.json @@ -5,6 +5,7 @@ "e2e", "../types", "./src/**/*.json", + "./e2e/**/*.json", "../../utils.ts", ".prettierrc.cjs", "*.js", diff --git a/app/eslint.config.mjs b/app/eslint.config.mjs index 8fc75503dda1..9d656a8cf094 100644 --- a/app/eslint.config.mjs +++ b/app/eslint.config.mjs @@ -196,7 +196,7 @@ export default [ eslintJs.configs.recommended, { // Playwright build cache and Vite build directory. - ignores: ['**/.cache/**', '**/playwright-report', '**/dist'], + ignores: ['**/.cache/**', '**/playwright-report', '**/dist', '**/build.mjs'], }, { settings: { @@ -403,6 +403,7 @@ export default [ }, { files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + ignores: ['**/build.mjs'], rules: { '@typescript-eslint/no-var-requires': 'off', // Parameter types must be specified using JSDoc in JS files. diff --git a/app/gui2/README.md b/app/gui2/README.md index 1593e4185f8c..93d293f9e660 100644 --- a/app/gui2/README.md +++ b/app/gui2/README.md @@ -30,44 +30,44 @@ pnpm install ### Compile and Hot-Reload for Development ```sh -npm run dev +pnpm -w dev:gui ``` ### Type-Check, Compile and Minify for Production ```sh -npm run build +pnpm build ``` ### Run Unit Tests with [Vitest](https://vitest.dev/) ```sh -npm run test:unit +# Run once +pnpm test:unit +# Run in watch mode +pnpm test-dev:unit ``` ### Run End-to-End Tests with [Playwright](https://playwright.dev) ```sh # Install browsers for the first run -npx playwright install - -# When testing on CI, must build the project first -npm run build +pnpm exec playwright install # Runs the end-to-end tests -npm run test:e2e +pnpm test:e2e # Runs the tests only on Chromium -npm run test:e2e -- --project=chromium +pnpm test:e2e -- --project=chromium # Runs the tests of a specific file -npm run test:e2e -- tests/example.spec.ts -# Runs the tests in debug mode -npm run test:e2e -- --debug +pnpm test:e2e -- tests/example.spec.ts +# Runs the tests in watch mode, open the testing UI +pnpm test-dev:e2e ``` -### Lint with [ESLint](https://eslint.org/) +### Format code with [ESLint](https://eslint.org/) and Prettier ```sh -npm run lint +pnpm format ``` ## Icons license diff --git a/app/gui2/e2e/expressionUpdates.ts b/app/gui2/e2e/expressionUpdates.ts index 540a6e828dcd..5846ef888f14 100644 --- a/app/gui2/e2e/expressionUpdates.ts +++ b/app/gui2/e2e/expressionUpdates.ts @@ -1,5 +1,5 @@ import type { Page } from '@playwright/test' -import type { ExpressionUpdate, MethodCall } from 'shared/languageServerTypes' +import type { ExpressionUpdate, MethodCall } from 'ydoc-shared/languageServerTypes' export type ExpressionLocator = string | { binding: string; expr: string } diff --git a/app/gui2/e2e/docPanel.spec.ts b/app/gui2/e2e/rightPanel.spec.ts similarity index 69% rename from app/gui2/e2e/docPanel.spec.ts rename to app/gui2/e2e/rightPanel.spec.ts index b1ee73b4baba..d8cc94aa4649 100644 --- a/app/gui2/e2e/docPanel.spec.ts +++ b/app/gui2/e2e/rightPanel.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from 'playwright/test' import * as actions from './actions' +import { mockMethodCallInfo } from './expressionUpdates' import { CONTROL_KEY } from './keyboard' import * as locate from './locate' @@ -48,3 +49,23 @@ test('Doc panel focus (regression #10471)', async ({ page }) => { expect(content.includes('The main TEST method')).toBe(true) await expect(locate.rightDock(page)).toContainText('The main TEST method') }) + +test('Component help', async ({ page }) => { + await actions.goToGraph(page, false) + await locate.rightDock(page).getByRole('button', { name: 'Help' }).click() + await expect(locate.rightDock(page)).toHaveText(/Select a single component/) + + await locate.graphNodeByBinding(page, 'final').click() + await expect(locate.rightDock(page)).toHaveText(/No documentation available/) + + await mockMethodCallInfo(page, 'data', { + methodPointer: { + module: 'Standard.Base.Data', + definedOnType: 'Standard.Base.Data', + name: 'read', + }, + notAppliedArguments: [0, 1, 2], + }) + await locate.graphNodeByBinding(page, 'data').click() + await expect(locate.rightDock(page)).toHaveText(/Reads a file into Enso/) +}) diff --git a/app/gui2/e2e/setup.ts b/app/gui2/e2e/setup.ts index a9a5cd83a1b5..72b1b2c2b588 100644 --- a/app/gui2/e2e/setup.ts +++ b/app/gui2/e2e/setup.ts @@ -18,7 +18,7 @@ export default function setup() { id: 'websocket', udp: true, ipv6: true, - port: 30535, + port: 30536, middleware: [], }, }, diff --git a/app/gui2/e2e/widgets.spec.ts b/app/gui2/e2e/widgets.spec.ts index 7f823d4b0e95..b42381857fdf 100644 --- a/app/gui2/e2e/widgets.spec.ts +++ b/app/gui2/e2e/widgets.spec.ts @@ -533,3 +533,65 @@ test('Autoscoped constructors', async ({ page }) => { await expect(groupBy).toBeVisible() await expect(groupBy.locator('.WidgetArgumentName')).toContainText(['column', 'new_name']) }) + +test('Table widget', async ({ page }) => { + await actions.goToGraph(page) + + // Adding `Table.new` component will display the widget + await locate.addNewNodeButton(page).click() + await expect(locate.componentBrowser(page)).toBeVisible() + await page.keyboard.type('Table.new') + // Wait for CB entry to appear; this way we're sure about node name (binding). + await expect(locate.componentBrowserSelectedEntry(page)).toHaveCount(1) + await expect(locate.componentBrowserSelectedEntry(page)).toHaveText('Table.new') + await page.keyboard.press('Enter') + const node = locate.selectedNodes(page) + await expect(node).toHaveCount(1) + await expect(node).toBeVisible() + await mockMethodCallInfo( + page, + { binding: 'table1', expr: 'Table.new' }, + { + methodPointer: { + module: 'Standard.Table.Table', + definedOnType: 'Standard.Table.Table.Table', + name: 'new', + }, + notAppliedArguments: [0], + }, + ) + const widget = node.locator('.WidgetTableEditor') + await expect(widget).toBeVisible() + await expect(widget.locator('.ag-header-cell-text')).toHaveText('New Column') + await expect(widget.locator('.ag-header-cell-text')).toHaveClass(/(?<=^| )virtualColumn(?=$| )/) + // There's one empty cell, allowing creating first row and column + await expect(widget.locator('.ag-cell')).toHaveCount(1) + + // Putting first value + await widget.locator('.ag-cell').dblclick() + await page.keyboard.type('Value') + await page.keyboard.press('Enter') + // There will be new blank column and new blank row allowing adding new columns and rows + // (so 4 cells in total) + await expect(widget.locator('.ag-header-cell-text')).toHaveText(['New Column', 'New Column']) + await expect(widget.locator('.ag-cell')).toHaveText(['Value', '', '', '']) + + // Renaming column + await widget.locator('.ag-header-cell-text').first().dblclick() + await page.keyboard.type('Header') + await page.keyboard.press('Enter') + await expect(widget.locator('.ag-header-cell-text')).toHaveText(['Header', 'New Column']) + + // Switching edit between cells and headers - check we will never edit two things at once. + await expect(widget.locator('.ag-text-field-input')).toHaveCount(0) + await widget.locator('.ag-header-cell-text').first().dblclick() + await expect(widget.locator('.ag-text-field-input')).toHaveCount(1) + await widget.locator('.ag-cell').first().dblclick() + await expect(widget.locator('.ag-text-field-input')).toHaveCount(1) + await widget.locator('.ag-header-cell-text').first().dblclick() + await expect(widget.locator('.ag-text-field-input')).toHaveCount(1) + await widget.locator('.ag-header-cell-text').last().dblclick() + await expect(widget.locator('.ag-text-field-input')).toHaveCount(1) + await page.keyboard.press('Escape') + await expect(widget.locator('.ag-text-field-input')).toHaveCount(0) +}) diff --git a/app/gui2/env.d.ts b/app/gui2/env.d.ts index 0ffbb98be199..321e2e395a7f 100644 --- a/app/gui2/env.d.ts +++ b/app/gui2/env.d.ts @@ -2,7 +2,6 @@ declare const PROJECT_MANAGER_URL: string declare const YDOC_SERVER_URL: string -declare const RUNNING_VITEST: boolean declare const IS_CLOUD_BUILD: boolean interface Document { diff --git a/app/gui2/env.story.d.ts b/app/gui2/env.story.d.ts deleted file mode 100644 index cbd6e23731fb..000000000000 --- a/app/gui2/env.story.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/app/gui2/eslint.config.js b/app/gui2/eslint.config.js index 53f1341c1198..a9ed1aeb7bc1 100644 --- a/app/gui2/eslint.config.js +++ b/app/gui2/eslint.config.js @@ -10,8 +10,6 @@ const DIR_NAME = path.dirname(url.fileURLToPath(import.meta.url)) const conf = [ { ignores: [ - 'rust-ffi/pkg', - 'rust-ffi/node-pkg', 'dist', 'shared/ast/generated', 'templates', @@ -30,14 +28,7 @@ const conf = [ parserOptions: { tsconfigRootDir: DIR_NAME, ecmaVersion: 'latest', - project: [ - './tsconfig.app.json', - './tsconfig.node.json', - './tsconfig.server.json', - './tsconfig.app.vitest.json', - './tsconfig.server.vitest.json', - './tsconfig.story.json', - ], + project: ['./tsconfig.app.json', './tsconfig.node.json', './tsconfig.app.vitest.json'], }, }, rules: { diff --git a/app/gui2/mock/MockFSWrapper.vue b/app/gui2/mock/MockFSWrapper.vue index b3da3db7a14e..3f2949192145 100644 --- a/app/gui2/mock/MockFSWrapper.vue +++ b/app/gui2/mock/MockFSWrapper.vue @@ -2,8 +2,8 @@ import { useProjectStore } from '@/stores/project' import { mockFsDirectoryHandle } from '@/util/convert/fsAccess' import { MockWebSocket, type WebSocketHandler } from '@/util/net' -import { type Path as LSPath } from 'shared/languageServerTypes' import { watchEffect } from 'vue' +import { type Path as LSPath } from 'ydoc-shared/languageServerTypes' import { mockDataWSHandler } from './dataServer' const projectStore = useProjectStore() diff --git a/app/gui2/mock/MockProjectStoreWrapper.vue b/app/gui2/mock/MockProjectStoreWrapper.vue index d44663234c24..cb88726f2145 100644 --- a/app/gui2/mock/MockProjectStoreWrapper.vue +++ b/app/gui2/mock/MockProjectStoreWrapper.vue @@ -5,8 +5,8 @@ import { useProjectStore } from '@/stores/project' import { Ast } from '@/util/ast' -import { SourceDocument } from 'shared/ast/sourceDocument' import { reactive, watch } from 'vue' +import { SourceDocument } from 'ydoc-shared/ast/sourceDocument' const props = defineProps<{ modelValue: string }>() const emit = defineEmits<{ 'update:modelValue': [modelValue: string] }>() diff --git a/app/gui2/mock/dataServer.ts b/app/gui2/mock/dataServer.ts index 822b3afdb9d5..cb67a4788533 100644 --- a/app/gui2/mock/dataServer.ts +++ b/app/gui2/mock/dataServer.ts @@ -27,9 +27,9 @@ import { type AnyOutboundPayload, type Offset, type Table, -} from 'shared/binaryProtocol' -import { LanguageServerErrorCode } from 'shared/languageServerTypes' -import { uuidToBits } from 'shared/uuid' +} from 'ydoc-shared/binaryProtocol' +import { LanguageServerErrorCode } from 'ydoc-shared/languageServerTypes' +import { uuidToBits } from 'ydoc-shared/uuid' const sha3 = createSHA3(224) diff --git a/app/gui2/mock/engine.ts b/app/gui2/mock/engine.ts index 2adc5dc7b0e7..0879aa60f86e 100644 --- a/app/gui2/mock/engine.ts +++ b/app/gui2/mock/engine.ts @@ -1,7 +1,9 @@ import { Pattern } from '@/util/ast/match' import type { MockYdocProviderImpl } from '@/util/crdt' +import type { WebSocketHandler } from '@/util/net' +import type { QualifiedName } from '@/util/qualifiedName' import * as random from 'lib0/random' -import * as Ast from 'shared/ast' +import * as Ast from 'ydoc-shared/ast' import { Builder, EnsoUUID, @@ -9,8 +11,8 @@ import { OutboundPayload, VisualizationContext, VisualizationUpdate, -} from 'shared/binaryProtocol' -import { ErrorCode } from 'shared/languageServer' +} from 'ydoc-shared/binaryProtocol' +import { ErrorCode } from 'ydoc-shared/languageServer' import type { ContextId, ExpressionId, @@ -19,11 +21,10 @@ import type { Uuid, VisualizationConfiguration, response, -} from 'shared/languageServerTypes' -import type { SuggestionEntry } from 'shared/languageServerTypes/suggestions' -import { uuidToBits } from 'shared/uuid' -import type { MockTransportData, WebSocketHandler } from 'src/util/net' -import type { QualifiedName } from 'src/util/qualifiedName' +} from 'ydoc-shared/languageServerTypes' +import type { SuggestionEntry } from 'ydoc-shared/languageServerTypes/suggestions' +import type { MockTransportData } from 'ydoc-shared/util/net' +import { uuidToBits } from 'ydoc-shared/uuid' import * as Y from 'yjs' import { mockFsDirectoryHandle, type FileTree } from '../src/util/convert/fsAccess' import mockDb from '../stories/mockSuggestions.json' assert { type: 'json' } diff --git a/app/gui2/package.json b/app/gui2/package.json index ec9146f96efb..6e4786cd5a09 100644 --- a/app/gui2/package.json +++ b/app/gui2/package.json @@ -8,27 +8,25 @@ "email": "contact@enso.org" }, "scripts": { - "dev": "vite", - "build": "npm --workspace enso-dashboard run compile && run-p typecheck build-only", - "build:cloud": "cross-env CLOUD_BUILD=true npm run build", + "dev": "echo DEPRECATED! Use `pnpm -w dev:gui` instead.", + "dev:vite": "vite", + "build": "corepack pnpm -r --filter enso-dashboard run compile && corepack pnpm run build:vite", + "build-cloud": "cross-env CLOUD_BUILD=true corepack pnpm run build", "preview": "vite preview", - "test": "vitest run && playwright test", - "test:unit": "vitest", + "test": "corepack pnpm run /^^^^test:.*/", + "test:unit": "vitest run", + "test-dev:unit": "vitest", "test:e2e": "playwright test", + "test-dev:e2e": "playwright test --ui", "story:dev": "histoire dev", "story:build": "histoire build", "story:preview": "histoire preview", - "build-only": "vite build", - "build-ydoc-server-polyglot": "vite build --config vite.ydoc-server-polyglot.config.ts", - "compile-server": "tsc -p tsconfig.server.json", - "typecheck": "vue-tsc --noEmit -p tsconfig.app.json --composite false", + "build:vite": "vite build", + "typecheck": "vue-tsc --noEmit -p tsconfig.app.json", "lint": "eslint .", "format": "prettier --version && prettier --write src/ && eslint . --fix", - "clean-old-generated-directory": "rimraf src/generated", "build-rust-ffi": "wasm-pack build ./rust-ffi --release --target web && wasm-pack build ./rust-ffi --out-dir node-pkg --target nodejs", - "generate-ast-schema": "cargo run -p enso-parser-schema > shared/ast/generated/ast-schema.json", - "generate-ast-types": "tsx ./parser-codegen/index.ts shared/ast/generated/ast-schema.json shared/ast/generated/ast.ts", - "preinstall": "npm run clean-old-generated-directory && npm run build-rust-ffi && npm run generate-ast-schema && npm run generate-ast-types && npm run generate-metadata && npm run download-fonts", + "preinstall": "corepack pnpm run generate-metadata && corepack pnpm run download-fonts", "postinstall": "playwright install", "generate-metadata": "node scripts/generateIconMetadata.js", "download-fonts": "node scripts/downloadFonts.js" @@ -62,17 +60,16 @@ "@lexical/utils": "^0.16.0", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.1.6", - "@noble/hashes": "^1.3.2", - "@open-rpc/client-js": "^1.8.1", + "@noble/hashes": "^1.4.0", "@tanstack/vue-query": ">= 5.45.0 < 5.46.0", "@vueuse/core": "^10.4.1", "ag-grid-community": "^30.2.1", "ag-grid-enterprise": "^30.2.1", + "ag-grid-vue3": "^30.2.1", "codemirror": "^6.0.1", "culori": "^3.2.0", "enso-dashboard": "workspace:*", "events": "^3.3.0", - "fast-diff": "^1.3.0", "hash-sum": "^2.0.0", "install": "^0.13.0", "isomorphic-ws": "^5.0.0", @@ -80,23 +77,23 @@ "lib0": "^0.2.85", "magic-string": "^0.30.3", "murmurhash": "^2.0.1", - "partysocket": "^1.0.1", "postcss-inline-svg": "^6.0.0", "postcss-nesting": "^12.0.1", "react-toastify": "^9.1.3", - "rimraf": "^5.0.5", "sucrase": "^3.34.0", "veaury": "^2.3.18", "vue": "^3.4.19", - "ws": "^8.13.0", + "vue-component-type-helpers": "^2.0.29", "y-codemirror.next": "^0.3.2", "y-protocols": "^1.0.5", "y-textarea": "^1.0.0", "y-websocket": "^1.5.0", + "ydoc-shared": "workspace:*", "yjs": "^13.6.7", - "zod": "^3.22.4" + "zod": "^3.23.8" }, "devDependencies": { + "@codemirror/theme-one-dark": "^6.1.2", "@danmarshall/deckgl-typings": "^4.9.28", "@eslint/eslintrc": "^3.0.2", "@eslint/js": "^8.57.0", @@ -104,7 +101,7 @@ "@open-rpc/server-js": "^1.9.4", "@playwright/test": "^1.40.0", "@rushstack/eslint-patch": "^1.3.2", - "@tsconfig/node18": "^18.2.0", + "@tsconfig/node20": "^20.1.4", "@types/css.escape": "^1.5.2", "@types/culori": "^2.0.1", "@types/d3": "^7.4.0", @@ -124,7 +121,6 @@ "@vue/eslint-config-typescript": "^13.0.0", "@vue/test-utils": "^2.4.6", "@vue/tsconfig": "^0.5.1", - "change-case": "^4.1.2", "cross-env": "^7.0.3", "css.escape": "^1.5.1", "d3": "^7.4.0", @@ -132,7 +128,7 @@ "eslint": "^8.49.0", "eslint-plugin-vue": "^9.22.0", "floating-vue": "^2.0.0-beta.24", - "hash-wasm": "^4.10.0", + "hash-wasm": "^4.11.0", "histoire": "^0.17.2", "jsdom": "^24.1.0", "playwright": "^1.39.0", @@ -150,9 +146,11 @@ "unbzip2-stream": "^1.4.3", "vite": "^5.3.5", "vite-plugin-vue-devtools": "7.3.7", + "vite-plugin-wasm": "^3.3.0", "vitest": "^1.3.1", "vue-react-wrapper": "^0.3.1", "vue-tsc": "^2.0.24", - "yaml": "^2.4.5" + "yaml": "^2.4.5", + "ydoc-server": "workspace:*" } } diff --git a/app/gui2/parser-codegen/index.ts b/app/gui2/parser-codegen/index.ts deleted file mode 100644 index ab06131db292..000000000000 --- a/app/gui2/parser-codegen/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as fs from 'node:fs' -import * as process from 'node:process' -import * as codegen from './codegen.js' -import * as Schema from './schema.js' - -const schemaPath = process.argv[2] -const outputPath = process.argv[3] - -if (!schemaPath || !outputPath) { - console.error('Usage: parser-codegen ') - process.exit(1) -} - -console.log(`Generating ${outputPath} from ${schemaPath}.`) -const schema: Schema.Schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8')) -const code = codegen.implement(schema) -fs.writeFileSync(outputPath, code) diff --git a/app/gui2/playwright.config.ts b/app/gui2/playwright.config.ts index 475883126712..ba778fe6eff0 100644 --- a/app/gui2/playwright.config.ts +++ b/app/gui2/playwright.config.ts @@ -114,8 +114,8 @@ export default defineConfig({ }, command: process.env.CI || process.env.PROD ? - `npx vite build && npx vite preview --port ${PORT} --strictPort` - : `npx vite dev --port ${PORT}`, + `corepack pnpm build && corepack pnpm exec vite preview --port ${PORT} --strictPort` + : `corepack pnpm exec vite dev --port ${PORT}`, // Build from scratch apparently can take a while on CI machines. timeout: 120 * 1000, port: PORT, diff --git a/app/gui2/shared/ast/ffi.ts b/app/gui2/shared/ast/ffi.ts deleted file mode 100644 index d0903325f9f4..000000000000 --- a/app/gui2/shared/ast/ffi.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file Provides the Rust ffi interface. The interface should be kept in sync with polyglot ffi inteface {@link module:ffiPolyglot}. - * - * @module ffi - */ - -import { createXXHash128 } from 'hash-wasm' -import type { IDataType } from 'hash-wasm/dist/lib/util' -import init, { - is_ident_or_operator, - is_numeric_literal, - parse, - parse_doc_to_json, -} from '../../rust-ffi/pkg/rust_ffi' -import { assertDefined } from '../util/assert' -import { isNode } from '../util/detect' - -let xxHasher128: Awaited> | undefined -export function xxHash128(input: IDataType) { - assertDefined(xxHasher128, 'Module should have been loaded with `initializeFFI`.') - xxHasher128.init() - xxHasher128.update(input) - return xxHasher128.digest() -} - -export async function initializeFFI(path?: string | undefined) { - if (xxHasher128 != null) return - if (isNode) { - const fs = await import('node:fs/promises') - const { fileURLToPath, URL: nodeURL } = await import('node:url') - const buffer = fs.readFile( - path ?? fileURLToPath(new nodeURL('../../rust-ffi/pkg/rust_ffi_bg.wasm', import.meta.url)), - ) - await init(buffer) - } else { - await init() - } - xxHasher128 = await createXXHash128() -} - -// TODO[ao]: We cannot to that, because the ffi is used by cjs modules. -// await initializeFFI() - -/* eslint-disable-next-line camelcase */ -export { is_ident_or_operator, is_numeric_literal, parse_doc_to_json, parse as parse_tree } diff --git a/app/gui2/shared/ast/ffiPolyglot.ts b/app/gui2/shared/ast/ffiPolyglot.ts deleted file mode 100644 index de2285bdee16..000000000000 --- a/app/gui2/shared/ast/ffiPolyglot.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @file This file is used as ffi {@link module:ffi} interface for building the polyglot ydoc server. - * All the exported methods are provided by the ydoc server implementation. - * The interface should be kept in sync with Rust ffi interface {@link module:ffi}. - * - * @module ffiPolyglot - */ - -import type { IDataType } from 'hash-wasm/dist/lib/util' - -declare global { - function parse_tree(code: string): Uint8Array - function parse_doc_to_json(docs: string): string - function is_ident_or_operator(code: string): number - function is_numeric_literal(code: string): boolean - function xxHash128(input: IDataType): string -} - -export async function initializeFFI(_path?: string | undefined) {} - -/* eslint-disable camelcase */ -export const { - is_ident_or_operator, - is_numeric_literal, - parse_doc_to_json, - parse_tree, - xxHash128, -} = globalThis -/* eslint-enable camelcase */ diff --git a/app/gui2/shared/util/data/result.ts b/app/gui2/shared/util/data/result.ts deleted file mode 100644 index ab3c6fa5dcbf..000000000000 --- a/app/gui2/shared/util/data/result.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** @file A generic type that can either hold a value representing a successful result, - * or an error. */ - -import { isSome, type Opt } from './opt' - -export type Result = - | { ok: true; value: T } - | { ok: false; error: ResultError } - -export function Ok(): Result -export function Ok(data: T): Result -export function Ok(data?: T): Result { - return { ok: true, value: data } -} - -export function Err(error: E): Result { - return { ok: false, error: new ResultError(error) } -} - -export function okOr(data: Opt, error: E): Result { - if (isSome(data)) return Ok(data) - else return Err(error) -} - -export function unwrap(result: Result): T { - if (result.ok) return result.value - else throw result.error -} - -export function unwrapOr(result: Result, alternative: A): T | A { - if (result.ok) return result.value - else return alternative -} - -export function mapOk(result: Result, f: (value: T) => U): Result { - if (result.ok) return Ok(f(result.value)) - else return result -} - -export function isResult(v: unknown): v is Result { - return ( - v != null && - typeof v === 'object' && - 'ok' in v && - typeof v.ok === 'boolean' && - ('value' in v || ('error' in v && v.error instanceof ResultError)) - ) -} - -export class ResultError { - payload: E - context: (() => string)[] - - constructor(payload: E) { - this.payload = payload - this.context = [] - } - - log(preamble: string = 'Error') { - console.error(this.message(preamble)) - } - - message(preamble: string = 'error') { - const ctx = - this.context.length > 0 ? `\n${Array.from(this.context, (ctx) => ctx()).join('\n')}` : '' - return `${preamble}: ${this.payload}${ctx}` - } -} - -export function withContext(context: () => string, f: () => Result): Result -export function withContext( - context: () => string, - f: () => Promise>, -): Promise> -export function withContext( - context: () => string, - f: () => Promise> | Result, -) { - const result = f() - const handleResult = (result: Result) => { - if (result == null) { - throw new Error('withContext: f() returned null or undefined') - } - if (!result.ok) result.error.context.splice(0, 0, context) - return result - } - if (result instanceof Promise) { - return result.then(handleResult) - } else { - return handleResult(result) - } -} - -/** - * Catch promise rejection of provided types and convert them to a Result type. - */ -export function rejectionToResult any>( - errorKinds: ErrorKind | ErrorKind[], -): (promise: Promise) => Promise>> { - const errorKindArray = Array.isArray(errorKinds) ? errorKinds : [errorKinds] - return async (promise) => { - try { - return Ok(await promise) - } catch (error) { - for (const errorKind of errorKindArray) { - if (error instanceof errorKind) return Err(error) - } - throw error - } - } -} diff --git a/app/gui2/shared/util/net/ReconnectingWSTransport.ts b/app/gui2/shared/util/net/ReconnectingWSTransport.ts deleted file mode 100644 index 8a961b5979b6..000000000000 --- a/app/gui2/shared/util/net/ReconnectingWSTransport.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * This file is modified version of open-rpc/client-js WebSocketTransport implementation - * (https://github.com/open-rpc/client-js/blob/master/src/transports/WebSocketTransport.ts) - * which uses the automatically reconnecting websocket. - */ - -import { ERR_UNKNOWN, JSONRPCError } from '@open-rpc/client-js/build/Error' -import { - getBatchRequests, - getNotifications, - type JSONRPCRequestData, -} from '@open-rpc/client-js/build/Request' -import { Transport } from '@open-rpc/client-js/build/transports/Transport' -import WS from 'isomorphic-ws' -import { WebSocket } from 'partysocket' -import { type WebSocketEventMap } from 'partysocket/ws' - -export interface AddEventListenerOptions { - capture?: boolean - once?: boolean - passive?: boolean - signal?: AbortSignal -} - -class ReconnectingWebSocketTransport extends Transport { - public connection: WebSocket - public uri: string - - constructor(uri: string) { - super() - this.uri = uri - this.connection = new WebSocket(uri, undefined, { WebSocket: WS }) - } - - public connect(): Promise { - return new Promise((resolve) => { - this.connection.addEventListener('open', () => resolve(), { once: true }) - this.connection.addEventListener('message', ({ data }: { data: string }) => { - this.transportRequestManager.resolveResponse(data) - }) - }) - } - - public reconnect() { - this.connection.reconnect() - } - - public async sendData(data: JSONRPCRequestData, timeout: number | null = 5000): Promise { - let promise = this.transportRequestManager.addRequest(data, timeout) - const notifications = getNotifications(data) - try { - this.connection.send(JSON.stringify(this.parseData(data))) - this.transportRequestManager.settlePendingRequest(notifications) - } catch (err) { - const jsonError = new JSONRPCError((err as any).message, ERR_UNKNOWN, err) - - this.transportRequestManager.settlePendingRequest(notifications, jsonError) - this.transportRequestManager.settlePendingRequest(getBatchRequests(data), jsonError) - - promise = Promise.reject(jsonError) - } - - return promise - } - - public close(): void { - this.connection.close() - } - - on( - type: K, - cb: ( - event: WebSocketEventMap[K] extends Event ? WebSocketEventMap[K] : never, - ) => WebSocketEventMap[K] extends Event ? void : never, - options?: AddEventListenerOptions, - ): void { - this.connection.addEventListener(type, cb, options) - } - - off( - type: K, - cb: ( - event: WebSocketEventMap[K] extends Event ? WebSocketEventMap[K] : never, - ) => WebSocketEventMap[K] extends Event ? void : never, - options?: AddEventListenerOptions, - ): void { - this.connection.removeEventListener(type, cb, options) - } -} - -export default ReconnectingWebSocketTransport diff --git a/app/gui2/src/App.vue b/app/gui2/src/App.vue index 30c3e6cfcdc2..1d3f5736059d 100644 --- a/app/gui2/src/App.vue +++ b/app/gui2/src/App.vue @@ -19,7 +19,6 @@ import { computed, markRaw, toRaw, toRef, watch } from 'vue' import TooltipDisplayer from './components/TooltipDisplayer.vue' import { provideTooltipRegistry } from './providers/tooltipState' import { provideVisibility } from './providers/visibility' -import { initializePrefixes } from './util/ast/node' import { urlParams } from './util/urlParams' const props = defineProps<{ @@ -37,8 +36,6 @@ provideBackend(() => markRaw(toRaw(props.backend))) const classSet = provideAppClassSet() const appTooltips = provideTooltipRegistry() -initializePrefixes() - const logger = provideEventLogger(toRef(props, 'logEvent'), toRef(props, 'projectId')) watch( toRef(props, 'projectId'), diff --git a/app/gui2/src/assets/base.css b/app/gui2/src/assets/base.css index 710635109077..261fb604d43a 100644 --- a/app/gui2/src/assets/base.css +++ b/app/gui2/src/assets/base.css @@ -86,4 +86,5 @@ --visualization-resize-handle-outside: 3px; --right-dock-default-width: 40%; --code-editor-default-height: 30%; + --scrollbar-scrollable-opacity: 100%; } diff --git a/app/gui2/src/asyncApp.ts b/app/gui2/src/asyncApp.ts index e9992f8e82e6..3500bcba1020 100644 --- a/app/gui2/src/asyncApp.ts +++ b/app/gui2/src/asyncApp.ts @@ -1,9 +1,6 @@ import '@/assets/base.css' export async function AsyncApp() { - const [_, app] = await Promise.all([ - import('shared/ast/ffi').then((mod) => mod.initializeFFI()), - import('@/App.vue'), - ]) + const app = await import('@/App.vue') return app } diff --git a/app/gui2/src/components/CodeEditor.vue b/app/gui2/src/components/CodeEditor.vue index e6880dbb36f6..42423ead74c0 100644 --- a/app/gui2/src/components/CodeEditor.vue +++ b/app/gui2/src/components/CodeEditor.vue @@ -9,10 +9,10 @@ import { unwrap } from '@/util/data/result' import { qnJoin, tryQualifiedName } from '@/util/qualifiedName' import { EditorSelection } from '@codemirror/state' import { createDebouncer } from 'lib0/eventloop' -import { MutableModule } from 'shared/ast' -import { textChangeToEdits, type SourceRangeEdit } from 'shared/util/data/text' -import { rangeEncloses, type Origin } from 'shared/yjsModel' import { computed, onMounted, onUnmounted, ref, shallowRef, watch, watchEffect } from 'vue' +import { MutableModule } from 'ydoc-shared/ast' +import { textChangeToEdits, type SourceRangeEdit } from 'ydoc-shared/util/data/text' +import { rangeEncloses, type Origin } from 'ydoc-shared/yjsModel' // Use dynamic imports to aid code splitting. The codemirror dependency is quite large. const { diff --git a/app/gui2/src/components/CodeEditor/codemirror.ts b/app/gui2/src/components/CodeEditor/codemirror.ts index aba0562e0166..a350c0f6434b 100644 --- a/app/gui2/src/components/CodeEditor/codemirror.ts +++ b/app/gui2/src/components/CodeEditor/codemirror.ts @@ -43,9 +43,9 @@ import { } from '@lezer/common' import { styleTags, tags } from '@lezer/highlight' import { EditorView } from 'codemirror' -import type { Diagnostic as LSDiagnostic } from 'shared/languageServerTypes' -import { tryGetSoleValue } from 'shared/util/data/iterable' -import type { SourceRangeEdit } from 'shared/util/data/text' +import type { Diagnostic as LSDiagnostic } from 'ydoc-shared/languageServerTypes' +import { tryGetSoleValue } from 'ydoc-shared/util/data/iterable' +import type { SourceRangeEdit } from 'ydoc-shared/util/data/text' export function lsDiagnosticsToCMDiagnostics( source: string, diff --git a/app/gui2/src/components/ColorPickerMenu.vue b/app/gui2/src/components/ColorPickerMenu.vue index ebd5559022f3..20f5842bb483 100644 --- a/app/gui2/src/components/ColorPickerMenu.vue +++ b/app/gui2/src/components/ColorPickerMenu.vue @@ -4,8 +4,8 @@ import { injectNodeColors } from '@/providers/graphNodeColors' import { injectGraphSelection } from '@/providers/graphSelection' import { useGraphStore, type NodeId } from '@/stores/graph' import { filterDefined } from '@/util/data/iterable' -import { tryGetSoleValue } from 'shared/util/data/iterable' import { ref } from 'vue' +import { tryGetSoleValue } from 'ydoc-shared/util/data/iterable' const emit = defineEmits<{ close: [] diff --git a/app/gui2/src/components/ColorRing/gradient.ts b/app/gui2/src/components/ColorRing/gradient.ts index d5d0df1c8f33..6c691d144d33 100644 --- a/app/gui2/src/components/ColorRing/gradient.ts +++ b/app/gui2/src/components/ColorRing/gradient.ts @@ -1,5 +1,5 @@ import { ensoColor, formatCssColor, normalizeHue } from '@/util/colors' -import { Resumable } from 'shared/util/data/iterable' +import { Resumable } from 'ydoc-shared/util/data/iterable' export interface FixedRange { start: number diff --git a/app/gui2/src/components/ComponentBrowser.vue b/app/gui2/src/components/ComponentBrowser.vue index 0f803fa95403..32184a2d32ca 100644 --- a/app/gui2/src/components/ComponentBrowser.vue +++ b/app/gui2/src/components/ComponentBrowser.vue @@ -1,11 +1,11 @@ + + + + diff --git a/app/gui2/src/components/DockPanel.vue b/app/gui2/src/components/DockPanel.vue index 41e676c32e23..f3e95a1b9b91 100644 --- a/app/gui2/src/components/DockPanel.vue +++ b/app/gui2/src/components/DockPanel.vue @@ -6,15 +6,21 @@ import ToggleIcon from '@/components/ToggleIcon.vue' import { useResizeObserver } from '@/composables/events' import { Rect } from '@/util/data/rect' import { Vec2 } from '@/util/data/vec2' +import { tabClipPath } from 'enso-common/src/utilities/style/tabBar' import { computed, ref } from 'vue' const MIN_DOCK_SIZE_PX = 200 +const TAB_EDGE_MARGIN_PX = 4 +const TAB_SIZE_PX = { width: 48 - TAB_EDGE_MARGIN_PX, height: 48 } +const TAB_RADIUS_PX = 8 -const toolbarElement = ref() -const slideInPanel = ref() +type Tab = 'docs' | 'help' const show = defineModel('show', { required: true }) const size = defineModel('size') +const tab = defineModel('tab') + +const slideInPanel = ref() const computedSize = useResizeObserver(slideInPanel) const computedBounds = computed(() => new Rect(Vec2.Zero, computedSize.value)) @@ -26,6 +32,14 @@ function clampSize(size: number) { const style = computed(() => ({ width: size.value != null ? `${clampSize(size.value)}px` : 'var(--right-dock-default-width)', })) + +const tabStyle = { + clipPath: tabClipPath(TAB_SIZE_PX, TAB_RADIUS_PX, 'right'), + width: `${TAB_SIZE_PX.width}px`, + height: `${TAB_SIZE_PX.height}px`, + margin: `${-TAB_RADIUS_PX}px ${TAB_EDGE_MARGIN_PX}px ${-TAB_RADIUS_PX}px 0`, + paddingLeft: `${TAB_EDGE_MARGIN_PX / 2}px`, +} @@ -765,11 +577,8 @@ onUnmounted(() => { height: 100%; } -.ag-theme-alpine { - --ag-grid-size: 3px; - --ag-list-item-height: 20px; +.grid { flex-grow: 1; - font-family: var(--font-mono); } .table-visualization-status-bar { diff --git a/app/gui2/src/components/widgets/AgGridTableView.vue b/app/gui2/src/components/widgets/AgGridTableView.vue new file mode 100644 index 000000000000..8a25b95bd8e5 --- /dev/null +++ b/app/gui2/src/components/widgets/AgGridTableView.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/app/gui2/src/components/widgets/ListWidget.vue b/app/gui2/src/components/widgets/ListWidget.vue index 2efb04d02784..f521eaec3ed2 100644 --- a/app/gui2/src/components/widgets/ListWidget.vue +++ b/app/gui2/src/components/widgets/ListWidget.vue @@ -6,7 +6,7 @@ import { useAppClass } from '@/providers/appClass' import { Range } from '@/util/data/range' import { Vec2 } from '@/util/data/vec2' import { uuidv4 } from 'lib0/random' -import { computed, nextTick, ref, shallowReactive, watchEffect, watchPostEffect } from 'vue' +import { computed, ref, shallowReactive, watchEffect, watchPostEffect } from 'vue'