Move pixel and link editing to dedicated pages

This commit is contained in:
Mike Cao
2026-03-09 22:09:54 -07:00
parent 48f8cd0ece
commit 9941181697
17 changed files with 285 additions and 60 deletions
+6 -7
View File
@@ -1,16 +1,15 @@
import { LinkButton } from '@/components/common/LinkButton';
import { useNavigation } from '@/components/hooks';
import { useMessages } from '@/components/hooks';
import { Edit } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { LinkEditForm } from './LinkEditForm';
export function LinkEditButton({ linkId }: { linkId: string }) {
const { t, labels } = useMessages();
const { renderUrl } = useNavigation();
return (
<DialogButton icon={<Edit />} title={t(labels.link)} variant="quiet" width="800px">
{({ close }) => {
return <LinkEditForm linkId={linkId} onClose={close} />;
}}
</DialogButton>
<LinkButton href={renderUrl(`/links/${linkId}/edit`, false)} variant="quiet" aria-label={t(labels.edit)}>
<Edit />
</LinkButton>
);
}
+2 -1
View File
@@ -40,7 +40,8 @@ export function LinkEditForm({
teamId,
},
);
const { linksUrl } = useConfig();
const config = useConfig();
const linksUrl = config?.linksUrl;
const hostUrl = linksUrl || LINKS_URL;
const { data, isLoading } = useLinkQuery(linkId);
const [defaultSlug] = useState(generateId());
+6 -4
View File
@@ -2,9 +2,8 @@ import { Row } from '@umami/react-zen';
import { IconLabel } from '@/components/common/IconLabel';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useLink, useMessages, useSlug } from '@/components/hooks';
import { ExternalLink, Link } from '@/components/icons';
import { LinkShareButton } from './LinkShareButton';
import { useLink, useMessages, useNavigation, useSlug } from '@/components/hooks';
import { Edit, ExternalLink, Link } from '@/components/icons';
export function LinkHeader({ showActions = true }: { showActions?: boolean }) {
const link = useLink();
@@ -18,11 +17,14 @@ export function LinkHeader({ showActions = true }: { showActions?: boolean }) {
function LinkHeaderActions({ linkId, slug }: { linkId: string; slug: string }) {
const { t, labels } = useMessages();
const { renderUrl } = useNavigation();
const { getSlugUrl } = useSlug('link');
return (
<Row alignItems="center" gap="3">
<LinkShareButton linkId={linkId} />
<LinkButton href={renderUrl(`/links/${linkId}/edit`, false)}>
<IconLabel icon={<Edit />} label={t(labels.edit)} />
</LinkButton>
<LinkButton href={getSlugUrl(slug)} target="_blank" prefetch={false} asAnchor>
<IconLabel icon={<ExternalLink />} label={t(labels.view)} />
</LinkButton>
@@ -1,14 +0,0 @@
import { useMessages } from '@/components/hooks';
import { Share } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { LinkShareDialog } from './LinkShareDialog';
export function LinkShareButton({ linkId }: { linkId: string }) {
const { t, labels } = useMessages();
return (
<DialogButton icon={<Share />} label={t(labels.share)} title={null} width="900px">
<LinkShareDialog linkId={linkId} />
</DialogButton>
);
}
@@ -7,19 +7,19 @@ import { Plus } from '@/components/icons';
import { SimpleShareCreateForm } from '@/components/share/SimpleShareCreateForm';
import { SimpleSharesTable } from '@/components/share/SimpleSharesTable';
export function LinkShareDialog({ linkId }: { linkId: string }) {
export function LinkShareForm({ linkId }: { linkId: string }) {
const { data, error, isLoading } = useLinkSharesQuery({ linkId });
const shares = data?.data || [];
const hasShares = shares.length > 0;
return (
<LoadingPanel data={data} isLoading={isLoading} error={error}>
<LinkShareDialogContent linkId={linkId} hasShares={hasShares} shares={shares} />
<LinkShareFormContent linkId={linkId} hasShares={hasShares} shares={shares} />
</LoadingPanel>
);
}
function LinkShareDialogContent({
function LinkShareFormContent({
linkId,
hasShares,
shares,
@@ -0,0 +1,46 @@
'use client';
import { Column } from '@umami/react-zen';
import Link from 'next/link';
import { LinkEditForm } from '@/app/(main)/links/LinkEditForm';
import { LinkProvider } from '@/app/(main)/links/LinkProvider';
import { LinkShareForm } from '@/app/(main)/links/[linkId]/LinkShareForm';
import { Panel } from '@/components/common/Panel';
import { IconLabel } from '@/components/common/IconLabel';
import { PageHeader } from '@/components/common/PageHeader';
import { useLink, useMessages, useNavigation } from '@/components/hooks';
import { ArrowLeft, Link as LinkIcon } from '@/components/icons';
export function LinkEditPage({ linkId }: { linkId: string }) {
return (
<LinkProvider linkId={linkId}>
<Column margin="2">
<LinkEditHeader />
<Column gap="6">
<Panel>
<LinkEditForm linkId={linkId} />
</Panel>
<Panel>
<LinkShareForm linkId={linkId} />
</Panel>
</Column>
</Column>
</LinkProvider>
);
}
function LinkEditHeader() {
const link = useLink();
const { t, labels } = useMessages();
const { renderUrl } = useNavigation();
return (
<>
<Column marginTop="6">
<Link href={renderUrl(`/links/${link.id}`)}>
<IconLabel icon={<ArrowLeft />} label={t(labels.link)} />
</Link>
</Column>
<PageHeader title={link.name} description={link.url} icon={<LinkIcon />} />
</>
);
}
@@ -0,0 +1,18 @@
import type { Metadata } from 'next';
import { getLink } from '@/queries/prisma';
import { LinkEditPage } from './LinkEditPage';
export default async function ({ params }: { params: Promise<{ linkId: string }> }) {
const { linkId } = await params;
const link = await getLink(linkId);
if (!link || link.deletedAt) {
return null;
}
return <LinkEditPage linkId={linkId} />;
}
export const metadata: Metadata = {
title: 'Edit Link',
};
+6 -7
View File
@@ -1,16 +1,15 @@
import { LinkButton } from '@/components/common/LinkButton';
import { useNavigation } from '@/components/hooks';
import { useMessages } from '@/components/hooks';
import { Edit } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { PixelEditForm } from './PixelEditForm';
export function PixelEditButton({ pixelId }: { pixelId: string }) {
const { t, labels } = useMessages();
const { renderUrl } = useNavigation();
return (
<DialogButton icon={<Edit />} title={t(labels.addPixel)} variant="quiet" width="600px">
{({ close }) => {
return <PixelEditForm pixelId={pixelId} onClose={close} />;
}}
</DialogButton>
<LinkButton href={renderUrl(`/pixels/${pixelId}/edit`, false)} variant="quiet" aria-label={t(labels.edit)}>
<Edit />
</LinkButton>
);
}
+2 -1
View File
@@ -38,7 +38,8 @@ export function PixelEditForm({
teamId,
},
);
const { pixelsUrl } = useConfig();
const config = useConfig();
const pixelsUrl = config?.pixelsUrl;
const hostUrl = pixelsUrl || PIXELS_URL;
const { data, isLoading } = usePixelQuery(pixelId);
const [slug, setSlug] = useState(generateId());
@@ -2,9 +2,8 @@ import { Row } from '@umami/react-zen';
import { IconLabel } from '@/components/common/IconLabel';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useMessages, usePixel, useSlug } from '@/components/hooks';
import { ExternalLink, Grid2x2 } from '@/components/icons';
import { PixelShareButton } from './PixelShareButton';
import { useMessages, useNavigation, usePixel, useSlug } from '@/components/hooks';
import { Edit, ExternalLink, Grid2x2 } from '@/components/icons';
export function PixelHeader({ showActions = true }: { showActions?: boolean }) {
const pixel = usePixel();
@@ -18,11 +17,14 @@ export function PixelHeader({ showActions = true }: { showActions?: boolean }) {
function PixelHeaderActions({ pixelId, slug }: { pixelId: string; slug: string }) {
const { t, labels } = useMessages();
const { renderUrl } = useNavigation();
const { getSlugUrl } = useSlug('pixel');
return (
<Row alignItems="center" gap="3">
<PixelShareButton pixelId={pixelId} />
<LinkButton href={renderUrl(`/pixels/${pixelId}/edit`, false)}>
<IconLabel icon={<Edit />} label={t(labels.edit)} />
</LinkButton>
<LinkButton href={getSlugUrl(slug)} target="_blank" prefetch={false} asAnchor>
<IconLabel icon={<ExternalLink />} label={t(labels.view)} />
</LinkButton>
@@ -1,14 +0,0 @@
import { useMessages } from '@/components/hooks';
import { Share } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { PixelShareDialog } from './PixelShareDialog';
export function PixelShareButton({ pixelId }: { pixelId: string }) {
const { t, labels } = useMessages();
return (
<DialogButton icon={<Share />} label={t(labels.share)} title={null} width="900px">
<PixelShareDialog pixelId={pixelId} />
</DialogButton>
);
}
@@ -7,19 +7,19 @@ import { Plus } from '@/components/icons';
import { SimpleShareCreateForm } from '@/components/share/SimpleShareCreateForm';
import { SimpleSharesTable } from '@/components/share/SimpleSharesTable';
export function PixelShareDialog({ pixelId }: { pixelId: string }) {
export function PixelShareForm({ pixelId }: { pixelId: string }) {
const { data, error, isLoading } = usePixelSharesQuery({ pixelId });
const shares = data?.data || [];
const hasShares = shares.length > 0;
return (
<LoadingPanel data={data} isLoading={isLoading} error={error}>
<PixelShareDialogContent pixelId={pixelId} hasShares={hasShares} shares={shares} />
<PixelShareFormContent pixelId={pixelId} hasShares={hasShares} shares={shares} />
</LoadingPanel>
);
}
function PixelShareDialogContent({
function PixelShareFormContent({
pixelId,
hasShares,
shares,
@@ -0,0 +1,46 @@
'use client';
import { Column } from '@umami/react-zen';
import Link from 'next/link';
import { PixelEditForm } from '@/app/(main)/pixels/PixelEditForm';
import { PixelProvider } from '@/app/(main)/pixels/PixelProvider';
import { PixelShareForm } from '@/app/(main)/pixels/[pixelId]/PixelShareForm';
import { Panel } from '@/components/common/Panel';
import { IconLabel } from '@/components/common/IconLabel';
import { PageHeader } from '@/components/common/PageHeader';
import { useMessages, useNavigation, usePixel } from '@/components/hooks';
import { ArrowLeft, Grid2x2 } from '@/components/icons';
export function PixelEditPage({ pixelId }: { pixelId: string }) {
return (
<PixelProvider pixelId={pixelId}>
<Column margin="2">
<PixelEditHeader />
<Column gap="6">
<Panel>
<PixelEditForm pixelId={pixelId} />
</Panel>
<Panel>
<PixelShareForm pixelId={pixelId} />
</Panel>
</Column>
</Column>
</PixelProvider>
);
}
function PixelEditHeader() {
const pixel = usePixel();
const { t, labels } = useMessages();
const { renderUrl } = useNavigation();
return (
<>
<Column marginTop="6">
<Link href={renderUrl(`/pixels/${pixel.id}`)}>
<IconLabel icon={<ArrowLeft />} label={t(labels.pixel)} />
</Link>
</Column>
<PageHeader title={pixel.name} icon={<Grid2x2 />} />
</>
);
}
@@ -0,0 +1,18 @@
import type { Metadata } from 'next';
import { getPixel } from '@/queries/prisma';
import { PixelEditPage } from './PixelEditPage';
export default async function ({ params }: { params: Promise<{ pixelId: string }> }) {
const { pixelId } = await params;
const pixel = await getPixel(pixelId);
if (!pixel || pixel.deletedAt) {
return null;
}
return <PixelEditPage pixelId={pixelId} />;
}
export const metadata: Metadata = {
title: 'Edit Pixel',
};
@@ -0,0 +1,14 @@
import { useMessages } from '@/components/hooks';
import { Edit } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { SimpleShareEditForm } from './SimpleShareEditForm';
export function SimpleShareEditButton({ shareId }: { shareId: string }) {
const { t, labels } = useMessages();
return (
<DialogButton icon={<Edit />} title={t(labels.share)} variant="quiet" width="600px">
{({ close }) => <SimpleShareEditForm shareId={shareId} onClose={close} />}
</DialogButton>
);
}
@@ -0,0 +1,104 @@
import {
Button,
Column,
Form,
FormField,
FormSubmitButton,
Label,
Loading,
Row,
TextField,
} from '@umami/react-zen';
import { useEffect, useState } from 'react';
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
export function SimpleShareEditForm({
shareId,
onSave,
onClose,
}: {
shareId: string;
onSave?: () => void;
onClose?: () => void;
}) {
const { t, labels, getErrorMessage } = useMessages();
const config = useConfig();
const { get, post } = useApi();
const { touch } = useModified();
const { modified } = useModified('shares');
const [share, setShare] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<any>(null);
const getUrl = (slug: string) => {
return `${config?.cloudMode ? process.env.cloudUrl : window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
};
useEffect(() => {
const loadShare = async () => {
setIsLoading(true);
try {
const data = await get(`/share/id/${shareId}`);
setShare(data);
} finally {
setIsLoading(false);
}
};
loadShare();
}, [get, modified, shareId]);
const handleSubmit = async (data: { name: string }) => {
setIsPending(true);
setError(null);
try {
await post(`/share/id/${shareId}`, {
name: data.name,
slug: share.slug,
parameters: share.parameters || {},
});
touch('shares');
onSave?.();
onClose?.();
} catch (e) {
setError(e);
} finally {
setIsPending(false);
}
};
if (isLoading) {
return <Loading placement="absolute" />;
}
return (
<Form
onSubmit={handleSubmit}
error={getErrorMessage(error)}
defaultValues={{ name: share?.name || '' }}
>
<Column gap="6">
<Column>
<Label>{t(labels.shareUrl)}</Label>
<TextField value={getUrl(share?.slug || '')} isReadOnly allowCopy />
</Column>
<FormField label={t(labels.name)} name="name" rules={{ required: t(labels.required) }}>
<TextField autoComplete="off" autoFocus />
</FormField>
<Row justifyContent="flex-end" paddingTop="3" gap="3">
{onClose && (
<Button isDisabled={isPending} onPress={onClose}>
{t(labels.cancel)}
</Button>
)}
<FormSubmitButton variant="primary" isDisabled={isPending}>
{t(labels.save)}
</FormSubmitButton>
</Row>
</Column>
</Form>
);
}
+5 -2
View File
@@ -4,10 +4,12 @@ import { CopyButton } from '@/components/common/CopyButton';
import { DateDistance } from '@/components/common/DateDistance';
import { ExternalLink } from '@/components/common/ExternalLink';
import { useConfig, useMessages, useMobile } from '@/components/hooks';
import { SimpleShareEditButton } from './SimpleShareEditButton';
export function SimpleSharesTable(props: DataTableProps) {
const { t, labels } = useMessages();
const { cloudMode } = useConfig();
const config = useConfig();
const cloudMode = config?.cloudMode;
const { isMobile } = useMobile();
const getUrl = (slug: string) => {
@@ -40,9 +42,10 @@ export function SimpleSharesTable(props: DataTableProps) {
<DataColumn id="created" label={t(labels.created)}>
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
</DataColumn>
<DataColumn id="action" align="end" width="60px">
<DataColumn id="action" align="end" width="100px">
{({ id, slug }: any) => (
<Row>
<SimpleShareEditButton shareId={id} />
<ShareDeleteButton shareId={id} slug={slug} />
</Row>
)}