refactor(link): simplify LinkEditForm state management and improve slug handling

- Removed unnecessary useEffect for slug initialization and replaced it with a more efficient state setup.
- Updated handleSubmit to conditionally include the slug in the payload based on link creation or modification.
- Enhanced form handling by utilizing watch for dynamic slug updates and improved validation messages for slug input.
This commit is contained in:
crbon 2026-01-22 17:27:46 +10:00
parent fc78c4a5ff
commit c3332552d7
2 changed files with 43 additions and 37 deletions

View file

@ -10,7 +10,7 @@ import {
Row, Row,
TextField, TextField,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { useConfig, useLinkQuery, useMessages } from '@/components/hooks'; import { useConfig, useLinkQuery, useMessages } from '@/components/hooks';
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery'; import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
import { ChevronDown, ChevronRight, RefreshCw } from '@/components/icons'; import { ChevronDown, ChevronRight, RefreshCw } from '@/components/icons';
@ -42,11 +42,15 @@ export function LinkEditForm({
const { linksUrl } = useConfig(); const { linksUrl } = useConfig();
const hostUrl = linksUrl || LINKS_URL; const hostUrl = linksUrl || LINKS_URL;
const { data, isLoading } = useLinkQuery(linkId); const { data, isLoading } = useLinkQuery(linkId);
const [slug, setSlug] = useState(generateId()); const [initialSlug] = useState(() => generateId());
const [showAdvanced, setShowAdvanced] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false);
const handleSubmit = async (data: any) => { const handleSubmit = async (formData: any) => {
await mutateAsync(data, { const { slug: formSlug, ...rest } = formData;
// Only include slug if creating new link or if it was modified
const payload = !linkId || formSlug !== data?.slug ? formData : rest;
await mutateAsync(payload, {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved)); toast(formatMessage(messages.saved));
touch('links'); touch('links');
@ -56,13 +60,7 @@ export function LinkEditForm({
}); });
}; };
const handleSlug = () => { const handleSlug = () => generateId();
const slug = generateId();
setSlug(slug);
return slug;
};
const checkUrl = (url: string) => { const checkUrl = (url: string) => {
if (!isValidUrl(url)) { if (!isValidUrl(url)) {
@ -71,12 +69,6 @@ export function LinkEditForm({
return true; return true;
}; };
useEffect(() => {
if (data) {
setSlug(data.slug);
}
}, [data]);
if (linkId && isLoading) { if (linkId && isLoading) {
return <Loading placement="absolute" />; return <Loading placement="absolute" />;
} }
@ -85,15 +77,27 @@ export function LinkEditForm({
<Form <Form
onSubmit={handleSubmit} onSubmit={handleSubmit}
error={getErrorMessage(error)} error={getErrorMessage(error)}
defaultValues={{ {...(linkId
slug, ? {
...data, values: {
ogTitle: data?.ogTitle || '', ...data,
ogDescription: data?.ogDescription || '', ogTitle: data?.ogTitle || '',
ogImageUrl: data?.ogImageUrl || '', ogDescription: data?.ogDescription || '',
}} ogImageUrl: data?.ogImageUrl || '',
},
}
: {
defaultValues: {
slug: initialSlug,
ogTitle: '',
ogDescription: '',
ogImageUrl: '',
},
})}
> >
{({ setValue }) => { {({ setValue, watch }) => {
const currentSlug = watch('slug') ?? initialSlug;
return ( return (
<> <>
<FormField <FormField
@ -116,7 +120,7 @@ export function LinkEditForm({
<Label>{formatMessage(labels.link)}</Label> <Label>{formatMessage(labels.link)}</Label>
<Row alignItems="center" gap> <Row alignItems="center" gap>
<TextField <TextField
value={`${hostUrl}/${slug}`} value={`${hostUrl}/${currentSlug}`}
autoComplete="off" autoComplete="off"
isReadOnly isReadOnly
allowCopy allowCopy
@ -157,17 +161,17 @@ export function LinkEditForm({
<TextField autoComplete="off" /> <TextField autoComplete="off" />
</FormField> </FormField>
<Column> <FormField
<Label>{formatMessage(labels.path)}</Label> label={formatMessage(labels.path)}
<TextField name="slug"
value={slug} rules={{
onChange={(value: string) => { required: formatMessage(labels.required),
setSlug(value); minLength: { value: 4, message: formatMessage(labels.tooShort) },
setValue('slug', value, { shouldDirty: true }); maxLength: { value: 100, message: formatMessage(labels.tooLong) },
}} }}
autoComplete="off" >
/> <TextField autoComplete="off" minLength={4} maxLength={100} />
</Column> </FormField>
</Column> </Column>
)} )}

View file

@ -4,6 +4,8 @@ export const labels = defineMessages({
ok: { id: 'label.ok', defaultMessage: 'OK' }, ok: { id: 'label.ok', defaultMessage: 'OK' },
unknown: { id: 'label.unknown', defaultMessage: 'Unknown' }, unknown: { id: 'label.unknown', defaultMessage: 'Unknown' },
required: { id: 'label.required', defaultMessage: 'Required' }, required: { id: 'label.required', defaultMessage: 'Required' },
tooShort: { id: 'label.too-short', defaultMessage: 'Too short' },
tooLong: { id: 'label.too-long', defaultMessage: 'Too long' },
save: { id: 'label.save', defaultMessage: 'Save' }, save: { id: 'label.save', defaultMessage: 'Save' },
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' }, cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
continue: { id: 'label.continue', defaultMessage: 'Continue' }, continue: { id: 'label.continue', defaultMessage: 'Continue' },