Skip to content
Draft
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
52 changes: 52 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Repository Guidelines

## Project Structure & Module Organization

- `src/`: SvelteKit app source.
- `src/lib/`: reusable components, elements, actions, helpers, stores, charts, images.
- `src/routes/`: route groups and pages (e.g., `+layout.svelte`, `+page.ts`).
- `src/themes/`, `hooks.*.ts`, `service-worker.ts`: app-wide setup.
- `static/`: public assets served as-is.
- `build/`: compiled output (local builds).
- `e2e/`: Playwright tests (`helpers/`, `journeys/`, `steps/`).
- Tooling: `eslint.config.js`, `.prettierrc`, `playwright.config.ts`, `vitest-setup-client.ts`.

## Build, Test, and Development Commands

- `pnpm dev`: start Vite dev server.
- `pnpm build`: production build via `build.js`.
- `pnpm preview`: preview the production build.
- `pnpm check`: Svelte/TS diagnostics.
- `pnpm format` / `pnpm lint`: format with Prettier, then lint with ESLint.
- `pnpm test` | `pnpm test:watch` | `pnpm test:ui`: run Vitest (unit/component tests).
- `pnpm e2e` | `pnpm e2e:ui`: run Playwright tests; config auto-builds and serves.
Requires Node >= 20 and pnpm (see `packageManager`).

## Coding Style & Naming Conventions

- Language: TypeScript + Svelte.
- Prettier: 4‑space indentation, single quotes, width 100, bracketSameLine.
- Svelte files: PascalCase components in `src/lib`, route files follow SvelteKit (`+page.svelte`, `+layout.ts`).
- Variables/functions: `camelCase`; constants: `SCREAMING_SNAKE_CASE`; types/interfaces: `PascalCase`.
- Keep logic in `$lib` and thin route loaders/actions.

## Testing Guidelines

- Unit/component: Vitest + Testing Library.
- Place near source or under `src/`; name `*.test.ts`/`*.test.svelte`.
- Run with `pnpm test`.
- End‑to‑end: Playwright under `e2e/`.
- Start with `pnpm e2e` (uses `playwright.config.ts` to build/preview on port 4173).

## Commit & Pull Request Guidelines

- Branch names: `TYPE-ISSUEID-short-description` (e.g., `feat-1234-add-billing-chart`).
- Commits: imperative, concise, reference issues when relevant.
- PRs: clear description, linked issues, screenshots/GIFs for UI changes, test coverage notes, and checklist of affected areas.
- Keep PRs focused; include `pnpm format`, `pnpm lint`, `pnpm check`, and `pnpm test` clean runs before requesting review.

## Security & Configuration Tips

- Do not commit secrets; use `.env` based on `.env.example`.
- Sentry/Stripe and other public keys are configured via env vars; prefer `PUBLIC_` prefix for client‑exposed values.
- Use `compose.yml`/`Dockerfile` for containerized workflows when needed.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ RUN corepack prepare [email protected] --activate
ADD ./package.json /app/package.json
ADD ./pnpm-lock.yaml /app/pnpm-lock.yaml

RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm install --frozen-lockfile

ADD ./build.js /app/build.js
ADD ./tsconfig.json /app/tsconfig.json
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,5 @@
"svelte-preprocess"
]
},
"packageManager": "pnpm@10.15.0"
"packageManager": "pnpm@10.16.1+sha512.0e155aa2629db8672b49e8475da6226aa4bdea85fdcdfdc15350874946d4f3c91faaf64cbdc4a5d1ab8002f473d5c3fcedcd197989cf0390f9badd3c04678706"
}
2,635 changes: 1,188 additions & 1,447 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
</head>
<body data-sveltekit-preload-data="hover" data-loading="true">
<script>
// Check if this is an embedded route
if (window.location.pathname.includes('/embed/')) {
document.body.setAttribute('data-embed', 'true');
document.documentElement.setAttribute('data-embed', 'true');
}

let themeInUse = 'auto';
const appwrite = localStorage.getItem('appwrite');

Expand Down
2 changes: 1 addition & 1 deletion src/lib/stores/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type Breadcrumb = {
title: string;
};

export type View = 'list' | 'grid';
export type View = 'table' | 'grid';

export type updateLayoutArguments = {
header?: Component;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/stores/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export async function confirmPayment(
if (error) {
throw error.message;
}
} catch (e) {
} catch {
addNotification({
title: 'Error',
message:
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(console)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
} from '@appwrite.io/pink-icons-svelte';
import type { LayoutData } from './$types';

export let data: LayoutData;
export let data: LayoutData & { currentOrgId?: string; allProjectsCount?: number };
let emailBannerClosed = false;

function kebabToSentenceCase(str: string) {
Expand Down
9 changes: 5 additions & 4 deletions src/routes/(console)/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
if (currentOrgId) {
try {
projectsCount = (
await sdk.forConsole.projects.list({
queries: [Query.equal('teamId', currentOrgId), Query.limit(1)]
})
await sdk.forConsole.projects.list([
Query.equal('teamId', currentOrgId),
Query.limit(1)
])
).total;
} catch (e) {
} catch {
projectsCount = 0;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/routes/(console)/project-[region]-[project]/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
const { plansInfo, organizations, preferences: prefs } = await parent();
depends(Dependencies.PROJECT);

const project = await sdk.forConsole.projects.get({ projectId: params.project });
const project = await sdk.forConsole.projects.get(params.project);
project.region ??= 'default';

// fast path without a network call!
Expand All @@ -27,7 +27,7 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {

const [org, regionalConsoleVariables, rolesResult, organizationPlan] = await Promise.all([
!organization
? (sdk.forConsole.teams.get({ teamId: project.teamId }) as Promise<Organization>)
? (sdk.forConsole.teams.get(project.teamId) as Promise<Organization>)
: organization,
sdk.forConsoleIn(project.region).console.variables(),
isCloud ? sdk.forConsole.billing.getRoles(project.teamId) : null,
Expand Down
176 changes: 11 additions & 165 deletions src/routes/(console)/project-[region]-[project]/auth/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,178 +3,24 @@
</script>

<script lang="ts">
import { goto } from '$app/navigation';
import { writable } from 'svelte/store';
import { base } from '$app/paths';
import { page } from '$app/state';
import {
AvatarInitials,
Copy,
Empty,
EmptySearch,
PaginationWithLimit,
SearchQuery
} from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
import { Container } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import Create from './createUser.svelte';
import { Badge, Icon, Table, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Tag } from '@appwrite.io/pink-svelte';
import { IconDuplicate, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { canWriteUsers } from '$lib/stores/roles';
import type { Column } from '$lib/helpers/types';
import ViewSelector from '$lib/components/viewSelector.svelte';
import { View } from '$lib/helpers/load';
import { Container } from '$lib/layout';
import View from './view.svelte';

export let data;

const columns = writable<Column[]>([
{ id: '$id', title: 'User ID', type: 'string', width: 200 },
{ id: 'name', title: 'Name', type: 'string', width: { min: 260 } },
{ id: 'identifiers', title: 'Identifiers', type: 'string', width: { min: 260 } },
{ id: 'status', title: 'Status', type: 'string', width: { min: 140 } },
{ id: 'labels', title: 'Labels', type: 'string', hide: true, width: { min: 140 } },
{ id: 'joined', title: 'Joined', type: 'string', width: { min: 140 } },
{
id: 'lastActivity',
title: 'Last activity',
type: 'string',
hide: true,
width: { min: 140 }
}
]);

async function userCreated(event: CustomEvent<Models.User<Record<string, unknown>>>) {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${event.detail.$id}`
);
}
const createUserUrl = (user: Models.User<{}>) =>
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${user.$id}`;
</script>

<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name, email, phone, or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector view={View.Table} {columns} hideView />
<Button on:click={() => ($showCreateUser = true)} event="create_user" size="s">
<Icon size="s" icon={IconPlus} slot="start" />
<span class="text">Create user</span>
</Button>
</Layout.Stack>
</Layout.Stack>

{#if data.users.total}
<Table.Root columns={$columns} let:root>
<svelte:fragment slot="header" let:root>
{#each $columns as { id, title } (id)}
<Table.Header.Cell column={id} {root}>{title}</Table.Header.Cell>
{/each}
</svelte:fragment>
{#each data.users.users as user}
<Table.Row.Link
href={`${base}/project-${page.params.region}-${page.params.project}/auth/user-${user.$id}`}
{root}>
{#each $columns as { id } (id)}
<Table.Cell column={id} {root}>
{#if id === '$id'}
<Copy value={user.$id} event="user">
<Tag size="xs" variant="code">
<Icon size="s" icon={IconDuplicate} slot="start" />
{user.$id}
</Tag>
</Copy>
{:else if id === 'name'}
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if user.email || user.phone}
{#if user.name}
<AvatarInitials size="xs" name={user.name} />
<Typography.Text truncate>
{user.name}
</Typography.Text>
{:else}
<div class="avatar is-size-small">
<span class="icon-minus-sm" aria-hidden="true"
></span>
</div>
{/if}
{:else}
<div class="avatar is-size-small">
<span class="icon-anonymous" aria-hidden="true"></span>
</div>
<Typography.Text truncate>
{user.name}
</Typography.Text>
{/if}
</Layout.Stack>
{:else if id === 'identifiers'}
<Typography.Text truncate>
{user.email && user.phone
? [user.email, user.phone].join(',')
: user.email || user.phone}
</Typography.Text>
{:else if id === 'status'}
{#if user.status}
{@const success =
user.emailVerification || user.phoneVerification}
<Badge
size="xs"
variant="secondary"
type={success ? 'success' : undefined}
content={user.emailVerification && user.phoneVerification
? 'Verified'
: user.emailVerification
? 'Verified email'
: user.phoneVerification
? 'Verified phone'
: 'Unverified'} />
{:else}
<Badge
size="xs"
variant="secondary"
type="error"
content="blocked" />
{/if}
{:else if id === 'labels'}
<Typography.Text truncate>
{user.labels.join(', ')}
</Typography.Text>
{:else if id === 'joined'}
{toLocaleDateTime(user.registration)}
{:else if id === 'lastActivity'}
{user.accessedAt ? toLocaleDate(user.accessedAt) : 'never'}
{:else}
{user[id]}
{/if}
</Table.Cell>
{/each}
</Table.Row.Link>
{/each}
</Table.Root>

<PaginationWithLimit
name="Users"
limit={data.limit}
offset={data.offset}
total={data.users.total} />
{:else if data.search}
<EmptySearch target="users" hidePagination>
<Button
href={`${base}/project-${page.params.region}-${page.params.project}/auth`}
size="s"
secondary>Clear Search</Button>
</EmptySearch>
{:else}
<Empty
single
href="https://appwrite.io/docs/references/cloud/server-nodejs/users"
target="user"
allowCreate={$canWriteUsers}
on:click={() => showCreateUser.set(true)} />
{/if}
<View
users={{ total: data.users.total, users: data.users.users }}
limit={data.limit}
offset={data.offset}
search={data.search}
{createUserUrl} />
</Container>

<Create bind:showCreate={$showCreateUser} on:created={userCreated} />
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import { canWriteProjects } from '$lib/stores/roles';
import { Typography } from '@appwrite.io/pink-svelte';

const path = `${base}/project-${page.params.region}-${page.params.project}/auth`;
export let path = `${base}/project-${page.params.region}-${page.params.project}/auth`;

const tabs = [
{
href: path,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import { Container } from '$lib/layout';
import UpdateMockNumbers from './updateMockNumbers.svelte';
import UpdatePasswordDictionary from './updatePasswordDictionary.svelte';
import UpdatePasswordHistory from './updatePasswordHistory.svelte';
import UpdatePersonalDataCheck from './updatePersonalDataCheck.svelte';
Expand All @@ -21,6 +20,5 @@
<UpdatePersonalDataCheck />
<UpdateSessionAlerts />
<UpdateSessionInvalidation />
<UpdateMockNumbers />
<UpdateMembershipPrivacy />
</Container>
Loading