Project labels UI

This commit is contained in:
Matej Bačo
2026-01-06 15:05:30 +01:00
parent b721ab3109
commit 6003039f1d
6 changed files with 131 additions and 20 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@f21fc7f",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@5372e56",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc",
"@appwrite.io/pink-legacy": "^1.0.3",
+6 -6
View File
@@ -12,8 +12,8 @@ importers:
specifier: ^1.1.24
version: 1.1.24(svelte@5.25.3)(zod@3.24.3)
'@appwrite.io/console':
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/console@f21fc7f
version: https://pkg.vc/-/@appwrite/@appwrite.io/console@f21fc7f
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/console@5372e56
version: https://pkg.vc/-/@appwrite/@appwrite.io/console@5372e56
'@appwrite.io/pink-icons':
specifier: 0.25.0
version: 0.25.0
@@ -272,9 +272,9 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@f21fc7f':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/console@f21fc7f}
version: 1.10.0
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@5372e56':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/console@5372e56}
version: 2.1.0
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
resolution: {integrity: sha512-iLFlV55hj8mGuAbmxJGenxN5RaZMmVT4GJb9dv/MP1xBAtYibFq7JvBcxm18qV2KU8c31Rntf+Ub4GL7HwqTYg==}
@@ -3823,7 +3823,7 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@f21fc7f': {}
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@5372e56': {}
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
dependencies:
+1
View File
@@ -247,6 +247,7 @@ export enum Submit {
ProjectDelete = 'submit_project_delete',
ProjectUpdateName = 'submit_project_update_name',
ProjectUpdateTeam = 'submit_project_update_team',
ProjectUpdateLabels = 'submit_project_update_labels',
ProjectService = 'submit_project_service',
ProjectUpdateSMTP = 'submit_project_update_smtp',
MemberCreate = 'submit_member_create',
@@ -23,6 +23,11 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
const archivedPage =
Number.isFinite(archivedPageRaw) && archivedPageRaw > 0 ? archivedPageRaw : 1;
const archivedOffset = pageToOffset(archivedPage, limit);
const searchQueries = search
? [Query.or([Query.search('search', search), Query.contains('labels', search)])]
: [];
const [activeProjects, archivedProjects, activeTotal, archivedTotal] = await Promise.all([
sdk.forConsole.projects.list({
queries: [
@@ -30,9 +35,9 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
Query.equal('teamId', params.organization),
Query.or([Query.equal('status', 'active'), Query.isNull('status')]),
Query.limit(limit),
Query.orderDesc('')
],
search: search || undefined
Query.orderDesc(''),
...searchQueries
]
}),
sdk.forConsole.projects.list({
queries: [
@@ -40,23 +45,23 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
Query.equal('teamId', params.organization),
Query.equal('status', 'archived'),
Query.limit(limit),
Query.orderDesc('')
],
search: search || undefined
Query.orderDesc(''),
...searchQueries
]
}),
sdk.forConsole.projects.list({
queries: [
Query.equal('teamId', params.organization),
Query.or([Query.equal('status', 'active'), Query.isNull('status')])
],
search: search || undefined
Query.or([Query.equal('status', 'active'), Query.isNull('status')]),
...searchQueries
]
}),
sdk.forConsole.projects.list({
queries: [
Query.equal('teamId', params.organization),
Query.equal('status', 'archived')
],
search: search || undefined
Query.equal('status', 'archived'),
...searchQueries
]
})
]);
@@ -15,6 +15,7 @@
import ChangeOrganization from './changeOrganization.svelte';
import UpdateVariables from '../updateVariables.svelte';
import { page } from '$app/state';
import UpdateLabels from './updateLabels.svelte';
export let data;
@@ -84,8 +85,9 @@
<Container>
{#if $project}
<UpdateName />
{#if $canWriteProjects}
<UpdateName />
<UpdateLabels />
<UpdateServices />
<UpdateInstallations {...data.installations} limit={data.limit} offset={data.offset} />
<UpdateVariables
@@ -0,0 +1,103 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputTags } from '$lib/elements/forms';
import { symmetricDifference } from '$lib/helpers/array';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { project } from '../store';
import { Tag, Input, Layout, Icon } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
const alphaNumericRegExp = /^[a-zA-Z0-9]+$/;
let suggestedLabels = ['live', 'stage', 'internal'];
let labels: string[] = [];
let error = '';
onMount(async () => {
labels = [...$project.labels];
});
async function updateLabels() {
try {
await sdk.forConsole.projects.updateLabels({ projectId: $project.$id, labels });
await invalidate(Dependencies.PROJECT);
await invalidate(Dependencies.ORGANIZATION);
isDisabled = true;
addNotification({
type: 'success',
message: 'Project labels have been updated'
});
trackEvent(Submit.ProjectUpdateLabels);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.ProjectUpdateLabels);
}
}
$: isDisabled = !!error || !symmetricDifference(labels, $project.labels).length;
$: if (labels) {
const invalidLabels = [];
labels.forEach((label) => {
if (!alphaNumericRegExp.test(label)) {
invalidLabels.push(label);
}
});
if (invalidLabels.length) {
error = `Invalid labels: ${invalidLabels.join(', ')}`;
} else {
error = '';
}
}
</script>
<Form onSubmit={updateLabels}>
<CardGrid>
<svelte:fragment slot="title">Labels</svelte:fragment>
Categorize and manage your projects for easy searching based on specific criteria by assigning
them customizable labels.
<svelte:fragment slot="aside">
<Layout.Stack gap="s">
{#key labels.length}
<InputTags
id="project-labels"
label="Labels"
placeholder="Select or type project labels"
bind:tags={labels} />
{/key}
<Layout.Stack direction="row">
{#each suggestedLabels as suggestedLabel}
<Tag
size="s"
selected={labels.includes(suggestedLabel)}
on:click={() => {
if (!labels.includes(suggestedLabel)) {
labels = [...labels, suggestedLabel];
} else {
labels = labels.filter((e) => e !== suggestedLabel);
}
}}>
<Icon icon={IconPlus} size="s" slot="start" />
{suggestedLabel}
</Tag>
{/each}
</Layout.Stack>
<Input.Helper state={error ? 'warning' : 'default'}
>{error ? error : 'Only alphanumeric characters are allowed'}</Input.Helper>
</Layout.Stack>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={isDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>