mirror of
https://github.com/umami-software/umami.git
synced 2026-02-07 06:07:17 +01:00
Segment editing.
This commit is contained in:
parent
fba7e12c36
commit
2dbcf63eeb
22 changed files with 306 additions and 42 deletions
|
|
@ -3,6 +3,7 @@ import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFi
|
|||
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||
import { FilterBar } from '@/components/input/FilterBar';
|
||||
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect';
|
||||
import { ExportButton } from '@/components/input/ExportButton';
|
||||
|
||||
export function WebsiteControls({
|
||||
websiteId,
|
||||
|
|
@ -10,21 +11,24 @@ export function WebsiteControls({
|
|||
allowDateFilter = true,
|
||||
allowMonthFilter,
|
||||
allowCompare,
|
||||
allowDownload = false,
|
||||
}: {
|
||||
websiteId: string;
|
||||
allowFilter?: boolean;
|
||||
allowCompare?: boolean;
|
||||
allowDateFilter?: boolean;
|
||||
allowMonthFilter?: boolean;
|
||||
allowDownload?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Column gap>
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3">
|
||||
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
||||
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
|
||||
{allowDownload && <ExportButton websiteId={websiteId} />}
|
||||
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
|
||||
</Row>
|
||||
{allowFilter && <FilterBar />}
|
||||
{allowFilter && <FilterBar websiteId={websiteId} />}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Button, Icon, DialogTrigger, Dialog, Modal, Text } from '@umami/react-z
|
|||
import { ListFilter } from '@/components/icons';
|
||||
import { FilterEditForm } from '@/components/common/FilterEditForm';
|
||||
import { useMessages, useNavigation, useFilters } from '@/components/hooks';
|
||||
import { filtersArrayToObject } from '@/lib/params';
|
||||
|
||||
export function WebsiteFilterButton({
|
||||
websiteId,
|
||||
|
|
@ -21,16 +22,7 @@ export function WebsiteFilterButton({
|
|||
const { filters } = useFilters();
|
||||
|
||||
const handleChange = ({ filters, segment }) => {
|
||||
const params = filters.reduce(
|
||||
(obj: { [x: string]: string }, filter: { name: any; operator: any; value: any }) => {
|
||||
const { name, operator, value } = filter;
|
||||
|
||||
obj[name] = `${operator}.${value}`;
|
||||
|
||||
return obj;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const params = filtersArrayToObject(filters);
|
||||
|
||||
const url = replaceParams({ ...params, segment });
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { Button, DialogTrigger, Modal, Text, Icon, Dialog } from '@umami/react-zen';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { Plus } from '@/components/icons';
|
||||
import { SegmentEditForm } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditForm';
|
||||
|
||||
export function SegmentAddButton({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
<Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.segment)}</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.segment)} style={{ width: 800 }}>
|
||||
{({ close }) => {
|
||||
return <SegmentEditForm websiteId={websiteId} onClose={close} />;
|
||||
}}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormField,
|
||||
FormSubmitButton,
|
||||
TextField,
|
||||
Label,
|
||||
} from '@umami/react-zen';
|
||||
import { subMonths, endOfDay } from 'date-fns';
|
||||
import { FieldFilters } from '@/components/input/FieldFilters';
|
||||
import { useState } from 'react';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { filtersArrayToObject } from '@/lib/params';
|
||||
|
||||
export function SegmentEditForm({
|
||||
websiteId,
|
||||
filters = [],
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
websiteId: string;
|
||||
filters?: any[];
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [currentFilters, setCurrentFilters] = useState(filters);
|
||||
const { touch } = useModified();
|
||||
const startDate = subMonths(endOfDay(new Date()), 6);
|
||||
const endDate = endOfDay(new Date());
|
||||
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) =>
|
||||
post(`/websites/${websiteId}/segments`, { ...data, type: 'segment' }),
|
||||
});
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(
|
||||
{ ...data, parameters: filtersArrayToObject(currentFilters) },
|
||||
{
|
||||
onSuccess: async () => {
|
||||
touch('segments');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form error={error} onSubmit={handleSubmit}>
|
||||
<FormField
|
||||
name="name"
|
||||
label={formatMessage(labels.name)}
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
<TextField />
|
||||
</FormField>
|
||||
<Label>{formatMessage(labels.filters)}</Label>
|
||||
<FieldFilters
|
||||
websiteId={websiteId}
|
||||
filters={currentFilters}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onSave={setCurrentFilters}
|
||||
/>
|
||||
<FormButtons>
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
</Button>
|
||||
<FormSubmitButton variant="primary" data-test="button-submit" isDisabled={isPending}>
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { SegmentAddButton } from './SegmentAddButton';
|
||||
import { useWebsiteSegmentsQuery } from '@/components/hooks';
|
||||
import { SegmentsTable } from './SegmentsTable';
|
||||
import { DataGrid } from '@/components/common/DataGrid';
|
||||
|
||||
export function SegmentsDataTable({ websiteId }: { websiteId?: string }) {
|
||||
const query = useWebsiteSegmentsQuery(websiteId, { type: 'segment' });
|
||||
|
||||
const renderActions = () => {
|
||||
return <SegmentAddButton websiteId={websiteId} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
query={query}
|
||||
allowSearch={true}
|
||||
autoFocus={false}
|
||||
allowPaging={true}
|
||||
renderActions={renderActions}
|
||||
>
|
||||
{({ data }) => <SegmentsTable data={data} />}
|
||||
</DataGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { SegmentsDataTable } from './SegmentsDataTable';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
|
||||
export function SegmentsPage({ websiteId }) {
|
||||
return (
|
||||
<Column gap="3">
|
||||
<WebsiteControls websiteId={websiteId} allowFilter={false} allowDateFilter={false} />
|
||||
<Panel>
|
||||
<SegmentsDataTable websiteId={websiteId} />
|
||||
</Panel>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { DataTable, DataColumn, Icon, Row, Text, MenuItem } from '@umami/react-zen';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { Edit, Trash } from '@/components/icons';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { MenuButton } from '@/components/input/MenuButton';
|
||||
|
||||
export function SegmentsTable({ data = [] }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
if (data.length === 0) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable data={data}>
|
||||
<DataColumn id="name" label={formatMessage(labels.name)} />
|
||||
<DataColumn id="created" label={formatMessage(labels.created)}>
|
||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||
</DataColumn>
|
||||
<DataColumn id="action" align="end" width="100px">
|
||||
{(row: any) => {
|
||||
const { id } = row;
|
||||
|
||||
return (
|
||||
<MenuButton>
|
||||
<MenuItem href={`/admin/users/${id}`} data-test="link-button-edit">
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<Edit />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.edit)}</Text>
|
||||
</Row>
|
||||
</MenuItem>
|
||||
<MenuItem id="delete" onAction={null} data-test="link-button-delete">
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<Trash />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.delete)}</Text>
|
||||
</Row>
|
||||
</MenuItem>
|
||||
</MenuButton>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
12
src/app/(main)/websites/[websiteId]/segments/page.tsx
Normal file
12
src/app/(main)/websites/[websiteId]/segments/page.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Metadata } from 'next';
|
||||
import { SegmentsPage } from './SegmentsPage';
|
||||
|
||||
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
|
||||
const { websiteId } = await params;
|
||||
|
||||
return <SegmentsPage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Segments',
|
||||
};
|
||||
|
|
@ -2,7 +2,7 @@ import { canUpdateWebsite, canViewWebsite } from '@/lib/auth';
|
|||
import { uuid } from '@/lib/crypto';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { segmentTypeParam } from '@/lib/schema';
|
||||
import { segmentTypeParam, searchParams } from '@/lib/schema';
|
||||
import { createSegment, getWebsiteSegments } from '@/queries';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ export async function GET(
|
|||
) {
|
||||
const schema = z.object({
|
||||
type: segmentTypeParam,
|
||||
...searchParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue