diff --git a/e2e/helpers/url.ts b/e2e/helpers/url.ts index 61c4c2425..a6fc879ac 100644 --- a/e2e/helpers/url.ts +++ b/e2e/helpers/url.ts @@ -12,7 +12,7 @@ export function getOrganizationIdFromUrl(pathname: string) { export function getProjectIdFromUrl(pathname: string) { // TODO: use base path from svelte here - const regex = /\/console\/project-([^/]+)(\/.*)?/; + const regex = /\/console\/project-(?:[a-z]{2,3}-)?([^/]+)(\/.*)?/; const match = pathname.match(regex); if (match) { diff --git a/e2e/journeys/upgrade-free-tier.spec.ts b/e2e/journeys/upgrade-free-tier.spec.ts index 024379820..f13adb4fa 100644 --- a/e2e/journeys/upgrade-free-tier.spec.ts +++ b/e2e/journeys/upgrade-free-tier.spec.ts @@ -8,12 +8,12 @@ test('upgrade - free tier', async ({ page }) => { await createFreeProject(page); await test.step('upgrade project', async () => { await page.getByRole('link', { name: 'Upgrade', exact: true }).click(); - await page.waitForURL('./organization-**/change-plan'); + await page.waitForURL(/\/organization-[^/]+\/change-plan/); await page.locator('input[value="tier-1"]').click(); await page.getByRole('button', { name: 'add' }).first().click(); await enterCreditCard(page); // skip members await page.getByRole('button', { name: 'change plan' }).click(); - await page.waitForURL('**/console/project-*/overview/platforms'); + await page.waitForURL(/\/console\/project-(?:[a-z0-9]+-)?([^/]+)\/overview\/platforms/); }); }); diff --git a/e2e/steps/free-project.ts b/e2e/steps/free-project.ts index 6a51bf313..8f8c775ed 100644 --- a/e2e/steps/free-project.ts +++ b/e2e/steps/free-project.ts @@ -13,18 +13,18 @@ export async function createFreeProject(page: Page): Promise { await page.locator('id=name').fill('test org'); await page.locator('id=plan').selectOption('tier-0'); await page.getByRole('button', { name: 'get started' }).click(); - await page.waitForURL('./organization-**'); + await page.waitForURL(/\/organization-[^/]+/); return getOrganizationIdFromUrl(page.url()); }); const projectId = await test.step('create project', async () => { - await page.waitForURL('./organization-**'); + await page.waitForURL(/\/organization-[^/]+/); await page.getByRole('button', { name: 'create project' }).first().click(); await page.locator('id=name').fill('test project'); await page.getByRole('button', { name: 'next' }).click(); await page.locator('label').filter({ hasText: 'Frankfurt' }).click(); await page.getByRole('button', { name: 'create' }).click(); - await page.waitForURL('./project-**/overview/platforms'); + await page.waitForURL(/\/project-(?:[a-z0-9]+-)?([^/]+)\/overview\/platforms/); expect(page.url()).toContain('/console/project-'); return getProjectIdFromUrl(page.url()); diff --git a/e2e/steps/pro-project.ts b/e2e/steps/pro-project.ts index 566658aff..fa42b2299 100644 --- a/e2e/steps/pro-project.ts +++ b/e2e/steps/pro-project.ts @@ -32,24 +32,24 @@ export async function createProProject(page: Page): Promise { await page.locator('id=name').fill('test org'); await page.locator('id=plan').selectOption('tier-1'); await page.getByRole('button', { name: 'get started' }).click(); - await page.waitForURL('./create-organization**'); + await page.waitForURL(/\/create-organization.*/); await page.getByRole('button', { name: 'add' }).first().click(); await enterCreditCard(page); // skip members await page.getByRole('button', { name: 'create organization' }).click(); - await page.waitForURL('./organization-**'); + await page.waitForURL(/\/organization-[^/]+/); return getOrganizationIdFromUrl(page.url()); }); const projectId = await test.step('create project', async () => { - await page.waitForURL('./organization-**'); + await page.waitForURL(/\/organization-[^/]+/); await page.getByRole('button', { name: 'create project' }).first().click(); await page.getByPlaceholder('project name').fill('test project'); await page.getByRole('button', { name: 'next' }).click(); await page.locator('label').filter({ hasText: 'frankfurt' }).click(); await page.getByRole('button', { name: 'create' }).click(); - await page.waitForURL('./project-**/overview/platforms'); + await page.waitForURL(/\/project-(?:[a-z0-9]+-)?([^/]+)\/overview\/platforms/); expect(page.url()).toContain('/project-'); return getProjectIdFromUrl(page.url()); diff --git a/package.json b/package.json index f5468f007..72518a618 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@stripe/stripe-js": "^3.5.0", "ai": "^2.2.37", "analytics": "^0.8.16", - "@ai-sdk/svelte": "^1.1.22", + "@ai-sdk/svelte": "^1.1.24", "cron-parser": "^4.9.0", "dayjs": "^1.11.13", "deep-equal": "^2.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c60db2ec1..9d4e84121 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@ai-sdk/svelte': - specifier: ^1.1.22 - version: 1.1.22(svelte@4.2.19)(zod@3.24.2) + specifier: ^1.1.24 + version: 1.1.24(svelte@4.2.19)(zod@3.24.2) '@appwrite.io/console': specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@e2f082e @@ -195,8 +195,8 @@ packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} - '@ai-sdk/provider-utils@2.1.11': - resolution: {integrity: sha512-lMnXA5KaRJidzW7gQmlo/SnX6D+AKk5GxHFcQtOaGOSJNmu/qcNZc1rGaO7K5qW52OvCLXtnWudR4cc/FvMpVQ==} + '@ai-sdk/provider-utils@2.1.13': + resolution: {integrity: sha512-kLjqsfOdONr6DGcGEntFYM1niXz1H05vyZNf9OAzK+KKKc64izyP4/q/9HX7W4+6g8hm6BnmKxu8vkr6FSOqDg==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -204,12 +204,12 @@ packages: zod: optional: true - '@ai-sdk/provider@1.0.10': - resolution: {integrity: sha512-pco8Zl9U0xwXI+nCLc0woMtxbvjU8hRmGTseAUiPHFLYAAL8trRPCukg69IDeinOvIeo1SmXxAIdWWPZOLb4Cg==} + '@ai-sdk/provider@1.0.11': + resolution: {integrity: sha512-CPyImHGiT3svyfmvPvAFTianZzWFtm0qK82XjwlQIA1C3IQ2iku/PMQXi7aFyrX0TyMh3VTkJPB03tjU2VXVrw==} engines: {node: '>=18'} - '@ai-sdk/svelte@1.1.22': - resolution: {integrity: sha512-xtJjvEPHD8GDB1iODvsRLKplZ9CVUN/gT4U+nttuEvjV42qoPXKkrWwjTLEdWKnwTnoHuuI8mnQMje6RhFWtpA==} + '@ai-sdk/svelte@1.1.24': + resolution: {integrity: sha512-nlSSd4FirQyM10MQb9vCzF3e/R4id0od0/cKtB1JdtcIvT4ZaJzyltIK3Q72ceOjEbHXSGbucW6gVyExBzHCLQ==} engines: {node: '>=18'} peerDependencies: svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -217,8 +217,8 @@ packages: svelte: optional: true - '@ai-sdk/ui-utils@1.1.17': - resolution: {integrity: sha512-fCnp/wntZGqPf6tiCmhuQoSDLSBhXoI5DU2JX4As96EO870+jliE6ozvYUwYOZC6Ta2OKAjjWPcSP7HeHX0b+g==} + '@ai-sdk/ui-utils@1.1.19': + resolution: {integrity: sha512-rDHy2uxlPMt3jjS9L6mBrsfhEInZ5BVoWevmD13fsAt2s/XWy2OwwKmgmUQkdLlY4mn/eyeYAfDGK8+5CbOAgg==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -3529,32 +3529,32 @@ snapshots: '@adobe/css-tools@4.4.2': {} - '@ai-sdk/provider-utils@2.1.11(zod@3.24.2)': + '@ai-sdk/provider-utils@2.1.13(zod@3.24.2)': dependencies: - '@ai-sdk/provider': 1.0.10 + '@ai-sdk/provider': 1.0.11 eventsource-parser: 3.0.0 nanoid: 3.3.9 secure-json-parse: 2.7.0 optionalDependencies: zod: 3.24.2 - '@ai-sdk/provider@1.0.10': + '@ai-sdk/provider@1.0.11': dependencies: json-schema: 0.4.0 - '@ai-sdk/svelte@1.1.22(svelte@4.2.19)(zod@3.24.2)': + '@ai-sdk/svelte@1.1.24(svelte@4.2.19)(zod@3.24.2)': dependencies: - '@ai-sdk/provider-utils': 2.1.11(zod@3.24.2) - '@ai-sdk/ui-utils': 1.1.17(zod@3.24.2) + '@ai-sdk/provider-utils': 2.1.13(zod@3.24.2) + '@ai-sdk/ui-utils': 1.1.19(zod@3.24.2) optionalDependencies: svelte: 4.2.19 transitivePeerDependencies: - zod - '@ai-sdk/ui-utils@1.1.17(zod@3.24.2)': + '@ai-sdk/ui-utils@1.1.19(zod@3.24.2)': dependencies: - '@ai-sdk/provider': 1.0.10 - '@ai-sdk/provider-utils': 2.1.11(zod@3.24.2) + '@ai-sdk/provider': 1.0.11 + '@ai-sdk/provider-utils': 2.1.13(zod@3.24.2) zod-to-json-schema: 3.24.3(zod@3.24.2) optionalDependencies: zod: 3.24.2 diff --git a/src/lib/commandCenter/panels/createAttribute.svelte b/src/lib/commandCenter/panels/createAttribute.svelte index 950d4af72..98d312698 100644 --- a/src/lib/commandCenter/panels/createAttribute.svelte +++ b/src/lib/commandCenter/panels/createAttribute.svelte @@ -1,6 +1,6 @@ diff --git a/src/lib/components/copy.svelte b/src/lib/components/copy.svelte index 970d04074..0579e6eb0 100644 --- a/src/lib/components/copy.svelte +++ b/src/lib/components/copy.svelte @@ -9,8 +9,9 @@ export let event: string = null; export let eventContext = 'click_id_tag'; export let tooltipDisabled = false; + export let copyText: string = 'Click to copy'; - let content = 'Click to copy'; + let content = copyText; async function handleClick() { const success = await copy(value); diff --git a/src/lib/components/creditCardBrandImage.svelte b/src/lib/components/creditCardBrandImage.svelte index 1a542bb53..b30f23f41 100644 --- a/src/lib/components/creditCardBrandImage.svelte +++ b/src/lib/components/creditCardBrandImage.svelte @@ -9,7 +9,7 @@ function getCreditCardImage(brand: string, width = 46, height = 32) { if (!isValueOfStringEnum(CreditCard, brand)) return ''; - return sdk.forConsole.avatars.getCreditCard(brand, width, height).toString(); + return sdk.forConsole.avatars.getCreditCard(brand, width, height); } diff --git a/src/lib/components/filePicker.svelte b/src/lib/components/filePicker.svelte index 9c16bb2e8..e844dfe13 100644 --- a/src/lib/components/filePicker.svelte +++ b/src/lib/components/filePicker.svelte @@ -49,20 +49,21 @@ function getPreview(bucketId: string, fileId: string, size: number = 64) { return ( - sdk.forProject.storage.getFilePreview(bucketId, fileId, size, size).toString() + - '&mode=admin' + sdk + .forProject(page.params.region, page.params.project) + .storage.getFilePreview(bucketId, fileId, size, size) + .toString() + '&mode=admin' ); } async function uploadFile() { try { uploading = true; - const file = await sdk.forProject.storage.createFile( - selectedBucket, - ID.unique(), - fileSelector.files[0], - [Permission.read(Role.any())] - ); + const file = await sdk + .forProject(page.params.region, page.params.project) + .storage.createFile(selectedBucket, ID.unique(), fileSelector.files[0], [ + Permission.read(Role.any()) + ]); search.set($search === null ? '' : null); selectFile(file); } catch (e) { @@ -110,7 +111,9 @@ let buckets: Promise = loadBuckets(); async function loadBuckets() { - const response = await sdk.forProject.storage.listBuckets(); + const response = await sdk + .forProject(page.params.region, page.params.project) + .storage.listBuckets(); const bucket = response.buckets[0] ?? null; if (bucket) { currentBucket = bucket; @@ -122,8 +125,9 @@ $: files = currentBucket && - sdk.forProject.storage - .listFiles( + sdk + .forProject(page.params.region, page.params.project) + .storage.listFiles( currentBucket.$id, [Query.startsWith('mimeType', mimeTypeQuery), Query.orderDesc('$createdAt')], $search || undefined @@ -218,6 +222,9 @@ Upload {/if} + ======= external href={`${base}/project-${page.params.region}-${page.params.project}/storage`}> + Create bucket >>>>>>> + 93754b342accbdb6db066c33b2addea7b9e92d67 @@ -355,7 +362,7 @@ slot="actions" secondary external - href={`${base}/project-${page.params.project}/storage`}> + href={`${base}/project-${page.params.region}-${page.params.project}/storage`}> Create bucket diff --git a/src/lib/components/id.svelte b/src/lib/components/id.svelte index 1f0552bb3..e447eb905 100644 --- a/src/lib/components/id.svelte +++ b/src/lib/components/id.svelte @@ -1,12 +1,5 @@ - + + diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index b20b38d1f..723ab85eb 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -82,3 +82,4 @@ export { default as BottomSheet } from './bottom-sheet/index'; export { default as Confirm } from './confirm.svelte'; export { default as UsageCard } from './usageCard.svelte'; export { default as ViewToggle } from './viewToggle.svelte'; +export { default as RegionEndpoint } from './regionEndpoint.svelte'; diff --git a/src/lib/components/limit.svelte b/src/lib/components/limit.svelte index f42351900..7ea7dd255 100644 --- a/src/lib/components/limit.svelte +++ b/src/lib/components/limit.svelte @@ -21,7 +21,7 @@ const url = new URL(pageStore.url); const previousLimit = Number(url.searchParams.get('limit')); url.searchParams.set('limit', limit.toString()); - preferences.setLimit(limit); + preferences.setLimit($pageStore.params.project, $pageStore.route, limit); if (url.searchParams.has('page')) { const page = Number(url.searchParams.get('page')); diff --git a/src/lib/components/migrationBox.svelte b/src/lib/components/migrationBox.svelte index 7bf837e7e..2493b32de 100644 --- a/src/lib/components/migrationBox.svelte +++ b/src/lib/components/migrationBox.svelte @@ -1,7 +1,8 @@ diff --git a/src/lib/components/modalWrapper.svelte b/src/lib/components/modalWrapper.svelte index 73ae9566a..fcbcda6d0 100644 --- a/src/lib/components/modalWrapper.svelte +++ b/src/lib/components/modalWrapper.svelte @@ -6,6 +6,7 @@ export let show = false; export let size: 'small' | 'big' | 'huge' = null; export let closable = true; + export let closeByEscape = true; export let headerDivider = true; export let style = ''; @@ -48,7 +49,7 @@ } function handleKeydown(event: KeyboardEvent) { - if (event.key === 'Escape') { + if (event.key === 'Escape' && closeByEscape) { event.preventDefault(); trackEvent(Click.ModalCloseClick, { from: 'escape' diff --git a/src/lib/components/permissions/row.svelte b/src/lib/components/permissions/row.svelte index aeb818340..31a6fd4db 100644 --- a/src/lib/components/permissions/row.svelte +++ b/src/lib/components/permissions/row.svelte @@ -27,10 +27,16 @@ const role = permission.split(':')[0]; const id = permission.split(':')[1].split('/')[0]; if (role === 'user') { - return await sdk.forProject.users.get(id); + const user = await sdk + .forProject(page.params.region, page.params.project) + .users.get(id); + return user; } if (role === 'team') { - return await sdk.forProject.teams.get(id); + const team = await sdk + .forProject(page.params.region, page.params.project) + .teams.get(id); + return team; } } @@ -82,7 +88,7 @@ {/if}
@@ -94,7 +100,7 @@ Members: {data?.total}
diff --git a/src/lib/components/permissions/team.svelte b/src/lib/components/permissions/team.svelte index 3e1cf353f..46d55e2b5 100644 --- a/src/lib/components/permissions/team.svelte +++ b/src/lib/components/permissions/team.svelte @@ -15,6 +15,7 @@ Table, Typography } from '@appwrite.io/pink-svelte'; + import { page } from '$app/state'; export let show: boolean; export let groups: Writable>; @@ -40,10 +41,9 @@ async function request() { if (!show) return; - results = await sdk.forProject.teams.list( - [Query.limit(5), Query.offset(offset)], - search || undefined - ); + results = await sdk + .forProject(page.params.region, page.params.project) + .teams.list([Query.limit(5), Query.offset(offset)], search || undefined); } function onSelection(role: string) { diff --git a/src/lib/components/permissions/user.svelte b/src/lib/components/permissions/user.svelte index b2a78515c..2c4022b7c 100644 --- a/src/lib/components/permissions/user.svelte +++ b/src/lib/components/permissions/user.svelte @@ -18,6 +18,7 @@ Typography } from '@appwrite.io/pink-svelte'; import { IconAnonymous, IconMinusSm } from '@appwrite.io/pink-icons-svelte'; + import { page } from '$app/state'; export let show: boolean; export let groups: Writable>; @@ -43,10 +44,9 @@ async function request() { if (!show) return; - results = await sdk.forProject.users.list( - [Query.limit(5), Query.offset(offset)], - search || undefined - ); + results = await sdk + .forProject(page.params.region, page.params.project) + .users.list([Query.limit(5), Query.offset(offset)], search || undefined); } function onSelection(role: string) { diff --git a/src/lib/components/regionEndpoint.svelte b/src/lib/components/regionEndpoint.svelte new file mode 100644 index 000000000..d85b3d215 --- /dev/null +++ b/src/lib/components/regionEndpoint.svelte @@ -0,0 +1,40 @@ + + + +
+ + {$projectRegion?.name} + + + {#if flagSrc} + {$projectRegion?.name} + {/if} +
+
diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 0f6b2c934..cded22092 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -3,6 +3,14 @@ export const CARD_LIMIT = 6; // default card limit export const INTERVAL = 5 * 60000; // default interval to check for feedback export const NEW_DEV_PRO_UPGRADE_COUPON = 'appw50'; +export const REGION_FRA = 'fra'; +export const REGION_SYD = 'syd'; +export const REGION_NYC = 'nyc'; + +export const SUBDOMAIN_FRA = 'fra.'; +export const SUBDOMAIN_SYD = 'syd.'; +export const SUBDOMAIN_NYC = 'nyc.'; + export enum Dependencies { FACTORS = 'dependency:factors', IDENTITIES = 'dependency:identities', diff --git a/src/lib/elements/flag.svelte b/src/lib/elements/flag.svelte index efb284cc8..9fc0f2dcf 100644 --- a/src/lib/elements/flag.svelte +++ b/src/lib/elements/flag.svelte @@ -2,6 +2,7 @@ import { isValueOfStringEnum } from '$lib/helpers/types'; import { sdk } from '$lib/stores/sdk'; import { Flag } from '@appwrite.io/console'; + export let flag: string; export let name: string = flag; export let width = 40; diff --git a/src/lib/helpers/load.ts b/src/lib/helpers/load.ts index 3dd28f126..2b0207307 100644 --- a/src/lib/helpers/load.ts +++ b/src/lib/helpers/load.ts @@ -9,8 +9,15 @@ export function getPage(url: URL): number { return Number(url.searchParams.get('page')); } -export function getLimit(url: URL, route: Page['route'], fallback: number): number { - return Number(url.searchParams.get('limit') ?? preferences.get(route).limit ?? fallback); +export function getLimit( + projectId: string, + url: URL, + route: Page['route'], + fallback: number +): number { + return Number( + url.searchParams.get('limit') ?? preferences.get(projectId, route).limit ?? fallback + ); } export enum View { @@ -28,8 +35,8 @@ export function getView(url: URL, route: Page['route'], fallback: View, overwrit : (overwrite ?? View.Table ?? fallback); } -export function getColumns(route: Page['route'], fallback: string[]): string[] { - return preferences.get(route).columns ?? fallback; +export function getColumns(projectId: string, route: Page['route'], fallback: string[]): string[] { + return preferences.get(projectId, route).columns ?? fallback; } export function getSearch(url: URL): string | undefined { diff --git a/src/lib/helpers/project.ts b/src/lib/helpers/project.ts index 91cfc90ac..5d9c8890f 100644 --- a/src/lib/helpers/project.ts +++ b/src/lib/helpers/project.ts @@ -1,6 +1,44 @@ -export function getProjectId() { +import { page } from '$app/stores'; +import { get } from 'svelte/store'; +import { sdk } from '$lib/stores/sdk'; +import { projectRegion } from '$routes/(console)/project-[region]-[project]/store'; + +/** + * Returns the current project ID. + * + * The function first checks for a `project` parameter in the Svelte `$page` store. + * If not found, it extracts the project ID from the pathname. + * + * Supports: + * - Legacy structure: `/project-{projectID}/` + * - Multi-region structure: `/project-{region}-{projectID}/` + * + * Example: + * - `/project-console/` → `console` + * - `/project-fra-console/` → `console` + * - `/project-nyc-console/` → `console` + */ +export function getProjectId(): string | null { + // safety check! + const projectFromParams = get(page)?.params?.project; + if (projectFromParams) { + return projectFromParams; + } + const pathname = window.location.pathname + '/'; - const projectMatch = pathname.match(/\/project-(.*?)\//); + const projectMatch = pathname.match(/\/project-(?:[a-z]{2,3}-)?([^/]+)/); return projectMatch?.[1] || null; } + +/** + * Returns the correct API endpoint for the project based on the current project region. + * + * @returns {string} The project-specific API endpoint. + */ +export function getProjectEndpoint(): string { + const currentProjectRegion = get(projectRegion); + const { protocol, hostname, href } = new URL(sdk.forConsole.client.config.endpoint); + + return currentProjectRegion ? `${protocol}//${currentProjectRegion.$id}.${hostname}/v1` : href; +} diff --git a/src/lib/layout/wizardStep.svelte b/src/lib/layout/wizardStep.svelte index 5ab8b014b..4b64ae445 100644 --- a/src/lib/layout/wizardStep.svelte +++ b/src/lib/layout/wizardStep.svelte @@ -34,6 +34,10 @@