mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Add display options form for website shares
Allow users to select which navigation items to display when creating or editing a share. Options include traffic, behavior, and growth sections with checkboxes for each nav item (excluding segments/cohorts). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0eb598c817
commit
ef3aec09be
4 changed files with 163 additions and 21 deletions
|
|
@ -0,0 +1,83 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Column,
|
||||||
|
Form,
|
||||||
|
FormField,
|
||||||
|
FormSubmitButton,
|
||||||
|
Row,
|
||||||
|
Text,
|
||||||
|
} from '@umami/react-zen';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||||
|
import { SHARE_NAV_ITEMS } from './constants';
|
||||||
|
|
||||||
|
export interface ShareCreateFormProps {
|
||||||
|
websiteId: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormProps) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { post } = useApi();
|
||||||
|
const { touch } = useModified();
|
||||||
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
|
// Build default values - all enabled by default
|
||||||
|
const defaultValues: Record<string, boolean> = {};
|
||||||
|
SHARE_NAV_ITEMS.forEach(section => {
|
||||||
|
section.items.forEach(item => {
|
||||||
|
defaultValues[item.id] = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (data: any) => {
|
||||||
|
setIsPending(true);
|
||||||
|
try {
|
||||||
|
const parameters: Record<string, boolean> = {};
|
||||||
|
SHARE_NAV_ITEMS.forEach(section => {
|
||||||
|
section.items.forEach(item => {
|
||||||
|
parameters[item.id] = data[item.id] ?? true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await post(`/websites/${websiteId}/shares`, { parameters });
|
||||||
|
touch('shares');
|
||||||
|
onSave?.();
|
||||||
|
onClose?.();
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
<Text size="1">{formatMessage((labels as any)[item.label])}</Text>
|
||||||
|
</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}>{formatMessage(labels.save)}</FormSubmitButton>
|
||||||
|
</Row>
|
||||||
|
</Column>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
Column,
|
Column,
|
||||||
Form,
|
Form,
|
||||||
|
FormField,
|
||||||
FormSubmitButton,
|
FormSubmitButton,
|
||||||
Label,
|
Label,
|
||||||
Loading,
|
Loading,
|
||||||
Row,
|
Row,
|
||||||
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
|
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
|
||||||
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
||||||
|
import { SHARE_NAV_ITEMS } from './constants';
|
||||||
|
|
||||||
export function ShareEditForm({
|
export function ShareEditForm({
|
||||||
shareId,
|
shareId,
|
||||||
|
|
@ -50,8 +54,15 @@ export function ShareEditForm({
|
||||||
}, [shareId, modified]);
|
}, [shareId, modified]);
|
||||||
|
|
||||||
const handleSubmit = async (data: any) => {
|
const handleSubmit = async (data: any) => {
|
||||||
|
const parameters: Record<string, boolean> = {};
|
||||||
|
SHARE_NAV_ITEMS.forEach(section => {
|
||||||
|
section.items.forEach(item => {
|
||||||
|
parameters[item.id] = data[item.id] ?? true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{ slug: data.slug, parameters: share?.parameters || {} },
|
{ slug: share.slug, parameters },
|
||||||
{
|
{
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
toast(formatMessage(messages.saved));
|
toast(formatMessage(messages.saved));
|
||||||
|
|
@ -69,24 +80,42 @@ export function ShareEditForm({
|
||||||
|
|
||||||
const url = getUrl(share?.slug || '');
|
const url = getUrl(share?.slug || '');
|
||||||
|
|
||||||
|
// Build default values from share parameters
|
||||||
|
const defaultValues: Record<string, boolean> = {};
|
||||||
|
SHARE_NAV_ITEMS.forEach(section => {
|
||||||
|
section.items.forEach(item => {
|
||||||
|
defaultValues[item.id] = share?.parameters?.[item.id] ?? true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}>
|
||||||
onSubmit={handleSubmit}
|
<Column gap="3">
|
||||||
error={getErrorMessage(error)}
|
|
||||||
defaultValues={{ slug: share?.slug }}
|
|
||||||
>
|
|
||||||
<Column gap>
|
|
||||||
<Column>
|
<Column>
|
||||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||||
<TextField value={url} isReadOnly allowCopy />
|
<TextField value={url} isReadOnly allowCopy />
|
||||||
</Column>
|
</Column>
|
||||||
|
{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">
|
<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>{formatMessage(labels.save)}</FormSubmitButton>
|
<FormSubmitButton variant="primary">{formatMessage(labels.save)}</FormSubmitButton>
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { Button, Column, Heading, Row, Text } from '@umami/react-zen';
|
import { Column, Heading, Row, Text } from '@umami/react-zen';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useApi, useMessages, useModified, useWebsiteSharesQuery } from '@/components/hooks';
|
import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
|
||||||
|
import { DialogButton } from '@/components/input/DialogButton';
|
||||||
|
import { ShareCreateForm } from './ShareCreateForm';
|
||||||
import { SharesTable } from './SharesTable';
|
import { SharesTable } from './SharesTable';
|
||||||
|
|
||||||
export interface WebsiteShareFormProps {
|
export interface WebsiteShareFormProps {
|
||||||
|
|
@ -10,13 +12,6 @@ export interface WebsiteShareFormProps {
|
||||||
export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
||||||
const { formatMessage, labels, messages } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
const { data, isLoading } = useWebsiteSharesQuery({ websiteId });
|
const { data, isLoading } = useWebsiteSharesQuery({ websiteId });
|
||||||
const { post } = useApi();
|
|
||||||
const { touch } = useModified();
|
|
||||||
|
|
||||||
const handleCreate = async () => {
|
|
||||||
await post(`/websites/${websiteId}/shares`, { parameters: {} });
|
|
||||||
touch('shares');
|
|
||||||
};
|
|
||||||
|
|
||||||
const shares = data?.data || [];
|
const shares = data?.data || [];
|
||||||
const hasShares = shares.length > 0;
|
const hasShares = shares.length > 0;
|
||||||
|
|
@ -25,10 +20,15 @@ export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
||||||
<Column gap="4">
|
<Column gap="4">
|
||||||
<Row justifyContent="space-between" alignItems="center">
|
<Row justifyContent="space-between" alignItems="center">
|
||||||
<Heading>{formatMessage(labels.share)}</Heading>
|
<Heading>{formatMessage(labels.share)}</Heading>
|
||||||
<Button variant="primary" onPress={handleCreate}>
|
<DialogButton
|
||||||
<Plus size={16} />
|
icon={<Plus size={16} />}
|
||||||
<Text>{formatMessage(labels.add)}</Text>
|
label={formatMessage(labels.add)}
|
||||||
</Button>
|
title={formatMessage(labels.share)}
|
||||||
|
variant="primary"
|
||||||
|
width="400px"
|
||||||
|
>
|
||||||
|
{({ close }) => <ShareCreateForm websiteId={websiteId} onClose={close} />}
|
||||||
|
</DialogButton>
|
||||||
</Row>
|
</Row>
|
||||||
{hasShares ? (
|
{hasShares ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
30
src/app/(main)/websites/[websiteId]/settings/constants.ts
Normal file
30
src/app/(main)/websites/[websiteId]/settings/constants.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
export const SHARE_NAV_ITEMS = [
|
||||||
|
{
|
||||||
|
section: 'traffic',
|
||||||
|
items: [
|
||||||
|
{ id: 'overview', label: 'overview' },
|
||||||
|
{ id: 'events', label: 'events' },
|
||||||
|
{ id: 'sessions', label: 'sessions' },
|
||||||
|
{ id: 'realtime', label: 'realtime' },
|
||||||
|
{ id: 'compare', label: 'compare' },
|
||||||
|
{ id: 'breakdown', label: 'breakdown' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'behavior',
|
||||||
|
items: [
|
||||||
|
{ id: 'goals', label: 'goals' },
|
||||||
|
{ id: 'funnels', label: 'funnels' },
|
||||||
|
{ id: 'journeys', label: 'journeys' },
|
||||||
|
{ id: 'retention', label: 'retention' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'growth',
|
||||||
|
items: [
|
||||||
|
{ id: 'utm', label: 'utm' },
|
||||||
|
{ id: 'revenue', label: 'revenue' },
|
||||||
|
{ id: 'attribution', label: 'attribution' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
Loading…
Add table
Add a link
Reference in a new issue