Skip to content
210 changes: 114 additions & 96 deletions src/lib/components/archiveProject.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { DropList, GridItem1, CardContainer } from '$lib/components';
import { DropList, GridItem1, CardContainer, Paginator } from '$lib/components';
import {
Badge,
Icon,
Expand Down Expand Up @@ -146,7 +146,7 @@
}
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>
Expand All @@ -160,103 +160,121 @@
</Typography.Text>

<div class="archive-projects-margin">
<CardContainer disableEmpty={true} total={projectsToArchive.length}>
{#each projectsToArchive as project}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
{@const formatted = formatName(project.name)}
<GridItem1>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">{formatted}</svelte:fragment>
<svelte:fragment slot="status">
<div class="status-container">
<DropList
bind:show={readOnlyInfoOpen[project.$id]}
placement="bottom-start"
noArrow>
<Tag
size="s"
style="white-space: nowrap;"
on:click={(e) => {
e.preventDefault();
e.stopPropagation();
readOnlyInfoOpen = {
...readOnlyInfoOpen,
[project.$id]: !readOnlyInfoOpen[project.$id]
};
}}>
<Icon icon={IconInfo} size="s" />
<span>Read only</span>
</Tag>
<svelte:fragment slot="list">
<li
class="drop-list-item u-width-250"
style="padding: var(--space-5, 12px) var(--space-6, 16px)">
<span class="u-block u-mb-8">
Archived projects are read-only. You can view
and migrate their data, but they no longer
accept edits or requests.
</span>
</li>
</svelte:fragment>
</DropList>
<Popover let:toggle padding="none" placement="bottom-end">
<Button
text
icon
size="s"
ariaLabel="more options"
on:click={(e) => {
e.preventDefault();
e.stopPropagation();
toggle(e);
}}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip">
<ActionMenu.Item.Button
leadingIcon={IconInboxIn}
disabled={isUnarchiveDisabled()}
on:click={() => handleUnarchiveProject(project)}
>Unarchive project</ActionMenu.Item.Button>
<ActionMenu.Item.Button
leadingIcon={IconSwitchHorizontal}
on:click={() => handleMigrateProject(project)}
>Migrate project</ActionMenu.Item.Button>
</ActionMenu.Root>
</Popover>
</div>
</svelte:fragment>
<Paginator
items={projectsToArchive}
limit={6}
hidePages={false}
hideFooter={projectsToArchive.length <= 6}>
{#snippet children(items)}
<CardContainer disableEmpty={true} total={projectsToArchive.length}>
{#each items as project}
{@const platforms = filterPlatforms(
project.platforms.map((platform) =>
getPlatformInfo(platform.type)
)
)}
{@const formatted = formatName(project.name)}
<GridItem1>
<svelte:fragment slot="eyebrow">
{project?.platforms?.length
? project?.platforms?.length
: 'No'} apps
</svelte:fragment>
<svelte:fragment slot="title">{formatted}</svelte:fragment>
<svelte:fragment slot="status">
<div class="status-container">
<DropList
bind:show={readOnlyInfoOpen[project.$id]}
placement="bottom-start"
noArrow>
<Tag
size="s"
style="white-space: nowrap;"
on:click={(e) => {
e.preventDefault();
e.stopPropagation();
readOnlyInfoOpen = {
...readOnlyInfoOpen,
[project.$id]:
!readOnlyInfoOpen[project.$id]
};
}}>
<Icon icon={IconInfo} size="s" />
<span>Read only</span>
</Tag>
<svelte:fragment slot="list">
<li
class="drop-list-item u-width-250"
style="padding: var(--space-5, 12px) var(--space-6, 16px)">
<span class="u-block u-mb-8">
Archived projects are read-only. You can
view and migrate their data, but they no
longer accept edits or requests.
</span>
</li>
</svelte:fragment>
</DropList>
<Popover
let:toggle
padding="none"
placement="bottom-end">
<Button
text
icon
size="s"
ariaLabel="more options"
on:click={(e) => {
e.preventDefault();
e.stopPropagation();
toggle(e);
}}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip">
<ActionMenu.Item.Button
leadingIcon={IconInboxIn}
disabled={isUnarchiveDisabled()}
on:click={() =>
handleUnarchiveProject(project)}
>Unarchive project</ActionMenu.Item.Button>
<ActionMenu.Item.Button
leadingIcon={IconSwitchHorizontal}
on:click={() =>
handleMigrateProject(project)}
>Migrate project</ActionMenu.Item.Button>
</ActionMenu.Root>
</Popover>
</div>
</svelte:fragment>

{#each platforms.slice(0, 2) as platform}
{@const icon = getIconForPlatform(platform.icon)}
<Badge
variant="secondary"
content={platform.name}
style="width: max-content;">
<Icon {icon} size="s" slot="start" />
</Badge>
{/each}
{#each platforms.slice(0, 2) as platform}
{@const icon = getIconForPlatform(platform.icon)}
<Badge
variant="secondary"
content={platform.name}
style="width: max-content;">
<Icon {icon} size="s" slot="start" />
</Badge>
{/each}

{#if platforms.length > 3}
<Badge
variant="secondary"
content={`+${platforms.length - 2}`}
style="width: max-content;" />
{/if}
{#if platforms.length > 3}
<Badge
variant="secondary"
content={`+${platforms.length - 2}`}
style="width: max-content;" />
{/if}

<svelte:fragment slot="icons">
{#if isCloud && $regionsStore?.regions}
{@const region = findRegion(project)}
<Typography.Text>{region?.name}</Typography.Text>
{/if}
</svelte:fragment>
</GridItem1>
{/each}
</CardContainer>
<svelte:fragment slot="icons">
{#if isCloud && $regionsStore?.regions}
{@const region = findRegion(project)}
<Typography.Text>{region?.name}</Typography.Text>
{/if}
</svelte:fragment>
</GridItem1>
{/each}
</CardContainer>
{/snippet}
</Paginator>
</div>
</Accordion>
</div>
Expand Down
17 changes: 13 additions & 4 deletions src/routes/(console)/organization-[organization]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,18 @@
return project.status !== 'active';
}

$: projectsToArchive = data.projects.projects.filter((project) => project.status !== 'active');
$: projectsToArchive = (data.archivedProjectsPage ?? data.projects.projects).filter(
(project) => project.status !== 'active'
);

$: activeProjects = data.projects.projects.filter((project) => project.status === 'active');
$: 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 @@ -157,7 +166,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 @@ -242,7 +251,7 @@
name="Projects"
limit={data.limit}
offset={data.offset}
total={data.projects.total} />
total={activeTotalOverall} />

<!-- Archived Projects Section -->
<ArchiveProject
Expand Down
31 changes: 29 additions & 2 deletions src/routes/(console)/organization-[organization]/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,42 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
search: search || undefined
});

let allProjects: typeof projects.projects = [];
let fetchedCount = 0;
const total = projects.total;

while (fetchedCount < total) {
const next = await sdk.forConsole.projects.list({
queries: [
Query.offset(fetchedCount),
Query.equal('teamId', params.organization),
Query.limit(limit),
Query.orderDesc('')
],
search: search || undefined
});
allProjects = allProjects.concat(next.projects);
fetchedCount += next.projects.length;
if (next.projects.length === 0) break;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Seed aggregation with the first response and avoid redundant page-0 fetch

You drop the first call’s results and refetch from offset 0, doubling cost and latency. Seed allProjects with the initial page and continue from there.

-    let allProjects: typeof projects.projects = [];
-    let fetchedCount = 0;
+    let allProjects: typeof projects.projects = projects.projects.slice();
+    let fetchedCount = allProjects.length;
     const total = projects.total;
 
-    while (fetchedCount < total) {
+    while (fetchedCount < total) {
         const next = await sdk.forConsole.projects.list({
             queries: [
-                Query.offset(fetchedCount),
+                Query.offset(fetchedCount),
                 Query.equal('teamId', params.organization),
-                Query.limit(limit),
+                Query.limit(limit),
                 Query.orderDesc('')
             ],
             search: search || undefined
         });
         allProjects = allProjects.concat(next.projects);
         fetchedCount += next.projects.length;
         if (next.projects.length === 0) break;
     }
📝 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
let allProjects: typeof projects.projects = [];
let fetchedCount = 0;
const total = projects.total;
while (fetchedCount < total) {
const next = await sdk.forConsole.projects.list({
queries: [
Query.offset(fetchedCount),
Query.equal('teamId', params.organization),
Query.limit(limit),
Query.orderDesc('')
],
search: search || undefined
});
allProjects = allProjects.concat(next.projects);
fetchedCount += next.projects.length;
if (next.projects.length === 0) break;
}
// Seed with the initial page of projects instead of starting empty
let allProjects: typeof projects.projects = projects.projects.slice();
let fetchedCount = allProjects.length;
const total = projects.total;
while (fetchedCount < total) {
const next = await sdk.forConsole.projects.list({
queries: [
Query.offset(fetchedCount),
Query.equal('teamId', params.organization),
Query.limit(limit),
Query.orderDesc('')
],
search: search || undefined
});
allProjects = allProjects.concat(next.projects);
fetchedCount += next.projects.length;
if (next.projects.length === 0) break;
}
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/+page.ts around lines 31 to
48, the code currently performs an initial projects fetch but then starts the
loop that refetches from offset 0, doubling cost and latency; instead, perform
the first sdk.forConsole.projects.list call once, initialize allProjects with
that response and set fetchedCount to that response.projects.length, then enter
a loop that requests subsequent pages starting at the current fetchedCount
(using Query.offset(fetchedCount)) until fetchedCount >= total or a page returns
zero items; this removes the redundant page-0 fetch while preserving the same
paging logic.


const allActiveProjects = allProjects.filter((p) => p.status === 'active');
const allArchivedProjects = allProjects.filter((p) => p.status !== 'active');

const activeProjectsForPage = allActiveProjects.slice(offset, offset + limit);

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

return {
offset,
limit,
projects,
projects: { ...projects, projects: allProjects, total: allActiveProjects.length },
activeProjectsPage: activeProjectsForPage,
archivedProjectsPage: allArchivedProjects,
activeTotalOverall: allActiveProjects.length,
search
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

Don’t mutate the projects contract; return aggregates separately

Overwriting projects.projects with all items and projects.total with “active total” breaks consumers expecting API semantics. Keep projects intact and expose new top-level fields for UI.

-        projects: { ...projects, projects: allProjects, total: allActiveProjects.length },
-        activeProjectsPage: activeProjectsForPage,
-        archivedProjectsPage: allArchivedProjects,
-        activeTotalOverall: allActiveProjects.length,
+        projects, // preserve API contract for stores and other consumers
+        activeProjectsPage: activeProjectsForPage,
+        archivedProjectsPage: allArchivedProjects,
+        activeTotalOverall: allActiveProjects.length,
+        // Optionally expose allProjects if needed elsewhere:
+        // allProjects,

🏁 Script executed:

#!/bin/bash
# Audit for consumers that assume `projects.total` is "all results" (not just active).
rg -nP --type=ts --type=svelte -C2 '\bprojects\.total\b'

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Audit for consumers that assume `projects.total` remains the full count
rg -nP -g '*.ts' -g '*.svelte' -C2 '\bprojects\.total\b'

Length of output: 4397


Preserve projects API contract and expose aggregates separately
Overriding projects.projects and projects.total with active-only values breaks UI components that rely on the full project count (plan checks, creation modals, settings pages). Undo the mutation and introduce new top-level fields for the aggregates, for example:

-        projects: { ...projects, projects: allProjects, total: allActiveProjects.length },
+        projects, // preserve API contract
         activeProjectsPage: activeProjectsForPage,
         archivedProjectsPage: allArchivedProjects,
+        activeTotalOverall: allActiveProjects.length,
+        allProjects, // full list if needed

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/+page.ts around lines 63 to
67, the code is mutating the existing projects object by replacing
projects.projects and projects.total with active-only values which breaks
consumers expecting the full projects API; revert that mutation so the original
projects object remains intact and add new top-level fields (e.g.,
activeProjectsPage and activeProjectsTotal or activeProjectsAggregate) to expose
active-only lists and counts, update any callers that need the active-only
values to use these new fields, and ensure the returned payload preserves the
original projects structure while exposing the aggregates separately.

};
};