mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 11:52:59 +03:00
Format TS code (#10648)
This commit is contained in:
parent
4b96be8ef8
commit
c46262dfb5
@ -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>
|
||||
|
@ -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')
|
||||
}),
|
||||
),
|
||||
);
|
||||
)
|
||||
```
|
||||
|
@ -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 }))
|
||||
}
|
||||
|
||||
// ===================================
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -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[)]/)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -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))
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -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)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -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)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -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)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -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]!)
|
||||
|
@ -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()
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -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/)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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 />}>
|
||||
|
@ -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 ===
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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)
|
||||
}}
|
||||
>
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export function DialogTrigger(props: DialogTriggerProps) {
|
||||
|
||||
onOpenChange?.(isOpened)
|
||||
},
|
||||
[setModal, unsetModal, onOpenChange]
|
||||
[setModal, unsetModal, onOpenChange],
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -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}
|
||||
|
@ -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'
|
||||
|
@ -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 */
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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, '<')
|
||||
.replace(/>/g, '>')
|
||||
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
|
||||
|
@ -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 })
|
||||
}
|
||||
|
@ -3,5 +3,5 @@
|
||||
*
|
||||
* Barrel export file for ResizableInput component.
|
||||
*/
|
||||
export * from './ResizableInput'
|
||||
export * from './ResizableContentEditableInput'
|
||||
export * from './ResizableInput'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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} />
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
}}
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 && (
|
||||
|
@ -44,7 +44,7 @@ export function ErrorBoundary(props: ErrorBoundaryProps) {
|
||||
sentry.captureException(error, { extra: { info } })
|
||||
onError(error, info)
|
||||
}}
|
||||
onReset={details => {
|
||||
onReset={(details) => {
|
||||
reset()
|
||||
onReset(details)
|
||||
}}
|
||||
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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} />,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
@ -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">
|
||||
|
@ -3,6 +3,6 @@
|
||||
*
|
||||
* Barrel file for the Paywall components.
|
||||
*/
|
||||
export * from './PaywallLock'
|
||||
export * from './PaywallBulletPoints'
|
||||
export * from './PaywallButton'
|
||||
export * from './PaywallLock'
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user