mirror of
https://github.com/umami-software/umami.git
synced 2026-02-17 02:55:38 +01:00
Pixel editing.
This commit is contained in:
parent
eabdd18604
commit
d130242a0a
23 changed files with 72 additions and 49 deletions
|
|
@ -7,19 +7,17 @@ import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||||
|
|
||||||
export function PixelDeleteButton({
|
export function PixelDeleteButton({
|
||||||
pixelId,
|
pixelId,
|
||||||
websiteId,
|
|
||||||
name,
|
name,
|
||||||
onSave,
|
onSave,
|
||||||
}: {
|
}: {
|
||||||
pixelId: string;
|
pixelId: string;
|
||||||
websiteId: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { del, useMutation } = useApi();
|
const { del, useMutation } = useApi();
|
||||||
const { mutate, isPending, error } = useMutation({
|
const { mutate, isPending, error } = useMutation({
|
||||||
mutationFn: () => del(`/websites/${websiteId}/pixels/${pixelId}`),
|
mutationFn: () => del(`/pixels/${pixelId}`),
|
||||||
});
|
});
|
||||||
const { touch } = useModified();
|
const { touch } = useModified();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export function PixelEditButton({ pixelId }: { pixelId: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
|
<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 }) => {
|
{({ close }) => {
|
||||||
return <PixelEditForm pixelId={pixelId} onClose={close} />;
|
return <PixelEditForm pixelId={pixelId} onClose={close} />;
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
Row,
|
Row,
|
||||||
TextField,
|
TextField,
|
||||||
Button,
|
Button,
|
||||||
Text,
|
|
||||||
Label,
|
Label,
|
||||||
Column,
|
Column,
|
||||||
Icon,
|
Icon,
|
||||||
|
|
@ -16,6 +15,8 @@ import { useMessages } from '@/components/hooks';
|
||||||
import { Refresh } from '@/components/icons';
|
import { Refresh } from '@/components/icons';
|
||||||
import { getRandomChars } from '@/lib/crypto';
|
import { getRandomChars } from '@/lib/crypto';
|
||||||
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { PIXELS_URL } from '@/lib/constants';
|
||||||
|
|
||||||
const generateId = () => getRandomChars(9);
|
const generateId = () => getRandomChars(9);
|
||||||
|
|
||||||
|
|
@ -31,29 +32,48 @@ export function PixelEditForm({
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { mutate, error, isPending } = useUpdateQuery('/pixels', { id: pixelId, teamId });
|
const { mutate, error, isPending, touch } = useUpdateQuery(
|
||||||
const { pixelDomain } = useConfig();
|
pixelId ? `/pixels/${pixelId}` : '/pixels',
|
||||||
|
{
|
||||||
|
id: pixelId,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { pixelsUrl } = useConfig();
|
||||||
|
const hostUrl = pixelsUrl || PIXELS_URL;
|
||||||
const { data, isLoading } = usePixelQuery(pixelId);
|
const { data, isLoading } = usePixelQuery(pixelId);
|
||||||
|
const [slug, setSlug] = useState(generateId());
|
||||||
|
|
||||||
const handleSubmit = async (data: any) => {
|
const handleSubmit = async (data: any) => {
|
||||||
mutate(data, {
|
mutate(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
touch('pixels');
|
||||||
onSave?.();
|
onSave?.();
|
||||||
onClose?.();
|
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 <Loading position="page" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form onSubmit={handleSubmit} error={error?.message} defaultValues={{ slug, ...data }}>
|
||||||
onSubmit={handleSubmit}
|
|
||||||
error={error?.message}
|
|
||||||
defaultValues={{ slug: generateId(), ...data }}
|
|
||||||
>
|
|
||||||
{({ setValue }) => {
|
{({ setValue }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -65,22 +85,29 @@ export function PixelEditForm({
|
||||||
<TextField autoComplete="off" />
|
<TextField autoComplete="off" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name="slug"
|
||||||
|
rules={{
|
||||||
|
required: formatMessage(labels.required),
|
||||||
|
}}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
>
|
||||||
|
<input type="hidden" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
<Column>
|
<Column>
|
||||||
<Label>{formatMessage(labels.pixel)}</Label>
|
<Label>{formatMessage(labels.link)}</Label>
|
||||||
<Row alignItems="center" gap>
|
<Row alignItems="center" gap>
|
||||||
<Text>{pixelDomain || window.location.origin}/</Text>
|
<TextField
|
||||||
<FormField
|
value={`${hostUrl}/${slug}`}
|
||||||
name="slug"
|
autoComplete="off"
|
||||||
rules={{
|
isReadOnly
|
||||||
required: formatMessage(labels.required),
|
allowCopy
|
||||||
}}
|
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
>
|
/>
|
||||||
<TextField autoComplete="off" isReadOnly />
|
|
||||||
</FormField>
|
|
||||||
<Button
|
<Button
|
||||||
variant="quiet"
|
variant="quiet"
|
||||||
onPress={() => setValue('slug', generateId(), { shouldDirty: true })}
|
onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}
|
||||||
>
|
>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Refresh />
|
<Refresh />
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import Link from 'next/link';
|
|
||||||
import { DataTable, DataColumn, Row } from '@umami/react-zen';
|
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 { Empty } from '@/components/common/Empty';
|
||||||
import { DateDistance } from '@/components/common/DateDistance';
|
import { DateDistance } from '@/components/common/DateDistance';
|
||||||
import { PixelEditButton } from './PixelEditButton';
|
import { PixelEditButton } from './PixelEditButton';
|
||||||
import { PixelDeleteButton } from './PixelDeleteButton';
|
import { PixelDeleteButton } from './PixelDeleteButton';
|
||||||
import { PIXELS_URL } from '@/lib/constants';
|
import { PIXELS_URL } from '@/lib/constants';
|
||||||
|
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||||
|
|
||||||
export function PixelsTable({ data = [] }) {
|
export function PixelsTable({ data = [] }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { websiteId } = useNavigation();
|
|
||||||
const { pixelsUrl } = useConfig();
|
const { pixelsUrl } = useConfig();
|
||||||
const hostUrl = pixelsUrl || PIXELS_URL;
|
const hostUrl = pixelsUrl || PIXELS_URL;
|
||||||
|
|
||||||
|
|
@ -23,7 +22,7 @@ export function PixelsTable({ data = [] }) {
|
||||||
<DataColumn id="url" label="URL">
|
<DataColumn id="url" label="URL">
|
||||||
{({ slug }: any) => {
|
{({ slug }: any) => {
|
||||||
const url = `${hostUrl}/${slug}`;
|
const url = `${hostUrl}/${slug}`;
|
||||||
return <Link href={url}>{url}</Link>;
|
return <ExternalLink href={url}>{url}</ExternalLink>;
|
||||||
}}
|
}}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
<DataColumn id="created" label={formatMessage(labels.created)}>
|
<DataColumn id="created" label={formatMessage(labels.created)}>
|
||||||
|
|
@ -36,7 +35,7 @@ export function PixelsTable({ data = [] }) {
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<PixelEditButton pixelId={id} />
|
<PixelEditButton pixelId={id} />
|
||||||
<PixelDeleteButton pixelId={id} websiteId={websiteId} name={name} />
|
<PixelDeleteButton pixelId={id} name={name} />
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { createContext, ReactNode } from 'react';
|
import { createContext, ReactNode } from 'react';
|
||||||
import { useWebsiteQuery } from '@/components/hooks';
|
import { useWebsiteQuery } from '@/components/hooks';
|
||||||
import { Loading } from '@umami/react-zen';
|
import { Loading } from '@umami/react-zen';
|
||||||
import { Website } from '@prisma/client';
|
import { Website } from '@/generated/prisma/client';
|
||||||
|
|
||||||
export const WebsiteContext = createContext<Website>(null);
|
export const WebsiteContext = createContext<Website>(null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }> }) {
|
export async function POST(request: Request, { params }: { params: Promise<{ pixelId: string }> }) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
url: z.string().url(),
|
|
||||||
slug: z.string().min(8),
|
slug: z.string().min(8),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -36,14 +35,14 @@ export async function POST(request: Request, { params }: { params: Promise<{ pix
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pixelId } = await params;
|
const { pixelId } = await params;
|
||||||
const { name, domain, shareId } = body;
|
const { name, slug } = body;
|
||||||
|
|
||||||
if (!(await canUpdatePixel(auth, pixelId))) {
|
if (!(await canUpdatePixel(auth, pixelId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pixel = await updatePixel(pixelId, { name, domain, shareId });
|
const pixel = await updatePixel(pixelId, { name, slug });
|
||||||
|
|
||||||
return Response.json(pixel);
|
return Response.json(pixel);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { json, unauthorized } from '@/lib/response';
|
||||||
import { uuid } from '@/lib/crypto';
|
import { uuid } from '@/lib/crypto';
|
||||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { pagingParams, searchParams } from '@/lib/schema';
|
import { pagingParams, searchParams } from '@/lib/schema';
|
||||||
import { createPixel, getUserLinks } from '@/queries';
|
import { createPixel, getUserPixels } from '@/queries';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
|
|
@ -20,7 +20,7 @@ export async function GET(request: Request) {
|
||||||
|
|
||||||
const filters = await getQueryFilters(query);
|
const filters = await getQueryFilters(query);
|
||||||
|
|
||||||
const links = await getUserLinks(auth.user.id, filters);
|
const links = await getUserPixels(auth.user.id, filters);
|
||||||
|
|
||||||
return json(links);
|
return json(links);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import Link from 'next/link';
|
||||||
import { Icon, Row, Text } from '@umami/react-zen';
|
import { Icon, Row, Text } from '@umami/react-zen';
|
||||||
import { ExternalLink as LinkIcon } from '@/components/icons';
|
import { ExternalLink as LinkIcon } from '@/components/icons';
|
||||||
|
|
||||||
export function ExternalLink({ href, children, ...props }: Icon) {
|
export function ExternalLink({ href, children, ...props }) {
|
||||||
return (
|
return (
|
||||||
<Row alignItems="center" overflow="hidden" gap>
|
<Row alignItems="center" overflow="hidden" gap>
|
||||||
<Text title={href} truncate>
|
<Text title={href} truncate>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export function usePixelQuery(pixelId: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['pixel', { pixelId, modified }],
|
queryKey: ['pixel', { pixelId, modified }],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
return get(`/pixel/${pixelId}`);
|
return get(`/pixels/${pixelId}`);
|
||||||
},
|
},
|
||||||
enabled: !!pixelId,
|
enabled: !!pixelId,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Website, Session } from '@prisma/client';
|
import { Website, Session } from '@/generated/prisma/client';
|
||||||
import redis from '@/lib/redis';
|
import redis from '@/lib/redis';
|
||||||
import { getWebsiteSession, getWebsite } from '@/queries';
|
import { getWebsiteSession, getWebsite } from '@/queries';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Prisma, Link } from '@prisma/client';
|
import { Prisma, Link } from '@/generated/prisma/client';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import { PageResult, QueryFilters } from '@/lib/types';
|
import { PageResult, QueryFilters } from '@/lib/types';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Prisma, Pixel } from '@prisma/client';
|
import { Prisma, Pixel } from '@/generated/prisma/client';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import { PageResult, QueryFilters } from '@/lib/types';
|
import { PageResult, QueryFilters } from '@/lib/types';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Prisma, Report } from '@prisma/client';
|
import { Prisma, Report } from '@/generated/prisma/client';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import { PageResult, QueryFilters } from '@/lib/types';
|
import { PageResult, QueryFilters } from '@/lib/types';
|
||||||
import ReportFindManyArgs = Prisma.ReportFindManyArgs;
|
import ReportFindManyArgs = Prisma.ReportFindManyArgs;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import prisma from '@/lib/prisma';
|
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> {
|
async function findSegment(criteria: Prisma.SegmentFindUniqueArgs): Promise<Segment> {
|
||||||
return prisma.client.Segment.findUnique(criteria);
|
return prisma.client.Segment.findUnique(criteria);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Prisma, Team } from '@prisma/client';
|
import { Prisma, Team } from '@/generated/prisma/client';
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
import { uuid } from '@/lib/crypto';
|
import { uuid } from '@/lib/crypto';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Prisma, TeamUser } from '@prisma/client';
|
import { Prisma, TeamUser } from '@/generated/prisma/client';
|
||||||
import { uuid } from '@/lib/crypto';
|
import { uuid } from '@/lib/crypto';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import { PageResult, QueryFilters } from '@/lib/types';
|
import { PageResult, QueryFilters } from '@/lib/types';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Prisma, User } from '@prisma/client';
|
import { Prisma, User } from '@/generated/prisma/client';
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import { PageResult, Role, QueryFilters } from '@/lib/types';
|
import { PageResult, Role, QueryFilters } from '@/lib/types';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Prisma, Website } from '@prisma/client';
|
import { Prisma, Website } from '@/generated/prisma/client';
|
||||||
import redis from '@/lib/redis';
|
import redis from '@/lib/redis';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import { PageResult, QueryFilters } from '@/lib/types';
|
import { PageResult, QueryFilters } from '@/lib/types';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { EventData } from '@prisma/client';
|
import { EventData } from '@/generated/prisma/client';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import clickhouse from '@/lib/clickhouse';
|
import clickhouse from '@/lib/clickhouse';
|
||||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@/generated/prisma/client';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
|
|
||||||
export async function createSession(
|
export async function createSession(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Auth } from '@/lib/types';
|
import { Auth } from '@/lib/types';
|
||||||
import { Report } from '@prisma/client';
|
import { Report } from '@/generated/prisma/client';
|
||||||
import { canViewWebsite } from './website';
|
import { canViewWebsite } from './website';
|
||||||
|
|
||||||
export async function canViewReport(auth: Auth, report: Report) {
|
export async function canViewReport(auth: Auth, report: Report) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue