|
1 | 1 | <script lang="ts" module> |
2 | | - export type DeleteOperationState = Error | void; |
| 2 | + export type DeleteOperationState = { |
| 3 | + error?: Error; |
| 4 | + deleted: string[]; |
| 5 | + } | void; |
| 6 | +
|
| 7 | + export type DeleteOperation = ( |
| 8 | + deleteFn: (id: string) => Promise<unknown>, |
| 9 | + batchSize?: number |
| 10 | + ) => Promise<Exclude<DeleteOperationState, void>>; |
3 | 11 | </script> |
4 | 12 |
|
5 | 13 | <script lang="ts"> |
|
40 | 48 | children: Snippet<[root: TableRootProps]>; |
41 | 49 | deleteContent?: Snippet<[count: number]>; |
42 | 50 | deleteContentNotice?: Snippet; |
43 | | - onDelete?: (selectedRows: string[]) => Promise<DeleteOperationState> | DeleteOperationState; |
| 51 | + onDelete?: ( |
| 52 | + batchDelete: DeleteOperation, |
| 53 | + /** |
| 54 | + * this is useful when you have a custom deletion logic |
| 55 | + * and the default `batchDelete` helper doesn't fit the use-case! |
| 56 | + */ |
| 57 | + selectedRows: string[] |
| 58 | + ) => Promise<DeleteOperationState> | DeleteOperationState; |
44 | 59 | onCancel?: () => Promise<void> | void; |
45 | 60 | } = $props(); |
46 | 61 |
|
|
49 | 64 | let onDeleteError: string | null = $state(null); |
50 | 65 | let showConfirmDeletion: boolean = $state(false); |
51 | 66 |
|
52 | | - function notifySuccess() { |
| 67 | + function notifySuccess(count: number) { |
53 | 68 | if (!showSuccessNotification) return; |
54 | | -
|
55 | | - const count = selectedRows.length; |
56 | 69 | if (count === 0) return; |
57 | 70 |
|
58 | 71 | const label = `${resource}${count > 1 ? 's' : ''}`; |
|
71 | 84 |
|
72 | 85 | return `${resource}s`; |
73 | 86 | } |
| 87 | +
|
| 88 | + async function batchDelete( |
| 89 | + ids: string[], |
| 90 | + deleteFn: (id: string) => Promise<unknown>, |
| 91 | + batchSize: number | undefined = undefined |
| 92 | + ): Promise<Exclude<DeleteOperationState, void>> { |
| 93 | + const deleted: string[] = []; |
| 94 | + let firstError: Error | undefined; |
| 95 | +
|
| 96 | + // prevent infinite loop |
| 97 | + if (batchSize !== undefined) { |
| 98 | + batchSize = Math.max(1, Math.floor(Math.abs(batchSize))); |
| 99 | + } |
| 100 | +
|
| 101 | + async function processBatch(batch: string[]) { |
| 102 | + // build promises |
| 103 | + const results = await Promise.allSettled(batch.map((id) => deleteFn(id))); |
| 104 | +
|
| 105 | + results.forEach((result, index) => { |
| 106 | + if (result.status === 'fulfilled') { |
| 107 | + // success, log it! |
| 108 | + deleted.push(batch[index]); |
| 109 | + } else if (!firstError) { |
| 110 | + // error |
| 111 | + firstError = |
| 112 | + result.reason instanceof Error |
| 113 | + ? result.reason |
| 114 | + : new Error(String(result.reason)); |
| 115 | + } |
| 116 | + }); |
| 117 | + } |
| 118 | +
|
| 119 | + // batch when needed. |
| 120 | + // example: >= 100 items to delete! |
| 121 | + if (batchSize && batchSize < ids.length) { |
| 122 | + for (let i = 0; i < ids.length; i += batchSize) { |
| 123 | + const batch = ids.slice(i, i + batchSize); |
| 124 | + await processBatch(batch); |
| 125 | + } |
| 126 | + } else { |
| 127 | + await processBatch(ids); |
| 128 | + } |
| 129 | +
|
| 130 | + return { |
| 131 | + deleted, |
| 132 | + error: firstError |
| 133 | + }; |
| 134 | + } |
| 135 | +
|
| 136 | + async function consumeDeleteOperation() { |
| 137 | + const state = await onDelete?.((deleteFn, batchSize) => { |
| 138 | + return batchDelete(selectedRows, deleteFn, batchSize); |
| 139 | + }, selectedRows); |
| 140 | +
|
| 141 | + if (!state) { |
| 142 | + return false; |
| 143 | + } |
| 144 | +
|
| 145 | + const deletedCount = state.deleted.length; |
| 146 | + selectedRows = selectedRows.filter((id) => !state.deleted.includes(id)); |
| 147 | +
|
| 148 | + if (state.error) { |
| 149 | + onDeleteError = `Some ${getPluralResource()} were not deleted. Error: ${state.error.message}`; |
| 150 | + return false; |
| 151 | + } |
| 152 | +
|
| 153 | + notifySuccess(deletedCount); |
| 154 | + return true; |
| 155 | + } |
74 | 156 | </script> |
75 | 157 |
|
76 | 158 | {#key computeKey} |
|
104 | 186 | if (confirmDeletion) { |
105 | 187 | showConfirmDeletion = true; |
106 | 188 | } else { |
107 | | - const state = await onDelete?.(selectedRows); |
108 | | - if (state instanceof Error) { |
109 | | - // user should handle error on their own! |
110 | | - } else { |
111 | | - notifySuccess(); |
112 | | - selectedRows = []; |
113 | | - } |
| 189 | + await consumeDeleteOperation(); |
114 | 190 | } |
115 | 191 | }}>Delete</Button> |
116 | 192 | </svelte:fragment> |
|
129 | 205 | disableModal = true; |
130 | 206 | onDeleteError = null; |
131 | 207 |
|
132 | | - const state = await onDelete?.(selectedRows); |
133 | | - if (state instanceof Error) { |
134 | | - disableModal = false; |
135 | | - onDeleteError = state.message || `Failed to delete ${resource}s`; |
136 | | - } else { |
137 | | - notifySuccess(); |
138 | | - selectedRows = []; |
139 | | - disableModal = false; |
| 208 | + const allDeleted = await consumeDeleteOperation(); |
| 209 | + |
| 210 | + if (allDeleted) { |
140 | 211 | showConfirmDeletion = false; |
141 | 212 | } |
| 213 | + |
| 214 | + disableModal = false; |
142 | 215 | }}> |
143 | 216 | <Typography.Text> |
144 | 217 | {@const selectionCount = selectedRows.length} |
|
0 commit comments