From bc936e0e169e279e7edd27e67d4f16ef47693016 Mon Sep 17 00:00:00 2001 From: jayesh Date: Sun, 17 Aug 2025 22:06:04 +0530 Subject: [PATCH 01/10] feat(templates): add helper for template source URL --- src/lib/helpers/templateSource.ts | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/lib/helpers/templateSource.ts diff --git a/src/lib/helpers/templateSource.ts b/src/lib/helpers/templateSource.ts new file mode 100644 index 000000000..5e34a8e1b --- /dev/null +++ b/src/lib/helpers/templateSource.ts @@ -0,0 +1,32 @@ +export type Template = { + key?: string; // template slug, e.g. "magic-portfolio" + frameworks?: Array<{ + key?: string; + providerRootDirectory?: string; + }>; // e.g. [{ key: "nextjs", providerRootDirectory: "./nextjs/magic-portfolio" }] + providerOwner?: string; // e.g. "appwrite" + providerRepositoryId?: string; // e.g. "templates-for-sites" or "template-for-store" + repository?: string; // if API already returns full URL, use it + repoUrl?: string; // alias some builds may use + source?: string; // alias some builds may use +}; + +export function getTemplateSourceUrl(template: Template): string | null { + // 1) Prefer a direct URL if the API already provides it + const direct = template.repository || template.repoUrl || template.source; + if (direct) return direct; + + // 2) Ensure we have the parts to build the GitHub URL + const owner = template.providerOwner || "appwrite"; + const repo = template.providerRepositoryId || "templates-for-sites"; + const rootDir = template.frameworks?.[0]?.providerRootDirectory ?? "./"; + + // strip leading "./" + const cleanDir = rootDir.startsWith("./") ? rootDir.slice(2) : rootDir; + + if (!cleanDir || cleanDir === "") { + return `https://github.com/${owner}/${repo}/tree/main`; + } + + return `https://github.com/${owner}/${repo}/tree/main/${cleanDir}`; +} From cab8754b48e2aea1dc23caa7ff09a7fef9768774 Mon Sep 17 00:00:00 2001 From: jayesh Date: Wed, 20 Aug 2025 20:13:39 +0530 Subject: [PATCH 02/10] feat: add View source button to template detail page --- .../sites/create-site/aside.svelte | 3 +++ .../templates/template-[template]/+page.svelte | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/aside.svelte b/src/routes/(console)/project-[region]-[project]/sites/create-site/aside.svelte index 96294b630..ceb05e128 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/aside.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/create-site/aside.svelte @@ -62,6 +62,9 @@ + + + {/if} {#if domain && showAfter} diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/templates/template-[template]/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/create-site/templates/template-[template]/+page.svelte index 4ad7bd998..de0d296aa 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/templates/template-[template]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/create-site/templates/template-[template]/+page.svelte @@ -37,6 +37,7 @@ import { connectGitHub } from '$lib/stores/git'; import { getFrameworkIcon } from '$lib/stores/sites'; import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store'; + import { getTemplateSourceUrl } from '$lib/helpers/templateSource'; export let data; @@ -351,6 +352,20 @@ alt={data.template.name} ratio="16/9" /> + + + {@const sourceUrl = getTemplateSourceUrl(data.template)} + {#if sourceUrl} + + {/if} + From be1844bf703ef2b723de9a1af46334e8eeddea7a Mon Sep 17 00:00:00 2001 From: jayesh Date: Thu, 21 Aug 2025 20:55:44 +0530 Subject: [PATCH 03/10] refactor(templates): use SDK models and build repo URL as https://// --- src/lib/helpers/templateSource.ts | 46 ++++++++----------- .../sites/create-site/aside.svelte | 2 +- .../template-[template]/+page.svelte | 1 + 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/lib/helpers/templateSource.ts b/src/lib/helpers/templateSource.ts index 5e34a8e1b..3c94967d1 100644 --- a/src/lib/helpers/templateSource.ts +++ b/src/lib/helpers/templateSource.ts @@ -1,32 +1,26 @@ -export type Template = { - key?: string; // template slug, e.g. "magic-portfolio" - frameworks?: Array<{ - key?: string; - providerRootDirectory?: string; - }>; // e.g. [{ key: "nextjs", providerRootDirectory: "./nextjs/magic-portfolio" }] - providerOwner?: string; // e.g. "appwrite" - providerRepositoryId?: string; // e.g. "templates-for-sites" or "template-for-store" - repository?: string; // if API already returns full URL, use it - repoUrl?: string; // alias some builds may use - source?: string; // alias some builds may use -}; +import type { Models } from "@appwrite.io/console"; -export function getTemplateSourceUrl(template: Template): string | null { - // 1) Prefer a direct URL if the API already provides it - const direct = template.repository || template.repoUrl || template.source; - if (direct) return direct; +/** + * Build VCS repo URL from the template response model. + * Example (GitHub): https://github.com/appwrite/templates-for-sites + */ +export function getTemplateSourceUrl( + t: Models.TemplateSite | Models.TemplateFunction +): string | null { + const owner = t.providerOwner; + const repo = t.providerRepositoryId; + const provider = t.vcsProvider; // e.g., "github" - // 2) Ensure we have the parts to build the GitHub URL - const owner = template.providerOwner || "appwrite"; - const repo = template.providerRepositoryId || "templates-for-sites"; - const rootDir = template.frameworks?.[0]?.providerRootDirectory ?? "./"; + if (!owner || !repo || !provider) return null; - // strip leading "./" - const cleanDir = rootDir.startsWith("./") ? rootDir.slice(2) : rootDir; + // Map provider → host (extend if needed) + const hostMap: Record = { + github: "github.com", + gitlab: "gitlab.com", + bitbucket: "bitbucket.org", + }; - if (!cleanDir || cleanDir === "") { - return `https://github.com/${owner}/${repo}/tree/main`; - } + const host = hostMap[provider.toLowerCase()] ?? provider; // fallback - return `https://github.com/${owner}/${repo}/tree/main/${cleanDir}`; + return `https://${host}/${owner}/${repo}`; } diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/aside.svelte b/src/routes/(console)/project-[region]-[project]/sites/create-site/aside.svelte index ceb05e128..85d65bb6e 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/aside.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/create-site/aside.svelte @@ -63,7 +63,7 @@ - + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/templates/template-[template]/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/create-site/templates/template-[template]/+page.svelte index de0d296aa..b4e3e2ea5 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/templates/template-[template]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/create-site/templates/template-[template]/+page.svelte @@ -357,6 +357,7 @@ {@const sourceUrl = getTemplateSourceUrl(data.template)} {#if sourceUrl} From b51f745be313aaf87473eeebf230ca410190609d Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 24 Aug 2025 12:58:59 +0530 Subject: [PATCH 10/10] update: optimize projects loading. --- .../billing/alerts/projectsLimit.svelte | 15 +- .../billing/alerts/selectProjectCloud.svelte | 148 ++++++++++++------ src/lib/components/breadcrumbs.svelte | 44 +++++- src/lib/components/navbar.svelte | 14 +- src/lib/layout/shell.svelte | 3 - src/lib/stores/billing.ts | 2 + src/routes/(console)/+layout.svelte | 23 +-- src/routes/(console)/+layout.ts | 20 ++- .../organization-[organization]/+page.svelte | 10 +- .../createProjectCloud.svelte | 2 +- 10 files changed, 171 insertions(+), 110 deletions(-) diff --git a/src/lib/components/billing/alerts/projectsLimit.svelte b/src/lib/components/billing/alerts/projectsLimit.svelte index cd3a0a3ee..8d7b10b33 100644 --- a/src/lib/components/billing/alerts/projectsLimit.svelte +++ b/src/lib/components/billing/alerts/projectsLimit.svelte @@ -9,17 +9,22 @@ upgradeURL } from '$lib/stores/billing'; import { currentPlan } from '$lib/stores/organization'; - import { onMount } from 'svelte'; import SelectProjectCloud from './selectProjectCloud.svelte'; import { toLocaleDate } from '$lib/helpers/date'; - let showSelectProject: boolean = $state(false); + let selectedProjects: string[] = $state([]); - onMount(() => { - selectedProjects = page.data.organization?.projects || []; + let showSelectProject: boolean = $state(false); + + const organizationId = $derived(page.data.organization?.$id); + + $effect(() => { + if (organizationId) { + selectedProjects = page.data.organization?.projects || []; + } }); - + {#if $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)} - import { type Models } from '@appwrite.io/console'; - import { Alert, Button, Table } from '@appwrite.io/pink-svelte'; + import { type Models, Query } from '@appwrite.io/console'; + import { Alert, Button, Skeleton, Table } from '@appwrite.io/pink-svelte'; import { Modal } from '$lib/components'; - import { onMount } from 'svelte'; import { sdk } from '$lib/stores/sdk'; import { addNotification } from '$lib/stores/notifications'; import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import { billingProjectsLimitDate } from '$lib/stores/billing'; - import { page } from '$app/state'; import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date'; import { currentPlan } from '$lib/stores/organization'; let { showSelectProject = $bindable(false), - selectedProjects = $bindable([]) + selectedProjects = $bindable([]), + organizationId }: { showSelectProject: boolean; selectedProjects: string[]; + organizationId: string; } = $props(); - let projects = $state>([]); - let error = $state(null); + let loading = $state(false); + let projectsLoadingError = $state(null); - onMount(() => { - const currentOrgId = page.data.organization?.$id; - projects = - page.data.currentOrgId === currentOrgId ? page.data.allProjects?.projects || [] : []; - }); + let error = $state(null); + let projects = $state>([]); let projectsToArchive = $derived( projects.filter((project) => !selectedProjects.includes(project.$id)) @@ -39,6 +36,7 @@ projects[0].teamId, selectedProjects ); + showSelectProject = false; invalidate(Dependencies.ORGANIZATION); addNotification({ @@ -69,47 +67,95 @@ return result; } + + $effect(() => { + if (!showSelectProject) return; + + const areProjectsLoaded = projects.length > 0; + const teamIdInLoadedProjects = areProjectsLoaded ? projects[0].teamId : null; + + if (organizationId != teamIdInLoadedProjects) { + loading = true; + + sdk.forConsole.projects + .list([ + Query.equal('teamId', organizationId), + Query.limit(1000) // Get all projects for organization + ]) + .then((loadedProjects) => (projects = loadedProjects.projects)) + .catch((err) => (projectsLoadingError = err.message)) + .finally(() => (loading = false)); + } + }); - + Choose which {$currentPlan?.projects || 2} projects to keep. Projects over the limit will be blocked after this date. - {#if error} - {error} - {/if} - - - Project Name - Created - - {#each projects as project} - - {project.name} - {toLocaleDateTime(project.$createdAt)} - - {/each} - - {#if selectedProjects.length > $currentPlan?.projects} -
- You can only select {$currentPlan?.projects} projects. Please deselect others to continue. + + {#if loading} +
+ + + Project Name + Created + + + {#each Array.from({ length: 5 }) as _} + + + + + + + + + {/each} +
- {/if} - {#if selectedProjects.length === $currentPlan?.projects} - - - {@html formatProjectsToArchive()} - will be archived. - - + {:else if projectsLoadingError} + {projectsLoadingError} + {:else} + {#if error} + {error} + {/if} + + + + Project Name + Created + + {#each projects as project} + + {project.name} + {toLocaleDateTime(project.$createdAt)} + + {/each} + + + {#if selectedProjects.length > $currentPlan?.projects} +
+ You can only select {$currentPlan?.projects} projects. Please deselect others to continue. +
+ {/if} + + {#if selectedProjects.length === $currentPlan?.projects} + + + {@html formatProjectsToArchive()} + will be archived. + + + {/if} {/if} (showSelectProject = false)} @@ -118,3 +164,13 @@ >Save + + diff --git a/src/lib/components/breadcrumbs.svelte b/src/lib/components/breadcrumbs.svelte index da4bfd5c0..e2ecda32f 100644 --- a/src/lib/components/breadcrumbs.svelte +++ b/src/lib/components/breadcrumbs.svelte @@ -22,7 +22,9 @@ import { base } from '$app/paths'; import { currentPlan, newOrgModal } from '$lib/stores/organization'; import { Click, trackEvent } from '$lib/actions/analytics'; - import type { Models } from '@appwrite.io/console'; + import { type Models, Query } from '@appwrite.io/console'; + import { sdk } from '$lib/stores/sdk'; + import { page } from '$app/state'; type Organization = { name: string; @@ -63,15 +65,15 @@ } } = createMenu(); - let isLoadingProjects = true; + let isLoadingProjects = false; let loadedProjects: Models.ProjectList = { total: 0, projects: [] }; export let organizations: Organization[] = []; export let currentProject: Models.Project | null = null; - export let projects: Promise = Promise.resolve(loadedProjects); let projectsBottomSheetOpen = false; let organisationBottomSheetOpen = false; + let projectsBottomSheet: Promise = null; function createOrg() { trackEvent(Click.OrganizationClickCreate, { source: 'breadcrumbs' }); @@ -99,13 +101,25 @@ } }; - async function createProjectsBottomSheet(organization: Organization): Promise { + async function createProjectsBottomSheet( + organization: Organization + ): Promise { + if (!isOnProjects) return null; + isLoadingProjects = true; + // null on non-org/project path like `onboarding`. - loadedProjects = (await projects) ?? loadedProjects; + loadedProjects = + (await sdk.forConsole.projects.list([ + Query.equal('teamId', organizationId ?? page.data.currentOrgId), + Query.limit(5), + Query.orderDesc('$updatedAt') + ])) ?? loadedProjects; + for (const project of loadedProjects.projects) { project.region ??= 'default'; } + isLoadingProjects = false; const createProjectItem = { @@ -185,8 +199,6 @@ $: selectedOrg = organizations.find((org) => org.isSelected); - $: projectsBottomSheet = createProjectsBottomSheet(selectedOrg); - $: organizationsBottomSheet = createOrganizationBottomSheet(selectedOrg); $: correctPlanName = @@ -198,6 +210,20 @@ : selectedOrg?.tierName; // fallback $: derivedKey = `${selectedOrg?.$id}-${currentProject?.$id}`; + + $: organizationId = currentProject?.teamId; + + $: isOnProjects = page.route.id.includes('project-[region]-[project]'); + + $: shouldReloadProjects = isLoadingProjects + ? false + : currentProject && loadedProjects.projects.length + ? loadedProjects.projects[0].teamId != currentProject.teamId + : !loadedProjects.projects.length; + + $: if (shouldReloadProjects) { + projectsBottomSheet = createProjectsBottomSheet(selectedOrg); + } @@ -380,7 +406,9 @@ {#await projectsBottomSheet then menu} - + {#if menu} + + {/if} {/await} {/key} diff --git a/src/lib/components/navbar.svelte b/src/lib/components/navbar.svelte index c87845b4e..51ed33e43 100644 --- a/src/lib/components/navbar.svelte +++ b/src/lib/components/navbar.svelte @@ -1,15 +1,6 @@ - +
diff --git a/src/routes/(console)/organization-[organization]/createProjectCloud.svelte b/src/routes/(console)/organization-[organization]/createProjectCloud.svelte index a0236076f..408c06982 100644 --- a/src/routes/(console)/organization-[organization]/createProjectCloud.svelte +++ b/src/routes/(console)/organization-[organization]/createProjectCloud.svelte @@ -12,8 +12,8 @@ import { Dependencies } from '$lib/constants'; export let teamId: string; - export let showCreateProjectCloud: boolean; export let projects: number; + export let showCreateProjectCloud: boolean; export let regions: Array = []; let id: string = null;