Skip to content

Commit cafe736

Browse files
committed
next: Add new project
1 parent 49adff4 commit cafe736

File tree

5 files changed

+136
-19
lines changed

5 files changed

+136
-19
lines changed

src/Exceptionless.Web/ClientApp/src/lib/features/events/components/filters/project-faceted-filter.svelte

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
}
2020
});
2121
22+
const projects = $derived(response.data?.data ?? []);
2223
const options = $derived(
23-
response.data?.map((project) => ({
24+
projects.map((project) => ({
2425
label: project.name!,
2526
value: project.id!
2627
})) ?? []
@@ -31,9 +32,9 @@
3132
return;
3233
}
3334
34-
const projects = response.data.filter((project) => filter.value.includes(project.id!));
35-
if (filter.value.length !== projects.length) {
36-
filter.value = projects.map((project) => project.id!);
35+
const filteredProjects = projects.filter((project) => filter.value.includes(project.id!)) ?? [];
36+
if (filter.value.length !== filteredProjects.length) {
37+
filter.value = filteredProjects.map((project) => project.id!);
3738
filterChanged(filter);
3839
}
3940
});

src/Exceptionless.Web/ClientApp/src/lib/features/projects/api.svelte.ts

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { accessToken } from '$features/auth/index.svelte';
44
import { type FetchClientResponse, type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient';
55
import { createMutation, createQuery, QueryClient, useQueryClient } from '@tanstack/svelte-query';
66

7-
import type { ViewProject } from './models';
7+
import type { NewProject, ViewProject } from './models';
88

99
export async function invalidateProjectQueries(queryClient: QueryClient, message: WebSocketMessageValue<'ProjectChanged'>) {
1010
const { id, organization_id } = message;
@@ -38,20 +38,26 @@ export interface DeleteProjectRequest {
3838
};
3939
}
4040

41-
export interface deletePromotedTabRequest {
41+
export interface DeletePromotedTabParams {
42+
name: string;
43+
}
44+
45+
export interface DeletePromotedTabRequest {
4246
route: {
4347
id: string;
4448
};
4549
}
4650

47-
export interface GetOrganizationProjectsRequest {
48-
params?: {
51+
export interface GetOrganizationProjectsParams {
4952
filter?: string;
5053
limit?: number;
51-
mode?: 'stats';
54+
mode?: GetProjectsMode;
5255
page?: number;
5356
sort?: string;
54-
};
57+
}
58+
59+
export interface GetOrganizationProjectsRequest {
60+
params?: GetOrganizationProjectsParams;
5561
route: {
5662
organizationId: string | undefined;
5763
};
@@ -63,6 +69,16 @@ export interface GetProjectRequest {
6369
};
6470
}
6571

72+
export type GetProjectsMode = 'stats' | null;
73+
74+
export interface GetProjectsParams {
75+
filter?: string;
76+
}
77+
78+
export interface PostPromotedTabParams {
79+
name: string;
80+
}
81+
6682
export interface PostPromotedTabRequest {
6783
route: {
6884
id: string | undefined;
@@ -92,13 +108,13 @@ export function deleteProject(request: DeleteProjectRequest) {
92108
}));
93109
}
94110

95-
export function deletePromotedTab(request: deletePromotedTabRequest) {
111+
export function deletePromotedTab(request: DeletePromotedTabRequest) {
96112
const queryClient = useQueryClient();
97-
return createMutation<boolean, ProblemDetails, { name: string }>(() => ({
98-
mutationFn: async (params: { name: string }) => {
113+
return createMutation<boolean, ProblemDetails, DeletePromotedTabParams>(() => ({
114+
mutationFn: async (params: DeletePromotedTabParams) => {
99115
const client = useFetchClient();
100116
const response = await client.delete(`projects/${request.route.id}/promotedtabs`, {
101-
params
117+
params: { ...params }
102118
});
103119

104120
return response.ok;
@@ -161,14 +177,29 @@ export function getProjectQuery(request: GetProjectRequest) {
161177
queryKey: queryKeys.id(request.route.id)
162178
}));
163179
}
180+
export function postProject() {
181+
const queryClient = useQueryClient();
182+
183+
return createMutation<ViewProject, ProblemDetails, NewProject>(() => ({
184+
enabled: () => !!accessToken.current,
185+
mutationFn: async (project: NewProject) => {
186+
const client = useFetchClient();
187+
const response = await client.postJSON<ViewProject>('projects', project);
188+
return response.data!;
189+
},
190+
onSuccess: () => {
191+
queryClient.invalidateQueries({ queryKey: queryKeys.type });
192+
}
193+
}));
194+
}
164195

165196
export function postPromotedTab(request: PostPromotedTabRequest) {
166197
const queryClient = useQueryClient();
167-
return createMutation<boolean, ProblemDetails, { name: string }>(() => ({
168-
mutationFn: async (params: { name: string }) => {
198+
return createMutation<boolean, ProblemDetails, PostPromotedTabParams>(() => ({
199+
mutationFn: async (params: PostPromotedTabParams) => {
169200
const client = useFetchClient();
170201
const response = await client.post(`projects/${request.route.id}/promotedtabs`, undefined, {
171-
params
202+
params: { ...params }
172203
});
173204

174205
return response.ok;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { ViewProject } from '$generated/api';
1+
export { NewProject, ViewProject } from '$generated/api';
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<script lang="ts">
2+
import { goto } from '$app/navigation';
3+
import ErrorMessage from '$comp/error-message.svelte';
4+
import Loading from '$comp/loading.svelte';
5+
import * as Card from '$comp/ui/card';
6+
import * as Form from '$comp/ui/form';
7+
import { Input } from '$comp/ui/input';
8+
import { organization } from '$features/organizations/context.svelte';
9+
import { postProject } from '$features/projects/api.svelte';
10+
import { applyServerSideErrors } from '$features/shared/validation';
11+
import { NewProject } from '$generated/api';
12+
import { ProblemDetails } from '@exceptionless/fetchclient';
13+
import { toast } from 'svelte-sonner';
14+
import { defaults, superForm } from 'sveltekit-superforms';
15+
import { classvalidatorClient } from 'sveltekit-superforms/adapters';
16+
17+
let toastId = $state<number | string>();
18+
const createProject = postProject();
19+
20+
const project = new NewProject();
21+
project.organization_id = organization.current!;
22+
project.delete_bot_data_enabled = true;
23+
24+
const form = superForm(defaults(project, classvalidatorClient(NewProject)), {
25+
dataType: 'json',
26+
id: 'post-project',
27+
async onUpdate({ form, result }) {
28+
if (!form.valid) {
29+
return;
30+
}
31+
32+
toast.dismiss(toastId);
33+
try {
34+
await createProject.mutateAsync(form.data);
35+
toastId = toast.success('Project created successfully');
36+
await goto('/next/project/configure');
37+
38+
// HACK: This is to prevent sveltekit from stealing focus
39+
result.type = 'failure';
40+
} catch (error: unknown) {
41+
if (error instanceof ProblemDetails) {
42+
applyServerSideErrors(form, error);
43+
result.status = error.status ?? 500;
44+
toastId = toast.error(form.message ?? 'Error creating project. Please try again.');
45+
}
46+
}
47+
},
48+
SPA: true,
49+
validators: classvalidatorClient(NewProject)
50+
});
51+
52+
const { enhance, form: formData, message, submitting } = form;
53+
</script>
54+
55+
<div class="p-6">
56+
<Card.Root>
57+
<Card.Header>
58+
<Card.Title class="text-2xl" level={2}>Add Project</Card.Title>
59+
<Card.Description>Add a new project to start tracking errors and events.</Card.Description>
60+
</Card.Header>
61+
62+
<Card.Content>
63+
<form method="POST" use:enhance>
64+
<ErrorMessage message={$message} />
65+
<Form.Field {form} name="name">
66+
<Form.Control>
67+
{#snippet children({ props })}
68+
<Form.Label>Project Name</Form.Label>
69+
<Input {...props} bind:value={$formData.name} placeholder="Enter project name" />
70+
{/snippet}
71+
</Form.Control>
72+
<Form.FieldErrors />
73+
</Form.Field>
74+
75+
<Form.Button>
76+
{#if $submitting}
77+
<Loading class="mr-2" variant="secondary"></Loading> Adding Project...
78+
{:else}
79+
Add Project
80+
{/if}</Form.Button
81+
>
82+
</form>
83+
</Card.Content>
84+
</Card.Root>
85+
</div>

src/Exceptionless.Web/ClientApp/src/routes/(app)/project/list/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
<Card.Header>
6767
<Card.Title class="text-2xl" level={2}>My Projects</Card.Title>
6868
<Card.Description>
69-
<Button href="/next/project/new">
69+
<Button href="/next/project/add">
7070
<Plus class="mr-2 size-4" />
7171
Add New Project
7272
</Button>

0 commit comments

Comments
 (0)