Skip to content

Commit f80f3ae

Browse files
Merge pull request #2249 from appwrite/feat-SER-204-New-Archive-projects-ui-change
2 parents a82e961 + 5cedece commit f80f3ae

37 files changed

+2036
-383
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@
2222
},
2323
"dependencies": {
2424
"@ai-sdk/svelte": "^1.1.24",
25-
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428",
25+
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134",
2626
"@appwrite.io/pink-icons": "0.25.0",
27-
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
27+
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0",
2828
"@appwrite.io/pink-legacy": "^1.0.3",
29-
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7",
29+
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0",
3030
"@faker-js/faker": "^9.9.0",
3131
"@popperjs/core": "^2.11.8",
3232
"@sentry/sveltekit": "^8.38.0",

pnpm-lock.yaml

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
<script lang="ts">
2+
import { Button } from '$lib/elements/forms';
3+
import { DropList, GridItem1, CardContainer } from '$lib/components';
4+
import {
5+
Badge,
6+
Icon,
7+
Typography,
8+
Tag,
9+
Accordion,
10+
ActionMenu,
11+
Popover,
12+
Layout
13+
} from '@appwrite.io/pink-svelte';
14+
import {
15+
IconAndroid,
16+
IconApple,
17+
IconCode,
18+
IconFlutter,
19+
IconReact,
20+
IconUnity,
21+
IconInfo,
22+
IconDotsHorizontal,
23+
IconInboxIn,
24+
IconSwitchHorizontal
25+
} from '@appwrite.io/pink-icons-svelte';
26+
import { getPlatformInfo } from '$lib/helpers/platform';
27+
import { Status, type Models } from '@appwrite.io/console';
28+
import type { ComponentType } from 'svelte';
29+
import { BillingPlan } from '$lib/constants';
30+
import { goto } from '$app/navigation';
31+
import { base } from '$app/paths';
32+
import { sdk } from '$lib/stores/sdk';
33+
import { addNotification } from '$lib/stores/notifications';
34+
import { invalidate } from '$app/navigation';
35+
import { Dependencies } from '$lib/constants';
36+
import { Modal } from '$lib/components';
37+
import { isSmallViewport } from '$lib/stores/viewport';
38+
import { isCloud } from '$lib/system';
39+
import { regions as regionsStore } from '$lib/stores/organization';
40+
import type { Organization } from '$lib/stores/organization';
41+
import type { Plan } from '$lib/sdk/billing';
42+
43+
// props
44+
interface Props {
45+
projectsToArchive: Models.Project[];
46+
organization: Organization;
47+
currentPlan: Plan;
48+
}
49+
50+
let { projectsToArchive, organization, currentPlan }: Props = $props();
51+
52+
// Track Read-only info droplist per archived project
53+
let readOnlyInfoOpen = $state<Record<string, boolean>>({});
54+
let showUnarchiveModal = $state(false);
55+
let projectToUnarchive = $state<Models.Project | null>(null);
56+
57+
function filterPlatforms(platforms: { name: string; icon: string }[]) {
58+
return platforms.filter(
59+
(value, index, self) => index === self.findIndex((t) => t.name === value.name)
60+
);
61+
}
62+
63+
function getIconForPlatform(platform: string): ComponentType {
64+
switch (platform) {
65+
case 'code':
66+
return IconCode;
67+
case 'flutter':
68+
return IconFlutter;
69+
case 'apple':
70+
return IconApple;
71+
case 'android':
72+
return IconAndroid;
73+
case 'react-native':
74+
return IconReact;
75+
case 'unity':
76+
return IconUnity;
77+
default:
78+
return IconCode;
79+
}
80+
}
81+
82+
// Check if unarchive should be disabled
83+
function isUnarchiveDisabled(): boolean {
84+
if (!organization || !currentPlan) return true;
85+
86+
if (organization.billingPlan === BillingPlan.FREE) {
87+
const currentProjectCount = organization.projects?.length || 0;
88+
const projectLimit = currentPlan.projects || 0;
89+
90+
return currentProjectCount >= projectLimit;
91+
}
92+
93+
return false;
94+
}
95+
96+
function handleMigrateProject(project: Models.Project) {
97+
goto(`${base}/project-${project.region}-${project.$id}/settings/migrations`);
98+
}
99+
100+
// Handle unarchive project action
101+
async function handleUnarchiveProject(project: Models.Project) {
102+
projectToUnarchive = project;
103+
showUnarchiveModal = true;
104+
}
105+
106+
// Confirm unarchive action
107+
async function confirmUnarchive() {
108+
if (!projectToUnarchive) return;
109+
110+
try {
111+
if (!organization) {
112+
addNotification({
113+
type: 'error',
114+
message: 'Organization not found'
115+
});
116+
return;
117+
}
118+
119+
await sdk.forConsole.projects.updateStatus(projectToUnarchive.$id, Status.Active);
120+
121+
await invalidate(Dependencies.ORGANIZATION);
122+
123+
addNotification({
124+
type: 'success',
125+
message: `${projectToUnarchive.name} has been unarchived`
126+
});
127+
128+
showUnarchiveModal = false;
129+
projectToUnarchive = null;
130+
} catch (error) {
131+
const msg =
132+
error && typeof error === 'object' && 'message' in error
133+
? String((error as { message: string }).message)
134+
: 'Failed to unarchive project';
135+
addNotification({ type: 'error', message: msg });
136+
}
137+
}
138+
139+
function cancelUnarchive() {
140+
showUnarchiveModal = false;
141+
projectToUnarchive = null;
142+
}
143+
144+
function findRegion(project: Models.Project) {
145+
return $regionsStore.regions.find((region) => region.$id === project.region);
146+
}
147+
148+
import { formatName as formatNameHelper } from '$lib/helpers/string';
149+
function formatName(name: string, limit: number = 19) {
150+
return formatNameHelper(name, limit, $isSmallViewport);
151+
}
152+
</script>
153+
154+
{#if projectsToArchive.length > 0}
155+
<div class="archive-projects-margin-top">
156+
<Accordion title="Archived projects" badge={`${projectsToArchive.length}`}>
157+
<Typography.Text tag="p" size="s">
158+
These projects have been archived and are read-only. You can view and migrate their
159+
data.
160+
</Typography.Text>
161+
162+
<div class="archive-projects-margin">
163+
<CardContainer disableEmpty={true} total={projectsToArchive.length}>
164+
{#each projectsToArchive as project}
165+
{@const platforms = filterPlatforms(
166+
project.platforms.map((platform) => getPlatformInfo(platform.type))
167+
)}
168+
{@const formatted = formatName(project.name)}
169+
<GridItem1>
170+
<svelte:fragment slot="eyebrow">
171+
{project?.platforms?.length ? project?.platforms?.length : 'No'} apps
172+
</svelte:fragment>
173+
<svelte:fragment slot="title">{formatted}</svelte:fragment>
174+
<svelte:fragment slot="status">
175+
<div class="status-container">
176+
<DropList
177+
bind:show={readOnlyInfoOpen[project.$id]}
178+
placement="bottom-start"
179+
noArrow>
180+
<Tag
181+
size="s"
182+
style="white-space: nowrap;"
183+
on:click={(e) => {
184+
e.preventDefault();
185+
e.stopPropagation();
186+
readOnlyInfoOpen = {
187+
...readOnlyInfoOpen,
188+
[project.$id]: !readOnlyInfoOpen[project.$id]
189+
};
190+
}}>
191+
<Icon icon={IconInfo} size="s" />
192+
<span>Read only</span>
193+
</Tag>
194+
<svelte:fragment slot="list">
195+
<li
196+
class="drop-list-item u-width-250"
197+
style="padding: var(--space-5, 12px) var(--space-6, 16px)">
198+
<span class="u-block u-mb-8">
199+
Archived projects are read-only. You can view
200+
and migrate their data, but they no longer
201+
accept edits or requests.
202+
</span>
203+
</li>
204+
</svelte:fragment>
205+
</DropList>
206+
<Popover let:toggle padding="none" placement="bottom-end">
207+
<Button
208+
text
209+
icon
210+
size="s"
211+
ariaLabel="more options"
212+
on:click={(e) => {
213+
e.preventDefault();
214+
e.stopPropagation();
215+
toggle(e);
216+
}}>
217+
<Icon icon={IconDotsHorizontal} size="s" />
218+
</Button>
219+
<ActionMenu.Root slot="tooltip">
220+
<ActionMenu.Item.Button
221+
leadingIcon={IconInboxIn}
222+
disabled={isUnarchiveDisabled()}
223+
on:click={() => handleUnarchiveProject(project)}
224+
>Unarchive project</ActionMenu.Item.Button>
225+
<ActionMenu.Item.Button
226+
leadingIcon={IconSwitchHorizontal}
227+
on:click={() => handleMigrateProject(project)}
228+
>Migrate project</ActionMenu.Item.Button>
229+
</ActionMenu.Root>
230+
</Popover>
231+
</div>
232+
</svelte:fragment>
233+
234+
{#each platforms.slice(0, 2) as platform}
235+
{@const icon = getIconForPlatform(platform.icon)}
236+
<Badge
237+
variant="secondary"
238+
content={platform.name}
239+
style="width: max-content;">
240+
<Icon {icon} size="s" slot="start" />
241+
</Badge>
242+
{/each}
243+
244+
{#if platforms.length > 3}
245+
<Badge
246+
variant="secondary"
247+
content={`+${platforms.length - 2}`}
248+
style="width: max-content;" />
249+
{/if}
250+
251+
<svelte:fragment slot="icons">
252+
{#if isCloud && $regionsStore?.regions}
253+
{@const region = findRegion(project)}
254+
<Typography.Text>{region?.name}</Typography.Text>
255+
{/if}
256+
</svelte:fragment>
257+
</GridItem1>
258+
{/each}
259+
</CardContainer>
260+
</div>
261+
</Accordion>
262+
</div>
263+
{/if}
264+
265+
<!-- Unarchive Confirmation Modal -->
266+
<Modal bind:show={showUnarchiveModal} title="Unarchive project" size="s">
267+
<p>Are you sure you want to unarchive <strong>{projectToUnarchive?.name}</strong>?</p>
268+
<p>This will move the project back to your active projects list.</p>
269+
270+
<svelte:fragment slot="footer">
271+
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
272+
<Button secondary on:click={cancelUnarchive}>Cancel</Button>
273+
<Button on:click={confirmUnarchive}>Unarchive</Button>
274+
</Layout.Stack>
275+
</svelte:fragment>
276+
</Modal>
277+
278+
<style>
279+
.archive-projects-margin-top {
280+
margin-top: 36px;
281+
}
282+
283+
.archive-projects-margin {
284+
margin-top: 16px;
285+
margin-bottom: 36px;
286+
}
287+
.status-container {
288+
display: flex;
289+
align-items: center;
290+
gap: 8px;
291+
}
292+
</style>

0 commit comments

Comments
 (0)