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}
-
-
-
- {
- e.preventDefault();
- e.stopPropagation();
- toggle(e);
- }}>
-
-
-
- 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.
-
-
-
- Cancel
- Unarchive
-
-
-
-
-
-
-
- The archived project {projectToDelete?.name} will be deleted along with all
- of its metadata, stats, and other resources.
- This action is irreversible.
-
-
-
-
-
- {
- resetDeleteState();
- }}>Cancel
-
- Delete
-
-
-
-
-
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.
-
-
- {
- showSelectProject = true;
- }}>Manage projects
-
- Upgrade
-
-
-
-{/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
-
- {
- trackEvent(Click.OrganizationClickUpgrade, {
- from: 'button',
- source: 'projects_archive_alert'
- });
- }}>
- Upgrade to Pro
-
-
-
- {/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!