mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
implement website share functionality using share table
- Add support for multiple share URLs per website with server-generated slugs - Create shares API endpoint for listing and creating website shares - Add SharesTable, ShareEditButton, ShareDeleteButton components - Move share management to website settings, remove header share button - Remove shareId from website update API (now uses separate share table) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3a498333a6
commit
0eb598c817
13 changed files with 374 additions and 127 deletions
|
|
@ -1,11 +1,9 @@
|
|||
import { Icon, Row, Text } from '@umami/react-zen';
|
||||
import { WebsiteShareForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteShareForm';
|
||||
import { Favicon } from '@/components/common/Favicon';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
|
||||
import { Edit, Share } from '@/components/icons';
|
||||
import { DialogButton } from '@/components/input/DialogButton';
|
||||
import { Edit } from '@/components/icons';
|
||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||
|
||||
export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
||||
|
|
@ -29,29 +27,14 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
|||
<ActiveUsers websiteId={website.id} />
|
||||
|
||||
{showActions && (
|
||||
<Row alignItems="center" gap>
|
||||
<ShareButton websiteId={website.id} shareId={website.shareId} />
|
||||
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
||||
<Icon>
|
||||
<Edit />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.edit)}</Text>
|
||||
</LinkButton>
|
||||
</Row>
|
||||
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
||||
<Icon>
|
||||
<Edit />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.edit)}</Text>
|
||||
</LinkButton>
|
||||
)}
|
||||
</Row>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
const ShareButton = ({ websiteId, shareId }) => {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<DialogButton icon={<Share />} label={formatMessage(labels.share)} width="800px">
|
||||
{({ close }) => {
|
||||
return <WebsiteShareForm websiteId={websiteId} shareId={shareId} onClose={close} />;
|
||||
}}
|
||||
</DialogButton>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
||||
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
|
||||
import { Trash } from '@/components/icons';
|
||||
import { DialogButton } from '@/components/input/DialogButton';
|
||||
import { messages } from '@/components/messages';
|
||||
|
||||
export function ShareDeleteButton({
|
||||
shareId,
|
||||
slug,
|
||||
onSave,
|
||||
}: {
|
||||
shareId: string;
|
||||
slug: string;
|
||||
onSave?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
|
||||
const { mutateAsync, isPending, error } = useDeleteQuery(`/share/id/${shareId}`);
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleConfirm = async (close: () => void) => {
|
||||
await mutateAsync(null, {
|
||||
onSuccess: () => {
|
||||
touch('shares');
|
||||
onSave?.();
|
||||
close();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogButton
|
||||
icon={<Trash />}
|
||||
title={formatMessage(labels.confirm)}
|
||||
variant="quiet"
|
||||
width="400px"
|
||||
>
|
||||
{({ close }) => (
|
||||
<ConfirmationForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
{...messages.confirmRemove}
|
||||
values={{
|
||||
target: <b>{slug}</b>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isLoading={isPending}
|
||||
error={getErrorMessage(error)}
|
||||
onConfirm={handleConfirm.bind(null, close)}
|
||||
onClose={close}
|
||||
buttonLabel={formatMessage(labels.delete)}
|
||||
buttonVariant="danger"
|
||||
/>
|
||||
)}
|
||||
</DialogButton>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { useMessages } from '@/components/hooks';
|
||||
import { Edit } from '@/components/icons';
|
||||
import { DialogButton } from '@/components/input/DialogButton';
|
||||
import { ShareEditForm } from './ShareEditForm';
|
||||
|
||||
export function ShareEditButton({ shareId }: { shareId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<DialogButton icon={<Edit />} title={formatMessage(labels.share)} variant="quiet" width="600px">
|
||||
{({ close }) => {
|
||||
return <ShareEditForm shareId={shareId} onClose={close} />;
|
||||
}}
|
||||
</DialogButton>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
Button,
|
||||
Column,
|
||||
Form,
|
||||
FormSubmitButton,
|
||||
Label,
|
||||
Loading,
|
||||
Row,
|
||||
TextField,
|
||||
} from '@umami/react-zen';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
|
||||
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
||||
|
||||
export function ShareEditForm({
|
||||
shareId,
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
shareId: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
|
||||
const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(`/share/id/${shareId}`);
|
||||
const { cloudMode } = useConfig();
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified('shares');
|
||||
const [share, setShare] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const getUrl = (slug: string) => {
|
||||
if (cloudMode) {
|
||||
return `${process.env.cloudUrl}/share/${slug}`;
|
||||
}
|
||||
return `${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();
|
||||
}, [shareId, modified]);
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
await mutateAsync(
|
||||
{ slug: data.slug, parameters: share?.parameters || {} },
|
||||
{
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('shares');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading placement="absolute" />;
|
||||
}
|
||||
|
||||
const url = getUrl(share?.slug || '');
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
error={getErrorMessage(error)}
|
||||
defaultValues={{ slug: share?.slug }}
|
||||
>
|
||||
<Column gap>
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||
<TextField value={url} isReadOnly allowCopy />
|
||||
</Column>
|
||||
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||
{onClose && (
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
</Button>
|
||||
)}
|
||||
<FormSubmitButton>{formatMessage(labels.save)}</FormSubmitButton>
|
||||
</Row>
|
||||
</Column>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
46
src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
Normal file
46
src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||
import { useConfig, useMessages } from '@/components/hooks';
|
||||
import { ShareDeleteButton } from './ShareDeleteButton';
|
||||
import { ShareEditButton } from './ShareEditButton';
|
||||
|
||||
export function SharesTable(props: DataTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { cloudMode } = useConfig();
|
||||
|
||||
const getUrl = (slug: string) => {
|
||||
if (cloudMode) {
|
||||
return `${process.env.cloudUrl}/share/${slug}`;
|
||||
}
|
||||
return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTable {...props}>
|
||||
<DataColumn id="slug" label={formatMessage(labels.shareUrl)}>
|
||||
{({ slug }: any) => {
|
||||
const url = getUrl(slug);
|
||||
return (
|
||||
<ExternalLink href={url} prefetch={false}>
|
||||
{url}
|
||||
</ExternalLink>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="created" label={formatMessage(labels.created)} width="200px">
|
||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||
</DataColumn>
|
||||
<DataColumn id="action" align="end" width="100px">
|
||||
{({ id, slug }: any) => {
|
||||
return (
|
||||
<Row>
|
||||
<ShareEditButton shareId={id} />
|
||||
<ShareDeleteButton shareId={id} slug={slug} />
|
||||
</Row>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,14 +1,11 @@
|
|||
import { Column } from '@umami/react-zen';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useWebsite } from '@/components/hooks';
|
||||
import { WebsiteData } from './WebsiteData';
|
||||
import { WebsiteEditForm } from './WebsiteEditForm';
|
||||
import { WebsiteShareForm } from './WebsiteShareForm';
|
||||
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
|
||||
|
||||
export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) {
|
||||
const website = useWebsite();
|
||||
|
||||
return (
|
||||
<Column gap="6">
|
||||
<Panel>
|
||||
|
|
@ -18,7 +15,7 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal
|
|||
<WebsiteTrackingCode websiteId={websiteId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<WebsiteShareForm websiteId={websiteId} shareId={website.shareId} />
|
||||
<WebsiteShareForm websiteId={websiteId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<WebsiteData websiteId={websiteId} />
|
||||
|
|
|
|||
|
|
@ -1,93 +1,43 @@
|
|||
import {
|
||||
Button,
|
||||
Column,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormSubmitButton,
|
||||
IconLabel,
|
||||
Label,
|
||||
Row,
|
||||
Switch,
|
||||
TextField,
|
||||
} from '@umami/react-zen';
|
||||
import { RefreshCcw } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useConfig, useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
import { getRandomChars } from '@/lib/generate';
|
||||
|
||||
const generateId = () => getRandomChars(16);
|
||||
import { Button, Column, Heading, Row, Text } from '@umami/react-zen';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useApi, useMessages, useModified, useWebsiteSharesQuery } from '@/components/hooks';
|
||||
import { SharesTable } from './SharesTable';
|
||||
|
||||
export interface WebsiteShareFormProps {
|
||||
websiteId: string;
|
||||
shareId?: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
|
||||
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
|
||||
const [currentId, setCurrentId] = useState(shareId);
|
||||
const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
|
||||
const { cloudMode } = useConfig();
|
||||
export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { data, isLoading } = useWebsiteSharesQuery({ websiteId });
|
||||
const { post } = useApi();
|
||||
const { touch } = useModified();
|
||||
|
||||
const getUrl = (shareId: string) => {
|
||||
if (cloudMode) {
|
||||
return `${process.env.cloudUrl}/share/${shareId}`;
|
||||
}
|
||||
|
||||
return `${window?.location.origin}${process.env.basePath || ''}/share/${shareId}`;
|
||||
const handleCreate = async () => {
|
||||
await post(`/websites/${websiteId}/shares`, { parameters: {} });
|
||||
touch('shares');
|
||||
};
|
||||
|
||||
const url = getUrl(currentId);
|
||||
|
||||
const handleGenerate = () => {
|
||||
setCurrentId(generateId());
|
||||
};
|
||||
|
||||
const handleSwitch = () => {
|
||||
setCurrentId(currentId ? null : generateId());
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const data = {
|
||||
shareId: currentId,
|
||||
};
|
||||
await mutateAsync(data, {
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch(`website:${websiteId}`);
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
const shares = data?.data || [];
|
||||
const hasShares = shares.length > 0;
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSave} error={getErrorMessage(error)} values={{ url }}>
|
||||
<Column gap>
|
||||
<Switch isSelected={!!currentId} onChange={handleSwitch}>
|
||||
{formatMessage(labels.enableShareUrl)}
|
||||
</Switch>
|
||||
{currentId && (
|
||||
<Row alignItems="flex-end" gap>
|
||||
<Column flexGrow={1}>
|
||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||
<TextField value={url} isReadOnly allowCopy />
|
||||
</Column>
|
||||
<Column>
|
||||
<Button onPress={handleGenerate}>
|
||||
<IconLabel icon={<RefreshCcw />} label={formatMessage(labels.regenerate)} />
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
)}
|
||||
<FormButtons justifyContent="flex-end">
|
||||
<Row alignItems="center" gap>
|
||||
{onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>}
|
||||
<FormSubmitButton isDisabled={false}>{formatMessage(labels.save)}</FormSubmitButton>
|
||||
</Row>
|
||||
</FormButtons>
|
||||
</Column>
|
||||
</Form>
|
||||
<Column gap="4">
|
||||
<Row justifyContent="space-between" alignItems="center">
|
||||
<Heading>{formatMessage(labels.share)}</Heading>
|
||||
<Button variant="primary" onPress={handleCreate}>
|
||||
<Plus size={16} />
|
||||
<Text>{formatMessage(labels.add)}</Text>
|
||||
</Button>
|
||||
</Row>
|
||||
{hasShares ? (
|
||||
<>
|
||||
<Text>{formatMessage(messages.shareUrl)}</Text>
|
||||
<SharesTable data={shares} />
|
||||
</>
|
||||
) : (
|
||||
<Text color="muted">{formatMessage(messages.noDataAvailable)}</Text>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import z from 'zod';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { getRandomChars } from '@/lib/generate';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { anyObjectParam } from '@/lib/schema';
|
||||
|
|
@ -10,7 +11,7 @@ export async function POST(request: Request) {
|
|||
const schema = z.object({
|
||||
entityId: z.uuid(),
|
||||
shareType: z.coerce.number().int(),
|
||||
slug: z.string().max(100),
|
||||
slug: z.string().max(100).optional(),
|
||||
parameters: anyObjectParam,
|
||||
});
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ export async function POST(request: Request) {
|
|||
id: uuid(),
|
||||
entityId,
|
||||
shareType,
|
||||
slug,
|
||||
slug: slug || getRandomChars(16),
|
||||
parameters,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
import { SHARE_ID_REGEX } from '@/lib/constants';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
|
||||
import { json, ok, unauthorized } from '@/lib/response';
|
||||
import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions';
|
||||
import { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma';
|
||||
|
||||
|
|
@ -33,7 +32,6 @@ export async function POST(
|
|||
const schema = z.object({
|
||||
name: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
|
@ -43,23 +41,15 @@ export async function POST(
|
|||
}
|
||||
|
||||
const { websiteId } = await params;
|
||||
const { name, domain, shareId } = body;
|
||||
const { name, domain } = body;
|
||||
|
||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
try {
|
||||
const website = await updateWebsite(websiteId, { name, domain, shareId });
|
||||
const website = await updateWebsite(websiteId, { name, domain });
|
||||
|
||||
return Response.json(website);
|
||||
} catch (e: any) {
|
||||
if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('share_id')) {
|
||||
return badRequest({ message: 'That share ID is already taken.' });
|
||||
}
|
||||
|
||||
return serverError(e);
|
||||
}
|
||||
return Response.json(website);
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
|
|
|
|||
74
src/app/api/websites/[websiteId]/shares/route.ts
Normal file
74
src/app/api/websites/[websiteId]/shares/route.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { z } from 'zod';
|
||||
import { ENTITY_TYPE } from '@/lib/constants';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { getRandomChars } from '@/lib/generate';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { anyObjectParam, filterParams, pagingParams } from '@/lib/schema';
|
||||
import { canUpdateWebsite, canViewWebsite } from '@/permissions';
|
||||
import { createShare, getSharesByEntityId } from '@/queries/prisma';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ websiteId: string }> },
|
||||
) {
|
||||
const schema = z.object({
|
||||
...filterParams,
|
||||
...pagingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { websiteId } = await params;
|
||||
const { page, pageSize, search } = query;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getSharesByEntityId(websiteId, {
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ websiteId: string }> },
|
||||
) {
|
||||
const schema = z.object({
|
||||
parameters: anyObjectParam.optional(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { websiteId } = await params;
|
||||
const { parameters = {} } = body;
|
||||
|
||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const slug = getRandomChars(16);
|
||||
|
||||
const share = await createShare({
|
||||
id: uuid(),
|
||||
entityId: websiteId,
|
||||
shareType: ENTITY_TYPE.website,
|
||||
slug,
|
||||
parameters,
|
||||
});
|
||||
|
||||
return json(share);
|
||||
}
|
||||
|
|
@ -51,6 +51,7 @@ export * from './queries/useWebsiteSegmentsQuery';
|
|||
export * from './queries/useWebsiteSessionQuery';
|
||||
export * from './queries/useWebsiteSessionStatsQuery';
|
||||
export * from './queries/useWebsiteSessionsQuery';
|
||||
export * from './queries/useWebsiteSharesQuery';
|
||||
export * from './queries/useWebsiteStatsQuery';
|
||||
export * from './queries/useWebsitesQuery';
|
||||
export * from './queries/useWebsiteValuesQuery';
|
||||
|
|
|
|||
20
src/components/hooks/queries/useWebsiteSharesQuery.ts
Normal file
20
src/components/hooks/queries/useWebsiteSharesQuery.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import type { ReactQueryOptions } from '@/lib/types';
|
||||
import { useApi } from '../useApi';
|
||||
import { useModified } from '../useModified';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
|
||||
export function useWebsiteSharesQuery(
|
||||
{ websiteId }: { websiteId: string },
|
||||
options?: ReactQueryOptions,
|
||||
) {
|
||||
const { modified } = useModified('shares');
|
||||
const { get } = useApi();
|
||||
|
||||
return usePagedQuery({
|
||||
queryKey: ['websiteShares', { websiteId, modified }],
|
||||
queryFn: pageParams => {
|
||||
return get(`/websites/${websiteId}/shares`, pageParams);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import type { Prisma } from '@/generated/prisma/client';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
|
||||
export async function findShare(criteria: Prisma.ShareFindUniqueArgs) {
|
||||
return prisma.client.share.findUnique(criteria);
|
||||
}
|
||||
|
||||
export async function getShare(entityId: string) {
|
||||
export async function getShare(shareId: string) {
|
||||
return findShare({
|
||||
where: {
|
||||
id: entityId,
|
||||
id: shareId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -21,6 +22,23 @@ export async function getShareByCode(slug: string) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getSharesByEntityId(entityId: string, filters?: QueryFilters) {
|
||||
const { pagedQuery } = prisma;
|
||||
|
||||
return pagedQuery(
|
||||
'share',
|
||||
{
|
||||
where: {
|
||||
entityId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
},
|
||||
filters,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createShare(
|
||||
data: Prisma.ShareCreateInput | Prisma.ShareUncheckedCreateInput,
|
||||
) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue