From 2fe062c49e9463ea58a237155855af2e76c4f5a9 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 4 Aug 2026 07:36:47 -0700 Subject: [PATCH 1/2] Fix dialog context error in language popover by using DialogTrigger Co-Authored-By: Claude Fable 5 --- src/components/input/LanguageButton.tsx | 39 +++++++++++++------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/components/input/LanguageButton.tsx b/src/components/input/LanguageButton.tsx index bf5c9ff26..53fca3063 100644 --- a/src/components/input/LanguageButton.tsx +++ b/src/components/input/LanguageButton.tsx @@ -1,4 +1,4 @@ -import { Button, Dialog, Grid, Icon, MenuTrigger, Popover, Text } from '@umami/react-zen'; +import { Button, Dialog, DialogTrigger, Grid, Icon, Popover, Text } from '@umami/react-zen'; import { Globe } from 'lucide-react'; import { useLocale } from '@/components/hooks'; import { languages } from '@/lib/lang'; @@ -7,12 +7,13 @@ export function LanguageButton() { const { locale, saveLocale } = useLocale(); const items = Object.keys(languages).map(key => ({ ...languages[key], value: key })); - function handleSelect(value: string) { + function handleSelect(value: string, close: () => void) { saveLocale(value); + close(); } return ( - + - - {items.map(({ value, label }) => { - return ( - - ); - })} - + {({ close }) => ( + + {items.map(({ value, label }) => { + return ( + + ); + })} + + )} - + ); } From 41e9ad38f11f3fbe91bd0c42af02382050f7fc4f Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 4 Aug 2026 07:49:21 -0700 Subject: [PATCH 2/2] fix sharetable UI/CSS issues --- .../boards/[boardId]/BoardSharesTable.tsx | 52 +----- .../[websiteId]/settings/SharesTable.test.tsx | 54 ++++++ .../[websiteId]/settings/SharesTable.tsx | 172 +++++++++++++----- src/components/share/SharedSharesTable.tsx | 124 +++++++++++++ src/components/share/SimpleSharesTable.tsx | 53 +----- 5 files changed, 313 insertions(+), 142 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/settings/SharesTable.test.tsx create mode 100644 src/components/share/SharedSharesTable.tsx 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)}> + + {editShareId && ( + 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()}> + + {share && ( + {chunks}, + })} + isLoading={isPending} + error={getErrorMessage(error)} + onConfirm={handleConfirm} + onClose={onClose} + buttonLabel={deleteLabel} + buttonVariant="danger" + /> + )} + + + ); } 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) => ( + ( )} - - + /> ); }