diff --git a/src/lib/components/archiveProject.svelte b/src/lib/components/archiveProject.svelte deleted file mode 100644 index 8a324b395b..0000000000 --- a/src/lib/components/archiveProject.svelte +++ /dev/null @@ -1,379 +0,0 @@ - - -{#if projectsToArchive.length > 0} -
- - - {#if isPlanBelowPro} - These projects are archived and require a plan upgrade to restore access. - {:else} - These projects will be archived at the end of your billing cycle. - {/if} - - -
- - {#each projectsToArchive as project} - {@const platforms = filterPlatforms( - project.platforms.map((platform) => getPlatformInfo(platform.type)) - )} - {@const formatted = formatName(project.name)} - - - {project?.platforms?.length ? project?.platforms?.length : 'No'} apps - - {formatted} - -
- - - - handleUnarchiveProject(project)} - >Unarchive project - handleMigrateProject(project)} - >Migrate project -
- -
- handleDeleteProject(project)} - >Delete project -
-
-
-
- - {#each platforms.slice(0, 2) as platform} - {@const icon = getIconForPlatform(platform.icon)} - - - - {/each} - - {#if platforms.length > 2} - - {/if} - - - {#if isCloud && $regionsStore?.regions} - {@const region = findRegion(project)} - {region?.name} - {/if} - -
- {/each} -
- - -
-
-
-{/if} - - - -

Are you sure you want to unarchive {projectToUnarchive?.name}?

-

This will move the project back to your active projects list.

- - - - - - - -
- - - - - The archived project {projectToDelete?.name} will be deleted along with all - of its metadata, stats, and other resources. - This action is irreversible. - - - - - - - - - - - diff --git a/src/lib/components/billing/alerts/projectsLimit.svelte b/src/lib/components/billing/alerts/projectsLimit.svelte deleted file mode 100644 index 493e445107..0000000000 --- a/src/lib/components/billing/alerts/projectsLimit.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - - - -{#if organizationId && $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)} - - - Choose which projects to keep before {toLocaleDate( - $organization.billingNextInvoiceDate - )} or upgrade to Pro. Projects over the limit will be blocked after this date. - - - - - - -{/if} diff --git a/src/lib/components/billing/alerts/selectProjectCloud.svelte b/src/lib/components/billing/alerts/selectProjectCloud.svelte deleted file mode 100644 index a0084f97ab..0000000000 --- a/src/lib/components/billing/alerts/selectProjectCloud.svelte +++ /dev/null @@ -1,180 +0,0 @@ - - - - - Choose which {$currentPlan?.projects || 2} projects to keep. Projects over the limit will be - blocked after this date. - - - {#if loading} -
- - - Project Name - Created - - - {#each Array.from({ length: 5 }) as _} - - - - - - - - - {/each} - -
- {: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} - {@const difference = projects.length - selectedProjects.length} - {@const messagePrefix = - difference > 1 ? `${difference} projects` : `${difference} project`} - - - {@html formatProjectsToArchive()} - will be archived. - - - {/if} - {/if} - - (showSelectProject = false)} - >Cancel - Save - -
- - diff --git a/src/lib/components/organizationUsageLimits.svelte b/src/lib/components/organizationUsageLimits.svelte index fc97c9b257..7d1257b266 100644 --- a/src/lib/components/organizationUsageLimits.svelte +++ b/src/lib/components/organizationUsageLimits.svelte @@ -31,45 +31,45 @@ const baseFreePlan = getBasePlanFromGroup(BillingPlanGroup.Starter); // Derived state using runes - let freePlanLimits = $derived({ + const freePlanLimits = $derived({ projects: baseFreePlan?.projects, members: getServiceLimit('members', null, baseFreePlan), storage: getServiceLimit('storage', null, baseFreePlan) }); // When preparing to downgrade to Free, enforce Free plan limit locally (2) - let allowedProjectsToKeep = $derived(freePlanLimits.projects); + const allowedProjectsToKeep = $derived(freePlanLimits.projects); - let currentUsage = $derived({ + const currentUsage = $derived({ projects: projects?.length || 0, members: members?.length || 0, storage: storageUsage || 0 }); - let storageUsageGB = $derived(storageUsage / (1024 * 1024 * 1024)); + const storageUsageGB = $derived(storageUsage / (1024 * 1024 * 1024)); - let isLimitExceeded = $derived({ + const isLimitExceeded = $derived({ projects: currentUsage.projects > freePlanLimits.projects, members: currentUsage.members > freePlanLimits.members, storage: storageUsageGB > freePlanLimits.storage }); - let excessUsage = $derived({ + const excessUsage = $derived({ projects: Math.max(0, currentUsage.projects), members: Math.max(0, currentUsage.members - freePlanLimits.members), storage: Math.max(0, storageUsageGB - freePlanLimits.storage) }); - // projects that would be archived with the current selection - let projectsToArchive = $derived( + // projects that would be deleted with the current selection + const projectsToDelete = $derived( projects.filter((project) => !selectedProjects.includes(project.$id)) ); - function formatProjectsToArchive(): string { + function formatProjectsToDelete(): string { let result = ''; - projectsToArchive.forEach((project, index) => { - const isLast = index === projectsToArchive.length - 1; - const isSecondLast = index === projectsToArchive.length - 2; + projectsToDelete.forEach((project, index) => { + const isLast = index === projectsToDelete.length - 1; + const isSecondLast = index === projectsToDelete.length - 2; result += `${index === 0 ? '' : ' '}${project.name}`; @@ -114,10 +114,12 @@ error = `You must select exactly ${allowedProjectsToKeep} projects to keep.`; return; } - // Keep selection locally; parent flow will apply after plan change + + // Keep selection locally; + // parent flow will apply after plan change showSelectProject = false; showSelectionReminder = false; - addNotification({ type: 'success', message: `Projects selected for archiving` }); + addNotification({ type: 'success', message: `Projects selected for deleting` }); } @@ -220,11 +222,7 @@ {:else} - - {formatNumber(currentUsage.members)} / {formatNumber( - freePlanLimits.members - )} - + N/A {/if} @@ -261,10 +259,16 @@ {#if showSelectProject} - Choose which {freePlanLimits.projects} projects to keep. Projects over the limit will be - blocked after your billing cycle ends on {toLocaleDate( - $organization.billingNextInvoiceDate - )}. + + + Projects not kept, including all associated data, will be permanently deleted + on {toLocaleDate($organization.billingNextInvoiceDate)} and cannot be recovered. + This action is irreversible. + + {#if error} @@ -292,14 +296,17 @@ {/each} + {#if selectedProjects.length === allowedProjectsToKeep} {@const difference = projects.length - selectedProjects.length} {@const messagePrefix = difference > 1 ? `${difference} projects` : `${difference} project`} - {formatProjectsToArchive()} will be archived + status="error" + title={`${messagePrefix} will be permanently deleted on ${toLocaleDate($organization.billingNextInvoiceDate)}`}> + {formatProjectsToDelete()} and all associated data, will be + permanently deleted. + This action is irreversible. {/if} diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index baf663114b..50e3a3071e 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -31,7 +31,6 @@ import { user } from './user'; import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budgetLimitAlert.svelte'; import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte'; -import ProjectsLimit from '$lib/components/billing/alerts/projectsLimit.svelte'; import EnterpriseTrial from '$routes/(console)/organization-[organization]/enterpriseTrial.svelte'; export const roles = [ @@ -368,32 +367,6 @@ export function calculateTrialDay(org: Models.Organization) { return days; } -export async function checkForProjectsLimit(org: Models.Organization, orgProjectCount?: number) { - if (!isCloud) return; - if (!org) return; - - const plan = await sdk.forConsole.organizations.getPlan({ - organizationId: org.$id - }); - if (!plan) return; - - if (!org.projects) return; - if (org.projects.length > 0) return; - - const projectCount = orgProjectCount; - if (projectCount === undefined) return; - - // not unlimited and current exceeds plan limits! - if (plan.projects > 0 && projectCount > plan.projects) { - headerAlert.add({ - id: 'projectsLimitReached', - component: ProjectsLimit, - show: true, - importance: 12 - }); - } -} - export async function checkForUsageLimit(organization: Models.Organization) { if ( organization?.status === teamStatusReadonly && diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index 1b0230829a..c7a11414c0 100644 --- a/src/routes/(console)/+layout.svelte +++ b/src/routes/(console)/+layout.svelte @@ -18,7 +18,6 @@ checkForMarkedForDeletion, checkForMissingPaymentMethod, checkForNewDevUpgradePro, - checkForProjectsLimit, checkForUsageLimit, checkPaymentAuthorizationRequired, paymentExpired, @@ -39,7 +38,7 @@ import { showSupportModal } from './wizard/support/store'; import { activeHeaderAlert, consoleVariables } from './store'; - import { base } from '$app/paths'; + import { base, resolve } from '$app/paths'; import { headerAlert } from '$lib/stores/headerAlert'; import { UsageRates } from '$lib/components/billing'; import { canSeeProjects } from '$lib/stores/roles'; @@ -54,11 +53,8 @@ IconSparkles, IconSwitchHorizontal } from '@appwrite.io/pink-icons-svelte'; - import type { LayoutData } from './$types'; import type { Models } from '@appwrite.io/console'; - export let data: LayoutData; - function kebabToSentenceCase(str: string) { return str .split('-') @@ -75,9 +71,7 @@ $: $registerCommands([ { label: 'Go to Projects', - callback: () => { - goto(base); - }, + callback: () => goto(resolve('/')), keys: ['g', 'p'], group: 'navigation', disabled: @@ -296,9 +290,6 @@ if (currentOrganizationId === org.$id) return; if (isCloud) { currentOrganizationId = org.$id; - const orgProjectCount = - data.currentOrgId === org.$id ? data.allProjectsCount : undefined; - await checkForProjectsLimit(org, orgProjectCount); checkForEnterpriseTrial(org); await checkForUsageLimit(org); checkForMarkedForDeletion(org); diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index 947cdedcaa..afa38a5ee2 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -7,7 +7,6 @@ import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system'; import { page } from '$app/state'; import { registerCommands } from '$lib/commandCenter'; - import { formatName as formatNameHelper } from '$lib/helpers/string'; import { CardContainer, Empty, @@ -23,8 +22,7 @@ import { onMount, type ComponentType } from 'svelte'; import { canWriteProjects } from '$lib/stores/roles'; import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect'; - import { Alert, Badge, Icon, Layout, Tag, Tooltip, Typography } from '@appwrite.io/pink-svelte'; - import { isSmallViewport } from '$lib/stores/viewport'; + import { Alert, Badge, Icon, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte'; import { IconAndroid, IconApple, @@ -38,14 +36,11 @@ import { getPlatformInfo } from '$lib/helpers/platform'; import CreateProjectCloud from './createProjectCloud.svelte'; import { regions as regionsStore } from '$lib/stores/organization'; - import SelectProjectCloud from '$lib/components/billing/alerts/selectProjectCloud.svelte'; - import ArchiveProject from '$lib/components/archiveProject.svelte'; let { data }: PageProps = $props(); let showCreate = $state(false); let addOrganization = $state(false); - let showSelectProject = $state(false); let showCreateProjectCloud = $state(false); let freePlanAlertDismissed = $state(false); @@ -123,24 +118,7 @@ return $regionsStore.regions.find((region) => region.$id === project.region); } - function isSetToArchive(project: Models.Project): boolean { - if (!isCloud) return false; - if (!project || !project.$id) return false; - return project.status === 'archived'; - } - - const projectsToArchive = $derived( - (data.archivedProjectsPage ?? data.projects.projects).filter( - (project) => project.status === 'archived' - ) - ); - - const activeTotalOverall = $derived( - data?.activeTotalOverall ?? - data?.organization?.projects?.length ?? - data?.projects?.total ?? - 0 - ); + const activeProjectsTotal = $derived(data?.projects.total); function clearSearch() { searchQuery?.clearInput(); @@ -162,11 +140,6 @@ }); - - @@ -196,30 +169,7 @@ {/if} - {#if isCloud && data.currentPlan?.projects && data.currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects && (projectsToArchive.length > 0 || data.projects.total > data.currentPlan.projects)} - {@const difference = projectsToArchive.length} - {@const messagePrefix = - difference !== 1 ? `${difference} projects are` : `${difference} project is`} - - Upgrade your plan to restore archived projects - - - - - {/if} - - {#if isCloud && data.currentPlan?.projects !== 0 && projectsToArchive.length === 0 && !freePlanAlertDismissed} + {#if isCloud && data.currentPlan?.projects !== 0 && activeProjectsTotal <= data.currentPlan.projects && !freePlanAlertDismissed} Your Free plan includes up to 2 projects and limited resources. Upgrade to unlock @@ -244,43 +194,20 @@ {#if data.projects.total > 0} {#each data.projects.projects as project} {@const platforms = filterPlatforms( project.platforms.map((platform) => getPlatformInfo(platform.type)) )} - {@const formatted = isSetToArchive(project) - ? formatNameHelper(project.name, isSmallViewport ? 19 : 25) - : project.name} {project?.platforms?.length ? project?.platforms?.length : 'No'} apps - - {formatted} - - {project.name} - - - - - - {#if isSetToArchive(project)} - { - event.preventDefault(); - showSelectProject = true; - }}>Set to archive - {/if} + {project.name} {#each platforms.slice(0, 2) as platform} @@ -329,16 +256,7 @@ name="Projects" limit={data.limit} offset={data.offset} - total={activeTotalOverall} /> - - - + total={activeProjectsTotal} /> diff --git a/src/routes/(console)/organization-[organization]/+page.ts b/src/routes/(console)/organization-[organization]/+page.ts index fc4256b032..9a6c850238 100644 --- a/src/routes/(console)/organization-[organization]/+page.ts +++ b/src/routes/(console)/organization-[organization]/+page.ts @@ -5,12 +5,17 @@ import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load'; import { CARD_LIMIT, Dependencies } from '$lib/constants'; import type { PageLoad } from './$types'; import { redirect } from '@sveltejs/kit'; -import { base } from '$app/paths'; +import { resolve } from '$app/paths'; export const load: PageLoad = async ({ params, url, route, depends, parent }) => { const { scopes } = await parent(); if (!scopes.includes('projects.read') && scopes.includes('billing.read')) { - return redirect(301, `${base}/organization-${params.organization}/billing`); + return redirect( + 301, + resolve('/(console)/organization-[organization]/billing', { + organization: params.organization + }) + ); } depends(Dependencies.ORGANIZATION); @@ -20,76 +25,33 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) => const offset = pageToOffset(page, limit); const search = getSearch(url); - const archivedPageRaw = parseInt(url.searchParams.get('archivedPage') || '1', 10); - const archivedPage = - Number.isFinite(archivedPageRaw) && archivedPageRaw > 0 ? archivedPageRaw : 1; - const archivedOffset = pageToOffset(archivedPage, limit); - const searchQueries = search ? [Query.or([Query.search('search', search), Query.contains('labels', search)])] : []; - const commonQueries = [Query.equal('teamId', params.organization)]; const activeQueries = isCloud ? [Query.or([Query.equal('status', 'active'), Query.isNull('status')])] : []; - const [activeProjects, archivedProjects, activeTotal, archivedTotal] = await Promise.all([ - sdk.forConsole.projects.list({ - queries: [ - Query.offset(offset), - Query.limit(limit), - Query.orderDesc(''), - ...commonQueries, - ...searchQueries, - ...activeQueries - ] - }), - isCloud - ? sdk.forConsole.projects.list({ - queries: [ - Query.offset(archivedOffset), - Query.limit(limit), - Query.orderDesc(''), - ...commonQueries, - ...searchQueries, - Query.equal('status', 'archived') - ] - }) - : Promise.resolve({ projects: [], total: 0 }), - sdk.forConsole.projects.list({ - queries: [...commonQueries, ...activeQueries, ...searchQueries] - }), - isCloud - ? sdk.forConsole.projects.list({ - queries: [...commonQueries, ...searchQueries, Query.equal('status', 'archived')] - }) - : Promise.resolve({ projects: [], total: 0 }) - ]); + const activeProjects = await sdk.forConsole.projects.list({ + queries: [ + ...searchQueries, + ...activeQueries, + Query.offset(offset), + Query.limit(limit), + Query.orderDesc(''), + Query.equal('teamId', params.organization) + ] + }); // set `default` if no region! for (const project of activeProjects.projects) { project.region ??= 'default'; } - if (isCloud) { - for (const project of archivedProjects.projects) { - project.region ??= 'default'; - } - } return { - offset, limit, - projects: { - ...activeProjects, - projects: activeProjects.projects, - total: activeTotal.total - }, - activeProjectsPage: activeProjects.projects, - archivedProjectsPage: archivedProjects.projects, - activeTotalOverall: activeTotal.total, - archivedTotalOverall: archivedTotal.total, - archivedOffset, - archivedPage, - search + offset, + search, + projects: activeProjects }; }; diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte index 0e06f10169..d55a49bd75 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte @@ -171,7 +171,7 @@ paymentMethodId }); - // 2) If the target plan has a project limit, apply selected projects now + // 2) If the plan has a project limit, delete excess const targetProjectsLimit = selectedPlan?.projects ?? 0; if (targetProjectsLimit > 0 && usageLimitsComponent) { const selected = usageLimitsComponent.getSelectedProjects(); diff --git a/src/routes/(console)/project-[region]-[project]/+layout.ts b/src/routes/(console)/project-[region]-[project]/+layout.ts index 26683eddaa..01700f3795 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/+layout.ts @@ -18,6 +18,16 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { depends(Dependencies.PROJECT); const project = await sdk.forConsole.projects.get({ projectId: params.project }); + if (project.status !== 'active') { + // project isn't active, redirect back to organizations page + redirect( + 303, + resolve('/(console)/organization-[organization]', { + organization: project.teamId + }) + ); + } + project.region ??= 'default'; // fast path without a network call!