Add name field to share feature and require at least one item selection.

- Add name field to ShareCreateForm and ShareEditForm
- Add name column to SharesTable
- Update API routes to handle name field
- Require at least one item to be selected before saving
- Display parameters in responsive 3-column grid in edit form

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mike Cao 2026-01-22 20:17:45 -08:00
parent 4c9b2f10da
commit af7f7adf5b
5 changed files with 105 additions and 53 deletions

View file

@ -7,6 +7,7 @@ import {
FormSubmitButton,
Row,
Text,
TextField,
} from '@umami/react-zen';
import { useState } from 'react';
import { useApi, useMessages, useModified } from '@/components/hooks';
@ -32,6 +33,9 @@ export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormP
});
});
// Get all item ids for validation
const allItemIds = SHARE_NAV_ITEMS.flatMap(section => section.items.map(item => item.id));
const handleSubmit = async (data: any) => {
setIsPending(true);
try {
@ -41,7 +45,7 @@ export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormP
parameters[item.id] = data[item.id] ?? false;
});
});
await post(`/websites/${websiteId}/shares`, { parameters });
await post(`/websites/${websiteId}/shares`, { name: data.name, parameters });
touch('shares');
onSave?.();
onClose?.();
@ -52,7 +56,19 @@ export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormP
return (
<Form onSubmit={handleSubmit} defaultValues={defaultValues}>
{({ watch }) => {
const values = watch();
const hasSelection = allItemIds.some(id => values[id]);
return (
<Column gap="3">
<FormField
label={formatMessage(labels.name)}
name="name"
rules={{ required: formatMessage(labels.required) }}
>
<TextField autoComplete="off" autoFocus />
</FormField>
{SHARE_NAV_ITEMS.map(section => (
<Column key={section.section} gap="1">
<Text size="2" weight="bold">
@ -73,9 +89,13 @@ export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormP
{formatMessage(labels.cancel)}
</Button>
)}
<FormSubmitButton isDisabled={isPending}>{formatMessage(labels.save)}</FormSubmitButton>
<FormSubmitButton isDisabled={isPending || !hasSelection}>
{formatMessage(labels.save)}
</FormSubmitButton>
</Row>
</Column>
);
}}
</Form>
);
}

View file

@ -5,6 +5,7 @@ import {
Form,
FormField,
FormSubmitButton,
Grid,
Label,
Loading,
Row,
@ -62,7 +63,7 @@ export function ShareEditForm({
});
await mutateAsync(
{ slug: share.slug, parameters },
{ name: data.name, slug: share.slug, parameters },
{
onSuccess: async () => {
toast(formatMessage(messages.saved));
@ -81,7 +82,9 @@ export function ShareEditForm({
const url = getUrl(share?.slug || '');
// Build default values from share parameters
const defaultValues: Record<string, boolean> = {};
const defaultValues: Record<string, any> = {
name: share?.name || '',
};
SHARE_NAV_ITEMS.forEach(section => {
section.items.forEach(item => {
const defaultSelected = item.id === 'overview' || item.id === 'events';
@ -89,15 +92,31 @@ export function ShareEditForm({
});
});
// Get all item ids for validation
const allItemIds = SHARE_NAV_ITEMS.flatMap(section => section.items.map(item => item.id));
return (
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}>
<Column gap="3">
{({ watch }) => {
const values = watch();
const hasSelection = allItemIds.some(id => values[id]);
return (
<Column gap="6">
<Column>
<Label>{formatMessage(labels.shareUrl)}</Label>
<TextField value={url} isReadOnly allowCopy />
</Column>
<FormField
label={formatMessage(labels.name)}
name="name"
rules={{ required: formatMessage(labels.required) }}
>
<TextField autoComplete="off" />
</FormField>
<Grid columns="repeat(auto-fit, minmax(150px, 1fr))" gap="3">
{SHARE_NAV_ITEMS.map(section => (
<Column key={section.section} gap="1">
<Column key={section.section} gap="3">
<Text weight="bold">{formatMessage((labels as any)[section.section])}</Text>
<Column gap="1">
{section.items.map(item => (
@ -108,15 +127,20 @@ export function ShareEditForm({
</Column>
</Column>
))}
</Grid>
<Row justifyContent="flex-end" paddingTop="3" gap="3">
{onClose && (
<Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)}
</Button>
)}
<FormSubmitButton variant="primary">{formatMessage(labels.save)}</FormSubmitButton>
<FormSubmitButton variant="primary" isDisabled={!hasSelection || !values.name}>
{formatMessage(labels.save)}
</FormSubmitButton>
</Row>
</Column>
);
}}
</Form>
);
}

View file

@ -18,6 +18,9 @@ export function SharesTable(props: DataTableProps) {
return (
<DataTable {...props}>
<DataColumn id="name" label={formatMessage(labels.name)}>
{({ name }: any) => name}
</DataColumn>
<DataColumn id="slug" label={formatMessage(labels.shareUrl)}>
{({ slug }: any) => {
const url = getUrl(slug);

View file

@ -25,6 +25,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ shar
export async function POST(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
const schema = z.object({
name: z.string().max(200),
slug: z.string().max(100),
parameters: anyObjectParam,
});
@ -36,7 +37,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ sha
}
const { shareId } = await params;
const { slug, parameters } = body;
const { name, slug, parameters } = body;
const share = await getShare(shareId);
@ -49,6 +50,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ sha
}
const result = await updateShare(shareId, {
name,
slug,
parameters,
} as any);

View file

@ -44,6 +44,7 @@ export async function POST(
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
name: z.string().max(200),
parameters: anyObjectParam.optional(),
});
@ -54,7 +55,8 @@ export async function POST(
}
const { websiteId } = await params;
const { parameters = {} } = body;
const { name, parameters } = body;
const shareParameters = parameters ?? {};
if (!(await canUpdateWebsite(auth, websiteId))) {
return unauthorized();
@ -66,8 +68,9 @@ export async function POST(
id: uuid(),
entityId: websiteId,
shareType: ENTITY_TYPE.website,
name,
slug,
parameters,
parameters: shareParameters,
});
return json(share);