|
| 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 | +} |
0 commit comments