mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +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,
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue