Skip to content

Commit a936832

Browse files
committed
refactor(resources/settings): migrate settings/webhooks fetchers
1 parent 4b7c677 commit a936832

11 files changed

Lines changed: 598 additions & 492 deletions

File tree

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { ExternalLink } from '@app-builder/components/ExternalLink';
2+
import { FormErrorOrDescription } from '@app-builder/components/Form/Tanstack/FormErrorOrDescription';
3+
import { FormInput } from '@app-builder/components/Form/Tanstack/FormInput';
4+
import { FormLabel } from '@app-builder/components/Form/Tanstack/FormLabel';
5+
import { Nudge } from '@app-builder/components/Nudge';
6+
import { LoadingIcon } from '@app-builder/components/Spinner';
7+
import { SelectEvents } from '@app-builder/components/Webhooks/EventTypes';
8+
import { useLoaderRevalidator } from '@app-builder/contexts/LoaderRevalidatorContext';
9+
import { type EventType } from '@app-builder/models/webhook';
10+
import {
11+
CreateWebhookPayload,
12+
createWebhookPayloadSchema,
13+
useCreateWebhookMutation,
14+
} from '@app-builder/queries/settings/webhooks/create-webhook';
15+
import { webhooksEventsDocHref } from '@app-builder/services/documentation-href';
16+
import { getFieldErrors } from '@app-builder/utils/form';
17+
import { useForm } from '@tanstack/react-form';
18+
import { type FeatureAccessLevelDto } from 'marble-api/generated/feature-access-api';
19+
import * as React from 'react';
20+
import { Trans, useTranslation } from 'react-i18next';
21+
import { match } from 'ts-pattern';
22+
import { Button, ModalV2 } from 'ui-design-system';
23+
24+
export function CreateWebhook({
25+
children,
26+
webhookStatus,
27+
}: {
28+
children: React.ReactElement;
29+
webhookStatus: FeatureAccessLevelDto;
30+
}) {
31+
const [open, setOpen] = React.useState(false);
32+
33+
return (
34+
<ModalV2.Root open={open} setOpen={setOpen}>
35+
<ModalV2.Trigger render={children} />
36+
<ModalV2.Content>
37+
<CreateWebhookContent webhookStatus={webhookStatus} onSuccess={() => setOpen(false)} />
38+
</ModalV2.Content>
39+
</ModalV2.Root>
40+
);
41+
}
42+
43+
function CreateWebhookContent({
44+
webhookStatus,
45+
onSuccess,
46+
}: {
47+
webhookStatus: FeatureAccessLevelDto;
48+
onSuccess: () => void;
49+
}) {
50+
const { t } = useTranslation(['common', 'settings']);
51+
const createWebhookMutation = useCreateWebhookMutation();
52+
const revalidate = useLoaderRevalidator();
53+
54+
const form = useForm({
55+
defaultValues: {
56+
url: '',
57+
eventTypes: [],
58+
} as CreateWebhookPayload,
59+
onSubmit: ({ value, formApi }) => {
60+
if (formApi.state.isValid) {
61+
createWebhookMutation.mutateAsync(value).then((res) => {
62+
if (!res) {
63+
onSuccess();
64+
}
65+
revalidate();
66+
});
67+
}
68+
},
69+
validators: {
70+
onSubmit: createWebhookPayloadSchema,
71+
},
72+
});
73+
74+
return (
75+
<form
76+
onSubmit={(e) => {
77+
e.preventDefault();
78+
e.stopPropagation();
79+
form.handleSubmit();
80+
}}
81+
>
82+
<ModalV2.Title>{t('settings:webhooks.new_webhook')}</ModalV2.Title>
83+
<div className="flex flex-col gap-6 p-6">
84+
<form.Field
85+
name="url"
86+
validators={{
87+
onChange: createWebhookPayloadSchema.shape.url,
88+
}}
89+
>
90+
{(field) => (
91+
<div className="flex flex-col items-start gap-2">
92+
<FormLabel name={field.name}>{t('settings:webhooks.url')}</FormLabel>
93+
<FormInput
94+
type="url"
95+
name={field.name}
96+
onBlur={field.handleBlur}
97+
onChange={(e) => field.handleChange(e.currentTarget.value)}
98+
defaultValue={field.state.value}
99+
valid={field.state.meta.errors.length === 0}
100+
className="w-full"
101+
/>
102+
<FormErrorOrDescription errors={getFieldErrors(field.state.meta.errors)} />
103+
</div>
104+
)}
105+
</form.Field>
106+
107+
<form.Field
108+
name="eventTypes"
109+
validators={{
110+
onChange: createWebhookPayloadSchema.shape.eventTypes,
111+
}}
112+
>
113+
{(field) => (
114+
<div className="flex flex-col items-start gap-2">
115+
<FormLabel name={field.name} className="flex items-center gap-2">
116+
{t('settings:webhooks.event_types')}
117+
{match(webhookStatus)
118+
.with('allowed', () => null)
119+
.otherwise((status) => (
120+
<Nudge
121+
kind={status}
122+
content={t('settings:webhooks.nudge')}
123+
className="size-6"
124+
/>
125+
))}
126+
</FormLabel>
127+
<SelectEvents
128+
selectedEventTypes={field.state.value}
129+
className="w-full"
130+
name={field.name}
131+
onBlur={field.handleBlur}
132+
onChange={(types) => field.handleChange(types as EventType[])}
133+
webhookStatus={webhookStatus}
134+
/>
135+
<FormErrorOrDescription
136+
errors={getFieldErrors(field.state.meta.errors)}
137+
description={
138+
<span className="whitespace-pre-wrap">
139+
<Trans
140+
t={t}
141+
i18nKey="settings:webhooks.events_documentation"
142+
components={{
143+
DocLink: <ExternalLink href={webhooksEventsDocHref} />,
144+
}}
145+
/>
146+
</span>
147+
}
148+
/>
149+
</div>
150+
)}
151+
</form.Field>
152+
153+
<form.Field
154+
name="httpTimeout"
155+
validators={{
156+
onChange: createWebhookPayloadSchema.shape.httpTimeout,
157+
}}
158+
>
159+
{(field) => (
160+
<div className="flex flex-col items-start gap-2">
161+
<FormLabel name={field.name}>{t('settings:webhooks.http_timeout')}</FormLabel>
162+
<FormInput
163+
type="number"
164+
name={field.name}
165+
onBlur={field.handleBlur}
166+
onChange={(e) => field.handleChange(+e.currentTarget.value)}
167+
defaultValue={field.state.value}
168+
valid={field.state.meta.errors.length === 0}
169+
className="w-full"
170+
/>
171+
<FormErrorOrDescription
172+
errors={getFieldErrors(field.state.meta.errors)}
173+
description={t('settings:webhooks.http_timeout.description')}
174+
/>
175+
</div>
176+
)}
177+
</form.Field>
178+
179+
<div className="flex flex-1 flex-row gap-2">
180+
<ModalV2.Close render={<Button className="flex-1" variant="secondary" />}>
181+
{t('common:cancel')}
182+
</ModalV2.Close>
183+
<Button
184+
className="flex-1"
185+
variant="primary"
186+
type="submit"
187+
name="create"
188+
disabled={createWebhookMutation.isPending}
189+
>
190+
<LoadingIcon icon="plus" className="size-5" loading={createWebhookMutation.isPending} />
191+
{t('settings:webhooks.new_webhook.create')}
192+
</Button>
193+
</div>
194+
</div>
195+
</form>
196+
);
197+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { LoadingIcon } from '@app-builder/components/Spinner';
2+
import { useLoaderRevalidator } from '@app-builder/contexts/LoaderRevalidatorContext';
3+
import { useDeleteWebhookMutation } from '@app-builder/queries/settings/webhooks/delete-webhook';
4+
import * as React from 'react';
5+
import { useTranslation } from 'react-i18next';
6+
import { Button, ModalV2 } from 'ui-design-system';
7+
8+
export function DeleteWebhook({
9+
webhookId,
10+
children,
11+
}: {
12+
webhookId: string;
13+
children: React.ReactElement;
14+
}) {
15+
const [open, setOpen] = React.useState(false);
16+
17+
return (
18+
<ModalV2.Root open={open} setOpen={setOpen}>
19+
<ModalV2.Trigger render={children} />
20+
<ModalV2.Content onClick={(e) => e.stopPropagation()}>
21+
<DeleteWebhookContent webhookId={webhookId} onSuccess={() => setOpen(false)} />
22+
</ModalV2.Content>
23+
</ModalV2.Root>
24+
);
25+
}
26+
27+
function DeleteWebhookContent({
28+
webhookId,
29+
onSuccess,
30+
}: {
31+
webhookId: string;
32+
onSuccess: () => void;
33+
}) {
34+
const { t } = useTranslation(['common', 'settings']);
35+
const deleteWebhookMutation = useDeleteWebhookMutation();
36+
const revalidate = useLoaderRevalidator();
37+
38+
const handleDeleteWebhook = () => {
39+
deleteWebhookMutation.mutateAsync({ webhookId }).then((res) => {
40+
if (!res) {
41+
onSuccess();
42+
}
43+
revalidate();
44+
});
45+
};
46+
47+
return (
48+
<>
49+
<ModalV2.Title>{t('settings:webhooks.delete_webhook.title')}</ModalV2.Title>
50+
<div className="flex flex-col gap-6 p-6">
51+
<div className="text-s flex flex-1 flex-col gap-4">
52+
<input name="webhookId" value={webhookId} type="hidden" />
53+
<p className="text-center">{t('settings:webhooks.delete_webhook.content')}</p>
54+
</div>
55+
56+
<div className="flex flex-1 flex-row gap-2">
57+
<ModalV2.Close render={<Button className="flex-1" variant="secondary" />}>
58+
{t('common:cancel')}
59+
</ModalV2.Close>
60+
<Button
61+
className="flex-1"
62+
variant="primary"
63+
color="red"
64+
type="submit"
65+
name="create"
66+
onClick={handleDeleteWebhook}
67+
disabled={deleteWebhookMutation.isPending}
68+
>
69+
<LoadingIcon
70+
icon="delete"
71+
className="size-5"
72+
loading={deleteWebhookMutation.isPending}
73+
/>
74+
{t('common:delete')}
75+
</Button>
76+
</div>
77+
</div>
78+
</>
79+
);
80+
}

0 commit comments

Comments
 (0)