mirror of
https://github.com/umami-software/umami.git
synced 2026-05-30 06:47:25 +00:00
Move pixel and link editing to dedicated pages
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -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',
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user