@open-mercato/ui 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +2 -0
- package/dist/ai/AiAssistantLauncher.js +8 -8
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChat.js +92 -93
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiIcon.js +17 -0
- package/dist/ai/AiIcon.js.map +7 -0
- package/dist/ai/LoopDisabledBanner.js +28 -0
- package/dist/ai/LoopDisabledBanner.js.map +7 -0
- package/dist/ai/LoopTracePanel.js +123 -0
- package/dist/ai/LoopTracePanel.js.map +7 -0
- package/dist/ai/ModelPicker.js.map +1 -1
- package/dist/ai/index.js +8 -0
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/parts/ConfirmationCard.js +1 -5
- package/dist/ai/parts/ConfirmationCard.js.map +2 -2
- package/dist/ai/parts/FieldDiffCard.js +8 -15
- package/dist/ai/parts/FieldDiffCard.js.map +2 -2
- package/dist/ai/parts/MutationResultCard.js +1 -4
- package/dist/ai/parts/MutationResultCard.js.map +2 -2
- package/dist/ai/ui-parts/pending-phase3-placeholder.js +0 -2
- package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +2 -2
- package/dist/ai/useAiChat.js +18 -4
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/CrudForm.js +24 -2
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +2 -2
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/confirm-dialog/ConfirmDialog.js +102 -96
- package/dist/backend/confirm-dialog/ConfirmDialog.js.map +2 -2
- package/dist/backend/detail/CustomDataSection.js +22 -3
- package/dist/backend/detail/CustomDataSection.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterBuilder.js +2 -2
- package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
- package/dist/backend/injection/ComponentOverrideProvider.js +1 -1
- package/dist/backend/injection/ComponentOverrideProvider.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +25 -7
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/progress/useProgressPoll.js +58 -3
- package/dist/backend/progress/useProgressPoll.js.map +2 -2
- package/dist/backend/progress/useProgressSse.js +15 -2
- package/dist/backend/progress/useProgressSse.js.map +2 -2
- package/dist/backend/utils/bulkDelete.js +76 -0
- package/dist/backend/utils/bulkDelete.js.map +2 -2
- package/package.json +11 -6
- package/src/ai/AiAssistantLauncher.tsx +8 -8
- package/src/ai/AiChat.tsx +44 -38
- package/src/ai/AiIcon.tsx +12 -0
- package/src/ai/LoopDisabledBanner.tsx +38 -0
- package/src/ai/LoopTracePanel.tsx +184 -0
- package/src/ai/ModelPicker.tsx +1 -1
- package/src/ai/__tests__/AiChat.test.tsx +174 -5
- package/src/ai/index.ts +8 -0
- package/src/ai/parts/ConfirmationCard.tsx +1 -5
- package/src/ai/parts/FieldDiffCard.tsx +0 -3
- package/src/ai/parts/MutationResultCard.tsx +1 -4
- package/src/ai/parts/__tests__/MutationResultCard.test.tsx +5 -0
- package/src/ai/ui-parts/pending-phase3-placeholder.tsx +0 -2
- package/src/ai/useAiChat.ts +35 -5
- package/src/backend/CrudForm.tsx +30 -3
- package/src/backend/DataTable.tsx +2 -2
- package/src/backend/__tests__/CrudForm.custom-fields.test.tsx +72 -0
- package/src/backend/__tests__/CustomDataSection.test.tsx +76 -0
- package/src/backend/__tests__/DataTable.render.test.tsx +36 -0
- package/src/backend/__tests__/component-replacement.test.tsx +44 -0
- package/src/backend/confirm-dialog/ConfirmDialog.tsx +101 -91
- package/src/backend/confirm-dialog/__tests__/ConfirmDialog.test.tsx +19 -0
- package/src/backend/detail/CustomDataSection.tsx +26 -3
- package/src/backend/filters/AdvancedFilterBuilder.tsx +6 -6
- package/src/backend/injection/ComponentOverrideProvider.tsx +2 -1
- package/src/backend/inputs/ComboboxInput.tsx +29 -7
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +33 -2
- package/src/backend/progress/useProgressPoll.ts +68 -3
- package/src/backend/progress/useProgressSse.ts +16 -2
- package/src/backend/utils/__tests__/bulkDelete.test.ts +42 -0
- package/src/backend/utils/bulkDelete.ts +93 -0
|
@@ -38,9 +38,10 @@ describe('ComboboxInput — eager label resolution', () => {
|
|
|
38
38
|
expect(getInput(container).value).toBe('uuid-123')
|
|
39
39
|
await waitFor(() => expect(getInput(container).value).toBe('Resolved uuid-123'))
|
|
40
40
|
expect(resolveLabel).toHaveBeenCalledWith('uuid-123')
|
|
41
|
+
expect(resolveLabel).toHaveBeenCalledTimes(1)
|
|
41
42
|
})
|
|
42
43
|
|
|
43
|
-
it('resolves the label via a synchronous resolveLabel', () => {
|
|
44
|
+
it('resolves the label via a synchronous resolveLabel', async () => {
|
|
44
45
|
const { container } = render(
|
|
45
46
|
<ComboboxInput
|
|
46
47
|
value="uuid-123"
|
|
@@ -48,7 +49,7 @@ describe('ComboboxInput — eager label resolution', () => {
|
|
|
48
49
|
resolveLabel={(value) => (value === 'uuid-123' ? 'Sync Label' : value)}
|
|
49
50
|
/>,
|
|
50
51
|
)
|
|
51
|
-
expect(getInput(container).value).toBe('Sync Label')
|
|
52
|
+
await waitFor(() => expect(getInput(container).value).toBe('Sync Label'))
|
|
52
53
|
})
|
|
53
54
|
|
|
54
55
|
it('does not call resolveLabel when the value is already covered by suggestions', () => {
|
|
@@ -73,6 +74,36 @@ describe('ComboboxInput — eager label resolution', () => {
|
|
|
73
74
|
expect(loadSuggestions).toHaveBeenCalledWith()
|
|
74
75
|
})
|
|
75
76
|
|
|
77
|
+
it('does not reload suggestions after a self-labeled value is covered', async () => {
|
|
78
|
+
const loadSuggestions = jest.fn(async () => [{ value: 'UTC', label: 'UTC' }])
|
|
79
|
+
const { container } = render(
|
|
80
|
+
<ComboboxInput value="UTC" onChange={() => {}} loadSuggestions={loadSuggestions} />,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
await waitFor(() => expect(loadSuggestions).toHaveBeenCalledTimes(1))
|
|
84
|
+
await act(async () => {
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(loadSuggestions).toHaveBeenCalledTimes(1)
|
|
89
|
+
expect(getInput(container).value).toBe('UTC')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('does not loop when the first suggestions page cannot resolve the value', async () => {
|
|
93
|
+
const loadSuggestions = jest.fn(async () => [{ value: 'Europe/Warsaw', label: 'Europe/Warsaw' }])
|
|
94
|
+
const { container } = render(
|
|
95
|
+
<ComboboxInput value="UTC" onChange={() => {}} loadSuggestions={loadSuggestions} />,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
await waitFor(() => expect(loadSuggestions).toHaveBeenCalledTimes(1))
|
|
99
|
+
await act(async () => {
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(loadSuggestions).toHaveBeenCalledTimes(1)
|
|
104
|
+
expect(getInput(container).value).toBe('UTC')
|
|
105
|
+
})
|
|
106
|
+
|
|
76
107
|
it('keeps the resolved label after a blur revert when custom values are disallowed', async () => {
|
|
77
108
|
const onChange = jest.fn()
|
|
78
109
|
const { container } = render(
|
|
@@ -4,6 +4,7 @@ import { apiCall } from '../utils/apiCall'
|
|
|
4
4
|
import {
|
|
5
5
|
subscribeProgressUpdate,
|
|
6
6
|
subscribeProgressComplete,
|
|
7
|
+
type ProgressUpdateDetail,
|
|
7
8
|
} from '@open-mercato/shared/lib/frontend/progressEvents'
|
|
8
9
|
|
|
9
10
|
export type ProgressJobDto = {
|
|
@@ -37,6 +38,62 @@ function isVisibleProgressJob(job: ProgressJobDto): boolean {
|
|
|
37
38
|
return job.meta?.hiddenFromTopBar !== true
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
export function isLocalProgressJob(job: ProgressJobDto): boolean {
|
|
42
|
+
return job.id.startsWith('client:')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isTerminalStatus(status: ProgressJobDto['status']): boolean {
|
|
46
|
+
return status === 'completed' || status === 'failed' || status === 'cancelled'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function localJobFromProgressDetail(detail: ProgressUpdateDetail): ProgressJobDto {
|
|
50
|
+
const now = new Date().toISOString()
|
|
51
|
+
const status = detail.status
|
|
52
|
+
return {
|
|
53
|
+
id: detail.jobId,
|
|
54
|
+
jobType: detail.jobType,
|
|
55
|
+
name: detail.name,
|
|
56
|
+
description: detail.description ?? null,
|
|
57
|
+
meta: detail.meta ?? null,
|
|
58
|
+
status,
|
|
59
|
+
progressPercent: detail.progressPercent,
|
|
60
|
+
processedCount: detail.processedCount,
|
|
61
|
+
totalCount: detail.totalCount ?? null,
|
|
62
|
+
etaSeconds: detail.etaSeconds ?? null,
|
|
63
|
+
cancellable: detail.cancellable ?? false,
|
|
64
|
+
startedAt: detail.startedAt ?? now,
|
|
65
|
+
finishedAt: detail.finishedAt ?? (isTerminalStatus(status) ? now : null),
|
|
66
|
+
errorMessage: detail.errorMessage ?? null,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function upsertLocalJob(list: ProgressJobDto[], job: ProgressJobDto): ProgressJobDto[] {
|
|
71
|
+
if (!isVisibleProgressJob(job)) {
|
|
72
|
+
return list.filter((item) => item.id !== job.id)
|
|
73
|
+
}
|
|
74
|
+
const next = [job, ...list.filter((item) => item.id !== job.id)]
|
|
75
|
+
return next.sort(
|
|
76
|
+
(a, b) =>
|
|
77
|
+
new Date(b.startedAt ?? b.finishedAt ?? 0).getTime()
|
|
78
|
+
- new Date(a.startedAt ?? a.finishedAt ?? 0).getTime(),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function applyLocalProgressUpdate(
|
|
83
|
+
detail: ProgressUpdateDetail,
|
|
84
|
+
setActiveJobs: React.Dispatch<React.SetStateAction<ProgressJobDto[]>>,
|
|
85
|
+
setRecentlyCompleted: React.Dispatch<React.SetStateAction<ProgressJobDto[]>>,
|
|
86
|
+
): void {
|
|
87
|
+
const job = localJobFromProgressDetail(detail)
|
|
88
|
+
if (isTerminalStatus(job.status)) {
|
|
89
|
+
setActiveJobs((prev) => prev.filter((item) => item.id !== job.id))
|
|
90
|
+
setRecentlyCompleted((prev) => upsertLocalJob(prev, job).slice(0, 10))
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
setRecentlyCompleted((prev) => prev.filter((item) => item.id !== job.id))
|
|
94
|
+
setActiveJobs((prev) => upsertLocalJob(prev, job).slice(0, 50))
|
|
95
|
+
}
|
|
96
|
+
|
|
40
97
|
export function useProgressPoll(): UseProgressPollResult {
|
|
41
98
|
const [activeJobs, setActiveJobs] = React.useState<ProgressJobDto[]>([])
|
|
42
99
|
const [recentlyCompleted, setRecentlyCompleted] = React.useState<ProgressJobDto[]>([])
|
|
@@ -49,8 +106,14 @@ export function useProgressPoll(): UseProgressPollResult {
|
|
|
49
106
|
'/api/progress/active'
|
|
50
107
|
)
|
|
51
108
|
if (result.ok && result.result) {
|
|
52
|
-
setActiveJobs(
|
|
53
|
-
|
|
109
|
+
setActiveJobs((prev) => [
|
|
110
|
+
...prev.filter((job) => isLocalProgressJob(job) && !isTerminalStatus(job.status)),
|
|
111
|
+
...result.result!.active.filter(isVisibleProgressJob),
|
|
112
|
+
])
|
|
113
|
+
setRecentlyCompleted((prev) => [
|
|
114
|
+
...prev.filter((job) => isLocalProgressJob(job) && isTerminalStatus(job.status)),
|
|
115
|
+
...result.result!.recentlyCompleted.filter(isVisibleProgressJob),
|
|
116
|
+
].slice(0, 10))
|
|
54
117
|
setError(null)
|
|
55
118
|
}
|
|
56
119
|
} catch (err) {
|
|
@@ -88,7 +151,9 @@ export function useProgressPoll(): UseProgressPollResult {
|
|
|
88
151
|
}, [fetchJobs])
|
|
89
152
|
|
|
90
153
|
React.useEffect(() => {
|
|
91
|
-
const unsubUpdate = subscribeProgressUpdate(() =>
|
|
154
|
+
const unsubUpdate = subscribeProgressUpdate((detail) => {
|
|
155
|
+
applyLocalProgressUpdate(detail, setActiveJobs, setRecentlyCompleted)
|
|
156
|
+
})
|
|
92
157
|
const unsubComplete = subscribeProgressComplete(() => refresh())
|
|
93
158
|
return () => {
|
|
94
159
|
unsubUpdate()
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { apiCall } from '../utils/apiCall'
|
|
4
4
|
import { useAppEvent } from '../injection/useAppEvent'
|
|
5
|
+
import { subscribeProgressUpdate } from '@open-mercato/shared/lib/frontend/progressEvents'
|
|
5
6
|
import type { ProgressJobDto, UseProgressPollResult } from './useProgressPoll'
|
|
7
|
+
import { applyLocalProgressUpdate, isLocalProgressJob } from './useProgressPoll'
|
|
6
8
|
|
|
7
9
|
function isVisibleProgressJob(job: ProgressJobDto): boolean {
|
|
8
10
|
return job.meta?.hiddenFromTopBar !== true
|
|
@@ -32,8 +34,14 @@ export function useProgressSse(): UseProgressPollResult {
|
|
|
32
34
|
'/api/progress/active',
|
|
33
35
|
)
|
|
34
36
|
if (result.ok && result.result) {
|
|
35
|
-
setActiveJobs(
|
|
36
|
-
|
|
37
|
+
setActiveJobs((prev) => [
|
|
38
|
+
...prev.filter((job) => isLocalProgressJob(job) && (job.status === 'pending' || job.status === 'running')),
|
|
39
|
+
...result.result!.active.filter(isVisibleProgressJob),
|
|
40
|
+
])
|
|
41
|
+
setRecentlyCompleted((prev) => [
|
|
42
|
+
...prev.filter((job) => isLocalProgressJob(job) && (job.status === 'completed' || job.status === 'failed')),
|
|
43
|
+
...result.result!.recentlyCompleted.filter(isVisibleProgressJob),
|
|
44
|
+
].slice(0, 10))
|
|
37
45
|
setError(null)
|
|
38
46
|
}
|
|
39
47
|
} catch (err) {
|
|
@@ -51,6 +59,12 @@ export function useProgressSse(): UseProgressPollResult {
|
|
|
51
59
|
void fetchJobs()
|
|
52
60
|
}, [fetchJobs])
|
|
53
61
|
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
return subscribeProgressUpdate((detail) => {
|
|
64
|
+
applyLocalProgressUpdate(detail, setActiveJobs, setRecentlyCompleted)
|
|
65
|
+
})
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
54
68
|
useAppEvent(
|
|
55
69
|
'progress.job.updated',
|
|
56
70
|
(event) => {
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { groupBulkDeleteFailures, runBulkDelete, type BulkDeleteFailure } from '../bulkDelete'
|
|
2
|
+
import { emitProgressUpdate } from '@open-mercato/shared/lib/frontend/progressEvents'
|
|
3
|
+
|
|
4
|
+
jest.mock('@open-mercato/shared/lib/frontend/progressEvents', () => ({
|
|
5
|
+
emitProgressUpdate: jest.fn(),
|
|
6
|
+
}))
|
|
2
7
|
|
|
3
8
|
describe('runBulkDelete', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.mocked(emitProgressUpdate).mockClear()
|
|
11
|
+
})
|
|
12
|
+
|
|
4
13
|
it('captures error message and code from thrown error', async () => {
|
|
5
14
|
const dependencyErr = Object.assign(new Error('Cannot delete company: linked deals (2).'), {
|
|
6
15
|
code: 'COMPANY_HAS_DEPENDENTS',
|
|
@@ -66,6 +75,39 @@ describe('runBulkDelete', () => {
|
|
|
66
75
|
warnSpy.mockRestore()
|
|
67
76
|
}
|
|
68
77
|
})
|
|
78
|
+
|
|
79
|
+
it('emits client progress events when progress metadata is provided', async () => {
|
|
80
|
+
const rows = [{ id: 'a' }, { id: 'b' }]
|
|
81
|
+
const deleteOne = jest.fn().mockResolvedValue(undefined)
|
|
82
|
+
|
|
83
|
+
await runBulkDelete(rows, deleteOne, {
|
|
84
|
+
progress: {
|
|
85
|
+
jobId: 'client:test',
|
|
86
|
+
jobType: 'customers.people.bulk_delete',
|
|
87
|
+
name: 'Delete selected people',
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const calls = jest.mocked(emitProgressUpdate).mock.calls.map(([detail]) => detail)
|
|
92
|
+
expect(calls).toHaveLength(4)
|
|
93
|
+
expect(calls[0]).toMatchObject({
|
|
94
|
+
jobId: 'client:test',
|
|
95
|
+
status: 'running',
|
|
96
|
+
processedCount: 0,
|
|
97
|
+
totalCount: 2,
|
|
98
|
+
progressPercent: 0,
|
|
99
|
+
})
|
|
100
|
+
expect(calls[2]).toMatchObject({
|
|
101
|
+
status: 'running',
|
|
102
|
+
processedCount: 2,
|
|
103
|
+
progressPercent: 100,
|
|
104
|
+
})
|
|
105
|
+
expect(calls[3]).toMatchObject({
|
|
106
|
+
status: 'completed',
|
|
107
|
+
processedCount: 2,
|
|
108
|
+
progressPercent: 100,
|
|
109
|
+
})
|
|
110
|
+
})
|
|
69
111
|
})
|
|
70
112
|
|
|
71
113
|
describe('groupBulkDeleteFailures', () => {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { emitProgressUpdate } from '@open-mercato/shared/lib/frontend/progressEvents'
|
|
4
|
+
|
|
3
5
|
export type BulkDeleteFailure = {
|
|
4
6
|
id: string
|
|
5
7
|
code: string | null
|
|
@@ -14,6 +16,35 @@ export type BulkDeleteOutcome<T extends { id: string }> = {
|
|
|
14
16
|
export type BulkDeleteOptions = {
|
|
15
17
|
fallbackErrorMessage?: string
|
|
16
18
|
logTag?: string
|
|
19
|
+
progress?: {
|
|
20
|
+
jobId?: string
|
|
21
|
+
jobType: string
|
|
22
|
+
name: string
|
|
23
|
+
description?: string | null
|
|
24
|
+
meta?: Record<string, unknown> | null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createClientProgressJobId(jobType: string): string {
|
|
29
|
+
const cryptoRef =
|
|
30
|
+
typeof globalThis !== 'undefined'
|
|
31
|
+
? (globalThis as { crypto?: { randomUUID?: () => string } }).crypto
|
|
32
|
+
: undefined
|
|
33
|
+
if (cryptoRef && typeof cryptoRef.randomUUID === 'function') {
|
|
34
|
+
return `client:${cryptoRef.randomUUID()}`
|
|
35
|
+
}
|
|
36
|
+
return `client:${jobType}:${Date.now()}:${Math.random().toString(36).slice(2)}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function calculateProgress(processed: number, total: number): number {
|
|
40
|
+
if (total <= 0) return 100
|
|
41
|
+
return Math.max(0, Math.min(100, Math.round((processed / total) * 100)))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function calculateEtaSeconds(startedAtMs: number, processed: number, total: number): number | null {
|
|
45
|
+
if (processed <= 0 || processed >= total) return null
|
|
46
|
+
const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAtMs) / 1000))
|
|
47
|
+
return Math.ceil((elapsedSeconds / processed) * (total - processed))
|
|
17
48
|
}
|
|
18
49
|
|
|
19
50
|
export async function runBulkDelete<T extends { id: string }>(
|
|
@@ -25,6 +56,26 @@ export async function runBulkDelete<T extends { id: string }>(
|
|
|
25
56
|
const logTag = options?.logTag
|
|
26
57
|
const succeeded: T[] = []
|
|
27
58
|
const failures: BulkDeleteFailure[] = []
|
|
59
|
+
const progress = options?.progress
|
|
60
|
+
const progressJobId = progress && rows.length > 0 ? progress.jobId ?? createClientProgressJobId(progress.jobType) : null
|
|
61
|
+
const startedAtMs = Date.now()
|
|
62
|
+
const startedAt = new Date(startedAtMs).toISOString()
|
|
63
|
+
if (progress && progressJobId) {
|
|
64
|
+
emitProgressUpdate({
|
|
65
|
+
jobId: progressJobId,
|
|
66
|
+
jobType: progress.jobType,
|
|
67
|
+
name: progress.name,
|
|
68
|
+
description: progress.description ?? null,
|
|
69
|
+
meta: progress.meta ?? null,
|
|
70
|
+
status: 'running',
|
|
71
|
+
progressPercent: 0,
|
|
72
|
+
processedCount: 0,
|
|
73
|
+
totalCount: rows.length,
|
|
74
|
+
etaSeconds: null,
|
|
75
|
+
cancellable: false,
|
|
76
|
+
startedAt,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
28
79
|
for (const row of rows) {
|
|
29
80
|
try {
|
|
30
81
|
await deleteOne(row)
|
|
@@ -40,6 +91,48 @@ export async function runBulkDelete<T extends { id: string }>(
|
|
|
40
91
|
console.warn(`[${logTag}] bulk delete failed`, { id: row.id, code, message, err })
|
|
41
92
|
}
|
|
42
93
|
}
|
|
94
|
+
if (progress && progressJobId) {
|
|
95
|
+
const processed = succeeded.length + failures.length
|
|
96
|
+
emitProgressUpdate({
|
|
97
|
+
jobId: progressJobId,
|
|
98
|
+
jobType: progress.jobType,
|
|
99
|
+
name: progress.name,
|
|
100
|
+
description: progress.description ?? null,
|
|
101
|
+
meta: progress.meta ?? null,
|
|
102
|
+
status: 'running',
|
|
103
|
+
progressPercent: calculateProgress(processed, rows.length),
|
|
104
|
+
processedCount: processed,
|
|
105
|
+
totalCount: rows.length,
|
|
106
|
+
etaSeconds: calculateEtaSeconds(startedAtMs, processed, rows.length),
|
|
107
|
+
cancellable: false,
|
|
108
|
+
startedAt,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (progress && progressJobId) {
|
|
113
|
+
emitProgressUpdate({
|
|
114
|
+
jobId: progressJobId,
|
|
115
|
+
jobType: progress.jobType,
|
|
116
|
+
name: progress.name,
|
|
117
|
+
description: progress.description ?? null,
|
|
118
|
+
meta: {
|
|
119
|
+
...(progress.meta ?? {}),
|
|
120
|
+
succeededCount: succeeded.length,
|
|
121
|
+
failedCount: failures.length,
|
|
122
|
+
},
|
|
123
|
+
status: failures.length === rows.length && rows.length > 0 ? 'failed' : 'completed',
|
|
124
|
+
progressPercent: 100,
|
|
125
|
+
processedCount: rows.length,
|
|
126
|
+
totalCount: rows.length,
|
|
127
|
+
etaSeconds: 0,
|
|
128
|
+
cancellable: false,
|
|
129
|
+
startedAt,
|
|
130
|
+
finishedAt: new Date().toISOString(),
|
|
131
|
+
errorMessage:
|
|
132
|
+
failures.length === rows.length && failures[0]?.message
|
|
133
|
+
? failures[0].message
|
|
134
|
+
: null,
|
|
135
|
+
})
|
|
43
136
|
}
|
|
44
137
|
return { succeeded, failures }
|
|
45
138
|
}
|