@plandesk/api 0.7.0 → 0.9.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/dist/events.d.ts +20 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -1
- package/dist/routes/agent-runs.d.ts +1 -1
- package/dist/routes/agent-runs.js +20 -0
- package/dist/routes/documents.js +2 -0
- package/dist/routes/folders.d.ts +4 -0
- package/dist/routes/folders.js +69 -0
- package/dist/routes/goals.d.ts +4 -0
- package/dist/routes/goals.js +138 -0
- package/dist/routes/projects.d.ts +1 -1
- package/dist/routes/projects.js +23 -3
- package/dist/routes/submissions.d.ts +5 -0
- package/dist/routes/submissions.js +58 -0
- package/dist/routes/tags.d.ts +4 -0
- package/dist/routes/tags.js +62 -0
- package/dist/routes/tasks.d.ts +1 -0
- package/dist/routes/tasks.js +9 -1
- package/dist/serialize.d.ts +51 -2
- package/dist/serialize.js +101 -1
- package/dist/server.js +9 -1
- package/dist/services/canvas.d.ts +4 -0
- package/dist/services/canvas.js +3 -2
- package/dist/services/documents.d.ts +5 -1
- package/dist/services/documents.js +31 -2
- package/dist/services/folders.d.ts +27 -0
- package/dist/services/folders.js +107 -0
- package/dist/services/goals.d.ts +239 -0
- package/dist/services/goals.js +323 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +9 -0
- package/dist/services/projects.js +8 -2
- package/dist/services/sync.d.ts +5 -3
- package/dist/services/sync.js +45 -2
- package/dist/services/tags.d.ts +27 -0
- package/dist/services/tags.js +79 -0
- package/dist/services/tasks.d.ts +20 -2
- package/dist/services/tasks.js +98 -26
- package/package.json +3 -3
- package/web/assets/index-DcD7-VGi.css +1 -0
- package/web/assets/index-Dcaq3wR-.js +318 -0
- package/web/index.html +2 -2
- package/web/assets/index-CUxSE_WZ.css +0 -1
- package/web/assets/index-cyyf9-Uw.js +0 -234
package/dist/services/sync.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Db, type ShareSubmission, type ShareSubmissionStatus
|
|
1
|
+
import { type Db, type ShareSubmission, type ShareSubmissionStatus } from '@plandesk/db';
|
|
2
2
|
import type { EventBus } from '../events.js';
|
|
3
3
|
import type { ShareService } from './share.js';
|
|
4
4
|
import type { TaskService } from './tasks.js';
|
|
@@ -11,6 +11,9 @@ export declare class SyncUnauthorizedError extends Error {
|
|
|
11
11
|
export declare class InvalidTriageError extends Error {
|
|
12
12
|
constructor(message?: string);
|
|
13
13
|
}
|
|
14
|
+
export declare class InvalidTriageInputError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
14
17
|
export type SyncRemote = {
|
|
15
18
|
serverUrl: string;
|
|
16
19
|
globalProjectId: string;
|
|
@@ -65,9 +68,8 @@ export declare function createSyncService(deps: SyncServiceDeps): {
|
|
|
65
68
|
getRemote(projectId: string): SyncRemote | undefined;
|
|
66
69
|
triage(submissionId: string, action: "accept" | "reject", remote: SyncRemote, asTask?: {
|
|
67
70
|
label?: string;
|
|
68
|
-
status?: TaskStatus;
|
|
69
71
|
description?: string;
|
|
70
|
-
}): Promise<SerializedSubmission>;
|
|
72
|
+
}, linkTaskId?: string): Promise<SerializedSubmission>;
|
|
71
73
|
watchPush(projectId: string, remote: SyncRemote, opts?: WatchPushOptions): WatchPushController;
|
|
72
74
|
};
|
|
73
75
|
export type SyncService = ReturnType<typeof createSyncService>;
|
package/dist/services/sync.js
CHANGED
|
@@ -18,6 +18,12 @@ export class InvalidTriageError extends Error {
|
|
|
18
18
|
this.name = 'InvalidTriageError';
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
export class InvalidTriageInputError extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'InvalidTriageInputError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
21
27
|
export function serializeSubmission(row) {
|
|
22
28
|
return {
|
|
23
29
|
id: row.id,
|
|
@@ -252,19 +258,56 @@ export function createSyncService(deps) {
|
|
|
252
258
|
syncToken: row.syncToken,
|
|
253
259
|
};
|
|
254
260
|
},
|
|
255
|
-
async triage(submissionId, action, remote, asTask) {
|
|
261
|
+
async triage(submissionId, action, remote, asTask, linkTaskId) {
|
|
262
|
+
if (asTask !== undefined && linkTaskId !== undefined) {
|
|
263
|
+
throw new InvalidTriageInputError('as_task and link_task_id are mutually exclusive');
|
|
264
|
+
}
|
|
256
265
|
const submission = dbGetSubmission(db, submissionId);
|
|
257
266
|
if (submission === undefined) {
|
|
258
267
|
throw new InvalidTriageError();
|
|
259
268
|
}
|
|
260
269
|
if (submission.status !== 'pending') {
|
|
270
|
+
// Idempotent retry recovery: a prior triage may have committed the terminal
|
|
271
|
+
// status locally but failed to ack the remote (sync server briefly down),
|
|
272
|
+
// leaving local/remote divergence with no recovery path. Re-ack when the
|
|
273
|
+
// retry's action matches the recorded outcome; ack is idempotent remotely.
|
|
274
|
+
if ((action === 'accept' && submission.status === 'accepted') ||
|
|
275
|
+
(action === 'reject' && submission.status === 'rejected')) {
|
|
276
|
+
await ackSubmission(remote, submissionId, submission.status);
|
|
277
|
+
}
|
|
261
278
|
return serializeSubmission(submission);
|
|
262
279
|
}
|
|
263
280
|
const projectId = submission.projectId;
|
|
281
|
+
if (action === 'accept' && linkTaskId !== undefined) {
|
|
282
|
+
const existingTask = taskService.get(linkTaskId);
|
|
283
|
+
if (existingTask === undefined || existingTask.project_id !== projectId) {
|
|
284
|
+
throw new InvalidTriageInputError('link_task_id does not reference an existing task in this project');
|
|
285
|
+
}
|
|
286
|
+
const updated = setSubmissionStatus(db, submissionId, {
|
|
287
|
+
status: 'accepted',
|
|
288
|
+
linkedTaskId: linkTaskId,
|
|
289
|
+
});
|
|
290
|
+
if (updated === undefined) {
|
|
291
|
+
throw new InvalidTriageError();
|
|
292
|
+
}
|
|
293
|
+
eventBus.emit({ type: 'submissions_pulled', projectId });
|
|
294
|
+
await ackSubmission(remote, submissionId, 'accepted');
|
|
295
|
+
return serializeSubmission(updated);
|
|
296
|
+
}
|
|
264
297
|
if (action === 'accept') {
|
|
298
|
+
// Triage never releases work to `todo`: the scope->todo gate is the human's
|
|
299
|
+
// own board action. Every accepted submission lands in `scope`, regardless of
|
|
300
|
+
// caller — enforced here at the single service chokepoint so both the HTTP
|
|
301
|
+
// route and the MCP tool are covered.
|
|
302
|
+
//
|
|
303
|
+
// create + setSubmissionStatus run as two synchronous steps, but the submission
|
|
304
|
+
// was validated to exist immediately above with no intervening await, so under
|
|
305
|
+
// better-sqlite3's single-connection sync model the row cannot vanish and
|
|
306
|
+
// setSubmissionStatus cannot spuriously return undefined. taskService.create
|
|
307
|
+
// self-transacts, so an outer transaction here is neither possible nor needed.
|
|
265
308
|
const task = taskService.create(projectId, {
|
|
266
309
|
label: asTask?.label ?? submission.title,
|
|
267
|
-
status:
|
|
310
|
+
status: 'scope',
|
|
268
311
|
description: asTask?.description ?? buildDesc(submission),
|
|
269
312
|
});
|
|
270
313
|
if (task === undefined) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Db } from '@plandesk/db';
|
|
2
|
+
import type { EventBus } from '../events.js';
|
|
3
|
+
import { type SerializedTag } from '../serialize.js';
|
|
4
|
+
export type TagServiceDeps = {
|
|
5
|
+
db: Db;
|
|
6
|
+
eventBus: EventBus;
|
|
7
|
+
};
|
|
8
|
+
export type CreateTagInput = {
|
|
9
|
+
name: string;
|
|
10
|
+
color?: string | null;
|
|
11
|
+
};
|
|
12
|
+
export type UpdateTagInput = {
|
|
13
|
+
name?: string;
|
|
14
|
+
color?: string | null;
|
|
15
|
+
};
|
|
16
|
+
export declare class InvalidTagError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
export declare function normalizeTagName(name: string): string;
|
|
20
|
+
export declare function createTagService(deps: TagServiceDeps): {
|
|
21
|
+
list(projectId: string): SerializedTag[] | undefined;
|
|
22
|
+
create(projectId: string, input: CreateTagInput): SerializedTag | undefined;
|
|
23
|
+
update(id: string, input: UpdateTagInput): SerializedTag | undefined;
|
|
24
|
+
delete(id: string): boolean;
|
|
25
|
+
};
|
|
26
|
+
export type TagService = ReturnType<typeof createTagService>;
|
|
27
|
+
//# sourceMappingURL=tags.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createTag as dbCreateTag, deleteTag as dbDeleteTag, getProject, getTag as dbGetTag, getTagByName, listTags as dbListTags, updateTag as dbUpdateTag, } from '@plandesk/db';
|
|
2
|
+
import { serializeTag } from '../serialize.js';
|
|
3
|
+
export class InvalidTagError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'InvalidTagError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function normalizeTagName(name) {
|
|
10
|
+
const trimmed = name.trim();
|
|
11
|
+
if (trimmed === '') {
|
|
12
|
+
throw new InvalidTagError('Tag name must not be empty');
|
|
13
|
+
}
|
|
14
|
+
return trimmed;
|
|
15
|
+
}
|
|
16
|
+
export function createTagService(deps) {
|
|
17
|
+
const { db, eventBus } = deps;
|
|
18
|
+
return {
|
|
19
|
+
list(projectId) {
|
|
20
|
+
const project = getProject(db, projectId);
|
|
21
|
+
if (!project) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return dbListTags(db, projectId).map(serializeTag);
|
|
25
|
+
},
|
|
26
|
+
create(projectId, input) {
|
|
27
|
+
const project = getProject(db, projectId);
|
|
28
|
+
if (!project) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const name = normalizeTagName(input.name);
|
|
32
|
+
if (getTagByName(db, projectId, name)) {
|
|
33
|
+
throw new InvalidTagError(`Tag already exists: ${name}`);
|
|
34
|
+
}
|
|
35
|
+
const tag = dbCreateTag(db, { projectId, name, color: input.color });
|
|
36
|
+
eventBus.emit({ type: 'tag_updated', projectId });
|
|
37
|
+
return serializeTag(tag);
|
|
38
|
+
},
|
|
39
|
+
// Renaming propagates everywhere automatically: tasks reference the single
|
|
40
|
+
// tag row through the join table.
|
|
41
|
+
update(id, input) {
|
|
42
|
+
const existing = dbGetTag(db, id);
|
|
43
|
+
if (!existing) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
let name;
|
|
47
|
+
if (input.name !== undefined) {
|
|
48
|
+
name = normalizeTagName(input.name);
|
|
49
|
+
const conflict = getTagByName(db, existing.projectId, name);
|
|
50
|
+
if (conflict && conflict.id !== id) {
|
|
51
|
+
throw new InvalidTagError(`Tag already exists: ${name}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const tag = dbUpdateTag(db, id, {
|
|
55
|
+
...(name !== undefined ? { name } : {}),
|
|
56
|
+
...(input.color !== undefined ? { color: input.color } : {}),
|
|
57
|
+
});
|
|
58
|
+
if (!tag) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
eventBus.emit({ type: 'tag_updated', projectId: tag.projectId });
|
|
62
|
+
return serializeTag(tag);
|
|
63
|
+
},
|
|
64
|
+
// Deleting a tag removes it from all its tasks (cascade on the join table).
|
|
65
|
+
delete(id) {
|
|
66
|
+
const existing = dbGetTag(db, id);
|
|
67
|
+
if (!existing) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const deleted = dbDeleteTag(db, id);
|
|
71
|
+
if (!deleted) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
eventBus.emit({ type: 'tag_updated', projectId: existing.projectId });
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=tags.js.map
|
package/dist/services/tasks.d.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { type Db, type TaskStatus } from '@plandesk/db';
|
|
|
2
2
|
import type { EventBus } from '../events.js';
|
|
3
3
|
import { serializeTask, type PaginationParams } from '../serialize.js';
|
|
4
4
|
type SerializedTask = ReturnType<typeof serializeTask>;
|
|
5
|
-
export
|
|
5
|
+
export declare class InvalidGoalReferenceError extends Error {
|
|
6
|
+
constructor(goalId: string);
|
|
7
|
+
}
|
|
8
|
+
export type NextActionableReason = 'ok' | 'no_tasks' | 'no_todo_tasks' | 'all_blocked' | 'no_active_goal' | 'multiple_active_goals';
|
|
6
9
|
export type NextActionableResult = {
|
|
7
10
|
next_task: SerializedTask | null;
|
|
8
11
|
reason: NextActionableReason;
|
|
@@ -23,6 +26,8 @@ export type CreateTaskInput = {
|
|
|
23
26
|
y?: number;
|
|
24
27
|
assignee?: string | null;
|
|
25
28
|
dueDate?: Date | null;
|
|
29
|
+
goalId?: string;
|
|
30
|
+
tags?: string[];
|
|
26
31
|
};
|
|
27
32
|
export type UpdateTaskInput = {
|
|
28
33
|
label?: string;
|
|
@@ -30,14 +35,18 @@ export type UpdateTaskInput = {
|
|
|
30
35
|
description?: string | null;
|
|
31
36
|
x?: number;
|
|
32
37
|
y?: number;
|
|
38
|
+
tags?: string[];
|
|
33
39
|
};
|
|
34
40
|
export type ListTasksFilter = {
|
|
35
41
|
status?: string;
|
|
42
|
+
tags?: string[];
|
|
36
43
|
};
|
|
37
44
|
export declare function createTaskService(deps: TaskServiceDeps): {
|
|
38
45
|
get(id: string): {
|
|
46
|
+
tags?: import("../serialize.js").SerializedTag[] | undefined;
|
|
39
47
|
id: string;
|
|
40
48
|
project_id: string;
|
|
49
|
+
goal_id: string;
|
|
41
50
|
label: string;
|
|
42
51
|
status: "scope" | "todo" | "in_progress" | "done" | "backlog";
|
|
43
52
|
description: string | null;
|
|
@@ -49,8 +58,10 @@ export declare function createTaskService(deps: TaskServiceDeps): {
|
|
|
49
58
|
updated_at: string;
|
|
50
59
|
} | undefined;
|
|
51
60
|
listByProject(projectId: string, filter?: ListTasksFilter, pagination?: PaginationParams): {
|
|
61
|
+
tags?: import("../serialize.js").SerializedTag[] | undefined;
|
|
52
62
|
id: string;
|
|
53
63
|
project_id: string;
|
|
64
|
+
goal_id: string;
|
|
54
65
|
label: string;
|
|
55
66
|
status: "scope" | "todo" | "in_progress" | "done" | "backlog";
|
|
56
67
|
description: string | null;
|
|
@@ -62,8 +73,10 @@ export declare function createTaskService(deps: TaskServiceDeps): {
|
|
|
62
73
|
updated_at: string;
|
|
63
74
|
}[] | undefined;
|
|
64
75
|
create(projectId: string, input: CreateTaskInput): {
|
|
76
|
+
tags?: import("../serialize.js").SerializedTag[] | undefined;
|
|
65
77
|
id: string;
|
|
66
78
|
project_id: string;
|
|
79
|
+
goal_id: string;
|
|
67
80
|
label: string;
|
|
68
81
|
status: "scope" | "todo" | "in_progress" | "done" | "backlog";
|
|
69
82
|
description: string | null;
|
|
@@ -75,8 +88,10 @@ export declare function createTaskService(deps: TaskServiceDeps): {
|
|
|
75
88
|
updated_at: string;
|
|
76
89
|
} | undefined;
|
|
77
90
|
update(id: string, input: UpdateTaskInput): {
|
|
91
|
+
tags?: import("../serialize.js").SerializedTag[] | undefined;
|
|
78
92
|
id: string;
|
|
79
93
|
project_id: string;
|
|
94
|
+
goal_id: string;
|
|
80
95
|
label: string;
|
|
81
96
|
status: "scope" | "todo" | "in_progress" | "done" | "backlog";
|
|
82
97
|
description: string | null;
|
|
@@ -88,7 +103,10 @@ export declare function createTaskService(deps: TaskServiceDeps): {
|
|
|
88
103
|
updated_at: string;
|
|
89
104
|
} | undefined;
|
|
90
105
|
delete(id: string): boolean;
|
|
91
|
-
nextActionable(projectId: string
|
|
106
|
+
nextActionable(projectId: string, filter?: {
|
|
107
|
+
goalId?: string;
|
|
108
|
+
tags?: string[];
|
|
109
|
+
}): NextActionableResult | undefined;
|
|
92
110
|
};
|
|
93
111
|
export type TaskService = ReturnType<typeof createTaskService>;
|
|
94
112
|
export {};
|
package/dist/services/tasks.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { createTask, deleteEdgesByTaskId, deleteTask as dbDeleteTask, getProject, getTask, InvalidTaskStatusError, isTaskStatus, listEdges, listTasks, nullDocumentsLinkedTask, updateTask, } from '@plandesk/db';
|
|
1
|
+
import { createTag, createTask, deleteEdgesByTaskId, deleteTask as dbDeleteTask, deleteTaskTagsByTaskId, getOrCreateDefaultGoal, getProject, listGoals, getTagByName, getTask, InvalidTaskStatusError, isTaskStatus, listEdges, listTagsByTaskForProject, listTagsForTask, listTasks, nullDocumentsLinkedTask, setTaskTags, taskIdsWithAnyTagName, updateTask, } from '@plandesk/db';
|
|
2
2
|
import { serializeTask } from '../serialize.js';
|
|
3
|
+
import { normalizeTagName } from './tags.js';
|
|
4
|
+
export class InvalidGoalReferenceError extends Error {
|
|
5
|
+
constructor(goalId) {
|
|
6
|
+
super(`Goal ${goalId} does not exist in this project`);
|
|
7
|
+
this.name = 'InvalidGoalReferenceError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
3
10
|
// depends_on: prerequisite = to, dependent = from. All other labels: prerequisite = from, dependent = to.
|
|
4
11
|
function prerequisiteAndDependent(edge) {
|
|
5
12
|
if (edge.fromTaskId === edge.toTaskId) {
|
|
@@ -10,6 +17,21 @@ function prerequisiteAndDependent(edge) {
|
|
|
10
17
|
}
|
|
11
18
|
return { prerequisite: edge.fromTaskId, dependent: edge.toTaskId };
|
|
12
19
|
}
|
|
20
|
+
// Resolves tag names to ids, auto-creating tags that do not exist yet.
|
|
21
|
+
function resolveTagIdsByName(db, projectId, names) {
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const ids = [];
|
|
24
|
+
for (const raw of names) {
|
|
25
|
+
const name = normalizeTagName(raw);
|
|
26
|
+
if (seen.has(name)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
seen.add(name);
|
|
30
|
+
const tag = getTagByName(db, projectId, name) ?? createTag(db, { projectId, name });
|
|
31
|
+
ids.push(tag.id);
|
|
32
|
+
}
|
|
33
|
+
return ids;
|
|
34
|
+
}
|
|
13
35
|
export function createTaskService(deps) {
|
|
14
36
|
const { db, eventBus } = deps;
|
|
15
37
|
return {
|
|
@@ -18,7 +40,7 @@ export function createTaskService(deps) {
|
|
|
18
40
|
if (!task) {
|
|
19
41
|
return undefined;
|
|
20
42
|
}
|
|
21
|
-
return serializeTask(task);
|
|
43
|
+
return serializeTask(task, listTagsForTask(db, id));
|
|
22
44
|
},
|
|
23
45
|
listByProject(projectId, filter = {}, pagination = {}) {
|
|
24
46
|
if (filter.status !== undefined && !isTaskStatus(filter.status)) {
|
|
@@ -29,10 +51,13 @@ export function createTaskService(deps) {
|
|
|
29
51
|
return undefined;
|
|
30
52
|
}
|
|
31
53
|
const statusFilter = filter.status;
|
|
32
|
-
const tasks =
|
|
33
|
-
|
|
34
|
-
:
|
|
35
|
-
|
|
54
|
+
const tasks = listTasks(db, projectId, {
|
|
55
|
+
...(statusFilter !== undefined ? { status: statusFilter } : {}),
|
|
56
|
+
...(filter.tags !== undefined ? { tagNames: filter.tags.map(normalizeTagName) } : {}),
|
|
57
|
+
...pagination,
|
|
58
|
+
});
|
|
59
|
+
const tagsByTask = listTagsByTaskForProject(db, projectId);
|
|
60
|
+
return tasks.map((task) => serializeTask(task, tagsByTask.get(task.id) ?? []));
|
|
36
61
|
},
|
|
37
62
|
create(projectId, input) {
|
|
38
63
|
if (input.status !== undefined && !isTaskStatus(input.status)) {
|
|
@@ -42,22 +67,34 @@ export function createTaskService(deps) {
|
|
|
42
67
|
if (!project) {
|
|
43
68
|
return undefined;
|
|
44
69
|
}
|
|
45
|
-
|
|
46
|
-
projectId
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
if (input.goalId !== undefined &&
|
|
71
|
+
!listGoals(db, projectId).some((g) => g.id === input.goalId)) {
|
|
72
|
+
throw new InvalidGoalReferenceError(input.goalId);
|
|
73
|
+
}
|
|
74
|
+
const { task, tags } = db.transaction((tx) => {
|
|
75
|
+
const goalId = input.goalId ?? getOrCreateDefaultGoal(tx, projectId).id;
|
|
76
|
+
const row = createTask(tx, {
|
|
77
|
+
projectId,
|
|
78
|
+
goalId,
|
|
79
|
+
label: input.label,
|
|
80
|
+
status: input.status,
|
|
81
|
+
description: input.description,
|
|
82
|
+
x: input.x,
|
|
83
|
+
y: input.y,
|
|
84
|
+
assignee: input.assignee,
|
|
85
|
+
dueDate: input.dueDate,
|
|
86
|
+
});
|
|
87
|
+
if (input.tags !== undefined) {
|
|
88
|
+
setTaskTags(tx, row.id, resolveTagIdsByName(tx, projectId, input.tags));
|
|
89
|
+
}
|
|
90
|
+
return { task: row, tags: listTagsForTask(tx, row.id) };
|
|
54
91
|
});
|
|
55
92
|
eventBus.emit({
|
|
56
93
|
type: 'task_updated',
|
|
57
94
|
taskId: task.id,
|
|
58
95
|
projectId,
|
|
59
96
|
});
|
|
60
|
-
return serializeTask(task);
|
|
97
|
+
return serializeTask(task, tags);
|
|
61
98
|
},
|
|
62
99
|
update(id, input) {
|
|
63
100
|
if (input.status !== undefined && !isTaskStatus(input.status)) {
|
|
@@ -67,16 +104,26 @@ export function createTaskService(deps) {
|
|
|
67
104
|
if (!existing) {
|
|
68
105
|
return undefined;
|
|
69
106
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
107
|
+
const { tags: tagNames, ...columns } = input;
|
|
108
|
+
const result = db.transaction((tx) => {
|
|
109
|
+
const row = updateTask(tx, id, columns);
|
|
110
|
+
if (!row) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
if (tagNames !== undefined) {
|
|
114
|
+
setTaskTags(tx, id, resolveTagIdsByName(tx, existing.projectId, tagNames));
|
|
115
|
+
}
|
|
116
|
+
return { task: row, tags: listTagsForTask(tx, id) };
|
|
117
|
+
});
|
|
118
|
+
if (!result) {
|
|
72
119
|
return undefined;
|
|
73
120
|
}
|
|
74
121
|
eventBus.emit({
|
|
75
122
|
type: 'task_updated',
|
|
76
|
-
taskId: task.id,
|
|
77
|
-
projectId: task.projectId,
|
|
123
|
+
taskId: result.task.id,
|
|
124
|
+
projectId: result.task.projectId,
|
|
78
125
|
});
|
|
79
|
-
return serializeTask(task);
|
|
126
|
+
return serializeTask(result.task, result.tags);
|
|
80
127
|
},
|
|
81
128
|
delete(id) {
|
|
82
129
|
const task = getTask(db, id);
|
|
@@ -87,19 +134,42 @@ export function createTaskService(deps) {
|
|
|
87
134
|
db.transaction((tx) => {
|
|
88
135
|
deleteEdgesByTaskId(tx, id);
|
|
89
136
|
nullDocumentsLinkedTask(tx, id);
|
|
137
|
+
deleteTaskTagsByTaskId(tx, id);
|
|
90
138
|
dbDeleteTask(tx, id);
|
|
91
139
|
});
|
|
92
140
|
eventBus.emit({ type: 'canvas_updated', projectId });
|
|
93
141
|
return true;
|
|
94
142
|
},
|
|
95
|
-
|
|
143
|
+
// filter.goalId scopes candidates to one goal; when omitted, the project's sole
|
|
144
|
+
// active goal is resolved. filter.tags (OR semantics) composes with goal scope;
|
|
145
|
+
// prerequisite completion is still evaluated against all tasks in the project.
|
|
146
|
+
nextActionable(projectId, filter = {}) {
|
|
96
147
|
const project = getProject(db, projectId);
|
|
97
148
|
if (!project) {
|
|
98
149
|
return undefined;
|
|
99
150
|
}
|
|
151
|
+
let goalId = filter.goalId;
|
|
152
|
+
if (goalId === undefined) {
|
|
153
|
+
const active = listGoals(db, projectId).filter((goal) => goal.status === 'active');
|
|
154
|
+
if (active.length === 0) {
|
|
155
|
+
return { next_task: null, reason: 'no_active_goal', blocked: [] };
|
|
156
|
+
}
|
|
157
|
+
if (active.length > 1) {
|
|
158
|
+
return { next_task: null, reason: 'multiple_active_goals', blocked: [] };
|
|
159
|
+
}
|
|
160
|
+
goalId = active[0]?.id;
|
|
161
|
+
}
|
|
162
|
+
else if (!listGoals(db, projectId).some((goal) => goal.id === goalId)) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
100
165
|
const tasks = listTasks(db, projectId).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
101
166
|
const edges = listEdges(db, projectId);
|
|
102
167
|
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
168
|
+
const tagsByTask = listTagsByTaskForProject(db, projectId);
|
|
169
|
+
const tagMatches = filter.tags !== undefined && filter.tags.length > 0
|
|
170
|
+
? taskIdsWithAnyTagName(db, projectId, filter.tags.map(normalizeTagName))
|
|
171
|
+
: undefined;
|
|
172
|
+
const serialize = (task) => serializeTask(task, tagsByTask.get(task.id) ?? []);
|
|
103
173
|
const prerequisites = new Map();
|
|
104
174
|
for (const edge of edges) {
|
|
105
175
|
const pair = prerequisiteAndDependent(edge);
|
|
@@ -116,7 +186,9 @@ export function createTaskService(deps) {
|
|
|
116
186
|
if (tasks.length === 0) {
|
|
117
187
|
return { next_task: null, reason: 'no_tasks', blocked: [] };
|
|
118
188
|
}
|
|
119
|
-
const todoTasks = tasks.filter((task) => task.
|
|
189
|
+
const todoTasks = tasks.filter((task) => task.goalId === goalId &&
|
|
190
|
+
task.status === 'todo' &&
|
|
191
|
+
(tagMatches === undefined || tagMatches.has(task.id)));
|
|
120
192
|
if (todoTasks.length === 0) {
|
|
121
193
|
return { next_task: null, reason: 'no_todo_tasks', blocked: [] };
|
|
122
194
|
}
|
|
@@ -141,19 +213,19 @@ export function createTaskService(deps) {
|
|
|
141
213
|
return [...prereqs]
|
|
142
214
|
.map((id) => taskById.get(id))
|
|
143
215
|
.filter((task) => task !== undefined && task.status !== 'done')
|
|
144
|
-
.map(
|
|
216
|
+
.map(serialize);
|
|
145
217
|
};
|
|
146
218
|
const blocked = [];
|
|
147
219
|
let nextTask = null;
|
|
148
220
|
for (const task of todoTasks) {
|
|
149
221
|
if (isActionable(task.id)) {
|
|
150
222
|
if (nextTask === null) {
|
|
151
|
-
nextTask =
|
|
223
|
+
nextTask = serialize(task);
|
|
152
224
|
}
|
|
153
225
|
}
|
|
154
226
|
else {
|
|
155
227
|
blocked.push({
|
|
156
|
-
task:
|
|
228
|
+
task: serialize(task),
|
|
157
229
|
waiting_on: unfinishedPrereqs(task.id),
|
|
158
230
|
});
|
|
159
231
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plandesk/api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"typescript": "^6.0.3",
|
|
20
20
|
"vitest": "^3.2.6",
|
|
21
|
-
"@plandesk/db": "0.
|
|
22
|
-
"@plandesk/sync-server": "0.
|
|
21
|
+
"@plandesk/db": "0.7.0",
|
|
22
|
+
"@plandesk/sync-server": "0.5.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@hono/node-server": "^2.0.4",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.document-editor-content img,.document-reader-content img,.portal-document-content img{max-width:100%;height:auto}.document-editor-content table,.document-reader-content table,.portal-document-content table{border-collapse:collapse;table-layout:fixed;width:100%;margin:.75rem 0}.document-editor-content th,.document-editor-content td,.document-reader-content th,.document-reader-content td,.portal-document-content th,.portal-document-content td{text-align:left;vertical-align:top;border:1px solid #d1d5db;padding:.375rem .625rem}.document-editor-content th,.document-reader-content th,.portal-document-content th{background:#f3f4f6;font-weight:600}.document-editor-content ul[data-type=taskList],.document-reader-content ul[data-type=taskList],.portal-document-content ul[data-type=taskList]{padding-left:0;list-style:none}.document-editor-content ul[data-type=taskList] li,.document-reader-content ul[data-type=taskList] li,.portal-document-content ul[data-type=taskList] li{align-items:flex-start;gap:.5rem;display:flex}.document-editor-content ul[data-type=taskList] li>label,.document-reader-content ul[data-type=taskList] li>label,.portal-document-content ul[data-type=taskList] li>label{margin-top:.15rem}.document-reader-content li[data-type=taskItem],.portal-document-content li[data-type=taskItem]{list-style:none}.document-reader-content li[data-type=taskItem]:before,.portal-document-content li[data-type=taskItem]:before{content:"☐";margin-right:.4rem}.document-reader-content li[data-type=taskItem][data-checked=true]:before,.portal-document-content li[data-type=taskItem][data-checked=true]:before{content:"☑"}.react-flow{--xy-edge-stroke-default:#b1b1b7;--xy-edge-stroke-width-default:1;--xy-edge-stroke-selected-default:#555;--xy-connectionline-stroke-default:#b1b1b7;--xy-connectionline-stroke-width-default:1;--xy-attribution-background-color-default:#ffffff80;--xy-minimap-background-color-default:#fff;--xy-minimap-mask-background-color-default:#f0f0f099;--xy-minimap-mask-stroke-color-default:transparent;--xy-minimap-mask-stroke-width-default:1;--xy-minimap-node-background-color-default:#e2e2e2;--xy-minimap-node-stroke-color-default:transparent;--xy-minimap-node-stroke-width-default:2;--xy-background-color-default:transparent;--xy-background-pattern-dots-color-default:#91919a;--xy-background-pattern-lines-color-default:#eee;--xy-background-pattern-cross-color-default:#e2e2e2;background-color:var(--xy-background-color,var(--xy-background-color-default));--xy-node-color-default:inherit;--xy-node-border-default:1px solid #1a192b;--xy-node-background-color-default:#fff;--xy-node-group-background-color-default:#f0f0f040;--xy-node-boxshadow-hover-default:0 1px 4px 1px #00000014;--xy-node-boxshadow-selected-default:0 0 0 .5px #1a192b;--xy-node-border-radius-default:3px;--xy-handle-background-color-default:#1a192b;--xy-handle-border-color-default:#fff;--xy-selection-background-color-default:#0059dc14;--xy-selection-border-default:1px dotted #0059dccc;--xy-controls-button-background-color-default:#fefefe;--xy-controls-button-background-color-hover-default:#f4f4f4;--xy-controls-button-color-default:inherit;--xy-controls-button-color-hover-default:inherit;--xy-controls-button-border-color-default:#eee;--xy-controls-box-shadow-default:0 0 2px 1px #00000014;--xy-edge-label-background-color-default:#fff;--xy-edge-label-color-default:inherit;--xy-resize-background-color-default:#3367d9;direction:ltr}.react-flow.dark{--xy-edge-stroke-default:#3e3e3e;--xy-edge-stroke-width-default:1;--xy-edge-stroke-selected-default:#727272;--xy-connectionline-stroke-default:#b1b1b7;--xy-connectionline-stroke-width-default:1;--xy-attribution-background-color-default:#96969640;--xy-minimap-background-color-default:#141414;--xy-minimap-mask-background-color-default:#3c3c3c99;--xy-minimap-mask-stroke-color-default:transparent;--xy-minimap-mask-stroke-width-default:1;--xy-minimap-node-background-color-default:#2b2b2b;--xy-minimap-node-stroke-color-default:transparent;--xy-minimap-node-stroke-width-default:2;--xy-background-color-default:#141414;--xy-background-pattern-dots-color-default:#777;--xy-background-pattern-lines-color-default:#777;--xy-background-pattern-cross-color-default:#777;--xy-node-color-default:#f8f8f8;--xy-node-border-default:1px solid #3c3c3c;--xy-node-background-color-default:#1e1e1e;--xy-node-group-background-color-default:#f0f0f040;--xy-node-boxshadow-hover-default:0 1px 4px 1px #ffffff14;--xy-node-boxshadow-selected-default:0 0 0 .5px #999;--xy-handle-background-color-default:#bebebe;--xy-handle-border-color-default:#1e1e1e;--xy-selection-background-color-default:#c8c8dc14;--xy-selection-border-default:1px dotted #c8c8dccc;--xy-controls-button-background-color-default:#2b2b2b;--xy-controls-button-background-color-hover-default:#3e3e3e;--xy-controls-button-color-default:#f8f8f8;--xy-controls-button-color-hover-default:#fff;--xy-controls-button-border-color-default:#5b5b5b;--xy-controls-box-shadow-default:0 0 2px 1px #00000014;--xy-edge-label-background-color-default:#141414;--xy-edge-label-color-default:#f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props,var(--xy-background-color,var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__pane{z-index:1;touch-action:none}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke,var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width,var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke,var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width,var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{pointer-events:none;position:absolute;overflow:visible}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:.5s linear infinite dashdraw}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected,var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke,var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke,var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:.5s linear infinite dashdraw}svg.react-flow__connectionline{z-index:1001;position:absolute;overflow:visible}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{-webkit-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default;position:absolute}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:0 0;pointer-events:none}.react-flow__nodesselection-rect{pointer-events:all;cursor:grab;position:absolute}.react-flow__handle{pointer-events:none;background-color:var(--xy-handle-background-color,var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color,var(--xy-handle-border-color-default));border-radius:100%;width:6px;min-width:5px;height:6px;min-height:5px;position:absolute}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;bottom:0;left:50%;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{z-index:5;margin:15px;position:absolute}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px)translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px)translateY(-50%)}.react-flow__attribution{background:var(--xy-attribution-background-color,var(--xy-attribution-background-color-default));margin:0;padding:2px 3px;font-size:10px}.react-flow__attribution a{color:#999;text-decoration:none}@keyframes dashdraw{0%{stroke-dashoffset:10px}}.react-flow__edgelabel-renderer{pointer-events:none;-webkit-user-select:none;user-select:none;width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__viewport-portal{-webkit-user-select:none;user-select:none;width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__minimap{background:var(--xy-minimap-background-color-props,var(--xy-minimap-background-color,var(--xy-minimap-background-color-default)))}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var(--xy-minimap-mask-background-color-props,var(--xy-minimap-mask-background-color,var(--xy-minimap-mask-background-color-default)));stroke:var(--xy-minimap-mask-stroke-color-props,var(--xy-minimap-mask-stroke-color,var(--xy-minimap-mask-stroke-color-default)));stroke-width:var(--xy-minimap-mask-stroke-width-props,var(--xy-minimap-mask-stroke-width,var(--xy-minimap-mask-stroke-width-default)))}.react-flow__minimap-node{fill:var(--xy-minimap-node-background-color-props,var(--xy-minimap-node-background-color,var(--xy-minimap-node-background-color-default)));stroke:var(--xy-minimap-node-stroke-color-props,var(--xy-minimap-node-stroke-color,var(--xy-minimap-node-stroke-color-default)));stroke-width:var(--xy-minimap-node-stroke-width-props,var(--xy-minimap-node-stroke-width,var(--xy-minimap-node-stroke-width-default)))}.react-flow__background-pattern.dots{fill:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-dots-color-default)))}.react-flow__background-pattern.lines{stroke:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-lines-color-default)))}.react-flow__background-pattern.cross{stroke:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-cross-color-default)))}.react-flow__controls{box-shadow:var(--xy-controls-box-shadow,var(--xy-controls-box-shadow-default));flex-direction:column;display:flex}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{background:var(--xy-controls-button-background-color,var(--xy-controls-button-background-color-default));border:none;border-bottom:1px solid var(--xy-controls-button-border-color-props,var(--xy-controls-button-border-color,var(--xy-controls-button-border-color-default)));width:26px;height:26px;color:var(--xy-controls-button-color-props,var(--xy-controls-button-color,var(--xy-controls-button-color-default)));cursor:pointer;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;padding:4px;display:flex}.react-flow__controls-button svg{fill:currentColor;width:100%;max-width:12px;max-height:12px}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{border-radius:var(--xy-node-border-radius,var(--xy-node-border-radius-default));width:150px;color:var(--xy-node-color,var(--xy-node-color-default));text-align:center;border:var(--xy-node-border,var(--xy-node-border-default));background-color:var(--xy-node-background-color,var(--xy-node-background-color-default));padding:10px;font-size:12px}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover,var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected,var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color,var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color,var(--xy-selection-background-color-default));border:var(--xy-selection-border,var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var(--xy-controls-button-background-color-hover-props,var(--xy-controls-button-background-color-hover,var(--xy-controls-button-background-color-hover-default)));color:var(--xy-controls-button-color-hover-props,var(--xy-controls-button-color-hover,var(--xy-controls-button-color-hover-default)))}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var(--xy-controls-button-border-color-props,var(--xy-controls-button-border-color,var(--xy-controls-button-border-color-default)))}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{background-color:var(--xy-resize-background-color,var(--xy-resize-background-color-default));border:1px solid #fff;border-radius:1px;width:5px;height:5px;translate:-50% -50%}.react-flow__resize-control.handle.left{top:50%;left:0}.react-flow__resize-control.handle.right{top:50%;left:100%}.react-flow__resize-control.handle.top{top:0;left:50%}.react-flow__resize-control.handle.bottom{top:100%;left:50%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color,var(--xy-resize-background-color-default));border-style:solid;border-width:0}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;height:100%;top:0;transform:translate(-50%)}.react-flow__resize-control.line.left{border-left-width:1px;left:0}.react-flow__resize-control.line.right{border-right-width:1px;left:100%}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{width:100%;height:1px;left:0;transform:translateY(-50%)}.react-flow__resize-control.line.top{border-top-width:1px;top:0}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color,var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color,var(--xy-edge-label-color-default))}
|