@taskforcehq/taskforce 0.3.300 → 0.3.302
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/README.md +19 -0
- package/dist/Taskforce.module.css +26 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +13 -24
- package/dist/TaskforceCore.test.js +167 -152
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.js +9 -8
- package/dist/components/views/StandaloneLayout.js +490 -207
- package/dist/components/views/panels/FilterToolbar.d.ts +2 -0
- package/dist/components/views/panels/FilterToolbarControls.d.ts +1 -0
- package/dist/components/views/panels/FilterToolbarControls.js +1 -1
- package/dist/components/views/panels/PlanningDrawer.js +19 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +49 -0
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +241 -78
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useTaskData.js +16 -11
- package/dist/hooks/useTaskData.test.js +127 -0
- package/dist/hooks/useTaskforce.d.ts +5 -0
- package/dist/hooks/useTaskforce.js +63 -15
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +45 -23
- package/dist/mcp/runtime.test.js +56 -3
- package/dist/migrations/taskSchemaMigrations.js +53 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +15 -1
- package/dist/server/index.test.js +14 -1
- package/dist/server/routes/admin.js +25 -22
- package/dist/server/routes/auth.d.ts +5 -0
- package/dist/server/routes/auth.js +350 -16
- package/dist/server/routes/billing.js +11 -1
- package/dist/server/routes/billing.test.js +32 -2
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +11 -3
- package/dist/server/routes/sync.integration.test.js +130 -0
- package/dist/server/routes/sync.js +226 -1924
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +26 -19
- package/dist/server/routes.test.js +352 -6
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +88 -0
- package/dist/sync/contentSyncState.js +743 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +28 -0
- package/dist/sync/workspacePullFeed.js +301 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +166 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-TVmCgizS.js → AgentsModule-JG5jc3he.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CAyhnPeR.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-BOCw5e4D.js → DocumentWorkspace-iys3zfca.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-CN0r02Oi.js → InitiativesModule-DJvLJI3A.js} +1 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-CY9R2zPB.js → WorkflowsModule-BguHeQaV.js} +1 -1
- package/dist/ui/assets/index-MXeWoebC.css +1 -0
- package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/billingStatusCache.d.ts +21 -0
- package/dist/utils/billingStatusCache.js +70 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -1
- package/scripts/deploy-prod.sh +41 -0
- package/scripts/deploy-staging.sh +23 -0
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-FcQSNLZd.js +0 -1
- package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +0 -8
- package/dist/ui/assets/index-Bzqy5QGm.js +0 -6
- package/dist/ui/assets/index-DxRiI3tY.css +0 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { compareWorkspaceMutationOrdering } from './workspaceSyncSurface.js';
|
|
5
|
+
export function resolveSyncConflictOrdering(incomingWatermark, existingWatermark, incomingTieBreak, existingTieBreak) {
|
|
6
|
+
return compareWorkspaceMutationOrdering(String(incomingWatermark || '').trim(), String(existingWatermark || '').trim(), String(incomingTieBreak || '').trim(), String(existingTieBreak || '').trim());
|
|
7
|
+
}
|
|
8
|
+
export function applyPlanningSyncChanges(input) {
|
|
9
|
+
const { core, workspaceId, incomingInitiatives, incomingWorkstreams, incomingTaxonomies, incomingTaxonomyState, incomingTaxonomiesUpdatedAt, workspaceTaxonomySyncMeta, recordSyncDiagnostic, } = input;
|
|
10
|
+
if (incomingInitiatives.size > 0) {
|
|
11
|
+
for (const initiative of incomingInitiatives.values()) {
|
|
12
|
+
try {
|
|
13
|
+
core.upsertInitiativeForSync({
|
|
14
|
+
id: initiative.id,
|
|
15
|
+
referenceNumber: initiative.referenceNumber,
|
|
16
|
+
referenceLabel: initiative.referenceLabel,
|
|
17
|
+
title: initiative.title,
|
|
18
|
+
description: initiative.description,
|
|
19
|
+
ownerId: initiative.ownerId,
|
|
20
|
+
createdAt: initiative.createdAt,
|
|
21
|
+
updatedAt: initiative.updatedAt,
|
|
22
|
+
order: initiative.order,
|
|
23
|
+
isArchived: initiative.isArchived
|
|
24
|
+
}, workspaceId);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert initiative during sync apply.', {
|
|
28
|
+
source: 'apply.initiativeUpsert',
|
|
29
|
+
initiativeId: initiative.id,
|
|
30
|
+
error: String(error?.message || error || '')
|
|
31
|
+
});
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (incomingWorkstreams.size > 0) {
|
|
37
|
+
for (const workstream of incomingWorkstreams.values()) {
|
|
38
|
+
try {
|
|
39
|
+
core.upsertWorkstreamForSync({
|
|
40
|
+
id: workstream.id,
|
|
41
|
+
referenceNumber: workstream.referenceNumber,
|
|
42
|
+
referenceLabel: workstream.referenceLabel,
|
|
43
|
+
initiativeId: workstream.initiativeId,
|
|
44
|
+
title: workstream.title,
|
|
45
|
+
description: workstream.description,
|
|
46
|
+
ownerId: workstream.ownerId,
|
|
47
|
+
createdAt: workstream.createdAt,
|
|
48
|
+
updatedAt: workstream.updatedAt,
|
|
49
|
+
order: workstream.order,
|
|
50
|
+
isArchived: workstream.isArchived
|
|
51
|
+
}, workspaceId);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert workstream during sync apply.', {
|
|
55
|
+
source: 'apply.workstreamUpsert',
|
|
56
|
+
workstreamId: workstream.id,
|
|
57
|
+
error: String(error?.message || error || '')
|
|
58
|
+
});
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (incomingTaxonomyState || Array.isArray(incomingTaxonomies)) {
|
|
64
|
+
const nextTaxonomyState = incomingTaxonomyState || { taxonomies: incomingTaxonomies || [] };
|
|
65
|
+
if (!incomingTaxonomiesUpdatedAt) {
|
|
66
|
+
if (typeof core.updateTaxonomyState === 'function') {
|
|
67
|
+
core.updateTaxonomyState(nextTaxonomyState, workspaceId);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
core.updateTaxonomies(Array.isArray(incomingTaxonomies) ? incomingTaxonomies : []);
|
|
71
|
+
}
|
|
72
|
+
const fallbackHash = createHash('sha256').update(JSON.stringify(nextTaxonomyState)).digest('hex');
|
|
73
|
+
workspaceTaxonomySyncMeta.set(workspaceId, {
|
|
74
|
+
updatedAt: new Date().toISOString(),
|
|
75
|
+
hash: fallbackHash
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const incomingTaxonomyHash = createHash('sha256').update(JSON.stringify(nextTaxonomyState)).digest('hex');
|
|
80
|
+
const currentMeta = workspaceTaxonomySyncMeta.get(workspaceId) || { updatedAt: '', hash: '' };
|
|
81
|
+
const compare = resolveSyncConflictOrdering(incomingTaxonomiesUpdatedAt, currentMeta.updatedAt, incomingTaxonomyHash, currentMeta.hash);
|
|
82
|
+
if (compare >= 0) {
|
|
83
|
+
if (typeof core.updateTaxonomyState === 'function') {
|
|
84
|
+
core.updateTaxonomyState(nextTaxonomyState, workspaceId);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
core.updateTaxonomies(Array.isArray(incomingTaxonomies) ? incomingTaxonomies : []);
|
|
88
|
+
}
|
|
89
|
+
workspaceTaxonomySyncMeta.set(workspaceId, {
|
|
90
|
+
updatedAt: incomingTaxonomiesUpdatedAt,
|
|
91
|
+
hash: incomingTaxonomyHash
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function applyAiProfileAndMemberSyncChanges(input) {
|
|
98
|
+
const { core, workspaceId, incomingAiProfiles, workspaceMembers, recordSyncDiagnostic, assertSyncedAiProfilesPersisted, createSyncRouteError, } = input;
|
|
99
|
+
if (incomingAiProfiles.size > 0) {
|
|
100
|
+
for (const profile of incomingAiProfiles.values()) {
|
|
101
|
+
try {
|
|
102
|
+
core.upsertAiProfileForSync({
|
|
103
|
+
id: profile.id,
|
|
104
|
+
workspaceId,
|
|
105
|
+
profileToken: profile.profileToken,
|
|
106
|
+
name: profile.name,
|
|
107
|
+
username: profile.username,
|
|
108
|
+
icon: profile.icon,
|
|
109
|
+
color: profile.color,
|
|
110
|
+
surfaceType: profile.surfaceType,
|
|
111
|
+
providerMetadata: profile.providerMetadata,
|
|
112
|
+
createdAt: profile.createdAt,
|
|
113
|
+
updatedAt: profile.updatedAt,
|
|
114
|
+
lastActiveAt: profile.lastActiveAt
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const details = {
|
|
119
|
+
source: 'apply.aiProfileUpsert',
|
|
120
|
+
workspaceId,
|
|
121
|
+
profileId: profile.id,
|
|
122
|
+
error: String(error?.message || error || ''),
|
|
123
|
+
errorCode: String(error?.code || '').trim() || null
|
|
124
|
+
};
|
|
125
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert AI profile during sync apply.', details);
|
|
126
|
+
throw createSyncRouteError(`Failed to persist AI profile ${profile.id} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'AI_PROFILE_SYNC_APPLY_FAILED', statusCode: 500 });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
assertSyncedAiProfilesPersisted(workspaceId, incomingAiProfiles);
|
|
130
|
+
}
|
|
131
|
+
if (Array.isArray(workspaceMembers)) {
|
|
132
|
+
core.replaceWorkspaceUsersForSync({
|
|
133
|
+
workspaceId,
|
|
134
|
+
users: workspaceMembers
|
|
135
|
+
.filter((member) => String(member?.workspaceId || workspaceId).trim() === workspaceId)
|
|
136
|
+
.map((member) => ({
|
|
137
|
+
userId: member.userId,
|
|
138
|
+
email: member.email,
|
|
139
|
+
displayName: member.displayName,
|
|
140
|
+
role: member.role,
|
|
141
|
+
permissionMode: member.permissionMode,
|
|
142
|
+
betaAccess: member.betaAccess,
|
|
143
|
+
status: member.status,
|
|
144
|
+
disabled: member.disabled,
|
|
145
|
+
createdAt: member.createdAt,
|
|
146
|
+
updatedAt: member.updatedAt,
|
|
147
|
+
emailVerifiedAt: member.emailVerifiedAt
|
|
148
|
+
}))
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export async function applyContentSyncChanges(input) {
|
|
153
|
+
const { core, workspaceId, incomingDocuments, incomingDocumentDeletes, incomingAssets, incomingAssetDeletes, basePath, hasObjectStorageClient, hasDocumentSyncMetadataDrift, hasBinaryAssetSyncMetadataDrift, writeWorkspaceSyncDocument, deleteWorkspaceSyncDocument, writeWorkspaceSyncBinaryAsset, deleteWorkspaceSyncBinaryAsset, getExistingBinaryAsset, recordSyncDiagnostic, options, } = input;
|
|
154
|
+
if (incomingDocuments.length > 0) {
|
|
155
|
+
for (const doc of incomingDocuments) {
|
|
156
|
+
try {
|
|
157
|
+
const existingDoc = core.getDocumentWatermark(workspaceId, doc.path);
|
|
158
|
+
const incomingHash = createHash('sha256').update(String(doc.content || '')).digest('hex');
|
|
159
|
+
const existingUpdatedAt = String(existingDoc?.updatedAt || '').trim();
|
|
160
|
+
const existingHash = String(existingDoc?.contentHash || '').trim();
|
|
161
|
+
const compare = resolveSyncConflictOrdering(doc.updatedAt, existingUpdatedAt, incomingHash, existingHash);
|
|
162
|
+
const metadataDrift = hasDocumentSyncMetadataDrift(workspaceId, doc, options);
|
|
163
|
+
if (compare < 0)
|
|
164
|
+
continue;
|
|
165
|
+
if (compare === 0 && existingHash === incomingHash && !metadataDrift)
|
|
166
|
+
continue;
|
|
167
|
+
await writeWorkspaceSyncDocument(basePath, doc, workspaceId, options);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to apply synced document change.', {
|
|
171
|
+
source: 'documents.apply.write',
|
|
172
|
+
path: doc.path,
|
|
173
|
+
assetId: typeof doc.assetId === 'string' ? doc.assetId : null,
|
|
174
|
+
error: String(error?.message || error || '')
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (incomingDocumentDeletes.size > 0) {
|
|
180
|
+
for (const [docPath, deletedAt] of incomingDocumentDeletes.entries()) {
|
|
181
|
+
const existingDoc = core.getDocumentWatermark(workspaceId, docPath);
|
|
182
|
+
const existingUpdatedAt = String(existingDoc?.updatedAt || '').trim();
|
|
183
|
+
const existingHash = String(existingDoc?.contentHash || '').trim();
|
|
184
|
+
const compare = resolveSyncConflictOrdering(deletedAt, existingUpdatedAt, `delete:${docPath}`, existingHash);
|
|
185
|
+
if (existingDoc && compare < 0)
|
|
186
|
+
continue;
|
|
187
|
+
deleteWorkspaceSyncDocument(basePath, docPath, workspaceId);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (incomingAssets.length > 0) {
|
|
191
|
+
for (const asset of incomingAssets) {
|
|
192
|
+
try {
|
|
193
|
+
const existing = getExistingBinaryAsset(asset.path, workspaceId);
|
|
194
|
+
const incomingHash = createHash('sha256').update(Buffer.from(String(asset.contentBase64 || ''), 'base64')).digest('hex');
|
|
195
|
+
const existingUpdatedAt = String(existing?.updatedAt || '').trim();
|
|
196
|
+
const existingHash = String(existing?.contentSha256 || '').trim();
|
|
197
|
+
const localFileMissing = !hasObjectStorageClient() && !fs.existsSync(path.resolve(basePath, asset.path));
|
|
198
|
+
const compare = resolveSyncConflictOrdering(asset.updatedAt, existingUpdatedAt, incomingHash, existingHash);
|
|
199
|
+
const metadataDrift = hasBinaryAssetSyncMetadataDrift(workspaceId, asset, options);
|
|
200
|
+
if (compare < 0)
|
|
201
|
+
continue;
|
|
202
|
+
if (compare === 0 && existingHash === incomingHash && !localFileMissing && !metadataDrift)
|
|
203
|
+
continue;
|
|
204
|
+
await writeWorkspaceSyncBinaryAsset(basePath, asset, workspaceId, options);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to apply synced asset change.', {
|
|
208
|
+
source: 'assets.apply.write',
|
|
209
|
+
path: asset.path,
|
|
210
|
+
assetId: typeof asset.assetId === 'string' ? asset.assetId : null,
|
|
211
|
+
kind: asset.kind,
|
|
212
|
+
error: String(error?.message || error || '')
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (incomingAssetDeletes.size > 0) {
|
|
218
|
+
for (const [assetPath, deletedAt] of incomingAssetDeletes.entries()) {
|
|
219
|
+
const existing = getExistingBinaryAsset(assetPath, workspaceId);
|
|
220
|
+
const existingUpdatedAt = String(existing?.updatedAt || '').trim();
|
|
221
|
+
const existingHash = String(existing?.contentSha256 || '').trim();
|
|
222
|
+
const compare = resolveSyncConflictOrdering(deletedAt, existingUpdatedAt, `delete:${assetPath}`, existingHash);
|
|
223
|
+
if (existing && compare < 0)
|
|
224
|
+
continue;
|
|
225
|
+
deleteWorkspaceSyncBinaryAsset(basePath, assetPath, workspaceId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (incomingDocuments.length > 0 || incomingAssets.length > 0) {
|
|
229
|
+
core.notifyDocumentMetadataMutation(workspaceId, [
|
|
230
|
+
...incomingDocuments.map((entry) => entry.path),
|
|
231
|
+
...incomingAssets.map((entry) => entry.path)
|
|
232
|
+
], 'upsert');
|
|
233
|
+
}
|
|
234
|
+
if (incomingDocumentDeletes.size > 0 || incomingAssetDeletes.size > 0) {
|
|
235
|
+
core.notifyDocumentMetadataMutation(workspaceId, [
|
|
236
|
+
...Array.from(incomingDocumentDeletes.keys()),
|
|
237
|
+
...Array.from(incomingAssetDeletes.keys())
|
|
238
|
+
], 'delete');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
export function applyCollaborationSyncChanges(input) {
|
|
242
|
+
const { core, workspaceId, incomingDocumentReviewSessions, incomingDocumentReviewSessionDeletes, incomingDocumentReviewComments, incomingDocumentReviewCommentDeletes, incomingAnnotatedAttachmentSessions, incomingAnnotatedAttachmentSessionDeletes, recordSyncDiagnostic, createSyncRouteError, } = input;
|
|
243
|
+
if (incomingDocumentReviewSessions.size > 0) {
|
|
244
|
+
for (const session of incomingDocumentReviewSessions.values()) {
|
|
245
|
+
try {
|
|
246
|
+
const existing = core.getDocumentReviewSession({
|
|
247
|
+
workspaceId,
|
|
248
|
+
sessionId: session.id,
|
|
249
|
+
includeDeleted: true
|
|
250
|
+
});
|
|
251
|
+
const incomingTieBreak = createHash('sha256').update(JSON.stringify(session.comments || [])).digest('hex');
|
|
252
|
+
const existingTieBreak = existing
|
|
253
|
+
? createHash('sha256').update(JSON.stringify(existing.comments || [])).digest('hex')
|
|
254
|
+
: '';
|
|
255
|
+
const compare = resolveSyncConflictOrdering(session.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
|
|
256
|
+
if (compare < 0)
|
|
257
|
+
continue;
|
|
258
|
+
core.upsertDocumentReviewSessionForSync({
|
|
259
|
+
id: session.id,
|
|
260
|
+
tenantId: core.getTenantId(),
|
|
261
|
+
workspaceId,
|
|
262
|
+
assetId: session.assetId,
|
|
263
|
+
documentId: session.documentId,
|
|
264
|
+
documentVersion: session.documentVersion,
|
|
265
|
+
title: session.title,
|
|
266
|
+
status: session.status,
|
|
267
|
+
comments: [],
|
|
268
|
+
createdByActorId: session.createdByActorId,
|
|
269
|
+
updatedByActorId: session.updatedByActorId,
|
|
270
|
+
createdAt: session.createdAt,
|
|
271
|
+
updatedAt: session.updatedAt,
|
|
272
|
+
deletedAt: session.deletedAt,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
const details = {
|
|
277
|
+
source: 'apply.documentReviewUpsert',
|
|
278
|
+
sessionId: session.id,
|
|
279
|
+
error: String(error?.message || error || ''),
|
|
280
|
+
errorCode: String(error?.code || '').trim() || null
|
|
281
|
+
};
|
|
282
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert document review session during sync apply.', details);
|
|
283
|
+
throw createSyncRouteError(`Failed to persist document review session ${session.id} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'DOCUMENT_REVIEW_SESSION_SYNC_APPLY_FAILED', statusCode: 500 });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (incomingDocumentReviewComments.size > 0) {
|
|
288
|
+
for (const comment of incomingDocumentReviewComments.values()) {
|
|
289
|
+
try {
|
|
290
|
+
const existingComments = core.listDocumentReviewCommentsForSync({
|
|
291
|
+
workspaceId,
|
|
292
|
+
includeDeleted: true
|
|
293
|
+
});
|
|
294
|
+
const existing = existingComments.find((entry) => entry.id === comment.id) || null;
|
|
295
|
+
const incomingTieBreak = createHash('sha256')
|
|
296
|
+
.update(JSON.stringify({ body: comment.body, anchor: comment.anchor, deletedAt: comment.deletedAt }))
|
|
297
|
+
.digest('hex');
|
|
298
|
+
const existingTieBreak = existing
|
|
299
|
+
? createHash('sha256')
|
|
300
|
+
.update(JSON.stringify({ body: existing.body, anchor: existing.anchor, deletedAt: existing.deletedAt }))
|
|
301
|
+
.digest('hex')
|
|
302
|
+
: '';
|
|
303
|
+
const compare = resolveSyncConflictOrdering(comment.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
|
|
304
|
+
if (compare < 0)
|
|
305
|
+
continue;
|
|
306
|
+
core.upsertDocumentReviewCommentForSync({
|
|
307
|
+
id: comment.id,
|
|
308
|
+
tenantId: core.getTenantId(),
|
|
309
|
+
workspaceId,
|
|
310
|
+
sessionId: comment.sessionId,
|
|
311
|
+
body: comment.body,
|
|
312
|
+
anchor: comment.anchor,
|
|
313
|
+
order: comment.order,
|
|
314
|
+
authorActorId: comment.authorActorId,
|
|
315
|
+
createdAt: comment.createdAt,
|
|
316
|
+
updatedAt: comment.updatedAt,
|
|
317
|
+
deletedAt: comment.deletedAt,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert document review comment during sync apply.', {
|
|
322
|
+
source: 'apply.documentReviewCommentUpsert',
|
|
323
|
+
commentId: comment.id,
|
|
324
|
+
error: String(error?.message || error || '')
|
|
325
|
+
});
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (incomingDocumentReviewSessionDeletes.size > 0) {
|
|
331
|
+
for (const [sessionId, deletedAt] of incomingDocumentReviewSessionDeletes.entries()) {
|
|
332
|
+
try {
|
|
333
|
+
const existing = core.getDocumentReviewSession({
|
|
334
|
+
workspaceId,
|
|
335
|
+
sessionId,
|
|
336
|
+
includeDeleted: true
|
|
337
|
+
});
|
|
338
|
+
const compare = resolveSyncConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${sessionId}`, String(existing?.id || '').trim());
|
|
339
|
+
if (existing && compare < 0)
|
|
340
|
+
continue;
|
|
341
|
+
core.deleteDocumentReviewSessionForSync({ workspaceId, sessionId, deletedAt });
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
const details = {
|
|
345
|
+
source: 'apply.documentReviewDelete',
|
|
346
|
+
sessionId,
|
|
347
|
+
error: String(error?.message || error || ''),
|
|
348
|
+
errorCode: String(error?.code || '').trim() || null
|
|
349
|
+
};
|
|
350
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete document review session during sync apply.', details);
|
|
351
|
+
throw createSyncRouteError(`Failed to delete document review session ${sessionId} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'DOCUMENT_REVIEW_SESSION_SYNC_DELETE_FAILED', statusCode: 500 });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (incomingDocumentReviewCommentDeletes.size > 0) {
|
|
356
|
+
for (const [commentId, deletedAt] of incomingDocumentReviewCommentDeletes.entries()) {
|
|
357
|
+
try {
|
|
358
|
+
const existing = core.listDocumentReviewCommentsForSync({
|
|
359
|
+
workspaceId,
|
|
360
|
+
includeDeleted: true
|
|
361
|
+
}).find((entry) => entry.id === commentId) || null;
|
|
362
|
+
const compare = resolveSyncConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${commentId}`, String(existing?.id || '').trim());
|
|
363
|
+
if (existing && compare < 0)
|
|
364
|
+
continue;
|
|
365
|
+
core.deleteDocumentReviewCommentForSync({ workspaceId, commentId, deletedAt });
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete document review comment during sync apply.', {
|
|
369
|
+
source: 'apply.documentReviewCommentDelete',
|
|
370
|
+
commentId,
|
|
371
|
+
error: String(error?.message || error || '')
|
|
372
|
+
});
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (incomingAnnotatedAttachmentSessions.size > 0) {
|
|
378
|
+
for (const session of incomingAnnotatedAttachmentSessions.values()) {
|
|
379
|
+
try {
|
|
380
|
+
const existing = core.getAnnotatedAttachmentSession({
|
|
381
|
+
workspaceId,
|
|
382
|
+
sessionId: session.id,
|
|
383
|
+
includeDeleted: true
|
|
384
|
+
});
|
|
385
|
+
const incomingTieBreak = createHash('sha256').update(JSON.stringify(session.annotations || [])).digest('hex');
|
|
386
|
+
const existingTieBreak = existing
|
|
387
|
+
? createHash('sha256').update(JSON.stringify(existing.annotations || [])).digest('hex')
|
|
388
|
+
: '';
|
|
389
|
+
const compare = resolveSyncConflictOrdering(session.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
|
|
390
|
+
if (compare < 0)
|
|
391
|
+
continue;
|
|
392
|
+
core.upsertAnnotatedAttachmentSessionForSync({
|
|
393
|
+
id: session.id,
|
|
394
|
+
tenantId: core.getTenantId(),
|
|
395
|
+
workspaceId,
|
|
396
|
+
taskId: session.taskId,
|
|
397
|
+
baseImageAssetId: session.baseImageAssetId,
|
|
398
|
+
title: session.title,
|
|
399
|
+
globalInstruction: session.globalInstruction,
|
|
400
|
+
annotations: session.annotations,
|
|
401
|
+
createdByActorId: session.createdByActorId,
|
|
402
|
+
updatedByActorId: session.updatedByActorId,
|
|
403
|
+
createdAt: session.createdAt,
|
|
404
|
+
updatedAt: session.updatedAt,
|
|
405
|
+
deletedAt: session.deletedAt,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
const details = {
|
|
410
|
+
source: 'apply.annotatedAttachmentUpsert',
|
|
411
|
+
sessionId: session.id,
|
|
412
|
+
error: String(error?.message || error || ''),
|
|
413
|
+
errorCode: String(error?.code || '').trim() || null
|
|
414
|
+
};
|
|
415
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert annotated attachment session during sync apply.', details);
|
|
416
|
+
throw createSyncRouteError(`Failed to persist annotated attachment session ${session.id} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'ANNOTATED_ATTACHMENT_SESSION_SYNC_APPLY_FAILED', statusCode: 500 });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (incomingAnnotatedAttachmentSessionDeletes.size > 0) {
|
|
421
|
+
for (const [sessionId, deletedAt] of incomingAnnotatedAttachmentSessionDeletes.entries()) {
|
|
422
|
+
try {
|
|
423
|
+
const existing = core.getAnnotatedAttachmentSession({
|
|
424
|
+
workspaceId,
|
|
425
|
+
sessionId,
|
|
426
|
+
includeDeleted: true
|
|
427
|
+
});
|
|
428
|
+
const compare = resolveSyncConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${sessionId}`, String(existing?.id || '').trim());
|
|
429
|
+
if (existing && compare < 0)
|
|
430
|
+
continue;
|
|
431
|
+
core.deleteAnnotatedAttachmentSessionForSync({ workspaceId, sessionId, deletedAt });
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
const details = {
|
|
435
|
+
source: 'apply.annotatedAttachmentDelete',
|
|
436
|
+
sessionId,
|
|
437
|
+
error: String(error?.message || error || ''),
|
|
438
|
+
errorCode: String(error?.code || '').trim() || null
|
|
439
|
+
};
|
|
440
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete annotated attachment session during sync apply.', details);
|
|
441
|
+
throw createSyncRouteError(`Failed to delete annotated attachment session ${sessionId} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'ANNOTATED_ATTACHMENT_SESSION_SYNC_DELETE_FAILED', statusCode: 500 });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AttachmentItem, ChecklistItem, Comment, TaskItem, TaskStatus } from '../types.js';
|
|
2
|
+
import type { TaskItem as CoreTaskItem } from '../core/Taskforce.js';
|
|
3
|
+
export interface TaskSyncPayload {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string | null;
|
|
7
|
+
status: TaskStatus;
|
|
8
|
+
priority?: number;
|
|
9
|
+
complexity?: number | null;
|
|
10
|
+
type: string;
|
|
11
|
+
category: string;
|
|
12
|
+
approach?: string;
|
|
13
|
+
canceledReason?: string | null;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
updatedAt?: string;
|
|
16
|
+
completedAt?: string | null;
|
|
17
|
+
isArchived?: boolean;
|
|
18
|
+
taxonomies?: Record<string, string | string[]>;
|
|
19
|
+
createdBy?: string;
|
|
20
|
+
assignee?: string;
|
|
21
|
+
scheduledDate?: string | null;
|
|
22
|
+
dueDate?: string | null;
|
|
23
|
+
scheduledWeekKey?: string | null;
|
|
24
|
+
orderInDay?: number | null;
|
|
25
|
+
workstreamId?: string | null;
|
|
26
|
+
referenceNumber?: number | null;
|
|
27
|
+
comments?: Comment[];
|
|
28
|
+
attachments?: Array<string | AttachmentItem>;
|
|
29
|
+
checklistItems?: ChecklistItem[];
|
|
30
|
+
}
|
|
31
|
+
export declare function serializeTaskForSync(task: Partial<TaskItem> | null | undefined): TaskSyncPayload;
|
|
32
|
+
export declare function normalizeTaskSyncPayload(raw: unknown): TaskSyncPayload | null;
|
|
33
|
+
export declare function taskSyncPayloadToCoreTask(task: TaskSyncPayload): Partial<CoreTaskItem>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
function toOptionalString(value) {
|
|
2
|
+
if (value === undefined || value === null)
|
|
3
|
+
return undefined;
|
|
4
|
+
const normalized = String(value).trim();
|
|
5
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
6
|
+
}
|
|
7
|
+
function toNullableString(value) {
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (value === null)
|
|
11
|
+
return null;
|
|
12
|
+
const normalized = String(value).trim();
|
|
13
|
+
return normalized.length > 0 ? normalized : null;
|
|
14
|
+
}
|
|
15
|
+
function toOptionalBoolean(value) {
|
|
16
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
17
|
+
}
|
|
18
|
+
function toOptionalInteger(value, options) {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (value === null || value === '')
|
|
22
|
+
return options?.nullable ? null : undefined;
|
|
23
|
+
const normalized = typeof value === 'number' ? value : Number(value);
|
|
24
|
+
if (!Number.isFinite(normalized))
|
|
25
|
+
return options?.nullable ? null : undefined;
|
|
26
|
+
const rounded = Math.floor(normalized);
|
|
27
|
+
if (Number.isFinite(options?.min) && rounded < Number(options?.min)) {
|
|
28
|
+
return options?.nullable ? null : undefined;
|
|
29
|
+
}
|
|
30
|
+
return rounded;
|
|
31
|
+
}
|
|
32
|
+
function cloneTaxonomies(value) {
|
|
33
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
34
|
+
return undefined;
|
|
35
|
+
const entries = Object.entries(value);
|
|
36
|
+
const normalized = entries.reduce((acc, [key, raw]) => {
|
|
37
|
+
if (Array.isArray(raw)) {
|
|
38
|
+
acc[key] = raw.map((entry) => String(entry ?? ''));
|
|
39
|
+
return acc;
|
|
40
|
+
}
|
|
41
|
+
if (raw === undefined || raw === null)
|
|
42
|
+
return acc;
|
|
43
|
+
acc[key] = String(raw);
|
|
44
|
+
return acc;
|
|
45
|
+
}, {});
|
|
46
|
+
return Object.keys(normalized).length > 0 ? normalized : {};
|
|
47
|
+
}
|
|
48
|
+
function cloneComments(value) {
|
|
49
|
+
return Array.isArray(value) ? value : undefined;
|
|
50
|
+
}
|
|
51
|
+
function cloneAttachments(value) {
|
|
52
|
+
return Array.isArray(value) ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
function cloneChecklistItems(value) {
|
|
55
|
+
return Array.isArray(value) ? value : undefined;
|
|
56
|
+
}
|
|
57
|
+
export function serializeTaskForSync(task) {
|
|
58
|
+
const source = task && typeof task === 'object' ? task : {};
|
|
59
|
+
return {
|
|
60
|
+
id: String(source.id || '').trim(),
|
|
61
|
+
title: String(source.title || '').trim(),
|
|
62
|
+
description: source.description === undefined ? undefined : (source.description || null),
|
|
63
|
+
status: (String(source.status || 'task').trim() || 'task'),
|
|
64
|
+
priority: toOptionalInteger(source.priority, { min: 1 }) ?? undefined,
|
|
65
|
+
complexity: toOptionalInteger(source.complexity, { min: 1, nullable: true }),
|
|
66
|
+
type: String(source.type || '').trim(),
|
|
67
|
+
category: String(source.category || '').trim(),
|
|
68
|
+
approach: toOptionalString(source.approach),
|
|
69
|
+
canceledReason: source.canceledReason === undefined ? undefined : (source.canceledReason || null),
|
|
70
|
+
createdAt: String(source.createdAt || '').trim(),
|
|
71
|
+
updatedAt: toOptionalString(source.updatedAt),
|
|
72
|
+
completedAt: source.completedAt === undefined ? undefined : (source.completedAt || null),
|
|
73
|
+
isArchived: toOptionalBoolean(source.isArchived),
|
|
74
|
+
taxonomies: cloneTaxonomies(source.taxonomies),
|
|
75
|
+
createdBy: toOptionalString(source.createdBy),
|
|
76
|
+
assignee: toOptionalString(source.assignee),
|
|
77
|
+
scheduledDate: toNullableString(source.scheduledDate),
|
|
78
|
+
dueDate: toNullableString(source.dueDate),
|
|
79
|
+
scheduledWeekKey: toNullableString(source.scheduledWeekKey),
|
|
80
|
+
orderInDay: toOptionalInteger(source.orderInDay, { min: 0, nullable: true }),
|
|
81
|
+
workstreamId: toNullableString(source.workstreamId),
|
|
82
|
+
referenceNumber: toOptionalInteger(source.referenceNumber, { min: 1, nullable: true }),
|
|
83
|
+
comments: cloneComments(source.comments),
|
|
84
|
+
attachments: cloneAttachments(source.attachments),
|
|
85
|
+
checklistItems: cloneChecklistItems(source.checklistItems),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function normalizeTaskSyncPayload(raw) {
|
|
89
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
90
|
+
return null;
|
|
91
|
+
const source = raw;
|
|
92
|
+
const id = String(source.id || '').trim();
|
|
93
|
+
if (!id)
|
|
94
|
+
return null;
|
|
95
|
+
return serializeTaskForSync(source);
|
|
96
|
+
}
|
|
97
|
+
export function taskSyncPayloadToCoreTask(task) {
|
|
98
|
+
return {
|
|
99
|
+
id: task.id,
|
|
100
|
+
title: task.title,
|
|
101
|
+
description: task.description,
|
|
102
|
+
status: task.status,
|
|
103
|
+
priority: task.priority,
|
|
104
|
+
complexity: task.complexity ?? undefined,
|
|
105
|
+
type: task.type,
|
|
106
|
+
category: task.category,
|
|
107
|
+
approach: task.approach,
|
|
108
|
+
canceledReason: task.canceledReason ?? undefined,
|
|
109
|
+
createdAt: task.createdAt,
|
|
110
|
+
updatedAt: task.updatedAt,
|
|
111
|
+
completedAt: task.completedAt,
|
|
112
|
+
isArchived: task.isArchived,
|
|
113
|
+
taxonomies: task.taxonomies,
|
|
114
|
+
createdBy: task.createdBy,
|
|
115
|
+
assignee: task.assignee,
|
|
116
|
+
scheduledDate: task.scheduledDate,
|
|
117
|
+
dueDate: task.dueDate,
|
|
118
|
+
scheduledWeekKey: task.scheduledWeekKey,
|
|
119
|
+
orderInDay: task.orderInDay ?? undefined,
|
|
120
|
+
workstreamId: task.workstreamId,
|
|
121
|
+
referenceNumber: task.referenceNumber ?? undefined,
|
|
122
|
+
comments: task.comments,
|
|
123
|
+
attachments: task.attachments,
|
|
124
|
+
checklistItems: task.checklistItems
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalizeTaskSyncPayload, serializeTaskForSync, taskSyncPayloadToCoreTask, } from './taskSyncPayload';
|
|
3
|
+
describe('taskSyncPayload', () => {
|
|
4
|
+
it('omits local-only task reference fields from serialized sync payloads', () => {
|
|
5
|
+
const payload = serializeTaskForSync({
|
|
6
|
+
id: 'task-1',
|
|
7
|
+
title: 'Serialized task',
|
|
8
|
+
status: 'task',
|
|
9
|
+
priority: 2,
|
|
10
|
+
category: 'general',
|
|
11
|
+
type: 'task',
|
|
12
|
+
referenceNumber: 7,
|
|
13
|
+
localReferenceNumber: 3,
|
|
14
|
+
referenceLabel: 'LT-3',
|
|
15
|
+
});
|
|
16
|
+
expect(payload).toEqual(expect.objectContaining({
|
|
17
|
+
id: 'task-1',
|
|
18
|
+
referenceNumber: 7,
|
|
19
|
+
}));
|
|
20
|
+
expect(payload).not.toHaveProperty('localReferenceNumber');
|
|
21
|
+
expect(payload).not.toHaveProperty('referenceLabel');
|
|
22
|
+
});
|
|
23
|
+
it('normalizes task sync payload numbers before converting back to core input', () => {
|
|
24
|
+
const payload = normalizeTaskSyncPayload({
|
|
25
|
+
id: 'task-2',
|
|
26
|
+
title: 'Incoming task',
|
|
27
|
+
status: 'review',
|
|
28
|
+
priority: '3',
|
|
29
|
+
complexity: '4',
|
|
30
|
+
category: 'general',
|
|
31
|
+
type: 'task',
|
|
32
|
+
referenceNumber: '9',
|
|
33
|
+
orderInDay: '2',
|
|
34
|
+
localReferenceNumber: 99,
|
|
35
|
+
referenceLabel: 'LT-99',
|
|
36
|
+
});
|
|
37
|
+
expect(payload).toEqual(expect.objectContaining({
|
|
38
|
+
id: 'task-2',
|
|
39
|
+
priority: 3,
|
|
40
|
+
complexity: 4,
|
|
41
|
+
referenceNumber: 9,
|
|
42
|
+
orderInDay: 2,
|
|
43
|
+
}));
|
|
44
|
+
expect(payload).not.toHaveProperty('localReferenceNumber');
|
|
45
|
+
expect(payload).not.toHaveProperty('referenceLabel');
|
|
46
|
+
expect(taskSyncPayloadToCoreTask(payload)).toEqual(expect.objectContaining({
|
|
47
|
+
id: 'task-2',
|
|
48
|
+
priority: 3,
|
|
49
|
+
complexity: 4,
|
|
50
|
+
referenceNumber: 9,
|
|
51
|
+
orderInDay: 2,
|
|
52
|
+
}));
|
|
53
|
+
});
|
|
54
|
+
});
|