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

View file

@ -5,6 +5,7 @@ import {
Form, Form,
FormField, FormField,
FormSubmitButton, FormSubmitButton,
Grid,
Label, Label,
Loading, Loading,
Row, Row,
@ -62,7 +63,7 @@ export function ShareEditForm({
}); });
await mutateAsync( await mutateAsync(
{ slug: share.slug, parameters }, { name: data.name, slug: share.slug, parameters },
{ {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved)); toast(formatMessage(messages.saved));
@ -81,7 +82,9 @@ export function ShareEditForm({
const url = getUrl(share?.slug || ''); const url = getUrl(share?.slug || '');
// Build default values from share parameters // Build default values from share parameters
const defaultValues: Record<string, boolean> = {}; const defaultValues: Record<string, any> = {
name: share?.name || '',
};
SHARE_NAV_ITEMS.forEach(section => { SHARE_NAV_ITEMS.forEach(section => {
section.items.forEach(item => { section.items.forEach(item => {
const defaultSelected = item.id === 'overview' || item.id === 'events'; 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 ( return (
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}> <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> <Column>
<Label>{formatMessage(labels.shareUrl)}</Label> <Label>{formatMessage(labels.shareUrl)}</Label>
<TextField value={url} isReadOnly allowCopy /> <TextField value={url} isReadOnly allowCopy />
</Column> </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 => ( {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> <Text weight="bold">{formatMessage((labels as any)[section.section])}</Text>
<Column gap="1"> <Column gap="1">
{section.items.map(item => ( {section.items.map(item => (
@ -108,15 +127,20 @@ export function ShareEditForm({
</Column> </Column>
</Column> </Column>
))} ))}
</Grid>
<Row justifyContent="flex-end" paddingTop="3" gap="3"> <Row justifyContent="flex-end" paddingTop="3" gap="3">
{onClose && ( {onClose && (
<Button isDisabled={isPending} onPress={onClose}> <Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)} {formatMessage(labels.cancel)}
</Button> </Button>
)} )}
<FormSubmitButton variant="primary">{formatMessage(labels.save)}</FormSubmitButton> <FormSubmitButton variant="primary" isDisabled={!hasSelection || !values.name}>
{formatMessage(labels.save)}
</FormSubmitButton>
</Row> </Row>
</Column> </Column>
);
}}
</Form> </Form>
); );
} }

View file

@ -18,6 +18,9 @@ export function SharesTable(props: DataTableProps) {
return ( return (
<DataTable {...props}> <DataTable {...props}>
<DataColumn id="name" label={formatMessage(labels.name)}>
{({ name }: any) => name}
</DataColumn>
<DataColumn id="slug" label={formatMessage(labels.shareUrl)}> <DataColumn id="slug" label={formatMessage(labels.shareUrl)}>
{({ slug }: any) => { {({ slug }: any) => {
const url = getUrl(slug); 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 }> }) { export async function POST(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
const schema = z.object({ const schema = z.object({
name: z.string().max(200),
slug: z.string().max(100), slug: z.string().max(100),
parameters: anyObjectParam, parameters: anyObjectParam,
}); });
@ -36,7 +37,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ sha
} }
const { shareId } = await params; const { shareId } = await params;
const { slug, parameters } = body; const { name, slug, parameters } = body;
const share = await getShare(shareId); const share = await getShare(shareId);
@ -49,6 +50,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ sha
} }
const result = await updateShare(shareId, { const result = await updateShare(shareId, {
name,
slug, slug,
parameters, parameters,
} as any); } as any);

View file

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