diff --git a/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx b/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx
index e8e5ff78f..7e716103b 100644
--- a/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardSharesTable.tsx
@@ -1,58 +1,18 @@
-import { DataColumn, DataTable, Icon, type DataTableProps, Row, Text } from '@umami/react-zen';
+import { Row, type DataTableProps } from '@umami/react-zen';
import { ShareDeleteButton } from '@/app/(main)/websites/[websiteId]/settings/ShareDeleteButton';
-import { CopyButton } from '@/components/common/CopyButton';
-import Link from '@/components/common/Link';
-import { useConfig, useMessages, useMobile } from '@/components/hooks';
-import { ExternalLink } from '@/components/icons';
import { SimpleShareEditButton } from '@/components/share/SimpleShareEditButton';
+import { SharedSharesTable } from '@/components/share/SharedSharesTable';
export function BoardSharesTable(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}
-
-
-
-
-
-
-
-
- );
- }}
-
-
- {({ id, slug }: any) => (
+ (
)}
-
-
+ />
);
}
diff --git a/src/app/(main)/websites/[websiteId]/settings/SharesTable.test.tsx b/src/app/(main)/websites/[websiteId]/settings/SharesTable.test.tsx
new file mode 100644
index 000000000..c0b41ceda
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/settings/SharesTable.test.tsx
@@ -0,0 +1,54 @@
+import { describe, expect, test, vi } from 'vitest';
+import { render, screen } from '@/test/render';
+import { SharesTable } from './SharesTable';
+
+vi.mock('@/components/hooks', () => ({
+ useConfig: () => ({ cloudMode: false }),
+ useMessages: () => ({
+ t: (value: string) => value,
+ labels: {
+ name: 'Name',
+ shareUrl: 'Share URL',
+ edit: 'Edit',
+ delete: 'Delete',
+ share: 'Share',
+ confirm: 'Confirm',
+ },
+ messages: { confirmRemove: 'Remove {target}' },
+ getErrorMessage: () => null,
+ }),
+ useMobile: () => ({ isMobile: false }),
+ useDeleteQuery: () => ({
+ mutateAsync: vi.fn(),
+ isPending: false,
+ error: null,
+ }),
+ useModified: () => ({
+ touch: vi.fn(),
+ }),
+}));
+
+vi.mock('@/components/common/ConfirmationForm', () => ({
+ ConfirmationForm: () =>
confirmation
,
+}));
+
+vi.mock('./ShareEditForm', () => ({
+ ShareEditForm: () => share edit form
,
+}));
+
+describe('SharesTable', () => {
+ test('renders the share url content in the table row with actions', () => {
+ render(, {
+ route: 'https://app.umami.is/settings',
+ });
+
+ const link = screen.getByRole('link', { name: 'http://localhost:3000/share/world' });
+
+ expect(link).toHaveAttribute('href', 'http://localhost:3000/share/world');
+ expect(screen.getByText('hello')).toBeInTheDocument();
+ expect(screen.getByText('Share URL')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /copy url/i })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /edit/i })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument();
+ });
+});
diff --git a/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx b/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
index 32760325b..946fd963e 100644
--- a/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
@@ -1,59 +1,133 @@
-import { CopyButton } from '@/components/common/CopyButton';
-import Link from '@/components/common/Link';
-import { useConfig, useMessages, useMobile } from '@/components/hooks';
-import { ExternalLink } from '@/components/icons';
-import { DataColumn, DataTable, type DataTableProps, Icon, Row, Text } from '@umami/react-zen';
-import { ShareDeleteButton } from './ShareDeleteButton';
-import { ShareEditButton } from './ShareEditButton';
+import {
+ Button,
+ Dialog,
+ Icon,
+ Modal,
+ Row,
+} from '@umami/react-zen';
+import { useState } from 'react';
+import { ControlledDialog } from '@/components/common/ControlledDialog';
+import { ConfirmationForm } from '@/components/common/ConfirmationForm';
+import { useDeleteQuery, useMessages, useMobile, useModified } from '@/components/hooks';
+import { Edit, Trash } from 'lucide-react';
+import { SharedSharesTable } from '@/components/share/SharedSharesTable';
+import { ShareEditForm } from './ShareEditForm';
-export function SharesTable(props: DataTableProps) {
+export function SharesTable({ data = [] }: { data?: any[] }) {
const { t, labels } = useMessages();
- const { cloudMode } = useConfig();
const { isMobile } = useMobile();
+ const [editShareId, setEditShareId] = useState(null);
+ const [deleteShare, setDeleteShare] = useState<{ id: string; slug: string } | null>(null);
- const getUrl = (slug: string) => {
- if (cloudMode) {
- return `${process.env.cloudUrl}/share/${slug}`;
+ return (
+ <>
+ (
+
+
+
+
+ )}
+ />
+
+ !isOpen && setEditShareId(null)}>
+
+
+
+ {deleteShare && (
+ setDeleteShare(null)}
+ title={t(labels.confirm)}
+ deleteLabel={t(labels.delete)}
+ />
+ )}
+ >
+ );
+}
+
+function ShareDeleteDialog({
+ share,
+ onClose,
+ title,
+ deleteLabel,
+}: {
+ share: { id: string; slug: string } | null;
+ onClose: () => void;
+ title: string;
+ deleteLabel: string;
+}) {
+ const { t, messages, getErrorMessage } = useMessages();
+ const { mutateAsync, isPending, error } = useDeleteQuery(`/share/id/${share.id}`);
+ const { touch } = useModified();
+
+ const handleConfirm = async () => {
+ if (!share) {
+ return;
}
- return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
+
+ await mutateAsync(null, {
+ onSuccess: () => {
+ touch('shares');
+ onClose();
+ },
+ });
};
return (
-
-
- {({ name }: any) => name}
-
-
- {({ slug }: any) => {
- const url = getUrl(slug);
-
- return (
-
-
-
-
- {isMobile ? slug : url}
-
-
-
-
-
-
-
-
- );
- }}
-
-
- {({ id, slug }: any) => {
- return (
-
-
-
-
- );
- }}
-
-
+
+ !isOpen && onClose()}>
+
+
+
);
}
diff --git a/src/components/share/SharedSharesTable.tsx b/src/components/share/SharedSharesTable.tsx
new file mode 100644
index 000000000..d89918270
--- /dev/null
+++ b/src/components/share/SharedSharesTable.tsx
@@ -0,0 +1,124 @@
+import { Icon, Row, Text } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { CopyButton } from '@/components/common/CopyButton';
+import { useConfig, useMessages, useMobile } from '@/components/hooks';
+import { ExternalLink } from '@/components/icons';
+
+const desktopGridTemplateColumns = 'minmax(180px, 1fr) minmax(0, 2fr) auto';
+const rowBorder = '1px solid var(--border-default)';
+
+export interface ShareRow {
+ id: string;
+ name: string;
+ slug: string;
+}
+
+export function SharedSharesTable({
+ data = [],
+ renderActions,
+}: {
+ data?: ShareRow[];
+ renderActions: (row: ShareRow) => ReactNode;
+}) {
+ const { t, labels } = useMessages();
+ const { cloudMode } = useConfig();
+ const { isMobile } = useMobile();
+
+ const getUrl = (slug: string) => {
+ if (cloudMode) {
+ return `${process.env.cloudUrl}/share/${slug}`;
+ }
+
+ const origin = typeof window !== 'undefined' ? window.location.origin : '';
+
+ return `${origin}${process.env.basePath || ''}/share/${slug}`;
+ };
+
+ return (
+
+ {!isMobile && (
+
+
{t(labels.name)}
+
{t(labels.shareUrl)}
+
+
+ )}
+
+ {data.map(row => {
+ const url = getUrl(row.slug);
+
+ if (isMobile) {
+ return (
+
+
{row.name}
+
+
+
+
+ {renderActions(row)}
+
+
+ );
+ }
+
+ return (
+
+ {row.name}
+
+ {renderActions(row)}
+
+ );
+ })}
+
+
+ );
+}
+
+function ShareUrlCell({ url, value }: { url: string; value: string }) {
+ return (
+
+
+
+
+ {value}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/share/SimpleSharesTable.tsx b/src/components/share/SimpleSharesTable.tsx
index b79c16513..e8d6f44b4 100644
--- a/src/components/share/SimpleSharesTable.tsx
+++ b/src/components/share/SimpleSharesTable.tsx
@@ -1,59 +1,18 @@
-import { DataColumn, DataTable, Icon, type DataTableProps, Row, Text } from '@umami/react-zen';
import { ShareDeleteButton } from '@/app/(main)/websites/[websiteId]/settings/ShareDeleteButton';
-import { CopyButton } from '@/components/common/CopyButton';
-import Link from '@/components/common/Link';
-import { useConfig, useMessages, useMobile } from '@/components/hooks';
-import { ExternalLink } from '@/components/icons';
+import { Row, type DataTableProps } from '@umami/react-zen';
import { SimpleShareEditButton } from './SimpleShareEditButton';
+import { SharedSharesTable } from './SharedSharesTable';
export function SimpleSharesTable(props: DataTableProps) {
- const { t, labels } = useMessages();
- const config = useConfig();
- const cloudMode = config?.cloudMode;
- 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}
-
-
-
-
-
-
-
-
- );
- }}
-
-
- {({ id, slug }: any) => (
+ (
)}
-
-
+ />
);
}