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,30 +56,46 @@ export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormP
return ( return (
<Form onSubmit={handleSubmit} defaultValues={defaultValues}> <Form onSubmit={handleSubmit} defaultValues={defaultValues}>
<Column gap="3"> {({ watch }) => {
{SHARE_NAV_ITEMS.map(section => ( const values = watch();
<Column key={section.section} gap="1"> const hasSelection = allItemIds.some(id => values[id]);
<Text size="2" weight="bold">
{formatMessage((labels as any)[section.section])} return (
</Text> <Column gap="3">
<Column gap="1"> <FormField
{section.items.map(item => ( label={formatMessage(labels.name)}
<FormField key={item.id} name={item.id}> name="name"
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox> rules={{ required: formatMessage(labels.required) }}
</FormField> >
))} <TextField autoComplete="off" autoFocus />
</Column> </FormField>
{SHARE_NAV_ITEMS.map(section => (
<Column key={section.section} gap="1">
<Text size="2" weight="bold">
{formatMessage((labels as any)[section.section])}
</Text>
<Column gap="1">
{section.items.map(item => (
<FormField key={item.id} name={item.id}>
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
</FormField>
))}
</Column>
</Column>
))}
<Row justifyContent="flex-end" paddingTop="3" gap="3">
{onClose && (
<Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)}
</Button>
)}
<FormSubmitButton isDisabled={isPending || !hasSelection}>
{formatMessage(labels.save)}
</FormSubmitButton>
</Row>
</Column> </Column>
))} );
<Row justifyContent="flex-end" paddingTop="3" gap="3"> }}
{onClose && (
<Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)}
</Button>
)}
<FormSubmitButton isDisabled={isPending}>{formatMessage(labels.save)}</FormSubmitButton>
</Row>
</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,34 +92,55 @@ 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 }) => {
<Column> const values = watch();
<Label>{formatMessage(labels.shareUrl)}</Label> const hasSelection = allItemIds.some(id => values[id]);
<TextField value={url} isReadOnly allowCopy />
</Column> return (
{SHARE_NAV_ITEMS.map(section => ( <Column gap="6">
<Column key={section.section} gap="1"> <Column>
<Text weight="bold">{formatMessage((labels as any)[section.section])}</Text> <Label>{formatMessage(labels.shareUrl)}</Label>
<Column gap="1"> <TextField value={url} isReadOnly allowCopy />
{section.items.map(item => (
<FormField key={item.id} name={item.id}>
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
</FormField>
))}
</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 => (
<Column key={section.section} gap="3">
<Text weight="bold">{formatMessage((labels as any)[section.section])}</Text>
<Column gap="1">
{section.items.map(item => (
<FormField key={item.id} name={item.id}>
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
</FormField>
))}
</Column>
</Column>
))}
</Grid>
<Row justifyContent="flex-end" paddingTop="3" gap="3">
{onClose && (
<Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)}
</Button>
)}
<FormSubmitButton variant="primary" isDisabled={!hasSelection || !values.name}>
{formatMessage(labels.save)}
</FormSubmitButton>
</Row>
</Column> </Column>
))} );
<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>
</Row>
</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);