Skip to content

Feature: Batch‑delete websites (FR #3320) #3438 #3442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/1.bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ body:
- type: input
attributes:
label: Which Umami version are you using? (if relevant)
description: 'For example: Chrome, Edge, Firefox, etc'
description: 'For example: 2.18.0, 2.15.1, 1.39.0, etc'
- type: input
attributes:
label: Which browser are you using? (if relevant)
Expand Down
5 changes: 3 additions & 2 deletions scripts/data-migrations/convert-utm-clid-columns.sql
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ FROM (SELECT event_id, website_id, session_id,
(regexp_matches(url_query, '(?:[&?]|^)utm_medium=([^&]+)', 'i'))[1] AS utm_medium,
(regexp_matches(url_query, '(?:[&?]|^)utm_source=([^&]+)', 'i'))[1] AS utm_source,
(regexp_matches(url_query, '(?:[&?]|^)utm_term=([^&]+)', 'i'))[1] AS utm_term
FROM "website_event") url
FROM "website_event"
WHERE url_query IS NOT NULL) url
WHERE we.event_id = url.event_id
and we.session_id = url.session_id
and we.website_id = url.website_id;
Expand All @@ -45,4 +46,4 @@ SET fbclid = LEFT(SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[
utm_medium = LEFT(SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)utm_medium=[^&]+'), '=', -1), '&', 1), 255),
utm_source = LEFT(SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)utm_source=[^&]+'), '=', -1), '&', 1), 255),
utm_term = LEFT(SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)utm_term=[^&]+'), '=', -1), '&', 1), 255)
WHERE 1 = 1;
WHERE url_query IS NOT NULL;
14 changes: 11 additions & 3 deletions src/app/(main)/settings/users/[userId]/UserWebsites.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import WebsitesTable from '@/app/(main)/settings/websites/WebsitesTable';
import DataTable from '@/components/common/DataTable';
import { useWebsites } from '@/components/hooks';
import { useWebsites, useModified } from '@/components/hooks';

export function UserWebsites({ userId }) {
const queryResult = useWebsites({ userId });

const { touch } = useModified('websites');
return (
<DataTable queryResult={queryResult}>
{({ data }) => (
<WebsitesTable data={data} showActions={true} allowEdit={true} allowView={true} />
<WebsitesTable
data={data}
showActions={true}
allowEdit={true}
allowView={true}
updateChildren={() => {
touch();
}}
/>
)}
</DataTable>
);
Expand Down
6 changes: 3 additions & 3 deletions src/app/(main)/settings/websites/WebsitesDataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ReactNode } from 'react';
import WebsitesTable from '@/app/(main)/settings/websites/WebsitesTable';
import DataTable from '@/components/common/DataTable';
import { useWebsites } from '@/components/hooks';

import { useWebsites, useModified } from '@/components/hooks';
export function WebsitesDataTable({
teamId,
allowEdit = true,
Expand All @@ -17,7 +16,7 @@ export function WebsitesDataTable({
children?: ReactNode;
}) {
const queryResult = useWebsites({ teamId });

const { touch } = useModified('websites');
return (
<DataTable queryResult={queryResult} renderEmpty={() => children}>
{({ data }) => (
Expand All @@ -27,6 +26,7 @@ export function WebsitesDataTable({
showActions={showActions}
allowEdit={allowEdit}
allowView={allowView}
updateChildren={() => touch()}
/>
)}
</DataTable>
Expand Down
117 changes: 89 additions & 28 deletions src/app/(main)/settings/websites/WebsitesTable.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { ReactNode } from 'react';
import { Text, Icon, Icons, GridTable, GridColumn } from 'react-basics';
import { ReactNode, useState } from 'react';
import { Text, Icon, Icons, GridTable, GridColumn, Checkbox, Modal } from 'react-basics';
import { useMessages, useTeamUrl } from '@/components/hooks';
import WebsiteDeleteForm from './[websiteId]/WebsiteDeleteForm';
import LinkButton from '@/components/common/LinkButton';

export interface WebsitesTableProps {
data: any[];
showActions?: boolean;
allowEdit?: boolean;
allowView?: boolean;
teamId?: string;
children?: ReactNode;
updateChildren?: () => void;
}

export function WebsitesTable({
Expand All @@ -18,47 +19,107 @@ export function WebsitesTable({
allowEdit,
allowView,
children,
updateChildren,
}: WebsitesTableProps) {
const { formatMessage, labels } = useMessages();
const { renderTeamUrl } = useTeamUrl();
const [deleteIds, setDeleteIds] = useState<string[]>([]);
const [showConf, setShowConf] = useState(false);

if (!data?.length) {
return children;
}
const checked = (websiteId: string) => {
if (deleteIds.includes(websiteId)) {
setDeleteIds(deleteIds.filter(prev => prev !== websiteId));
} else {
setDeleteIds(prev => [...prev, websiteId]);
}
};

return (
<GridTable data={data}>
<GridColumn name="name" label={formatMessage(labels.name)} />
<GridColumn name="domain" label={formatMessage(labels.domain)} />
{showActions && (
<GridColumn name="action" label=" " alignment="end">
<>
<GridTable data={data}>
<GridColumn
width="40px"
name="delete"
label={
deleteIds.length > 0 ? (
<Icon
style={{ color: 'red' }}
onClick={() => {
setShowConf(true);
}}
size={'lg'}
>
<Icons.Trash />
</Icon>
) : (
''
)
}
>
{row => {
const { id: websiteId } = row;

return (
<>
{allowEdit && (
<LinkButton href={renderTeamUrl(`/settings/websites/${websiteId}`)}>
<Icon data-test="link-button-edit">
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</LinkButton>
)}
{allowView && (
<LinkButton href={renderTeamUrl(`/websites/${websiteId}`)}>
<Icon>
<Icons.ArrowRight />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</LinkButton>
)}
</>
<Checkbox
defaultChecked={false}
checked={deleteIds.includes(websiteId)}
onChange={() => {
checked(websiteId);
}}
value={websiteId}
/>
);
}}
</GridColumn>
<GridColumn name="name" label={formatMessage(labels.name)} />
<GridColumn name="domain" label={formatMessage(labels.domain)} />
{showActions && (
<GridColumn name="action" label=" " alignment="end">
{row => {
const { id: websiteId } = row;

return (
<>
{allowEdit && (
<LinkButton href={renderTeamUrl(`/settings/websites/${websiteId}`)}>
<Icon data-test="link-button-edit">
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</LinkButton>
)}
{allowView && (
<LinkButton href={renderTeamUrl(`/websites/${websiteId}`)}>
<Icon>
<Icons.ArrowRight />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</LinkButton>
)}
</>
);
}}
</GridColumn>
)}
</GridTable>
{showConf && (
<Modal title={formatMessage(labels.deleteWebsite)}>
{
<WebsiteDeleteForm
websiteId={deleteIds}
CONFIRM_VALUE={'DELETE MULTIPLE'}
onClose={() => {
setShowConf(false);
updateChildren();
setDeleteIds([]);
}}
/>
}
</Modal>
)}
</GridTable>
</>
);
}

Expand Down
7 changes: 6 additions & 1 deletion src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?:
</Button>
<Modal title={formatMessage(labels.deleteWebsite)}>
{(close: () => void) => (
<WebsiteDeleteForm websiteId={websiteId} onSave={handleSave} onClose={close} />
<WebsiteDeleteForm
CONFIRM_VALUE={'DELETE'}
websiteId={websiteId}
onSave={handleSave}
onClose={close}
/>
)}
</Modal>
</ModalTrigger>
Expand Down
15 changes: 11 additions & 4 deletions src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import { useApi, useMessages } from '@/components/hooks';
import TypeConfirmationForm from '@/components/common/TypeConfirmationForm';

const CONFIRM_VALUE = 'DELETE';

export function WebsiteDeleteForm({
websiteId,
CONFIRM_VALUE,
onSave,
onClose,
}: {
websiteId: string;
websiteId: string | string[];
CONFIRM_VALUE: string;
onSave?: () => void;
onClose?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const { del, useMutation } = useApi();
const { mutate, isPending, error } = useMutation({
mutationFn: () => del(`/websites/${websiteId}`),
mutationFn: async () => {
if (typeof websiteId === 'string') {
return del(`/websites/${websiteId}`);
} else {
const ids = websiteId;
return Promise.all(ids.map(id => del(`/websites/${id}`)));
}
},
});

const handleConfirm = async () => {
Expand Down