Skip to content
32 changes: 27 additions & 5 deletions src/lib/components/archiveProject.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import ArchivedPaginationWithLimit from './archivedPaginationWithLimit.svelte';
import { Button, InputText } from '$lib/elements/forms';
import { DropList, GridItem1, CardContainer, Modal } from '$lib/components';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
Expand Down Expand Up @@ -48,9 +49,19 @@
projectsToArchive: Models.Project[];
organization: Organization;
currentPlan: Plan;
archivedTotalOverall: number;
archivedOffset: number;
limit: number;
}
let { projectsToArchive, organization, currentPlan }: Props = $props();
let {
projectsToArchive,
organization,
currentPlan,
archivedTotalOverall,
archivedOffset,
limit
}: Props = $props();
// Track Read-only info droplist per archived project
let readOnlyInfoOpen = $state<Record<string, boolean>>({});
Expand Down Expand Up @@ -189,21 +200,21 @@
}
import { formatName as formatNameHelper } from '$lib/helpers/string';
function formatName(name: string, limit: number = 19) {
function formatName(name: string, limit: number = 16) {
return formatNameHelper(name, limit, $isSmallViewport);
}
</script>

{#if projectsToArchive.length > 0}
<div class="archive-projects-margin-top">
<Accordion title="Archived projects" badge={`${projectsToArchive.length}`}>
<Accordion title="Archived projects" badge={`${archivedTotalOverall}`}>
<Typography.Text tag="p" size="s">
These projects have been archived and are read-only. You can view and migrate their
data.
</Typography.Text>

<div class="archive-projects-margin">
<CardContainer disableEmpty={true} total={projectsToArchive.length}>
<CardContainer disableEmpty={true} total={archivedTotalOverall}>
{#each projectsToArchive as project}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
Expand Down Expand Up @@ -292,7 +303,7 @@
</Badge>
{/each}

{#if platforms.length > 3}
{#if platforms.length > 2}
<Badge
variant="secondary"
content={`+${platforms.length - 2}`}
Expand All @@ -308,6 +319,14 @@
</GridItem1>
{/each}
</CardContainer>

<div class="pagination-container">
<ArchivedPaginationWithLimit
name="Archived Projects"
{limit}
offset={archivedOffset}
total={archivedTotalOverall} />
</div>
</div>
</Accordion>
</div>
Expand Down Expand Up @@ -381,4 +400,7 @@
align-items: center;
gap: 8px;
}
.pagination-container {
margin-top: 16px;
}
</style>
44 changes: 44 additions & 0 deletions src/lib/components/archivedLimit.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page as pageStore } from '$app/state';
import { InputSelect } from '$lib/elements/forms';
import { preferences } from '$lib/stores/preferences';
import { Layout } from '@appwrite.io/pink-svelte';
let { sum, limit, name }: { sum: number; limit: number; name: string } = $props();
const options = [
{ label: '6', value: 6 },
{ label: '12', value: 12 },
{ label: '24', value: 24 },
{ label: '48', value: 48 },
{ label: '96', value: 96 }
];
async function limitChange() {
const url = new URL(pageStore.url);
const previousLimit = Number(url.searchParams.get('limit'));
url.searchParams.set('limit', limit.toString());
preferences.setLimit(limit);
// Handle archived page pagination
if (url.searchParams.has('archivedPage')) {
const page = Number(url.searchParams.get('archivedPage'));
const newPage = Math.floor(((page - 1) * previousLimit) / limit);
if (newPage === 1) {
url.searchParams.delete('archivedPage');
} else {
url.searchParams.set('archivedPage', newPage.toString());
}
}
await goto(url.toString());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix off-by-one and null handling when recalculating archivedPage on limit change

  • Off-by-one: should be floor(startIndex/newLimit) + 1.
  • When limit query is missing, Number(null) becomes 0 and can produce page 0.
  • Clamp to page >= 1 and defend against zero/invalid limits.
-        const previousLimit = Number(url.searchParams.get('limit'));
+        const previousLimit = Number(url.searchParams.get('limit') ?? limit) || limit;
@@
-        if (url.searchParams.has('archivedPage')) {
-            const page = Number(url.searchParams.get('archivedPage'));
-            const newPage = Math.floor(((page - 1) * previousLimit) / limit);
-            if (newPage === 1) {
+        if (url.searchParams.has('archivedPage')) {
+            const page = Math.max(1, Number(url.searchParams.get('archivedPage')) || 1);
+            const startIndex = (page - 1) * Math.max(1, previousLimit);
+            const newPage = Math.max(1, Math.floor(startIndex / Math.max(1, limit)) + 1);
+            if (newPage <= 1) {
                 url.searchParams.delete('archivedPage');
             } else {
-                url.searchParams.set('archivedPage', newPage.toString());
+                url.searchParams.set('archivedPage', String(newPage));
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function limitChange() {
const url = new URL(pageStore.url);
const previousLimit = Number(url.searchParams.get('limit'));
url.searchParams.set('limit', limit.toString());
preferences.setLimit(limit);
// Handle archived page pagination
if (url.searchParams.has('archivedPage')) {
const page = Number(url.searchParams.get('archivedPage'));
const newPage = Math.floor(((page - 1) * previousLimit) / limit);
if (newPage === 1) {
url.searchParams.delete('archivedPage');
} else {
url.searchParams.set('archivedPage', newPage.toString());
}
}
await goto(url.toString());
}
async function limitChange() {
const url = new URL(pageStore.url);
const previousLimit = Number(url.searchParams.get('limit') ?? limit) || limit;
url.searchParams.set('limit', limit.toString());
preferences.setLimit(limit);
// Handle archived page pagination
if (url.searchParams.has('archivedPage')) {
const page = Math.max(1, Number(url.searchParams.get('archivedPage')) || 1);
const startIndex = (page - 1) * Math.max(1, previousLimit);
const newPage = Math.max(1, Math.floor(startIndex / Math.max(1, limit)) + 1);
if (newPage <= 1) {
url.searchParams.delete('archivedPage');
} else {
url.searchParams.set('archivedPage', String(newPage));
}
}
await goto(url.toString());
}

</script>

<Layout.Stack direction="row" alignItems="center" inline>
<InputSelect id="archived-rows" {options} bind:value={limit} on:change={limitChange} />
<p class="text" style:white-space="nowrap">
{name} per page. Total: {sum >= 5000 ? `${sum}+` : sum}
</p>
</Layout.Stack>
23 changes: 23 additions & 0 deletions src/lib/components/archivedPagination.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import { page as pageStore } from '$app/state';
import { Pagination } from '@appwrite.io/pink-svelte';

let { sum, limit, offset }: { sum: number; limit: number; offset: number } = $props();

const currentPage = $derived(Math.floor(offset / limit + 1));

function getLink(page: number): string {
const url = new URL(pageStore.url);
if (page === 1) {
url.searchParams.delete('archivedPage');
} else {
url.searchParams.set('archivedPage', page.toString());
}

return url.toString();
}

const paginationProps = $derived({ type: 'link', createLink: getLink } as const);
</script>

<Pagination on:page {limit} total={sum} page={currentPage} {...paginationProps} />
31 changes: 31 additions & 0 deletions src/lib/components/archivedPaginationWithLimit.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import ArchivedLimit from './archivedLimit.svelte';
import ArchivedPagination from './archivedPagination.svelte';
import { Layout } from '@appwrite.io/pink-svelte';

let {
limit,
offset,
total,
name,
useCreateLink = true
}: {
limit: number;
offset: number;
total: number;
name: string;
useCreateLink?: boolean;
} = $props();

const showLimit = $derived(!!useCreateLink);
const direction = $derived(showLimit ? 'row' : 'column');
const alignItems = $derived(showLimit ? 'center' : 'flex-end');
</script>

<Layout.Stack wrap="wrap" {direction} {alignItems} justifyContent="space-between">
{#if showLimit}
<ArchivedLimit {limit} sum={total} {name} />
{/if}

<ArchivedPagination on:page {limit} {offset} sum={total} />
</Layout.Stack>
25 changes: 18 additions & 7 deletions src/routes/(console)/organization-[organization]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,19 @@
return project.status === 'archived';
}
$: projectsToArchive = isCloud
? data.projects.projects.filter((project) => project.status === 'archived')
: [];
$: projectsToArchive = (data.archivedProjectsPage ?? data.projects.projects).filter(
(project) => (isCloud ? project.status === 'archived' : project.status !== 'active') // fallback for non-cloud
);
$: activeProjects = data.projects.projects.filter((project) => project.status !== 'archived');
$: activeProjects = (data.activeProjectsPage ?? data.projects.projects).filter(
(project) => project.status === 'active'
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct archived/active filters for non-cloud (null means active)

Per prior guidance, only status === 'archived' is archived; null is active. Current fallback misclassifies nulls.

-    $: projectsToArchive = (data.archivedProjectsPage ?? data.projects.projects).filter(
-        (project) => (isCloud ? project.status === 'archived' : project.status !== 'active') // fallback for non-cloud
-    );
+    $: projectsToArchive = (data.archivedProjectsPage ?? data.projects.projects).filter(
+        (project) => project.status === 'archived'
+    );
@@
-    $: activeProjects = (data.activeProjectsPage ?? data.projects.projects).filter(
-        (project) => project.status === 'active'
-    );
+    $: activeProjects = (data.activeProjectsPage ?? data.projects.projects).filter(
+        (project) => project.status !== 'archived'
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$: projectsToArchive = (data.archivedProjectsPage ?? data.projects.projects).filter(
(project) => (isCloud ? project.status === 'archived' : project.status !== 'active') // fallback for non-cloud
);
$: activeProjects = data.projects.projects.filter((project) => project.status !== 'archived');
$: activeProjects = (data.activeProjectsPage ?? data.projects.projects).filter(
(project) => project.status === 'active'
);
$: projectsToArchive = (data.archivedProjectsPage ?? data.projects.projects).filter(
(project) => project.status === 'archived'
);
$: activeProjects = (data.activeProjectsPage ?? data.projects.projects).filter(
(project) => project.status !== 'archived'
);
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/+page.svelte around lines
112–118, the fallback for non-cloud currently treats null as non-active causing
misclassification; change the archived filter to only include project.status ===
'archived' (no non-cloud fallback that treats null as archived), and change the
activeProjects filter to include project.status === 'active' or, for non-cloud,
treat null as active (i.e. project.status === 'active' || project.status == null
when isCloud is false).

$: activeTotalOverall =
data?.activeTotalOverall ??
data?.organization?.projects?.length ??
data?.projects?.total ??
0;
function clearSearch() {
searchQuery?.clearInput();
}
Expand Down Expand Up @@ -165,7 +173,7 @@
{#if activeProjects.length > 0}
<CardContainer
disableEmpty={!$canWriteProjects}
total={data.projects.total}
total={activeTotalOverall}
offset={data.offset}
on:click={handleCreateProject}>
{#each activeProjects as project}
Expand Down Expand Up @@ -250,13 +258,16 @@
name="Projects"
limit={data.limit}
offset={data.offset}
total={data.projects.total} />
total={activeTotalOverall} />

<!-- Archived Projects Section -->
<ArchiveProject
{projectsToArchive}
organization={data.organization}
currentPlan={$currentPlan} />
currentPlan={$currentPlan}
archivedTotalOverall={data.archivedTotalOverall}
archivedOffset={data.archivedOffset}
limit={data.limit} />
</Container>
<CreateOrganization bind:show={addOrganization} />
<CreateProject bind:show={showCreate} teamId={page.params.organization} />
Expand Down
50 changes: 47 additions & 3 deletions src/routes/(console)/organization-[organization]/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,69 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
const offset = pageToOffset(page, limit);
const search = getSearch(url);

const projects = await sdk.forConsole.projects.list({
const archivedPage = parseInt(url.searchParams.get('archivedPage') || '1');
const archivedOffset = pageToOffset(archivedPage, limit);

// fetch active projects with offset set
const activeProjects = await sdk.forConsole.projects.list({
queries: [
Query.offset(offset),
Query.equal('teamId', params.organization),
Query.or([Query.equal('status', 'active'), Query.isNull('status')]),
Query.limit(limit),
Query.orderDesc('')
],
search: search || undefined
});

// Fetch archived projects with separate pagination
const archivedProjects = await sdk.forConsole.projects.list({
queries: [
Query.offset(archivedOffset),
Query.equal('teamId', params.organization),
Query.equal('status', 'archived'),
Query.limit(limit),
Query.orderDesc('')
],
search: search || undefined
});

// get total counts
const activeTotal = await sdk.forConsole.projects.list({
queries: [
Query.equal('teamId', params.organization),
Query.or([Query.equal('status', 'active'), Query.isNull('status')])
],
search: search || undefined
});

const archivedTotal = await sdk.forConsole.projects.list({
queries: [Query.equal('teamId', params.organization), Query.equal('status', 'archived')],
search: search || undefined
});

// set `default` if no region!
for (const project of projects.projects) {
for (const project of activeProjects.projects) {
project.region ??= 'default';
}
for (const project of archivedProjects.projects) {
project.region ??= 'default';
}

return {
offset,
limit,
projects,
projects: {
...activeProjects,
projects: activeProjects.projects,
total: activeTotal.total
},
activeProjectsPage: activeProjects.projects,
archivedProjectsPage: archivedProjects.projects,
activeTotalOverall: activeTotal.total,
archivedTotalOverall: archivedTotal.total,
archivedOffset,
archivedPage,
search
};
};