m365-agent-cli 1.2.0
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/LICENSE +22 -0
- package/README.md +916 -0
- package/package.json +50 -0
- package/src/cli.ts +100 -0
- package/src/commands/auto-reply.ts +182 -0
- package/src/commands/calendar.ts +576 -0
- package/src/commands/counter.ts +87 -0
- package/src/commands/create-event.ts +544 -0
- package/src/commands/delegates.ts +286 -0
- package/src/commands/delete-event.ts +321 -0
- package/src/commands/drafts.ts +502 -0
- package/src/commands/files.ts +532 -0
- package/src/commands/find.ts +195 -0
- package/src/commands/findtime.ts +270 -0
- package/src/commands/folders.ts +177 -0
- package/src/commands/forward-event.ts +49 -0
- package/src/commands/graph-calendar.ts +217 -0
- package/src/commands/login.ts +195 -0
- package/src/commands/mail.ts +950 -0
- package/src/commands/oof.ts +263 -0
- package/src/commands/outlook-categories.ts +173 -0
- package/src/commands/outlook-graph.ts +880 -0
- package/src/commands/planner.ts +1678 -0
- package/src/commands/respond.ts +291 -0
- package/src/commands/rooms.ts +210 -0
- package/src/commands/rules.ts +511 -0
- package/src/commands/schedule.ts +109 -0
- package/src/commands/send.ts +204 -0
- package/src/commands/serve.ts +14 -0
- package/src/commands/sharepoint.ts +179 -0
- package/src/commands/site-pages.ts +163 -0
- package/src/commands/subscribe.ts +103 -0
- package/src/commands/subscriptions.ts +29 -0
- package/src/commands/suggest.ts +155 -0
- package/src/commands/todo.ts +2092 -0
- package/src/commands/update-event.ts +608 -0
- package/src/commands/update.ts +88 -0
- package/src/commands/verify-token.ts +62 -0
- package/src/commands/whoami.ts +74 -0
- package/src/index.ts +190 -0
- package/src/lib/atomic-write.ts +20 -0
- package/src/lib/attach-link-spec.test.ts +24 -0
- package/src/lib/attach-link-spec.ts +70 -0
- package/src/lib/attachments.ts +79 -0
- package/src/lib/auth.ts +192 -0
- package/src/lib/calendar-range.test.ts +41 -0
- package/src/lib/calendar-range.ts +103 -0
- package/src/lib/dates.test.ts +74 -0
- package/src/lib/dates.ts +137 -0
- package/src/lib/delegate-client.test.ts +74 -0
- package/src/lib/delegate-client.ts +322 -0
- package/src/lib/ews-client.ts +3418 -0
- package/src/lib/git-commit.ts +4 -0
- package/src/lib/glitchtip-eligibility.ts +220 -0
- package/src/lib/glitchtip.ts +253 -0
- package/src/lib/global-env.ts +3 -0
- package/src/lib/graph-auth.ts +223 -0
- package/src/lib/graph-calendar-client.test.ts +118 -0
- package/src/lib/graph-calendar-client.ts +112 -0
- package/src/lib/graph-client.test.ts +107 -0
- package/src/lib/graph-client.ts +1058 -0
- package/src/lib/graph-constants.ts +12 -0
- package/src/lib/graph-directory.ts +116 -0
- package/src/lib/graph-event.ts +134 -0
- package/src/lib/graph-schedule.ts +173 -0
- package/src/lib/graph-subscriptions.ts +94 -0
- package/src/lib/graph-user-path.ts +13 -0
- package/src/lib/jwt-utils.ts +34 -0
- package/src/lib/markdown.test.ts +21 -0
- package/src/lib/markdown.ts +174 -0
- package/src/lib/mime-type.ts +106 -0
- package/src/lib/oof-client.test.ts +59 -0
- package/src/lib/oof-client.ts +122 -0
- package/src/lib/outlook-graph-client.test.ts +146 -0
- package/src/lib/outlook-graph-client.ts +649 -0
- package/src/lib/outlook-master-categories.ts +145 -0
- package/src/lib/package-info.ts +59 -0
- package/src/lib/places-client.ts +144 -0
- package/src/lib/planner-client.ts +1226 -0
- package/src/lib/rules-client.ts +178 -0
- package/src/lib/sharepoint-client.ts +101 -0
- package/src/lib/site-pages-client.ts +73 -0
- package/src/lib/todo-client.test.ts +298 -0
- package/src/lib/todo-client.ts +1309 -0
- package/src/lib/url-validation.ts +40 -0
- package/src/lib/utils.ts +45 -0
- package/src/lib/webhook-server.ts +51 -0
- package/src/test/auth.test.ts +104 -0
- package/src/test/cli.integration.test.ts +1083 -0
- package/src/test/ews-client.test.ts +268 -0
- package/src/test/mocks/index.ts +375 -0
- package/src/test/mocks/responses.ts +861 -0
|
@@ -0,0 +1,1226 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
callGraph,
|
|
4
|
+
callGraphAbsolute,
|
|
5
|
+
callGraphAt,
|
|
6
|
+
fetchAllPages,
|
|
7
|
+
GraphApiError,
|
|
8
|
+
type GraphResponse,
|
|
9
|
+
graphError,
|
|
10
|
+
graphResult
|
|
11
|
+
} from './graph-client.js';
|
|
12
|
+
import { GRAPH_BASE_URL, GRAPH_BETA_URL } from './graph-constants.js';
|
|
13
|
+
|
|
14
|
+
export interface PlannerPlanContainer {
|
|
15
|
+
'@odata.type'?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
containerId?: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PlannerPlan {
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
/** @deprecated Prefer `container`; Graph may still return this for group-backed plans. */
|
|
25
|
+
owner?: string;
|
|
26
|
+
container?: PlannerPlanContainer;
|
|
27
|
+
'@odata.etag'?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PlannerBucket {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
planId: string;
|
|
34
|
+
orderHint?: string;
|
|
35
|
+
'@odata.etag'?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Planner label slots (plan defines display names in plan details). */
|
|
39
|
+
export type PlannerCategorySlot = 'category1' | 'category2' | 'category3' | 'category4' | 'category5' | 'category6';
|
|
40
|
+
|
|
41
|
+
export type PlannerAppliedCategories = Partial<Record<PlannerCategorySlot, boolean>>;
|
|
42
|
+
|
|
43
|
+
export interface PlannerTask {
|
|
44
|
+
id: string;
|
|
45
|
+
planId: string;
|
|
46
|
+
bucketId: string;
|
|
47
|
+
title: string;
|
|
48
|
+
orderHint: string;
|
|
49
|
+
assigneePriority: string;
|
|
50
|
+
percentComplete: number;
|
|
51
|
+
hasDescription: boolean;
|
|
52
|
+
createdDateTime: string;
|
|
53
|
+
/** Planner due/start (ISO date-time strings per Graph). */
|
|
54
|
+
dueDateTime?: string;
|
|
55
|
+
startDateTime?: string;
|
|
56
|
+
/** Teams / Outlook thread id when linked. */
|
|
57
|
+
conversationThreadId?: string;
|
|
58
|
+
/** 0 (highest) .. 10 (lowest). */
|
|
59
|
+
priority?: number;
|
|
60
|
+
/** Card preview on task: automatic, noPreview, checklist, description, reference. */
|
|
61
|
+
previewType?: string;
|
|
62
|
+
assignments?: Record<string, any>;
|
|
63
|
+
/** Label slots (boolean per category1..category6); names come from plan details. */
|
|
64
|
+
appliedCategories?: PlannerAppliedCategories;
|
|
65
|
+
'@odata.etag'?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Checklist entry on Planner task details (keyed by client-generated id in PATCH body). */
|
|
69
|
+
export interface PlannerTaskDetailsChecklistItem {
|
|
70
|
+
'@odata.type'?: string;
|
|
71
|
+
isChecked: boolean;
|
|
72
|
+
title: string;
|
|
73
|
+
orderHint: string;
|
|
74
|
+
lastModifiedDateTime?: string;
|
|
75
|
+
lastModifiedBy?: { user?: { id?: string } };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PlannerTaskDetails {
|
|
79
|
+
id: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
checklist?: Record<string, PlannerTaskDetailsChecklistItem>;
|
|
82
|
+
references?: Record<string, unknown>;
|
|
83
|
+
previewType?: string;
|
|
84
|
+
'@odata.etag'?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface PlannerPlanDetails {
|
|
88
|
+
id: string;
|
|
89
|
+
categoryDescriptions?: Partial<Record<PlannerCategorySlot, string>>;
|
|
90
|
+
sharedWith?: Record<string, boolean>;
|
|
91
|
+
'@odata.etag'?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const PLANNER_SLOTS: PlannerCategorySlot[] = [
|
|
95
|
+
'category1',
|
|
96
|
+
'category2',
|
|
97
|
+
'category3',
|
|
98
|
+
'category4',
|
|
99
|
+
'category5',
|
|
100
|
+
'category6'
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/** Accept `1`..`6` or `category1`..`category6` (case-insensitive). */
|
|
104
|
+
export function parsePlannerLabelKey(input: string): PlannerCategorySlot | null {
|
|
105
|
+
const t = input.trim().toLowerCase();
|
|
106
|
+
const m = t.match(/^category([1-6])$/);
|
|
107
|
+
if (m) return `category${m[1]}` as PlannerCategorySlot;
|
|
108
|
+
if (/^[1-6]$/.test(t)) return `category${t}` as PlannerCategorySlot;
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Build a full slot map for PATCH (Planner expects explicit booleans per slot). */
|
|
113
|
+
export function normalizeAppliedCategories(
|
|
114
|
+
current: PlannerAppliedCategories | undefined,
|
|
115
|
+
patch: { clearAll?: boolean; setTrue?: PlannerCategorySlot[]; setFalse?: PlannerCategorySlot[] }
|
|
116
|
+
): PlannerAppliedCategories {
|
|
117
|
+
const out: PlannerAppliedCategories = {};
|
|
118
|
+
for (const s of PLANNER_SLOTS) {
|
|
119
|
+
if (patch.clearAll) {
|
|
120
|
+
out[s] = false;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
let v = current?.[s] === true;
|
|
124
|
+
for (const u of patch.setTrue ?? []) if (u === s) v = true;
|
|
125
|
+
for (const u of patch.setFalse ?? []) if (u === s) v = false;
|
|
126
|
+
out[s] = v;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Build `assignments` for create/update from user object IDs. */
|
|
132
|
+
export function buildPlannerAssignments(assigneeUserIds: string[]): Record<string, unknown> {
|
|
133
|
+
const out: Record<string, unknown> = {};
|
|
134
|
+
for (const id of assigneeUserIds) {
|
|
135
|
+
out[id] = {
|
|
136
|
+
'@odata.type': '#microsoft.graph.plannerAssignment',
|
|
137
|
+
orderHint: ' !'
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Merge assignees: add and remove user IDs from current `assignments` map. */
|
|
144
|
+
export function mergePlannerAssignments(
|
|
145
|
+
current: Record<string, unknown> | undefined,
|
|
146
|
+
addUserIds: string[],
|
|
147
|
+
removeUserIds: string[]
|
|
148
|
+
): Record<string, unknown> {
|
|
149
|
+
const out: Record<string, unknown> = { ...(current || {}) };
|
|
150
|
+
for (const r of removeUserIds) delete out[r];
|
|
151
|
+
for (const a of addUserIds) {
|
|
152
|
+
out[a] = {
|
|
153
|
+
'@odata.type': '#microsoft.graph.plannerAssignment',
|
|
154
|
+
orderHint: ' !'
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function listUserTasks(token: string): Promise<GraphResponse<PlannerTask[]>> {
|
|
161
|
+
return fetchAllPages<PlannerTask>(token, '/me/planner/tasks', 'Failed to list tasks');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function listUserPlans(token: string): Promise<GraphResponse<PlannerPlan[]>> {
|
|
165
|
+
return fetchAllPages<PlannerPlan>(token, '/me/planner/plans', 'Failed to list plans');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* List Planner tasks for a user (`GET /users/{id}/planner/tasks`).
|
|
170
|
+
* Graph may return 403 for users other than the signed-in user depending on tenant and token.
|
|
171
|
+
*/
|
|
172
|
+
export async function listPlannerTasksForUser(token: string, userId: string): Promise<GraphResponse<PlannerTask[]>> {
|
|
173
|
+
return fetchAllPages<PlannerTask>(
|
|
174
|
+
token,
|
|
175
|
+
`/users/${encodeURIComponent(userId)}/planner/tasks`,
|
|
176
|
+
'Failed to list tasks for user'
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* List Planner plans for a user (`GET /users/{id}/planner/plans`).
|
|
182
|
+
* Same permission caveats as {@link listPlannerTasksForUser}.
|
|
183
|
+
*/
|
|
184
|
+
export async function listPlannerPlansForUser(token: string, userId: string): Promise<GraphResponse<PlannerPlan[]>> {
|
|
185
|
+
return fetchAllPages<PlannerPlan>(
|
|
186
|
+
token,
|
|
187
|
+
`/users/${encodeURIComponent(userId)}/planner/plans`,
|
|
188
|
+
'Failed to list plans for user'
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function listGroupPlans(token: string, groupId: string): Promise<GraphResponse<PlannerPlan[]>> {
|
|
193
|
+
return fetchAllPages<PlannerPlan>(
|
|
194
|
+
token,
|
|
195
|
+
`/groups/${encodeURIComponent(groupId)}/planner/plans`,
|
|
196
|
+
'Failed to list group plans'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function listPlanBuckets(token: string, planId: string): Promise<GraphResponse<PlannerBucket[]>> {
|
|
201
|
+
return fetchAllPages<PlannerBucket>(
|
|
202
|
+
token,
|
|
203
|
+
`/planner/plans/${encodeURIComponent(planId)}/buckets`,
|
|
204
|
+
'Failed to list buckets'
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function listPlanTasks(token: string, planId: string): Promise<GraphResponse<PlannerTask[]>> {
|
|
209
|
+
return fetchAllPages<PlannerTask>(
|
|
210
|
+
token,
|
|
211
|
+
`/planner/plans/${encodeURIComponent(planId)}/tasks`,
|
|
212
|
+
'Failed to list plan tasks'
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function getPlanDetails(token: string, planId: string): Promise<GraphResponse<PlannerPlanDetails>> {
|
|
217
|
+
try {
|
|
218
|
+
const result = await callGraph<PlannerPlanDetails>(token, `/planner/plans/${encodeURIComponent(planId)}/details`);
|
|
219
|
+
if (!result.ok || !result.data) {
|
|
220
|
+
return graphError(
|
|
221
|
+
result.error?.message || 'Failed to get plan details',
|
|
222
|
+
result.error?.code,
|
|
223
|
+
result.error?.status
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return graphResult(result.data);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
229
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get plan details');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function getTask(token: string, taskId: string): Promise<GraphResponse<PlannerTask>> {
|
|
234
|
+
try {
|
|
235
|
+
const result = await callGraph<PlannerTask>(token, `/planner/tasks/${encodeURIComponent(taskId)}`);
|
|
236
|
+
if (!result.ok || !result.data) {
|
|
237
|
+
return graphError(result.error?.message || 'Failed to get task', result.error?.code, result.error?.status);
|
|
238
|
+
}
|
|
239
|
+
return graphResult(result.data);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
242
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get task');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface CreatePlannerTaskExtras {
|
|
247
|
+
/** ISO date-time after creation (PATCH). */
|
|
248
|
+
dueDateTime?: string | null;
|
|
249
|
+
startDateTime?: string | null;
|
|
250
|
+
conversationThreadId?: string | null;
|
|
251
|
+
orderHint?: string | null;
|
|
252
|
+
assigneePriority?: string | null;
|
|
253
|
+
priority?: number | null;
|
|
254
|
+
previewType?: string | null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function createTask(
|
|
258
|
+
token: string,
|
|
259
|
+
planId: string,
|
|
260
|
+
title: string,
|
|
261
|
+
bucketId?: string,
|
|
262
|
+
assignments?: Record<string, any>,
|
|
263
|
+
appliedCategories?: PlannerAppliedCategories,
|
|
264
|
+
extras?: CreatePlannerTaskExtras
|
|
265
|
+
): Promise<GraphResponse<PlannerTask>> {
|
|
266
|
+
try {
|
|
267
|
+
const body: Record<string, unknown> = { planId, title };
|
|
268
|
+
if (bucketId) body.bucketId = bucketId;
|
|
269
|
+
if (assignments) body.assignments = assignments;
|
|
270
|
+
|
|
271
|
+
const result = await callGraph<PlannerTask>(token, '/planner/tasks', {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
body: JSON.stringify(body)
|
|
274
|
+
});
|
|
275
|
+
if (!result.ok || !result.data) {
|
|
276
|
+
return graphError(result.error?.message || 'Failed to create task', result.error?.code, result.error?.status);
|
|
277
|
+
}
|
|
278
|
+
let task = result.data;
|
|
279
|
+
const etag1 = task['@odata.etag'];
|
|
280
|
+
const patchPayload: Record<string, unknown> = {};
|
|
281
|
+
if (appliedCategories && Object.keys(appliedCategories).length > 0) {
|
|
282
|
+
patchPayload.appliedCategories = appliedCategories;
|
|
283
|
+
}
|
|
284
|
+
if (extras?.dueDateTime !== undefined && extras.dueDateTime !== null) {
|
|
285
|
+
patchPayload.dueDateTime = extras.dueDateTime;
|
|
286
|
+
}
|
|
287
|
+
if (extras?.startDateTime !== undefined && extras.startDateTime !== null) {
|
|
288
|
+
patchPayload.startDateTime = extras.startDateTime;
|
|
289
|
+
}
|
|
290
|
+
if (extras?.conversationThreadId !== undefined && extras.conversationThreadId !== null) {
|
|
291
|
+
patchPayload.conversationThreadId = extras.conversationThreadId;
|
|
292
|
+
}
|
|
293
|
+
if (extras?.orderHint !== undefined && extras.orderHint !== null) {
|
|
294
|
+
patchPayload.orderHint = extras.orderHint;
|
|
295
|
+
}
|
|
296
|
+
if (extras?.assigneePriority !== undefined && extras.assigneePriority !== null) {
|
|
297
|
+
patchPayload.assigneePriority = extras.assigneePriority;
|
|
298
|
+
}
|
|
299
|
+
if (extras?.priority !== undefined && extras.priority !== null) {
|
|
300
|
+
patchPayload.priority = extras.priority;
|
|
301
|
+
}
|
|
302
|
+
if (extras?.previewType !== undefined && extras.previewType !== null) {
|
|
303
|
+
patchPayload.previewType = extras.previewType;
|
|
304
|
+
}
|
|
305
|
+
if (Object.keys(patchPayload).length > 0) {
|
|
306
|
+
if (!etag1) {
|
|
307
|
+
return graphError('Created task missing ETag; cannot set fields', 'MISSING_ETAG', 500);
|
|
308
|
+
}
|
|
309
|
+
const patch = await callGraph<void>(token, `/planner/tasks/${encodeURIComponent(task.id)}`, {
|
|
310
|
+
method: 'PATCH',
|
|
311
|
+
headers: { 'If-Match': etag1 },
|
|
312
|
+
body: JSON.stringify(patchPayload)
|
|
313
|
+
});
|
|
314
|
+
if (!patch.ok) {
|
|
315
|
+
return graphError(patch.error?.message || 'Failed to update new task', patch.error?.code, patch.error?.status);
|
|
316
|
+
}
|
|
317
|
+
const again = await getTask(token, task.id);
|
|
318
|
+
if (again.ok && again.data) task = again.data;
|
|
319
|
+
}
|
|
320
|
+
return graphResult(task);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
323
|
+
return graphError(err instanceof Error ? err.message : 'Failed to create task');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function updateTask(
|
|
328
|
+
token: string,
|
|
329
|
+
taskId: string,
|
|
330
|
+
etag: string,
|
|
331
|
+
updates: {
|
|
332
|
+
title?: string;
|
|
333
|
+
bucketId?: string;
|
|
334
|
+
assignments?: Record<string, any> | null;
|
|
335
|
+
percentComplete?: number;
|
|
336
|
+
appliedCategories?: PlannerAppliedCategories;
|
|
337
|
+
dueDateTime?: string | null;
|
|
338
|
+
startDateTime?: string | null;
|
|
339
|
+
orderHint?: string | null;
|
|
340
|
+
conversationThreadId?: string | null;
|
|
341
|
+
assigneePriority?: string | null;
|
|
342
|
+
priority?: number | null;
|
|
343
|
+
previewType?: string | null;
|
|
344
|
+
}
|
|
345
|
+
): Promise<GraphResponse<void>> {
|
|
346
|
+
try {
|
|
347
|
+
const result = await callGraph<void>(token, `/planner/tasks/${encodeURIComponent(taskId)}`, {
|
|
348
|
+
method: 'PATCH',
|
|
349
|
+
headers: {
|
|
350
|
+
'If-Match': etag
|
|
351
|
+
},
|
|
352
|
+
body: JSON.stringify(updates)
|
|
353
|
+
});
|
|
354
|
+
if (!result.ok) {
|
|
355
|
+
return graphError(result.error?.message || 'Failed to update task', result.error?.code, result.error?.status);
|
|
356
|
+
}
|
|
357
|
+
return graphResult(undefined as undefined);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
360
|
+
return graphError(err instanceof Error ? err.message : 'Failed to update task');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function getPlannerPlan(token: string, planId: string): Promise<GraphResponse<PlannerPlan>> {
|
|
365
|
+
try {
|
|
366
|
+
const result = await callGraph<PlannerPlan>(token, `/planner/plans/${encodeURIComponent(planId)}`);
|
|
367
|
+
if (!result.ok || !result.data) {
|
|
368
|
+
return graphError(result.error?.message || 'Failed to get plan', result.error?.code, result.error?.status);
|
|
369
|
+
}
|
|
370
|
+
return graphResult(result.data);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
373
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get plan');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function createPlannerPlan(
|
|
378
|
+
token: string,
|
|
379
|
+
ownerGroupId: string,
|
|
380
|
+
title: string
|
|
381
|
+
): Promise<GraphResponse<PlannerPlan>> {
|
|
382
|
+
try {
|
|
383
|
+
const base = GRAPH_BASE_URL.replace(/\/$/, '');
|
|
384
|
+
const result = await callGraph<PlannerPlan>(token, '/planner/plans', {
|
|
385
|
+
method: 'POST',
|
|
386
|
+
body: JSON.stringify({
|
|
387
|
+
title,
|
|
388
|
+
container: {
|
|
389
|
+
url: `${base}/groups/${encodeURIComponent(ownerGroupId)}`
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
});
|
|
393
|
+
if (!result.ok || !result.data) {
|
|
394
|
+
return graphError(result.error?.message || 'Failed to create plan', result.error?.code, result.error?.status);
|
|
395
|
+
}
|
|
396
|
+
return graphResult(result.data);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
399
|
+
return graphError(err instanceof Error ? err.message : 'Failed to create plan');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export async function updatePlannerPlan(
|
|
404
|
+
token: string,
|
|
405
|
+
planId: string,
|
|
406
|
+
etag: string,
|
|
407
|
+
title: string
|
|
408
|
+
): Promise<GraphResponse<void>> {
|
|
409
|
+
try {
|
|
410
|
+
const result = await callGraph<void>(token, `/planner/plans/${encodeURIComponent(planId)}`, {
|
|
411
|
+
method: 'PATCH',
|
|
412
|
+
headers: { 'If-Match': etag },
|
|
413
|
+
body: JSON.stringify({ title })
|
|
414
|
+
});
|
|
415
|
+
if (!result.ok) {
|
|
416
|
+
return graphError(result.error?.message || 'Failed to update plan', result.error?.code, result.error?.status);
|
|
417
|
+
}
|
|
418
|
+
return graphResult(undefined as undefined);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
421
|
+
return graphError(err instanceof Error ? err.message : 'Failed to update plan');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export async function deletePlannerPlan(token: string, planId: string, etag: string): Promise<GraphResponse<void>> {
|
|
426
|
+
try {
|
|
427
|
+
const result = await callGraph<void>(
|
|
428
|
+
token,
|
|
429
|
+
`/planner/plans/${encodeURIComponent(planId)}`,
|
|
430
|
+
{
|
|
431
|
+
method: 'DELETE',
|
|
432
|
+
headers: { 'If-Match': etag }
|
|
433
|
+
},
|
|
434
|
+
false
|
|
435
|
+
);
|
|
436
|
+
if (!result.ok) {
|
|
437
|
+
return graphError(result.error?.message || 'Failed to delete plan', result.error?.code, result.error?.status);
|
|
438
|
+
}
|
|
439
|
+
return graphResult(undefined as undefined);
|
|
440
|
+
} catch (err) {
|
|
441
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
442
|
+
return graphError(err instanceof Error ? err.message : 'Failed to delete plan');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export async function getPlannerBucket(token: string, bucketId: string): Promise<GraphResponse<PlannerBucket>> {
|
|
447
|
+
try {
|
|
448
|
+
const result = await callGraph<PlannerBucket>(token, `/planner/buckets/${encodeURIComponent(bucketId)}`);
|
|
449
|
+
if (!result.ok || !result.data) {
|
|
450
|
+
return graphError(result.error?.message || 'Failed to get bucket', result.error?.code, result.error?.status);
|
|
451
|
+
}
|
|
452
|
+
return graphResult(result.data);
|
|
453
|
+
} catch (err) {
|
|
454
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
455
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get bucket');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export async function createPlannerBucket(
|
|
460
|
+
token: string,
|
|
461
|
+
planId: string,
|
|
462
|
+
name: string
|
|
463
|
+
): Promise<GraphResponse<PlannerBucket>> {
|
|
464
|
+
try {
|
|
465
|
+
const result = await callGraph<PlannerBucket>(token, '/planner/buckets', {
|
|
466
|
+
method: 'POST',
|
|
467
|
+
body: JSON.stringify({ planId, name, orderHint: ' !' })
|
|
468
|
+
});
|
|
469
|
+
if (!result.ok || !result.data) {
|
|
470
|
+
return graphError(result.error?.message || 'Failed to create bucket', result.error?.code, result.error?.status);
|
|
471
|
+
}
|
|
472
|
+
return graphResult(result.data);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
475
|
+
return graphError(err instanceof Error ? err.message : 'Failed to create bucket');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export async function updatePlannerBucket(
|
|
480
|
+
token: string,
|
|
481
|
+
bucketId: string,
|
|
482
|
+
etag: string,
|
|
483
|
+
updates: { name?: string; orderHint?: string }
|
|
484
|
+
): Promise<GraphResponse<void>> {
|
|
485
|
+
const body: Record<string, unknown> = {};
|
|
486
|
+
if (updates.name !== undefined) body.name = updates.name;
|
|
487
|
+
if (updates.orderHint !== undefined) body.orderHint = updates.orderHint;
|
|
488
|
+
if (Object.keys(body).length === 0) {
|
|
489
|
+
return graphError('No bucket updates', 'NO_UPDATES', 400);
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
const result = await callGraph<void>(token, `/planner/buckets/${encodeURIComponent(bucketId)}`, {
|
|
493
|
+
method: 'PATCH',
|
|
494
|
+
headers: { 'If-Match': etag },
|
|
495
|
+
body: JSON.stringify(body)
|
|
496
|
+
});
|
|
497
|
+
if (!result.ok) {
|
|
498
|
+
return graphError(result.error?.message || 'Failed to update bucket', result.error?.code, result.error?.status);
|
|
499
|
+
}
|
|
500
|
+
return graphResult(undefined as undefined);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
503
|
+
return graphError(err instanceof Error ? err.message : 'Failed to update bucket');
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export async function deletePlannerBucket(token: string, bucketId: string, etag: string): Promise<GraphResponse<void>> {
|
|
508
|
+
try {
|
|
509
|
+
const result = await callGraph<void>(
|
|
510
|
+
token,
|
|
511
|
+
`/planner/buckets/${encodeURIComponent(bucketId)}`,
|
|
512
|
+
{
|
|
513
|
+
method: 'DELETE',
|
|
514
|
+
headers: { 'If-Match': etag }
|
|
515
|
+
},
|
|
516
|
+
false
|
|
517
|
+
);
|
|
518
|
+
if (!result.ok) {
|
|
519
|
+
return graphError(result.error?.message || 'Failed to delete bucket', result.error?.code, result.error?.status);
|
|
520
|
+
}
|
|
521
|
+
return graphResult(undefined as undefined);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
524
|
+
return graphError(err instanceof Error ? err.message : 'Failed to delete bucket');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export async function deletePlannerTask(token: string, taskId: string, etag: string): Promise<GraphResponse<void>> {
|
|
529
|
+
try {
|
|
530
|
+
const result = await callGraph<void>(
|
|
531
|
+
token,
|
|
532
|
+
`/planner/tasks/${encodeURIComponent(taskId)}`,
|
|
533
|
+
{
|
|
534
|
+
method: 'DELETE',
|
|
535
|
+
headers: { 'If-Match': etag }
|
|
536
|
+
},
|
|
537
|
+
false
|
|
538
|
+
);
|
|
539
|
+
if (!result.ok) {
|
|
540
|
+
return graphError(result.error?.message || 'Failed to delete task', result.error?.code, result.error?.status);
|
|
541
|
+
}
|
|
542
|
+
return graphResult(undefined as undefined);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
545
|
+
return graphError(err instanceof Error ? err.message : 'Failed to delete task');
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export async function getPlannerTaskDetails(token: string, taskId: string): Promise<GraphResponse<PlannerTaskDetails>> {
|
|
550
|
+
try {
|
|
551
|
+
const result = await callGraph<PlannerTaskDetails>(token, `/planner/tasks/${encodeURIComponent(taskId)}/details`);
|
|
552
|
+
if (!result.ok || !result.data) {
|
|
553
|
+
return graphError(
|
|
554
|
+
result.error?.message || 'Failed to get task details',
|
|
555
|
+
result.error?.code,
|
|
556
|
+
result.error?.status
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
return graphResult(result.data);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
562
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get task details');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export interface UpdatePlannerTaskDetailsParams {
|
|
567
|
+
description?: string | null;
|
|
568
|
+
checklist?: Record<string, PlannerTaskDetailsChecklistItem> | null;
|
|
569
|
+
references?: Record<string, unknown> | null;
|
|
570
|
+
previewType?: string;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export async function updatePlannerTaskDetails(
|
|
574
|
+
token: string,
|
|
575
|
+
taskDetailsId: string,
|
|
576
|
+
etag: string,
|
|
577
|
+
updates: UpdatePlannerTaskDetailsParams
|
|
578
|
+
): Promise<GraphResponse<void>> {
|
|
579
|
+
try {
|
|
580
|
+
const result = await callGraph<void>(token, `/planner/taskDetails/${encodeURIComponent(taskDetailsId)}`, {
|
|
581
|
+
method: 'PATCH',
|
|
582
|
+
headers: { 'If-Match': etag },
|
|
583
|
+
body: JSON.stringify(updates)
|
|
584
|
+
});
|
|
585
|
+
if (!result.ok) {
|
|
586
|
+
return graphError(
|
|
587
|
+
result.error?.message || 'Failed to update task details',
|
|
588
|
+
result.error?.code,
|
|
589
|
+
result.error?.status
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
return graphResult(undefined as undefined);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
595
|
+
return graphError(err instanceof Error ? err.message : 'Failed to update task details');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export interface UpdatePlannerPlanDetailsParams {
|
|
600
|
+
categoryDescriptions?: Partial<Record<PlannerCategorySlot, string | null>>;
|
|
601
|
+
sharedWith?: Record<string, boolean>;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** PATCH `/planner/plans/{id}/details` (label names, sharedWith). */
|
|
605
|
+
export async function updatePlannerPlanDetails(
|
|
606
|
+
token: string,
|
|
607
|
+
planId: string,
|
|
608
|
+
etag: string,
|
|
609
|
+
updates: UpdatePlannerPlanDetailsParams
|
|
610
|
+
): Promise<GraphResponse<void>> {
|
|
611
|
+
try {
|
|
612
|
+
const result = await callGraph<void>(
|
|
613
|
+
token,
|
|
614
|
+
`/planner/plans/${encodeURIComponent(planId)}/details`,
|
|
615
|
+
{
|
|
616
|
+
method: 'PATCH',
|
|
617
|
+
headers: { 'If-Match': etag },
|
|
618
|
+
body: JSON.stringify(updates)
|
|
619
|
+
},
|
|
620
|
+
false
|
|
621
|
+
);
|
|
622
|
+
if (!result.ok) {
|
|
623
|
+
return graphError(
|
|
624
|
+
result.error?.message || 'Failed to update plan details',
|
|
625
|
+
result.error?.code,
|
|
626
|
+
result.error?.status
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
return graphResult(undefined as undefined);
|
|
630
|
+
} catch (err) {
|
|
631
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
632
|
+
return graphError(err instanceof Error ? err.message : 'Failed to update plan details');
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Beta: plans marked favorite by the current user. */
|
|
637
|
+
export async function listFavoritePlans(token: string): Promise<GraphResponse<PlannerPlan[]>> {
|
|
638
|
+
return fetchAllPages<PlannerPlan>(
|
|
639
|
+
token,
|
|
640
|
+
'/me/planner/favoritePlans',
|
|
641
|
+
'Failed to list favorite plans',
|
|
642
|
+
GRAPH_BETA_URL
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Beta: plans from rosters the user belongs to. */
|
|
647
|
+
export async function listRosterPlans(token: string): Promise<GraphResponse<PlannerPlan[]>> {
|
|
648
|
+
return fetchAllPages<PlannerPlan>(token, '/me/planner/rosterPlans', 'Failed to list roster plans', GRAPH_BETA_URL);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export interface PlannerDeltaPage {
|
|
652
|
+
value?: unknown[];
|
|
653
|
+
'@odata.nextLink'?: string;
|
|
654
|
+
'@odata.deltaLink'?: string;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Beta: one page of `/me/planner/all/delta` (pass `nextLink` or `deltaLink` URL from a prior response). */
|
|
658
|
+
export async function getPlannerDeltaPage(
|
|
659
|
+
token: string,
|
|
660
|
+
nextOrDeltaUrl?: string
|
|
661
|
+
): Promise<GraphResponse<PlannerDeltaPage>> {
|
|
662
|
+
try {
|
|
663
|
+
if (nextOrDeltaUrl) {
|
|
664
|
+
return await callGraphAbsolute<PlannerDeltaPage>(token, nextOrDeltaUrl);
|
|
665
|
+
}
|
|
666
|
+
return await callGraphAt<PlannerDeltaPage>(GRAPH_BETA_URL, token, '/me/planner/all/delta');
|
|
667
|
+
} catch (err) {
|
|
668
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
669
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get Planner delta');
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Add a checklist row (client-generated id) and PATCH task details. */
|
|
674
|
+
export async function addPlannerChecklistItem(
|
|
675
|
+
token: string,
|
|
676
|
+
taskId: string,
|
|
677
|
+
title: string,
|
|
678
|
+
checklistItemId?: string
|
|
679
|
+
): Promise<GraphResponse<void>> {
|
|
680
|
+
const id = checklistItemId || randomUUID();
|
|
681
|
+
const dr = await getPlannerTaskDetails(token, taskId);
|
|
682
|
+
if (!dr.ok || !dr.data) {
|
|
683
|
+
return graphError(dr.error?.message || 'Failed to get task details', dr.error?.code, dr.error?.status);
|
|
684
|
+
}
|
|
685
|
+
const etag = dr.data['@odata.etag'];
|
|
686
|
+
if (!etag) return graphError('Task details missing ETag', 'MISSING_ETAG', 500);
|
|
687
|
+
const checklist = { ...(dr.data.checklist || {}) };
|
|
688
|
+
checklist[id] = {
|
|
689
|
+
'@odata.type': '#microsoft.graph.plannerChecklistItem',
|
|
690
|
+
isChecked: false,
|
|
691
|
+
title,
|
|
692
|
+
orderHint: ' !'
|
|
693
|
+
};
|
|
694
|
+
return updatePlannerTaskDetails(token, dr.data.id, etag, { checklist });
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Remove a checklist item by id. */
|
|
698
|
+
export async function removePlannerChecklistItem(
|
|
699
|
+
token: string,
|
|
700
|
+
taskId: string,
|
|
701
|
+
checklistItemId: string
|
|
702
|
+
): Promise<GraphResponse<void>> {
|
|
703
|
+
const dr = await getPlannerTaskDetails(token, taskId);
|
|
704
|
+
if (!dr.ok || !dr.data) {
|
|
705
|
+
return graphError(dr.error?.message || 'Failed to get task details', dr.error?.code, dr.error?.status);
|
|
706
|
+
}
|
|
707
|
+
const etag = dr.data['@odata.etag'];
|
|
708
|
+
if (!etag) return graphError('Task details missing ETag', 'MISSING_ETAG', 500);
|
|
709
|
+
const checklist = { ...(dr.data.checklist || {}) };
|
|
710
|
+
delete checklist[checklistItemId];
|
|
711
|
+
return updatePlannerTaskDetails(token, dr.data.id, etag, { checklist });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Add or replace a reference URL entry on task details. */
|
|
715
|
+
export async function addPlannerReference(
|
|
716
|
+
token: string,
|
|
717
|
+
taskId: string,
|
|
718
|
+
resourceUrl: string,
|
|
719
|
+
alias: string,
|
|
720
|
+
type?: string
|
|
721
|
+
): Promise<GraphResponse<void>> {
|
|
722
|
+
const dr = await getPlannerTaskDetails(token, taskId);
|
|
723
|
+
if (!dr.ok || !dr.data) {
|
|
724
|
+
return graphError(dr.error?.message || 'Failed to get task details', dr.error?.code, dr.error?.status);
|
|
725
|
+
}
|
|
726
|
+
const etag = dr.data['@odata.etag'];
|
|
727
|
+
if (!etag) return graphError('Task details missing ETag', 'MISSING_ETAG', 500);
|
|
728
|
+
const references = { ...(dr.data.references || {}) };
|
|
729
|
+
references[resourceUrl] = {
|
|
730
|
+
'@odata.type': '#microsoft.graph.plannerExternalReference',
|
|
731
|
+
alias,
|
|
732
|
+
...(type ? { type } : {}),
|
|
733
|
+
previewPriority: ' !'
|
|
734
|
+
};
|
|
735
|
+
return updatePlannerTaskDetails(token, dr.data.id, etag, { references });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** Remove a reference by key URL. */
|
|
739
|
+
export async function removePlannerReference(
|
|
740
|
+
token: string,
|
|
741
|
+
taskId: string,
|
|
742
|
+
resourceUrl: string
|
|
743
|
+
): Promise<GraphResponse<void>> {
|
|
744
|
+
const dr = await getPlannerTaskDetails(token, taskId);
|
|
745
|
+
if (!dr.ok || !dr.data) {
|
|
746
|
+
return graphError(dr.error?.message || 'Failed to get task details', dr.error?.code, dr.error?.status);
|
|
747
|
+
}
|
|
748
|
+
const etag = dr.data['@odata.etag'];
|
|
749
|
+
if (!etag) return graphError('Task details missing ETag', 'MISSING_ETAG', 500);
|
|
750
|
+
const references = { ...(dr.data.references || {}) };
|
|
751
|
+
delete references[resourceUrl];
|
|
752
|
+
return updatePlannerTaskDetails(token, dr.data.id, etag, { references });
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** Update one checklist row (title, checked state, orderHint). */
|
|
756
|
+
export async function updatePlannerChecklistItem(
|
|
757
|
+
token: string,
|
|
758
|
+
taskId: string,
|
|
759
|
+
checklistItemId: string,
|
|
760
|
+
patch: { title?: string; isChecked?: boolean; orderHint?: string }
|
|
761
|
+
): Promise<GraphResponse<void>> {
|
|
762
|
+
const dr = await getPlannerTaskDetails(token, taskId);
|
|
763
|
+
if (!dr.ok || !dr.data) {
|
|
764
|
+
return graphError(dr.error?.message || 'Failed to get task details', dr.error?.code, dr.error?.status);
|
|
765
|
+
}
|
|
766
|
+
const etag = dr.data['@odata.etag'];
|
|
767
|
+
if (!etag) return graphError('Task details missing ETag', 'MISSING_ETAG', 500);
|
|
768
|
+
const checklist = { ...(dr.data.checklist || {}) };
|
|
769
|
+
const cur = checklist[checklistItemId];
|
|
770
|
+
if (!cur) {
|
|
771
|
+
return graphError(`Checklist item not found: ${checklistItemId}`, 'NOT_FOUND', 404);
|
|
772
|
+
}
|
|
773
|
+
checklist[checklistItemId] = {
|
|
774
|
+
...cur,
|
|
775
|
+
'@odata.type': cur['@odata.type'] ?? '#microsoft.graph.plannerChecklistItem',
|
|
776
|
+
...(patch.title !== undefined ? { title: patch.title } : {}),
|
|
777
|
+
...(patch.isChecked !== undefined ? { isChecked: patch.isChecked } : {}),
|
|
778
|
+
...(patch.orderHint !== undefined ? { orderHint: patch.orderHint } : {})
|
|
779
|
+
};
|
|
780
|
+
return updatePlannerTaskDetails(token, dr.data.id, etag, { checklist });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
export interface PlannerAssignedToTaskBoardFormat {
|
|
784
|
+
id: string;
|
|
785
|
+
unassignedOrderHint?: string;
|
|
786
|
+
orderHintsByAssignee?: Record<string, string>;
|
|
787
|
+
'@odata.etag'?: string;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export interface PlannerBucketTaskBoardFormat {
|
|
791
|
+
id: string;
|
|
792
|
+
orderHint?: string;
|
|
793
|
+
'@odata.etag'?: string;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export interface PlannerProgressTaskBoardFormat {
|
|
797
|
+
id: string;
|
|
798
|
+
orderHint?: string;
|
|
799
|
+
'@odata.etag'?: string;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export async function getAssignedToTaskBoardFormat(
|
|
803
|
+
token: string,
|
|
804
|
+
taskId: string
|
|
805
|
+
): Promise<GraphResponse<PlannerAssignedToTaskBoardFormat>> {
|
|
806
|
+
try {
|
|
807
|
+
const result = await callGraph<PlannerAssignedToTaskBoardFormat>(
|
|
808
|
+
token,
|
|
809
|
+
`/planner/tasks/${encodeURIComponent(taskId)}/assignedToTaskBoardFormat`
|
|
810
|
+
);
|
|
811
|
+
if (!result.ok || !result.data) {
|
|
812
|
+
return graphError(
|
|
813
|
+
result.error?.message || 'Failed to get assignedTo task board format',
|
|
814
|
+
result.error?.code,
|
|
815
|
+
result.error?.status
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
return graphResult(result.data);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
821
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get assignedTo task board format');
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export async function updateAssignedToTaskBoardFormat(
|
|
826
|
+
token: string,
|
|
827
|
+
taskId: string,
|
|
828
|
+
etag: string,
|
|
829
|
+
updates: Partial<{ orderHintsByAssignee: Record<string, string> | null; unassignedOrderHint: string | null }>
|
|
830
|
+
): Promise<GraphResponse<void>> {
|
|
831
|
+
try {
|
|
832
|
+
const result = await callGraph<void>(
|
|
833
|
+
token,
|
|
834
|
+
`/planner/tasks/${encodeURIComponent(taskId)}/assignedToTaskBoardFormat`,
|
|
835
|
+
{
|
|
836
|
+
method: 'PATCH',
|
|
837
|
+
headers: { 'If-Match': etag },
|
|
838
|
+
body: JSON.stringify(updates)
|
|
839
|
+
},
|
|
840
|
+
false
|
|
841
|
+
);
|
|
842
|
+
if (!result.ok) {
|
|
843
|
+
return graphError(
|
|
844
|
+
result.error?.message || 'Failed to update assignedTo task board format',
|
|
845
|
+
result.error?.code,
|
|
846
|
+
result.error?.status
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
return graphResult(undefined as undefined);
|
|
850
|
+
} catch (err) {
|
|
851
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
852
|
+
return graphError(err instanceof Error ? err.message : 'Failed to update assignedTo task board format');
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export async function getBucketTaskBoardFormat(
|
|
857
|
+
token: string,
|
|
858
|
+
taskId: string
|
|
859
|
+
): Promise<GraphResponse<PlannerBucketTaskBoardFormat>> {
|
|
860
|
+
try {
|
|
861
|
+
const result = await callGraph<PlannerBucketTaskBoardFormat>(
|
|
862
|
+
token,
|
|
863
|
+
`/planner/tasks/${encodeURIComponent(taskId)}/bucketTaskBoardFormat`
|
|
864
|
+
);
|
|
865
|
+
if (!result.ok || !result.data) {
|
|
866
|
+
return graphError(
|
|
867
|
+
result.error?.message || 'Failed to get bucket task board format',
|
|
868
|
+
result.error?.code,
|
|
869
|
+
result.error?.status
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
return graphResult(result.data);
|
|
873
|
+
} catch (err) {
|
|
874
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
875
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get bucket task board format');
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export async function updateBucketTaskBoardFormat(
|
|
880
|
+
token: string,
|
|
881
|
+
taskId: string,
|
|
882
|
+
etag: string,
|
|
883
|
+
orderHint: string
|
|
884
|
+
): Promise<GraphResponse<void>> {
|
|
885
|
+
try {
|
|
886
|
+
const result = await callGraph<void>(
|
|
887
|
+
token,
|
|
888
|
+
`/planner/tasks/${encodeURIComponent(taskId)}/bucketTaskBoardFormat`,
|
|
889
|
+
{
|
|
890
|
+
method: 'PATCH',
|
|
891
|
+
headers: { 'If-Match': etag },
|
|
892
|
+
body: JSON.stringify({ orderHint })
|
|
893
|
+
},
|
|
894
|
+
false
|
|
895
|
+
);
|
|
896
|
+
if (!result.ok) {
|
|
897
|
+
return graphError(
|
|
898
|
+
result.error?.message || 'Failed to update bucket task board format',
|
|
899
|
+
result.error?.code,
|
|
900
|
+
result.error?.status
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
return graphResult(undefined as undefined);
|
|
904
|
+
} catch (err) {
|
|
905
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
906
|
+
return graphError(err instanceof Error ? err.message : 'Failed to update bucket task board format');
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
export async function getProgressTaskBoardFormat(
|
|
911
|
+
token: string,
|
|
912
|
+
taskId: string
|
|
913
|
+
): Promise<GraphResponse<PlannerProgressTaskBoardFormat>> {
|
|
914
|
+
try {
|
|
915
|
+
const result = await callGraph<PlannerProgressTaskBoardFormat>(
|
|
916
|
+
token,
|
|
917
|
+
`/planner/tasks/${encodeURIComponent(taskId)}/progressTaskBoardFormat`
|
|
918
|
+
);
|
|
919
|
+
if (!result.ok || !result.data) {
|
|
920
|
+
return graphError(
|
|
921
|
+
result.error?.message || 'Failed to get progress task board format',
|
|
922
|
+
result.error?.code,
|
|
923
|
+
result.error?.status
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
return graphResult(result.data);
|
|
927
|
+
} catch (err) {
|
|
928
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
929
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get progress task board format');
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
export async function updateProgressTaskBoardFormat(
|
|
934
|
+
token: string,
|
|
935
|
+
taskId: string,
|
|
936
|
+
etag: string,
|
|
937
|
+
orderHint: string
|
|
938
|
+
): Promise<GraphResponse<void>> {
|
|
939
|
+
try {
|
|
940
|
+
const result = await callGraph<void>(
|
|
941
|
+
token,
|
|
942
|
+
`/planner/tasks/${encodeURIComponent(taskId)}/progressTaskBoardFormat`,
|
|
943
|
+
{
|
|
944
|
+
method: 'PATCH',
|
|
945
|
+
headers: { 'If-Match': etag },
|
|
946
|
+
body: JSON.stringify({ orderHint })
|
|
947
|
+
},
|
|
948
|
+
false
|
|
949
|
+
);
|
|
950
|
+
if (!result.ok) {
|
|
951
|
+
return graphError(
|
|
952
|
+
result.error?.message || 'Failed to update progress task board format',
|
|
953
|
+
result.error?.code,
|
|
954
|
+
result.error?.status
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
return graphResult(undefined as undefined);
|
|
958
|
+
} catch (err) {
|
|
959
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
960
|
+
return graphError(err instanceof Error ? err.message : 'Failed to update progress task board format');
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** Beta: current user's Planner preferences (favorites, recents). */
|
|
965
|
+
export interface PlannerUser {
|
|
966
|
+
id: string;
|
|
967
|
+
'@odata.etag'?: string;
|
|
968
|
+
favoritePlanReferences?: Record<string, unknown>;
|
|
969
|
+
recentPlanReferences?: Record<string, unknown>;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
export async function getPlannerUser(token: string): Promise<GraphResponse<PlannerUser>> {
|
|
973
|
+
try {
|
|
974
|
+
const result = await callGraphAt<PlannerUser>(GRAPH_BETA_URL, token, '/me/planner');
|
|
975
|
+
if (!result.ok || !result.data) {
|
|
976
|
+
return graphError(
|
|
977
|
+
result.error?.message || 'Failed to get planner user',
|
|
978
|
+
result.error?.code,
|
|
979
|
+
result.error?.status
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
return graphResult(result.data);
|
|
983
|
+
} catch (err) {
|
|
984
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
985
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get planner user');
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function patchPlannerUser(
|
|
990
|
+
token: string,
|
|
991
|
+
etag: string,
|
|
992
|
+
body: {
|
|
993
|
+
favoritePlanReferences?: Record<string, unknown> | null;
|
|
994
|
+
recentPlanReferences?: Record<string, unknown> | null;
|
|
995
|
+
}
|
|
996
|
+
): Promise<GraphResponse<PlannerUser | undefined>> {
|
|
997
|
+
try {
|
|
998
|
+
const result = await callGraphAt<PlannerUser>(
|
|
999
|
+
GRAPH_BETA_URL,
|
|
1000
|
+
token,
|
|
1001
|
+
'/me/planner',
|
|
1002
|
+
{
|
|
1003
|
+
method: 'PATCH',
|
|
1004
|
+
headers: {
|
|
1005
|
+
'If-Match': etag,
|
|
1006
|
+
Prefer: 'return=representation'
|
|
1007
|
+
},
|
|
1008
|
+
body: JSON.stringify(body)
|
|
1009
|
+
},
|
|
1010
|
+
true
|
|
1011
|
+
);
|
|
1012
|
+
if (!result.ok) {
|
|
1013
|
+
return graphError(
|
|
1014
|
+
result.error?.message || 'Failed to update planner user',
|
|
1015
|
+
result.error?.code,
|
|
1016
|
+
result.error?.status
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
return graphResult(result.data);
|
|
1020
|
+
} catch (err) {
|
|
1021
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
1022
|
+
return graphError(err instanceof Error ? err.message : 'Failed to update planner user');
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** Beta: add or update a favorite plan entry (PATCH /me/planner merge). */
|
|
1027
|
+
export async function addPlannerFavoritePlan(
|
|
1028
|
+
token: string,
|
|
1029
|
+
planId: string,
|
|
1030
|
+
planTitle: string
|
|
1031
|
+
): Promise<GraphResponse<PlannerUser | undefined>> {
|
|
1032
|
+
const ur = await getPlannerUser(token);
|
|
1033
|
+
if (!ur.ok || !ur.data) {
|
|
1034
|
+
return graphError(ur.error?.message || 'Failed to get planner user', ur.error?.code, ur.error?.status);
|
|
1035
|
+
}
|
|
1036
|
+
const etag = ur.data['@odata.etag'];
|
|
1037
|
+
if (!etag) return graphError('plannerUser missing ETag', 'MISSING_ETAG', 500);
|
|
1038
|
+
return patchPlannerUser(token, etag, {
|
|
1039
|
+
favoritePlanReferences: {
|
|
1040
|
+
[planId]: {
|
|
1041
|
+
'@odata.type': '#microsoft.graph.plannerFavoritePlanReference',
|
|
1042
|
+
orderHint: ' !',
|
|
1043
|
+
planTitle
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/** Beta: remove a plan from favorites (set reference to null). */
|
|
1050
|
+
export async function removePlannerFavoritePlan(
|
|
1051
|
+
token: string,
|
|
1052
|
+
planId: string
|
|
1053
|
+
): Promise<GraphResponse<PlannerUser | undefined>> {
|
|
1054
|
+
const ur = await getPlannerUser(token);
|
|
1055
|
+
if (!ur.ok || !ur.data) {
|
|
1056
|
+
return graphError(ur.error?.message || 'Failed to get planner user', ur.error?.code, ur.error?.status);
|
|
1057
|
+
}
|
|
1058
|
+
const etag = ur.data['@odata.etag'];
|
|
1059
|
+
if (!etag) return graphError('plannerUser missing ETag', 'MISSING_ETAG', 500);
|
|
1060
|
+
const favoritePlanReferences: Record<string, unknown> = { [planId]: null };
|
|
1061
|
+
return patchPlannerUser(token, etag, { favoritePlanReferences });
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/** Beta: security container for roster-backed plans (see Graph `plannerRoster`). */
|
|
1065
|
+
export interface PlannerRoster {
|
|
1066
|
+
id: string;
|
|
1067
|
+
'@odata.type'?: string;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/** Beta: one member of a Planner roster. */
|
|
1071
|
+
export interface PlannerRosterMember {
|
|
1072
|
+
id: string;
|
|
1073
|
+
userId: string;
|
|
1074
|
+
roles?: string[];
|
|
1075
|
+
'@odata.type'?: string;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/** Beta: `POST /planner/rosters` — create an empty roster (then add members and add a plan). */
|
|
1079
|
+
export async function createPlannerRoster(token: string): Promise<GraphResponse<PlannerRoster>> {
|
|
1080
|
+
try {
|
|
1081
|
+
const result = await callGraphAt<PlannerRoster>(GRAPH_BETA_URL, token, '/planner/rosters', {
|
|
1082
|
+
method: 'POST',
|
|
1083
|
+
body: JSON.stringify({ '@odata.type': '#microsoft.graph.plannerRoster' })
|
|
1084
|
+
});
|
|
1085
|
+
if (!result.ok || !result.data) {
|
|
1086
|
+
return graphError(result.error?.message || 'Failed to create roster', result.error?.code, result.error?.status);
|
|
1087
|
+
}
|
|
1088
|
+
return graphResult(result.data);
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
1091
|
+
return graphError(err instanceof Error ? err.message : 'Failed to create roster');
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/** Beta: `GET /planner/rosters/{id}`. */
|
|
1096
|
+
export async function getPlannerRoster(token: string, rosterId: string): Promise<GraphResponse<PlannerRoster>> {
|
|
1097
|
+
try {
|
|
1098
|
+
const result = await callGraphAt<PlannerRoster>(
|
|
1099
|
+
GRAPH_BETA_URL,
|
|
1100
|
+
token,
|
|
1101
|
+
`/planner/rosters/${encodeURIComponent(rosterId)}`
|
|
1102
|
+
);
|
|
1103
|
+
if (!result.ok || !result.data) {
|
|
1104
|
+
return graphError(result.error?.message || 'Failed to get roster', result.error?.code, result.error?.status);
|
|
1105
|
+
}
|
|
1106
|
+
return graphResult(result.data);
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
1109
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get roster');
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/** Beta: `GET /planner/rosters/{id}/members`. */
|
|
1114
|
+
export async function listPlannerRosterMembers(
|
|
1115
|
+
token: string,
|
|
1116
|
+
rosterId: string
|
|
1117
|
+
): Promise<GraphResponse<PlannerRosterMember[]>> {
|
|
1118
|
+
return fetchAllPages<PlannerRosterMember>(
|
|
1119
|
+
token,
|
|
1120
|
+
`/planner/rosters/${encodeURIComponent(rosterId)}/members`,
|
|
1121
|
+
'Failed to list roster members',
|
|
1122
|
+
GRAPH_BETA_URL
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/** Beta: `POST /planner/rosters/{id}/members`. */
|
|
1127
|
+
export async function addPlannerRosterMember(
|
|
1128
|
+
token: string,
|
|
1129
|
+
rosterId: string,
|
|
1130
|
+
userId: string,
|
|
1131
|
+
options?: { tenantId?: string; roles?: string[] }
|
|
1132
|
+
): Promise<GraphResponse<PlannerRosterMember>> {
|
|
1133
|
+
try {
|
|
1134
|
+
const body: Record<string, unknown> = {
|
|
1135
|
+
'@odata.type': '#microsoft.graph.plannerRosterMember',
|
|
1136
|
+
userId
|
|
1137
|
+
};
|
|
1138
|
+
if (options?.tenantId !== undefined) body.tenantId = options.tenantId;
|
|
1139
|
+
if (options?.roles !== undefined && options.roles.length > 0) body.roles = options.roles;
|
|
1140
|
+
const result = await callGraphAt<PlannerRosterMember>(
|
|
1141
|
+
GRAPH_BETA_URL,
|
|
1142
|
+
token,
|
|
1143
|
+
`/planner/rosters/${encodeURIComponent(rosterId)}/members`,
|
|
1144
|
+
{
|
|
1145
|
+
method: 'POST',
|
|
1146
|
+
body: JSON.stringify(body)
|
|
1147
|
+
}
|
|
1148
|
+
);
|
|
1149
|
+
if (!result.ok || !result.data) {
|
|
1150
|
+
return graphError(
|
|
1151
|
+
result.error?.message || 'Failed to add roster member',
|
|
1152
|
+
result.error?.code,
|
|
1153
|
+
result.error?.status
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
return graphResult(result.data);
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
1159
|
+
return graphError(err instanceof Error ? err.message : 'Failed to add roster member');
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/** Beta: `DELETE /planner/rosters/{rosterId}/members/{memberId}` (member id is the roster member resource id). */
|
|
1164
|
+
export async function removePlannerRosterMember(
|
|
1165
|
+
token: string,
|
|
1166
|
+
rosterId: string,
|
|
1167
|
+
memberId: string
|
|
1168
|
+
): Promise<GraphResponse<void>> {
|
|
1169
|
+
try {
|
|
1170
|
+
const result = await callGraphAt<void>(
|
|
1171
|
+
GRAPH_BETA_URL,
|
|
1172
|
+
token,
|
|
1173
|
+
`/planner/rosters/${encodeURIComponent(rosterId)}/members/${encodeURIComponent(memberId)}`,
|
|
1174
|
+
{ method: 'DELETE' },
|
|
1175
|
+
false
|
|
1176
|
+
);
|
|
1177
|
+
if (!result.ok) {
|
|
1178
|
+
return graphError(
|
|
1179
|
+
result.error?.message || 'Failed to remove roster member',
|
|
1180
|
+
result.error?.code,
|
|
1181
|
+
result.error?.status
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
return graphResult(undefined as undefined);
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
1187
|
+
return graphError(err instanceof Error ? err.message : 'Failed to remove roster member');
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Beta: create a plan contained by a roster (`POST /planner/plans` on beta with `container.type` roster).
|
|
1193
|
+
* @see https://learn.microsoft.com/en-us/graph/api/resources/plannerplancontainer
|
|
1194
|
+
*/
|
|
1195
|
+
export async function createPlannerPlanInRoster(
|
|
1196
|
+
token: string,
|
|
1197
|
+
rosterId: string,
|
|
1198
|
+
title: string
|
|
1199
|
+
): Promise<GraphResponse<PlannerPlan>> {
|
|
1200
|
+
try {
|
|
1201
|
+
const base = GRAPH_BETA_URL.replace(/\/$/, '');
|
|
1202
|
+
const result = await callGraphAt<PlannerPlan>(GRAPH_BETA_URL, token, '/planner/plans', {
|
|
1203
|
+
method: 'POST',
|
|
1204
|
+
headers: { Prefer: 'include-unknown-enum-members' },
|
|
1205
|
+
body: JSON.stringify({
|
|
1206
|
+
title,
|
|
1207
|
+
container: {
|
|
1208
|
+
'@odata.type': '#microsoft.graph.plannerPlanContainer',
|
|
1209
|
+
url: `${base}/planner/rosters/${encodeURIComponent(rosterId)}`,
|
|
1210
|
+
type: 'roster'
|
|
1211
|
+
}
|
|
1212
|
+
})
|
|
1213
|
+
});
|
|
1214
|
+
if (!result.ok || !result.data) {
|
|
1215
|
+
return graphError(
|
|
1216
|
+
result.error?.message || 'Failed to create plan in roster',
|
|
1217
|
+
result.error?.code,
|
|
1218
|
+
result.error?.status
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
return graphResult(result.data);
|
|
1222
|
+
} catch (err) {
|
|
1223
|
+
if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
|
|
1224
|
+
return graphError(err instanceof Error ? err.message : 'Failed to create plan in roster');
|
|
1225
|
+
}
|
|
1226
|
+
}
|