Skip to content

Commit 50fcdef

Browse files
authored
Merge pull request #2247 from appwrite/fix-projects-list-call
2 parents 405a4f1 + 31fda55 commit 50fcdef

File tree

10 files changed

+171
-110
lines changed

10 files changed

+171
-110
lines changed

src/lib/components/billing/alerts/projectsLimit.svelte

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,22 @@
99
upgradeURL
1010
} from '$lib/stores/billing';
1111
import { currentPlan } from '$lib/stores/organization';
12-
import { onMount } from 'svelte';
1312
import SelectProjectCloud from './selectProjectCloud.svelte';
1413
import { toLocaleDate } from '$lib/helpers/date';
15-
let showSelectProject: boolean = $state(false);
14+
1615
let selectedProjects: string[] = $state([]);
17-
onMount(() => {
18-
selectedProjects = page.data.organization?.projects || [];
16+
let showSelectProject: boolean = $state(false);
17+
18+
const organizationId = $derived(page.data.organization?.$id);
19+
20+
$effect(() => {
21+
if (organizationId) {
22+
selectedProjects = page.data.organization?.projects || [];
23+
}
1924
});
2025
</script>
2126

22-
<SelectProjectCloud bind:showSelectProject bind:selectedProjects />
27+
<SelectProjectCloud bind:showSelectProject bind:selectedProjects {organizationId} />
2328

2429
{#if $currentPlan && $currentPlan.projects > 0 && !hideBillingHeaderRoutes.includes(page.url.pathname)}
2530
<HeaderAlert
Lines changed: 102 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,30 @@
11
<script lang="ts">
2-
import { type Models } from '@appwrite.io/console';
3-
import { Alert, Button, Table } from '@appwrite.io/pink-svelte';
2+
import { type Models, Query } from '@appwrite.io/console';
3+
import { Alert, Button, Skeleton, Table } from '@appwrite.io/pink-svelte';
44
import { Modal } from '$lib/components';
5-
import { onMount } from 'svelte';
65
import { sdk } from '$lib/stores/sdk';
76
import { addNotification } from '$lib/stores/notifications';
87
import { invalidate } from '$app/navigation';
98
import { Dependencies } from '$lib/constants';
109
import { billingProjectsLimitDate } from '$lib/stores/billing';
11-
import { page } from '$app/state';
1210
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
1311
import { currentPlan } from '$lib/stores/organization';
1412
1513
let {
1614
showSelectProject = $bindable(false),
17-
selectedProjects = $bindable([])
15+
selectedProjects = $bindable([]),
16+
organizationId
1817
}: {
1918
showSelectProject: boolean;
2019
selectedProjects: string[];
20+
organizationId: string;
2121
} = $props();
2222
23-
let projects = $state<Array<Models.Project>>([]);
24-
let error = $state<string | null>(null);
23+
let loading = $state(false);
24+
let projectsLoadingError = $state<string | null>(null);
2525
26-
onMount(() => {
27-
const currentOrgId = page.data.organization?.$id;
28-
projects =
29-
page.data.currentOrgId === currentOrgId ? page.data.allProjects?.projects || [] : [];
30-
});
26+
let error = $state<string | null>(null);
27+
let projects = $state<Array<Models.Project>>([]);
3128
3229
let projectsToArchive = $derived(
3330
projects.filter((project) => !selectedProjects.includes(project.$id))
@@ -39,6 +36,7 @@
3936
projects[0].teamId,
4037
selectedProjects
4138
);
39+
4240
showSelectProject = false;
4341
invalidate(Dependencies.ORGANIZATION);
4442
addNotification({
@@ -69,47 +67,95 @@
6967
7068
return result;
7169
}
70+
71+
$effect(() => {
72+
if (!showSelectProject) return;
73+
74+
const areProjectsLoaded = projects.length > 0;
75+
const teamIdInLoadedProjects = areProjectsLoaded ? projects[0].teamId : null;
76+
77+
if (organizationId != teamIdInLoadedProjects) {
78+
loading = true;
79+
80+
sdk.forConsole.projects
81+
.list([
82+
Query.equal('teamId', organizationId),
83+
Query.limit(1000) // Get all projects for organization
84+
])
85+
.then((loadedProjects) => (projects = loadedProjects.projects))
86+
.catch((err) => (projectsLoadingError = err.message))
87+
.finally(() => (loading = false));
88+
}
89+
});
7290
</script>
7391

74-
<Modal bind:show={showSelectProject} title={'Manage projects'} onSubmit={updateSelected}>
92+
<Modal bind:show={showSelectProject} title="Manage projects" onSubmit={updateSelected}>
7593
<svelte:fragment slot="description">
7694
Choose which {$currentPlan?.projects || 2} projects to keep. Projects over the limit will be
7795
blocked after this date.
7896
</svelte:fragment>
79-
{#if error}
80-
<Alert.Inline status="error" title="Error">{error}</Alert.Inline>
81-
{/if}
82-
<Table.Root
83-
let:root
84-
allowSelection
85-
bind:selectedRows={selectedProjects}
86-
columns={[{ id: 'name' }, { id: 'created' }]}>
87-
<svelte:fragment slot="header" let:root>
88-
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
89-
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
90-
</svelte:fragment>
91-
{#each projects as project}
92-
<Table.Row.Base {root} id={project.$id}>
93-
<Table.Cell column="name" {root}>{project.name}</Table.Cell>
94-
<Table.Cell column="created" {root}
95-
>{toLocaleDateTime(project.$createdAt)}</Table.Cell>
96-
</Table.Row.Base>
97-
{/each}
98-
</Table.Root>
99-
{#if selectedProjects.length > $currentPlan?.projects}
100-
<div class="u-text-warning u-mb-4">
101-
You can only select {$currentPlan?.projects} projects. Please deselect others to continue.
97+
98+
{#if loading}
99+
<div class="skeleton-projects">
100+
<Table.Root let:root allowSelection columns={[{ id: 'name' }, { id: 'created' }]}>
101+
<svelte:fragment slot="header" let:root>
102+
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
103+
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
104+
</svelte:fragment>
105+
106+
{#each Array.from({ length: 5 }) as _}
107+
<Table.Row.Base {root} select="disabled">
108+
<Table.Cell column="name" {root}>
109+
<Skeleton variant="line" height={20} width="80%" />
110+
</Table.Cell>
111+
<Table.Cell column="created" {root}>
112+
<Skeleton variant="line" height={20} width="80%" />
113+
</Table.Cell>
114+
</Table.Row.Base>
115+
{/each}
116+
</Table.Root>
102117
</div>
103-
{/if}
104-
{#if selectedProjects.length === $currentPlan?.projects}
105-
<Alert.Inline
106-
status="warning"
107-
title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
108-
<span>
109-
{@html formatProjectsToArchive()}
110-
will be archived.
111-
</span>
112-
</Alert.Inline>
118+
{:else if projectsLoadingError}
119+
<Alert.Inline status="error" title="Error">{projectsLoadingError}</Alert.Inline>
120+
{:else}
121+
{#if error}
122+
<Alert.Inline status="error" title="Error">{error}</Alert.Inline>
123+
{/if}
124+
125+
<Table.Root
126+
let:root
127+
allowSelection
128+
bind:selectedRows={selectedProjects}
129+
columns={[{ id: 'name' }, { id: 'created' }]}>
130+
<svelte:fragment slot="header" let:root>
131+
<Table.Header.Cell column="name" {root}>Project Name</Table.Header.Cell>
132+
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
133+
</svelte:fragment>
134+
{#each projects as project}
135+
<Table.Row.Base {root} id={project.$id}>
136+
<Table.Cell column="name" {root}>{project.name}</Table.Cell>
137+
<Table.Cell column="created" {root}
138+
>{toLocaleDateTime(project.$createdAt)}</Table.Cell>
139+
</Table.Row.Base>
140+
{/each}
141+
</Table.Root>
142+
143+
{#if selectedProjects.length > $currentPlan?.projects}
144+
<div class="u-text-warning u-mb-4">
145+
You can only select {$currentPlan?.projects} projects. Please deselect others to continue.
146+
</div>
147+
{/if}
148+
149+
{#if selectedProjects.length === $currentPlan?.projects}
150+
<Alert.Inline
151+
status="warning"
152+
title={`${projects.length - selectedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
153+
<span>
154+
{@html formatProjectsToArchive()}
155+
will be archived.
156+
</span>
157+
</Alert.Inline>
158+
{/if}
113159
{/if}
114160
<svelte:fragment slot="footer">
115161
<Button.Button size="s" variant="secondary" on:click={() => (showSelectProject = false)}
@@ -118,3 +164,13 @@
118164
>Save</Button.Button>
119165
</svelte:fragment>
120166
</Modal>
167+
168+
<style lang="scss">
169+
/* disable the top select all selector button */
170+
.skeleton-projects :global([role='rowheader'] button) {
171+
opacity: 0.4;
172+
cursor: default;
173+
pointer-events: none;
174+
background: var(--bgcolor-neutral-tertiary);
175+
}
176+
</style>

src/lib/components/breadcrumbs.svelte

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import { base } from '$app/paths';
2323
import { currentPlan, newOrgModal } from '$lib/stores/organization';
2424
import { Click, trackEvent } from '$lib/actions/analytics';
25-
import type { Models } from '@appwrite.io/console';
25+
import { type Models, Query } from '@appwrite.io/console';
26+
import { sdk } from '$lib/stores/sdk';
27+
import { page } from '$app/state';
2628
2729
type Organization = {
2830
name: string;
@@ -63,15 +65,15 @@
6365
}
6466
} = createMenu();
6567
66-
let isLoadingProjects = true;
68+
let isLoadingProjects = false;
6769
let loadedProjects: Models.ProjectList = { total: 0, projects: [] };
6870
6971
export let organizations: Organization[] = [];
7072
export let currentProject: Models.Project | null = null;
71-
export let projects: Promise<Models.ProjectList> = Promise.resolve(loadedProjects);
7273
7374
let projectsBottomSheetOpen = false;
7475
let organisationBottomSheetOpen = false;
76+
let projectsBottomSheet: Promise<SheetMenu | null> = null;
7577
7678
function createOrg() {
7779
trackEvent(Click.OrganizationClickCreate, { source: 'breadcrumbs' });
@@ -99,13 +101,25 @@
99101
}
100102
};
101103
102-
async function createProjectsBottomSheet(organization: Organization): Promise<SheetMenu> {
104+
async function createProjectsBottomSheet(
105+
organization: Organization
106+
): Promise<SheetMenu | null> {
107+
if (!isOnProjects) return null;
108+
103109
isLoadingProjects = true;
110+
104111
// null on non-org/project path like `onboarding`.
105-
loadedProjects = (await projects) ?? loadedProjects;
112+
loadedProjects =
113+
(await sdk.forConsole.projects.list([
114+
Query.equal('teamId', organizationId ?? page.data.currentOrgId),
115+
Query.limit(5),
116+
Query.orderDesc('$updatedAt')
117+
])) ?? loadedProjects;
118+
106119
for (const project of loadedProjects.projects) {
107120
project.region ??= 'default';
108121
}
122+
109123
isLoadingProjects = false;
110124
111125
const createProjectItem = {
@@ -185,8 +199,6 @@
185199
186200
$: selectedOrg = organizations.find((org) => org.isSelected);
187201
188-
$: projectsBottomSheet = createProjectsBottomSheet(selectedOrg);
189-
190202
$: organizationsBottomSheet = createOrganizationBottomSheet(selectedOrg);
191203
192204
$: correctPlanName =
@@ -198,6 +210,20 @@
198210
: selectedOrg?.tierName; // fallback
199211
200212
$: derivedKey = `${selectedOrg?.$id}-${currentProject?.$id}`;
213+
214+
$: organizationId = currentProject?.teamId;
215+
216+
$: isOnProjects = page.route.id.includes('project-[region]-[project]');
217+
218+
$: shouldReloadProjects = isLoadingProjects
219+
? false
220+
: currentProject && loadedProjects.projects.length
221+
? loadedProjects.projects[0].teamId != currentProject.teamId
222+
: !loadedProjects.projects.length;
223+
224+
$: if (shouldReloadProjects) {
225+
projectsBottomSheet = createProjectsBottomSheet(selectedOrg);
226+
}
201227
</script>
202228

203229
<svelte:window on:resize={onResize} />
@@ -380,7 +406,9 @@
380406
<BottomSheet.Menu bind:isOpen={organisationBottomSheetOpen} menu={organizationsBottomSheet} />
381407

382408
{#await projectsBottomSheet then menu}
383-
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} {menu} />
409+
{#if menu}
410+
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} {menu} />
411+
{/if}
384412
{/await}
385413
{/key}
386414

src/lib/components/navbar.svelte

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
<script lang="ts" context="module">
22
import type { HTMLAttributes } from 'svelte/elements';
33
4-
export type NavbarProject = {
5-
name: string;
6-
$id: string;
7-
region: string;
8-
isSelected: boolean;
9-
platformCount: number;
10-
pingCount: number;
11-
};
12-
134
export type BaseNavbarProps = HTMLAttributes<HTMLHeadElement> & {
145
logo: {
156
src: string;
@@ -106,7 +97,6 @@
10697
export let sideBarIsOpen: $$Props['sideBarIsOpen'] = false;
10798
export let showAccountMenu = false;
10899
export let currentProject: Models.Project = undefined;
109-
export let projects: Promise<Models.ProjectList> = undefined;
110100
111101
let activeTheme = $app.theme;
112102
let shouldAnimateThemeToggle = false;
@@ -138,7 +128,9 @@
138128
class="only-desktop">
139129
<img src={logo.src} alt={logo.alt} />
140130
</a>
141-
<Breadcrumbs {organizations} {projects} {currentProject} />
131+
132+
<Breadcrumbs {organizations} {currentProject} />
133+
142134
{#if page.route?.id?.includes('/project-[region]-[project]') && currentProject && currentProject.pingCount === 0}
143135
<div class="only-desktop" style:margin-inline-start="-16px">
144136
<Button.Anchor

src/lib/layout/shell.svelte

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
export let showFooter = true;
2323
export let showSideNavigation = false;
2424
export let selectedProject: Models.Project = null;
25-
export let projects: Promise<Models.ProjectList> = undefined;
2625
2726
let yOnMenuOpen: number;
2827
let showContentTransition = false;
@@ -104,8 +103,6 @@
104103
};
105104
}),
106105
107-
projects: projects,
108-
109106
currentProject: selectedProject
110107
};
111108

src/lib/stores/billing.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,10 @@ export function calculateTrialDay(org: Organization) {
320320
export async function checkForProjectsLimit(org: Organization, orgProjectCount?: number) {
321321
if (!isCloud) return;
322322
if (!org) return;
323+
323324
const plan = await sdk.forConsole.billing.getOrganizationPlan(org.$id);
324325
if (!plan) return;
326+
325327
if (plan.$id !== BillingPlan.FREE) return;
326328
if (org.projects?.length > 0) return;
327329

0 commit comments

Comments
 (0)