Skip to content

Commit 5fe1524

Browse files
XS⚠️ ◾ Added work item cancel status logic (#685)
* added cancel status logic * clean up * use util to format error message
1 parent 98fd091 commit 5fe1524

File tree

16 files changed

+160
-18
lines changed

16 files changed

+160
-18
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE `shaves` ADD COLUMN `portal_work_item_id` text;

src/backend/db/migrations/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
"when": 1767769174030,
1616
"tag": "0001_schema_v2_add_tables",
1717
"breakpoints": true
18+
},
19+
{
20+
"idx": 2,
21+
"version": "6",
22+
"when": 1770622000000,
23+
"tag": "0002_add_portal_work_item_id_to_shaves",
24+
"breakpoints": true
1825
}
1926
]
2027
}

src/backend/db/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export const shaves = sqliteTable(
135135
title: text("title").notNull(),
136136
projectName: text("project_name"),
137137
workItemUrl: text("work_item_url"),
138+
portalWorkItemId: text("portal_work_item_id"),
138139
shaveStatus: text("shave_status").$type<ShaveStatus>().default(ShaveStatus.Unknown).notNull(),
139140
videoEmbedUrl: text("video_embed_url"),
140141
totalTokens: integer("total_tokens"),

src/backend/db/services/shave-service.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { randomUUID } from "node:crypto";
12
import { beforeEach, describe, expect, it } from "vitest";
23
import { ShaveStatus, VideoHostingProvider } from "../../types";
34
import { createTestDb } from "../client";
@@ -142,6 +143,15 @@ describe("ShaveService", () => {
142143
expect(updated?.videoEmbedUrl).toBe(testShaveData.videoEmbedUrl);
143144
});
144145

146+
it("should update portal work item id", () => {
147+
const created = createShave(testShaveData);
148+
149+
const updated = updateShave(created.id, { portalWorkItemId: randomUUID() });
150+
151+
expect(updated).toBeDefined();
152+
expect(updated?.portalWorkItemId).toBeTruthy();
153+
});
154+
145155
it("should return undefined when updating non-existent shave", () => {
146156
const result = updateShave("non-existent-id", { title: "New Title" });
147157
expect(result).toBeUndefined();
@@ -168,6 +178,15 @@ describe("ShaveService", () => {
168178
expect(updated?.shaveStatus).toBe(ShaveStatus.Completed);
169179
});
170180

181+
it("should update to cancelled status", () => {
182+
const created = createShave(testShaveData);
183+
184+
const updated = updateShaveStatus(created.id, ShaveStatus.Cancelled);
185+
186+
expect(updated).toBeDefined();
187+
expect(updated?.shaveStatus).toBe(ShaveStatus.Cancelled);
188+
});
189+
171190
it("should return undefined when updating non-existent shave", () => {
172191
const result = updateShaveStatus("non-existent-id", ShaveStatus.Completed);
173192
expect(result).toBeUndefined();

src/backend/ipc/channels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export const IPC_CHANNELS = {
9898

9999
// Portal API
100100
PORTAL_GET_MY_SHAVES: "portal:get-my-shaves",
101+
PORTAL_CANCEL_WORK_ITEM: "portal:cancel-work-item",
101102

102103
// Shave Management
103104
SHAVE_CREATE: "shave:create",

src/backend/ipc/portal-handlers.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,38 @@ export function registerPortalHandlers(microsoftAuthService: MicrosoftAuthServic
6565
return { success: false, error: formatErrorMessage(error) };
6666
}
6767
});
68+
69+
ipcMain.handle(IPC_CHANNELS.PORTAL_CANCEL_WORK_ITEM, async (_event, workItemId?: string) => {
70+
if (!workItemId) {
71+
return { success: false, error: "Work item id is required" };
72+
}
73+
74+
try {
75+
const result = await microsoftAuthService.getToken();
76+
77+
const apiUrl = config.portalApiUrl();
78+
const portalApiUrl = new URL(apiUrl);
79+
const requestUrl = new URL(portalApiUrl.origin);
80+
requestUrl.pathname = `${portalApiUrl.pathname.replace(/\/$/, "")}/desktopapp/work-items/${workItemId}:cancel`;
81+
82+
const response = await fetch(requestUrl.toString(), {
83+
method: "POST",
84+
headers: {
85+
Authorization: `Bearer ${result.accessToken}`,
86+
Accept: "application/json",
87+
},
88+
});
89+
90+
if (!response.ok) {
91+
const errorText = await response.text();
92+
console.warn("Portal API error:", errorText);
93+
return { success: false, error: errorText || response.statusText } as const;
94+
}
95+
96+
return { success: true } as const;
97+
} catch (error) {
98+
console.error("Portal API error:", formatErrorMessage(error));
99+
return { success: false, error: formatErrorMessage(error) } as const;
100+
}
101+
});
68102
}

src/backend/ipc/process-video-handlers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,13 @@ export class ProcessVideoIPCHandlers {
305305
const errorMessage = formatErrorMessage(portalResult.error);
306306
notify(ProgressStage.ERROR, { error: errorMessage });
307307
workflowManager.failStage(WorkflowProgressStage.UPDATING_METADATA, errorMessage);
308+
} else if (shaveId) {
309+
try {
310+
const shaveService = ShaveService.getInstance();
311+
shaveService.updateShave(shaveId, { portalWorkItemId: portalResult.workItemId });
312+
} catch (savePortalIdError) {
313+
console.warn("[ProcessVideo] Failed to persist portal work item id", savePortalIdError);
314+
}
308315
}
309316
}
310317

src/backend/preload.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ const IPC_CHANNELS = {
116116

117117
// Portal API
118118
PORTAL_GET_MY_SHAVES: "portal:get-my-shaves",
119+
PORTAL_CANCEL_WORK_ITEM: "portal:cancel-work-item",
119120

120121
// Shave Management
121122
SHAVE_CREATE: "shave:create",
@@ -136,14 +137,14 @@ const onIpcEvent = <T>(channel: string, callback: (payload: T) => void) => {
136137

137138
const electronAPI = {
138139
pipelines: {
139-
processVideoFile: (filePath: string, shaveId?: number) =>
140+
processVideoFile: (filePath: string, shaveId?: string) =>
140141
ipcRenderer.invoke(IPC_CHANNELS.PROCESS_VIDEO_FILE, filePath, shaveId),
141-
processVideoUrl: (url: string, shaveId?: number) =>
142+
processVideoUrl: (url: string, shaveId?: string) =>
142143
ipcRenderer.invoke(IPC_CHANNELS.PROCESS_VIDEO_URL, url, shaveId),
143144
retryVideo: (
144145
intermediateOutput: string,
145146
videoUploadResult: VideoUploadResult,
146-
shaveId?: number,
147+
shaveId?: string,
147148
) =>
148149
ipcRenderer.invoke(IPC_CHANNELS.RETRY_VIDEO, intermediateOutput, videoUploadResult, shaveId),
149150
},
@@ -314,6 +315,8 @@ const electronAPI = {
314315
},
315316
portal: {
316317
getMyShaves: () => ipcRenderer.invoke(IPC_CHANNELS.PORTAL_GET_MY_SHAVES),
318+
cancelWorkItem: (workItemId: string) =>
319+
ipcRenderer.invoke(IPC_CHANNELS.PORTAL_CANCEL_WORK_ITEM, workItemId),
317320
},
318321
shave: {
319322
create: (

src/backend/services/portal/actions.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { basename } from "node:path";
44
import FormData from "form-data";
55
import { z } from "zod";
66
import { config } from "../../config/env";
7+
import { formatErrorMessage } from "../../utils/error-utils";
78
import { MicrosoftAuthService } from "../auth/microsoft-auth";
89

910
/**
@@ -80,20 +81,24 @@ export const WorkItemDtoSchema = z.object({
8081
});
8182
export type WorkItemDto = z.infer<typeof WorkItemDtoSchema>;
8283

84+
const PostTaskRecordResponseSchema = z.object({
85+
workItemId: z.string().uuid(),
86+
});
87+
8388
/**
8489
* Sends work item details to the portal API.
8590
* Requires the user to be authenticated with Microsoft.
8691
*/
8792
export async function SendWorkItemDetailsToPortal(
8893
payload: WorkItemDto,
89-
): Promise<{ success: true } | { success: false; error: string }> {
94+
): Promise<{ success: true; workItemId: string } | { success: false; error: string }> {
9095
const ms = MicrosoftAuthService.getInstance();
9196
const result = await ms.getToken();
9297

9398
const body = JSON.stringify(payload);
9499

95100
try {
96-
await makePortalRequest(
101+
const responseData = await makePortalRequest(
97102
"/desktopapp/post-task-record",
98103
{
99104
headers: {
@@ -104,6 +109,34 @@ export async function SendWorkItemDetailsToPortal(
104109
result.accessToken,
105110
);
106111

112+
const parsed = JSON.parse(responseData);
113+
const validated = PostTaskRecordResponseSchema.parse(parsed);
114+
return { success: true, workItemId: validated.workItemId } as const;
115+
} catch (error) {
116+
const message = formatErrorMessage(error);
117+
return { success: false, error: message } as const;
118+
}
119+
}
120+
121+
export async function CancelWorkItemInPortal(
122+
workItemId: string,
123+
): Promise<{ success: true } | { success: false; error: string }> {
124+
const ms = MicrosoftAuthService.getInstance();
125+
if (!(await ms.isAuthenticated())) {
126+
return { success: false, error: "User is not authenticated with Microsoft" };
127+
}
128+
const result = await ms.getToken();
129+
130+
try {
131+
await makePortalRequest(
132+
`/desktopapp/work-items/${workItemId}:cancel`,
133+
{
134+
headers: {
135+
"Content-Type": "application/json",
136+
},
137+
},
138+
result.accessToken,
139+
);
107140
return { success: true } as const;
108141
} catch (error) {
109142
const message = error instanceof Error ? error.message : String(error);
@@ -169,7 +202,7 @@ export async function UploadScreenshotToPortal(
169202

170203
return { success: true, url: validatedResponse.url };
171204
} catch (error) {
172-
const message = error instanceof Error ? error.message : String(error);
205+
const message = formatErrorMessage(error);
173206
return { success: false, error: message };
174207
}
175208
}

src/backend/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export enum ShaveStatus {
4141
Pending = "Pending",
4242
Processing = "Processing",
4343
Completed = "Completed",
44+
Cancelled = "Cancelled",
4445
Failed = "Failed",
4546
}
4647

0 commit comments

Comments
 (0)