Pixel editing.

This commit is contained in:
Mike Cao 2025-08-15 13:04:13 -07:00
parent eabdd18604
commit d130242a0a
23 changed files with 72 additions and 49 deletions

View file

@ -7,19 +7,17 @@ import { useApi, useMessages, useModified } from '@/components/hooks';
export function PixelDeleteButton({
pixelId,
websiteId,
name,
onSave,
}: {
pixelId: string;
websiteId: string;
name: string;
onSave?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const { del, useMutation } = useApi();
const { mutate, isPending, error } = useMutation({
mutationFn: () => del(`/websites/${websiteId}/pixels/${pixelId}`),
mutationFn: () => del(`/pixels/${pixelId}`),
});
const { touch } = useModified();

View file

@ -9,7 +9,7 @@ export function PixelEditButton({ pixelId }: { pixelId: string }) {
return (
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
<Dialog title={formatMessage(labels.pixel)} style={{ width: 800 }}>
<Dialog title={formatMessage(labels.pixel)} style={{ width: 800, minHeight: 300 }}>
{({ close }) => {
return <PixelEditForm pixelId={pixelId} onClose={close} />;
}}

View file

@ -5,7 +5,6 @@ import {
Row,
TextField,
Button,
Text,
Label,
Column,
Icon,
@ -16,6 +15,8 @@ import { useMessages } from '@/components/hooks';
import { Refresh } from '@/components/icons';
import { getRandomChars } from '@/lib/crypto';
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
import { useEffect, useState } from 'react';
import { PIXELS_URL } from '@/lib/constants';
const generateId = () => getRandomChars(9);
@ -31,29 +32,48 @@ export function PixelEditForm({
onClose?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const { mutate, error, isPending } = useUpdateQuery('/pixels', { id: pixelId, teamId });
const { pixelDomain } = useConfig();
const { mutate, error, isPending, touch } = useUpdateQuery(
pixelId ? `/pixels/${pixelId}` : '/pixels',
{
id: pixelId,
teamId,
},
);
const { pixelsUrl } = useConfig();
const hostUrl = pixelsUrl || PIXELS_URL;
const { data, isLoading } = usePixelQuery(pixelId);
const [slug, setSlug] = useState(generateId());
const handleSubmit = async (data: any) => {
mutate(data, {
onSuccess: async () => {
touch('pixels');
onSave?.();
onClose?.();
},
});
};
if (pixelId && !isLoading) {
const handleSlug = () => {
const slug = generateId();
setSlug(slug);
return slug;
};
useEffect(() => {
if (data) {
setSlug(data.slug);
}
}, [data]);
if (pixelId && isLoading) {
return <Loading position="page" />;
}
return (
<Form
onSubmit={handleSubmit}
error={error?.message}
defaultValues={{ slug: generateId(), ...data }}
>
<Form onSubmit={handleSubmit} error={error?.message} defaultValues={{ slug, ...data }}>
{({ setValue }) => {
return (
<>
@ -65,22 +85,29 @@ export function PixelEditForm({
<TextField autoComplete="off" />
</FormField>
<FormField
name="slug"
rules={{
required: formatMessage(labels.required),
}}
style={{ display: 'none' }}
>
<input type="hidden" />
</FormField>
<Column>
<Label>{formatMessage(labels.pixel)}</Label>
<Label>{formatMessage(labels.link)}</Label>
<Row alignItems="center" gap>
<Text>{pixelDomain || window.location.origin}/</Text>
<FormField
name="slug"
rules={{
required: formatMessage(labels.required),
}}
<TextField
value={`${hostUrl}/${slug}`}
autoComplete="off"
isReadOnly
allowCopy
style={{ width: '100%' }}
>
<TextField autoComplete="off" isReadOnly />
</FormField>
/>
<Button
variant="quiet"
onPress={() => setValue('slug', generateId(), { shouldDirty: true })}
onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}
>
<Icon>
<Refresh />

View file

@ -1,15 +1,14 @@
import Link from 'next/link';
import { DataTable, DataColumn, Row } from '@umami/react-zen';
import { useConfig, useMessages, useNavigation } from '@/components/hooks';
import { useConfig, useMessages } from '@/components/hooks';
import { Empty } from '@/components/common/Empty';
import { DateDistance } from '@/components/common/DateDistance';
import { PixelEditButton } from './PixelEditButton';
import { PixelDeleteButton } from './PixelDeleteButton';
import { PIXELS_URL } from '@/lib/constants';
import { ExternalLink } from '@/components/common/ExternalLink';
export function PixelsTable({ data = [] }) {
const { formatMessage, labels } = useMessages();
const { websiteId } = useNavigation();
const { pixelsUrl } = useConfig();
const hostUrl = pixelsUrl || PIXELS_URL;
@ -23,7 +22,7 @@ export function PixelsTable({ data = [] }) {
<DataColumn id="url" label="URL">
{({ slug }: any) => {
const url = `${hostUrl}/${slug}`;
return <Link href={url}>{url}</Link>;
return <ExternalLink href={url}>{url}</ExternalLink>;
}}
</DataColumn>
<DataColumn id="created" label={formatMessage(labels.created)}>
@ -36,7 +35,7 @@ export function PixelsTable({ data = [] }) {
return (
<Row>
<PixelEditButton pixelId={id} />
<PixelDeleteButton pixelId={id} websiteId={websiteId} name={name} />
<PixelDeleteButton pixelId={id} name={name} />
</Row>
);
}}

View file

@ -2,7 +2,7 @@
import { createContext, ReactNode } from 'react';
import { useWebsiteQuery } from '@/components/hooks';
import { Loading } from '@umami/react-zen';
import { Website } from '@prisma/client';
import { Website } from '@/generated/prisma/client';
export const WebsiteContext = createContext<Website>(null);

View file

@ -25,7 +25,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ pixe
export async function POST(request: Request, { params }: { params: Promise<{ pixelId: string }> }) {
const schema = z.object({
name: z.string(),
url: z.string().url(),
slug: z.string().min(8),
});
@ -36,14 +35,14 @@ export async function POST(request: Request, { params }: { params: Promise<{ pix
}
const { pixelId } = await params;
const { name, domain, shareId } = body;
const { name, slug } = body;
if (!(await canUpdatePixel(auth, pixelId))) {
return unauthorized();
}
try {
const pixel = await updatePixel(pixelId, { name, domain, shareId });
const pixel = await updatePixel(pixelId, { name, slug });
return Response.json(pixel);
} catch (e: any) {

View file

@ -4,7 +4,7 @@ import { json, unauthorized } from '@/lib/response';
import { uuid } from '@/lib/crypto';
import { getQueryFilters, parseRequest } from '@/lib/request';
import { pagingParams, searchParams } from '@/lib/schema';
import { createPixel, getUserLinks } from '@/queries';
import { createPixel, getUserPixels } from '@/queries';
export async function GET(request: Request) {
const schema = z.object({
@ -20,7 +20,7 @@ export async function GET(request: Request) {
const filters = await getQueryFilters(query);
const links = await getUserLinks(auth.user.id, filters);
const links = await getUserPixels(auth.user.id, filters);
return json(links);
}

View file

@ -2,7 +2,7 @@ import Link from 'next/link';
import { Icon, Row, Text } from '@umami/react-zen';
import { ExternalLink as LinkIcon } from '@/components/icons';
export function ExternalLink({ href, children, ...props }: Icon) {
export function ExternalLink({ href, children, ...props }) {
return (
<Row alignItems="center" overflow="hidden" gap>
<Text title={href} truncate>

View file

@ -8,7 +8,7 @@ export function usePixelQuery(pixelId: string) {
return useQuery({
queryKey: ['pixel', { pixelId, modified }],
queryFn: () => {
return get(`/pixel/${pixelId}`);
return get(`/pixels/${pixelId}`);
},
enabled: !!pixelId,
});

View file

@ -1,4 +1,4 @@
import { Website, Session } from '@prisma/client';
import { Website, Session } from '@/generated/prisma/client';
import redis from '@/lib/redis';
import { getWebsiteSession, getWebsite } from '@/queries';

View file

@ -1,4 +1,4 @@
import { Prisma, Link } from '@prisma/client';
import { Prisma, Link } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
import { PageResult, QueryFilters } from '@/lib/types';

View file

@ -1,4 +1,4 @@
import { Prisma, Pixel } from '@prisma/client';
import { Prisma, Pixel } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
import { PageResult, QueryFilters } from '@/lib/types';

View file

@ -1,4 +1,4 @@
import { Prisma, Report } from '@prisma/client';
import { Prisma, Report } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
import { PageResult, QueryFilters } from '@/lib/types';
import ReportFindManyArgs = Prisma.ReportFindManyArgs;

View file

@ -1,5 +1,5 @@
import prisma from '@/lib/prisma';
import { Prisma, Segment } from '@prisma/client';
import { Prisma, Segment } from '@/generated/prisma/client';
async function findSegment(criteria: Prisma.SegmentFindUniqueArgs): Promise<Segment> {
return prisma.client.Segment.findUnique(criteria);

View file

@ -1,4 +1,4 @@
import { Prisma, Team } from '@prisma/client';
import { Prisma, Team } from '@/generated/prisma/client';
import { ROLES } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
import prisma from '@/lib/prisma';

View file

@ -1,4 +1,4 @@
import { Prisma, TeamUser } from '@prisma/client';
import { Prisma, TeamUser } from '@/generated/prisma/client';
import { uuid } from '@/lib/crypto';
import prisma from '@/lib/prisma';
import { PageResult, QueryFilters } from '@/lib/types';

View file

@ -1,4 +1,4 @@
import { Prisma, User } from '@prisma/client';
import { Prisma, User } from '@/generated/prisma/client';
import { ROLES } from '@/lib/constants';
import prisma from '@/lib/prisma';
import { PageResult, Role, QueryFilters } from '@/lib/types';

View file

@ -1,4 +1,4 @@
import { Prisma, Website } from '@prisma/client';
import { Prisma, Website } from '@/generated/prisma/client';
import redis from '@/lib/redis';
import prisma from '@/lib/prisma';
import { PageResult, QueryFilters } from '@/lib/types';

View file

@ -1,4 +1,4 @@
import { EventData } from '@prisma/client';
import { EventData } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';

View file

@ -1,4 +1,4 @@
import { Prisma } from '@prisma/client';
import { Prisma } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
export async function createSession(

View file

@ -1,5 +1,5 @@
import { Auth } from '@/lib/types';
import { Report } from '@prisma/client';
import { Report } from '@/generated/prisma/client';
import { canViewWebsite } from './website';
export async function canViewReport(auth: Auth, report: Report) {