Skip to content

Commit 39a66f9

Browse files
committed
Archive projects ui
1 parent 0f0c76b commit 39a66f9

File tree

7 files changed

+225
-95
lines changed

7 files changed

+225
-95
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<script lang="ts">
2+
import { Button } from '$lib/elements/forms';
3+
import { CardContainer, DropList, GridItem1 } from '$lib/components';
4+
import {
5+
Badge,
6+
Icon,
7+
Typography,
8+
Tag,
9+
Accordion,
10+
ActionMenu,
11+
Popover
12+
} from '@appwrite.io/pink-svelte';
13+
import {
14+
IconAndroid,
15+
IconApple,
16+
IconCode,
17+
IconFlutter,
18+
IconReact,
19+
IconUnity,
20+
IconInfo,
21+
IconDotsHorizontal,
22+
IconInboxIn,
23+
IconSwitchHorizontal
24+
} from '@appwrite.io/pink-icons-svelte';
25+
import { getPlatformInfo } from '$lib/helpers/platform';
26+
import type { Models } from '@appwrite.io/console';
27+
import type { ComponentType } from 'svelte';
28+
29+
// props
30+
interface Props {
31+
projectsToArchive: Models.Project[];
32+
}
33+
34+
let { projectsToArchive }: Props = $props();
35+
36+
// Track Read-only info droplist per archived project
37+
let readOnlyInfoOpen = $state<Record<string, boolean>>({});
38+
39+
function filterPlatforms(platforms: { name: string; icon: string }[]) {
40+
return platforms.filter(
41+
(value, index, self) => index === self.findIndex((t) => t.name === value.name)
42+
);
43+
}
44+
45+
function getIconForPlatform(platform: string): ComponentType {
46+
switch (platform) {
47+
case 'code':
48+
return IconCode;
49+
case 'flutter':
50+
return IconFlutter;
51+
case 'apple':
52+
return IconApple;
53+
case 'android':
54+
return IconAndroid;
55+
case 'react-native':
56+
return IconReact;
57+
case 'unity':
58+
return IconUnity;
59+
default:
60+
return IconCode;
61+
}
62+
}
63+
</script>
64+
65+
{#if projectsToArchive.length > 0}
66+
<div style="margin-top: 36px;">
67+
<Accordion title="Archived projects" badge={`${projectsToArchive.length}`}>
68+
<Typography.Text tag="p" size="s">
69+
These projects have been archived and are read-only. You can view and migrate their
70+
data.
71+
</Typography.Text>
72+
73+
<div style="margin-top: 16px; margin-bottom: 36px;">
74+
<CardContainer>
75+
{#each projectsToArchive as project}
76+
{@const platforms = filterPlatforms(
77+
project.platforms.map((platform) => getPlatformInfo(platform.type))
78+
)}
79+
<GridItem1>
80+
<svelte:fragment slot="eyebrow">
81+
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
82+
</svelte:fragment>
83+
<svelte:fragment slot="title">{project.name}</svelte:fragment>
84+
<svelte:fragment slot="status">
85+
<span style="display: flex; align-items: center; gap: 8px;">
86+
<DropList
87+
bind:show={readOnlyInfoOpen[project.$id]}
88+
placement="bottom-start"
89+
noArrow>
90+
<Tag
91+
size="s"
92+
style="white-space: nowrap; max-width: none;"
93+
on:click={(e) => {
94+
e.preventDefault();
95+
e.stopPropagation();
96+
readOnlyInfoOpen = {
97+
...readOnlyInfoOpen,
98+
[project.$id]: !readOnlyInfoOpen[project.$id]
99+
};
100+
}}>
101+
<Icon icon={IconInfo} size="s" />
102+
<span>Read only</span>
103+
</Tag>
104+
<svelte:fragment slot="list">
105+
<li
106+
class="drop-list-item u-width-250"
107+
style="padding: var(--space-5, 12px) var(--space-6, 16px)">
108+
<span class="u-block u-mb-8">
109+
Archived projects are read-only. You can view
110+
and migrate their data, but they no longer
111+
accept edits or requests.
112+
</span>
113+
</li>
114+
</svelte:fragment>
115+
</DropList>
116+
<Popover let:toggle padding="none" placement="bottom-end">
117+
<Button
118+
text
119+
icon
120+
size="s"
121+
ariaLabel="more options"
122+
on:click={(e) => {
123+
e.preventDefault();
124+
e.stopPropagation();
125+
toggle(e);
126+
}}>
127+
<Icon icon={IconDotsHorizontal} size="s" />
128+
</Button>
129+
<ActionMenu.Root slot="tooltip">
130+
<ActionMenu.Item.Button leadingIcon={IconInboxIn}
131+
>Unarchive project</ActionMenu.Item.Button>
132+
<ActionMenu.Item.Button
133+
leadingIcon={IconSwitchHorizontal}
134+
>Migrate project</ActionMenu.Item.Button>
135+
</ActionMenu.Root>
136+
</Popover>
137+
</span>
138+
</svelte:fragment>
139+
{#each platforms.slice(0, 2) as platform}
140+
{@const icon = getIconForPlatform(platform.icon)}
141+
<Badge
142+
variant="secondary"
143+
content={platform.name}
144+
style="width: max-content;">
145+
<Icon {icon} size="s" slot="start" />
146+
</Badge>
147+
{/each}
148+
</GridItem1>
149+
{/each}
150+
<svelte:fragment slot="empty">
151+
<p>No archived projects</p>
152+
</svelte:fragment>
153+
</CardContainer>
154+
</div>
155+
</Accordion>
156+
</div>
157+
{/if}

src/lib/components/gridItem1.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { Card, Layout, Typography } from '@appwrite.io/pink-svelte';
3-
export let href: string;
3+
export let href: string = null;
44
</script>
55

66
<Card.Link class="card" {href}>

src/lib/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,4 @@ export { default as UsageCard } from './usageCard.svelte';
8484
export { default as ViewToggle } from './viewToggle.svelte';
8585
export { default as RegionEndpoint } from './regionEndpoint.svelte';
8686
export { default as ExpirationInput } from './expirationInput.svelte';
87+
export { default as ArchiveProject } from './archiveProject.svelte';

src/lib/sdk/billing.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,21 +207,21 @@ export type AggregationTeam = {
207207
* Aggregation billing plan
208208
*/
209209
plan: string;
210-
projectBreakdown: ProjectBreakdown[]
210+
projectBreakdown: ProjectBreakdown[];
211211
};
212212

213213
export type ProjectBreakdown = {
214214
$id: string;
215215
name: string;
216216
amount: number;
217217
resources: InvoiceUsage[];
218-
}
218+
};
219219

220220
export type InvoiceUsage = {
221221
resourceId: string;
222222
value: number;
223223
amount: number;
224-
}
224+
};
225225

226226
export type AvailableCredit = {
227227
available: number;

src/routes/(console)/organization-[organization]/+page.svelte

Lines changed: 60 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
import { onMount, type ComponentType } from 'svelte';
2626
import { canWriteProjects } from '$lib/stores/roles';
2727
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
28-
import { Badge, Icon, Typography, Alert, Tag, Tooltip } from '@appwrite.io/pink-svelte';
29-
import { isSmallViewport } from '$lib/stores/viewport';
28+
import { Badge, Icon, Typography, Alert } from '@appwrite.io/pink-svelte';
29+
3030
import {
3131
IconAndroid,
3232
IconApple,
@@ -41,6 +41,7 @@
4141
import { currentPlan, regions as regionsStore } from '$lib/stores/organization';
4242
import SelectProjectCloud from '$lib/components/billing/alerts/selectProjectCloud.svelte';
4343
import { toLocaleDate } from '$lib/helpers/date';
44+
import { ArchiveProject } from '$lib/components';
4445
4546
export let data;
4647
@@ -129,15 +130,8 @@
129130
return !data.organization.projects?.includes(project.$id);
130131
}
131132
132-
function formatName(name: string, limit: number = 19) {
133-
const mobileLimit = 16;
134-
const actualLimit = $isSmallViewport ? mobileLimit : limit;
135-
return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-';
136-
}
137-
138-
$: projectsToArchive = data.projects.projects.filter(
139-
(project) => !data.organization.projects?.includes(project.$id)
140-
);
133+
$: activeProjects = data.projects.projects.filter((project) => !isSetToArchive(project));
134+
$: archivedProjects = data.projects.projects.filter((project) => isSetToArchive(project));
141135
</script>
142136

143137
<SelectProjectCloud selectedProjects={data.organization.projects || []} bind:showSelectProject />
@@ -167,14 +161,14 @@
167161
</DropList>
168162
</div>
169163

170-
{#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && data.projects.total > 2 && $canWriteProjects}
164+
{#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && archivedProjects.length > 0 && $canWriteProjects}
171165
<Alert.Inline
172-
title={`${data.projects.total - data.organization.projects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
166+
title={`${archivedProjects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}>
173167
<Typography.Text>
174-
{#each projectsToArchive as project, index}{@const text = `<b>${project.name}</b>`}
175-
{@html text}{index == projectsToArchive.length - 2
168+
{#each archivedProjects as project, index}{@const text = `<b>${project.name}</b>`}
169+
{@html text}{index == archivedProjects.length - 2
176170
? ', and '
177-
: index < projectsToArchive.length - 1
171+
: index < archivedProjects.length - 1
178172
? ', '
179173
: ''}
180174
{/each}
@@ -199,77 +193,59 @@
199193
</Alert.Inline>
200194
{/if}
201195

202-
{#if data.projects.total}
203-
<CardContainer
204-
disableEmpty={!$canWriteProjects}
205-
total={data.projects.total}
206-
offset={data.offset}
207-
on:click={handleCreateProject}>
208-
{#each data.projects.projects as project}
209-
{@const platforms = filterPlatforms(
210-
project.platforms.map((platform) => getPlatformInfo(platform.type))
211-
)}
212-
{@const formatted = isSetToArchive(project)
213-
? formatName(project.name)
214-
: project.name}
215-
<GridItem1
216-
href={`${base}/project-${project.region}-${project.$id}/overview/platforms`}>
217-
<svelte:fragment slot="eyebrow">
218-
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
219-
</svelte:fragment>
220-
<svelte:fragment slot="title">
221-
<Tooltip
222-
maxWidth={project.name.length.toString()}
223-
placement="top"
224-
disabled={!isSetToArchive(project)}>
225-
{formatted}
226-
<span slot="tooltip">
227-
{project.name}
228-
</span>
229-
</Tooltip>
230-
</svelte:fragment>
196+
{#if data.projects.total > 0}
197+
{#if activeProjects.length > 0}
198+
<CardContainer
199+
disableEmpty={!$canWriteProjects}
200+
total={activeProjects.length}
201+
offset={data.offset}
202+
on:click={handleCreateProject}>
203+
{#each activeProjects as project}
204+
{@const platforms = filterPlatforms(
205+
project.platforms.map((platform) => getPlatformInfo(platform.type))
206+
)}
207+
<GridItem1
208+
href={`${base}/project-${project.region}-${project.$id}/overview/platforms`}>
209+
<svelte:fragment slot="eyebrow">
210+
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
211+
</svelte:fragment>
212+
<svelte:fragment slot="title">
213+
{project.name}
214+
</svelte:fragment>
231215

232-
<svelte:fragment slot="status">
233-
{#if isSetToArchive(project)}
234-
<Tag
235-
size="s"
236-
style="white-space: nowrap;"
237-
on:click={(event) => {
238-
event.preventDefault();
239-
showSelectProject = true;
240-
}}>Set to archive</Tag>
241-
{/if}
242-
</svelte:fragment>
216+
{#each platforms.slice(0, 2) as platform}
217+
{@const icon = getIconForPlatform(platform.icon)}
218+
<Badge
219+
variant="secondary"
220+
content={platform.name}
221+
style="width: max-content;">
222+
<Icon {icon} size="s" slot="start" />
223+
</Badge>
224+
{/each}
243225

244-
{#each platforms.slice(0, 2) as platform}
245-
{@const icon = getIconForPlatform(platform.icon)}
246-
<Badge
247-
variant="secondary"
248-
content={platform.name}
249-
style="width: max-content;">
250-
<Icon {icon} size="s" slot="start" />
251-
</Badge>
252-
{/each}
226+
{#if platforms.length > 3}
227+
<Badge
228+
variant="secondary"
229+
content={`+${platforms.length - 2}`}
230+
style="width: max-content;" />
231+
{/if}
253232

254-
{#if platforms.length > 3}
255-
<Badge
256-
variant="secondary"
257-
content={`+${platforms.length - 2}`}
258-
style="width: max-content;" />
259-
{/if}
233+
<svelte:fragment slot="icons">
234+
{#if isCloud && $regionsStore?.regions}
235+
{@const region = findRegion(project)}
236+
<Typography.Text>{region.name}</Typography.Text>
237+
{/if}
238+
</svelte:fragment>
239+
</GridItem1>
240+
{/each}
241+
<svelte:fragment slot="empty">
242+
<p>Create a new project</p>
243+
</svelte:fragment>
244+
</CardContainer>
245+
{/if}
260246

261-
<svelte:fragment slot="icons">
262-
{#if isCloud && $regionsStore?.regions}
263-
{@const region = findRegion(project)}
264-
<Typography.Text>{region.name}</Typography.Text>
265-
{/if}
266-
</svelte:fragment>
267-
</GridItem1>
268-
{/each}
269-
<svelte:fragment slot="empty">
270-
<p>Create a new project</p>
271-
</svelte:fragment>
272-
</CardContainer>
247+
<!-- Archived Projects Section -->
248+
<ArchiveProject projectsToArchive={archivedProjects} />
273249
{:else}
274250
<Empty
275251
single
@@ -283,7 +259,7 @@
283259
name="Projects"
284260
limit={data.limit}
285261
offset={data.offset}
286-
total={data.projects.total} />
262+
total={activeProjects.length} />
287263
</Container>
288264

289265
<CreateOrganization bind:show={addOrganization} />

0 commit comments

Comments
 (0)