diff --git a/src/app/(main)/links/[linkId]/LinkHeader.tsx b/src/app/(main)/links/[linkId]/LinkHeader.tsx
index 5ca713c58..9765dea76 100644
--- a/src/app/(main)/links/[linkId]/LinkHeader.tsx
+++ b/src/app/(main)/links/[linkId]/LinkHeader.tsx
@@ -1,19 +1,31 @@
+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';
-export function LinkHeader() {
- const { t, labels } = useMessages();
- const { getSlugUrl } = useSlug('link');
+export function LinkHeader({ showActions = true }: { showActions?: boolean }) {
const link = useLink();
return (
}>
-
- } label={t(labels.view)} />
-
+ {showActions && link.id && }
);
}
+
+function LinkHeaderActions({ linkId, slug }: { linkId: string; slug: string }) {
+ const { t, labels } = useMessages();
+ const { getSlugUrl } = useSlug('link');
+
+ return (
+
+
+
+ } label={t(labels.view)} />
+
+
+ );
+}
diff --git a/src/app/(main)/links/[linkId]/LinkPage.tsx b/src/app/(main)/links/[linkId]/LinkPage.tsx
index ddacf08fc..5d10fa82b 100644
--- a/src/app/(main)/links/[linkId]/LinkPage.tsx
+++ b/src/app/(main)/links/[linkId]/LinkPage.tsx
@@ -12,13 +12,19 @@ import { Panel } from '@/components/common/Panel';
const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
-export function LinkPage({ linkId }: { linkId: string }) {
+export function LinkPage({
+ linkId,
+ showHeaderActions = true,
+}: {
+ linkId: string;
+ showHeaderActions?: boolean;
+}) {
return (
-
+
diff --git a/src/app/(main)/links/[linkId]/LinkShareButton.tsx b/src/app/(main)/links/[linkId]/LinkShareButton.tsx
new file mode 100644
index 000000000..4c3084ed5
--- /dev/null
+++ b/src/app/(main)/links/[linkId]/LinkShareButton.tsx
@@ -0,0 +1,14 @@
+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 (
+ } label={t(labels.share)} title={null} width="900px">
+
+
+ );
+}
diff --git a/src/app/(main)/links/[linkId]/LinkShareDialog.tsx b/src/app/(main)/links/[linkId]/LinkShareDialog.tsx
new file mode 100644
index 000000000..3bf55ebf5
--- /dev/null
+++ b/src/app/(main)/links/[linkId]/LinkShareDialog.tsx
@@ -0,0 +1,62 @@
+import { Button, Column, Heading, Row, Text } from '@umami/react-zen';
+import { useState } from 'react';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { IconLabel } from '@/components/common/IconLabel';
+import { useLinkSharesQuery, useMessages } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+import { SimpleShareCreateForm } from '@/components/share/SimpleShareCreateForm';
+import { SimpleSharesTable } from '@/components/share/SimpleSharesTable';
+
+export function LinkShareDialog({ linkId }: { linkId: string }) {
+ const { data, error, isLoading } = useLinkSharesQuery({ linkId });
+ const shares = data?.data || [];
+ const hasShares = shares.length > 0;
+
+ return (
+
+
+
+ );
+}
+
+function LinkShareDialogContent({
+ linkId,
+ hasShares,
+ shares,
+}: {
+ linkId: string;
+ hasShares: boolean;
+ shares: any[];
+}) {
+ const { t, labels, messages } = useMessages();
+ const [isCreating, setIsCreating] = useState(false);
+ const showCreateForm = !hasShares || isCreating;
+
+ return (
+
+
+ {t(labels.share)}
+ {hasShares && !isCreating && (
+
+ )}
+
+ {showCreateForm && (
+ setIsCreating(false)}
+ onCancel={hasShares ? () => setIsCreating(false) : undefined}
+ />
+ )}
+ {hasShares ? (
+ <>
+ {t(messages.shareUrl)}
+
+ >
+ ) : (
+ !showCreateForm && {t(messages.noDataAvailable)}
+ )}
+
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx
index 26afc811e..89da44483 100644
--- a/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx
+++ b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx
@@ -1,19 +1,31 @@
+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';
-export function PixelHeader() {
- const { t, labels } = useMessages();
- const { getSlugUrl } = useSlug('pixel');
+export function PixelHeader({ showActions = true }: { showActions?: boolean }) {
const pixel = usePixel();
return (
}>
-
- } label={t(labels.view)} />
-
+ {showActions && pixel.id && }
);
}
+
+function PixelHeaderActions({ pixelId, slug }: { pixelId: string; slug: string }) {
+ const { t, labels } = useMessages();
+ const { getSlugUrl } = useSlug('pixel');
+
+ return (
+
+
+
+ } label={t(labels.view)} />
+
+
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx
index 7a4ae9d79..e8d605d24 100644
--- a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx
+++ b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx
@@ -12,13 +12,19 @@ import { Panel } from '@/components/common/Panel';
const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
-export function PixelPage({ pixelId }: { pixelId: string }) {
+export function PixelPage({
+ pixelId,
+ showHeaderActions = true,
+}: {
+ pixelId: string;
+ showHeaderActions?: boolean;
+}) {
return (
-
+
diff --git a/src/app/(main)/pixels/[pixelId]/PixelShareButton.tsx b/src/app/(main)/pixels/[pixelId]/PixelShareButton.tsx
new file mode 100644
index 000000000..69ffd0adf
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelShareButton.tsx
@@ -0,0 +1,14 @@
+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 (
+ } label={t(labels.share)} title={null} width="900px">
+
+
+ );
+}
diff --git a/src/app/(main)/pixels/[pixelId]/PixelShareDialog.tsx b/src/app/(main)/pixels/[pixelId]/PixelShareDialog.tsx
new file mode 100644
index 000000000..58d6b37ca
--- /dev/null
+++ b/src/app/(main)/pixels/[pixelId]/PixelShareDialog.tsx
@@ -0,0 +1,62 @@
+import { Button, Column, Heading, Row, Text } from '@umami/react-zen';
+import { useState } from 'react';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { IconLabel } from '@/components/common/IconLabel';
+import { useMessages, usePixelSharesQuery } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+import { SimpleShareCreateForm } from '@/components/share/SimpleShareCreateForm';
+import { SimpleSharesTable } from '@/components/share/SimpleSharesTable';
+
+export function PixelShareDialog({ pixelId }: { pixelId: string }) {
+ const { data, error, isLoading } = usePixelSharesQuery({ pixelId });
+ const shares = data?.data || [];
+ const hasShares = shares.length > 0;
+
+ return (
+
+
+
+ );
+}
+
+function PixelShareDialogContent({
+ pixelId,
+ hasShares,
+ shares,
+}: {
+ pixelId: string;
+ hasShares: boolean;
+ shares: any[];
+}) {
+ const { t, labels, messages } = useMessages();
+ const [isCreating, setIsCreating] = useState(false);
+ const showCreateForm = !hasShares || isCreating;
+
+ return (
+
+
+ {t(labels.share)}
+ {hasShares && !isCreating && (
+
+ )}
+
+ {showCreateForm && (
+ setIsCreating(false)}
+ onCancel={hasShares ? () => setIsCreating(false) : undefined}
+ />
+ )}
+ {hasShares ? (
+ <>
+ {t(messages.shareUrl)}
+
+ >
+ ) : (
+ !showCreateForm && {t(messages.noDataAvailable)}
+ )}
+
+ );
+}
diff --git a/src/app/api/links/[linkId]/shares/route.ts b/src/app/api/links/[linkId]/shares/route.ts
new file mode 100644
index 000000000..0ed8516e6
--- /dev/null
+++ b/src/app/api/links/[linkId]/shares/route.ts
@@ -0,0 +1,73 @@
+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 { filterParams, pagingParams } from '@/lib/schema';
+import { canUpdateLink, canViewLink } from '@/permissions';
+import { createShare, getSharesByEntityId } from '@/queries/prisma';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ linkId: string }> },
+) {
+ const schema = z.object({
+ ...filterParams,
+ ...pagingParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { linkId } = await params;
+ const { page, pageSize, search } = query;
+
+ if (!(await canViewLink(auth, linkId))) {
+ return unauthorized();
+ }
+
+ const data = await getSharesByEntityId(linkId, {
+ page,
+ pageSize,
+ search,
+ });
+
+ return json(data);
+}
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ linkId: string }> },
+) {
+ const schema = z.object({
+ name: z.string().max(200),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { linkId } = await params;
+ const { name } = body;
+
+ if (!(await canUpdateLink(auth, linkId))) {
+ return unauthorized();
+ }
+
+ const share = await createShare({
+ id: uuid(),
+ entityId: linkId,
+ shareType: ENTITY_TYPE.link,
+ name,
+ slug: getRandomChars(16),
+ parameters: {},
+ });
+
+ return json(share);
+}
diff --git a/src/app/api/pixels/[pixelId]/shares/route.ts b/src/app/api/pixels/[pixelId]/shares/route.ts
new file mode 100644
index 000000000..8b688cd7c
--- /dev/null
+++ b/src/app/api/pixels/[pixelId]/shares/route.ts
@@ -0,0 +1,73 @@
+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 { filterParams, pagingParams } from '@/lib/schema';
+import { canUpdatePixel, canViewPixel } from '@/permissions';
+import { createShare, getSharesByEntityId } from '@/queries/prisma';
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ pixelId: string }> },
+) {
+ const schema = z.object({
+ ...filterParams,
+ ...pagingParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { pixelId } = await params;
+ const { page, pageSize, search } = query;
+
+ if (!(await canViewPixel(auth, pixelId))) {
+ return unauthorized();
+ }
+
+ const data = await getSharesByEntityId(pixelId, {
+ page,
+ pageSize,
+ search,
+ });
+
+ return json(data);
+}
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ pixelId: string }> },
+) {
+ const schema = z.object({
+ name: z.string().max(200),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ const { pixelId } = await params;
+ const { name } = body;
+
+ if (!(await canUpdatePixel(auth, pixelId))) {
+ return unauthorized();
+ }
+
+ const share = await createShare({
+ id: uuid(),
+ entityId: pixelId,
+ shareType: ENTITY_TYPE.pixel,
+ name,
+ slug: getRandomChars(16),
+ parameters: {},
+ });
+
+ return json(share);
+}
diff --git a/src/app/api/share/[slug]/route.ts b/src/app/api/share/[slug]/route.ts
index fa7f99343..a237a8f17 100644
--- a/src/app/api/share/[slug]/route.ts
+++ b/src/app/api/share/[slug]/route.ts
@@ -6,17 +6,17 @@ import prisma from '@/lib/prisma';
import redis from '@/lib/redis';
import { json, notFound } from '@/lib/response';
import type { WhiteLabel } from '@/lib/types';
-import { getBoard, getShareByCode, getWebsite } from '@/queries/prisma';
+import { getBoard, getLink, getPixel, getShareByCode, getWebsite } from '@/queries/prisma';
-async function getAccountId(website: { userId?: string; teamId?: string }): Promise {
- if (website.userId) {
- return website.userId;
+async function getAccountId(entity: { userId?: string; teamId?: string }): Promise {
+ if (entity.userId) {
+ return entity.userId;
}
- if (website.teamId) {
+ if (entity.teamId) {
const teamOwner = await prisma.client.teamUser.findFirst({
where: {
- teamId: website.teamId,
+ teamId: entity.teamId,
role: ROLES.teamOwner,
},
select: {
@@ -73,22 +73,46 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
return json(data);
}
- const website = await getWebsite(share.entityId);
-
- if (!website) {
- return notFound();
- }
-
+ let entity: { userId?: string; teamId?: string } | null = null;
const data: Record = {
shareId: share.id,
shareType: share.shareType,
- websiteId: share.entityId,
parameters: share.parameters,
};
+ if (share.shareType === ENTITY_TYPE.website) {
+ entity = await getWebsite(share.entityId);
+
+ if (!entity) {
+ return notFound();
+ }
+
+ data.websiteId = share.entityId;
+ } else if (share.shareType === ENTITY_TYPE.pixel) {
+ entity = await getPixel(share.entityId);
+
+ if (!entity) {
+ return notFound();
+ }
+
+ data.websiteId = share.entityId;
+ data.pixelId = share.entityId;
+ } else if (share.shareType === ENTITY_TYPE.link) {
+ entity = await getLink(share.entityId);
+
+ if (!entity) {
+ return notFound();
+ }
+
+ data.websiteId = share.entityId;
+ data.linkId = share.entityId;
+ } else {
+ return notFound();
+ }
+
data.token = createToken(data, secret());
- const accountId = await getAccountId(website);
+ const accountId = await getAccountId(entity);
if (accountId) {
const whiteLabel = await getWhiteLabel(accountId);
diff --git a/src/app/share/ShareProvider.tsx b/src/app/share/ShareProvider.tsx
index 4d148d6a6..a09a3776e 100644
--- a/src/app/share/ShareProvider.tsx
+++ b/src/app/share/ShareProvider.tsx
@@ -13,6 +13,8 @@ export interface ShareData {
websiteId?: string;
websiteIds?: string[];
boardId?: string;
+ pixelId?: string;
+ linkId?: string;
parameters: any;
token: string;
whiteLabel?: WhiteLabel;
@@ -54,12 +56,13 @@ export function ShareProvider({ slug, children }: { slug: string; children: Reac
const pathname = usePathname();
const path = getSharePath(pathname);
const isBoardShare = share?.shareType === ENTITY_TYPE.board;
+ const isWebsiteShare = share?.shareType === ENTITY_TYPE.website;
- const allowedSections = !isBoardShare && share?.parameters
+ const allowedSections = isWebsiteShare && share?.parameters
? ALL_SECTION_IDS.filter(id => share.parameters[id] !== false)
: [];
- const shouldRedirect = !isBoardShare &&
+ const shouldRedirect = isWebsiteShare &&
allowedSections.length === 1 &&
allowedSections[0] !== 'overview' &&
(path === undefined || path === '' || path === 'overview');
diff --git a/src/app/share/[slug]/[[...path]]/SharePage.tsx b/src/app/share/[slug]/[[...path]]/SharePage.tsx
index 83a85317d..88b8967a9 100644
--- a/src/app/share/[slug]/[[...path]]/SharePage.tsx
+++ b/src/app/share/[slug]/[[...path]]/SharePage.tsx
@@ -2,6 +2,8 @@
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
+import { LinkPage } from '@/app/(main)/links/[linkId]/LinkPage';
+import { PixelPage } from '@/app/(main)/pixels/[pixelId]/PixelPage';
import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage';
import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage';
import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage';
@@ -59,7 +61,7 @@ export function SharePage() {
const { setTheme } = useTheme();
const pathname = usePathname();
const path = getSharePath(pathname);
- const { websiteId, boardId, parameters = {}, shareType } = share;
+ const { websiteId, boardId, pixelId, linkId, parameters = {}, shareType } = share;
useEffect(() => {
const url = new URL(window?.location?.href);
@@ -81,6 +83,14 @@ export function SharePage() {
);
}
+ if (shareType === ENTITY_TYPE.pixel && pixelId) {
+ return ;
+ }
+
+ if (shareType === ENTITY_TYPE.link && linkId) {
+ return ;
+ }
+
// Check if the requested path is allowed
const pageKey = path || '';
const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false;
diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts
index 2d844edf4..5a1d17422 100644
--- a/src/components/hooks/index.ts
+++ b/src/components/hooks/index.ts
@@ -22,9 +22,11 @@ export * from './queries/useEventDataPropertiesQuery';
export * from './queries/useEventDataQuery';
export * from './queries/useEventDataValuesQuery';
export * from './queries/useLinkQuery';
+export * from './queries/useLinkSharesQuery';
export * from './queries/useLinksQuery';
export * from './queries/useLoginQuery';
export * from './queries/usePixelQuery';
+export * from './queries/usePixelSharesQuery';
export * from './queries/usePixelsQuery';
export * from './queries/useRealtimeQuery';
export * from './queries/useReplayQuery';
diff --git a/src/components/hooks/queries/useLinkSharesQuery.ts b/src/components/hooks/queries/useLinkSharesQuery.ts
new file mode 100644
index 000000000..990952d05
--- /dev/null
+++ b/src/components/hooks/queries/useLinkSharesQuery.ts
@@ -0,0 +1,20 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useLinkSharesQuery(
+ { linkId }: { linkId: string },
+ options?: ReactQueryOptions,
+) {
+ const { modified } = useModified('shares');
+ const { get } = useApi();
+
+ return usePagedQuery({
+ queryKey: ['linkShares', { linkId, modified }],
+ queryFn: pageParams => {
+ return get(`/links/${linkId}/shares`, pageParams);
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/usePixelSharesQuery.ts b/src/components/hooks/queries/usePixelSharesQuery.ts
new file mode 100644
index 000000000..aacd07a7a
--- /dev/null
+++ b/src/components/hooks/queries/usePixelSharesQuery.ts
@@ -0,0 +1,20 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function usePixelSharesQuery(
+ { pixelId }: { pixelId: string },
+ options?: ReactQueryOptions,
+) {
+ const { modified } = useModified('shares');
+ const { get } = useApi();
+
+ return usePagedQuery({
+ queryKey: ['pixelShares', { pixelId, modified }],
+ queryFn: pageParams => {
+ return get(`/pixels/${pixelId}/shares`, pageParams);
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/useSlug.ts b/src/components/hooks/useSlug.ts
index f795dfeb3..b4f585af0 100644
--- a/src/components/hooks/useSlug.ts
+++ b/src/components/hooks/useSlug.ts
@@ -2,7 +2,9 @@ import { useConfig } from '@/components/hooks/useConfig';
import { LINKS_URL, PIXELS_URL } from '@/lib/constants';
export function useSlug(type: 'link' | 'pixel') {
- const { linksUrl, pixelsUrl } = useConfig();
+ const config = useConfig();
+ const linksUrl = config?.linksUrl;
+ const pixelsUrl = config?.pixelsUrl;
const hostUrl = type === 'link' ? linksUrl || LINKS_URL : pixelsUrl || PIXELS_URL;
diff --git a/src/components/share/SimpleShareCreateForm.tsx b/src/components/share/SimpleShareCreateForm.tsx
new file mode 100644
index 000000000..9ab850bcb
--- /dev/null
+++ b/src/components/share/SimpleShareCreateForm.tsx
@@ -0,0 +1,57 @@
+import { Button, Column, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen';
+import { useState } from 'react';
+import { useApi, useMessages, useModified } from '@/components/hooks';
+
+export function SimpleShareCreateForm({
+ createPath,
+ onSave,
+ onCancel,
+}: {
+ createPath: string;
+ onSave?: () => void;
+ onCancel?: () => void;
+}) {
+ const { post } = useApi();
+ const { touch } = useModified();
+ const { t, labels, getErrorMessage } = useMessages();
+ const [isPending, setIsPending] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleSubmit = async (data: { name: string }) => {
+ setIsPending(true);
+ setError(null);
+
+ try {
+ await post(createPath, {
+ name: data.name,
+ });
+
+ touch('shares');
+ onSave?.();
+ } catch (e) {
+ setError(e);
+ } finally {
+ setIsPending(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/share/SimpleSharesTable.tsx b/src/components/share/SimpleSharesTable.tsx
new file mode 100644
index 000000000..b938e6412
--- /dev/null
+++ b/src/components/share/SimpleSharesTable.tsx
@@ -0,0 +1,52 @@
+import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
+import { ShareDeleteButton } from '@/app/(main)/websites/[websiteId]/settings/ShareDeleteButton';
+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';
+
+export function SimpleSharesTable(props: DataTableProps) {
+ const { t, labels } = useMessages();
+ const { cloudMode } = useConfig();
+ const { isMobile } = useMobile();
+
+ const getUrl = (slug: string) => {
+ if (cloudMode) {
+ return `${process.env.cloudUrl}/share/${slug}`;
+ }
+
+ return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
+ };
+
+ return (
+
+
+ {({ name }: any) => name}
+
+
+ {({ slug }: any) => {
+ const url = getUrl(slug);
+
+ return (
+
+
+ {isMobile ? slug : url}
+
+
+
+ );
+ }}
+
+
+ {(row: any) => }
+
+
+ {({ id, slug }: any) => (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 7b174d9f5..f2e49dab6 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -23,6 +23,8 @@ export interface Auth {
websiteId?: string;
websiteIds?: string[];
boardId?: string;
+ pixelId?: string;
+ linkId?: string;
};
}
diff --git a/src/permissions/link.ts b/src/permissions/link.ts
index 8dd1d7c6b..e77676998 100644
--- a/src/permissions/link.ts
+++ b/src/permissions/link.ts
@@ -3,15 +3,19 @@ import { PERMISSIONS } from '@/lib/constants';
import type { Auth } from '@/lib/types';
import { getLink, getTeamUser } from '@/queries/prisma';
-export async function canViewLink({ user }: Auth, linkId: string) {
- if (!user) {
- return false;
+export async function canViewLink({ user, shareToken }: Auth, linkId: string) {
+ if (user?.isAdmin) {
+ return true;
}
- if (user.isAdmin) {
+ if (shareToken?.linkId === linkId || shareToken?.websiteId === linkId) {
return true;
}
+ if (!user) {
+ return false;
+ }
+
const link = await getLink(linkId);
if (link.userId) {
diff --git a/src/permissions/pixel.ts b/src/permissions/pixel.ts
index 14b69acef..bf7c7aed0 100644
--- a/src/permissions/pixel.ts
+++ b/src/permissions/pixel.ts
@@ -3,15 +3,19 @@ import { PERMISSIONS } from '@/lib/constants';
import type { Auth } from '@/lib/types';
import { getPixel, getTeamUser } from '@/queries/prisma';
-export async function canViewPixel({ user }: Auth, pixelId: string) {
- if (!user) {
- return false;
+export async function canViewPixel({ user, shareToken }: Auth, pixelId: string) {
+ if (user?.isAdmin) {
+ return true;
}
- if (user.isAdmin) {
+ if (shareToken?.pixelId === pixelId || shareToken?.websiteId === pixelId) {
return true;
}
+ if (!user) {
+ return false;
+ }
+
const pixel = await getPixel(pixelId);
if (pixel.userId) {
diff --git a/src/permissions/website.ts b/src/permissions/website.ts
index 69d8c6f2d..7d4efe1d3 100644
--- a/src/permissions/website.ts
+++ b/src/permissions/website.ts
@@ -9,7 +9,12 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
return true;
}
- if (shareToken?.websiteId === websiteId || shareToken?.websiteIds?.includes(websiteId)) {
+ if (
+ shareToken?.websiteId === websiteId ||
+ shareToken?.pixelId === websiteId ||
+ shareToken?.linkId === websiteId ||
+ shareToken?.websiteIds?.includes(websiteId)
+ ) {
return true;
}