mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 12:47:13 +01:00
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:
parent
4c9b2f10da
commit
af7f7adf5b
5 changed files with 105 additions and 53 deletions
|
|
@ -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,30 +56,46 @@ export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormP
|
|||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} defaultValues={defaultValues}>
|
||||
<Column gap="3">
|
||||
{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>
|
||||
{({ 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">
|
||||
{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>
|
||||
))}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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 (
|
||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}>
|
||||
<Column gap="3">
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||
<TextField value={url} isReadOnly allowCopy />
|
||||
</Column>
|
||||
{SHARE_NAV_ITEMS.map(section => (
|
||||
<Column key={section.section} gap="1">
|
||||
<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>
|
||||
))}
|
||||
{({ 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="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>
|
||||
))}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue