Added DialogButton to handle mobile.
Some checks are pending
Create docker images (cloud) / Build, push, and deploy (push) Waiting to run
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run

This commit is contained in:
Mike Cao 2025-10-16 23:59:18 -07:00
parent 036748cdeb
commit 40492ec7c4
32 changed files with 2146 additions and 1807 deletions

View file

@ -77,12 +77,12 @@
"@prisma/extension-read-replicas": "^0.4.1",
"@react-spring/web": "^10.0.3",
"@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.90.2",
"@umami/react-zen": "^0.198.0",
"@tanstack/react-query": "^5.90.5",
"@umami/react-zen": "^0.200.0",
"@umami/redis-client": "^0.29.0",
"bcryptjs": "^3.0.2",
"chalk": "^5.6.2",
"chart.js": "^4.5.0",
"chart.js": "^4.5.1",
"chartjs-adapter-date-fns": "^3.0.0",
"classnames": "^2.3.1",
"colord": "^2.9.2",
@ -94,7 +94,7 @@
"del": "^6.0.0",
"detect-browser": "^5.2.0",
"dotenv": "^17.2.3",
"esbuild": "^0.25.10",
"esbuild": "^0.25.11",
"eslint-plugin-promise": "^6.1.1",
"fs-extra": "^11.3.2",
"immer": "^10.1.3",
@ -115,25 +115,25 @@
"pg": "^8.16.3",
"prisma": "6.16.3",
"pure-rand": "^7.0.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-error-boundary": "^4.0.4",
"react-intl": "^7.1.11",
"react-intl": "^7.1.14",
"react-simple-maps": "^2.3.0",
"react-use-measure": "^2.0.4",
"react-window": "^1.8.6",
"request-ip": "^3.3.0",
"semver": "^7.5.4",
"semver": "^7.7.3",
"serialize-error": "^12.0.0",
"thenby": "^1.3.4",
"ua-parser-js": "^2.0.5",
"ua-parser-js": "^2.0.6",
"uuid": "^11.1.0",
"zod": "^4.1.11",
"zod": "^4.1.12",
"zustand": "^5.0.8"
},
"devDependencies": {
"@formatjs/cli": "^4.2.29",
"@netlify/plugin-nextjs": "^5.13.3",
"@netlify/plugin-nextjs": "^5.14.0",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.0",
@ -142,12 +142,12 @@
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.4",
"@types/jest": "^30.0.0",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@types/node": "^24.8.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/react-window": "^1.8.8",
"@typescript-eslint/eslint-plugin": "^8.45.0",
"@typescript-eslint/parser": "^8.45.0",
"@typescript-eslint/eslint-plugin": "^8.46.1",
"@typescript-eslint/parser": "^8.46.1",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"cross-env": "^10.1.0",
"cypress": "^13.6.6",
@ -163,14 +163,14 @@
"extract-react-intl-messages": "^4.1.1",
"husky": "^9.1.7",
"jest": "^29.7.0",
"lint-staged": "^16.2.3",
"lint-staged": "^16.2.4",
"postcss": "^8.5.6",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-import": "^15.1.0",
"postcss-preset-env": "7.8.3",
"prettier": "^3.6.2",
"prompts": "2.4.2",
"rollup": "^4.52.3",
"rollup": "^4.52.4",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-delete": "^3.0.1",
"rollup-plugin-dts": "^6.2.3",

3260
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,19 @@
import { useMessages } from '@/components/hooks';
import { Button, Icon, Modal, Dialog, DialogTrigger, Text } from '@umami/react-zen';
import { Plus } from '@/components/icons';
import { LinkEditForm } from './LinkEditForm';
import { DialogButton } from '@/components/input/DialogButton';
export function LinkAddButton({ teamId }: { teamId?: string }) {
const { formatMessage, labels } = useMessages();
return (
<DialogTrigger>
<Button data-test="button-website-add" variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.addLink)}</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.addLink)} style={{ width: 600 }}>
{({ close }) => <LinkEditForm teamId={teamId} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
<DialogButton
icon={<Plus />}
label={formatMessage(labels.addLink)}
variant="primary"
width="600px"
>
{({ close }) => <LinkEditForm teamId={teamId} onClose={close} />}
</DialogButton>
);
}

View file

@ -1,9 +1,8 @@
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { Trash } from '@/components/icons';
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { messages } from '@/components/messages';
import { useDeleteQuery, useMessages } from '@/components/hooks';
import { DialogButton } from '@/components/input/DialogButton';
export function LinkDeleteButton({
linkId,
@ -29,27 +28,30 @@ export function LinkDeleteButton({
};
return (
<ActionButton title={formatMessage(labels.delete)} icon={<Trash />}>
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Trash />}
title={formatMessage(labels.confirm)}
variant="quiet"
width="400px"
>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</DialogButton>
);
}

View file

@ -1,19 +1,16 @@
import { ActionButton } from '@/components/input/ActionButton';
import { Edit } from '@/components/icons';
import { Dialog } from '@umami/react-zen';
import { LinkEditForm } from './LinkEditForm';
import { useMessages } from '@/components/hooks';
import { DialogButton } from '@/components/input/DialogButton';
export function LinkEditButton({ linkId }: { linkId: string }) {
const { formatMessage, labels } = useMessages();
return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog title={formatMessage(labels.link)} style={{ width: 800, minHeight: 300 }}>
{({ close }) => {
return <LinkEditForm linkId={linkId} onClose={close} />;
}}
</Dialog>
</ActionButton>
<DialogButton icon={<Edit />} title={formatMessage(labels.link)} variant="quiet" width="800px">
{({ close }) => {
return <LinkEditForm linkId={linkId} onClose={close} />;
}}
</DialogButton>
);
}

View file

@ -1,24 +1,19 @@
import { useMessages } from '@/components/hooks';
import { Button, Icon, Modal, Dialog, DialogTrigger, Text } from '@umami/react-zen';
import { Plus } from '@/components/icons';
import { PixelEditForm } from './PixelEditForm';
import { DialogButton } from '@/components/input/DialogButton';
export function PixelAddButton({ teamId }: { teamId?: string }) {
const { formatMessage, labels } = useMessages();
return (
<DialogTrigger>
<Button data-test="button-website-add" variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.addPixel)}</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.addPixel)} style={{ width: 600 }}>
{({ close }) => <PixelEditForm teamId={teamId} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
<DialogButton
icon={<Plus />}
label={formatMessage(labels.addPixel)}
variant="primary"
width="600px"
>
{({ close }) => <PixelEditForm teamId={teamId} onClose={close} />}
</DialogButton>
);
}

View file

@ -1,9 +1,9 @@
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { Trash } from '@/components/icons';
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { messages } from '@/components/messages';
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
import { DialogButton } from '@/components/input/DialogButton';
export function PixelDeleteButton({
pixelId,
name,
@ -28,27 +28,30 @@ export function PixelDeleteButton({
};
return (
<ActionButton title={formatMessage(labels.delete)} icon={<Trash />}>
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Trash />}
variant="quiet"
title={formatMessage(labels.confirm)}
width="400px"
>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</DialogButton>
);
}

View file

@ -1,19 +1,21 @@
import { ActionButton } from '@/components/input/ActionButton';
import { Edit } from '@/components/icons';
import { Dialog } from '@umami/react-zen';
import { PixelEditForm } from './PixelEditForm';
import { useMessages } from '@/components/hooks';
import { DialogButton } from '@/components/input/DialogButton';
export function PixelEditButton({ pixelId }: { pixelId: string }) {
const { formatMessage, labels } = useMessages();
return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog title={formatMessage(labels.pixel)} style={{ width: 600, minHeight: 300 }}>
{({ close }) => {
return <PixelEditForm pixelId={pixelId} onClose={close} />;
}}
</Dialog>
</ActionButton>
<DialogButton
icon={<Edit />}
title={formatMessage(labels.addPixel)}
variant="quiet"
width="600px"
>
{({ close }) => {
return <PixelEditForm pixelId={pixelId} onClose={close} />;
}}
</DialogButton>
);
}

View file

@ -1,7 +1,7 @@
import { useMessages, useModified } from '@/components/hooks';
import { Dialog, useToast } from '@umami/react-zen';
import { useToast } from '@umami/react-zen';
import { TeamMemberEditForm } from './TeamMemberEditForm';
import { ActionButton } from '@/components/input/ActionButton';
import { DialogButton } from '@/components/input/DialogButton';
import { Edit } from '@/components/icons';
export function TeamMemberEditButton({
@ -26,18 +26,21 @@ export function TeamMemberEditButton({
};
return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog title={formatMessage(labels.editMember)} style={{ width: 400 }}>
{({ close }) => (
<TeamMemberEditForm
teamId={teamId}
userId={userId}
role={role}
onSave={handleSave}
onClose={close}
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Edit />}
title={formatMessage(labels.editMember)}
variant="quiet"
width="400px"
>
{({ close }) => (
<TeamMemberEditForm
teamId={teamId}
userId={userId}
role={role}
onSave={handleSave}
onClose={close}
/>
)}
</DialogButton>
);
}

View file

@ -2,8 +2,7 @@ import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
import { messages } from '@/components/messages';
import { Trash } from '@/components/icons';
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { DialogButton } from '@/components/input/DialogButton';
export function TeamMemberRemoveButton({
teamId,
@ -32,27 +31,30 @@ export function TeamMemberRemoveButton({
};
return (
<ActionButton title={formatMessage(labels.delete)} icon={<Trash />}>
<Dialog title={formatMessage(labels.removeMember)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{userName}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.remove)}
buttonVariant="danger"
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Trash />}
title={formatMessage(labels.confirm)}
variant="quiet"
width="400px"
>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{userName}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.remove)}
buttonVariant="danger"
/>
)}
</DialogButton>
);
}

View file

@ -1,7 +1,8 @@
import { useMessages, useModified } from '@/components/hooks';
import { Button, Icon, Modal, Dialog, DialogTrigger, Text, useToast } from '@umami/react-zen';
import { useToast } from '@umami/react-zen';
import { Plus } from '@/components/icons';
import { WebsiteAddForm } from './WebsiteAddForm';
import { DialogButton } from '@/components/input/DialogButton';
export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?: () => void }) {
const { formatMessage, labels, messages } = useMessages();
@ -15,18 +16,13 @@ export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?:
};
return (
<DialogTrigger>
<Button data-test="button-website-add" variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.addWebsite)}</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.addWebsite)} style={{ width: 400 }}>
{({ close }) => <WebsiteAddForm teamId={teamId} onSave={handleSave} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
<DialogButton
icon={<Plus />}
label={formatMessage(labels.addWebsite)}
variant="primary"
width="400px"
>
{({ close }) => <WebsiteAddForm teamId={teamId} onSave={handleSave} onClose={close} />}
</DialogButton>
);
}

View file

@ -22,7 +22,7 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
</SectionHeader>
<LoadingPanel data={data} isLoading={isLoading} error={error}>
{data && (
<Grid columns="1fr 1fr" gap>
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
{data['data'].map((report: any) => (
<Panel key={report.id}>
<Goal {...report} startDate={startDate} endDate={endDate} />

View file

@ -8,7 +8,7 @@ import { endOfMonth, startOfMonth } from 'date-fns';
export function RetentionPage({ websiteId }: { websiteId: string }) {
const {
dateRange: { startDate },
} = useDateRange(websiteId, { ignoreOffset: true });
} = useDateRange();
const monthStartDate = startOfMonth(startDate);
const monthEndDate = endOfMonth(startDate);

View file

@ -1,29 +1,16 @@
import { Button, DialogTrigger, Modal, Text, Icon, Dialog } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { CohortEditForm } from './CohortEditForm';
export function CohortAddButton({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
return (
<DialogTrigger>
<Button variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.cohort)}</Text>
</Button>
<Modal>
<Dialog
title={formatMessage(labels.cohort)}
style={{ width: 800, minHeight: 300, maxHeight: '90vh' }}
>
{({ close }) => {
return <CohortEditForm websiteId={websiteId} onClose={close} />;
}}
</Dialog>
</Modal>
</DialogTrigger>
<DialogButton icon={<Plus />} label={formatMessage(labels.cohort)} variant="primary">
{({ close }) => {
return <CohortEditForm websiteId={websiteId} onClose={close} />;
}}
</DialogButton>
);
}

View file

@ -1,9 +1,8 @@
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { Trash } from '@/components/icons';
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { messages } from '@/components/messages';
import { useDeleteQuery, useMessages } from '@/components/hooks';
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { DialogButton } from '@/components/input/DialogButton';
export function CohortDeleteButton({
cohortId,
@ -32,27 +31,30 @@ export function CohortDeleteButton({
};
return (
<ActionButton title={formatMessage(labels.delete)} icon={<Trash />}>
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Trash />}
variant="quiet"
title={formatMessage(labels.confirm)}
width="400px"
>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</DialogButton>
);
}

View file

@ -1,9 +1,8 @@
import { ActionButton } from '@/components/input/ActionButton';
import { Edit } from '@/components/icons';
import { Dialog } from '@umami/react-zen';
import { CohortEditForm } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditForm';
import { useMessages } from '@/components/hooks';
import { Filter } from '@/lib/types';
import { DialogButton } from '@/components/input/DialogButton';
export function CohortEditButton({
cohortId,
@ -17,22 +16,23 @@ export function CohortEditButton({
const { formatMessage, labels } = useMessages();
return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog
title={formatMessage(labels.cohort)}
style={{ width: 800, minHeight: 300, maxHeight: '90vh' }}
>
{({ close }) => {
return (
<CohortEditForm
cohortId={cohortId}
websiteId={websiteId}
filters={filters}
onClose={close}
/>
);
}}
</Dialog>
</ActionButton>
<DialogButton
icon={<Edit />}
variant="quiet"
title={formatMessage(labels.cohort)}
width="800px"
minHeight="300px"
>
{({ close }) => {
return (
<CohortEditForm
cohortId={cohortId}
websiteId={websiteId}
filters={filters}
onClose={close}
/>
);
}}
</DialogButton>
);
}

View file

@ -80,7 +80,7 @@ export function CohortEditForm({
<Column>
<Label>{formatMessage(labels.action)}</Label>
<Grid columns="260px 1fr" gap>
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
<Column>
<FormField
name="parameters.action.type"

View file

@ -1,22 +1,17 @@
import { DataTable, DataColumn, Row } from '@umami/react-zen';
import { DataTable, DataColumn, Row, DataTableProps } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks';
import { Empty } from '@/components/common/Empty';
import { DateDistance } from '@/components/common/DateDistance';
import { filtersObjectToArray } from '@/lib/params';
import { CohortEditButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditButton';
import { CohortDeleteButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton';
import Link from 'next/link';
export function CohortsTable({ data = [] }) {
export function CohortsTable(props: DataTableProps) {
const { formatMessage, labels } = useMessages();
const { websiteId, renderUrl } = useNavigation();
if (data.length === 0) {
return <Empty />;
}
return (
<DataTable data={data}>
<DataTable {...props}>
<DataColumn id="name" label={formatMessage(labels.name)}>
{(row: any) => (
<Link href={renderUrl(`/websites/${websiteId}?cohort=${row.id}`, false)}>{row.name}</Link>

View file

@ -1,29 +1,16 @@
import { Button, DialogTrigger, Modal, Text, Icon, Dialog } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { SegmentEditForm } from './SegmentEditForm';
import { DialogButton } from '@/components/input/DialogButton';
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, minHeight: 300, maxHeight: '90vh' }}
>
{({ close }) => {
return <SegmentEditForm websiteId={websiteId} onClose={close} />;
}}
</Dialog>
</Modal>
</DialogTrigger>
<DialogButton icon={<Plus />} label={formatMessage(labels.segment)} variant="primary">
{({ close }) => {
return <SegmentEditForm websiteId={websiteId} onClose={close} />;
}}
</DialogButton>
);
}

View file

@ -1,9 +1,8 @@
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { Trash } from '@/components/icons';
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { messages } from '@/components/messages';
import { useDeleteQuery, useMessages } from '@/components/hooks';
import { DialogButton } from '@/components/input/DialogButton';
export function SegmentDeleteButton({
segmentId,
@ -32,27 +31,30 @@ export function SegmentDeleteButton({
};
return (
<ActionButton title={formatMessage(labels.delete)} icon={<Trash />}>
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</Dialog>
</ActionButton>
<DialogButton
icon={<Trash />}
title={formatMessage(labels.confirm)}
variant="quiet"
width="600px"
>
{({ close }) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}
onClose={close}
buttonLabel={formatMessage(labels.delete)}
buttonVariant="danger"
/>
)}
</DialogButton>
);
}

View file

@ -1,9 +1,8 @@
import { Dialog } from '@umami/react-zen';
import { ActionButton } from '@/components/input/ActionButton';
import { Edit } from '@/components/icons';
import { useMessages } from '@/components/hooks';
import { SegmentEditForm } from './SegmentEditForm';
import { Filter } from '@/lib/types';
import { DialogButton } from '@/components/input/DialogButton';
export function SegmentEditButton({
segmentId,
@ -17,22 +16,22 @@ export function SegmentEditButton({
const { formatMessage, labels } = useMessages();
return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog
title={formatMessage(labels.segment)}
style={{ width: 800, minHeight: 300, maxHeight: '90vh' }}
>
{({ close }) => {
return (
<SegmentEditForm
segmentId={segmentId}
websiteId={websiteId}
filters={filters}
onClose={close}
/>
);
}}
</Dialog>
</ActionButton>
<DialogButton
icon={<Edit />}
title={formatMessage(labels.segment)}
variant="quiet"
width="800px"
>
{({ close }) => {
return (
<SegmentEditForm
segmentId={segmentId}
websiteId={websiteId}
filters={filters}
onClose={close}
/>
);
}}
</DialogButton>
);
}

View file

@ -1,21 +1,16 @@
import { DataTable, DataColumn, Row } from '@umami/react-zen';
import { DataTable, DataColumn, Row, DataTableProps } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks';
import { Empty } from '@/components/common/Empty';
import { DateDistance } from '@/components/common/DateDistance';
import { SegmentEditButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditButton';
import { SegmentDeleteButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton';
import Link from 'next/link';
export function SegmentsTable({ data = [] }) {
export function SegmentsTable(props: DataTableProps) {
const { formatMessage, labels } = useMessages();
const { websiteId, renderUrl } = useNavigation();
if (data.length === 0) {
return <Empty />;
}
return (
<DataTable data={data}>
<DataTable {...props}>
<DataColumn id="name" label={formatMessage(labels.name)}>
{(row: any) => (
<Link href={renderUrl(`/websites/${websiteId}?segment=${row.id}`, false)}>

View file

@ -64,7 +64,7 @@ export function DataGrid({
return (
<Column gap="4" minHeight="300px">
{allowSearch && (
<Row alignItems="center" justifyContent="space-between">
<Row alignItems="center" justifyContent="space-between" wrap="wrap" gap>
<SearchField
value={search}
onSearch={handleSearch}

View file

@ -104,8 +104,8 @@ export function FilterRecord({
</Select>
)}
</Grid>
<Column justifyContent="flex-end">
<Button variant="quiet" onPress={() => onRemove?.(name)}>
<Column justifyContent="flex-start">
<Button onPress={() => onRemove?.(name)}>
<Icon>
<X />
</Icon>

View file

@ -9,7 +9,7 @@ export function MobileMenu(props: DialogProps) {
<Menu />
</Icon>
</Button>
<Modal position="left" offset="80px">
<Modal placement="left" offset="80px">
<Dialog variant="sheet" {...props} />
</Modal>
</DialogTrigger>

View file

@ -73,6 +73,7 @@ export * from './useGlobalState';
export * from './useLanguageNames';
export * from './useLocale';
export * from './useMessages';
export * from './useMobile';
export * from './useModified';
export * from './useNavigation';
export * from './usePagedQuery';

View file

@ -0,0 +1,8 @@
import { useBreakpoint } from '@umami/react-zen';
export function useMobile() {
const breakpoint = useBreakpoint();
const isMobile = ['xs', 'sm', 'md'].includes(breakpoint);
return { breakpoint, isMobile };
}

View file

@ -1,25 +0,0 @@
import { ReactNode } from 'react';
import { Button, Icon, Modal, Text, DialogTrigger } from '@umami/react-zen';
export function ActionButton({
onClick,
icon,
title,
children,
}: {
onClick?: () => void;
icon?: ReactNode;
title?: string;
children?: ReactNode;
}) {
return (
<DialogTrigger>
<Text title={title}>
<Button variant="quiet" onPress={onClick}>
<Icon>{icon}</Icon>
</Button>
</Text>
<Modal>{children}</Modal>
</DialogTrigger>
);
}

View file

@ -0,0 +1,57 @@
import { CSSProperties, ReactNode } from 'react';
import {
Button,
ButtonProps,
Modal,
Dialog,
DialogTrigger,
DialogProps,
IconLabel,
} from '@umami/react-zen';
import { useMobile } from '@/components/hooks';
export interface DialogButtonProps extends Omit<ButtonProps, 'children'> {
icon?: ReactNode;
label?: ReactNode;
title?: ReactNode;
width?: string;
height?: string;
minWidth?: string;
minHeight?: string;
children?: DialogProps['children'];
}
export function DialogButton({
icon,
label,
title,
width = '800px',
height,
minWidth,
minHeight,
children,
...props
}: DialogButtonProps) {
const { isMobile } = useMobile();
const style: CSSProperties = { width, height, minWidth, minHeight, padding: '32px' };
if (isMobile) {
style.width = '100%';
style.height = '100%';
style.overflowY = 'auto';
}
return (
<DialogTrigger>
<Button {...props}>
<IconLabel icon={icon} label={label} />
</Button>
<Modal placement={isMobile ? 'fullscreen' : 'center'}>
<Dialog variant={isMobile ? 'sheet' : undefined} title={title || label} style={style}>
{children}
</Dialog>
</Modal>
</DialogTrigger>
);
}

View file

@ -13,7 +13,7 @@ import {
MenuItem,
Icon,
} from '@umami/react-zen';
import { useFields, useMessages } from '@/components/hooks';
import { useFields, useMessages, useMobile } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { FilterRecord } from '@/components/common/FilterRecord';
import { Empty } from '@/components/common/Empty';
@ -30,6 +30,7 @@ export function FieldFilters({ websiteId, value, exclude = [], onChange }: Field
const { fields } = useFields();
const startDate = subMonths(endOfDay(new Date()), 6);
const endDate = endOfDay(new Date());
const { isMobile } = useMobile();
const updateFilter = (name: string, props: Record<string, any>) => {
onChange(value.map(filter => (filter.name === name ? { ...filter, ...props } : filter)));
@ -60,8 +61,11 @@ export function FieldFilters({ websiteId, value, exclude = [], onChange }: Field
<Plus />
</Icon>
</Button>
<Popover placement="bottom start">
<Menu onAction={handleAdd}>
<Popover placement={isMobile ? 'left' : 'bottom start'} shouldFlip>
<Menu
onAction={handleAdd}
style={{ maxHeight: 'calc(100vh - 2rem)', overflowY: 'auto' }}
>
{fields
.filter(({ name }) => !exclude.includes(name))
.map(field => {

View file

@ -34,7 +34,7 @@ export function WebsiteFilterButton({
</Icon>
{showText && <Text>{formatMessage(labels.filter)}</Text>}
</Button>
<Modal position={isMobile ? 'fullscreen' : 'center'}>
<Modal placement={isMobile ? 'fullscreen' : 'center'}>
<Dialog title={formatMessage(labels.filters)}>
{({ close }) => {
return <FilterEditForm websiteId={websiteId} onChange={handleChange} onClose={close} />;

View file

@ -69,8 +69,8 @@ export * from '@/components/common/SectionHeader';
export * from '@/components/common/SideMenu';
export * from '@/components/common/TypeConfirmationForm';
export * from '@/components/input/ActionButton';
export * from '@/components/input/DateFilter';
export * from '@/components/input/DialogButton';
export * from '@/components/input/DownloadButton';
export * from '@/components/input/ExportButton';
export * from '@/components/input/FilterButtons';