Format TS code (#10648)

This commit is contained in:
somebody1234 2024-07-26 16:34:51 +10:00
parent 4b96be8ef8
commit c46262dfb5
336 changed files with 10860 additions and 10983 deletions

View File

@ -39,8 +39,6 @@
<div id="enso-dashboard" class="enso-dashboard"></div>
<div id="enso-chat" class="enso-chat"></div>
<div id="enso-portal-root" class="enso-portal-root"></div>
<noscript>
This page requires JavaScript to run. Please enable it in your browser.
</noscript>
<noscript> This page requires JavaScript to run. Please enable it in your browser. </noscript>
</body>
</html>

View File

@ -25,7 +25,7 @@ PROD=1 npm run test:e2e:debug -- e2e/file-name-here.spec.ts
## Getting started
```ts
test.test("test name here", ({ page }) =>
test.test('test name here', ({ page }) =>
actions.mockAllAndLogin({ page }).then(
// ONLY chain methods from `pageActions`.
// Using methods not in `pageActions` is UNDEFINED BEHAVIOR.
@ -34,20 +34,20 @@ test.test("test name here", ({ page }) =>
// not `Promise`s, which causes Playwright to output a type error.
async ({ pageActions }) => await pageActions.goTo.drive(),
),
);
)
```
### Perform arbitrary actions (e.g. actions on the API)
```ts
test.test("test name here", ({ page }) =>
test.test('test name here', ({ page }) =>
actions.mockAllAndLogin({ page }).then(
async ({ pageActions, api }) =>
await pageActions.do(() => {
api.foo();
api.bar();
test.expect(api.baz()?.quux).toEqual("bar");
api.foo()
api.bar()
test.expect(api.baz()?.quux).toEqual('bar')
}),
),
);
)
```

View File

@ -621,7 +621,7 @@ export namespace settings {
* DO NOT assume the left side of the outer container will change. This means that it is NOT SAFE
* to do anything with the returned values other than comparing them. */
export function getAssetRowLeftPx(locator: test.Locator) {
return locator.evaluate(el => el.children[0]?.children[0]?.getBoundingClientRect().left ?? 0)
return locator.evaluate((el) => el.children[0]?.children[0]?.getBoundingClientRect().left ?? 0)
}
// ===================================
@ -644,7 +644,7 @@ export async function expectOpacity0(locator: test.Locator) {
await test.test.step('Expect `opacity: 0`', async () => {
await test
.expect(async () => {
test.expect(await locator.evaluate(el => getComputedStyle(el).opacity)).toBe('0')
test.expect(await locator.evaluate((el) => getComputedStyle(el).opacity)).toBe('0')
})
.toPass()
})
@ -655,7 +655,7 @@ export async function expectNotOpacity0(locator: test.Locator) {
await test.test.step('Expect not `opacity: 0`', async () => {
await test
.expect(async () => {
test.expect(await locator.evaluate(el => getComputedStyle(el).opacity)).not.toBe('0')
test.expect(await locator.evaluate((el) => getComputedStyle(el).opacity)).not.toBe('0')
})
.toPass()
})
@ -667,13 +667,13 @@ export async function expectOnScreen(locator: test.Locator) {
await test
.expect(async () => {
const pageBounds = await locator.evaluate(() => document.body.getBoundingClientRect())
const bounds = await locator.evaluate(el => el.getBoundingClientRect())
const bounds = await locator.evaluate((el) => el.getBoundingClientRect())
test
.expect(
bounds.left < pageBounds.right &&
bounds.right > pageBounds.left &&
bounds.top < pageBounds.bottom &&
bounds.bottom > pageBounds.top
bounds.bottom > pageBounds.top,
)
.toBe(true)
})
@ -687,13 +687,13 @@ export async function expectNotOnScreen(locator: test.Locator) {
await test
.expect(async () => {
const pageBounds = await locator.evaluate(() => document.body.getBoundingClientRect())
const bounds = await locator.evaluate(el => el.getBoundingClientRect())
const bounds = await locator.evaluate((el) => el.getBoundingClientRect())
test
.expect(
bounds.left >= pageBounds.right ||
bounds.right <= pageBounds.left ||
bounds.top >= pageBounds.bottom ||
bounds.bottom <= pageBounds.top
bounds.bottom <= pageBounds.top,
)
.toBe(true)
})
@ -773,7 +773,7 @@ export async function login(
{ page, setupAPI }: MockParams,
email = 'email@example.com',
password = VALID_PASSWORD,
first = true
first = true,
) {
await test.test.step('Login', async () => {
await page.goto('/')
@ -812,7 +812,7 @@ export async function reload({ page }: MockParams) {
export async function relog(
{ page, setupAPI }: MockParams,
email = 'email@example.com',
password = VALID_PASSWORD
password = VALID_PASSWORD,
) {
await test.test.step('Relog', async () => {
await page.getByAltText('User Settings').locator('visible=true').click()
@ -901,7 +901,7 @@ export function mockAllAndLogin({ page, setupAPI }: MockParams) {
await mockApi({ page, setupAPI })
await mockDate({ page, setupAPI })
})
.do(thePage => login({ page: thePage, setupAPI }))
.do((thePage) => login({ page: thePage, setupAPI }))
}
// ===================================

View File

@ -37,7 +37,7 @@ export default class BaseActions implements Promise<void> {
/** Create a {@link BaseActions}. */
constructor(
protected readonly page: test.Page,
private readonly promise = Promise.resolve()
private readonly promise = Promise.resolve(),
) {}
/** Get the string name of the class of this instance. Required for this class to implement
@ -71,7 +71,7 @@ export default class BaseActions implements Promise<void> {
// eslint-disable-next-line no-restricted-syntax
onfulfilled?: (() => PromiseLike<T> | T) | null | undefined,
// eslint-disable-next-line no-restricted-syntax
onrejected?: ((reason: unknown) => E | PromiseLike<E>) | null | undefined
onrejected?: ((reason: unknown) => E | PromiseLike<E>) | null | undefined,
) {
return await this.promise.then(onfulfilled, onrejected)
}
@ -109,7 +109,7 @@ export default class BaseActions implements Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return new this.constructor(
this.page,
this.then(() => callback(this.page))
this.then(() => callback(this.page)),
)
}
@ -121,18 +121,18 @@ export default class BaseActions implements Promise<void> {
/** Press a key, replacing the text `Mod` with `Meta` (`Cmd`) on macOS, and `Control`
* on all other platforms. */
press<Key extends string>(keyOrShortcut: inputBindings.AutocompleteKeybind<Key>) {
return this.do(page => BaseActions.press(page, keyOrShortcut))
return this.do((page) => BaseActions.press(page, keyOrShortcut))
}
/** Perform actions until a predicate passes. */
retry(
callback: (actions: this) => this,
predicate: (page: test.Page) => Promise<boolean>,
options: { retries?: number; delay?: number } = {}
options: { retries?: number; delay?: number } = {},
) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const { retries = 3, delay = 1_000 } = options
return this.step('Perform actions with retries', async thePage => {
return this.step('Perform actions with retries', async (thePage) => {
for (let i = 0; i < retries; i += 1) {
await callback(this)
if (await predicate(thePage)) {
@ -148,10 +148,10 @@ export default class BaseActions implements Promise<void> {
/** Perform actions with the "Mod" modifier key pressed. */
withModPressed<R extends BaseActions>(callback: (actions: this) => R) {
return callback(
this.step('Press "Mod"', async page => {
this.step('Press "Mod"', async (page) => {
await page.keyboard.down(await actions.modModifier(page))
})
).step('Release "Mod"', async page => {
}),
).step('Release "Mod"', async (page) => {
await page.keyboard.up(await actions.modModifier(page))
})
}

View File

@ -49,26 +49,26 @@ export default class DrivePageActions extends PageActions {
return {
/** Switch to the "cloud" category. */
cloud() {
return self.step('Go to "Cloud" category', page =>
page.getByRole('button', { name: 'Cloud' }).getByText('Cloud').click()
return self.step('Go to "Cloud" category', (page) =>
page.getByRole('button', { name: 'Cloud' }).getByText('Cloud').click(),
)
},
/** Switch to the "local" category. */
local() {
return self.step('Go to "Local" category', page =>
page.getByRole('button', { name: 'Local' }).getByText('Local').click()
return self.step('Go to "Local" category', (page) =>
page.getByRole('button', { name: 'Local' }).getByText('Local').click(),
)
},
/** Switch to the "recent" category. */
recent() {
return self.step('Go to "Recent" category', page =>
page.getByRole('button', { name: 'Recent' }).getByText('Recent').click()
return self.step('Go to "Recent" category', (page) =>
page.getByRole('button', { name: 'Recent' }).getByText('Recent').click(),
)
},
/** Switch to the "trash" category. */
trash() {
return self.step('Go to "Trash" category', page =>
page.getByRole('button', { name: 'Trash' }).getByText('Trash').click()
return self.step('Go to "Trash" category', (page) =>
page.getByRole('button', { name: 'Trash' }).getByText('Trash').click(),
)
},
}
@ -81,49 +81,49 @@ export default class DrivePageActions extends PageActions {
return {
/** 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()
return self.step('Click "name" column heading', (page) =>
page.getByLabel('Sort by name').or(page.getByLabel('Stop sorting by name')).click(),
)
},
/** Click the column heading for the "modified" column to change its sort order. */
clickModifiedColumnHeading() {
return self.step('Click "modified" column heading', page =>
return self.step('Click "modified" column heading', (page) =>
page
.getByLabel('Sort by modification date')
.or(page.getByLabel('Stop sorting by modification date'))
.click()
.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 })
return self.step(`Click drive table row #${index}`, (page) =>
locateAssetRows(page).nth(index).click({ position: actions.ASSET_ROW_SAFE_POSITION }),
)
},
/** Right click a specific row to bring up its context menu, or the context menu for multiple
* assets when right clicking on a selected asset when multiple assets are selected. */
rightClickRow(index: number) {
return self.step(`Right click drive table row #${index}`, page =>
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: actions.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 })
return self.step(`Double dlick drive table row #${index}`, (page) =>
locateAssetRows(page).nth(index).dblclick({ position: actions.ASSET_ROW_SAFE_POSITION }),
)
},
/** Interact with the set of all rows in the Drive table. */
withRows(callback: baseActions.LocatorCallback) {
return self.step('Interact with drive table rows', async page => {
return self.step('Interact with drive table rows', async (page) => {
await callback(locateAssetRows(page))
})
},
/** Drag a row onto another row. */
dragRowToRow(from: number, to: number) {
return self.step(`Drag drive table row #${from} to row #${to}`, async page => {
return self.step(`Drag drive table row #${from} to row #${to}`, async (page) => {
const rows = locateAssetRows(page)
await rows.nth(from).dragTo(rows.nth(to), {
sourcePosition: ASSET_ROW_SAFE_POSITION,
@ -133,19 +133,19 @@ export default class DrivePageActions extends PageActions {
},
/** Drag a row onto another row. */
dragRow(from: number, to: test.Locator, force?: boolean) {
return self.step(`Drag drive table row #${from} to custom locator`, page =>
return self.step(`Drag drive table row #${from} to custom locator`, (page) =>
locateAssetRows(page)
.nth(from)
.dragTo(to, {
sourcePosition: ASSET_ROW_SAFE_POSITION,
...(force == null ? {} : { force }),
})
}),
)
},
/** 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 to show. */
expectPlaceholderRow() {
return self.step('Expect placeholder row', async page => {
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/)
@ -154,7 +154,7 @@ export default class DrivePageActions extends PageActions {
/** 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 => {
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/)
@ -165,38 +165,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('Expect trash placeholder row', (page) =>
page.getByAltText('Modified').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('Expect trash placeholder row', (page) =>
page.getByAltText('Shared With').click(),
)
},
/** Toggle visibility for the "labels" column. */
labels() {
return self.step('Expect trash placeholder row', page =>
page.getByAltText('Labels').click()
return self.step('Expect trash placeholder row', (page) =>
page.getByAltText('Labels').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('Expect trash placeholder row', (page) =>
page.getByAltText('Accessed By Projects').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('Expect trash placeholder row', (page) =>
page.getByAltText('Accessed Data').click(),
)
},
/** Toggle visibility for the "docs" column. */
docs() {
return self.step('Expect trash placeholder row', page =>
page.getByAltText('Docs').click()
return self.step('Expect trash placeholder row', (page) =>
page.getByAltText('Docs').click(),
)
},
}
@ -206,27 +206,27 @@ 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()
return this.step('Open "start" modal', (page) =>
page.getByText('Start with a template').click(),
).into(StartModalActions)
}
/** Create a new empty project. */
newEmptyProject() {
return this.step('Create empty project', page =>
page.getByText('New Empty Project').click()
return this.step('Create empty project', (page) =>
page.getByText('New Empty Project').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(actions.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' }).click()
return this.step('Create folder', (page) =>
page.getByRole('button', { name: 'New Folder' }).click(),
)
}
@ -234,9 +234,9 @@ export default class DrivePageActions extends PageActions {
uploadFile(
name: string,
contents: WithImplicitCoercion<Uint8Array | string | readonly number[]>,
mimeType = 'text/plain'
mimeType = 'text/plain',
) {
return this.step(`Upload file '${name}'`, async page => {
return this.step(`Upload file '${name}'`, async (page) => {
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByRole('button', { name: 'Import' }).click()
const fileChooser = await fileChooserPromise
@ -246,7 +246,7 @@ 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 => {
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)
@ -256,35 +256,35 @@ export default class DrivePageActions extends PageActions {
/** Toggle the Asset Panel open or closed. */
toggleAssetPanel() {
return this.step('Toggle asset panel', page =>
page.getByLabel('Asset Panel').locator('visible=true').click()
return this.step('Toggle asset panel', (page) =>
page.getByLabel('Asset Panel').locator('visible=true').click(),
)
}
/** Interact with the container element of the assets table. */
withAssetsTable(callback: baseActions.LocatorCallback) {
return this.step('Interact with drive table', async page => {
return this.step('Interact with drive table', async (page) => {
await callback(actions.locateAssetsTable(page))
})
}
/** Interact with the Asset Panel. */
withAssetPanel(callback: baseActions.LocatorCallback) {
return this.step('Interact with asset panel', async page => {
return this.step('Interact with asset panel', async (page) => {
await callback(actions.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()
return this.step('Open "new data link" modal', (page) =>
page.getByRole('button', { name: 'New Datalink' }).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 => {
return this.step('Interact with context menus', async (page) => {
await callback(actions.locateContextMenus(page))
})
}

View File

@ -20,7 +20,7 @@ export default class LoginPageActions extends BaseActions {
/** 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) {
return this.step('Login (as new user)', () => this.loginInternal(email, password)).into(
SetUsernamePageActions
SetUsernamePageActions,
)
}

View File

@ -29,7 +29,7 @@ export default class NewDataLinkModalActions extends BaseActions {
/** Interact with the "name" input - for example, to set the name using `.fill("")`. */
withNameInput(callback: baseActions.LocatorCallback) {
return this.step('Interact with "name" input', async page => {
return this.step('Interact with "name" input', async (page) => {
const locator = locateNewDataLinkModal(page).getByLabel('Name')
await callback(locator)
})

View File

@ -11,7 +11,7 @@ import DrivePageActions from './DrivePageActions'
export default class SetUsernamePageActions 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 => {
return this.step(`Set username to '${username}'`, async (page) => {
await actions.locateUsernameInput(page).fill(username)
await actions.locateSetUsernameButton(page).click()
}).into(DrivePageActions)

View File

@ -12,18 +12,18 @@ import EditorPageActions from './EditorPageActions'
export default class StartModalActions extends BaseActions {
/** Close this modal and go back to the Drive page. */
close() {
return this.step('Close "start" modal', page => page.getByLabel('Close').click()).into(
DrivePageActions
return this.step('Close "start" modal', (page) => page.getByLabel('Close').click()).into(
DrivePageActions,
)
}
/** Create a project from the template at the given index. */
createProjectFromTemplate(index: number) {
return this.step(`Create project from template #${index}`, page =>
return this.step(`Create project from template #${index}`, (page) =>
actions
.locateSamples(page)
.nth(index + 1)
.click()
.click(),
).into(EditorPageActions)
}
}

View File

@ -38,102 +38,102 @@ export interface ContextMenuActions<T extends BaseActions> {
/** Generate actions for the context menu. */
export function contextMenuActions<T extends BaseActions>(
step: (name: string, callback: baseActions.PageCallback) => T
step: (name: string, callback: baseActions.PageCallback) => T,
): ContextMenuActions<T> {
return {
open: () =>
step('Open (context menu)', page =>
page.getByRole('button', { name: 'Open' }).getByText('Open').click()
step('Open (context menu)', (page) =>
page.getByRole('button', { name: 'Open' }).getByText('Open').click(),
),
uploadToCloud: () =>
step('Upload to cloud (context menu)', page =>
page.getByRole('button', { name: 'Upload To Cloud' }).getByText('Upload To Cloud').click()
step('Upload to cloud (context menu)', (page) =>
page.getByRole('button', { name: 'Upload To Cloud' }).getByText('Upload To Cloud').click(),
),
rename: () =>
step('Rename (context menu)', page =>
page.getByRole('button', { name: 'Rename' }).getByText('Rename').click()
step('Rename (context menu)', (page) =>
page.getByRole('button', { name: 'Rename' }).getByText('Rename').click(),
),
snapshot: () =>
step('Snapshot (context menu)', page =>
page.getByRole('button', { name: 'Snapshot' }).getByText('Snapshot').click()
step('Snapshot (context menu)', (page) =>
page.getByRole('button', { name: 'Snapshot' }).getByText('Snapshot').click(),
),
moveToTrash: () =>
step('Move to trash (context menu)', page =>
page.getByRole('button', { name: 'Move To Trash' }).getByText('Move To Trash').click()
step('Move to trash (context menu)', (page) =>
page.getByRole('button', { name: 'Move To Trash' }).getByText('Move To Trash').click(),
),
moveAllToTrash: () =>
step('Move all to trash (context menu)', page =>
step('Move all to trash (context menu)', (page) =>
page
.getByRole('button', { name: 'Move All To Trash' })
.getByText('Move All To Trash')
.click()
.click(),
),
restoreFromTrash: () =>
step('Restore from trash (context menu)', page =>
step('Restore from trash (context menu)', (page) =>
page
.getByRole('button', { name: 'Restore From Trash' })
.getByText('Restore From Trash')
.click()
.click(),
),
restoreAllFromTrash: () =>
step('Restore all from trash (context menu)', page =>
step('Restore all from trash (context menu)', (page) =>
page
.getByRole('button', { name: 'Restore All From Trash' })
.getByText('Restore All From Trash')
.click()
.click(),
),
share: () =>
step('Share (context menu)', page =>
page.getByRole('button', { name: 'Share' }).getByText('Share').click()
step('Share (context menu)', (page) =>
page.getByRole('button', { name: 'Share' }).getByText('Share').click(),
),
label: () =>
step('Label (context menu)', page =>
page.getByRole('button', { name: 'Label' }).getByText('Label').click()
step('Label (context menu)', (page) =>
page.getByRole('button', { name: 'Label' }).getByText('Label').click(),
),
duplicate: () =>
step('Duplicate (context menu)', page =>
page.getByRole('button', { name: 'Duplicate' }).getByText('Duplicate').click()
step('Duplicate (context menu)', (page) =>
page.getByRole('button', { name: 'Duplicate' }).getByText('Duplicate').click(),
),
duplicateProject: () =>
step('Duplicate project (context menu)', page =>
page.getByRole('button', { name: 'Duplicate' }).getByText('Duplicate').click()
step('Duplicate project (context menu)', (page) =>
page.getByRole('button', { name: 'Duplicate' }).getByText('Duplicate').click(),
).into(EditorPageActions),
copy: () =>
step('Copy (context menu)', page =>
page.getByRole('button', { name: 'Copy' }).getByText('Copy', { exact: true }).click()
step('Copy (context menu)', (page) =>
page.getByRole('button', { name: 'Copy' }).getByText('Copy', { exact: true }).click(),
),
cut: () =>
step('Cut (context menu)', page =>
page.getByRole('button', { name: 'Cut' }).getByText('Cut').click()
step('Cut (context menu)', (page) =>
page.getByRole('button', { name: 'Cut' }).getByText('Cut').click(),
),
paste: () =>
step('Paste (context menu)', page =>
page.getByRole('button', { name: 'Paste' }).getByText('Paste').click()
step('Paste (context menu)', (page) =>
page.getByRole('button', { name: 'Paste' }).getByText('Paste').click(),
),
copyAsPath: () =>
step('Copy as path (context menu)', page =>
page.getByRole('button', { name: 'Copy As Path' }).getByText('Copy As Path').click()
step('Copy as path (context menu)', (page) =>
page.getByRole('button', { name: 'Copy As Path' }).getByText('Copy As Path').click(),
),
download: () =>
step('Download (context menu)', page =>
page.getByRole('button', { name: 'Download' }).getByText('Download').click()
step('Download (context menu)', (page) =>
page.getByRole('button', { name: 'Download' }).getByText('Download').click(),
),
// TODO: Specify the files in parameters.
uploadFiles: () =>
step('Upload files (context menu)', page =>
page.getByRole('button', { name: 'Upload Files' }).getByText('Upload Files').click()
step('Upload files (context menu)', (page) =>
page.getByRole('button', { name: 'Upload Files' }).getByText('Upload Files').click(),
),
newFolder: () =>
step('New folder (context menu)', page =>
page.getByRole('button', { name: 'New Folder' }).getByText('New Folder').click()
step('New folder (context menu)', (page) =>
page.getByRole('button', { name: 'New Folder' }).getByText('New Folder').click(),
),
newSecret: () =>
step('New secret (context menu)', page =>
page.getByRole('button', { name: 'New Secret' }).getByText('New Secret').click()
step('New secret (context menu)', (page) =>
page.getByRole('button', { name: 'New Secret' }).getByText('New Secret').click(),
),
newDataLink: () =>
step('New Data Link (context menu)', page =>
page.getByRole('button', { name: 'New Data Link' }).getByText('New Data Link').click()
step('New Data Link (context menu)', (page) =>
page.getByRole('button', { name: 'New Data Link' }).getByText('New Data Link').click(),
),
}
}

View File

@ -22,23 +22,23 @@ export interface GoToPageActions {
/** Generate actions for going to a different page. */
export function goToPageActions(
step: (name: string, callback: baseActions.PageCallback) => BaseActions
step: (name: string, callback: baseActions.PageCallback) => BaseActions,
): GoToPageActions {
return {
drive: () =>
step('Go to "Data Catalog" page', page =>
step('Go to "Data Catalog" page', (page) =>
page
.getByRole('tab')
.filter({ has: page.getByText('Data Catalog') })
.click()
.click(),
).into(DrivePageActions),
editor: () =>
step('Go to "Spatial Analysis" page', page =>
page.getByTestId('editor-tab-button').click()
step('Go to "Spatial Analysis" page', (page) =>
page.getByTestId('editor-tab-button').click(),
).into(EditorPageActions),
settings: () =>
step('Go to "settings" page', page => BaseActions.press(page, 'Mod+,')).into(
SettingsPageActions
step('Go to "settings" page', (page) => BaseActions.press(page, 'Mod+,')).into(
SettingsPageActions,
),
}
}

View File

@ -8,9 +8,9 @@ import type BaseActions from './BaseActions'
/** An action to open the User Menu. */
export function openUserMenuAction<T extends BaseActions>(
step: (name: string, callback: baseActions.PageCallback) => T
step: (name: string, callback: baseActions.PageCallback) => T,
) {
return step('Open user menu', page =>
page.getByAltText('User Settings').locator('visible=true').click()
return step('Open user menu', (page) =>
page.getByAltText('User Settings').locator('visible=true').click(),
)
}

View File

@ -24,26 +24,26 @@ export interface UserMenuActions<T extends BaseActions> {
/** Generate actions for the user menu. */
export function userMenuActions<T extends BaseActions>(
step: (name: string, callback: baseActions.PageCallback) => T
step: (name: string, callback: baseActions.PageCallback) => T,
): UserMenuActions<T> {
return {
downloadApp: (callback: (download: test.Download) => Promise<void> | void) =>
step('Download app (user menu)', async page => {
step('Download app (user menu)', async (page) => {
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Download App' }).getByText('Download App').click()
await callback(await downloadPromise)
}),
settings: () =>
step('Go to Settings (user menu)', async page => {
step('Go to Settings (user menu)', async (page) => {
await page.getByRole('button', { name: 'Settings' }).getByText('Settings').click()
}).into(SettingsPageActions),
logout: () =>
step('Logout (user menu)', page =>
page.getByRole('button', { name: 'Logout' }).getByText('Logout').click()
step('Logout (user menu)', (page) =>
page.getByRole('button', { name: 'Logout' }).getByText('Logout').click(),
).into(LoginPageActions),
goToLoginPage: () =>
step('Login (user menu)', page =>
page.getByRole('button', { name: 'Login', exact: true }).getByText('Login').click()
step('Login (user menu)', (page) =>
page.getByRole('button', { name: 'Login', exact: true }).getByText('Login').click(),
).into(LoginPageActions),
}
}

View File

@ -129,7 +129,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
const createDirectory = (
title: string,
rest: Partial<backend.DirectoryAsset> = {}
rest: Partial<backend.DirectoryAsset> = {},
): backend.DirectoryAsset =>
object.merge(
{
@ -143,12 +143,12 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
parentId: defaultDirectoryId,
permissions: [],
},
rest
rest,
)
const createProject = (
title: string,
rest: Partial<backend.ProjectAsset> = {}
rest: Partial<backend.ProjectAsset> = {},
): backend.ProjectAsset =>
object.merge(
{
@ -165,7 +165,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
parentId: defaultDirectoryId,
permissions: [],
},
rest
rest,
)
const createFile = (title: string, rest: Partial<backend.FileAsset> = {}): backend.FileAsset =>
@ -181,12 +181,12 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
parentId: defaultDirectoryId,
permissions: [],
},
rest
rest,
)
const createSecret = (
title: string,
rest: Partial<backend.SecretAsset> = {}
rest: Partial<backend.SecretAsset> = {},
): backend.SecretAsset =>
object.merge(
{
@ -200,7 +200,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
parentId: defaultDirectoryId,
permissions: [],
},
rest
rest,
)
const createLabel = (value: string, color: backend.LChColor): backend.Label => ({
@ -268,7 +268,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
const deleteUser = (userId: backend.UserId) => {
usersMap.delete(userId)
const index = users.findIndex(user => user.userId === userId)
const index = users.findIndex((user) => user.userId === userId)
if (index === -1) {
return false
} else {
@ -289,7 +289,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
}
const deleteUserGroup = (userGroupId: backend.UserGroupId) => {
const index = userGroups.findIndex(userGroup => userGroup.id === userGroupId)
const index = userGroups.findIndex((userGroup) => userGroup.id === userGroupId)
if (index === -1) {
return false
} else {
@ -350,14 +350,14 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const delete_ = method('DELETE')
await page.route('https://cdn.enso.org/**', route => route.fulfill())
await page.route('https://www.google-analytics.com/**', route => route.fulfill())
await page.route('https://www.googletagmanager.com/gtag/js*', route =>
route.fulfill({ contentType: 'text/javascript', body: 'export {};' })
await page.route('https://cdn.enso.org/**', (route) => route.fulfill())
await page.route('https://www.google-analytics.com/**', (route) => route.fulfill())
await page.route('https://www.googletagmanager.com/gtag/js*', (route) =>
route.fulfill({ contentType: 'text/javascript', body: 'export {};' }),
)
const isActuallyOnline = await page.evaluate(() => navigator.onLine)
if (!isActuallyOnline) {
await page.route('https://fonts.googleapis.com/*', route => route.abort())
await page.route('https://fonts.googleapis.com/*', (route) => route.abort())
}
await page.route(BASE_URL + '**', (_route, request) => {
@ -402,24 +402,24 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
// The type of the body sent by this app is statically known.
// eslint-disable-next-line no-restricted-syntax
const body = Object.fromEntries(
new URL(request.url()).searchParams.entries()
new URL(request.url()).searchParams.entries(),
) as unknown as Query
const parentId = body.parent_id ?? defaultDirectoryId
let filteredAssets = assets.filter(asset => asset.parentId === parentId)
let filteredAssets = assets.filter((asset) => asset.parentId === parentId)
// This lint rule is broken; there is clearly a case for `undefined` below.
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
switch (body.filter_by) {
case backend.FilterBy.active: {
filteredAssets = filteredAssets.filter(asset => !deletedAssets.has(asset.id))
filteredAssets = filteredAssets.filter((asset) => !deletedAssets.has(asset.id))
break
}
case backend.FilterBy.trashed: {
filteredAssets = filteredAssets.filter(asset => deletedAssets.has(asset.id))
filteredAssets = filteredAssets.filter((asset) => deletedAssets.has(asset.id))
break
}
case backend.FilterBy.recent: {
filteredAssets = assets
.filter(asset => !deletedAssets.has(asset.id))
.filter((asset) => !deletedAssets.has(asset.id))
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
.slice(0, 10)
break
@ -436,28 +436,28 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
}
}
filteredAssets.sort(
(a, b) => backend.ASSET_TYPE_ORDER[a.type] - backend.ASSET_TYPE_ORDER[b.type]
(a, b) => backend.ASSET_TYPE_ORDER[a.type] - backend.ASSET_TYPE_ORDER[b.type],
)
const json: remoteBackend.ListDirectoryResponseBody = { assets: filteredAssets }
return json
})
await get(
remoteBackendPaths.LIST_FILES_PATH + '*',
() => ({ files: [] }) satisfies remoteBackend.ListFilesResponseBody
() => ({ files: [] }) satisfies remoteBackend.ListFilesResponseBody,
)
await get(
remoteBackendPaths.LIST_PROJECTS_PATH + '*',
() => ({ projects: [] }) satisfies remoteBackend.ListProjectsResponseBody
() => ({ projects: [] }) satisfies remoteBackend.ListProjectsResponseBody,
)
await get(
remoteBackendPaths.LIST_SECRETS_PATH + '*',
() => ({ secrets: [] }) satisfies remoteBackend.ListSecretsResponseBody
() => ({ secrets: [] }) satisfies remoteBackend.ListSecretsResponseBody,
)
await get(
remoteBackendPaths.LIST_TAGS_PATH + '*',
() => ({ tags: labels }) satisfies remoteBackend.ListTagsResponseBody
() => ({ tags: labels }) satisfies remoteBackend.ListTagsResponseBody,
)
await get(remoteBackendPaths.LIST_USERS_PATH + '*', async route => {
await get(remoteBackendPaths.LIST_USERS_PATH + '*', async (route) => {
if (currentUser != null) {
return { users } satisfies remoteBackend.ListUsersResponseBody
} else {
@ -553,18 +553,18 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
await route.fulfill({ json })
}
})
await get(remoteBackendPaths.INVITATION_PATH + '*', async route => {
await get(remoteBackendPaths.INVITATION_PATH + '*', async (route) => {
await route.fulfill({
json: { invitations: [] } satisfies backend.ListInvitationsResponseBody,
})
})
await post(remoteBackendPaths.INVITE_USER_PATH + '*', async route => {
await post(remoteBackendPaths.INVITE_USER_PATH + '*', async (route) => {
await route.fulfill()
})
await post(remoteBackendPaths.CREATE_PERMISSION_PATH + '*', async route => {
await post(remoteBackendPaths.CREATE_PERMISSION_PATH + '*', async (route) => {
await route.fulfill()
})
await delete_(remoteBackendPaths.deleteAssetPath(GLOB_ASSET_ID), async route => {
await delete_(remoteBackendPaths.deleteAssetPath(GLOB_ASSET_ID), async (route) => {
await route.fulfill()
})
await post(remoteBackendPaths.closeProjectPath(GLOB_PROJECT_ID), async (route, request) => {
@ -583,10 +583,10 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
}
await route.fulfill()
})
await delete_(remoteBackendPaths.deleteTagPath(GLOB_TAG_ID), async route => {
await delete_(remoteBackendPaths.deleteTagPath(GLOB_TAG_ID), async (route) => {
await route.fulfill()
})
await post(remoteBackendPaths.POST_LOG_EVENT_PATH, async route => {
await post(remoteBackendPaths.POST_LOG_EVENT_PATH, async (route) => {
await route.fulfill()
})
@ -625,7 +625,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
// The type of the search params sent by this app is statically known.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-restricted-syntax
const searchParams: SearchParams = Object.fromEntries(
new URL(request.url()).searchParams.entries()
new URL(request.url()).searchParams.entries(),
) as never
const file = createFile(searchParams.file_name)
return { path: '', id: file.id, project: null } satisfies backend.FileInfo
@ -672,7 +672,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
// `DirectoryId` to make TypeScript happy.
setLabels(backend.DirectoryId(assetId), body.labels)
const json: Response = {
tags: body.labels.flatMap(value => {
tags: body.labels.flatMap((value) => {
const label = labelsByValue.get(value)
return label != null ? [label] : []
}),
@ -722,7 +722,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
const body: backend.CreateUserRequestBody = await request.postDataJSON()
const organizationId = body.organizationId ?? defaultUser.organizationId
const rootDirectoryId = backend.DirectoryId(
organizationId.replace(/^organization-/, 'directory-')
organizationId.replace(/^organization-/, 'directory-'),
)
currentUser = {
email: body.userEmail,
@ -763,14 +763,14 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
return
}
})
await get(remoteBackendPaths.GET_ORGANIZATION_PATH + '*', async route => {
await get(remoteBackendPaths.GET_ORGANIZATION_PATH + '*', async (route) => {
await route.fulfill({
json: currentOrganization,
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
status: currentOrganization == null ? 404 : 200,
})
})
await post(remoteBackendPaths.CREATE_TAG_PATH + '*', route => {
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 = {
@ -845,7 +845,7 @@ async function mockApiInternal({ page, setupAPI }: MockParams) {
return json
})
await page.route('*', async route => {
await page.route('*', async (route) => {
if (!isOnline) {
await route.abort('connectionfailed')
}

View File

@ -27,24 +27,24 @@ test.test('open and close asset panel', ({ page }) =>
.mockAllAndLogin({ page })
.createFolder()
.driveTable.clickRow(0)
.withAssetPanel(async assetPanel => {
.withAssetPanel(async (assetPanel) => {
await actions.expectNotOnScreen(assetPanel)
})
.toggleAssetPanel()
.withAssetPanel(async assetPanel => {
.withAssetPanel(async (assetPanel) => {
await actions.expectOnScreen(assetPanel)
})
.toggleAssetPanel()
.withAssetPanel(async assetPanel => {
.withAssetPanel(async (assetPanel) => {
await actions.expectNotOnScreen(assetPanel)
})
}),
)
test.test('asset panel contents', ({ page }) =>
actions
.mockAll({
page,
setupAPI: api => {
setupAPI: (api) => {
const { defaultOrganizationId, defaultUserId } = api
api.addProject('project', {
description: DESCRIPTION,
@ -64,7 +64,7 @@ test.test('asset panel contents', ({ page }) =>
},
})
.login()
.do(async thePage => {
.do(async (thePage) => {
await actions.passTermsAndConditionsDialog({ page: thePage })
})
.driveTable.clickRow(0)
@ -73,5 +73,5 @@ test.test('asset panel contents', ({ page }) =>
await test.expect(actions.locateAssetPanelDescription(page)).toHaveText(DESCRIPTION)
// `getByText` is required so that this assertion works if there are multiple permissions.
await test.expect(actions.locateAssetPanelPermissions(page).getByText(USERNAME)).toBeVisible()
})
}),
)

View File

@ -34,7 +34,7 @@ test.test('tags', async ({ page }) => {
test.test('labels', async ({ page }) => {
await actions.mockAllAndLogin({
page,
setupAPI: api => {
setupAPI: (api) => {
api.addLabel('aaaa', backend.COLORS[0])
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('bbbb', backend.COLORS[1]!)
@ -63,7 +63,7 @@ test.test('labels', async ({ page }) => {
test.test('suggestions', async ({ page }) => {
await actions.mockAllAndLogin({
page,
setupAPI: api => {
setupAPI: (api) => {
api.addDirectory('foo')
api.addProject('bar')
api.addSecret('baz')
@ -89,7 +89,7 @@ test.test('suggestions', async ({ page }) => {
test.test('suggestions (keyboard)', async ({ page }) => {
await actions.mockAllAndLogin({
page,
setupAPI: api => {
setupAPI: (api) => {
api.addDirectory('foo')
api.addProject('bar')
api.addSecret('baz')
@ -114,7 +114,7 @@ test.test('complex flows', async ({ page }) => {
await actions.mockAllAndLogin({
page,
setupAPI: api => {
setupAPI: (api) => {
api.addDirectory(firstName)
api.addProject('bar')
api.addSecret('baz')

View File

@ -10,8 +10,8 @@ test.test('extra columns should stick to right side of assets table', ({ page })
.mockAllAndLogin({ page })
.driveTable.toggleColumn.accessedByProjects()
.driveTable.toggleColumn.accessedData()
.withAssetsTable(async table => {
await table.evaluate(element => {
.withAssetsTable(async (table) => {
await table.evaluate((element) => {
let scrollableParent: HTMLElement | SVGElement | null = element
while (
scrollableParent != null &&
@ -23,27 +23,27 @@ test.test('extra columns should stick to right side of assets table', ({ page })
scrollableParent?.scrollTo({ left: 999999, behavior: 'instant' })
})
})
.do(async thePage => {
.do(async (thePage) => {
const extraColumns = actions.locateExtraColumns(thePage)
const assetsTable = actions.locateAssetsTable(thePage)
await test
.expect(async () => {
const extraColumnsRight = await extraColumns.evaluate(
element => element.getBoundingClientRect().right
(element) => element.getBoundingClientRect().right,
)
const assetsTableRight = await assetsTable.evaluate(
element => element.getBoundingClientRect().right
(element) => element.getBoundingClientRect().right,
)
test.expect(extraColumnsRight).toEqual(assetsTableRight)
})
.toPass({ timeout: PASS_TIMEOUT })
})
}),
)
test.test('extra columns should stick to top of scroll container', async ({ page }) => {
await actions.mockAllAndLogin({
page,
setupAPI: api => {
setupAPI: (api) => {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
for (let i = 0; i < 100; i += 1) {
api.addFile('a')
@ -53,7 +53,7 @@ test.test('extra columns should stick to top of scroll container', async ({ page
await actions.locateAccessedByProjectsColumnToggle(page).click()
await actions.locateAccessedDataColumnToggle(page).click()
await actions.locateAssetsTable(page).evaluate(element => {
await actions.locateAssetsTable(page).evaluate((element) => {
let scrollableParent: HTMLElement | SVGElement | null = element
while (
scrollableParent != null &&
@ -69,9 +69,9 @@ test.test('extra columns should stick to top of scroll container', async ({ page
await test
.expect(async () => {
const extraColumnsTop = await extraColumns.evaluate(
element => element.getBoundingClientRect().top
(element) => element.getBoundingClientRect().top,
)
const assetsTableTop = await assetsTable.evaluate(element => {
const assetsTableTop = await assetsTable.evaluate((element) => {
let scrollableParent: HTMLElement | SVGElement | null = element
while (
scrollableParent != null &&
@ -92,17 +92,17 @@ test.test('can drop onto root directory dropzone', ({ page }) =>
.createFolder()
.uploadFile('b', 'testing')
.driveTable.doubleClickRow(0)
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(0))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(1))
test.expect(childLeft, 'Child is indented further than parent').toBeGreaterThan(parentLeft)
})
.driveTable.dragRow(1, actions.locateRootDirectoryDropzone(page))
.driveTable.withRows(async rows => {
.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))
test.expect(firstLeft, 'Siblings have same indentation').toEqual(secondLeft)
})
}),
)

View File

@ -20,14 +20,14 @@ test.test('copy', ({ page }) =>
.driveTable.rightClickRow(1)
// Assets: [0: Folder 2, 1: Folder 1, 2: Folder 2 (copy) <child { depth=1 }>]
.contextMenu.paste()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(3)
await test.expect(rows.nth(2)).toBeVisible()
await test.expect(rows.nth(2)).toHaveText(/^New Folder 2 [(]copy[)]/)
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(1))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(2))
test.expect(childLeft, 'child is indented further than parent').toBeGreaterThan(parentLeft)
})
}),
)
test.test('copy (keyboard)', ({ page }) =>
@ -43,14 +43,14 @@ test.test('copy (keyboard)', ({ page }) =>
.driveTable.clickRow(1)
// Assets: [0: Folder 2, 1: Folder 1, 2: Folder 2 (copy) <child { depth=1 }>]
.press('Mod+V')
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(3)
await test.expect(rows.nth(2)).toBeVisible()
await test.expect(rows.nth(2)).toHaveText(/^New Folder 2 [(]copy[)]/)
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(1))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(2))
test.expect(childLeft, 'child is indented further than parent').toBeGreaterThan(parentLeft)
})
}),
)
test.test('move', ({ page }) =>
@ -66,14 +66,14 @@ test.test('move', ({ page }) =>
.driveTable.rightClickRow(1)
// Assets: [0: Folder 1, 1: Folder 2 <child { depth=1 }>]
.contextMenu.paste()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(2)
await test.expect(rows.nth(1)).toBeVisible()
await test.expect(rows.nth(1)).toHaveText(/^New Folder 2/)
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(0))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(1))
test.expect(childLeft, 'child is indented further than parent').toBeGreaterThan(parentLeft)
})
}),
)
test.test('move (drag)', ({ page }) =>
@ -85,14 +85,14 @@ test.test('move (drag)', ({ page }) =>
.createFolder()
// Assets: [0: Folder 1, 1: Folder 2 <child { depth=1 }>]
.driveTable.dragRowToRow(0, 1)
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(2)
await test.expect(rows.nth(1)).toBeVisible()
await test.expect(rows.nth(1)).toHaveText(/^New Folder 2/)
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(0))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(1))
test.expect(childLeft, 'child is indented further than parent').toBeGreaterThan(parentLeft)
})
}),
)
test.test('move to trash', ({ page }) =>
@ -104,13 +104,13 @@ test.test('move to trash', ({ page }) =>
.createFolder()
// NOTE: For some reason, `react-aria-components` causes drag-n-drop to break if `Mod` is still
// held.
.withModPressed(modActions => modActions.driveTable.clickRow(0).driveTable.clickRow(1))
.withModPressed((modActions) => modActions.driveTable.clickRow(0).driveTable.clickRow(1))
.driveTable.dragRow(0, actions.locateTrashCategory(page))
.driveTable.expectPlaceholderRow()
.goToCategory.trash()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveText([/^New Folder 1/, /^New Folder 2/])
})
}),
)
test.test('move (keyboard)', ({ page }) =>
@ -126,14 +126,14 @@ test.test('move (keyboard)', ({ page }) =>
.driveTable.clickRow(1)
// Assets: [0: Folder 1, 1: Folder 2 <child { depth=1 }>]
.press('Mod+V')
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(2)
await test.expect(rows.nth(1)).toBeVisible()
await test.expect(rows.nth(1)).toHaveText(/^New Folder 2/)
const parentLeft = await actions.getAssetRowLeftPx(rows.nth(0))
const childLeft = await actions.getAssetRowLeftPx(rows.nth(1))
test.expect(childLeft, 'child is indented further than parent').toBeGreaterThan(parentLeft)
})
}),
)
test.test('cut (keyboard)', async ({ page }) =>
@ -142,16 +142,16 @@ test.test('cut (keyboard)', async ({ page }) =>
.createFolder()
.driveTable.clickRow(0)
.press('Mod+X')
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
// This action is not a builtin `expect` action, so it needs to be manually retried.
await test
.expect(async () => {
test
.expect(await rows.nth(0).evaluate(el => Number(getComputedStyle(el).opacity)))
.expect(await rows.nth(0).evaluate((el) => Number(getComputedStyle(el).opacity)))
.toBeLessThan(1)
})
.toPass()
})
}),
)
test.test('duplicate', ({ page }) =>
@ -163,13 +163,13 @@ test.test('duplicate', ({ page }) =>
.goToPage.drive()
.driveTable.rightClickRow(0)
.contextMenu.duplicate()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
// Assets: [0: New Project 1 (copy), 1: New Project 1]
await test.expect(rows).toHaveCount(2)
await test.expect(actions.locateContextMenus(page)).not.toBeVisible()
await test.expect(rows.nth(0)).toBeVisible()
await test.expect(rows.nth(0)).toHaveText(/^New Project 1 [(]copy[)]/)
})
}),
)
test.test('duplicate (keyboard)', ({ page }) =>
@ -181,10 +181,10 @@ test.test('duplicate (keyboard)', ({ page }) =>
.goToPage.drive()
.driveTable.clickRow(0)
.press('Mod+D')
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
// Assets: [0: New Project 1 (copy), 1: New Project 1]
await test.expect(rows).toHaveCount(2)
await test.expect(rows.nth(0)).toBeVisible()
await test.expect(rows.nth(0)).toHaveText(/^New Project 1 [(]copy[)]/)
})
}),
)

View File

@ -24,40 +24,40 @@ test.test('create folder', ({ page }) =>
actions
.mockAllAndLogin({ page })
.createFolder()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(1)
await test.expect(rows.nth(0)).toBeVisible()
await test.expect(rows.nth(0)).toHaveText(/^New Folder 1/)
})
}),
)
test.test('create project', ({ page }) =>
actions
.mockAllAndLogin({ page })
.newEmptyProject()
.do(thePage => test.expect(actions.locateEditor(thePage)).toBeAttached())
.do((thePage) => test.expect(actions.locateEditor(thePage)).toBeAttached())
.goToPage.drive()
.driveTable.withRows(rows => test.expect(rows).toHaveCount(1))
.driveTable.withRows((rows) => test.expect(rows).toHaveCount(1)),
)
test.test('upload file', ({ page }) =>
actions
.mockAllAndLogin({ page })
.uploadFile(FILE_NAME, FILE_CONTENTS)
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(1)
await test.expect(rows.nth(0)).toBeVisible()
await test.expect(rows.nth(0)).toHaveText(new RegExp('^' + FILE_NAME))
})
}),
)
test.test('create secret', ({ page }) =>
actions
.mockAllAndLogin({ page })
.createSecret(SECRET_NAME, SECRET_VALUE)
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(1)
await test.expect(rows.nth(0)).toBeVisible()
await test.expect(rows.nth(0)).toHaveText(new RegExp('^' + SECRET_NAME))
})
}),
)

View File

@ -9,7 +9,7 @@ test.test('data link editor', ({ page }) =>
actions
.mockAllAndLogin({ page })
.openDataLinkModal()
.withNameInput(async input => {
.withNameInput(async (input) => {
await input.fill(DATA_LINK_NAME)
})
}),
)

View File

@ -7,44 +7,44 @@ test.test('delete and restore', ({ page }) =>
actions
.mockAllAndLogin({ page })
.createFolder()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(1)
})
.driveTable.rightClickRow(0)
.contextMenu.moveToTrash()
.driveTable.expectPlaceholderRow()
.goToCategory.trash()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(1)
})
.driveTable.rightClickRow(0)
.contextMenu.restoreFromTrash()
.driveTable.expectTrashPlaceholderRow()
.goToCategory.cloud()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(1)
})
}),
)
test.test('delete and restore (keyboard)', ({ page }) =>
actions
.mockAllAndLogin({ page })
.createFolder()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(1)
})
.driveTable.clickRow(0)
.press('Delete')
.driveTable.expectPlaceholderRow()
.goToCategory.trash()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(1)
})
.driveTable.clickRow(0)
.press('Mod+R')
.driveTable.expectTrashPlaceholderRow()
.goToCategory.cloud()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(1)
})
}),
)

View File

@ -6,7 +6,7 @@ import * as actions from './actions'
test.test('drive view', ({ page }) =>
actions
.mockAllAndLogin({ page })
.withDriveView(async view => {
.withDriveView(async (view) => {
await test.expect(view).toBeVisible()
})
.driveTable.expectPlaceholderRow()
@ -15,7 +15,7 @@ test.test('drive view', ({ page }) =>
await test.expect(actions.locateEditor(page)).toBeAttached()
})
.goToPage.drive()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(1)
})
.do(async () => {
@ -26,19 +26,19 @@ test.test('drive view', ({ page }) =>
await test.expect(actions.locateEditor(page)).toBeAttached()
})
.goToPage.drive()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(2)
})
// The last opened project needs to be stopped, to remove the toast notification notifying the
// user that project creation may take a while. Previously opened projects are stopped when the
// new project is created.
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await actions.locateStopProjectButton(rows.nth(0)).click()
})
// Project context menu
.driveTable.rightClickRow(0)
.contextMenu.moveToTrash()
.driveTable.withRows(async rows => {
.driveTable.withRows(async (rows) => {
await test.expect(rows).toHaveCount(1)
})
}),
)

View File

@ -9,7 +9,7 @@ test.test('drag labels onto single row', async ({ page }) => {
const label = 'aaaa'
await actions.mockAllAndLogin({
page,
setupAPI: api => {
setupAPI: (api) => {
api.addLabel(label, backend.COLORS[0])
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('bbbb', backend.COLORS[1]!)
@ -39,7 +39,7 @@ test.test('drag labels onto multiple rows', async ({ page }) => {
const label = 'aaaa'
await actions.mockAllAndLogin({
page,
setupAPI: api => {
setupAPI: (api) => {
api.addLabel(label, backend.COLORS[0])
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
api.addLabel('bbbb', backend.COLORS[1]!)

View File

@ -11,15 +11,15 @@ test.test('login and logout', ({ page }) =>
actions
.mockAll({ page })
.login()
.do(async thePage => {
.do(async (thePage) => {
await actions.passTermsAndConditionsDialog({ page: thePage })
await test.expect(actions.locateDriveView(thePage)).toBeVisible()
await test.expect(actions.locateLoginButton(thePage)).not.toBeVisible()
})
.openUserMenu()
.userMenu.logout()
.do(async thePage => {
.do(async (thePage) => {
await test.expect(actions.locateDriveView(thePage)).not.toBeVisible()
await test.expect(actions.locateLoginButton(thePage)).toBeVisible()
})
}),
)

View File

@ -17,7 +17,7 @@ test.test('login screen', async ({ page }) => {
test
.expect(
await page.evaluate(() => document.querySelector('form')?.checkValidity()),
'form should reject invalid email'
'form should reject invalid email',
)
.toBe(false)
await actions.locateLoginButton(page).click()
@ -28,7 +28,7 @@ test.test('login screen', async ({ page }) => {
test
.expect(
await page.evaluate(() => document.querySelector('form')?.checkValidity()),
'form should accept invalid password'
'form should accept invalid password',
)
.toBe(true)
await actions.locateLoginButton(page).click()

View File

@ -9,13 +9,13 @@ test.test('page switcher', ({ page }) =>
// Create a new project so that the editor page can be switched to.
.newEmptyProject()
.goToPage.drive()
.do(async thePage => {
.do(async (thePage) => {
await test.expect(actions.locateDriveView(thePage)).toBeVisible()
await test.expect(actions.locateEditor(thePage)).not.toBeVisible()
})
.goToPage.editor()
.do(async thePage => {
.do(async (thePage) => {
await test.expect(actions.locateDriveView(thePage)).not.toBeVisible()
await test.expect(actions.locateEditor(thePage)).toBeVisible()
})
}),
)

View File

@ -22,7 +22,7 @@ test.test('sign up with organization id', async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('domcontentloaded')
await page.goto(
'/registration?' + new URLSearchParams([['organization_id', ORGANIZATION_ID]]).toString()
'/registration?' + new URLSearchParams([['organization_id', ORGANIZATION_ID]]).toString(),
)
const api = await actions.mockApi({ page })
api.setCurrentUser(null)
@ -81,7 +81,7 @@ test.test('sign up flow', ({ page }) => {
return actions
.mockAll({
page,
setupAPI: theApi => {
setupAPI: (theApi) => {
api = theApi
theApi.setCurrentUser(null)
@ -91,11 +91,11 @@ test.test('sign up flow', ({ page }) => {
},
})
.loginAsNewUser(EMAIL, actions.VALID_PASSWORD)
.do(async thePage => {
.do(async (thePage) => {
await actions.passTermsAndConditionsDialog({ page: thePage })
})
.setUsername(NAME)
.do(async thePage => {
.do(async (thePage) => {
await test.expect(actions.locateUpgradeButton(thePage)).toBeVisible()
await test.expect(actions.locateDriveView(thePage)).not.toBeVisible()
})

View File

@ -22,7 +22,7 @@ const MIN_MS = 60_000
test.test('sort', async ({ page }) => {
await actions.mockAll({
page,
setupAPI: api => {
setupAPI: (api) => {
const date1 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS))
const date2 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 1 * MIN_MS))
const date3 = dateTime.toRfc3339(new Date(START_DATE_EPOCH_MS + 2 * MIN_MS))

View File

@ -8,8 +8,8 @@ test.test('create project from template', ({ page }) =>
.mockAllAndLogin({ page })
.openStartModal()
.createProjectFromTemplate(0)
.do(async thePage => {
.do(async (thePage) => {
await test.expect(actions.locateEditor(thePage)).toBeAttached()
await test.expect(actions.locateSamples(page).first()).not.toBeVisible()
})
}),
)

View File

@ -7,17 +7,17 @@ test.test('user menu', ({ page }) =>
actions
.mockAllAndLogin({ page })
.openUserMenu()
.do(async thePage => {
.do(async (thePage) => {
await test.expect(actions.locateUserMenu(thePage)).toBeVisible()
})
}),
)
test.test('download app', ({ page }) =>
actions
.mockAllAndLogin({ page })
.openUserMenu()
.userMenu.downloadApp(async download => {
.userMenu.downloadApp(async (download) => {
await download.cancel()
test.expect(download.url()).toMatch(/^https:[/][/]objects.githubusercontent.com/)
})
}),
)

View File

@ -38,7 +38,7 @@ test.test('change password form', async ({ page }) => {
await localActions
.locateNewPasswordInput(page)
.evaluate((element: HTMLInputElement) => element.validity.valid),
'invalid new password should be rejected'
'invalid new password should be rejected',
)
.toBe(false)
await test
@ -55,13 +55,13 @@ test.test('change password form', async ({ page }) => {
await localActions
.locateConfirmNewPasswordInput(page)
.evaluate((element: HTMLInputElement) => element.validity.valid),
'invalid new password confirmation should be rejected'
'invalid new password confirmation should be rejected',
)
.toBe(false)
await test
.expect(
localActions.locateChangeButton(page),
'invalid new password confirmation should be rejected'
'invalid new password confirmation should be rejected',
)
.toBeDisabled()
})

View File

@ -41,8 +41,6 @@
<div id="enso-dashboard" class="enso-dashboard"></div>
<div id="enso-chat" class="enso-chat"></div>
<div id="enso-portal-root" class="enso-portal-root"></div>
<noscript>
This page requires JavaScript to run. Please enable it in your browser.
</noscript>
<noscript> This page requires JavaScript to run. Please enable it in your browser. </noscript>
</body>
</html>

View File

@ -33,12 +33,12 @@ export default test.defineConfig({
launchOptions: {
ignoreDefaultArgs: ['--headless'],
args: [
...(DEBUG
? []
: [
// Much closer to headful Chromium than classic headless.
'--headless=new',
]),
...(DEBUG ?
[]
: [
// Much closer to headful Chromium than classic headless.
'--headless=new',
]),
// Required for `backdrop-filter: blur` to work.
'--use-angle=swiftshader',
// FIXME: `--disable-gpu` disables `backdrop-filter: blur`, which is not handled by

View File

@ -53,8 +53,8 @@ import DevtoolsProvider from '#/providers/EnsoDevtoolsProvider'
import * as httpClientProvider from '#/providers/HttpClientProvider'
import InputBindingsProvider from '#/providers/InputBindingsProvider'
import LocalStorageProvider, * as localStorageProvider from '#/providers/LocalStorageProvider'
import LoggerProvider from '#/providers/LoggerProvider'
import type * as loggerProvider from '#/providers/LoggerProvider'
import LoggerProvider from '#/providers/LoggerProvider'
import ModalProvider, * as modalProvider from '#/providers/ModalProvider'
import * as navigator2DProvider from '#/providers/Navigator2DProvider'
import SessionProvider from '#/providers/SessionProvider'
@ -86,8 +86,7 @@ import * as setOrganizationNameModal from '#/modals/SetOrganizationNameModal'
import * as termsOfServiceModal from '#/modals/TermsOfServiceModal'
import LocalBackend from '#/services/LocalBackend'
import * as projectManager from '#/services/ProjectManager'
import ProjectManager from '#/services/ProjectManager'
import ProjectManager, * as projectManager from '#/services/ProjectManager'
import RemoteBackend from '#/services/RemoteBackend'
import * as appBaseUrl from '#/utilities/appBaseUrl'
@ -110,17 +109,17 @@ declare module '#/utilities/LocalStorage' {
}
LocalStorage.registerKey('inputBindings', {
tryParse: value =>
typeof value !== 'object' || value == null
? null
: Object.fromEntries(
Object.entries<unknown>({ ...value }).flatMap(kv => {
const [k, v] = kv
return Array.isArray(v) && v.every((item): item is string => typeof item === 'string')
? [[k, v]]
: []
})
),
tryParse: (value) =>
typeof value !== 'object' || value == null ?
null
: Object.fromEntries(
Object.entries<unknown>({ ...value }).flatMap((kv) => {
const [k, v] = kv
return Array.isArray(v) && v.every((item): item is string => typeof item === 'string') ?
[[k, v]]
: []
}),
),
})
// ======================
@ -276,12 +275,12 @@ function AppRouter(props: AppRouterProps) {
const localBackend = React.useMemo(
() => (projectManagerInstance != null ? new LocalBackend(projectManagerInstance) : null),
[projectManagerInstance]
[projectManagerInstance],
)
const remoteBackend = React.useMemo(
() => new RemoteBackend(httpClient, logger, getText),
[httpClient, logger, getText]
[httpClient, logger, getText],
)
if (detect.IS_DEV_MODE) {
@ -296,7 +295,7 @@ function AppRouter(props: AppRouterProps) {
if (savedInputBindings != null) {
const filteredInputBindings = object.mapEntries(
inputBindingsRaw.metadata,
k => savedInputBindings[k]
(k) => savedInputBindings[k],
)
for (const [bindingKey, newBindings] of object.unsafeEntries(filteredInputBindings)) {
for (const oldBinding of inputBindingsRaw.metadata[bindingKey].bindings) {
@ -314,11 +313,11 @@ function AppRouter(props: AppRouterProps) {
localStorage.set(
'inputBindings',
Object.fromEntries(
Object.entries(inputBindingsRaw.metadata).map(kv => {
Object.entries(inputBindingsRaw.metadata).map((kv) => {
const [k, v] = kv
return [k, v.bindings]
})
)
}),
),
)
}
return {
@ -438,11 +437,11 @@ function AppRouter(props: AppRouterProps) {
<router.Route element={<authProvider.ProtectedLayout />}>
<router.Route
element={
detect.IS_DEV_MODE ? (
detect.IS_DEV_MODE ?
<devtools.EnsoDevtools>
<router.Outlet />
</devtools.EnsoDevtools>
) : null
: null
}
>
<router.Route element={<termsOfServiceModal.TermsOfServiceModal />}>

View File

@ -31,7 +31,7 @@ export const SUBSCRIBE_SUCCESS_PATH = '/subscribe/success'
export const ALL_PATHS_REGEX = new RegExp(
`(?:${DASHBOARD_PATH}|${LOGIN_PATH}|${REGISTRATION_PATH}|${CONFIRM_REGISTRATION_PATH}|` +
`${FORGOT_PASSWORD_PATH}|${RESET_PASSWORD_PATH}|${SET_USERNAME_PATH}|${RESTORE_USER_PATH}|` +
`${SUBSCRIBE_PATH}|${SUBSCRIBE_SUCCESS_PATH})$`
`${SUBSCRIBE_PATH}|${SUBSCRIBE_SUCCESS_PATH})$`,
)
// === Constants related to URLs ===

View File

@ -81,7 +81,7 @@ export class Cognito {
constructor(
private readonly logger: loggerProvider.Logger,
private readonly supportsDeepLinks: boolean,
private readonly amplifyConfig: service.AmplifyConfig
private readonly amplifyConfig: service.AmplifyConfig,
) {}
/** Save the access token to a file for further reuse. */
@ -137,7 +137,7 @@ export class Cognito {
jti: '5ab178b7-97a6-4956-8913-1cffee4a0da1',
username: mockEmail,
/* eslint-enable @typescript-eslint/naming-convention */
})
}),
)}.`,
}),
})
@ -269,7 +269,7 @@ export class Cognito {
fetch('https://mock-cognito.com/change-password', {
method: 'POST',
body: JSON.stringify({ oldPassword, newPassword }),
})
}),
)
return result.mapErr(original.intoAmplifyErrorOrThrow)
} else {
@ -329,7 +329,7 @@ async function signUp(
_supportsDeepLinks: boolean,
_username: string,
_password: string,
_organizationId: string | null
_organizationId: string | null,
) {
const result = await results.Result.wrapAsync(async () => {
// Ignored.
@ -346,8 +346,8 @@ async function signUp(
async function confirmSignUp(_email: string, _code: string) {
return results.Result.wrapAsync(async () => {
// Ignored.
}).then(result =>
result.mapErr(original.intoAmplifyErrorOrThrow).mapErr(original.intoConfirmSignUpErrorOrThrow)
}).then((result) =>
result.mapErr(original.intoAmplifyErrorOrThrow).mapErr(original.intoConfirmSignUpErrorOrThrow),
)
}
@ -361,7 +361,7 @@ async function currentAuthenticatedUser() {
const result = await results.Result.wrapAsync(
// The methods are not needed.
// eslint-disable-next-line no-restricted-syntax
async () => await Promise.resolve<amplify.CognitoUser>({} as unknown as amplify.CognitoUser)
async () => await Promise.resolve<amplify.CognitoUser>({} as unknown as amplify.CognitoUser),
)
return result.mapErr(original.intoAmplifyErrorOrThrow)
}

View File

@ -178,7 +178,7 @@ export class Cognito {
constructor(
private readonly logger: loggerProvider.Logger,
private readonly supportsDeepLinks: boolean,
private readonly amplifyConfig: service.AmplifyConfig
private readonly amplifyConfig: service.AmplifyConfig,
) {
/** Amplify expects `Auth.configure` to be called before any other `Auth` methods are
* called. By wrapping all the `Auth` methods we care about and returning an `Cognito` API
@ -201,7 +201,7 @@ export class Cognito {
const amplifySession = currentSession.mapErr(intoCurrentSessionErrorType)
return amplifySession
.map(session => parseUserSession(session, this.amplifyConfig.userPoolWebClientId))
.map((session) => parseUserSession(session, this.amplifyConfig.userPoolWebClientId))
.unwrapOr(null)
}
@ -296,7 +296,7 @@ export class Cognito {
})
return result
.map(session => parseUserSession(session, this.amplifyConfig.userPoolWebClientId))
.map((session) => parseUserSession(session, this.amplifyConfig.userPoolWebClientId))
.unwrapOr(null)
}
@ -476,7 +476,7 @@ function intoSignUpParams(
supportsDeepLinks: boolean,
username: string,
password: string,
organizationId: string | null
organizationId: string | null,
): amplify.SignUpParams {
return {
username,
@ -703,7 +703,7 @@ async function currentAuthenticatedUser() {
* Therefore, it is necessary to use `as` to narrow down the type to
* `Promise<CognitoUser>`. */
// eslint-disable-next-line no-restricted-syntax
() => amplify.Auth.currentAuthenticatedUser() as Promise<amplify.CognitoUser>
() => amplify.Auth.currentAuthenticatedUser() as Promise<amplify.CognitoUser>,
)
return result.mapErr(intoAmplifyErrorOrThrow)
}

View File

@ -56,7 +56,7 @@ export type ListenFunction = (listener: ListenerCallback) => UnsubscribeFunction
/** Listen to authentication state changes. */
export function registerAuthEventListener(listener: ListenerCallback) {
return amplify.Hub.listen(AUTHENTICATION_HUB, data => {
return amplify.Hub.listen(AUTHENTICATION_HUB, (data) => {
if (isAuthEvent(data.payload.event)) {
listener(data.payload.event, data.payload.data)
}

View File

@ -124,20 +124,20 @@ 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)
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 }
)
}
/** Return the appropriate Amplify configuration for the current platform. */
function loadAmplifyConfig(
logger: loggerProvider.Logger,
supportsDeepLinks: boolean,
navigate: (url: string) => void
navigate: (url: string) => void,
): AmplifyConfig | null {
let urlOpener: ((url: string) => void) | null = null
let saveAccessToken: ((accessToken: saveAccessTokenModule.AccessToken | null) => void) | null =
@ -175,11 +175,13 @@ function loadAmplifyConfig(
/** Load the platform-specific Amplify configuration. */
const signInOutRedirect = supportsDeepLinks ? `${common.DEEP_LINK_SCHEME}://auth` : redirectUrl
return process.env.ENSO_CLOUD_COGNITO_USER_POOL_ID == null ||
process.env.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID == null ||
process.env.ENSO_CLOUD_COGNITO_DOMAIN == null ||
process.env.ENSO_CLOUD_COGNITO_REGION == null
? null
return (
process.env.ENSO_CLOUD_COGNITO_USER_POOL_ID == null ||
process.env.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID == null ||
process.env.ENSO_CLOUD_COGNITO_DOMAIN == null ||
process.env.ENSO_CLOUD_COGNITO_REGION == null
) ?
null
: {
userPoolId: process.env.ENSO_CLOUD_COGNITO_USER_POOL_ID,
userPoolWebClientId: process.env.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID,

View File

@ -58,7 +58,7 @@ export interface AlertProps
/** Alert component. */
export const Alert = React.forwardRef(function Alert(
props: AlertProps,
ref: React.ForwardedRef<HTMLDivElement>
ref: React.ForwardedRef<HTMLDivElement>,
) {
const { children, className, variant, size, rounded, fullWidth, ...containerProps } = props
@ -70,7 +70,7 @@ export const Alert = React.forwardRef(function Alert(
return (
<div
className={ALERT_STYLES({ variant, size, className, rounded, fullWidth })}
ref={mergeRefs.mergeRefs(ref, e => {
ref={mergeRefs.mergeRefs(ref, (e) => {
if (variant === 'error') {
e?.focus()
}

View File

@ -280,7 +280,7 @@ export const BUTTON_STYLES = twv.tv({
/** A button allows a user to perform an action, with mouse, touch, and keyboard interactions. */
export const Button = React.forwardRef(function Button(
props: ButtonProps,
ref: React.ForwardedRef<HTMLButtonElement>
ref: React.ForwardedRef<HTMLButtonElement>,
) {
const {
className,
@ -341,17 +341,17 @@ export const Button = React.forwardRef(function Button(
if (isLoading) {
const loaderAnimation = loaderRef.current?.animate(
[{ opacity: 0 }, { opacity: 0, offset: 1 }, { opacity: 1 }],
{ duration: delay, easing: 'linear', delay: 0, fill: 'forwards' }
{ duration: delay, easing: 'linear', delay: 0, fill: 'forwards' },
)
const contentAnimation =
loaderPosition !== 'full'
? null
: contentRef.current?.animate([{ opacity: 1 }, { opacity: 0 }], {
duration: 0,
easing: 'linear',
delay,
fill: 'forwards',
})
loaderPosition !== 'full' ? null : (
contentRef.current?.animate([{ opacity: 1 }, { opacity: 0 }], {
duration: 0,
easing: 'linear',
delay,
fill: 'forwards',
})
)
return () => {
loaderAnimation?.cancel()
@ -399,7 +399,7 @@ export const Button = React.forwardRef(function Button(
})
const childrenFactory = (
render: aria.ButtonRenderProps | aria.LinkRenderProps
render: aria.ButtonRenderProps | aria.LinkRenderProps,
): React.ReactNode => {
const iconComponent = (() => {
if (icon == null) {
@ -447,12 +447,12 @@ export const Button = React.forwardRef(function Button(
// onPress on EXTRA_CLICK_ZONE, but onPress{start,end} are triggered
onPressEnd: handlePress,
className: aria.composeRenderProps(className, (classNames, states) =>
base({ className: classNames, ...states })
base({ className: classNames, ...states }),
),
})}
>
{/* @ts-expect-error any here is safe because we transparently pass it to the children, and ts infer the type outside correctly */}
{render => (
{(render) => (
<>
<span className={wrapper()}>
<span ref={contentRef} className={content({ className: contentClassName })}>
@ -471,17 +471,15 @@ export const Button = React.forwardRef(function Button(
</Tag>
)
return tooltipElement == null ? (
button
) : (
<ariaComponents.TooltipTrigger delay={0} closeDelay={0}>
{button}
return tooltipElement == null ? button : (
<ariaComponents.TooltipTrigger delay={0} closeDelay={0}>
{button}
<ariaComponents.Tooltip
{...(tooltipPlacement != null ? { placement: tooltipPlacement } : {})}
>
{tooltipElement}
</ariaComponents.Tooltip>
</ariaComponents.TooltipTrigger>
)
<ariaComponents.Tooltip
{...(tooltipPlacement != null ? { placement: tooltipPlacement } : {})}
>
{tooltipElement}
</ariaComponents.Tooltip>
</ariaComponents.TooltipTrigger>
)
})

View File

@ -34,12 +34,12 @@ export function CloseButton(props: CloseButtonProps) {
return (
<button.Button
variant="icon"
className={values =>
className={(values) =>
tailwindMerge.twMerge(
'bg-primary/30 hover:bg-red-500/80 focus-visible:bg-red-500/80 focus-visible:outline-offset-1',
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
// @ts-expect-error ts fails to infer the type of the className prop
typeof className === 'function' ? className(values) : className
typeof className === 'function' ? className(values) : className,
)
}
tooltip={tooltip}

View File

@ -46,7 +46,12 @@ export function CopyButton(props: CopyButtonProps) {
const successfullyCopied = copyQuery.isSuccess
const isError = copyQuery.isError
const showIcon = copyIcon !== false
const icon = showIcon ? (isError ? errorIcon : successfullyCopied ? successIcon : copyIcon) : null
const icon =
showIcon ?
isError ? errorIcon
: successfullyCopied ? successIcon
: copyIcon
: null
return (
<button.Button

View File

@ -1,5 +1,5 @@
/** @file Barrel export file for Button component. */
export * from './Button'
export * from './ButtonGroup'
export * from './CopyButton'
export * from './CloseButton'
export * from './CopyButton'

View File

@ -27,7 +27,7 @@ export function Close(props: CloseProps) {
const onPressCallback = eventCallback.useEventCallback<
NonNullable<button.ButtonProps['onPress']>
>(event => {
>((event) => {
dialogContext.close()
return props.onPress?.(event)
})

View File

@ -138,7 +138,7 @@ export function Dialog(props: DialogProps) {
const duration = 200 // 200ms
dialogRef.current?.animate(
[{ transform: 'scale(1)' }, { transform: 'scale(1.015)' }, { transform: 'scale(1)' }],
{ duration, iterations: 1, direction: 'alternate' }
{ duration, iterations: 1, direction: 'alternate' },
)
}
},
@ -154,7 +154,7 @@ export function Dialog(props: DialogProps) {
shouldCloseOnInteractOutside={() => false}
{...modalProps}
>
{values => {
{(values) => {
overlayState.current = values.state
return (
@ -173,7 +173,7 @@ export function Dialog(props: DialogProps) {
>
<aria.Dialog
id={dialogId}
ref={mergeRefs.mergeRefs(dialogRef, element => {
ref={mergeRefs.mergeRefs(dialogRef, (element) => {
if (element) {
// This is a workaround for the `data-testid` attribute not being
// supported by the 'react-aria-components' library.
@ -189,7 +189,7 @@ export function Dialog(props: DialogProps) {
className={dialogSlots.base()}
{...ariaDialogProps}
>
{opts => {
{(opts) => {
return (
<dialogProvider.DialogProvider value={{ close: opts.close, dialogId }}>
<aria.Header
@ -213,13 +213,13 @@ export function Dialog(props: DialogProps) {
</aria.Header>
<div
ref={ref => {
ref={(ref) => {
if (ref) {
handleScroll(ref.scrollTop)
}
}}
className={dialogSlots.content()}
onScroll={event => {
onScroll={(event) => {
handleScroll(event.currentTarget.scrollTop)
}}
>

View File

@ -37,11 +37,11 @@ export function DialogStackProvider(props: React.PropsWithChildren) {
const [stack, setStack] = React.useState<DialogStackItem[]>([])
const addToStack = eventCallbackHooks.useEventCallback((item: DialogStackItem) => {
setStack(currentStack => [...currentStack, item])
setStack((currentStack) => [...currentStack, item])
})
const sliceFromStack = eventCallbackHooks.useEventCallback((currentId: string) => {
setStack(currentStack => {
setStack((currentStack) => {
const lastItem = currentStack.at(-1)
if (lastItem?.id === currentId) {
@ -62,11 +62,11 @@ updated properly.`)
const value = React.useMemo<DialogStackContextType>(
() => ({
stack,
dialogsStack: stack.filter(item => ['dialog-fullscreen', 'dialog'].includes(item.type)),
dialogsStack: stack.filter((item) => ['dialog-fullscreen', 'dialog'].includes(item.type)),
add: addToStack,
slice: sliceFromStack,
}),
[stack, addToStack, sliceFromStack]
[stack, addToStack, sliceFromStack],
)
return <DialogStackContext.Provider value={value}>{children}</DialogStackContext.Provider>
@ -118,7 +118,7 @@ export function useDialogStackState(props: UseDialogStackStateProps) {
const { id } = props
const isLatest = ctx.stack.at(-1)?.id === id
const index = ctx.stack.findIndex(item => item.id === id)
const index = ctx.stack.findIndex((item) => item.id === id)
return { isLatest, index }
}

View File

@ -32,7 +32,7 @@ export function DialogTrigger(props: DialogTriggerProps) {
onOpenChange?.(isOpened)
},
[setModal, unsetModal, onOpenChange]
[setModal, unsetModal, onOpenChange],
)
return (

View File

@ -90,7 +90,7 @@ export function Popover(props: PopoverProps) {
return (
<aria.Popover
className={values =>
className={(values) =>
POPOVER_STYLES({
...values,
size,
@ -104,7 +104,7 @@ export function Popover(props: PopoverProps) {
shouldCloseOnInteractOutside={() => false}
{...ariaPopoverProps}
>
{opts => (
{(opts) => (
<dialogStackProvider.DialogStackRegistrar id={dialogId} type="popover">
<aria.Dialog
id={dialogId}

View File

@ -3,10 +3,10 @@
*
* Re-exports the Dialog component.
*/
export * from './Close'
export * from './Dialog'
export * from './DialogTrigger'
export * from './Popover'
export * from './Close'
export * from './variants'
// eslint-disable-next-line no-restricted-syntax
export { useDialogContext, type DialogContextValue } from './DialogProvider'
@ -14,7 +14,7 @@ export {
// eslint-disable-next-line no-restricted-syntax
DialogStackProvider,
// eslint-disable-next-line no-restricted-syntax
type DialogStackItem,
// eslint-disable-next-line no-restricted-syntax
type DialogStackContextType,
// eslint-disable-next-line no-restricted-syntax
type DialogStackItem,
} from './DialogStackProvider'

View File

@ -31,7 +31,7 @@ export const Form = React.forwardRef(function Form<
TTransformedValues extends components.FieldValues<Schema> | undefined = undefined,
>(
props: types.FormProps<Schema, TFieldValues, TTransformedValues>,
ref: React.Ref<HTMLFormElement>
ref: React.Ref<HTMLFormElement>,
) {
const formId = React.useId()
@ -67,7 +67,7 @@ export const Form = React.forwardRef(function Form<
shouldFocusError: true,
schema,
...formOptions,
}
},
)
const dialogContext = dialog.useDialogContext()
@ -96,8 +96,9 @@ export const Form = React.forwardRef(function Form<
})
}
const message = isJSError
? getText('arbitraryFormErrorMessage')
const message =
isJSError ?
getText('arbitraryFormErrorMessage')
: errorUtils.tryGetMessage(error, getText('arbitraryFormErrorMessage'))
innerForm.setError('root.submit', { message })
@ -120,14 +121,14 @@ export const Form = React.forwardRef(function Form<
const { isOffline } = offlineHooks.useOffline()
offlineHooks.useOfflineChange(
offline => {
(offline) => {
if (offline) {
innerForm.setError('root.offline', { message: getText('unavailableOffline') })
} else {
innerForm.clearErrors('root.offline')
}
},
{ isDisabled: canSubmitOffline }
{ isDisabled: canSubmitOffline },
)
const {
@ -160,10 +161,10 @@ export const Form = React.forwardRef(function Form<
}
}
const onChange: types.UseFormRegisterReturn<Schema, TFieldValues>['onChange'] = value =>
const onChange: types.UseFormRegisterReturn<Schema, TFieldValues>['onChange'] = (value) =>
registered.onChange(mapValueOnEvent(value))
const onBlur: types.UseFormRegisterReturn<Schema, TFieldValues>['onBlur'] = value =>
const onBlur: types.UseFormRegisterReturn<Schema, TFieldValues>['onBlur'] = (value) =>
registered.onBlur(mapValueOnEvent(value))
const result: types.UseFormRegisterReturn<Schema, TFieldValues, typeof name> = {
@ -198,14 +199,14 @@ export const Form = React.forwardRef(function Form<
Object.entries(formState.errors).map(([key, error]) => {
const message = error?.message ?? getText('arbitraryFormErrorMessage')
return [key, message]
})
}),
) as Record<keyof TFieldValues, string>
return (
<form
id={id}
ref={ref}
onSubmit={event => {
onSubmit={(event) => {
event.preventDefault()
event.stopPropagation()
@ -234,7 +235,7 @@ export const Form = React.forwardRef(function Form<
TTransformedValues extends components.FieldValues<Schema> | undefined = undefined,
>(
props: React.RefAttributes<HTMLFormElement> &
types.FormProps<Schema, TFieldValues, TTransformedValues>
types.FormProps<Schema, TFieldValues, TTransformedValues>,
// eslint-disable-next-line no-restricted-syntax
) => React.JSX.Element) & {
/* eslint-disable @typescript-eslint/naming-convention */

View File

@ -60,7 +60,7 @@ export const FIELD_STYLES = twv.tv({
*/
export const Field = React.forwardRef(function Field(
props: FieldComponentProps,
ref: React.ForwardedRef<HTMLFieldSetElement>
ref: React.ForwardedRef<HTMLFieldSetElement>,
) {
const {
form = formContext.useFormContext(),
@ -110,15 +110,15 @@ export const Field = React.forwardRef(function Field(
)}
<div className={classes.content()}>
{typeof children === 'function'
? children({
isInvalid: invalid,
isDirty: fieldState.isDirty,
isTouched: fieldState.isTouched,
isValidating: fieldState.isValidating,
error: fieldState.error?.message,
})
: children}
{typeof children === 'function' ?
children({
isInvalid: invalid,
isDirty: fieldState.isDirty,
isTouched: fieldState.isTouched,
isValidating: fieldState.isValidating,
error: fieldState.error?.message,
})
: children}
</div>
</aria.Label>

View File

@ -63,28 +63,28 @@ export function FormError(props: FormErrorProps) {
const errorMessage = getSubmitError()
const submitErrorAlert =
errorMessage != null ? (
errorMessage != null ?
<reactAriaComponents.Alert size={size} variant={variant} rounded={rounded} {...alertProps}>
<reactAriaComponents.Text variant="body" truncate="3" color="primary">
{errorMessage}
</reactAriaComponents.Text>
</reactAriaComponents.Alert>
) : null
: null
const offlineErrorAlert =
offlineMessage != null ? (
offlineMessage != null ?
<reactAriaComponents.Alert size={size} variant="outline" rounded={rounded} {...alertProps}>
<reactAriaComponents.Text variant="body" truncate="3" color="primary">
{offlineMessage}
</reactAriaComponents.Text>
</reactAriaComponents.Alert>
) : null
: null
const hasSomethingToShow = submitErrorAlert || offlineErrorAlert
return hasSomethingToShow ? (
<div className="flex w-full flex-col gap-4">
{submitErrorAlert} {offlineErrorAlert}
</div>
) : null
return hasSomethingToShow ?
<div className="flex w-full flex-col gap-4">
{submitErrorAlert} {offlineErrorAlert}
</div>
: null
}

View File

@ -3,12 +3,12 @@
*
* Barrel file for form components.
*/
export * from './Submit'
export * from './Reset'
export * from './useForm'
export * from './FormError'
export * from './types'
export * from './useFormSchema'
export * from './schema'
export * from './useField'
export * from './Field'
export * from './FormError'
export * from './Reset'
export * from './schema'
export * from './Submit'
export * from './types'
export * from './useField'
export * from './useForm'
export * from './useFormSchema'

View File

@ -15,9 +15,8 @@ import type * as schemaModule from './schema'
* Field values type.
*/
// eslint-disable-next-line no-restricted-syntax
export type FieldValues<Schema extends TSchema | undefined> = Schema extends z.AnyZodObject
? z.infer<Schema>
: reactHookForm.FieldValues
export type FieldValues<Schema extends TSchema | undefined> =
Schema extends z.AnyZodObject ? z.infer<Schema> : reactHookForm.FieldValues
/**
* Field path type.
@ -84,9 +83,9 @@ export interface FormWithValueValidation<
TTransformedValues extends FieldValues<Schema> | undefined = undefined,
> {
readonly form?:
| (BaseValueType extends TFieldValues[TFieldName]
? FormInstance<Schema, TFieldValues, TTransformedValues>
: 'Type mismatch: Field with this name has a different type than the value of the component.')
| (BaseValueType extends TFieldValues[TFieldName] ?
FormInstance<Schema, TFieldValues, TTransformedValues>
: 'Type mismatch: Field with this name has a different type than the value of the component.')
| undefined
}

View File

@ -34,7 +34,7 @@ export function useForm<
>(
optionsOrFormInstance:
| types.UseFormProps<Schema, TFieldValues>
| types.UseFormReturn<Schema, TFieldValues, TTransformedValues>
| types.UseFormReturn<Schema, TFieldValues, TTransformedValues>,
): types.UseFormReturn<Schema, TFieldValues, TTransformedValues> {
const initialTypePassed = React.useRef(getArgsType(optionsOrFormInstance))
@ -45,7 +45,7 @@ export function useForm<
`
Found a switch between form options and form instance. This is not allowed. Please use either form options or form instance and stick to it.\n\n
Initially passed: ${initialTypePassed.current}, Currently passed: ${argsType}.
`
`,
)
if ('formState' in optionsOrFormInstance) {
@ -74,7 +74,7 @@ function getArgsType<
>(
args:
| types.UseFormProps<Schema, TFieldValues>
| types.UseFormReturn<Schema, TFieldValues, TTransformedValues>
| types.UseFormReturn<Schema, TFieldValues, TTransformedValues>,
) {
return 'formState' in args ? 'formInstance' : 'formOptions'
}

View File

@ -12,7 +12,7 @@ import type * as types from '#/components/AriaComponents/Form/components/types'
/** A hook to create a form schema. */
export function useFormSchema<Schema extends types.TSchema, T extends types.FieldValues<Schema>>(
callback: (schema: typeof schemaComponent.schema) => schemaComponent.schema.ZodObject<T>
callback: (schema: typeof schemaComponent.schema) => schemaComponent.schema.ZodObject<T>,
) {
const callbackEvent = callbackEventHooks.useEventCallback(callback)

View File

@ -50,12 +50,12 @@ interface BaseFormProps<
readonly defaultValues?: components.UseFormProps<Schema, TFieldValues>['defaultValues']
readonly onSubmit?: (
values: TFieldValues,
form: components.UseFormReturn<Schema, TFieldValues, TTransformedValues>
form: components.UseFormReturn<Schema, TFieldValues, TTransformedValues>,
) => unknown
readonly style?:
| React.CSSProperties
| ((
props: FormStateRenderProps<Schema, TFieldValues, TTransformedValues>
props: FormStateRenderProps<Schema, TFieldValues, TTransformedValues>,
) => React.CSSProperties)
readonly children:
| React.ReactNode
@ -123,7 +123,7 @@ export type UseFormRegister<
>,
>(
name: TFieldName,
options?: reactHookForm.RegisterOptions<TFieldValues, TFieldName>
options?: reactHookForm.RegisterOptions<TFieldValues, TFieldName>,
// eslint-disable-next-line no-restricted-syntax
) => UseFormRegisterReturn<Schema, TFieldValues, TFieldName>

View File

@ -62,7 +62,7 @@ export const ResizableContentEditableInput = React.forwardRef(
TTransformedValues extends ariaComponents.FieldValues<Schema> | undefined = undefined,
>(
props: ResizableContentEditableInputProps<Schema, TFieldValues, TFieldName, TTransformedValues>,
ref: React.ForwardedRef<HTMLDivElement>
ref: React.ForwardedRef<HTMLDivElement>,
) {
const {
placeholder = '',
@ -87,7 +87,7 @@ export const ResizableContentEditableInput = React.forwardRef(
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
document.execCommand('insertHTML', false, text)
}
},
)
const { field, fieldState, formInstance } = ariaComponents.Form.useField({
@ -126,7 +126,7 @@ export const ResizableContentEditableInput = React.forwardRef(
aria-autocomplete="none"
onPaste={onPaste}
onBlur={field.onBlur}
onInput={event => {
onInput={(event) => {
field.onChange(event.currentTarget.textContent ?? '')
}}
/>
@ -146,7 +146,7 @@ export const ResizableContentEditableInput = React.forwardRef(
</div>
</ariaComponents.Form.Field>
)
}
},
) as <
Schema extends ariaComponents.TSchema,
TFieldName extends ariaComponents.FieldPath<Schema, TFieldValues>,
@ -155,5 +155,5 @@ export const ResizableContentEditableInput = React.forwardRef(
TTransformedValues extends ariaComponents.FieldValues<Schema> | undefined = undefined,
>(
props: React.RefAttributes<HTMLDivElement> &
ResizableContentEditableInputProps<Schema, TFieldValues, TFieldName, TTransformedValues>
ResizableContentEditableInputProps<Schema, TFieldValues, TFieldName, TTransformedValues>,
) => React.JSX.Element

View File

@ -24,7 +24,7 @@ export interface ResizableInputProps extends aria.TextFieldProps {
*/
export const ResizableInput = React.forwardRef(function ResizableInput(
props: ResizableInputProps,
ref: React.ForwardedRef<HTMLTextAreaElement>
ref: React.ForwardedRef<HTMLTextAreaElement>,
) {
const { value = '', placeholder = '', description = null, ...textFieldProps } = props
const inputRef = React.useRef<HTMLTextAreaElement>(null)
@ -36,7 +36,7 @@ export const ResizableInput = React.forwardRef(function ResizableInput(
event.preventDefault()
const text = event.clipboardData.getData('text/plain')
document.execCommand('insertHTML', false, text)
}
},
)
React.useLayoutEffect(() => {
@ -57,7 +57,7 @@ export const ResizableInput = React.forwardRef(function ResizableInput(
<aria.TextField {...textFieldProps}>
<div
className={base()}
onClick={event => {
onClick={(event) => {
if (event.target !== inputRef.current && inputRef.current) {
inputRef.current.focus({ preventScroll: true })
}

View File

@ -3,5 +3,5 @@
*
* Barrel export file for ResizableInput component.
*/
export * from './ResizableInput'
export * from './ResizableContentEditableInput'
export * from './ResizableInput'

View File

@ -58,7 +58,7 @@ export interface RadioProps extends aria.RadioProps {
// eslint-disable-next-line no-restricted-syntax
export const Radio = React.forwardRef(function Radio(
props: RadioProps,
ref: React.ForwardedRef<HTMLLabelElement>
ref: React.ForwardedRef<HTMLLabelElement>,
) {
const { children, label, className, ...ariaProps } = props
@ -77,7 +77,7 @@ export const Radio = React.forwardRef(function Radio(
children: label ?? (typeof children === 'function' ? true : children),
}),
state,
inputRef
inputRef,
)
const { isFocused, isFocusVisible, focusProps } = aria.useFocusRing()
@ -149,7 +149,7 @@ export const Radio = React.forwardRef(function Radio(
</label>
)
}) as unknown as ((
props: RadioProps & React.RefAttributes<HTMLLabelElement>
props: RadioProps & React.RefAttributes<HTMLLabelElement>,
// eslint-disable-next-line no-restricted-syntax
) => React.JSX.Element) & {
// eslint-disable-next-line @typescript-eslint/naming-convention

View File

@ -50,7 +50,7 @@ export const RadioGroup = React.forwardRef(function RadioGroup<
TTransformedValues extends formComponent.FieldValues<Schema> | undefined = undefined,
>(
props: RadioGroupProps<Schema, TFieldValues, TFieldName, TTransformedValues>,
ref: React.ForwardedRef<HTMLDivElement>
ref: React.ForwardedRef<HTMLDivElement>,
) {
const {
children,
@ -117,5 +117,5 @@ export const RadioGroup = React.forwardRef(function RadioGroup<
TTransformedValues extends formComponent.FieldValues<Schema> | undefined = undefined,
>(
props: RadioGroupProps<Schema, TFieldValues, TFieldName, TTransformedValues> &
React.RefAttributes<HTMLFormElement>
React.RefAttributes<HTMLFormElement>,
) => React.JSX.Element

View File

@ -62,7 +62,7 @@ export function RadioGroupProvider(props: React.PropsWithChildren) {
setPressedRadio: setRadioPressed,
clearPressedRadio,
}),
[pressedRadio, setRadioPressed, clearPressedRadio]
[pressedRadio, setRadioPressed, clearPressedRadio],
)
return <RadioGroupContext.Provider value={value}>{children}</RadioGroupContext.Provider>

View File

@ -114,7 +114,7 @@ export const TEXT_STYLE = twv.tv({
// eslint-disable-next-line no-restricted-syntax
export const Text = React.forwardRef(function Text(
props: TextProps,
ref: React.Ref<HTMLSpanElement>
ref: React.Ref<HTMLSpanElement>,
) {
const {
className,
@ -184,10 +184,10 @@ export const Text = React.forwardRef(function Text(
{...aria.mergeProps<React.HTMLAttributes<HTMLElement>>()(
ariaProps,
targetProps,
truncate === 'custom'
? // eslint-disable-next-line @typescript-eslint/naming-convention,no-restricted-syntax
({ style: { '--line-clamp': `${lineClamp}` } } as React.HTMLAttributes<HTMLElement>)
: {}
truncate === 'custom' ?
// eslint-disable-next-line @typescript-eslint/naming-convention,no-restricted-syntax
({ style: { '--line-clamp': `${lineClamp}` } } as React.HTMLAttributes<HTMLElement>)
: {},
)}
>
{children}
@ -215,7 +215,7 @@ export interface HeadingProps extends Omit<TextProps, 'elementType'> {
*/
Text.Heading = React.forwardRef(function Heading(
props: HeadingProps,
ref: React.Ref<HTMLHeadingElement>
ref: React.Ref<HTMLHeadingElement>,
) {
const { level = 1, ...textProps } = props
return <Text ref={ref} elementType={`h${level}`} variant="h1" balance {...textProps} />

View File

@ -82,8 +82,8 @@ export function useVisualTooltip(props: VisualTooltipProps) {
const handleHoverChange = eventCallback.useEventCallback((isHovered: boolean) => {
const shouldDisplay = () => {
if (isHovered && targetRef.current != null) {
return typeof display === 'function'
? display(targetRef.current)
return typeof display === 'function' ?
display(targetRef.current)
: DISPLAY_STRATEGIES[display](targetRef.current)
} else {
return false
@ -143,7 +143,7 @@ export function useVisualTooltip(props: VisualTooltipProps) {
// Remove z-index from the overlay style
// because it's not needed(we show latest element on top) and can cause issues with stacking context
style: { zIndex: '' },
}
},
)}
>
{children}
@ -159,6 +159,6 @@ export function useVisualTooltip(props: VisualTooltipProps) {
const DISPLAY_STRATEGIES: Record<DisplayStrategy, (target: HTMLElement) => boolean> = {
always: () => true,
whenOverflowing: target =>
whenOverflowing: (target) =>
target.scrollWidth > target.clientWidth || target.scrollHeight > target.clientHeight,
}

View File

@ -79,7 +79,7 @@ export function Tooltip(props: TooltipProps) {
containerPadding={containerPadding}
UNSTABLE_portalContainer={root}
className={aria.composeRenderProps(className, (classNames, values) =>
TOOLTIP_STYLES({ className: classNames, ...values })
TOOLTIP_STYLES({ className: classNames, ...values }),
)}
data-ignore-click-outside
{...ariaTooltipProps}

View File

@ -21,5 +21,5 @@ export const VisuallyHidden = React.forwardRef<HTMLSpanElement, VisuallyHiddenPr
function VisuallyHidden(props, ref) {
const { className } = props
return <span ref={ref} className={VISUALLY_HIDDEN_STYLES({ className })} {...props} />
}
},
)

View File

@ -2,14 +2,14 @@
* @file index.ts
* Index file for Aria Components
*/
export * from './Button'
export * from './Tooltip'
export * from './Dialog'
export * from './Alert'
export * from './Button'
export * from './CopyBlock'
export * from './Inputs'
export * from './Dialog'
export * from './Form'
export * from './Text'
export * from './Separator'
export * from './VisuallyHidden'
export * from './Inputs'
export * from './Radio'
export * from './Separator'
export * from './Text'
export * from './Tooltip'
export * from './VisuallyHidden'

View File

@ -85,8 +85,8 @@ export default function Autocomplete<T>(props: AutocompleteProps<T>) {
const canEditTextRef = React.useRef(canEditText)
const isMultipleAndCustomValue = multiple === true && text != null
const matchingItems = React.useMemo(
() => (text == null ? items : items.filter(item => matches(item, text))),
[items, matches, text]
() => (text == null ? items : items.filter((item) => matches(item, text))),
[items, matches, text],
)
React.useEffect(() => {
@ -124,11 +124,11 @@ export default function Autocomplete<T>(props: AutocompleteProps<T>) {
const toggleValue = (value: T) => {
overrideValues(
multiple === true && !isMultipleAndCustomValue
? valuesSet.has(value)
? values.filter(theItem => theItem !== value)
: [...values, value]
: [value]
multiple === true && !isMultipleAndCustomValue ?
valuesSet.has(value) ?
values.filter((theItem) => theItem !== value)
: [...values, value]
: [value],
)
}
@ -184,7 +184,7 @@ export default function Autocomplete<T>(props: AutocompleteProps<T>) {
<div onKeyDown={onKeyDown} className="grow">
<FocusRing within>
<div className="flex flex-1 rounded-full">
{canEditText ? (
{canEditText ?
<Input
type={type}
ref={inputRef}
@ -202,14 +202,13 @@ export default function Autocomplete<T>(props: AutocompleteProps<T>) {
setIsDropdownVisible(false)
})
}}
onChange={event => {
onChange={(event) => {
setIsDropdownVisible(true)
setText(event.currentTarget.value === '' ? null : event.currentTarget.value)
}}
/>
) : (
<div
ref={element => element?.focus()}
: <div
ref={(element) => element?.focus()}
tabIndex={-1}
className="text grow cursor-pointer whitespace-nowrap bg-transparent px-button-x"
onClick={() => {
@ -223,7 +222,7 @@ export default function Autocomplete<T>(props: AutocompleteProps<T>) {
>
{itemsToString?.(values) ?? (values[0] != null ? itemToString(values[0]) : ZWSP)}
</div>
)}
}
</div>
</FocusRing>
<div className="h">
@ -232,13 +231,13 @@ export default function Autocomplete<T>(props: AutocompleteProps<T>) {
'relative top-2 z-1 h-max w-full rounded-default shadow-soft before:absolute before:top before:h-full before:w-full before:rounded-default before:bg-frame before:backdrop-blur-default',
isDropdownVisible &&
matchingItems.length !== 0 &&
'before:border before:border-primary/10'
'before:border before:border-primary/10',
)}
>
<div
className={tailwindMerge.twMerge(
'relative max-h-autocomplete-suggestions w-full overflow-y-auto overflow-x-hidden rounded-default',
isDropdownVisible && matchingItems.length !== 0 ? '' : 'h-0'
isDropdownVisible && matchingItems.length !== 0 ? '' : 'h-0',
)}
>
{/* FIXME: "Invite" modal does not take into account the height of the autocomplete,
@ -249,12 +248,12 @@ export default function Autocomplete<T>(props: AutocompleteProps<T>) {
className={tailwindMerge.twMerge(
'text relative cursor-pointer whitespace-nowrap px-input-x first:rounded-t-default last:rounded-b-default hover:bg-hover-bg',
valuesSet.has(item) && 'bg-hover-bg',
index === selectedIndex && 'bg-black/5'
index === selectedIndex && 'bg-black/5',
)}
onMouseDown={event => {
onMouseDown={(event) => {
event.preventDefault()
}}
onClick={event => {
onClick={(event) => {
event.stopPropagation()
toggleValue(item)
}}

View File

@ -30,7 +30,7 @@ function ColorPickerItem(props: InternalColorPickerItemProps) {
return (
<FocusRing within>
<aria.Radio
ref={element => {
ref={(element) => {
element?.querySelector('input')?.classList.add(focusChildClass)
}}
value={cssColor}
@ -63,7 +63,7 @@ function ColorPicker(props: ColorPickerProps, ref: React.ForwardedRef<HTMLDivEle
ref={ref}
{...radioGroupProps}
orientation="horizontal"
onChange={value => {
onChange={(value) => {
const color = backend.COLOR_STRING_TO_COLOR.get(value)
if (color != null) {
setColor(color)

View File

@ -22,26 +22,24 @@ export interface ContextMenuProps extends Readonly<React.PropsWithChildren> {
export default function ContextMenu(props: ContextMenuProps) {
const { hidden = false, children } = props
return hidden ? (
children
) : (
<FocusArea direction="vertical">
{innerProps => (
<div
className="pointer-events-auto relative rounded-default before:absolute before:h-full before:w-full before:rounded-default before:bg-selected-frame before:backdrop-blur-default"
{...innerProps}
>
return hidden ? children : (
<FocusArea direction="vertical">
{(innerProps) => (
<div
aria-label={props['aria-label']}
className={tailwindMerge.twMerge(
'relative flex flex-col rounded-default p-context-menu',
detect.isOnMacOS() ? 'w-context-menu-macos' : 'w-context-menu'
)}
className="pointer-events-auto relative rounded-default before:absolute before:h-full before:w-full before:rounded-default before:bg-selected-frame before:backdrop-blur-default"
{...innerProps}
>
{children}
<div
aria-label={props['aria-label']}
className={tailwindMerge.twMerge(
'relative flex flex-col rounded-default p-context-menu',
detect.isOnMacOS() ? 'w-context-menu-macos' : 'w-context-menu',
)}
>
{children}
</div>
</div>
</div>
)}
</FocusArea>
)
)}
</FocusArea>
)
}

View File

@ -32,30 +32,28 @@ export interface ContextMenusProps extends Readonly<React.PropsWithChildren> {
function ContextMenus(props: ContextMenusProps, ref: React.ForwardedRef<HTMLDivElement>) {
const { hidden = false, children, event } = props
return hidden ? (
<>{children}</>
) : (
<Modal
className="absolute size-full overflow-hidden bg-dim"
onContextMenu={innerEvent => {
innerEvent.preventDefault()
}}
>
<div
data-testid="context-menus"
ref={ref}
style={{ left: event.pageX - HALF_MENU_WIDTH, top: event.pageY }}
className={tailwindMerge.twMerge(
'pointer-events-none sticky flex w-min items-start gap-context-menus'
)}
onClick={clickEvent => {
clickEvent.stopPropagation()
return hidden ?
<>{children}</>
: <Modal
className="absolute size-full overflow-hidden bg-dim"
onContextMenu={(innerEvent) => {
innerEvent.preventDefault()
}}
>
{children}
</div>
</Modal>
)
<div
data-testid="context-menus"
ref={ref}
style={{ left: event.pageX - HALF_MENU_WIDTH, top: event.pageY }}
className={tailwindMerge.twMerge(
'pointer-events-none sticky flex w-min items-start gap-context-menus',
)}
onClick={(clickEvent) => {
clickEvent.stopPropagation()
}}
>
{children}
</div>
</Modal>
}
export default React.forwardRef(ContextMenus)

View File

@ -49,12 +49,12 @@ export default function ControlledInput(props: ControlledInputProps) {
{...aria.mergeProps<aria.InputProps>()(inputProps, focusChildProps, {
className:
'w-full rounded-full border py-auth-input-y pl-auth-icon-container-w pr-auth-input-r text-sm placeholder-gray-500 transition-all duration-auth hover:bg-gray-100 focus:bg-gray-100',
onKeyDown: event => {
onKeyDown: (event) => {
if (!event.isPropagationStopped()) {
onKeyDown?.(event)
}
},
onChange: event => {
onChange: (event) => {
onChange?.(event)
setValue(event.target.value)
setWasJustBlurred(false)
@ -66,9 +66,9 @@ export default function ControlledInput(props: ControlledInputProps) {
if (error != null) {
currentTarget.setCustomValidity('')
currentTarget.setCustomValidity(
currentTarget.checkValidity() || shouldReportValidityRef?.current === false
? ''
: error
currentTarget.checkValidity() || shouldReportValidityRef?.current === false ?
''
: error,
)
}
if (hasReportedValidity) {
@ -84,13 +84,14 @@ export default function ControlledInput(props: ControlledInputProps) {
) {
setHasReportedValidity(true)
}
}, DEBOUNCE_MS)
}, DEBOUNCE_MS),
)
}
}
},
onBlur: validate
? event => {
onBlur:
validate ?
(event) => {
onBlur?.(event)
if (wasJustBlurred) {
setHasReportedValidity(false)

View File

@ -2,8 +2,8 @@
import * as React from 'react'
import CrossIcon from '#/assets/cross.svg'
import FolderArrowDoubleIcon from '#/assets/folder_arrow_double.svg'
import FolderArrowIcon from '#/assets/folder_arrow.svg'
import FolderArrowDoubleIcon from '#/assets/folder_arrow_double.svg'
import * as focusHooks from '#/hooks/focusHooks'
@ -97,7 +97,7 @@ export default function DateInput(props: DateInputProps) {
return (
<div
className="relative flex flex-col"
onClick={event => {
onClick={(event) => {
event.stopPropagation()
}}
>
@ -108,13 +108,13 @@ export default function DateInput(props: DateInputProps) {
tabIndex: 0,
className: tailwindMerge.twMerge(
'flex h-text w-date-picker items-center rounded-full border border-primary/10 px-date-input transition-colors hover:[&:not(:has(button:hover))]:bg-hover-bg',
date == null && 'placeholder'
date == null && 'placeholder',
),
onClick: event => {
onClick: (event) => {
event.stopPropagation()
setIsPickerVisible(!isPickerVisible)
},
onKeyDown: event => {
onKeyDown: (event) => {
if (event.key === 'Enter' || event.key === 'Space') {
event.stopPropagation()
setIsPickerVisible(!isPickerVisible)
@ -223,7 +223,7 @@ export default function DateInput(props: DateInputProps) {
const currentDate = new Date(
selectedYear,
selectedMonthIndex + day.monthOffset,
day.date
day.date,
)
const isSelectedDate =
date != null &&
@ -238,7 +238,7 @@ export default function DateInput(props: DateInputProps) {
isDisabled={isSelectedDate}
className={tailwindMerge.twMerge(
'w-full rounded-small-rectangle-button text-center hover:bg-primary/10 disabled:bg-frame disabled:font-bold',
day.monthOffset !== 0 && 'opacity-unimportant'
day.monthOffset !== 0 && 'opacity-unimportant',
)}
onPress={() => {
setIsPickerVisible(false)

View File

@ -80,9 +80,9 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
const onConfigurationChange = React.useCallback(
(feature: billing.PaywallFeatureName, configuration: PaywallDevtoolsFeatureConfiguration) => {
setFeatures(prev => ({ ...prev, [feature]: configuration }))
setFeatures((prev) => ({ ...prev, [feature]: configuration }))
},
[]
[],
)
return (
@ -114,7 +114,7 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
<ariaComponents.Form
gap="small"
schema={schema => schema.object({ plan: schema.string() })}
schema={(schema) => schema.object({ plan: schema.string() })}
defaultValues={{ plan: session.user.plan ?? 'free' }}
>
{({ form }) => (
@ -122,7 +122,7 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
<ariaComponents.RadioGroup
form={form}
name="plan"
onChange={value => {
onChange={(value) => {
queryClient.setQueryData(authQueryKey, {
...session,
user: { ...session.user, plan: value },
@ -193,7 +193,7 @@ export function EnsoDevtools(props: EnsoDevtoolsProps) {
<aria.Switch
className="group flex items-center gap-1"
isSelected={configuration.isForceEnabled ?? true}
onChange={value => {
onChange={(value) => {
onConfigurationChange(featureName, {
isForceEnabled: value,
})

View File

@ -8,9 +8,9 @@ import * as reactQueryDevtools from '@tanstack/react-query-devtools'
import * as errorBoundary from 'react-error-boundary'
const ReactQueryDevtoolsProduction = React.lazy(() =>
import('@tanstack/react-query-devtools/build/modern/production.js').then(d => ({
import('@tanstack/react-query-devtools/build/modern/production.js').then((d) => ({
default: d.ReactQueryDevtools,
}))
})),
)
/** Show the React Query Devtools and provide the ability to show them in production. */
@ -24,7 +24,7 @@ export function ReactQueryDevtools() {
React.useEffect(() => {
window.toggleDevtools = () => {
setShowDevtools(old => !old)
setShowDevtools((old) => !old)
}
}, [])

View File

@ -63,7 +63,7 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
const multiple = props.multiple === true
const selectedIndex = 'selectedIndex' in props ? props.selectedIndex : null
const selectedIndices = 'selectedIndices' in props ? props.selectedIndices : []
const selectedItems = selectedIndices.flatMap(index => {
const selectedItems = selectedIndices.flatMap((index) => {
const item = items[index]
return item != null ? [item] : []
})
@ -109,15 +109,16 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
const item = items[tempSelectedIndex]
if (item != null) {
if (multiple) {
const newIndices = selectedIndices.includes(tempSelectedIndex)
? selectedIndices.filter(index => index !== tempSelectedIndex)
const newIndices =
selectedIndices.includes(tempSelectedIndex) ?
selectedIndices.filter((index) => index !== tempSelectedIndex)
: [...selectedIndices, tempSelectedIndex]
props.onClick(
newIndices.flatMap(index => {
newIndices.flatMap((index) => {
const otherItem = items[index]
return otherItem != null ? [otherItem] : []
}),
newIndices
newIndices,
)
} else {
props.onClick(item, tempSelectedIndex)
@ -134,11 +135,13 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
if (!isDropdownVisible) break
event.preventDefault()
setTempSelectedIndex(
tempSelectedIndex == null ||
tempSelectedIndex === 0 ||
tempSelectedIndex >= items.length
? items.length - 1
: tempSelectedIndex - 1
(
tempSelectedIndex == null ||
tempSelectedIndex === 0 ||
tempSelectedIndex >= items.length
) ?
items.length - 1
: tempSelectedIndex - 1,
)
break
}
@ -146,9 +149,9 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
if (!isDropdownVisible) break
event.preventDefault()
setTempSelectedIndex(
tempSelectedIndex == null || tempSelectedIndex >= items.length - 1
? 0
: tempSelectedIndex + 1
tempSelectedIndex == null || tempSelectedIndex >= items.length - 1 ?
0
: tempSelectedIndex + 1,
)
break
}
@ -159,7 +162,7 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
return (
<FocusRing placement="outset">
<div
ref={element => {
ref={(element) => {
if (typeof ref === 'function') {
ref(element)
} else if (ref != null) {
@ -170,16 +173,16 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
tabIndex={0}
className={tailwindMerge.twMerge(
'focus-child group relative flex w-max cursor-pointer flex-col items-start whitespace-nowrap rounded-input leading-cozy',
className
className,
)}
onFocus={event => {
onFocus={(event) => {
if (!justBlurredRef.current && !readOnly && event.target === event.currentTarget) {
setIsDropdownVisible(true)
justFocusedRef.current = true
}
justBlurredRef.current = false
}}
onBlur={event => {
onBlur={(event) => {
if (!readOnly && event.target === event.currentTarget) {
setIsDropdownVisible(false)
justBlurredRef.current = true
@ -189,28 +192,28 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
onKeyUp={() => {
justFocusedRef.current = false
}}
onClick={event => {
onClick={(event) => {
event.stopPropagation()
}}
>
<div
className={tailwindMerge.twMerge(
'absolute left-0 h-full w-full min-w-max',
isDropdownVisible ? 'z-1' : 'overflow-hidden'
isDropdownVisible ? 'z-1' : 'overflow-hidden',
)}
>
<div
className={tailwindMerge.twMerge(
'relative before:absolute before:top before:w-full before:rounded-input before:border before:border-primary/10 before:backdrop-blur-default before:transition-colors',
isDropdownVisible
? 'before:h-full before:shadow-soft'
: 'before:h-text group-hover:before:bg-hover-bg'
isDropdownVisible ?
'before:h-full before:shadow-soft'
: 'before:h-text group-hover:before:bg-hover-bg',
)}
>
{/* Spacing. */}
<div
className="padding relative h-text"
onClick={event => {
onClick={(event) => {
event.stopPropagation()
if (!justFocusedRef.current && !readOnly) {
setIsDropdownVisible(!isDropdownVisible)
@ -221,7 +224,7 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
<div
className={tailwindMerge.twMerge(
'relative grid max-h-dropdown-items w-full overflow-auto rounded-input transition-grid-template-rows',
isDropdownVisible ? 'grid-rows-1fr' : 'grid-rows-0fr'
isDropdownVisible ? 'grid-rows-1fr' : 'grid-rows-0fr',
)}
>
<div className="overflow-hidden">
@ -231,12 +234,12 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
className={tailwindMerge.twMerge(
'flex h-text items-center gap-dropdown-arrow rounded-input px-input-x transition-colors',
multiple && 'hover:font-semibold',
i === visuallySelectedIndex
? 'cursor-default bg-frame font-bold focus-ring'
: 'hover:bg-hover-bg'
i === visuallySelectedIndex ?
'cursor-default bg-frame font-bold focus-ring'
: 'hover:bg-hover-bg',
)}
key={i}
onMouseDown={event => {
onMouseDown={(event) => {
event.preventDefault()
isMouseDown.current = true
}}
@ -246,15 +249,16 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
onClick={() => {
if (i !== visuallySelectedIndex) {
if (multiple) {
const newIndices = selectedIndices.includes(i)
? selectedIndices.filter(index => index !== i)
const newIndices =
selectedIndices.includes(i) ?
selectedIndices.filter((index) => index !== i)
: [...selectedIndices, i]
props.onClick(
newIndices.flatMap(index => {
newIndices.flatMap((index) => {
const otherItem = items[index]
return otherItem != null ? [otherItem] : []
}),
newIndices
newIndices,
)
rootRef.current?.focus()
} else {
@ -290,9 +294,9 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
className={tailwindMerge.twMerge(
'relative flex h-text items-center gap-dropdown-arrow px-input-x',
isDropdownVisible && 'z-1',
readOnly && 'read-only'
readOnly && 'read-only',
)}
onClick={event => {
onClick={(event) => {
event.stopPropagation()
if (!justFocusedRef.current && !readOnly) {
setIsDropdownVisible(!isDropdownVisible)
@ -302,11 +306,9 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
>
<SvgMask src={FolderArrowIcon} className="rotate-90" />
<div className="grow">
{visuallySelectedItem != null ? (
{visuallySelectedItem != null ?
<Child item={visuallySelectedItem} />
) : (
multiple && <props.renderMultiple items={selectedItems} render={Child} />
)}
: multiple && <props.renderMultiple items={selectedItems} render={Child} />}
</div>
</div>
{/* Hidden, but required to exist for the width of the parent element to be correct.
@ -328,5 +330,5 @@ function Dropdown<T>(props: DropdownProps<T>, ref: React.ForwardedRef<HTMLDivEle
// This is REQUIRED, as `React.forwardRef` does not preserve types of generic functions.
// eslint-disable-next-line no-restricted-syntax
export default React.forwardRef(Dropdown) as <T>(
props: DropdownProps<T> & React.RefAttributes<HTMLDivElement>
props: DropdownProps<T> & React.RefAttributes<HTMLDivElement>,
) => React.JSX.Element

View File

@ -82,7 +82,7 @@ export default function EditableSpan(props: EditableSpanProps) {
return (
<form
className={tailwindMerge.twMerge('flex grow gap-1.5', WIDTH_CLASS_NAME)}
onBlur={event => {
onBlur={(event) => {
const currentTarget = event.currentTarget
if (!currentTarget.contains(event.relatedTarget)) {
// This must run AFTER the cancel button's event handler runs.
@ -93,7 +93,7 @@ export default function EditableSpan(props: EditableSpanProps) {
})
}
}}
onSubmit={event => {
onSubmit={(event) => {
event.preventDefault()
if (inputRef.current != null) {
if (isSubmittable) {
@ -107,7 +107,7 @@ export default function EditableSpan(props: EditableSpanProps) {
<aria.Input
data-testid={props['data-testid']}
className={tailwindMerge.twMerge('rounded-lg', className)}
ref={element => {
ref={(element) => {
inputRef.current = element
if (element) {
element.style.width = '0'
@ -118,10 +118,10 @@ export default function EditableSpan(props: EditableSpanProps) {
type="text"
size={1}
defaultValue={children}
onContextMenu={event => {
onContextMenu={(event) => {
event.stopPropagation()
}}
onKeyDown={event => {
onKeyDown={(event) => {
if (event.key !== 'Escape') {
event.stopPropagation()
}
@ -132,13 +132,13 @@ export default function EditableSpan(props: EditableSpanProps) {
}}
{...(inputPattern == null ? {} : { pattern: inputPattern })}
{...(inputTitle == null ? {} : { title: inputTitle })}
{...(checkSubmittable == null
? {}
: {
onInput: event => {
setIsSubmittable(checkSubmittable(event.currentTarget.value))
},
})}
{...(checkSubmittable == null ?
{}
: {
onInput: (event) => {
setIsSubmittable(checkSubmittable(event.currentTarget.value))
},
})}
/>
<ariaComponents.ButtonGroup gap="xsmall" className="grow-0 items-center">
{isSubmittable && (

View File

@ -44,7 +44,7 @@ export function ErrorBoundary(props: ErrorBoundaryProps) {
sentry.captureException(error, { extra: { info } })
onError(error, info)
}}
onReset={details => {
onReset={(details) => {
reset()
onReset(details)
}}

View File

@ -1,8 +1,8 @@
/** @file A styled input that includes an icon. */
import * as React from 'react'
import EyeCrossedIcon from '#/assets/eye_crossed.svg'
import EyeIcon from '#/assets/eye.svg'
import EyeCrossedIcon from '#/assets/eye_crossed.svg'
import type * as controlledInput from '#/components/ControlledInput'
import ControlledInput from '#/components/ControlledInput'
@ -32,7 +32,7 @@ export default function Input(props: InputProps) {
src={isShowingPassword ? EyeIcon : EyeCrossedIcon}
className="left-[unset] right-0 cursor-pointer rounded-full"
onClick={() => {
setIsShowingPassword(show => !show)
setIsShowingPassword((show) => !show)
}}
/>
)}

View File

@ -41,7 +41,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
const remoteBackend = backendProvider.useRemoteBackend()
const { getText } = textProvider.useText()
const [autocompleteText, setAutocompleteText] = React.useState(() =>
typeof value === 'string' ? value : null
typeof value === 'string' ? value : null,
)
const [selectedChildIndex, setSelectedChildIndex] = React.useState<number | null>(null)
const [autocompleteItems, setAutocompleteItems] = React.useState<string[] | null>(null)
@ -61,35 +61,35 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
setAutocompleteItems([])
void (async () => {
const secrets = (await remoteBackend?.listSecrets()) ?? []
setAutocompleteItems(secrets.map(secret => secret.path))
setAutocompleteItems(secrets.map((secret) => secret.path))
})()
}
children.push(
<div
className={tailwindMerge.twMerge(
'grow rounded-default border',
isValid ? 'border-primary/10' : 'border-red-700/60'
isValid ? 'border-primary/10' : 'border-red-700/60',
)}
>
<Autocomplete
items={autocompleteItems ?? []}
itemToKey={item => item}
itemToString={item => item}
itemToKey={(item) => item}
itemToString={(item) => item}
placeholder={getText('enterSecretPath')}
matches={(item, text) => item.toLowerCase().includes(text.toLowerCase())}
values={isValid ? [value] : []}
setValues={values => {
setValues={(values) => {
setValue(values[0])
}}
text={autocompleteText}
setText={setAutocompleteText}
/>
</div>
</div>,
)
} else {
children.push(
<FocusArea direction="horizontal">
{innerProps => (
{(innerProps) => (
<FocusRing>
<aria.Input
type="text"
@ -98,10 +98,10 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
size={1}
className={tailwindMerge.twMerge(
'focus-child w-data-link-text-input text grow rounded-input border bg-transparent px-input-x read-only:read-only',
getValidator(path)(value) ? 'border-primary/10' : 'border-red-700/60'
getValidator(path)(value) ? 'border-primary/10' : 'border-red-700/60',
)}
placeholder={getText('enterText')}
onChange={event => {
onChange={(event) => {
const newValue: string = event.currentTarget.value
setValue(newValue)
}}
@ -109,7 +109,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
/>
</FocusRing>
)}
</FocusArea>
</FocusArea>,
)
}
break
@ -117,7 +117,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
case 'number': {
children.push(
<FocusArea direction="horizontal">
{innerProps => (
{(innerProps) => (
<FocusRing>
<aria.Input
type="number"
@ -126,10 +126,10 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
size={1}
className={tailwindMerge.twMerge(
'focus-child w-data-link-text-input text grow rounded-input border bg-transparent px-input-x read-only:read-only',
getValidator(path)(value) ? 'border-primary/10' : 'border-red-700/60'
getValidator(path)(value) ? 'border-primary/10' : 'border-red-700/60',
)}
placeholder={getText('enterNumber')}
onChange={event => {
onChange={(event) => {
const newValue: number = event.currentTarget.valueAsNumber
if (Number.isFinite(newValue)) {
setValue(newValue)
@ -139,14 +139,14 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
/>
</FocusRing>
)}
</FocusArea>
</FocusArea>,
)
break
}
case 'integer': {
children.push(
<FocusArea direction="horizontal">
{innerProps => (
{(innerProps) => (
<FocusRing>
<aria.Input
type="number"
@ -155,10 +155,10 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
size={1}
className={tailwindMerge.twMerge(
'focus-child w-data-link-text-input text grow rounded-input border bg-transparent px-input-x read-only:read-only',
getValidator(path)(value) ? 'border-primary/10' : 'border-red-700/60'
getValidator(path)(value) ? 'border-primary/10' : 'border-red-700/60',
)}
placeholder={getText('enterInteger')}
onChange={event => {
onChange={(event) => {
const newValue: number = Math.floor(event.currentTarget.valueAsNumber)
setValue(newValue)
}}
@ -166,7 +166,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
/>
</FocusRing>
)}
</FocusArea>
</FocusArea>,
)
break
}
@ -176,7 +176,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
isReadOnly={readOnly}
isSelected={typeof value === 'boolean' && value}
onChange={setValue}
/>
/>,
)
break
}
@ -190,104 +190,111 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
const [k, v] = kv
return object
.singletonObjectOrNull(v)
.map(childSchema => ({ key: k, schema: childSchema }))
}
.map((childSchema) => ({ key: k, schema: childSchema }))
},
)
if (jsonSchema.constantValue(defs, schema).length !== 1) {
children.push(
<div className="flex flex-col gap-json-schema rounded-default border border-primary/10 p-json-schema-object-input">
{propertyDefinitions.map(definition => {
{propertyDefinitions.map((definition) => {
const { key, schema: childSchema } = definition
const isOptional = !requiredProperties.includes(key)
return jsonSchema.constantValue(defs, childSchema).length === 1 ? null : (
<div
key={key}
className="flex flex-wrap items-center gap-2"
{...('description' in childSchema
? { title: String(childSchema.description) }
return jsonSchema.constantValue(defs, childSchema).length === 1 ?
null
: <div
key={key}
className="flex flex-wrap items-center gap-2"
{...('description' in childSchema ?
{ title: String(childSchema.description) }
: {})}
>
<FocusArea active={isOptional} direction="horizontal">
{innerProps => {
const isPresent = value != null && key in value
return (
<ariaComponents.Button
size="custom"
variant="custom"
isDisabled={!isOptional}
isActive={!isOptional || isPresent}
className={tailwindMerge.twMerge(
'text inline-block w-json-schema-object-key whitespace-nowrap rounded-full px-button-x text-left',
isOptional && 'hover:bg-hover-bg'
)}
onPress={() => {
if (isOptional) {
setValue(oldValue => {
if (oldValue != null && key in oldValue) {
// This is SAFE, as `value` is an untyped object.
// The removed key is intentionally unused.
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-unused-vars
const { [key]: removed, ...newValue } = oldValue as Record<
string,
NonNullable<unknown> | null
>
return newValue
} else {
return {
...oldValue,
[key]: jsonSchema.constantValue(defs, childSchema, true)[0],
>
<FocusArea active={isOptional} direction="horizontal">
{(innerProps) => {
const isPresent = value != null && key in value
return (
<ariaComponents.Button
size="custom"
variant="custom"
isDisabled={!isOptional}
isActive={!isOptional || isPresent}
className={tailwindMerge.twMerge(
'text inline-block w-json-schema-object-key whitespace-nowrap rounded-full px-button-x text-left',
isOptional && 'hover:bg-hover-bg',
)}
onPress={() => {
if (isOptional) {
setValue((oldValue) => {
if (oldValue != null && key in oldValue) {
// This is SAFE, as `value` is an untyped object.
// The removed key is intentionally unused.
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-unused-vars
const { [key]: removed, ...newValue } = oldValue as Record<
string,
NonNullable<unknown> | null
>
return newValue
} else {
return {
...oldValue,
[key]: jsonSchema.constantValue(
defs,
childSchema,
true,
)[0],
}
}
}
})
}
}}
{...innerProps}
>
{'title' in childSchema ? String(childSchema.title) : key}
</ariaComponents.Button>
)
}}
</FocusArea>
{value != null && key in value && (
<JSONSchemaInput
readOnly={readOnly}
defs={defs}
schema={childSchema}
path={`${path}/properties/${key}`}
getValidator={getValidator}
// This is SAFE, as `value` is an untyped object.
// eslint-disable-next-line no-restricted-syntax
value={(value as Record<string, unknown>)[key] ?? null}
setValue={newValue => {
setValue(oldValue => {
if (typeof newValue === 'function') {
const unsafeValue: unknown = newValue(
// This is SAFE; but there is no way to tell TypeScript that an object
// has an index signature.
// eslint-disable-next-line no-restricted-syntax
(oldValue as Readonly<Record<string, unknown>>)[key] ?? null
)
// The value MAY be `null`, but it is better than the value being a
// function (which is *never* the intended result).
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
newValue = unsafeValue!
}
return typeof oldValue === 'object' &&
oldValue != null &&
// This is SAFE; but there is no way to tell TypeScript that an object
// has an index signature.
// eslint-disable-next-line no-restricted-syntax
(oldValue as Readonly<Record<string, unknown>>)[key] === newValue
? oldValue
: { ...oldValue, [key]: newValue }
})
})
}
}}
{...innerProps}
>
{'title' in childSchema ? String(childSchema.title) : key}
</ariaComponents.Button>
)
}}
/>
)}
</div>
)
</FocusArea>
{value != null && key in value && (
<JSONSchemaInput
readOnly={readOnly}
defs={defs}
schema={childSchema}
path={`${path}/properties/${key}`}
getValidator={getValidator}
// This is SAFE, as `value` is an untyped object.
// eslint-disable-next-line no-restricted-syntax
value={(value as Record<string, unknown>)[key] ?? null}
setValue={(newValue) => {
setValue((oldValue) => {
if (typeof newValue === 'function') {
const unsafeValue: unknown = newValue(
// This is SAFE; but there is no way to tell TypeScript that an object
// has an index signature.
// eslint-disable-next-line no-restricted-syntax
(oldValue as Readonly<Record<string, unknown>>)[key] ?? null,
)
// The value MAY be `null`, but it is better than the value being a
// function (which is *never* the intended result).
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
newValue = unsafeValue!
}
return (
typeof oldValue === 'object' &&
oldValue != null &&
// This is SAFE; but there is no way to tell TypeScript that an object
// has an index signature.
// eslint-disable-next-line no-restricted-syntax
(oldValue as Readonly<Record<string, unknown>>)[key] ===
newValue
) ?
oldValue
: { ...oldValue, [key]: newValue }
})
}}
/>
)}
</div>
})}
</div>
</div>,
)
}
break
@ -303,7 +310,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
key={schema.$ref}
schema={referencedSchema}
path={schema.$ref}
/>
/>,
)
}
}
@ -319,7 +326,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
(selectedChildSchema == null || getValidator(selectedChildPath)(value) !== true)
) {
const newIndexRaw = childSchemas.findIndex((_, index) =>
getValidator(`${path}/anyOf/${index}`)(value)
getValidator(`${path}/anyOf/${index}`)(value),
)
const newIndex = selectedChildSchema == null && newIndexRaw === -1 ? 0 : newIndexRaw
if (newIndex !== -1 && newIndex !== selectedChildIndex) {
@ -328,12 +335,12 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
}
const dropdown = (
<FocusArea direction="horizontal">
{innerProps => (
{(innerProps) => (
<Dropdown
readOnly={readOnly}
items={childSchemas}
selectedIndex={selectedChildIndex}
render={childProps => (
render={(childProps) => (
<aria.Text>{jsonSchema.getSchemaName(defs, childProps.item)}</aria.Text>
)}
className="self-start"
@ -351,17 +358,15 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
<div
className={tailwindMerge.twMerge(
'flex flex-col gap-json-schema',
childValue.length === 0 && 'w-full'
childValue.length === 0 && 'w-full',
)}
>
{dropdownTitle != null ? (
{dropdownTitle != null ?
<div className="flex h-row items-center">
<div className="h-text w-json-schema-dropdown-title">{dropdownTitle}</div>
{dropdown}
</div>
) : (
dropdown
)}
: dropdown}
{selectedChildSchema != null && (
<JSONSchemaInput
key={selectedChildIndex}
@ -374,7 +379,7 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
setValue={setValue}
/>
)}
</div>
</div>,
)
}
if ('allOf' in schema && Array.isArray(schema.allOf)) {
@ -393,10 +398,10 @@ export default function JSONSchemaInput(props: JSONSchemaInputProps) {
))
children.push(...newChildren)
}
return children.length === 0 ? null : children.length === 1 && children[0] != null ? (
children[0]
) : (
<div className="flex flex-col gap-json-schema">{...children}</div>
return (
children.length === 0 ? null
: children.length === 1 && children[0] != null ? children[0]
: <div className="flex flex-col gap-json-schema">{...children}</div>
)
}
}

View File

@ -41,7 +41,7 @@ export default function Link(props: LinkProps) {
return (
<FocusRing>
{openInBrowser ? (
{openInBrowser ?
<aria.Link
{...aria.mergeProps<aria.LinkProps>()(focusChildProps, {
href: to,
@ -54,8 +54,7 @@ export default function Link(props: LinkProps) {
>
{contents}
</aria.Link>
) : (
<router.Link
: <router.Link
{...aria.mergeProps<router.LinkProps>()(focusChildProps, {
to,
className,
@ -64,7 +63,7 @@ export default function Link(props: LinkProps) {
>
{contents}
</router.Link>
)}
}
</FocusRing>
)
}

View File

@ -120,11 +120,11 @@ export default function MenuEntry(props: MenuEntryProps) {
const info = inputBindings.metadata[action]
const labelTextId: text.TextId = (() => {
if (action === 'openInFileBrowser') {
return detect.isOnMacOS()
? 'openInFileBrowserShortcutMacOs'
: detect.isOnWindows()
? 'openInFileBrowserShortcutWindows'
: 'openInFileBrowserShortcut'
return (
detect.isOnMacOS() ? 'openInFileBrowserShortcutMacOs'
: detect.isOnWindows() ? 'openInFileBrowserShortcutWindows'
: 'openInFileBrowserShortcut'
)
} else {
return ACTION_TO_TEXT_ID[action]
}
@ -143,25 +143,27 @@ export default function MenuEntry(props: MenuEntryProps) {
}, [isDisabled, inputBindings, action, doAction])
return hidden ? null : (
<FocusRing>
<aria.Button
{...aria.mergeProps<aria.ButtonProps>()(focusChildProps, {
isDisabled,
className: 'group flex w-full rounded-menu-entry',
onPress: () => {
unsetModal()
doAction()
},
})}
>
<div className={MENU_ENTRY_VARIANTS(variantProps)}>
<div title={title} className="flex items-center gap-menu-entry whitespace-nowrap">
<SvgMask src={icon ?? info.icon ?? BlankIcon} color={info.color} className="size-4" />
<ariaComponents.Text slot="label">{label ?? getText(labelTextId)}</ariaComponents.Text>
<FocusRing>
<aria.Button
{...aria.mergeProps<aria.ButtonProps>()(focusChildProps, {
isDisabled,
className: 'group flex w-full rounded-menu-entry',
onPress: () => {
unsetModal()
doAction()
},
})}
>
<div className={MENU_ENTRY_VARIANTS(variantProps)}>
<div title={title} className="flex items-center gap-menu-entry whitespace-nowrap">
<SvgMask src={icon ?? info.icon ?? BlankIcon} color={info.color} className="size-4" />
<ariaComponents.Text slot="label">
{label ?? getText(labelTextId)}
</ariaComponents.Text>
</div>
<KeyboardShortcut action={action} />
</div>
<KeyboardShortcut action={action} />
</div>
</aria.Button>
</FocusRing>
)
</aria.Button>
</FocusRing>
)
}

View File

@ -44,14 +44,14 @@ export default function Modal(props: ModalProps) {
return (
<FocusRoot active={!hidden}>
{innerProps => (
{(innerProps) => (
<div
{...(!hidden ? { 'data-testid': 'modal-background' } : {})}
style={style}
className={MODAL_VARIANTS(variantProps)}
onClick={
onClick ??
(event => {
((event) => {
if (event.currentTarget === event.target && getSelection()?.type !== 'Range') {
event.stopPropagation()
unsetModal()
@ -60,7 +60,7 @@ export default function Modal(props: ModalProps) {
}
onContextMenu={onContextMenu}
{...innerProps}
onKeyDown={event => {
onKeyDown={(event) => {
innerProps.onKeyDown?.(event)
if (event.key !== 'Escape') {
event.stopPropagation()

View File

@ -38,7 +38,7 @@ export function OfflineNotificationManager(props: OfflineNotificationManagerProp
const toastId = 'offline'
const { getText } = textProvider.useText()
offlineHooks.useOfflineChange(isOffline => {
offlineHooks.useOfflineChange((isOffline) => {
toast.toast.dismiss(toastId)
if (isOffline) {

View File

@ -41,17 +41,13 @@ export default function Page(props: PageProps) {
{!hideChat && (
<>
{/* `session.accessToken` MUST be present in order for the `Chat` component to work. */}
{!hideInfoBar &&
session?.accessToken != null &&
process.env.ENSO_CLOUD_CHAT_URL != null ? (
{!hideInfoBar && session?.accessToken != null && process.env.ENSO_CLOUD_CHAT_URL != null ?
<Chat
isOpen={isHelpChatOpen}
doClose={doCloseChat}
endpoint={process.env.ENSO_CLOUD_CHAT_URL}
/>
) : (
<ChatPlaceholder hideLoginButtons isOpen={isHelpChatOpen} doClose={doCloseChat} />
)}
: <ChatPlaceholder hideLoginButtons isOpen={isHelpChatOpen} doClose={doCloseChat} />}
</>
)}
<Portal>

View File

@ -39,7 +39,7 @@ export function ContextMenuEntry(props: ContextMenuEntryProps) {
icon={LockIcon}
doAction={() => {
setModal(
<paywallDialog.PaywallDialog modalProps={{ defaultOpen: true }} feature={feature} />
<paywallDialog.PaywallDialog modalProps={{ defaultOpen: true }} feature={feature} />,
)
}}
/>

View File

@ -33,14 +33,14 @@ export function PaywallBulletPoints(props: PaywallBulletPointsProps) {
const { getText } = textProvider.useText()
const bulletPoints = getText(bulletPointsTextId)
.split(';')
.map(bulletPoint => bulletPoint.trim())
.map((bulletPoint) => bulletPoint.trim())
if (bulletPoints.length === 0) {
return null
} else {
return (
<ul className={tw.twMerge('m-0 flex w-full list-inside list-none flex-col gap-1', className)}>
{bulletPoints.map(bulletPoint => (
{bulletPoints.map((bulletPoint) => (
<li key={bulletPoint} className="flex items-start gap-1.5">
<div className="m-0 flex">
<div className="m-0 flex">

View File

@ -3,6 +3,6 @@
*
* Barrel file for the Paywall components.
*/
export * from './PaywallLock'
export * from './PaywallBulletPoints'
export * from './PaywallButton'
export * from './PaywallLock'

View File

@ -3,10 +3,10 @@
*
* Barrel file for Paywall components.
*/
export * from './PaywallScreen'
export * from './PaywallDialogButton'
export * from './PaywallDialog'
export * from './UpgradeButton'
export * from './PaywallAlert'
export * from './ContextMenuEntry'
export { PaywallButton, type PaywallButtonProps } from './components'
export * from './ContextMenuEntry'
export * from './PaywallAlert'
export * from './PaywallDialog'
export * from './PaywallDialogButton'
export * from './PaywallScreen'
export * from './UpgradeButton'

View File

@ -28,7 +28,7 @@ export function usePortal(props: types.PortalProps) {
invariant(
!(contextRoot == null && currentRoot == null),
'Before using Portal, you need to specify a root, where the component should be mounted or put the component under the <Root /> component'
'Before using Portal, you need to specify a root, where the component should be mounted or put the component under the <Root /> component',
)
setMountRoot(currentRoot ?? contextRoot)

View File

@ -113,40 +113,32 @@ export function Result(props: ResultProps) {
return (
<section className={classes.base({ className })} data-testid={testId}>
{showIcon ? (
{showIcon ?
<>
{statusIcon != null ? (
{statusIcon != null ?
<div className={classes.statusIcon({ className: statusIcon.bgClassName })}>
{typeof statusIcon.icon === 'string' ? (
{typeof statusIcon.icon === 'string' ?
<SvgMask
src={icon ?? statusIcon.icon}
className={classes.icon({ className: statusIcon.colorClassName })}
/>
) : (
statusIcon.icon
)}
: statusIcon.icon}
</div>
) : (
status
)}
: status}
</>
) : null}
: null}
{typeof title === 'string' ? (
{typeof title === 'string' ?
<ariaComponents.Text.Heading level={2} className={classes.title()} variant="subtitle">
{title}
</ariaComponents.Text.Heading>
) : (
title
)}
: title}
{typeof subtitle === 'string' ? (
{typeof subtitle === 'string' ?
<ariaComponents.Text elementType="p" className={classes.subtitle()} balance variant="body">
{subtitle}
</ariaComponents.Text>
) : (
subtitle
)}
: subtitle}
{children != null && <div className={classes.content()}>{children}</div>}
</section>

View File

@ -51,7 +51,7 @@ export default function SelectionBrush(props: SelectionBrushProps) {
const [lastSetAnchor, setLastSetAnchor] = React.useState<geometry.Coordinate2D | null>(null)
const anchorAnimFactor = animationHooks.useApproach(
anchor != null ? 1 : 0,
ANIMATION_TIME_HORIZON
ANIMATION_TIME_HORIZON,
)
const hidden =
anchor == null ||
@ -112,19 +112,19 @@ export default function SelectionBrush(props: SelectionBrushProps) {
didMoveWhileDraggingRef.current = true
lastMouseEvent.current = event
const positionLeft =
parentBounds.current == null
? event.pageX
: Math.max(
parentBounds.current.left - margin,
Math.min(parentBounds.current.right + margin, event.pageX)
)
parentBounds.current == null ?
event.pageX
: Math.max(
parentBounds.current.left - margin,
Math.min(parentBounds.current.right + margin, event.pageX),
)
const positionTop =
parentBounds.current == null
? event.pageY
: Math.max(
parentBounds.current.top - margin,
Math.min(parentBounds.current.bottom + margin, event.pageY)
)
parentBounds.current == null ?
event.pageY
: Math.max(
parentBounds.current.top - margin,
Math.min(parentBounds.current.bottom + margin, event.pageY),
)
setPosition({ left: positionLeft, top: positionTop })
}
}
@ -188,20 +188,20 @@ export default function SelectionBrush(props: SelectionBrushProps) {
}, [selectionRectangle])
const brushStyle =
rectangle == null
? {}
: {
left: `${rectangle.left}px`,
top: `${rectangle.top}px`,
width: `${rectangle.width}px`,
height: `${rectangle.height}px`,
}
rectangle == null ?
{}
: {
left: `${rectangle.left}px`,
top: `${rectangle.top}px`,
width: `${rectangle.width}px`,
height: `${rectangle.height}px`,
}
return (
<Portal>
<div
className={tailwindMerge.twMerge(
'pointer-events-none fixed z-1 box-content rounded-selection-brush border-transparent bg-selection-brush transition-border-margin',
hidden ? 'm border-0' : '-m-selection-brush-border border-selection-brush'
hidden ? 'm border-0' : '-m-selection-brush-border border-selection-brush',
)}
style={brushStyle}
/>

View File

@ -57,7 +57,7 @@ export default function Spinner(props: SpinnerProps) {
strokeWidth={3}
className={tailwindMerge.twMerge(
'pointer-events-none origin-center !animate-spin-ease transition-stroke-dasharray [transition-duration:var(--spinner-slow-transition-duration)]',
SPINNER_CSS_CLASSES[state]
SPINNER_CSS_CLASSES[state],
)}
/>
</svg>

View File

@ -63,7 +63,7 @@ export function Loader(props: SuspenseProps) {
const paused = reactQuery.useIsFetching({ fetchStatus: 'paused' })
const fetching = reactQuery.useIsFetching({
predicate: query =>
predicate: (query) =>
query.state.fetchStatus === 'fetching' ||
query.state.status === 'pending' ||
query.state.status === 'success',
@ -73,7 +73,7 @@ export function Loader(props: SuspenseProps) {
// but fallback is still showing
const shouldDisplayOfflineMessage = debounceValue.useDebounceValue(
isOffline && paused >= 0 && fetching === 0,
OFFLINE_FETCHING_TOGGLE_DELAY_MS
OFFLINE_FETCHING_TOGGLE_DELAY_MS,
)
if (shouldDisplayOfflineMessage) {

View File

@ -23,7 +23,7 @@ export default function SvgIcon(props: SvgIconProps) {
<div
className={tailwindMerge.twMerge(
'absolute left-0 top-0 inline-flex h-full w-auth-icon-container items-center justify-center text-gray-400',
className
className,
)}
onClick={onClick}
>

View File

@ -21,9 +21,10 @@ export interface TwemojiProps {
// Only accepts strings that are two code points - for example, emojis.
/** Returns the input type if it consists of two codepoints. Otherwise, it returns
* an error message. */
type MustBeLength2String<T extends string> = T extends `${string}${string}${infer Rest}`
? Rest extends ''
? T
type MustBeLength2String<T extends string> =
T extends `${string}${string}${infer Rest}` ?
Rest extends '' ?
T
: 'Error: string must have a length of 2'
: 'Error: string must have a length of 2'

View File

@ -88,7 +88,7 @@ export interface AssetRowProps
readonly onClick: (props: AssetRowInnerProps, event: React.MouseEvent) => void
readonly onContextMenu?: (
props: AssetRowInnerProps,
event: React.MouseEvent<HTMLTableRowElement>
event: React.MouseEvent<HTMLTableRowElement>,
) => void
readonly updateAssetRef: React.Ref<(asset: backendModule.AnyAsset) => void>
}
@ -118,15 +118,15 @@ export default function AssetRow(props: AssetRowProps) {
const asset = item.item
const [insertionVisibility, setInsertionVisibility] = React.useState(Visibility.visible)
const [rowState, setRowState] = React.useState<assetsTable.AssetRowState>(() =>
object.merge(assetRowUtils.INITIAL_ROW_STATE, { setVisibility: setInsertionVisibility })
object.merge(assetRowUtils.INITIAL_ROW_STATE, { setVisibility: setInsertionVisibility }),
)
const key = AssetTreeNode.getKey(item)
const isCloud = backend.type === backendModule.BackendType.remote
const outerVisibility = visibilities.get(key)
const visibility =
outerVisibility == null || outerVisibility === Visibility.visible
? insertionVisibility
: outerVisibility
outerVisibility == null || outerVisibility === Visibility.visible ?
insertionVisibility
: outerVisibility
const hidden = hiddenRaw || visibility === Visibility.hidden
const copyAssetMutation = backendHooks.useBackendMutation(backend, 'copyAsset')
@ -176,13 +176,13 @@ export default function AssetRow(props: AssetRowProps) {
const doCopyOnBackend = React.useCallback(
async (newParentId: backendModule.DirectoryId | null) => {
try {
setAsset(oldAsset =>
setAsset((oldAsset) =>
object.merge(oldAsset, {
title: oldAsset.title + ' (copy)',
labels: [],
permissions: permissions.tryGetSingletonOwnerPermission(user),
modifiedAt: dateTime.toRfc3339(new Date()),
})
}),
)
newParentId ??= rootDirectoryId
const copiedAsset = await copyAssetMutate([
@ -198,7 +198,7 @@ export default function AssetRow(props: AssetRowProps) {
object.merger({
...copiedAsset.asset,
state: { type: backendModule.ProjectState.new },
} as Partial<backendModule.AnyAsset>)
} as Partial<backendModule.AnyAsset>),
)
} catch (error) {
toastAndLog('copyAssetError', error, asset.title)
@ -216,19 +216,19 @@ export default function AssetRow(props: AssetRowProps) {
nodeMap,
setAsset,
dispatchAssetListEvent,
]
],
)
const doMove = React.useCallback(
async (
newParentKey: backendModule.DirectoryId | null,
newParentId: backendModule.DirectoryId | null
newParentId: backendModule.DirectoryId | null,
) => {
const nonNullNewParentKey = newParentKey ?? rootDirectoryId
const nonNullNewParentId = newParentId ?? rootDirectoryId
try {
setItem(oldItem =>
oldItem.with({ directoryKey: nonNullNewParentKey, directoryId: nonNullNewParentId })
setItem((oldItem) =>
oldItem.with({ directoryKey: nonNullNewParentKey, directoryId: nonNullNewParentId }),
)
const newParentPath = localBackend.extractTypeAndId(nonNullNewParentId).id
let newId = asset.id
@ -282,10 +282,10 @@ export default function AssetRow(props: AssetRowProps) {
id: asset.id as never,
parentId: asset.parentId,
projectState: asset.projectState,
})
}),
)
setItem(oldItem =>
oldItem.with({ directoryKey: item.directoryKey, directoryId: item.directoryId })
setItem((oldItem) =>
oldItem.with({ directoryKey: item.directoryKey, directoryId: item.directoryId }),
)
// Move the asset back to its original position.
dispatchAssetListEvent({
@ -308,7 +308,7 @@ export default function AssetRow(props: AssetRowProps) {
updateAssetMutate,
setAsset,
dispatchAssetListEvent,
]
],
)
React.useEffect(() => {
@ -364,7 +364,7 @@ export default function AssetRow(props: AssetRowProps) {
deleteAssetMutate,
item.key,
toastAndLog,
]
],
)
const doRestore = React.useCallback(async () => {
@ -382,24 +382,24 @@ export default function AssetRow(props: AssetRowProps) {
const doTriggerDescriptionEdit = React.useCallback(() => {
setModal(
<EditAssetDescriptionModal
doChangeDescription={async description => {
doChangeDescription={async (description) => {
if (description !== asset.description) {
setAsset(object.merger({ description }))
await backend
.updateAsset(item.item.id, { parentDirectoryId: null, description }, item.item.title)
.catch(error => {
.catch((error) => {
setAsset(object.merger({ description: asset.description }))
throw error
})
}
}}
initialDescription={asset.description}
/>
/>,
)
}, [setModal, asset.description, setAsset, backend, item.item.id, item.item.title])
eventListProvider.useAssetEventListener(async event => {
eventListProvider.useAssetEventListener(async (event) => {
if (state.category === Category.trash) {
switch (event.type) {
case AssetEventType.deleteForever: {
@ -521,9 +521,9 @@ export default function AssetRow(props: AssetRowProps) {
URL.createObjectURL(
new File([JSON.stringify(value)], fileName, {
type: 'application/json+x-enso-data-link',
})
}),
),
fileName
fileName,
)
} catch (error) {
toastAndLog('downloadDatalinkError', error, asset.title)
@ -542,7 +542,7 @@ export default function AssetRow(props: AssetRowProps) {
const queryString = new URLSearchParams({ projectsDirectory }).toString()
await backend.download(
`./api/project-manager/projects/${uuid}/enso-project?${queryString}`,
`${asset.title}.enso-project`
`${asset.title}.enso-project`,
)
}
}
@ -571,44 +571,48 @@ export default function AssetRow(props: AssetRowProps) {
}
case AssetEventType.temporarilyAddLabels: {
const labels = event.ids.has(item.key) ? event.labelNames : set.EMPTY
setRowState(oldRowState =>
oldRowState.temporarilyAddedLabels === labels &&
oldRowState.temporarilyRemovedLabels === set.EMPTY
? oldRowState
: object.merge(oldRowState, {
temporarilyAddedLabels: labels,
temporarilyRemovedLabels: set.EMPTY,
})
setRowState((oldRowState) =>
(
oldRowState.temporarilyAddedLabels === labels &&
oldRowState.temporarilyRemovedLabels === set.EMPTY
) ?
oldRowState
: object.merge(oldRowState, {
temporarilyAddedLabels: labels,
temporarilyRemovedLabels: set.EMPTY,
}),
)
break
}
case AssetEventType.temporarilyRemoveLabels: {
const labels = event.ids.has(item.key) ? event.labelNames : set.EMPTY
setRowState(oldRowState =>
oldRowState.temporarilyAddedLabels === set.EMPTY &&
oldRowState.temporarilyRemovedLabels === labels
? oldRowState
: object.merge(oldRowState, {
temporarilyAddedLabels: set.EMPTY,
temporarilyRemovedLabels: labels,
})
setRowState((oldRowState) =>
(
oldRowState.temporarilyAddedLabels === set.EMPTY &&
oldRowState.temporarilyRemovedLabels === labels
) ?
oldRowState
: object.merge(oldRowState, {
temporarilyAddedLabels: set.EMPTY,
temporarilyRemovedLabels: labels,
}),
)
break
}
case AssetEventType.addLabels: {
setRowState(oldRowState =>
oldRowState.temporarilyAddedLabels === set.EMPTY
? oldRowState
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY })
setRowState((oldRowState) =>
oldRowState.temporarilyAddedLabels === set.EMPTY ?
oldRowState
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY }),
)
const labels = asset.labels
if (
event.ids.has(item.key) &&
(labels == null || [...event.labelNames].some(label => !labels.includes(label)))
(labels == null || [...event.labelNames].some((label) => !labels.includes(label)))
) {
const newLabels = [
...(labels ?? []),
...[...event.labelNames].filter(label => labels?.includes(label) !== true),
...[...event.labelNames].filter((label) => labels?.includes(label) !== true),
]
setAsset(object.merger({ labels: newLabels }))
try {
@ -621,18 +625,18 @@ export default function AssetRow(props: AssetRowProps) {
break
}
case AssetEventType.removeLabels: {
setRowState(oldRowState =>
oldRowState.temporarilyAddedLabels === set.EMPTY
? oldRowState
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY })
setRowState((oldRowState) =>
oldRowState.temporarilyAddedLabels === set.EMPTY ?
oldRowState
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY }),
)
const labels = asset.labels
if (
event.ids.has(item.key) &&
labels != null &&
[...event.labelNames].some(label => labels.includes(label))
[...event.labelNames].some((label) => labels.includes(label))
) {
const newLabels = labels.filter(label => !event.labelNames.has(label))
const newLabels = labels.filter((label) => !event.labelNames.has(label))
setAsset(object.merger({ labels: newLabels }))
try {
await associateTagMutation.mutateAsync([asset.id, newLabels, asset.title])
@ -644,11 +648,11 @@ export default function AssetRow(props: AssetRowProps) {
break
}
case AssetEventType.deleteLabel: {
setAsset(oldAsset => {
setAsset((oldAsset) => {
// The IIFE is required to prevent TypeScript from narrowing this value.
let found = (() => false)()
const labels =
oldAsset.labels?.filter(label => {
oldAsset.labels?.filter((label) => {
if (label === event.labelName) {
found = true
return false
@ -672,10 +676,10 @@ export default function AssetRow(props: AssetRowProps) {
const clearDragState = React.useCallback(() => {
setIsDraggedOver(false)
setRowState(oldRowState =>
oldRowState.temporarilyAddedLabels === set.EMPTY
? oldRowState
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY })
setRowState((oldRowState) =>
oldRowState.temporarilyAddedLabels === set.EMPTY ?
oldRowState
: object.merge(oldRowState, { temporarilyAddedLabels: set.EMPTY }),
)
}, [])
@ -684,7 +688,7 @@ export default function AssetRow(props: AssetRowProps) {
item.item.type === backendModule.AssetType.directory ? item.key : item.directoryKey
const payload = drag.ASSET_ROWS.lookup(event)
if (
(payload != null && payload.every(innerItem => innerItem.key !== directoryKey)) ||
(payload != null && payload.every((innerItem) => innerItem.key !== directoryKey)) ||
event.dataTransfer.types.includes('Files')
) {
event.preventDefault()
@ -710,7 +714,7 @@ export default function AssetRow(props: AssetRowProps) {
<FocusRing>
<tr
tabIndex={0}
ref={element => {
ref={(element) => {
rootRef.current = element
if (isSoleSelected && element != null && scrollContainerRef.current != null) {
const rect = element.getBoundingClientRect()
@ -731,10 +735,10 @@ export default function AssetRow(props: AssetRowProps) {
className={tailwindMerge.twMerge(
'h-table-row rounded-full transition-all ease-in-out rounded-rows-child',
visibility,
(isDraggedOver || selected) && 'selected'
(isDraggedOver || selected) && 'selected',
)}
{...draggableProps}
onClick={event => {
onClick={(event) => {
unsetModal()
onClick(innerProps, event)
if (
@ -750,7 +754,7 @@ export default function AssetRow(props: AssetRowProps) {
doToggleDirectoryExpansion(item.item.id, item.key, asset.title)
}
}}
onContextMenu={event => {
onContextMenu={(event) => {
if (allowContextMenu) {
event.preventDefault()
event.stopPropagation()
@ -768,20 +772,20 @@ export default function AssetRow(props: AssetRowProps) {
doPaste={doPaste}
doDelete={doDelete}
doTriggerDescriptionEdit={doTriggerDescriptionEdit}
/>
/>,
)
} else {
onContextMenu?.(innerProps, event)
}
}}
onDragStart={event => {
onDragStart={(event) => {
if (rowState.isEditingName) {
event.preventDefault()
} else {
props.onDragStart?.(event)
}
}}
onDragEnter={event => {
onDragEnter={(event) => {
if (dragOverTimeoutHandle.current != null) {
window.clearTimeout(dragOverTimeoutHandle.current)
}
@ -794,18 +798,18 @@ export default function AssetRow(props: AssetRowProps) {
props.onDragOver?.(event)
onDragOver(event)
}}
onDragOver={event => {
onDragOver={(event) => {
if (state.category === Category.trash) {
event.dataTransfer.dropEffect = 'none'
}
props.onDragOver?.(event)
onDragOver(event)
}}
onDragEnd={event => {
onDragEnd={(event) => {
clearDragState()
props.onDragEnd?.(event)
}}
onDragLeave={event => {
onDragLeave={(event) => {
if (
dragOverTimeoutHandle.current != null &&
(!(event.relatedTarget instanceof Node) ||
@ -821,26 +825,26 @@ export default function AssetRow(props: AssetRowProps) {
}
props.onDragLeave?.(event)
}}
onDrop={event => {
onDrop={(event) => {
if (state.category !== Category.trash) {
props.onDrop?.(event)
clearDragState()
const [directoryKey, directoryId, directoryTitle] =
item.type === backendModule.AssetType.directory
? [item.key, item.item.id, asset.title]
: [item.directoryKey, item.directoryId, null]
item.type === backendModule.AssetType.directory ?
[item.key, item.item.id, asset.title]
: [item.directoryKey, item.directoryId, null]
const payload = drag.ASSET_ROWS.lookup(event)
if (
payload != null &&
payload.every(innerItem => innerItem.key !== directoryKey)
payload.every((innerItem) => innerItem.key !== directoryKey)
) {
event.preventDefault()
event.stopPropagation()
unsetModal()
doToggleDirectoryExpansion(directoryId, directoryKey, directoryTitle, true)
const ids = payload
.filter(payloadItem => payloadItem.asset.parentId !== directoryId)
.map(dragItem => dragItem.key)
.filter((payloadItem) => payloadItem.asset.parentId !== directoryId)
.map((dragItem) => dragItem.key)
dispatchAssetEvent({
type: AssetEventType.move,
newParentKey: directoryKey,
@ -861,7 +865,7 @@ export default function AssetRow(props: AssetRowProps) {
}
}}
>
{columns.map(column => {
{columns.map((column) => {
// This is a React component even though it does not contain JSX.
// eslint-disable-next-line no-restricted-syntax
const Render = columnModule.COLUMN_RENDERER[column]
@ -916,38 +920,38 @@ export default function AssetRow(props: AssetRowProps) {
}
case backendModule.AssetType.specialLoading: {
return hidden ? null : (
<tr>
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
<div
className={tailwindMerge.twMerge(
'flex h-table-row w-container items-center justify-center rounded-full rounded-rows-child',
indent.indentClass(item.depth)
)}
>
<StatelessSpinner size={24} state={statelessSpinner.SpinnerState.loadingMedium} />
</div>
</td>
</tr>
)
<tr>
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
<div
className={tailwindMerge.twMerge(
'flex h-table-row w-container items-center justify-center rounded-full rounded-rows-child',
indent.indentClass(item.depth),
)}
>
<StatelessSpinner size={24} state={statelessSpinner.SpinnerState.loadingMedium} />
</div>
</td>
</tr>
)
}
case backendModule.AssetType.specialEmpty: {
return hidden ? null : (
<tr>
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
<div
className={tailwindMerge.twMerge(
'flex h-table-row items-center rounded-full rounded-rows-child',
indent.indentClass(item.depth)
)}
>
<img src={BlankIcon} />
<aria.Text className="px-name-column-x placeholder">
{getText('thisFolderIsEmpty')}
</aria.Text>
</div>
</td>
</tr>
)
<tr>
<td colSpan={columns.length} className="border-r p-0 rounded-rows-skip-level">
<div
className={tailwindMerge.twMerge(
'flex h-table-row items-center rounded-full rounded-rows-child',
indent.indentClass(item.depth),
)}
>
<img src={BlankIcon} />
<aria.Text className="px-name-column-x placeholder">
{getText('thisFolderIsEmpty')}
</aria.Text>
</div>
</td>
</tr>
)
}
}
}

View File

@ -34,7 +34,7 @@ export default function AssetSummary(props: AssetSummaryProps) {
<div
className={tailwindMerge.twMerge(
'flex min-h-row items-center gap-icon-with-text rounded-default bg-frame px-button-x',
className
className,
)}
>
<div className="grid size-4 place-items-center">

Some files were not shown because too many files have changed in this diff Show More