diff options
| author | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
| commit | 396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b (patch) | |
| tree | b9df4ca6a70db45cfffbae6fdd7252e20fb8e93c /src/app/(main)/links/LinkEditForm.tsx | |
| download | umami-main.tar.xz umami-main.zip | |
Created from https://vercel.com/new
Diffstat (limited to 'src/app/(main)/links/LinkEditForm.tsx')
| -rw-r--r-- | src/app/(main)/links/LinkEditForm.tsx | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/src/app/(main)/links/LinkEditForm.tsx b/src/app/(main)/links/LinkEditForm.tsx new file mode 100644 index 0000000..6c10c7f --- /dev/null +++ b/src/app/(main)/links/LinkEditForm.tsx @@ -0,0 +1,148 @@ +import { + Button, + Column, + Form, + FormField, + FormSubmitButton, + Icon, + Label, + Loading, + Row, + TextField, +} from '@umami/react-zen'; +import { useEffect, useState } from 'react'; +import { useConfig, useLinkQuery, useMessages } from '@/components/hooks'; +import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery'; +import { RefreshCw } from '@/components/icons'; +import { LINKS_URL } from '@/lib/constants'; +import { getRandomChars } from '@/lib/generate'; +import { isValidUrl } from '@/lib/url'; + +const generateId = () => getRandomChars(9); + +export function LinkEditForm({ + linkId, + teamId, + onSave, + onClose, +}: { + linkId?: string; + teamId?: string; + onSave?: () => void; + onClose?: () => void; +}) { + const { formatMessage, labels, messages, getErrorMessage } = useMessages(); + const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery( + linkId ? `/links/${linkId}` : '/links', + { + id: linkId, + teamId, + }, + ); + const { linksUrl } = useConfig(); + const hostUrl = linksUrl || LINKS_URL; + const { data, isLoading } = useLinkQuery(linkId); + const [slug, setSlug] = useState(generateId()); + + const handleSubmit = async (data: any) => { + await mutateAsync(data, { + onSuccess: async () => { + toast(formatMessage(messages.saved)); + touch('links'); + onSave?.(); + onClose?.(); + }, + }); + }; + + const handleSlug = () => { + const slug = generateId(); + + setSlug(slug); + + return slug; + }; + + const checkUrl = (url: string) => { + if (!isValidUrl(url)) { + return formatMessage(labels.invalidUrl); + } + return true; + }; + + useEffect(() => { + if (data) { + setSlug(data.slug); + } + }, [data]); + + if (linkId && isLoading) { + return <Loading placement="absolute" />; + } + + return ( + <Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={{ slug, ...data }}> + {({ setValue }) => { + return ( + <> + <FormField + label={formatMessage(labels.name)} + name="name" + rules={{ required: formatMessage(labels.required) }} + > + <TextField autoComplete="off" autoFocus /> + </FormField> + + <FormField + label={formatMessage(labels.destinationUrl)} + name="url" + rules={{ required: formatMessage(labels.required), validate: checkUrl }} + > + <TextField placeholder="https://example.com" autoComplete="off" /> + </FormField> + + <FormField + name="slug" + rules={{ + required: formatMessage(labels.required), + }} + style={{ display: 'none' }} + > + <input type="hidden" /> + </FormField> + + <Column> + <Label>{formatMessage(labels.link)}</Label> + <Row alignItems="center" gap> + <TextField + value={`${hostUrl}/${slug}`} + autoComplete="off" + isReadOnly + allowCopy + style={{ width: '100%' }} + /> + <Button + variant="quiet" + onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })} + > + <Icon> + <RefreshCw /> + </Icon> + </Button> + </Row> + </Column> + + <Row justifyContent="flex-end" paddingTop="3" gap="3"> + {onClose && ( + <Button isDisabled={isPending} onPress={onClose}> + {formatMessage(labels.cancel)} + </Button> + )} + <FormSubmitButton>{formatMessage(labels.save)}</FormSubmitButton> + </Row> + </> + ); + }} + </Form> + ); +} |