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

@ -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';