@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
|
@@ -9,6 +9,51 @@ const POLL_INTERVAL = 5e3;
|
|
|
9
9
|
function isVisibleProgressJob(job) {
|
|
10
10
|
return job.meta?.hiddenFromTopBar !== true;
|
|
11
11
|
}
|
|
12
|
+
function isLocalProgressJob(job) {
|
|
13
|
+
return job.id.startsWith("client:");
|
|
14
|
+
}
|
|
15
|
+
function isTerminalStatus(status) {
|
|
16
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
17
|
+
}
|
|
18
|
+
function localJobFromProgressDetail(detail) {
|
|
19
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20
|
+
const status = detail.status;
|
|
21
|
+
return {
|
|
22
|
+
id: detail.jobId,
|
|
23
|
+
jobType: detail.jobType,
|
|
24
|
+
name: detail.name,
|
|
25
|
+
description: detail.description ?? null,
|
|
26
|
+
meta: detail.meta ?? null,
|
|
27
|
+
status,
|
|
28
|
+
progressPercent: detail.progressPercent,
|
|
29
|
+
processedCount: detail.processedCount,
|
|
30
|
+
totalCount: detail.totalCount ?? null,
|
|
31
|
+
etaSeconds: detail.etaSeconds ?? null,
|
|
32
|
+
cancellable: detail.cancellable ?? false,
|
|
33
|
+
startedAt: detail.startedAt ?? now,
|
|
34
|
+
finishedAt: detail.finishedAt ?? (isTerminalStatus(status) ? now : null),
|
|
35
|
+
errorMessage: detail.errorMessage ?? null
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function upsertLocalJob(list, job) {
|
|
39
|
+
if (!isVisibleProgressJob(job)) {
|
|
40
|
+
return list.filter((item) => item.id !== job.id);
|
|
41
|
+
}
|
|
42
|
+
const next = [job, ...list.filter((item) => item.id !== job.id)];
|
|
43
|
+
return next.sort(
|
|
44
|
+
(a, b) => new Date(b.startedAt ?? b.finishedAt ?? 0).getTime() - new Date(a.startedAt ?? a.finishedAt ?? 0).getTime()
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
function applyLocalProgressUpdate(detail, setActiveJobs, setRecentlyCompleted) {
|
|
48
|
+
const job = localJobFromProgressDetail(detail);
|
|
49
|
+
if (isTerminalStatus(job.status)) {
|
|
50
|
+
setActiveJobs((prev) => prev.filter((item) => item.id !== job.id));
|
|
51
|
+
setRecentlyCompleted((prev) => upsertLocalJob(prev, job).slice(0, 10));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
setRecentlyCompleted((prev) => prev.filter((item) => item.id !== job.id));
|
|
55
|
+
setActiveJobs((prev) => upsertLocalJob(prev, job).slice(0, 50));
|
|
56
|
+
}
|
|
12
57
|
function useProgressPoll() {
|
|
13
58
|
const [activeJobs, setActiveJobs] = React.useState([]);
|
|
14
59
|
const [recentlyCompleted, setRecentlyCompleted] = React.useState([]);
|
|
@@ -20,8 +65,14 @@ function useProgressPoll() {
|
|
|
20
65
|
"/api/progress/active"
|
|
21
66
|
);
|
|
22
67
|
if (result.ok && result.result) {
|
|
23
|
-
setActiveJobs(
|
|
24
|
-
|
|
68
|
+
setActiveJobs((prev) => [
|
|
69
|
+
...prev.filter((job) => isLocalProgressJob(job) && !isTerminalStatus(job.status)),
|
|
70
|
+
...result.result.active.filter(isVisibleProgressJob)
|
|
71
|
+
]);
|
|
72
|
+
setRecentlyCompleted((prev) => [
|
|
73
|
+
...prev.filter((job) => isLocalProgressJob(job) && isTerminalStatus(job.status)),
|
|
74
|
+
...result.result.recentlyCompleted.filter(isVisibleProgressJob)
|
|
75
|
+
].slice(0, 10));
|
|
25
76
|
setError(null);
|
|
26
77
|
}
|
|
27
78
|
} catch (err) {
|
|
@@ -54,7 +105,9 @@ function useProgressPoll() {
|
|
|
54
105
|
};
|
|
55
106
|
}, [fetchJobs]);
|
|
56
107
|
React.useEffect(() => {
|
|
57
|
-
const unsubUpdate = subscribeProgressUpdate(() =>
|
|
108
|
+
const unsubUpdate = subscribeProgressUpdate((detail) => {
|
|
109
|
+
applyLocalProgressUpdate(detail, setActiveJobs, setRecentlyCompleted);
|
|
110
|
+
});
|
|
58
111
|
const unsubComplete = subscribeProgressComplete(() => refresh());
|
|
59
112
|
return () => {
|
|
60
113
|
unsubUpdate();
|
|
@@ -64,6 +117,8 @@ function useProgressPoll() {
|
|
|
64
117
|
return { activeJobs, recentlyCompleted, isLoading, error, refresh };
|
|
65
118
|
}
|
|
66
119
|
export {
|
|
120
|
+
applyLocalProgressUpdate,
|
|
121
|
+
isLocalProgressJob,
|
|
67
122
|
useProgressPoll
|
|
68
123
|
};
|
|
69
124
|
//# sourceMappingURL=useProgressPoll.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/progress/useProgressPoll.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { apiCall } from '../utils/apiCall'\nimport {\n subscribeProgressUpdate,\n subscribeProgressComplete,\n} from '@open-mercato/shared/lib/frontend/progressEvents'\n\nexport type ProgressJobDto = {\n id: string\n jobType: string\n name: string\n description?: string | null\n meta?: Record<string, unknown> | null\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'\n progressPercent: number\n processedCount: number\n totalCount?: number | null\n etaSeconds?: number | null\n cancellable: boolean\n startedAt?: string | null\n finishedAt?: string | null\n errorMessage?: string | null\n}\n\nexport type UseProgressPollResult = {\n activeJobs: ProgressJobDto[]\n recentlyCompleted: ProgressJobDto[]\n isLoading: boolean\n error: string | null\n refresh: () => void\n}\n\nconst POLL_INTERVAL = 5000\n\nfunction isVisibleProgressJob(job: ProgressJobDto): boolean {\n return job.meta?.hiddenFromTopBar !== true\n}\n\nexport function useProgressPoll(): UseProgressPollResult {\n const [activeJobs, setActiveJobs] = React.useState<ProgressJobDto[]>([])\n const [recentlyCompleted, setRecentlyCompleted] = React.useState<ProgressJobDto[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchJobs = React.useCallback(async () => {\n try {\n const result = await apiCall<{ active: ProgressJobDto[]; recentlyCompleted: ProgressJobDto[] }>(\n '/api/progress/active'\n )\n if (result.ok && result.result) {\n setActiveJobs(result.result
|
|
5
|
-
"mappings": ";AACA,YAAY,WAAW;AACvB,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { apiCall } from '../utils/apiCall'\nimport {\n subscribeProgressUpdate,\n subscribeProgressComplete,\n type ProgressUpdateDetail,\n} from '@open-mercato/shared/lib/frontend/progressEvents'\n\nexport type ProgressJobDto = {\n id: string\n jobType: string\n name: string\n description?: string | null\n meta?: Record<string, unknown> | null\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'\n progressPercent: number\n processedCount: number\n totalCount?: number | null\n etaSeconds?: number | null\n cancellable: boolean\n startedAt?: string | null\n finishedAt?: string | null\n errorMessage?: string | null\n}\n\nexport type UseProgressPollResult = {\n activeJobs: ProgressJobDto[]\n recentlyCompleted: ProgressJobDto[]\n isLoading: boolean\n error: string | null\n refresh: () => void\n}\n\nconst POLL_INTERVAL = 5000\n\nfunction isVisibleProgressJob(job: ProgressJobDto): boolean {\n return job.meta?.hiddenFromTopBar !== true\n}\n\nexport function isLocalProgressJob(job: ProgressJobDto): boolean {\n return job.id.startsWith('client:')\n}\n\nfunction isTerminalStatus(status: ProgressJobDto['status']): boolean {\n return status === 'completed' || status === 'failed' || status === 'cancelled'\n}\n\nfunction localJobFromProgressDetail(detail: ProgressUpdateDetail): ProgressJobDto {\n const now = new Date().toISOString()\n const status = detail.status\n return {\n id: detail.jobId,\n jobType: detail.jobType,\n name: detail.name,\n description: detail.description ?? null,\n meta: detail.meta ?? null,\n status,\n progressPercent: detail.progressPercent,\n processedCount: detail.processedCount,\n totalCount: detail.totalCount ?? null,\n etaSeconds: detail.etaSeconds ?? null,\n cancellable: detail.cancellable ?? false,\n startedAt: detail.startedAt ?? now,\n finishedAt: detail.finishedAt ?? (isTerminalStatus(status) ? now : null),\n errorMessage: detail.errorMessage ?? null,\n }\n}\n\nfunction upsertLocalJob(list: ProgressJobDto[], job: ProgressJobDto): ProgressJobDto[] {\n if (!isVisibleProgressJob(job)) {\n return list.filter((item) => item.id !== job.id)\n }\n const next = [job, ...list.filter((item) => item.id !== job.id)]\n return next.sort(\n (a, b) =>\n new Date(b.startedAt ?? b.finishedAt ?? 0).getTime()\n - new Date(a.startedAt ?? a.finishedAt ?? 0).getTime(),\n )\n}\n\nexport function applyLocalProgressUpdate(\n detail: ProgressUpdateDetail,\n setActiveJobs: React.Dispatch<React.SetStateAction<ProgressJobDto[]>>,\n setRecentlyCompleted: React.Dispatch<React.SetStateAction<ProgressJobDto[]>>,\n): void {\n const job = localJobFromProgressDetail(detail)\n if (isTerminalStatus(job.status)) {\n setActiveJobs((prev) => prev.filter((item) => item.id !== job.id))\n setRecentlyCompleted((prev) => upsertLocalJob(prev, job).slice(0, 10))\n return\n }\n setRecentlyCompleted((prev) => prev.filter((item) => item.id !== job.id))\n setActiveJobs((prev) => upsertLocalJob(prev, job).slice(0, 50))\n}\n\nexport function useProgressPoll(): UseProgressPollResult {\n const [activeJobs, setActiveJobs] = React.useState<ProgressJobDto[]>([])\n const [recentlyCompleted, setRecentlyCompleted] = React.useState<ProgressJobDto[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchJobs = React.useCallback(async () => {\n try {\n const result = await apiCall<{ active: ProgressJobDto[]; recentlyCompleted: ProgressJobDto[] }>(\n '/api/progress/active'\n )\n if (result.ok && result.result) {\n setActiveJobs((prev) => [\n ...prev.filter((job) => isLocalProgressJob(job) && !isTerminalStatus(job.status)),\n ...result.result!.active.filter(isVisibleProgressJob),\n ])\n setRecentlyCompleted((prev) => [\n ...prev.filter((job) => isLocalProgressJob(job) && isTerminalStatus(job.status)),\n ...result.result!.recentlyCompleted.filter(isVisibleProgressJob),\n ].slice(0, 10))\n setError(null)\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to fetch progress')\n } finally {\n setIsLoading(false)\n }\n }, [])\n\n const refresh = React.useCallback(() => {\n fetchJobs()\n }, [fetchJobs])\n\n React.useEffect(() => {\n fetchJobs()\n let interval: ReturnType<typeof setInterval> | null = setInterval(fetchJobs, POLL_INTERVAL)\n\n const onVisibilityChange = () => {\n if (document.hidden) {\n if (interval) {\n clearInterval(interval)\n interval = null\n }\n } else {\n fetchJobs()\n interval = setInterval(fetchJobs, POLL_INTERVAL)\n }\n }\n\n document.addEventListener('visibilitychange', onVisibilityChange)\n return () => {\n if (interval) clearInterval(interval)\n document.removeEventListener('visibilitychange', onVisibilityChange)\n }\n }, [fetchJobs])\n\n React.useEffect(() => {\n const unsubUpdate = subscribeProgressUpdate((detail) => {\n applyLocalProgressUpdate(detail, setActiveJobs, setRecentlyCompleted)\n })\n const unsubComplete = subscribeProgressComplete(() => refresh())\n return () => {\n unsubUpdate()\n unsubComplete()\n }\n }, [refresh])\n\n return { activeJobs, recentlyCompleted, isLoading, error, refresh }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { apiCall } from "../utils/apiCall.js";
|
|
4
4
|
import { useAppEvent } from "../injection/useAppEvent.js";
|
|
5
|
+
import { subscribeProgressUpdate } from "@open-mercato/shared/lib/frontend/progressEvents";
|
|
6
|
+
import { applyLocalProgressUpdate, isLocalProgressJob } from "./useProgressPoll.js";
|
|
5
7
|
function isVisibleProgressJob(job) {
|
|
6
8
|
return job.meta?.hiddenFromTopBar !== true;
|
|
7
9
|
}
|
|
@@ -25,8 +27,14 @@ function useProgressSse() {
|
|
|
25
27
|
"/api/progress/active"
|
|
26
28
|
);
|
|
27
29
|
if (result.ok && result.result) {
|
|
28
|
-
setActiveJobs(
|
|
29
|
-
|
|
30
|
+
setActiveJobs((prev) => [
|
|
31
|
+
...prev.filter((job) => isLocalProgressJob(job) && (job.status === "pending" || job.status === "running")),
|
|
32
|
+
...result.result.active.filter(isVisibleProgressJob)
|
|
33
|
+
]);
|
|
34
|
+
setRecentlyCompleted((prev) => [
|
|
35
|
+
...prev.filter((job) => isLocalProgressJob(job) && (job.status === "completed" || job.status === "failed")),
|
|
36
|
+
...result.result.recentlyCompleted.filter(isVisibleProgressJob)
|
|
37
|
+
].slice(0, 10));
|
|
30
38
|
setError(null);
|
|
31
39
|
}
|
|
32
40
|
} catch (err) {
|
|
@@ -41,6 +49,11 @@ function useProgressSse() {
|
|
|
41
49
|
React.useEffect(() => {
|
|
42
50
|
void fetchJobs();
|
|
43
51
|
}, [fetchJobs]);
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
return subscribeProgressUpdate((detail) => {
|
|
54
|
+
applyLocalProgressUpdate(detail, setActiveJobs, setRecentlyCompleted);
|
|
55
|
+
});
|
|
56
|
+
}, []);
|
|
44
57
|
useAppEvent(
|
|
45
58
|
"progress.job.updated",
|
|
46
59
|
(event) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/progress/useProgressSse.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { apiCall } from '../utils/apiCall'\nimport { useAppEvent } from '../injection/useAppEvent'\nimport type { ProgressJobDto, UseProgressPollResult } from './useProgressPoll'\n\nfunction isVisibleProgressJob(job: ProgressJobDto): boolean {\n return job.meta?.hiddenFromTopBar !== true\n}\n\nfunction upsertJob(list: ProgressJobDto[], job: ProgressJobDto): ProgressJobDto[] {\n if (!isVisibleProgressJob(job)) {\n return list.filter((item) => item.id !== job.id)\n }\n const next = [job, ...list.filter((item) => item.id !== job.id)]\n return next.sort(\n (a, b) =>\n new Date(b.startedAt ?? b.finishedAt ?? 0).getTime()\n - new Date(a.startedAt ?? a.finishedAt ?? 0).getTime(),\n )\n}\n\nexport function useProgressSse(): UseProgressPollResult {\n const [activeJobs, setActiveJobs] = React.useState<ProgressJobDto[]>([])\n const [recentlyCompleted, setRecentlyCompleted] = React.useState<ProgressJobDto[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchJobs = React.useCallback(async () => {\n try {\n const result = await apiCall<{ active: ProgressJobDto[]; recentlyCompleted: ProgressJobDto[] }>(\n '/api/progress/active',\n )\n if (result.ok && result.result) {\n setActiveJobs(result.result
|
|
5
|
-
"mappings": ";AACA,YAAY,WAAW;AACvB,SAAS,eAAe;AACxB,SAAS,mBAAmB;
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { apiCall } from '../utils/apiCall'\nimport { useAppEvent } from '../injection/useAppEvent'\nimport { subscribeProgressUpdate } from '@open-mercato/shared/lib/frontend/progressEvents'\nimport type { ProgressJobDto, UseProgressPollResult } from './useProgressPoll'\nimport { applyLocalProgressUpdate, isLocalProgressJob } from './useProgressPoll'\n\nfunction isVisibleProgressJob(job: ProgressJobDto): boolean {\n return job.meta?.hiddenFromTopBar !== true\n}\n\nfunction upsertJob(list: ProgressJobDto[], job: ProgressJobDto): ProgressJobDto[] {\n if (!isVisibleProgressJob(job)) {\n return list.filter((item) => item.id !== job.id)\n }\n const next = [job, ...list.filter((item) => item.id !== job.id)]\n return next.sort(\n (a, b) =>\n new Date(b.startedAt ?? b.finishedAt ?? 0).getTime()\n - new Date(a.startedAt ?? a.finishedAt ?? 0).getTime(),\n )\n}\n\nexport function useProgressSse(): UseProgressPollResult {\n const [activeJobs, setActiveJobs] = React.useState<ProgressJobDto[]>([])\n const [recentlyCompleted, setRecentlyCompleted] = React.useState<ProgressJobDto[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchJobs = React.useCallback(async () => {\n try {\n const result = await apiCall<{ active: ProgressJobDto[]; recentlyCompleted: ProgressJobDto[] }>(\n '/api/progress/active',\n )\n if (result.ok && result.result) {\n setActiveJobs((prev) => [\n ...prev.filter((job) => isLocalProgressJob(job) && (job.status === 'pending' || job.status === 'running')),\n ...result.result!.active.filter(isVisibleProgressJob),\n ])\n setRecentlyCompleted((prev) => [\n ...prev.filter((job) => isLocalProgressJob(job) && (job.status === 'completed' || job.status === 'failed')),\n ...result.result!.recentlyCompleted.filter(isVisibleProgressJob),\n ].slice(0, 10))\n setError(null)\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to fetch progress')\n } finally {\n setIsLoading(false)\n }\n }, [])\n\n const refresh = React.useCallback(() => {\n void fetchJobs()\n }, [fetchJobs])\n\n React.useEffect(() => {\n void fetchJobs()\n }, [fetchJobs])\n\n React.useEffect(() => {\n return subscribeProgressUpdate((detail) => {\n applyLocalProgressUpdate(detail, setActiveJobs, setRecentlyCompleted)\n })\n }, [])\n\n useAppEvent(\n 'progress.job.updated',\n (event) => {\n const payload = event.payload as Partial<ProgressJobDto> & { jobId?: string }\n const jobId = payload?.jobId\n if (!jobId) {\n void fetchJobs()\n return\n }\n setActiveJobs((prev) =>\n upsertJob(prev, {\n id: jobId,\n jobType: payload.jobType ?? 'progress',\n name: payload.name ?? payload.jobType ?? 'Progress job',\n description: payload.description ?? null,\n meta: (payload.meta && typeof payload.meta === 'object') ? payload.meta as Record<string, unknown> : null,\n status: (payload.status as ProgressJobDto['status']) ?? 'running',\n progressPercent: payload.progressPercent ?? 0,\n processedCount: payload.processedCount ?? 0,\n totalCount: payload.totalCount ?? null,\n etaSeconds: payload.etaSeconds ?? null,\n cancellable: payload.cancellable ?? false,\n startedAt: payload.startedAt ?? null,\n finishedAt: payload.finishedAt ?? null,\n errorMessage: payload.errorMessage ?? null,\n }),\n )\n },\n [fetchJobs],\n )\n\n useAppEvent('progress.job.created', () => {\n void fetchJobs()\n }, [fetchJobs])\n\n useAppEvent('progress.job.started', () => {\n void fetchJobs()\n }, [fetchJobs])\n\n useAppEvent('progress.job.completed', () => {\n void fetchJobs()\n }, [fetchJobs])\n\n useAppEvent('progress.job.failed', () => {\n void fetchJobs()\n }, [fetchJobs])\n\n useAppEvent('progress.job.cancelled', () => {\n void fetchJobs()\n }, [fetchJobs])\n\n useAppEvent('om:bridge:reconnected', () => {\n void fetchJobs()\n }, [fetchJobs])\n\n React.useEffect(() => {\n const onFocus = () => {\n void fetchJobs()\n }\n window.addEventListener('focus', onFocus)\n return () => window.removeEventListener('focus', onFocus)\n }, [fetchJobs])\n\n return { activeJobs, recentlyCompleted, isLoading, error, refresh }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,9 +1,46 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { emitProgressUpdate } from "@open-mercato/shared/lib/frontend/progressEvents";
|
|
3
|
+
function createClientProgressJobId(jobType) {
|
|
4
|
+
const cryptoRef = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
|
|
5
|
+
if (cryptoRef && typeof cryptoRef.randomUUID === "function") {
|
|
6
|
+
return `client:${cryptoRef.randomUUID()}`;
|
|
7
|
+
}
|
|
8
|
+
return `client:${jobType}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
9
|
+
}
|
|
10
|
+
function calculateProgress(processed, total) {
|
|
11
|
+
if (total <= 0) return 100;
|
|
12
|
+
return Math.max(0, Math.min(100, Math.round(processed / total * 100)));
|
|
13
|
+
}
|
|
14
|
+
function calculateEtaSeconds(startedAtMs, processed, total) {
|
|
15
|
+
if (processed <= 0 || processed >= total) return null;
|
|
16
|
+
const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAtMs) / 1e3));
|
|
17
|
+
return Math.ceil(elapsedSeconds / processed * (total - processed));
|
|
18
|
+
}
|
|
2
19
|
async function runBulkDelete(rows, deleteOne, options) {
|
|
3
20
|
const fallback = options?.fallbackErrorMessage ?? "Delete failed";
|
|
4
21
|
const logTag = options?.logTag;
|
|
5
22
|
const succeeded = [];
|
|
6
23
|
const failures = [];
|
|
24
|
+
const progress = options?.progress;
|
|
25
|
+
const progressJobId = progress && rows.length > 0 ? progress.jobId ?? createClientProgressJobId(progress.jobType) : null;
|
|
26
|
+
const startedAtMs = Date.now();
|
|
27
|
+
const startedAt = new Date(startedAtMs).toISOString();
|
|
28
|
+
if (progress && progressJobId) {
|
|
29
|
+
emitProgressUpdate({
|
|
30
|
+
jobId: progressJobId,
|
|
31
|
+
jobType: progress.jobType,
|
|
32
|
+
name: progress.name,
|
|
33
|
+
description: progress.description ?? null,
|
|
34
|
+
meta: progress.meta ?? null,
|
|
35
|
+
status: "running",
|
|
36
|
+
progressPercent: 0,
|
|
37
|
+
processedCount: 0,
|
|
38
|
+
totalCount: rows.length,
|
|
39
|
+
etaSeconds: null,
|
|
40
|
+
cancellable: false,
|
|
41
|
+
startedAt
|
|
42
|
+
});
|
|
43
|
+
}
|
|
7
44
|
for (const row of rows) {
|
|
8
45
|
try {
|
|
9
46
|
await deleteOne(row);
|
|
@@ -17,6 +54,45 @@ async function runBulkDelete(rows, deleteOne, options) {
|
|
|
17
54
|
console.warn(`[${logTag}] bulk delete failed`, { id: row.id, code, message, err });
|
|
18
55
|
}
|
|
19
56
|
}
|
|
57
|
+
if (progress && progressJobId) {
|
|
58
|
+
const processed = succeeded.length + failures.length;
|
|
59
|
+
emitProgressUpdate({
|
|
60
|
+
jobId: progressJobId,
|
|
61
|
+
jobType: progress.jobType,
|
|
62
|
+
name: progress.name,
|
|
63
|
+
description: progress.description ?? null,
|
|
64
|
+
meta: progress.meta ?? null,
|
|
65
|
+
status: "running",
|
|
66
|
+
progressPercent: calculateProgress(processed, rows.length),
|
|
67
|
+
processedCount: processed,
|
|
68
|
+
totalCount: rows.length,
|
|
69
|
+
etaSeconds: calculateEtaSeconds(startedAtMs, processed, rows.length),
|
|
70
|
+
cancellable: false,
|
|
71
|
+
startedAt
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (progress && progressJobId) {
|
|
76
|
+
emitProgressUpdate({
|
|
77
|
+
jobId: progressJobId,
|
|
78
|
+
jobType: progress.jobType,
|
|
79
|
+
name: progress.name,
|
|
80
|
+
description: progress.description ?? null,
|
|
81
|
+
meta: {
|
|
82
|
+
...progress.meta ?? {},
|
|
83
|
+
succeededCount: succeeded.length,
|
|
84
|
+
failedCount: failures.length
|
|
85
|
+
},
|
|
86
|
+
status: failures.length === rows.length && rows.length > 0 ? "failed" : "completed",
|
|
87
|
+
progressPercent: 100,
|
|
88
|
+
processedCount: rows.length,
|
|
89
|
+
totalCount: rows.length,
|
|
90
|
+
etaSeconds: 0,
|
|
91
|
+
cancellable: false,
|
|
92
|
+
startedAt,
|
|
93
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
94
|
+
errorMessage: failures.length === rows.length && failures[0]?.message ? failures[0].message : null
|
|
95
|
+
});
|
|
20
96
|
}
|
|
21
97
|
return { succeeded, failures };
|
|
22
98
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/utils/bulkDelete.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nexport type BulkDeleteFailure = {\n id: string\n code: string | null\n message: string\n}\n\nexport type BulkDeleteOutcome<T extends { id: string }> = {\n succeeded: T[]\n failures: BulkDeleteFailure[]\n}\n\nexport type BulkDeleteOptions = {\n fallbackErrorMessage?: string\n logTag?: string\n}\n\nexport async function runBulkDelete<T extends { id: string }>(\n rows: T[],\n deleteOne: (row: T) => Promise<void>,\n options?: BulkDeleteOptions,\n): Promise<BulkDeleteOutcome<T>> {\n const fallback = options?.fallbackErrorMessage ?? 'Delete failed'\n const logTag = options?.logTag\n const succeeded: T[] = []\n const failures: BulkDeleteFailure[] = []\n for (const row of rows) {\n try {\n await deleteOne(row)\n succeeded.push(row)\n } catch (err) {\n const message =\n err instanceof Error && err.message.trim().length > 0 ? err.message : fallback\n const rawCode =\n err && typeof err === 'object' ? (err as { code?: unknown }).code : undefined\n const code = typeof rawCode === 'string' && rawCode.length > 0 ? rawCode : null\n failures.push({ id: row.id, code, message })\n if (logTag && typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn(`[${logTag}] bulk delete failed`, { id: row.id, code, message, err })\n }\n }\n }\n return { succeeded, failures }\n}\n\nexport type BulkDeleteFailureGroup = {\n key: string\n count: number\n sampleMessage: string\n ids: string[]\n}\n\n// Keyed by `code` when present so semantically identical failures collapse into\n// one toast even if their message text varies (e.g. row-specific dependency\n// counts). Falls back to the raw message when the server did not surface a code.\nexport function groupBulkDeleteFailures(\n failures: BulkDeleteFailure[],\n): BulkDeleteFailureGroup[] {\n const buckets = new Map<string, BulkDeleteFailureGroup>()\n for (const failure of failures) {\n const key = failure.code || failure.message || 'unknown'\n const existing = buckets.get(key)\n if (existing) {\n existing.count += 1\n existing.ids.push(failure.id)\n continue\n }\n buckets.set(key, {\n key,\n count: 1,\n sampleMessage: failure.message,\n ids: [failure.id],\n })\n }\n return Array.from(buckets.values())\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { emitProgressUpdate } from '@open-mercato/shared/lib/frontend/progressEvents'\n\nexport type BulkDeleteFailure = {\n id: string\n code: string | null\n message: string\n}\n\nexport type BulkDeleteOutcome<T extends { id: string }> = {\n succeeded: T[]\n failures: BulkDeleteFailure[]\n}\n\nexport type BulkDeleteOptions = {\n fallbackErrorMessage?: string\n logTag?: string\n progress?: {\n jobId?: string\n jobType: string\n name: string\n description?: string | null\n meta?: Record<string, unknown> | null\n }\n}\n\nfunction createClientProgressJobId(jobType: string): string {\n const cryptoRef =\n typeof globalThis !== 'undefined'\n ? (globalThis as { crypto?: { randomUUID?: () => string } }).crypto\n : undefined\n if (cryptoRef && typeof cryptoRef.randomUUID === 'function') {\n return `client:${cryptoRef.randomUUID()}`\n }\n return `client:${jobType}:${Date.now()}:${Math.random().toString(36).slice(2)}`\n}\n\nfunction calculateProgress(processed: number, total: number): number {\n if (total <= 0) return 100\n return Math.max(0, Math.min(100, Math.round((processed / total) * 100)))\n}\n\nfunction calculateEtaSeconds(startedAtMs: number, processed: number, total: number): number | null {\n if (processed <= 0 || processed >= total) return null\n const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAtMs) / 1000))\n return Math.ceil((elapsedSeconds / processed) * (total - processed))\n}\n\nexport async function runBulkDelete<T extends { id: string }>(\n rows: T[],\n deleteOne: (row: T) => Promise<void>,\n options?: BulkDeleteOptions,\n): Promise<BulkDeleteOutcome<T>> {\n const fallback = options?.fallbackErrorMessage ?? 'Delete failed'\n const logTag = options?.logTag\n const succeeded: T[] = []\n const failures: BulkDeleteFailure[] = []\n const progress = options?.progress\n const progressJobId = progress && rows.length > 0 ? progress.jobId ?? createClientProgressJobId(progress.jobType) : null\n const startedAtMs = Date.now()\n const startedAt = new Date(startedAtMs).toISOString()\n if (progress && progressJobId) {\n emitProgressUpdate({\n jobId: progressJobId,\n jobType: progress.jobType,\n name: progress.name,\n description: progress.description ?? null,\n meta: progress.meta ?? null,\n status: 'running',\n progressPercent: 0,\n processedCount: 0,\n totalCount: rows.length,\n etaSeconds: null,\n cancellable: false,\n startedAt,\n })\n }\n for (const row of rows) {\n try {\n await deleteOne(row)\n succeeded.push(row)\n } catch (err) {\n const message =\n err instanceof Error && err.message.trim().length > 0 ? err.message : fallback\n const rawCode =\n err && typeof err === 'object' ? (err as { code?: unknown }).code : undefined\n const code = typeof rawCode === 'string' && rawCode.length > 0 ? rawCode : null\n failures.push({ id: row.id, code, message })\n if (logTag && typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn(`[${logTag}] bulk delete failed`, { id: row.id, code, message, err })\n }\n }\n if (progress && progressJobId) {\n const processed = succeeded.length + failures.length\n emitProgressUpdate({\n jobId: progressJobId,\n jobType: progress.jobType,\n name: progress.name,\n description: progress.description ?? null,\n meta: progress.meta ?? null,\n status: 'running',\n progressPercent: calculateProgress(processed, rows.length),\n processedCount: processed,\n totalCount: rows.length,\n etaSeconds: calculateEtaSeconds(startedAtMs, processed, rows.length),\n cancellable: false,\n startedAt,\n })\n }\n }\n if (progress && progressJobId) {\n emitProgressUpdate({\n jobId: progressJobId,\n jobType: progress.jobType,\n name: progress.name,\n description: progress.description ?? null,\n meta: {\n ...(progress.meta ?? {}),\n succeededCount: succeeded.length,\n failedCount: failures.length,\n },\n status: failures.length === rows.length && rows.length > 0 ? 'failed' : 'completed',\n progressPercent: 100,\n processedCount: rows.length,\n totalCount: rows.length,\n etaSeconds: 0,\n cancellable: false,\n startedAt,\n finishedAt: new Date().toISOString(),\n errorMessage:\n failures.length === rows.length && failures[0]?.message\n ? failures[0].message\n : null,\n })\n }\n return { succeeded, failures }\n}\n\nexport type BulkDeleteFailureGroup = {\n key: string\n count: number\n sampleMessage: string\n ids: string[]\n}\n\n// Keyed by `code` when present so semantically identical failures collapse into\n// one toast even if their message text varies (e.g. row-specific dependency\n// counts). Falls back to the raw message when the server did not surface a code.\nexport function groupBulkDeleteFailures(\n failures: BulkDeleteFailure[],\n): BulkDeleteFailureGroup[] {\n const buckets = new Map<string, BulkDeleteFailureGroup>()\n for (const failure of failures) {\n const key = failure.code || failure.message || 'unknown'\n const existing = buckets.get(key)\n if (existing) {\n existing.count += 1\n existing.ids.push(failure.id)\n continue\n }\n buckets.set(key, {\n key,\n count: 1,\n sampleMessage: failure.message,\n ids: [failure.id],\n })\n }\n return Array.from(buckets.values())\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.1
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -141,18 +141,24 @@
|
|
|
141
141
|
"remark-gfm": "^4.0.1"
|
|
142
142
|
},
|
|
143
143
|
"peerDependencies": {
|
|
144
|
-
"@open-mercato/shared": "0.6.1
|
|
144
|
+
"@open-mercato/shared": "0.6.1",
|
|
145
145
|
"react": ">=18.0.0",
|
|
146
|
-
"react-dom": ">=18.0.0"
|
|
146
|
+
"react-dom": ">=18.0.0",
|
|
147
|
+
"react-is": ">=18.0.0"
|
|
147
148
|
},
|
|
148
149
|
"devDependencies": {
|
|
149
|
-
"@open-mercato/shared": "0.6.1
|
|
150
|
+
"@open-mercato/shared": "0.6.1",
|
|
150
151
|
"@testing-library/dom": "^10.4.1",
|
|
151
152
|
"@testing-library/jest-dom": "^6.9.1",
|
|
152
153
|
"@testing-library/react": "^16.3.1",
|
|
153
154
|
"@types/jest": "^30.0.0",
|
|
155
|
+
"@types/react": "^19.2.14",
|
|
156
|
+
"@types/react-dom": "^19.2.3",
|
|
154
157
|
"jest": "^30.4.2",
|
|
155
158
|
"jest-environment-jsdom": "^30.4.1",
|
|
159
|
+
"react": "19.2.6",
|
|
160
|
+
"react-dom": "19.2.6",
|
|
161
|
+
"react-is": "^19.2.6",
|
|
156
162
|
"ts-jest": "^29.4.9"
|
|
157
163
|
},
|
|
158
164
|
"publishConfig": {
|
|
@@ -162,6 +168,5 @@
|
|
|
162
168
|
"type": "git",
|
|
163
169
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
164
170
|
"directory": "packages/ui"
|
|
165
|
-
}
|
|
166
|
-
"stableVersion": "0.6.0"
|
|
171
|
+
}
|
|
167
172
|
}
|
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
Loader2,
|
|
32
32
|
PanelRightOpen,
|
|
33
33
|
Search,
|
|
34
|
-
Sparkles,
|
|
35
34
|
} from 'lucide-react'
|
|
36
35
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
37
36
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
@@ -49,6 +48,7 @@ import { Kbd, KbdShortcut } from '../primitives/kbd'
|
|
|
49
48
|
import { useAiDock } from './AiDock'
|
|
50
49
|
import { useAiChatSessions } from './AiChatSessions'
|
|
51
50
|
import { ChatPaneTabs } from './ChatPaneTabs'
|
|
51
|
+
import { AiIcon } from './AiIcon'
|
|
52
52
|
import type { AiChatContextItem, AiChatSuggestion } from './AiChat'
|
|
53
53
|
|
|
54
54
|
// Lazy-load the chat surface so AppShell tests (and any other importers that
|
|
@@ -407,7 +407,7 @@ export function AiAssistantLauncher({
|
|
|
407
407
|
{
|
|
408
408
|
label: t('ai_assistant.launcher.welcome.suggestion1', 'What can you help me with?'),
|
|
409
409
|
prompt: 'What can you help me with on this tenant?',
|
|
410
|
-
icon: <
|
|
410
|
+
icon: <AiIcon className="size-4" />,
|
|
411
411
|
},
|
|
412
412
|
{
|
|
413
413
|
label: t('ai_assistant.launcher.welcome.suggestion2', 'Show what data you can access'),
|
|
@@ -467,7 +467,7 @@ export function AiAssistantLauncher({
|
|
|
467
467
|
aria-label={triggerLabel}
|
|
468
468
|
title={triggerLabel}
|
|
469
469
|
>
|
|
470
|
-
<
|
|
470
|
+
<AiIcon className="size-4" />
|
|
471
471
|
<span>{shortLabel}</span>
|
|
472
472
|
<span className="ml-2 rounded border px-1 text-xs text-muted-foreground">
|
|
473
473
|
⌘L
|
|
@@ -483,7 +483,7 @@ export function AiAssistantLauncher({
|
|
|
483
483
|
aria-label={triggerLabel}
|
|
484
484
|
data-ai-launcher-trigger-mobile=""
|
|
485
485
|
>
|
|
486
|
-
<
|
|
486
|
+
<AiIcon className="size-4" />
|
|
487
487
|
</IconButton>
|
|
488
488
|
<Dialog open={pickerOpen} onOpenChange={setPickerOpen}>
|
|
489
489
|
<DialogContent
|
|
@@ -493,7 +493,7 @@ export function AiAssistantLauncher({
|
|
|
493
493
|
>
|
|
494
494
|
<DialogHeader className="px-4 pt-4 pb-2">
|
|
495
495
|
<DialogTitle className="flex items-center gap-2 text-base">
|
|
496
|
-
<
|
|
496
|
+
<AiIcon className="size-4" />
|
|
497
497
|
{dialogTitle}
|
|
498
498
|
</DialogTitle>
|
|
499
499
|
<DialogDescription className="text-xs">
|
|
@@ -551,8 +551,8 @@ export function AiAssistantLauncher({
|
|
|
551
551
|
isActive ? 'bg-accent text-accent-foreground' : 'hover:bg-accent/60',
|
|
552
552
|
)}
|
|
553
553
|
>
|
|
554
|
-
<span className="mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-full bg-
|
|
555
|
-
<
|
|
554
|
+
<span className="mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-full bg-brand-violet/10">
|
|
555
|
+
<AiIcon className="size-3.5" />
|
|
556
556
|
</span>
|
|
557
557
|
<span className="flex-1 min-w-0 space-y-0.5">
|
|
558
558
|
<span className="flex items-center gap-2">
|
|
@@ -673,7 +673,7 @@ export function AiAssistantLauncher({
|
|
|
673
673
|
<PanelRightOpen className="size-4" aria-hidden />
|
|
674
674
|
</IconButton>
|
|
675
675
|
<DialogTitle className="flex-1 min-w-0 flex items-center gap-2">
|
|
676
|
-
<
|
|
676
|
+
<AiIcon className="size-4 shrink-0" />
|
|
677
677
|
<span className="min-w-0 truncate">{activeAgent?.label ?? dialogTitle}</span>
|
|
678
678
|
<span
|
|
679
679
|
className="inline-flex shrink-0 items-center rounded-full border border-border bg-secondary px-1.5 py-0 text-[10px] font-medium uppercase tracking-wide text-secondary-foreground"
|