Skip to content

Commit fa32fd9

Browse files
committed
WIP: Next Grid Bulk Actions
1 parent 4dce987 commit fa32fd9

18 files changed

+435
-137
lines changed

src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { WebSocketMessageValue } from '$features/websockets/models';
2-
import type { CountResult } from '$shared/models';
2+
import type { CountResult, WorkInProgressResult } from '$shared/models';
33

44
import { accessToken } from '$features/auth/index.svelte';
55
import { DEFAULT_OFFSET } from '$features/shared/api/api.svelte';
66
import { type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient';
7-
import { createQuery, QueryClient, useQueryClient } from '@tanstack/svelte-query';
7+
import { createMutation, createQuery, QueryClient, useQueryClient } from '@tanstack/svelte-query';
88

99
import type { PersistentEvent } from './models';
1010

@@ -33,11 +33,18 @@ export const queryKeys = {
3333
count: () => [...queryKeys.type, 'count'] as const,
3434
id: (id: string | undefined) => [...queryKeys.type, id] as const,
3535
projectsCount: (id: string | undefined) => [...queryKeys.type, 'projects', id] as const,
36+
remove: (ids: string[] | undefined) => [...queryKeys.type, 'remove', ...(ids ?? [])] as const,
3637
stacks: (id: string | undefined) => [...queryKeys.type, 'stacks', id] as const,
3738
stacksCount: (id: string | undefined) => [...queryKeys.stacks(id), 'count'] as const,
3839
type: ['PersistentEvent'] as const
3940
};
4041

42+
export interface DeleteEventsRequest {
43+
route: {
44+
ids: string[] | undefined;
45+
};
46+
}
47+
4148
export interface GetCountRequest {
4249
params?: {
4350
aggregations?: string;
@@ -113,6 +120,26 @@ export interface GetStackEventsRequest {
113120
};
114121
}
115122

123+
export function deleteEvent(request: DeleteEventsRequest) {
124+
const queryClient = useQueryClient();
125+
return createMutation<WorkInProgressResult, ProblemDetails, void>(() => ({
126+
enabled: () => !!accessToken.value && !!request.route.ids?.length,
127+
mutationFn: async () => {
128+
const client = useFetchClient();
129+
const response = await client.delete(`events/${request.route.ids?.join(',')}`);
130+
131+
return response.data as WorkInProgressResult;
132+
},
133+
mutationKey: queryKeys.remove(request.route.ids),
134+
onError: () => {
135+
request.route.ids?.forEach((id) => queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }));
136+
},
137+
onSuccess: () => {
138+
request.route.ids?.forEach((id) => queryClient.invalidateQueries({ queryKey: queryKeys.id(id) }));
139+
}
140+
}));
141+
}
142+
116143
export function getCountQuery(request: GetCountRequest) {
117144
const queryClient = useQueryClient();
118145

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script lang="ts">
2+
import * as AlertDialog from '$comp/ui/alert-dialog';
3+
import { buttonVariants } from '$comp/ui/button';
4+
5+
interface Props {
6+
open: boolean;
7+
remove: () => Promise<void>;
8+
}
9+
10+
let { open = $bindable(), remove }: Props = $props();
11+
12+
async function onSubmit() {
13+
await remove();
14+
open = false;
15+
}
16+
</script>
17+
18+
<AlertDialog.Root bind:open>
19+
<AlertDialog.Content>
20+
<AlertDialog.Header>
21+
<AlertDialog.Title>Delete Event</AlertDialog.Title>
22+
<AlertDialog.Description>Are you sure you want to delete this event?</AlertDialog.Description>
23+
</AlertDialog.Header>
24+
<AlertDialog.Footer>
25+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
26+
<AlertDialog.Action class={buttonVariants({ variant: 'destructive' })} onclick={onSubmit}>Delete Event</AlertDialog.Action>
27+
</AlertDialog.Footer>
28+
</AlertDialog.Content>
29+
</AlertDialog.Root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script module lang="ts">
2+
type TData = unknown;
3+
</script>
4+
5+
<script generics="TData" lang="ts">
6+
import Button from '$comp/ui/button/button.svelte';
7+
import * as DropdownMenu from '$comp/ui/dropdown-menu';
8+
import { deleteEvent } from '$features/events/api.svelte';
9+
import { type Table as SvelteTable } from '@tanstack/svelte-table';
10+
import { toast } from 'svelte-sonner';
11+
import ChevronDown from '~icons/mdi/chevron-down';
12+
13+
import RemoveEventDialog from '../dialogs/RemoveEventDialog.svelte';
14+
15+
interface Props {
16+
table: SvelteTable<TData>;
17+
}
18+
19+
let { table }: Props = $props();
20+
const ids = $derived(table.getSelectedRowModel().flatRows.map((row) => row.id));
21+
22+
let openRemoveEventDialog = $state<boolean>(false);
23+
24+
const removeEvents = deleteEvent({
25+
route: {
26+
get ids() {
27+
return ids;
28+
}
29+
}
30+
});
31+
32+
async function remove() {
33+
await removeEvents.mutateAsync();
34+
if (ids.length === 1) {
35+
toast.success('Successfully deleted event.');
36+
} else {
37+
toast.success(`Successfully deleted ${Intl.NumberFormat().format(ids.length)} events.`);
38+
}
39+
}
40+
</script>
41+
42+
<DropdownMenu.Root>
43+
<DropdownMenu.Trigger>
44+
<Button variant="outline">
45+
Bulk Actions
46+
<ChevronDown class="size-4" />
47+
</Button>
48+
</DropdownMenu.Trigger>
49+
<DropdownMenu.Content>
50+
<DropdownMenu.Group>
51+
<DropdownMenu.GroupHeading>Bulk Actions</DropdownMenu.GroupHeading>
52+
<DropdownMenu.Item onclick={() => (openRemoveEventDialog = true)} class="text-destructive" title="Delete event">Delete</DropdownMenu.Item>
53+
</DropdownMenu.Group>
54+
</DropdownMenu.Content>
55+
</DropdownMenu.Root>
56+
57+
{#if openRemoveEventDialog}
58+
<RemoveEventDialog bind:open={openRemoveEventDialog} {remove} />
59+
{/if}

src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/EventsDataTable.svelte

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
1010
interface Props {
1111
bodyChildren?: Snippet;
12+
footerChildren?: Snippet;
1213
isLoading: boolean;
1314
limit: number;
1415
rowClick?: (row: EventSummaryModel<SummaryTemplateKeys>) => void;
1516
table: Table<EventSummaryModel<SummaryTemplateKeys>>;
1617
toolbarChildren?: Snippet;
1718
}
1819
19-
let { bodyChildren, isLoading, limit = $bindable(), rowClick, table, toolbarChildren }: Props = $props();
20+
let { bodyChildren, footerChildren, isLoading, limit = $bindable(), rowClick, table, toolbarChildren }: Props = $props();
2021
</script>
2122

2223
<DataTable.Root>
@@ -37,7 +38,13 @@
3738
{@render bodyChildren()}
3839
{/if}
3940
</DataTable.Body>
40-
<DataTable.Pagination {table}>
41+
<DataTable.Footer {table} class="space-x-6 lg:space-x-8">
42+
{#if footerChildren}
43+
{@render footerChildren()}
44+
{:else}
45+
<DataTable.Selection {table} />
46+
{/if}
4147
<DataTable.PageSize bind:value={limit} {table}></DataTable.PageSize>
42-
</DataTable.Pagination>
48+
<DataTable.Pagination {table} />
49+
</DataTable.Footer>
4350
</DataTable.Root>

src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ export function getTableContext<TSummaryModel extends SummaryModel<SummaryTempla
199199
get data() {
200200
return _data;
201201
},
202+
set data(value) {
203+
_data = value;
204+
},
202205
enableMultiRowSelection: true,
203206
enableRowSelection: true,
204207
enableSortingRemoval: false,

src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/AutomaticRefreshIndicatorButton.svelte

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
1010
let { canRefresh, refresh }: Props = $props();
1111
12-
const refreshButtonTitle = $derived(
13-
canRefresh ? 'Data will automatically-refresh' : 'Refresh data which is not automatically-refreshing'
14-
);
12+
const refreshButtonTitle = $derived(canRefresh ? 'Data will automatically-refresh' : 'Refresh data which is not automatically-refreshing');
1513
</script>
1614

1715
{#if canRefresh}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script module lang="ts">
2+
type TData = unknown;
3+
</script>
4+
5+
<script generics="TData" lang="ts">
6+
import type { Snippet } from 'svelte';
7+
import type { HTMLAttributes } from 'svelte/elements';
8+
9+
import { type Table as SvelteTable } from '@tanstack/svelte-table';
10+
11+
import DataTablePagination from './data-table-pagination.svelte';
12+
import DataTableSelection from './data-table-selection.svelte';
13+
14+
type Props = HTMLAttributes<Element> & {
15+
children?: Snippet;
16+
table: SvelteTable<TData>;
17+
};
18+
19+
let { children, class: className, table }: Props = $props();
20+
</script>
21+
22+
<div class={['flex items-center justify-between', className]}>
23+
{#if children}
24+
{@render children()}
25+
{:else}
26+
<DataTableSelection {table} />
27+
<div class="flex items-center space-x-6 lg:space-x-8">
28+
<DataTablePagination {table} />
29+
</div>
30+
{/if}
31+
</div>

src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-pagination.svelte

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
<script generics="TData" lang="ts">
66
import type { Table } from '@tanstack/svelte-table';
7-
import type { Snippet } from 'svelte';
87
98
import Number from '$comp/formatters/Number.svelte';
109
import { Button } from '$comp/ui/button';
@@ -13,41 +12,30 @@
1312
import IconChevronRight from '~icons/mdi/chevron-right';
1413
1514
interface Props {
16-
children: Snippet;
1715
table: Table<TData>;
1816
}
1917
20-
let { children, table }: Props = $props();
18+
let { table }: Props = $props();
2119
</script>
2220

23-
<div class="flex items-center justify-between px-2">
24-
<div class="flex-1 text-sm text-muted-foreground">
25-
{#if table.getSelectedRowModel().rows.length > 0}
26-
{Object.keys(table.getSelectedRowModel().rows).length} of{' '}
27-
{table.getRowModel().rows.length} row(s) selected.
28-
{/if}
21+
<div class="flex items-center space-x-6 lg:space-x-8">
22+
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
23+
Page <Number value={table.getState().pagination.pageIndex + 1} /> of <Number value={table.getPageCount()} />
2924
</div>
30-
<div class="flex items-center space-x-6 lg:space-x-8">
31-
{@render children()}
32-
33-
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
34-
Page <Number value={table.getState().pagination.pageIndex + 1} /> of <Number value={table.getPageCount()} />
35-
</div>
36-
<div class="flex items-center space-x-2">
37-
{#if table.getState().pagination.pageIndex > 1}
38-
<Button class="hidden h-8 w-8 p-0 lg:flex" onclick={() => table.resetPageIndex(true)} variant="outline">
39-
<span class="sr-only">Go to first page</span>
40-
<IconChevronDoubleLeft class="mr-2 size-4" />
41-
</Button>
42-
{/if}
43-
<Button class="h-8 w-8 p-0" disabled={!table.getCanPreviousPage()} onclick={() => table.previousPage()} variant="outline">
44-
<span class="sr-only">Go to previous page</span>
45-
<IconChevronLeft class="size-4" />
25+
<div class="flex items-center space-x-2">
26+
{#if table.getState().pagination.pageIndex > 1}
27+
<Button class="hidden size-8 p-0 lg:flex" onclick={() => table.resetPageIndex(true)} variant="outline">
28+
<span class="sr-only">Go to first page</span>
29+
<IconChevronDoubleLeft class="mr-2 size-4" />
4630
</Button>
47-
<Button class="h-8 w-8 p-0" disabled={!table.getCanNextPage()} onclick={() => table.nextPage()} variant="outline">
48-
<span class="sr-only">Go to next page</span>
49-
<IconChevronRight class="size-4" />
50-
</Button>
51-
</div>
31+
{/if}
32+
<Button class="size-8 p-0" disabled={!table.getCanPreviousPage()} onclick={() => table.previousPage()} variant="outline">
33+
<span class="sr-only">Go to previous page</span>
34+
<IconChevronLeft class="size-4" />
35+
</Button>
36+
<Button class="size-8 p-0" disabled={!table.getCanNextPage()} onclick={() => table.nextPage()} variant="outline">
37+
<span class="sr-only">Go to next page</span>
38+
<IconChevronRight class="size-4" />
39+
</Button>
5240
</div>
5341
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script module lang="ts">
2+
type TData = unknown;
3+
</script>
4+
5+
<script generics="TData" lang="ts">
6+
import type { Table } from '@tanstack/svelte-table';
7+
8+
interface Props {
9+
table: Table<TData>;
10+
}
11+
12+
let { table }: Props = $props();
13+
</script>
14+
15+
<div class="flex text-pretty text-sm text-muted-foreground">
16+
{#if table.getSelectedRowModel().rows.length > 0}
17+
{Object.keys(table.getSelectedRowModel().rows).length} of{' '}
18+
{table.getRowModel().rows.length} row(s) selected.
19+
{/if}
20+
</div>

src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import Body from './data-table-body.svelte';
22
import Empty from './data-table-empty.svelte';
3+
import Footer from './data-table-footer.svelte';
34
import Loading from './data-table-loading.svelte';
45
import PageSize from './data-table-page-size.svelte';
56
import Pagination from './data-table-pagination.svelte';
67
import Refresh from './data-table-refresh.svelte';
8+
import Selection from './data-table-selection.svelte';
79
import Toolbar from './data-table-toolbar.svelte';
810
import Root from './data-table.svelte';
911

@@ -12,16 +14,20 @@ export {
1214
Root as DataTable,
1315
Body as DataTableBody,
1416
Empty as DataTableEmpty,
17+
Footer as DataTableFooter,
1518
Loading as DataTableLoading,
1619
PageSize as DataTablePageSize,
1720
Pagination as DataTablePagination,
1821
Refresh as DataTableRefresh,
22+
Selection as DataTableSelection,
1923
Toolbar as DataTableToolbar,
2024
Empty,
25+
Footer,
2126
Loading,
2227
PageSize,
2328
Pagination,
2429
Refresh,
2530
Root,
31+
Selection,
2632
Toolbar
2733
};

0 commit comments

Comments
 (0)