mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
- Add custom title field for user-defined link names - Implement custom OG image URL input for social media previews - Enable Open Graph metadata customization (description, type, etc.) - Add link slug/path customization for personalized URLs - Update link creation form with new metadata fields - Extend database schema to store custom link properties - Add validation for OG image URLs and metadata inputs This allows users to fully customize their shared analytics links with custom titles, Open Graph images, and other metadata for better social media presentation and link management.
150 lines
4 KiB
TypeScript
150 lines
4 KiB
TypeScript
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 label={formatMessage(labels.title)} name="title">
|
|
<TextField autoComplete="off" />
|
|
</FormField>
|
|
|
|
<FormField label={formatMessage(labels.description)} name="description">
|
|
<TextField autoComplete="off" />
|
|
</FormField>
|
|
|
|
<FormField label="OG Image URL" name="image">
|
|
<TextField autoComplete="off" />
|
|
</FormField>
|
|
|
|
<Column>
|
|
<Label>{formatMessage(labels.link)}</Label>
|
|
<Row alignItems="center" gap>
|
|
<FormField
|
|
name="slug"
|
|
rules={{ required: formatMessage(labels.required) }}
|
|
style={{ width: '100%' }}
|
|
>
|
|
<TextField autoComplete="off" />
|
|
</FormField>
|
|
<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>
|
|
);
|
|
}
|