Skip to content
33 changes: 28 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 PaginationWithLimit from './paginationWithLimit.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,15 @@
</GridItem1>
{/each}
</CardContainer>

<PaginationWithLimit
name="Archived Projects"
{limit}
offset={archivedOffset}
total={archivedTotalOverall}
pageParam="archivedPage"
removeOnFirstPage
class="pagination-container" />
</div>
</Accordion>
</div>
Expand Down Expand Up @@ -381,4 +401,7 @@
align-items: center;
gap: 8px;
}
:global(.pagination-container) {
margin-top: 16px;
}
</style>
13 changes: 10 additions & 3 deletions src/lib/components/limit.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
export let sum: number;
export let limit: number;
export let name: string;
export let pageParam: string = 'page';
export let removeOnFirstPage: boolean = false;
const options = [
{ label: '6', value: 6 },
Expand All @@ -23,10 +25,15 @@
url.searchParams.set('limit', limit.toString());
await preferences.setLimit(limit);
if (url.searchParams.has('page')) {
const page = Number(url.searchParams.get('page'));
if (url.searchParams.has(pageParam)) {
const page = Number(url.searchParams.get(pageParam));
const newPage = Math.floor(((page - 1) * previousLimit) / limit);
url.searchParams.set('page', newPage.toString());
const safePage = Math.max(1, Number.isFinite(newPage) ? newPage : 1);
if (removeOnFirstPage && safePage === 1) {
url.searchParams.delete(pageParam);
} else {
url.searchParams.set(pageParam, safePage.toString());
}
Comment on lines +28 to +36
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 when recomputing page after limit change.

newPage needs a +1 to convert from 0-based index to 1-based page. Without it, unchanged limits incorrectly drop page N to page N-1, and scaling limits can jump to the wrong page.

Apply:

-        if (url.searchParams.has(pageParam)) {
-            const page = Number(url.searchParams.get(pageParam));
-            const newPage = Math.floor(((page - 1) * previousLimit) / limit);
-            const safePage = Math.max(1, Number.isFinite(newPage) ? newPage : 1);
+        if (url.searchParams.has(pageParam)) {
+            const page = Number(url.searchParams.get(pageParam));
+            const prev = Number.isFinite(previousLimit) && previousLimit > 0 ? previousLimit : limit;
+            const newPage = Math.floor(((page - 1) * prev) / limit) + 1;
+            const safePage = Math.max(1, Number.isFinite(newPage) ? newPage : 1);
             if (removeOnFirstPage && safePage === 1) {
                 url.searchParams.delete(pageParam);
             } else {
                 url.searchParams.set(pageParam, safePage.toString());
             }
         }
📝 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
if (url.searchParams.has(pageParam)) {
const page = Number(url.searchParams.get(pageParam));
const newPage = Math.floor(((page - 1) * previousLimit) / limit);
url.searchParams.set('page', newPage.toString());
const safePage = Math.max(1, Number.isFinite(newPage) ? newPage : 1);
if (removeOnFirstPage && safePage === 1) {
url.searchParams.delete(pageParam);
} else {
url.searchParams.set(pageParam, safePage.toString());
}
if (url.searchParams.has(pageParam)) {
const page = Number(url.searchParams.get(pageParam));
const prev = Number.isFinite(previousLimit) && previousLimit > 0 ? previousLimit : limit;
const newPage = Math.floor(((page - 1) * prev) / limit) + 1;
const safePage = Math.max(1, Number.isFinite(newPage) ? newPage : 1);
if (removeOnFirstPage && safePage === 1) {
url.searchParams.delete(pageParam);
} else {
url.searchParams.set(pageParam, safePage.toString());
}
}
🤖 Prompt for AI Agents
In src/lib/components/limit.svelte around lines 28 to 36, the recomputation of
newPage when adjusting limits uses a 0-based index but the app uses 1-based page
numbers; add +1 to the newPage calculation so it converts back to 1-based pages
(i.e., compute newPage = Math.floor(((page - 1) * previousLimit) / limit) + 1),
then keep the existing safePage logic and pageParam removal/setting unchanged.

}
await goto(url.toString());
Expand Down
10 changes: 8 additions & 2 deletions src/lib/components/pagination.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@
export let limit: number;
export let offset: number;
export let useCreateLink = true;
export let pageParam: string = 'page';
export let removeOnFirstPage: boolean = false;

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

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

return url.toString();
Expand Down
21 changes: 17 additions & 4 deletions src/lib/components/paginationWithLimit.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,37 @@
offset,
total,
name,
useCreateLink = true
useCreateLink = true,
pageParam = 'page',
removeOnFirstPage = false,
...restProps
}: {
limit: number;
offset: number;
total: number;
name: string;
useCreateLink?: boolean;
pageParam?: string;
removeOnFirstPage?: boolean;
[key: string]: unknown;
} = $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">
<Layout.Stack wrap="wrap" {direction} {alignItems} justifyContent="space-between" {...restProps}>
{#if showLimit}
<Limit {limit} sum={total} {name} />
<Limit {limit} sum={total} {name} {pageParam} {removeOnFirstPage} />
{/if}

<Pagination on:page {limit} {offset} sum={total} {useCreateLink} />
<Pagination
on:page
{limit}
{offset}
sum={total}
{useCreateLink}
{pageParam}
{removeOnFirstPage} />
</Layout.Stack>
6 changes: 4 additions & 2 deletions src/lib/components/paginator.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
name = 'items',
gap = 's',
offset = $bindable(0),
children
children,
...restProps
}: {
items: T[];
limit?: number;
Expand All @@ -26,14 +27,15 @@
| undefined;
offset?: number;
children: Snippet<[T[], number]>;
[key: string]: unknown;
} = $props();

let total = $derived(items.length);

let paginatedItems = $derived(items.slice(offset, offset + limit));
</script>

<Layout.Stack {gap}>
<Layout.Stack {gap} {...restProps}>
{@render children(paginatedItems, limit)}

{#if !hideFooter}
Expand Down
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) => project.status === 'archived'
);

$: activeProjects = data.projects.projects.filter((project) => project.status !== 'archived');
$: activeProjects = (data.activeProjectsPage ?? data.projects.projects).filter(
(project) => project.status === 'active'
);

$: 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
66 changes: 55 additions & 11 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({
queries: [
Query.offset(offset),
Query.equal('teamId', params.organization),
Query.limit(limit),
Query.orderDesc('')
],
search: search || undefined
});
const archivedPageRaw = parseInt(url.searchParams.get('archivedPage') || '1', 10);
const archivedPage =
Number.isFinite(archivedPageRaw) && archivedPageRaw > 0 ? archivedPageRaw : 1;
const archivedOffset = pageToOffset(archivedPage, limit);
const [activeProjects, archivedProjects, activeTotal, archivedTotal] = await Promise.all([
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
}),
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
}),
sdk.forConsole.projects.list({
queries: [
Query.equal('teamId', params.organization),
Query.or([Query.equal('status', 'active'), Query.isNull('status')])
],
search: search || undefined
}),
sdk.forConsole.projects.list({
queries: [
Query.equal('teamId', params.organization),
Query.equal('status', 'archived')
],
search: search || undefined
})
]);
Comment on lines +25 to +60
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Optional: verify Query.orderDesc('') usage.

Empty field name may be ignored or error depending on SDK version. Confirm intended default ordering or set an explicit field (e.g., createdAt/updatedAt).


🌐 Web query:

Is an empty string a valid field for Appwrite `Query.orderDesc('')`? What field should be used to order projects by most recently created in the Appwrite Console SDK?

💡 Result:

No — an empty string is not a valid field for Query.orderDesc(''). To sort by most-recently-created use the created timestamp field (Appwrite exposes $createdAt). Example: Query.orderDesc('$createdAt') — (escape the $ if your language/string parser requires, e.g. '$createdAt' in Flutter). You can also use $sequence for insertion order if preferred. [1][2][3]


Use explicit timestamp field in Query.orderDesc
Replace both instances of Query.orderDesc('') with Query.orderDesc('$createdAt') to ensure projects are sorted by most-recently-created timestamp.

🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/+page.ts around lines 24 to
59, the two calls using Query.orderDesc('') should explicitly sort by creation
timestamp; replace both Query.orderDesc('') occurrences with
Query.orderDesc('$createdAt') so projects are ordered by most-recently-created
items.


// 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
};
};