@taskforcehq/taskforce 0.3.327 → 0.3.328
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/Taskforce.module.css +24 -53
- package/dist/components/activity/EntityActivityTimeline.js +15 -5
- package/dist/components/context/AttachmentPreviewHost.js +1 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
- package/dist/components/context/ContextAttachmentManager.js +116 -20
- package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
- package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
- package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
- package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
- package/dist/components/features/planning/PlanningModule.js +26 -9
- package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
- package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
- package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +2 -2
- package/dist/components/task/TaskCard.js +37 -4
- package/dist/components/task/TaskContextUpload.d.ts +3 -0
- package/dist/components/task/TaskContextUpload.js +2 -2
- package/dist/components/task/TaskForm.d.ts +4 -0
- package/dist/components/task/TaskForm.js +32 -7
- package/dist/components/task/TaskImageReviews.js +16 -16
- package/dist/components/task/TaskStatusActions.js +11 -2
- package/dist/components/task/TaskWorkstreamPicker.js +2 -1
- package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
- package/dist/components/ui/DocumentTypeIcon.js +8 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
- package/dist/components/ui/TaxonomyDropdown.js +30 -3
- package/dist/components/views/StandaloneLayout.js +62 -9
- package/dist/components/views/WidgetView.js +8 -5
- package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
- package/dist/components/views/panels/PlanningDrawer.js +48 -44
- package/dist/config/envSchema.js +1 -0
- package/dist/config/localServiceCloudTarget.d.ts +10 -0
- package/dist/config/localServiceCloudTarget.js +76 -0
- package/dist/core/AiProfileService.d.ts +1 -0
- package/dist/core/AiProfileService.js +2 -1
- package/dist/core/AttachmentLinkService.js +7 -4
- package/dist/core/DeletedTaskLifecycleService.d.ts +4 -0
- package/dist/core/DeletedTaskLifecycleService.js +21 -0
- package/dist/core/McpTokenService.d.ts +4 -0
- package/dist/core/McpTokenService.js +52 -0
- package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +5 -0
- package/dist/core/TaskAttachmentLinksDurableMutationService.js +20 -11
- package/dist/core/Taskforce.d.ts +63 -1
- package/dist/core/Taskforce.js +558 -73
- package/dist/core/types.d.ts +2 -1
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
- package/dist/documentReviews/service.d.ts +4 -0
- package/dist/documentReviews/service.js +141 -46
- package/dist/hooks/tasks/useTaskFormActions.d.ts +3 -0
- package/dist/hooks/tasks/useTaskFormActions.js +33 -7
- package/dist/hooks/useTaskforce.d.ts +59 -50
- package/dist/hooks/useTaskforce.js +103 -1
- package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
- package/dist/mcp/canonicalAssetHelpers.js +39 -5
- package/dist/mcp/collaborationRegistrar.js +15 -13
- package/dist/mcp/documentAssetRegistrar.js +16 -9
- package/dist/mcp/localServiceProxy.js +4 -1
- package/dist/mcp/runtime.d.ts +16 -0
- package/dist/mcp/runtime.js +566 -171
- package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
- package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
- package/dist/mcp/taskPlanningRegistrar.js +42 -34
- package/dist/mcp/toolRegistry.js +2 -2
- package/dist/migrations/taskSchemaMigrations.js +35 -0
- package/dist/runtime/appGateDefinitions.d.ts +2 -2
- package/dist/runtime/appGateDefinitions.js +2 -2
- package/dist/server/appAccess.d.ts +12 -0
- package/dist/server/appAccess.js +21 -0
- package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
- package/dist/server/authorizedWorkspaceAccess.js +40 -0
- package/dist/server/cloudCollectionRead.d.ts +25 -0
- package/dist/server/cloudCollectionRead.js +120 -0
- package/dist/server/cloudCollectionReadPool.d.ts +52 -0
- package/dist/server/cloudCollectionReadPool.js +140 -0
- package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
- package/dist/server/cloudCollectionReadWorker.js +36 -0
- package/dist/server/index.d.ts +3 -11
- package/dist/server/index.js +53 -40
- package/dist/server/localServiceLease.d.ts +3 -0
- package/dist/server/localServiceLease.js +9 -1
- package/dist/server/routes/documents.d.ts +5 -2
- package/dist/server/routes/documents.js +160 -13
- package/dist/server/routes/durableTaskHttpRouters.d.ts +2 -0
- package/dist/server/routes/durableTaskHttpRouters.js +2 -2
- package/dist/server/routes/localService.js +2 -0
- package/dist/server/routes/primitives.d.ts +2 -0
- package/dist/server/routes/shared.d.ts +24 -0
- package/dist/server/routes/shared.js +114 -18
- package/dist/server/routes/sync.js +22 -0
- package/dist/server/routes/syncV3FeedRoutes.js +4 -2
- package/dist/server/routes/syncV3LocalCaptureRoutes.js +20 -2
- package/dist/server/routes/syncV3OperationRoutes.js +17 -2
- package/dist/server/routes/tasks.js +199 -89
- package/dist/server/routes.js +35 -7
- package/dist/service/localServiceCli.js +36 -25
- package/dist/service/localServiceClientLifecycle.js +67 -6
- package/dist/shared/planningIdentity.d.ts +20 -0
- package/dist/shared/planningIdentity.js +79 -0
- package/dist/storage/postgresAdapter.js +4 -0
- package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
- package/dist/storage/postgresRequestDiagnostics.js +24 -0
- package/dist/storage/workspaceAssetStore.d.ts +32 -0
- package/dist/storage/workspaceAssetStore.js +70 -0
- package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +16 -0
- package/dist/sync/coordinator/localSyncExecutionPermitStore.js +70 -22
- package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
- package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +5 -0
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +15 -0
- package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +26 -3
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
- package/dist/sync/syncApplyHandlers.d.ts +3 -1
- package/dist/sync/syncApplyHandlers.js +29 -0
- package/dist/sync/syncService.js +9 -0
- package/dist/sync/taskSyncPayload.d.ts +1 -0
- package/dist/sync/taskSyncPayload.js +6 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +1 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +21 -3
- package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +17 -4
- package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
- package/dist/sync/v3/localOutboxDispatcher.js +5 -1
- package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +1 -0
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +15 -3
- package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
- package/dist/sync/v3/syncDurabilityStore.d.ts +8 -0
- package/dist/sync/v3/syncDurabilityStore.js +171 -33
- package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +1 -0
- package/dist/sync/v3/taskAttachmentLinksMutationService.js +18 -2
- package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
- package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +1 -9
- package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +1 -0
- package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +7 -1
- package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +15 -3
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +4 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +29 -6
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +29 -7
- package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +62 -7
- package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
- package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
- package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
- package/dist/sync/workspacePullFeed.d.ts +6 -1
- package/dist/sync/workspacePullFeed.js +172 -161
- package/dist/sync/workspaceSyncModel.d.ts +11 -0
- package/dist/sync/workspaceSyncModel.js +27 -2
- package/dist/types.d.ts +8 -0
- package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-MZs7lVED.js} +1 -1
- package/dist/ui/assets/{AiProfilesModule-DPxQeF9N.js → AiProfilesModule-C3i0LOxe.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-DHNYaNg6.js → AnnotatedAttachmentWorkspace-DVZk5Tln.js} +1 -1
- package/dist/ui/assets/AssetTraySortControl-Kf1pelSy.js +1 -0
- package/dist/ui/assets/ContextAttachmentManager-B8boOs6Q.js +3 -0
- package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-DXH4XUsB.js} +4 -4
- package/dist/ui/assets/EntityActivityTimeline-BtSTq0ON.js +1 -0
- package/dist/ui/assets/PlanningModule-BPVyEa-f.js +1 -0
- package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
- package/dist/ui/assets/{PlansPage-BVH_NfgT.js → PlansPage-ChqgxALc.js} +1 -1
- package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +1 -0
- package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
- package/dist/ui/assets/{TaskSettings-xVsxSa0g.js → TaskSettings-g4ECKNOz.js} +1 -1
- package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BazdUwxZ.js} +1 -1
- package/dist/ui/assets/WorkflowManagerModule-DWFspC-o.js +1 -0
- package/dist/ui/assets/index-BUplSsv_.js +7 -0
- package/dist/ui/assets/index-gz2EXuoK.css +1 -0
- package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-BkFLXavV.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/constants.d.ts +2 -2
- package/dist/utils/constants.js +1 -1
- package/dist/utils/contextAttachmentUpload.d.ts +2 -0
- package/dist/utils/contextAttachmentUpload.js +7 -0
- package/dist/utils/contextFiles.d.ts +1 -1
- package/dist/utils/contextFiles.js +4 -4
- package/package.json +6 -1
- package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
- package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
- package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
- package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
- package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
- package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
- package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
- package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
- package/dist/ui/assets/index-BzJlVnNS.css +0 -1
- package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
package/dist/core/types.d.ts
CHANGED
|
@@ -104,7 +104,7 @@ export interface ScheduleBoardResult {
|
|
|
104
104
|
}>;
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
-
export type RealtimeMutationEntityType = 'task' | 'taxonomy' | 'document-metadata';
|
|
107
|
+
export type RealtimeMutationEntityType = 'task' | 'initiative' | 'workstream' | 'taxonomy' | 'document-metadata';
|
|
108
108
|
export interface RealtimeMutationEvent {
|
|
109
109
|
eventId: string;
|
|
110
110
|
workspaceId: string;
|
|
@@ -141,6 +141,7 @@ export interface TaskforceConfig {
|
|
|
141
141
|
storagePath?: string;
|
|
142
142
|
tenantId?: string;
|
|
143
143
|
runtimeMode?: RuntimeMode;
|
|
144
|
+
initializationMode?: 'standard' | 'cloud-read-worker';
|
|
144
145
|
database?: {
|
|
145
146
|
provider?: DatabaseProvider;
|
|
146
147
|
connectionString?: string;
|
|
@@ -28,7 +28,18 @@ export declare class DocumentReviewCommentStore {
|
|
|
28
28
|
threadStatusUpdatedAt?: string | null;
|
|
29
29
|
resolvedAt?: string | null;
|
|
30
30
|
resolvedByActorId?: string | null;
|
|
31
|
+
idempotencyKey?: string | null;
|
|
32
|
+
idempotencyFingerprint?: string | null;
|
|
33
|
+
idempotencyActorScope?: string | null;
|
|
31
34
|
}): DocumentReviewCommentSyncRecord;
|
|
35
|
+
getByIdempotencyKey(input: {
|
|
36
|
+
workspaceId: string;
|
|
37
|
+
actorScope: string;
|
|
38
|
+
idempotencyKey: string;
|
|
39
|
+
}): {
|
|
40
|
+
comment: DocumentReviewCommentSyncRecord;
|
|
41
|
+
fingerprint: string;
|
|
42
|
+
} | null;
|
|
32
43
|
get(commentIdRaw: string, workspaceIdRaw: string, options?: {
|
|
33
44
|
includeDeleted?: boolean;
|
|
34
45
|
}): DocumentReviewCommentSyncRecord | null;
|
|
@@ -149,16 +149,49 @@ export class DocumentReviewCommentStore {
|
|
|
149
149
|
thread_status_updated_at,
|
|
150
150
|
resolved_at,
|
|
151
151
|
resolved_by_actor_id,
|
|
152
|
+
idempotency_key,
|
|
153
|
+
idempotency_fingerprint,
|
|
154
|
+
idempotency_actor_scope,
|
|
152
155
|
order_index,
|
|
153
156
|
created_at,
|
|
154
157
|
updated_at,
|
|
155
158
|
deleted_at
|
|
156
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
159
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
160
|
+
${normalizeNullable(input.idempotencyKey)
|
|
161
|
+
? 'ON CONFLICT (tenant_id, workspace_id, idempotency_actor_scope, idempotency_key) DO NOTHING'
|
|
162
|
+
: ''}
|
|
157
163
|
`).run(id, sessionId, this.tenantId, workspaceId, normalizeId(input.body), input.anchor ? JSON.stringify(input.anchor) : null, normalizeNullable(input.authorActorId), normalizeNullable(input.updatedByActorId ?? input.authorActorId), normalizeNullable(input.deletedByActorId), parentCommentId, parentCommentId
|
|
158
164
|
? null
|
|
159
|
-
: (input.threadStatus === 'resolved' ? 'resolved' : 'open'), parentCommentId ? null : (normalizeNullable(input.threadStatusUpdatedAt) || updatedAt), parentCommentId ? null : normalizeNullable(input.resolvedAt), parentCommentId ? null : normalizeNullable(input.resolvedByActorId), normalizePositiveInteger(input.order) ?? 0, createdAt, updatedAt, normalizeNullable(input.deletedAt));
|
|
165
|
+
: (input.threadStatus === 'resolved' ? 'resolved' : 'open'), parentCommentId ? null : (normalizeNullable(input.threadStatusUpdatedAt) || updatedAt), parentCommentId ? null : normalizeNullable(input.resolvedAt), parentCommentId ? null : normalizeNullable(input.resolvedByActorId), normalizeNullable(input.idempotencyKey), normalizeNullable(input.idempotencyFingerprint), normalizeId(input.idempotencyActorScope), normalizePositiveInteger(input.order) ?? 0, createdAt, updatedAt, normalizeNullable(input.deletedAt));
|
|
166
|
+
if (normalizeNullable(input.idempotencyKey)) {
|
|
167
|
+
const replay = this.getByIdempotencyKey({
|
|
168
|
+
workspaceId,
|
|
169
|
+
actorScope: normalizeId(input.idempotencyActorScope),
|
|
170
|
+
idempotencyKey: normalizeId(input.idempotencyKey),
|
|
171
|
+
});
|
|
172
|
+
if (replay)
|
|
173
|
+
return replay.comment;
|
|
174
|
+
}
|
|
160
175
|
return this.get(id, workspaceId, { includeDeleted: true });
|
|
161
176
|
}
|
|
177
|
+
getByIdempotencyKey(input) {
|
|
178
|
+
const workspaceId = normalizeWorkspaceId(input.workspaceId);
|
|
179
|
+
const row = this.db.prepare(`
|
|
180
|
+
SELECT *
|
|
181
|
+
FROM document_review_comments
|
|
182
|
+
WHERE tenant_id = ?
|
|
183
|
+
AND workspace_id = ?
|
|
184
|
+
AND idempotency_actor_scope = ?
|
|
185
|
+
AND idempotency_key = ?
|
|
186
|
+
LIMIT 1
|
|
187
|
+
`).get(this.tenantId, workspaceId, normalizeId(input.actorScope), normalizeId(input.idempotencyKey));
|
|
188
|
+
if (!row)
|
|
189
|
+
return null;
|
|
190
|
+
return {
|
|
191
|
+
comment: mapRow(row),
|
|
192
|
+
fingerprint: normalizeId(row.idempotency_fingerprint),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
162
195
|
get(commentIdRaw, workspaceIdRaw, options) {
|
|
163
196
|
const commentId = normalizeId(commentIdRaw);
|
|
164
197
|
const workspaceId = normalizeWorkspaceId(workspaceIdRaw);
|
|
@@ -49,6 +49,8 @@ export declare function addDocumentReviewCommentService(deps: DocumentReviewServ
|
|
|
49
49
|
body: string;
|
|
50
50
|
anchor?: DocumentReviewAnchor | null;
|
|
51
51
|
actorId?: string | null;
|
|
52
|
+
idempotencyKey?: string | null;
|
|
53
|
+
commentId?: string | null;
|
|
52
54
|
}): DocumentReviewSession;
|
|
53
55
|
export declare function addDocumentReviewReplyService(deps: DocumentReviewServiceDeps, input: {
|
|
54
56
|
workspaceId?: string | null;
|
|
@@ -56,6 +58,8 @@ export declare function addDocumentReviewReplyService(deps: DocumentReviewServic
|
|
|
56
58
|
parentCommentId: string;
|
|
57
59
|
body: string;
|
|
58
60
|
actorId?: string | null;
|
|
61
|
+
idempotencyKey?: string | null;
|
|
62
|
+
commentId?: string | null;
|
|
59
63
|
}): DocumentReviewSession;
|
|
60
64
|
export declare function updateDocumentReviewThreadStatusService(deps: DocumentReviewServiceDeps, input: {
|
|
61
65
|
workspaceId?: string | null;
|
|
@@ -1,9 +1,50 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
function normalizeId(value) {
|
|
2
3
|
return String(value || '').trim();
|
|
3
4
|
}
|
|
4
5
|
function normalizeCommentBody(value) {
|
|
5
6
|
return String(value || '').trim();
|
|
6
7
|
}
|
|
8
|
+
function normalizeIdempotencyKey(value) {
|
|
9
|
+
if (value === undefined || value === null)
|
|
10
|
+
return null;
|
|
11
|
+
const key = String(value).trim();
|
|
12
|
+
if (!key || key.length > 128)
|
|
13
|
+
return null;
|
|
14
|
+
return key;
|
|
15
|
+
}
|
|
16
|
+
function documentReviewCommentFingerprint(input) {
|
|
17
|
+
return createHash('sha256').update(JSON.stringify(input)).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
function resolveDocumentReviewCommentReplay(deps, input) {
|
|
20
|
+
if (!input.idempotencyKey)
|
|
21
|
+
return null;
|
|
22
|
+
const existing = deps.commentStore.getByIdempotencyKey({
|
|
23
|
+
workspaceId: input.workspaceId,
|
|
24
|
+
actorScope: input.actorScope,
|
|
25
|
+
idempotencyKey: input.idempotencyKey,
|
|
26
|
+
});
|
|
27
|
+
const synchronizedCommentId = normalizeId(input.commentId);
|
|
28
|
+
const synchronizedComment = !existing && synchronizedCommentId
|
|
29
|
+
? deps.commentStore.get(synchronizedCommentId, input.workspaceId, { includeDeleted: true })
|
|
30
|
+
: null;
|
|
31
|
+
const replayComment = existing?.comment || synchronizedComment;
|
|
32
|
+
const replayFingerprint = existing?.fingerprint || (synchronizedComment
|
|
33
|
+
? documentReviewCommentFingerprint({
|
|
34
|
+
sessionId: synchronizedComment.sessionId,
|
|
35
|
+
parentCommentId: synchronizedComment.parentCommentId,
|
|
36
|
+
body: normalizeCommentBody(synchronizedComment.body),
|
|
37
|
+
anchor: normalizeAnchorInput(synchronizedComment.anchor),
|
|
38
|
+
actorScope: normalizeId(synchronizedComment.authorActorId) || 'anonymous',
|
|
39
|
+
})
|
|
40
|
+
: null);
|
|
41
|
+
if (!replayComment)
|
|
42
|
+
return null;
|
|
43
|
+
if (replayFingerprint !== input.fingerprint || replayComment.deletedAt) {
|
|
44
|
+
throw deps.createRuleError('idempotencyKey is already assigned to a different document review comment.', 409, 'DOCUMENT_REVIEW_IDEMPOTENCY_CONFLICT');
|
|
45
|
+
}
|
|
46
|
+
return replayComment;
|
|
47
|
+
}
|
|
7
48
|
function normalizeStatus(value) {
|
|
8
49
|
return String(value || '').trim().toLowerCase() === 'resolved' ? 'resolved' : 'open';
|
|
9
50
|
}
|
|
@@ -194,46 +235,74 @@ export function updateDocumentReviewSessionService(deps, input) {
|
|
|
194
235
|
}
|
|
195
236
|
export function addDocumentReviewCommentService(deps, input) {
|
|
196
237
|
const workspaceId = deps.normalizeWorkspaceId(input.workspaceId);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
return inTransaction(deps, () => {
|
|
239
|
+
const existing = deps.sessionStore.get(input.sessionId, workspaceId);
|
|
240
|
+
if (!existing)
|
|
241
|
+
throw deps.createRuleError('Document review session not found.', 404, 'DOCUMENT_REVIEW_SESSION_NOT_FOUND');
|
|
242
|
+
getRequiredDocumentAsset(deps, workspaceId, existing.assetId);
|
|
243
|
+
const body = normalizeCommentBody(input.body);
|
|
244
|
+
if (!body)
|
|
245
|
+
throw deps.createRuleError('Comment body is required.', 400, 'DOCUMENT_REVIEW_COMMENT_REQUIRED');
|
|
246
|
+
const anchorError = validateDocumentReviewAnchorInput(input.anchor);
|
|
247
|
+
if (anchorError)
|
|
248
|
+
throw deps.createRuleError(anchorError, 400, 'DOCUMENT_REVIEW_ANCHOR_INVALID');
|
|
249
|
+
const anchor = normalizeAnchorInput(input.anchor);
|
|
250
|
+
const actorScope = normalizeId(input.actorId) || 'anonymous';
|
|
251
|
+
const idempotencyKey = normalizeIdempotencyKey(input.idempotencyKey);
|
|
252
|
+
const commentId = normalizeId(input.commentId) || null;
|
|
253
|
+
if (input.idempotencyKey != null && !idempotencyKey) {
|
|
254
|
+
throw deps.createRuleError('idempotencyKey must contain 1 to 128 characters.', 400, 'DOCUMENT_REVIEW_IDEMPOTENCY_KEY_INVALID');
|
|
255
|
+
}
|
|
256
|
+
const fingerprint = documentReviewCommentFingerprint({
|
|
257
|
+
sessionId: existing.id, parentCommentId: null, body, anchor, actorScope,
|
|
258
|
+
});
|
|
259
|
+
const replay = resolveDocumentReviewCommentReplay(deps, {
|
|
260
|
+
workspaceId, idempotencyKey, commentId, actorScope, fingerprint,
|
|
261
|
+
});
|
|
262
|
+
if (replay)
|
|
263
|
+
return hydrateSession(deps, existing);
|
|
264
|
+
if (existing.status === 'resolved') {
|
|
265
|
+
throw deps.createRuleError('Reopen this review before adding comments.', 409, 'DOCUMENT_REVIEW_RESOLVED');
|
|
266
|
+
}
|
|
267
|
+
const now = new Date().toISOString();
|
|
268
|
+
const createdCommentId = commentId || deps.generateEntityId('document-review-comment');
|
|
269
|
+
const created = deps.commentStore.create({
|
|
270
|
+
id: createdCommentId,
|
|
271
|
+
sessionId: existing.id,
|
|
272
|
+
workspaceId,
|
|
273
|
+
order: nextCommentOrder(deps.commentStore.listForSession(existing.id, workspaceId, { includeDeleted: true })),
|
|
274
|
+
body,
|
|
275
|
+
anchor,
|
|
276
|
+
authorActorId: normalizeId(input.actorId) || null,
|
|
277
|
+
updatedByActorId: normalizeId(input.actorId) || null,
|
|
278
|
+
deletedByActorId: null,
|
|
279
|
+
parentCommentId: null,
|
|
280
|
+
threadStatus: 'open',
|
|
281
|
+
threadStatusUpdatedAt: now,
|
|
282
|
+
resolvedAt: null,
|
|
283
|
+
resolvedByActorId: null,
|
|
284
|
+
createdAt: now,
|
|
285
|
+
updatedAt: now,
|
|
286
|
+
deletedAt: null,
|
|
287
|
+
idempotencyKey,
|
|
288
|
+
idempotencyFingerprint: idempotencyKey ? fingerprint : null,
|
|
289
|
+
idempotencyActorScope: idempotencyKey ? actorScope : '',
|
|
290
|
+
});
|
|
291
|
+
if (created.id !== createdCommentId) {
|
|
292
|
+
const concurrentReplay = resolveDocumentReviewCommentReplay(deps, {
|
|
293
|
+
workspaceId, idempotencyKey, commentId: createdCommentId, actorScope, fingerprint,
|
|
294
|
+
});
|
|
295
|
+
if (concurrentReplay)
|
|
296
|
+
return hydrateSession(deps, existing);
|
|
297
|
+
}
|
|
298
|
+
const updated = deps.sessionStore.update({
|
|
299
|
+
id: existing.id,
|
|
300
|
+
workspaceId,
|
|
301
|
+
updatedByActorId: normalizeId(input.actorId) || null,
|
|
302
|
+
updatedAt: now,
|
|
303
|
+
});
|
|
304
|
+
return hydrateSession(deps, updated);
|
|
235
305
|
});
|
|
236
|
-
return hydrateSession(deps, updated);
|
|
237
306
|
}
|
|
238
307
|
export function addDocumentReviewReplyService(deps, input) {
|
|
239
308
|
const workspaceId = deps.normalizeWorkspaceId(input.workspaceId);
|
|
@@ -242,24 +311,40 @@ export function addDocumentReviewReplyService(deps, input) {
|
|
|
242
311
|
if (!session)
|
|
243
312
|
throw deps.createRuleError('Document review session not found.', 404, 'DOCUMENT_REVIEW_SESSION_NOT_FOUND');
|
|
244
313
|
getRequiredDocumentAsset(deps, workspaceId, session.assetId);
|
|
314
|
+
const body = normalizeCommentBody(input.body);
|
|
315
|
+
if (!body)
|
|
316
|
+
throw deps.createRuleError('Reply body is required.', 400, 'DOCUMENT_REVIEW_COMMENT_REQUIRED');
|
|
317
|
+
const actorId = normalizeId(input.actorId) || null;
|
|
318
|
+
const actorScope = actorId || 'anonymous';
|
|
319
|
+
const parentCommentId = normalizeId(input.parentCommentId);
|
|
320
|
+
const idempotencyKey = normalizeIdempotencyKey(input.idempotencyKey);
|
|
321
|
+
const commentId = normalizeId(input.commentId) || null;
|
|
322
|
+
if (input.idempotencyKey != null && !idempotencyKey) {
|
|
323
|
+
throw deps.createRuleError('idempotencyKey must contain 1 to 128 characters.', 400, 'DOCUMENT_REVIEW_IDEMPOTENCY_KEY_INVALID');
|
|
324
|
+
}
|
|
325
|
+
const fingerprint = documentReviewCommentFingerprint({
|
|
326
|
+
sessionId: session.id, parentCommentId, body, anchor: null, actorScope,
|
|
327
|
+
});
|
|
328
|
+
const replay = resolveDocumentReviewCommentReplay(deps, {
|
|
329
|
+
workspaceId, idempotencyKey, commentId, actorScope, fingerprint,
|
|
330
|
+
});
|
|
331
|
+
if (replay)
|
|
332
|
+
return hydrateSession(deps, session);
|
|
245
333
|
if (session.status === 'resolved') {
|
|
246
334
|
throw deps.createRuleError('Reopen this review before replying.', 409, 'DOCUMENT_REVIEW_RESOLVED');
|
|
247
335
|
}
|
|
248
336
|
const comments = deps.commentStore.listForSession(session.id, workspaceId, { includeDeleted: true });
|
|
249
|
-
const root = comments.find((comment) => comment.id ===
|
|
337
|
+
const root = comments.find((comment) => comment.id === parentCommentId && !comment.deletedAt);
|
|
250
338
|
if (!root || root.parentCommentId) {
|
|
251
339
|
throw deps.createRuleError('Document review thread not found.', 404, 'DOCUMENT_REVIEW_THREAD_NOT_FOUND');
|
|
252
340
|
}
|
|
253
341
|
if (root.threadStatus === 'resolved') {
|
|
254
342
|
throw deps.createRuleError('Reopen this thread before replying.', 409, 'DOCUMENT_REVIEW_THREAD_RESOLVED');
|
|
255
343
|
}
|
|
256
|
-
const body = normalizeCommentBody(input.body);
|
|
257
|
-
if (!body)
|
|
258
|
-
throw deps.createRuleError('Reply body is required.', 400, 'DOCUMENT_REVIEW_COMMENT_REQUIRED');
|
|
259
|
-
const actorId = normalizeId(input.actorId) || null;
|
|
260
344
|
const now = new Date().toISOString();
|
|
261
|
-
deps.
|
|
262
|
-
|
|
345
|
+
const createdCommentId = commentId || deps.generateEntityId('document-review-comment');
|
|
346
|
+
const created = deps.commentStore.create({
|
|
347
|
+
id: createdCommentId,
|
|
263
348
|
sessionId: session.id,
|
|
264
349
|
workspaceId,
|
|
265
350
|
parentCommentId: root.id,
|
|
@@ -276,7 +361,17 @@ export function addDocumentReviewReplyService(deps, input) {
|
|
|
276
361
|
createdAt: now,
|
|
277
362
|
updatedAt: now,
|
|
278
363
|
deletedAt: null,
|
|
364
|
+
idempotencyKey,
|
|
365
|
+
idempotencyFingerprint: idempotencyKey ? fingerprint : null,
|
|
366
|
+
idempotencyActorScope: idempotencyKey ? actorScope : '',
|
|
279
367
|
});
|
|
368
|
+
if (created.id !== createdCommentId) {
|
|
369
|
+
const concurrentReplay = resolveDocumentReviewCommentReplay(deps, {
|
|
370
|
+
workspaceId, idempotencyKey, commentId: createdCommentId, actorScope, fingerprint,
|
|
371
|
+
});
|
|
372
|
+
if (concurrentReplay)
|
|
373
|
+
return hydrateSession(deps, session);
|
|
374
|
+
}
|
|
280
375
|
const updated = deps.sessionStore.update({
|
|
281
376
|
id: session.id,
|
|
282
377
|
workspaceId,
|
|
@@ -22,11 +22,14 @@ interface UseTaskFormActionsInput {
|
|
|
22
22
|
editingTaskId: string | null;
|
|
23
23
|
fetchTasks: (isSilent?: boolean) => Promise<void>;
|
|
24
24
|
formTaxonomies: Record<string, string | number | string[] | number[]>;
|
|
25
|
+
getAttachmentsDraft?: () => Array<string | AttachmentItem>;
|
|
25
26
|
getDescriptionDraft?: () => string;
|
|
26
27
|
getPreferredCategoryValue: (categories: CategoryConfig[]) => string;
|
|
27
28
|
getTitleDraft?: () => string;
|
|
29
|
+
awaitPendingDescriptionImageUpload?: () => Promise<void>;
|
|
28
30
|
mergeTaskFromServer: (task: unknown) => void;
|
|
29
31
|
onTaskCreated?: (task: TaskItem) => void;
|
|
32
|
+
onTaskCreateAccepted?: (task: TaskItem) => Promise<void> | void;
|
|
30
33
|
workstreamInput: string;
|
|
31
34
|
priority: number | string;
|
|
32
35
|
pushNotice: (message: string, tone?: 'success' | 'error' | 'info') => void;
|
|
@@ -4,13 +4,18 @@ import { normalizeTaskFromApi } from '../../utils/taskNormalization.js';
|
|
|
4
4
|
import { createCloudTaskHttpMutationClient, fetchTaskHttpMutation, isConclusiveTaskHttpResponse, } from './taskHttpMutation.js';
|
|
5
5
|
import { canonicalWorkspaceSyncJson } from '../../sync/v3/canonicalWorkspaceSyncJson.js';
|
|
6
6
|
export function useTaskFormActions(input) {
|
|
7
|
-
const { activeCategories, activeTab, apiEndpoint, assignee, attachments, attachmentsDirty, category, checklistItems, comments, complexity, description, dueDate, createTaskDurably, durableTaskHttpRootActivationEnabled = false, workspaceId, taskHttpMutationClient, updateTask, editingTaskId, fetchTasks, formTaxonomies, getDescriptionDraft: getDescriptionDraftInput, getPreferredCategoryValue, getTitleDraft: getTitleDraftInput, mergeTaskFromServer, onTaskCreated, workstreamInput, priority, pushNotice, queueWorkspaceSyncFromAuthoritativeTaskState, relationshipTasks, supplementalTasks = [], resetForm, resolveWorkstreamIdInput, scheduledDate, setActiveTab, setAttachmentsDirty, setComments, setError, setLastUsedCategory, setLoading, setNewCommentText, status, title, taxonomies, textDraftOccupied, type } = input;
|
|
7
|
+
const { activeCategories, activeTab, apiEndpoint, assignee, attachments, attachmentsDirty, category, checklistItems, comments, complexity, description, dueDate, createTaskDurably, durableTaskHttpRootActivationEnabled = false, workspaceId, taskHttpMutationClient, updateTask, editingTaskId, fetchTasks, formTaxonomies, getAttachmentsDraft: getAttachmentsDraftInput, getDescriptionDraft: getDescriptionDraftInput, getPreferredCategoryValue, getTitleDraft: getTitleDraftInput, awaitPendingDescriptionImageUpload: awaitPendingDescriptionImageUploadInput, mergeTaskFromServer, onTaskCreated, onTaskCreateAccepted, workstreamInput, priority, pushNotice, queueWorkspaceSyncFromAuthoritativeTaskState, relationshipTasks, supplementalTasks = [], resetForm, resolveWorkstreamIdInput, scheduledDate, setActiveTab, setAttachmentsDirty, setComments, setError, setLastUsedCategory, setLoading, setNewCommentText, status, title, taxonomies, textDraftOccupied, type } = input;
|
|
8
8
|
const getTitleDraft = getTitleDraftInput || (() => title);
|
|
9
9
|
const getDescriptionDraft = getDescriptionDraftInput || (() => description);
|
|
10
|
+
const getAttachmentsDraft = getAttachmentsDraftInput || (() => attachments);
|
|
10
11
|
const getTitleDraftRef = useRef(getTitleDraft);
|
|
11
12
|
const getDescriptionDraftRef = useRef(getDescriptionDraft);
|
|
13
|
+
const getAttachmentsDraftRef = useRef(getAttachmentsDraft);
|
|
14
|
+
const awaitPendingDescriptionImageUploadRef = useRef(awaitPendingDescriptionImageUploadInput);
|
|
12
15
|
getTitleDraftRef.current = getTitleDraft;
|
|
13
16
|
getDescriptionDraftRef.current = getDescriptionDraft;
|
|
17
|
+
getAttachmentsDraftRef.current = getAttachmentsDraft;
|
|
18
|
+
awaitPendingDescriptionImageUploadRef.current = awaitPendingDescriptionImageUploadInput;
|
|
14
19
|
const readTextDraft = useCallback(() => ({
|
|
15
20
|
title: getTitleDraftRef.current(),
|
|
16
21
|
description: getDescriptionDraftRef.current(),
|
|
@@ -48,7 +53,7 @@ export function useTaskFormActions(input) {
|
|
|
48
53
|
}
|
|
49
54
|
createDraftOccupiedRef.current = createDraftOccupied;
|
|
50
55
|
}, [createDraftOccupied, createTaskDurably, workspaceId]);
|
|
51
|
-
const buildTaskDraftSignature = useCallback((textDraft = readTextDraft()) => JSON.stringify({
|
|
56
|
+
const buildTaskDraftSignature = useCallback((textDraft = readTextDraft(), attachmentDraft = getAttachmentsDraftRef.current()) => JSON.stringify({
|
|
52
57
|
title: textDraft.title.trim(),
|
|
53
58
|
description: textDraft.description,
|
|
54
59
|
category: typeof category === 'string'
|
|
@@ -64,11 +69,10 @@ export function useTaskFormActions(input) {
|
|
|
64
69
|
checklistItems,
|
|
65
70
|
comments,
|
|
66
71
|
taxonomies: formTaxonomies,
|
|
67
|
-
attachments,
|
|
72
|
+
attachments: attachmentDraft,
|
|
68
73
|
}), [
|
|
69
74
|
activeCategories,
|
|
70
75
|
assignee,
|
|
71
|
-
attachments,
|
|
72
76
|
category,
|
|
73
77
|
checklistItems,
|
|
74
78
|
comments,
|
|
@@ -123,9 +127,23 @@ export function useTaskFormActions(input) {
|
|
|
123
127
|
const saveTask = useCallback(async (options) => {
|
|
124
128
|
const quiet = options?.quiet ?? false;
|
|
125
129
|
const source = options?.source ?? 'manual';
|
|
130
|
+
if (!editingTaskId && source === 'manual') {
|
|
131
|
+
try {
|
|
132
|
+
await awaitPendingDescriptionImageUploadRef.current?.();
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.error('Description image upload did not settle before task creation.', error);
|
|
136
|
+
const message = 'Wait for the Description image upload to finish, then try again.';
|
|
137
|
+
setError(message);
|
|
138
|
+
if (!quiet)
|
|
139
|
+
pushNotice(message, 'error');
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
126
143
|
const textDraft = options?.textDraft ?? readTextDraft();
|
|
127
144
|
const draftTitle = textDraft.title;
|
|
128
145
|
const draftDescription = textDraft.description;
|
|
146
|
+
const draftAttachments = getAttachmentsDraftRef.current();
|
|
129
147
|
if (!editingTaskId) {
|
|
130
148
|
createdTaskRef.current = null;
|
|
131
149
|
}
|
|
@@ -218,7 +236,7 @@ export function useTaskFormActions(input) {
|
|
|
218
236
|
if (!editingTaskId)
|
|
219
237
|
body.comments = comments;
|
|
220
238
|
if (!editingTaskId || attachmentsDirty) {
|
|
221
|
-
body.attachments =
|
|
239
|
+
body.attachments = draftAttachments;
|
|
222
240
|
}
|
|
223
241
|
if (editingTaskId && Object.keys(body).length > 0) {
|
|
224
242
|
body.saveSource = source;
|
|
@@ -383,7 +401,6 @@ export function useTaskFormActions(input) {
|
|
|
383
401
|
activeCategories,
|
|
384
402
|
apiEndpoint,
|
|
385
403
|
assignee,
|
|
386
|
-
attachments,
|
|
387
404
|
attachmentsDirty,
|
|
388
405
|
category,
|
|
389
406
|
checklistItems,
|
|
@@ -424,6 +441,15 @@ export function useTaskFormActions(input) {
|
|
|
424
441
|
suppressNextCreateSuccessNoticeRef.current = false;
|
|
425
442
|
if (activeTab === 'add' && !editingTaskId) {
|
|
426
443
|
const createdTask = createdTaskRef.current;
|
|
444
|
+
if (createdTask && onTaskCreateAccepted) {
|
|
445
|
+
try {
|
|
446
|
+
await onTaskCreateAccepted(createdTask);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
console.warn('Task create draft upload promotion failed.', error);
|
|
450
|
+
pushNotice('Task created successfully. Temporary image cleanup will retry automatically.', 'info');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
427
453
|
if (createdTask && onTaskCreated) {
|
|
428
454
|
onTaskCreated(createdTask);
|
|
429
455
|
return;
|
|
@@ -433,7 +459,7 @@ export function useTaskFormActions(input) {
|
|
|
433
459
|
await fetchTasks();
|
|
434
460
|
}
|
|
435
461
|
}
|
|
436
|
-
}, [activeTab, editingTaskId, fetchTasks, onTaskCreated, pushNotice, resetForm, saveTask, setActiveTab]);
|
|
462
|
+
}, [activeTab, editingTaskId, fetchTasks, onTaskCreateAccepted, onTaskCreated, pushNotice, resetForm, saveTask, setActiveTab]);
|
|
437
463
|
useEffect(() => {
|
|
438
464
|
clearAutoSaveTimer();
|
|
439
465
|
queuedAutoSaveRef.current = null;
|