Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion e2e/steps/account.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, type Page } from '@playwright/test';
import { test, expect, type Page } from '@playwright/test';

type Metadata = {
name: string;
Expand Down Expand Up @@ -27,6 +27,8 @@ export function registerUserStep(page: Page): Promise<Metadata> {
await inputs.terms.check({ force: true });
await page.getByRole('button', { name: 'Sign up', exact: true }).click();
await page.waitForURL('./onboarding/create-project');
await expect(page.getByRole('button', { name: 'Create', exact: true })).toBeVisible();
await expect(page.getByRole('dialog', { name: 'The new Appwrite Console' })).toHaveCount(0);

return values;
});
Expand Down
2 changes: 2 additions & 0 deletions e2e/steps/free-project.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { test, expect, type Page } from '@playwright/test';
import { getOrganizationIdFromUrl, getProjectIdFromUrl } from '../helpers/url';
import { dismissNewConsolePromotion } from './new-console';

type Metadata = {
id: string;
Expand All @@ -10,6 +11,7 @@ export async function createFreeProject(page: Page): Promise<Metadata> {
const organizationId = await test.step('create organization', async () => {
await page.goto('./');
await page.waitForURL(/\/organization-[^/]+/);
await dismissNewConsolePromotion(page);
return getOrganizationIdFromUrl(page.url());
});

Expand Down
10 changes: 10 additions & 0 deletions e2e/steps/new-console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { test, expect, type Page } from '@playwright/test';

export function dismissNewConsolePromotion(page: Page) {
return test.step('dismiss new Console promotion after onboarding', async () => {
const dialog = page.getByRole('dialog', { name: 'The new Appwrite Console' });
await expect(dialog).toBeVisible();
await dialog.getByRole('button', { name: 'Stay here for now' }).click();
await expect(dialog).toHaveCount(0);
});
}
2 changes: 2 additions & 0 deletions e2e/steps/pro-project.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { test, expect, type Page } from '@playwright/test';
import { getOrganizationIdFromUrl, getProjectIdFromUrl } from '../helpers/url';
import { dismissNewConsolePromotion } from './new-console';

type Metadata = {
id: string;
Expand Down Expand Up @@ -34,6 +35,7 @@ export async function enterCreditCard(page: Page) {
export async function createProProject(page: Page): Promise<Metadata> {
const organizationId = await test.step('create organization', async () => {
await page.goto('./create-organization');
await dismissNewConsolePromotion(page);
await page.locator('id=name').fill('test org');
await page.getByRole('radio', { name: /^Pro\b/ }).check();
// `create organization` because there's already free created on start!
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@codemirror/search": "^6.7.0",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.43.0",
"@faker-js/faker": "^9.9.0",
"@faker-js/faker": "^10.6.0",
"@lezer/highlight": "^1.2.3",
"@plausible-analytics/tracker": "^0.4.5",
"@popperjs/core": "^2.11.8",
Expand Down Expand Up @@ -104,7 +104,7 @@
"vite": "npm:rolldown-vite@latest",
"minimatch": "10.2.3",
"brace-expansion": ">=5.0.9",
"js-yaml": "^4.3.1",
"js-yaml": "^4.3.2",
"immutable": "^5.1.8",
"flatted": "^3.4.2",
"devalue": "^5.8.1",
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export { default as ImagePreview } from './imagePreview.svelte';
export { default as MfaChallengeFormList } from './mfaChallengeFormList.svelte';
export { default as BottomModalAlert } from './bottomModalAlert.svelte';
export { default as NewConsoleBanner } from './newConsoleBanner.svelte';
export { default as NewConsoleModal } from './newConsoleModal.svelte';
export { default as Navbar } from './navbar.svelte';
export { default as Breadcrumbs } from './breadcrumbs.svelte';
export { default as Sidebar } from './sidebar.svelte';
Expand Down
6 changes: 3 additions & 3 deletions src/lib/components/newConsoleBanner.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { trackEvent } from '$lib/actions/analytics';
import { Button } from '$lib/elements/forms';
import { Layout, Typography } from '@appwrite.io/pink-svelte';
import { isTabletViewport } from '$lib/stores/viewport';
import { isSmallViewport } from '$lib/stores/viewport';
import { hideNotification } from '$lib/helpers/notifications';
import { headerAlert } from '$lib/stores/headerAlert';
import { activeHeaderAlert } from '$routes/(console)/store';
Expand Down Expand Up @@ -35,8 +35,8 @@
alignItems="center"
alignContent="center"
justifyContent="center"
direction={$isTabletViewport ? 'column' : 'row'}>
<Typography.Text align={$isTabletViewport ? 'center' : 'start'}>
direction={$isSmallViewport ? 'column' : 'row'}>
<Typography.Text align={$isSmallViewport ? 'center' : 'start'}>
Introducing the new Appwrite Console, rebuilt from the ground up.
</Typography.Text>

Expand Down
242 changes: 242 additions & 0 deletions src/lib/components/newConsoleModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { trackEvent } from '$lib/actions/analytics';
import { disableCommands } from '$lib/commandCenter';
import { hideNotification } from '$lib/helpers/notifications';
import NewConsoleCover from '$lib/images/promos/new-console-modal.png';
import { Alert, Button, Icon, Typography } from '@appwrite.io/pink-svelte';
import { IconArrowSmRight, IconX } from '@appwrite.io/pink-icons-svelte';

let { show = $bindable(false) }: { show?: boolean } = $props();
const isOnOnboarding = $derived(
page.url.pathname === `${base}/onboarding` ||
page.url.pathname.startsWith(`${base}/onboarding/`)
);

Comment thread
greptile-apps[bot] marked this conversation as resolved.
$effect(() => {
$disableCommands(show && !isOnOnboarding);
});

// @todo: replace with the confirmed retirement date once it is set.
const SUNSET_NOTE = 'The old Console will be retired in the coming weeks.';
Comment thread
greptile-apps[bot] marked this conversation as resolved.

// utm_medium separates this from the banner and the promo card.
const href =
'https://appwrite.io/?utm_source=old-console&utm_medium=modal&utm_campaign=new-console';

// The most interruptive of the three surfaces, so it snoozes for a month rather than the
// banner's week, and still doubles on each dismissal.
const COOL_OFF_HOURS = 24 * 30;

let recorded = false;

function close(action: 'try' | 'continue') {
if (!show || recorded) return;

recorded = true;
trackEvent('close_new_console_modal', { source: 'new_console_modal', action });
hideNotification('newConsoleModal', {
coolOffPeriod: COOL_OFF_HOURS,
exponentialBackoff: true
});

show = false;
}
Comment on lines +34 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Dismissal behavior is untested

The new modal has bespoke handling for Escape, backdrop clicks, action choices, one-time snoozing, and navigation behavior, but none of these observable paths is covered by a test. A later change could silently restore premature dismissal, double-snoozing, or navigation-triggered dismissal. Add behavior-level coverage that exercises user exits and verifies the resulting notification state rather than mirroring constants or source structure.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/components/newConsoleModal.svelte
Line: 24-39

Comment:
**Dismissal behavior is untested**

The new modal has bespoke handling for Escape, backdrop clicks, action choices, one-time snoozing, and navigation behavior, but none of these observable paths is covered by a test. A later change could silently restore premature dismissal, double-snoozing, or navigation-triggered dismissal. Add behavior-level coverage that exercises user exits and verifies the resulting notification state rather than mirroring constants or source structure.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex


function openDialog(node: HTMLDialogElement) {
const previousFocus = document.activeElement;
recorded = false;
// Native modality contains keyboard focus and makes the rest of the page inert.
node.showModal();

return {
destroy() {
node.close();
// Also restore focus when Svelte removes the dialog during teardown.
if (previousFocus instanceof HTMLElement && previousFocus.isConnected) {
previousFocus.focus({ preventScroll: true });
}
}
};
}
</script>

{#if show && !isOnOnboarding}
<dialog
class="scrim"
aria-labelledby="new-console-title"
use:openDialog
oncancel={(event) => {
event.preventDefault();
close('continue');
}}
onkeydown={(event) => {
// Let the native cancel event close only this dialog, not a background wizard.
if (event.key === 'Escape') event.stopPropagation();
}}
onclick={(event) => {
if (event.target === event.currentTarget) close('continue');
}}>
<div class="dialog">
<button
class="dismiss"
type="button"
aria-label="Close"
onclick={() => close('continue')}>
<Icon icon={IconX} size="s" />
</button>

<img class="cover" src={NewConsoleCover} alt="" />

<div class="body">
<h2 id="new-console-title">The new Appwrite Console</h2>
<p class="lede">
Faster, redesigned, and everything you're working on comes with you. Nothing to
migrate.
</p>

<div class="notice">
<Alert.Inline status="info">
<Typography.Text>
{SUNSET_NOTE} You can switch over any time from the top bar.
</Typography.Text>
</Alert.Inline>
</div>
</div>

<div class="actions">
<Button.Anchor
{href}
size="s"
target="_blank"
rel="noopener noreferrer"
onclick={() => {
trackEvent('click_new_console', { source: 'new_console_modal' });
close('try');
}}>
Take me to the new Console
<Icon icon={IconArrowSmRight} size="s" slot="end" />
</Button.Anchor>

<Button.Button
type="button"
variant="text"
size="s"
onclick={() => close('continue')}>Stay here for now</Button.Button>
</div>
</div>
</dialog>
{/if}

<style lang="scss">
.scrim {
box-sizing: border-box;
position: fixed;
inset: 0;
inline-size: 100%;
max-inline-size: 100%;
block-size: 100%;
max-block-size: 100%;
margin: 0;
border: none;
background: transparent;
padding: var(--space-6, 12px);
overflow-y: auto;
}

.scrim[open] {
display: grid;
place-items: center;
align-items: safe center;
}

.scrim::backdrop {
background: var(--overlay-scrim);
backdrop-filter: blur(4px);
}

.dialog {
position: relative;
inline-size: min(100%, 30rem);
overflow: hidden;
outline: none;
border-radius: var(--border-radius-m, 12px);
border: var(--border-width-s, 1px) solid var(--border-neutral);
background: var(--bgcolor-neutral-primary);
box-shadow: 0 24px 64px rgb(0 0 0 / 24%);
}

/* The source is a wide 2400x1260 capture, so it is cropped rather than letterboxed. */
.cover {
display: block;
inline-size: 100%;
block-size: 11rem;
object-fit: cover;
object-position: center;
border-block-end: var(--border-width-s, 1px) solid var(--border-neutral);
}

.body {
padding: var(--space-9, 24px) var(--space-9, 24px) 0;
}

h2 {
margin: 0 0 var(--space-3, 6px);
font-size: var(--font-size-l, 20px);
font-weight: 600;
line-height: 1.3;
color: var(--fgcolor-neutral-primary);
}

.lede {
margin: 0;
font-size: var(--font-size-s, 14px);
line-height: 1.6;
color: var(--fgcolor-neutral-secondary);
text-wrap: pretty;
}

.notice {
margin-block-start: var(--space-7, 16px);
}

.actions {
display: flex;
align-items: center;
gap: var(--space-4, 8px);
padding: var(--space-8, 20px) var(--space-9, 24px) var(--space-9, 24px);
}

.dismiss {
position: absolute;
z-index: 2;
inset-block-start: var(--space-5, 10px);
inset-inline-end: var(--space-5, 10px);
display: grid;
place-items: center;
inline-size: 28px;
block-size: 28px;
border: none;
border-radius: var(--border-radius-xs, 6px);
background: rgb(0 0 0 / 35%);
color: #fff;
cursor: pointer;

&:hover {
background: rgb(0 0 0 / 55%);
}

&:focus-visible {
outline: 2px solid var(--border-accent);
outline-offset: 2px;
}
}

@media (max-width: 768px) {
.actions {
flex-direction: column;
align-items: stretch;
}
}
</style>
Loading