@taskforcehq/taskforce 0.3.301 → 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/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 +466 -204
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- 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 +3 -2
- package/dist/hooks/useTaskData.test.js +66 -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 +41 -19
- package/dist/mcp/runtime.test.js +49 -2
- 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-Bdq3T1Ts.js → AgentsModule-JG5jc3he.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-iys3zfca.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.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-b6TLuHwJ.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 +2 -1
- 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-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
package/dist/core/Taskforce.js
CHANGED
|
@@ -31,7 +31,8 @@ import { normalizePriorityDefinitions as normalizePriorityDefinitionsInternal, n
|
|
|
31
31
|
import { DEFAULT_CATEGORY_LABEL, DEFAULT_CATEGORY_VALUE, DEFAULT_TYPE_VALUE, } from '../shared/taxonomyDefaults.js';
|
|
32
32
|
import { cloneDefaultWorkflowDefinitions, DEFAULT_WORKFLOW_VALUE, } from '../shared/workflowDefaults.js';
|
|
33
33
|
import { getBuiltInTaxonomyLibraryPack, getDefaultStarterTaxonomyLibraryPack, getEmergencyStarterTaxonomyLibraryPack } from '../shared/taxonomyLibrary.js';
|
|
34
|
-
import {
|
|
34
|
+
import { parseTaskReference, } from '../utils/taskReferences.js';
|
|
35
|
+
import { allocateNextLocalTaskReferenceNumber as allocateNextLocalTaskReferenceNumberPolicy, deriveTaskReferenceLabel, allocateNextTaskReferenceNumber as allocateNextTaskReferenceNumberPolicy, getLocalTaskReferenceSequenceValue as getLocalTaskReferenceSequenceValuePolicy, getMaxLocalTaskReferenceNumber as getMaxLocalTaskReferenceNumberPolicy, getMaxTaskReferenceNumber as getMaxTaskReferenceNumberPolicy, getTaskByLocalReferenceNumber as getTaskByLocalReferenceNumberPolicy, getTaskByReferenceNumber as getTaskByReferenceNumberPolicy, getTaskReferenceSequenceValue as getTaskReferenceSequenceValuePolicy, persistLocalTaskReferenceSequence as persistLocalTaskReferenceSequencePolicy, persistTaskReferenceSequence as persistTaskReferenceSequencePolicy, relocateTaskReferenceConflict as relocateTaskReferenceConflictPolicy, reserveTaskReferenceNumber as reserveTaskReferenceNumberPolicy, resolveTaskIdentifier as resolveTaskIdentifierPolicy, } from './taskReferencePolicy.js';
|
|
35
36
|
import { formatInitiativeReference, formatWorkstreamReference, } from '../utils/planningReferences.js';
|
|
36
37
|
import { getDocumentReferenceLabel } from '../utils/documentReferences.js';
|
|
37
38
|
import { getImageReferenceLabel } from '../utils/imageReferences.js';
|
|
@@ -1109,7 +1110,7 @@ export class TaskforceCore {
|
|
|
1109
1110
|
: path.resolve(this.projectRoot, storagePath);
|
|
1110
1111
|
this.dbPath = path.resolve(this.basePath, 'taskforce.db');
|
|
1111
1112
|
this.configFile = path.resolve(this.basePath, 'config.json');
|
|
1112
|
-
this.globalSettingsDbPath = path.resolve(os.homedir(), '.
|
|
1113
|
+
this.globalSettingsDbPath = path.resolve(os.homedir(), '.taskforce', 'taskforce-global.db');
|
|
1113
1114
|
this.ensureDirectories();
|
|
1114
1115
|
this.migrateLegacyGlobalSettingsDbIfNeeded();
|
|
1115
1116
|
// Initialize Database Adapter (SQLite default; Postgres scaffolded)
|
|
@@ -1269,7 +1270,6 @@ export class TaskforceCore {
|
|
|
1269
1270
|
if (fs.existsSync(this.globalSettingsDbPath))
|
|
1270
1271
|
return;
|
|
1271
1272
|
const candidates = [
|
|
1272
|
-
path.resolve(os.homedir(), '.taskforce', 'taskforce-global.db'),
|
|
1273
1273
|
path.resolve(this.basePath, '_global', 'taskforce-global.db')
|
|
1274
1274
|
];
|
|
1275
1275
|
for (const legacyPath of candidates) {
|
|
@@ -1297,6 +1297,7 @@ export class TaskforceCore {
|
|
|
1297
1297
|
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
1298
1298
|
workspace_id TEXT NOT NULL DEFAULT 'default',
|
|
1299
1299
|
reference_number INTEGER,
|
|
1300
|
+
local_reference_number INTEGER,
|
|
1300
1301
|
title TEXT NOT NULL,
|
|
1301
1302
|
description TEXT,
|
|
1302
1303
|
status TEXT NOT NULL,
|
|
@@ -1331,6 +1332,13 @@ export class TaskforceCore {
|
|
|
1331
1332
|
PRIMARY KEY (tenant_id, workspace_id)
|
|
1332
1333
|
);
|
|
1333
1334
|
|
|
1335
|
+
CREATE TABLE IF NOT EXISTS workspace_local_task_reference_sequences (
|
|
1336
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
1337
|
+
workspace_id TEXT NOT NULL DEFAULT 'default',
|
|
1338
|
+
next_reference_number INTEGER NOT NULL DEFAULT 0,
|
|
1339
|
+
PRIMARY KEY (tenant_id, workspace_id)
|
|
1340
|
+
);
|
|
1341
|
+
|
|
1334
1342
|
CREATE TABLE IF NOT EXISTS initiatives (
|
|
1335
1343
|
id TEXT PRIMARY KEY,
|
|
1336
1344
|
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
@@ -2788,10 +2796,12 @@ export class TaskforceCore {
|
|
|
2788
2796
|
const dueDate = this.sanitizeDateOnly(row.due_date);
|
|
2789
2797
|
const scheduledWeekKey = row.scheduled_week_key || (scheduledDate ? this.dateOnlyToIsoWeekKey(scheduledDate) : null);
|
|
2790
2798
|
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
2799
|
+
const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
|
|
2791
2800
|
return {
|
|
2792
2801
|
id: taskId,
|
|
2793
2802
|
referenceNumber,
|
|
2794
|
-
|
|
2803
|
+
localReferenceNumber,
|
|
2804
|
+
referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
|
|
2795
2805
|
title: row.title,
|
|
2796
2806
|
description: row.description,
|
|
2797
2807
|
status: this.normalizeStatusValue(row.status),
|
|
@@ -2879,31 +2889,24 @@ export class TaskforceCore {
|
|
|
2879
2889
|
const normalized = Math.floor(numeric);
|
|
2880
2890
|
return normalized > 0 ? normalized : null;
|
|
2881
2891
|
}
|
|
2892
|
+
getTaskReferencePolicyDeps() {
|
|
2893
|
+
return {
|
|
2894
|
+
db: this.db,
|
|
2895
|
+
tenantId: this.tenantId,
|
|
2896
|
+
normalizeWorkspaceId: this.normalizeWorkspaceId.bind(this),
|
|
2897
|
+
sanitizeTaskReferenceNumber: this.sanitizeTaskReferenceNumber.bind(this),
|
|
2898
|
+
rowToTask: this.rowToTask.bind(this),
|
|
2899
|
+
getTask: this.getTask.bind(this),
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2882
2902
|
getTaskByReferenceNumber(referenceNumber, workspaceId) {
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
const row = this.db.prepare(`
|
|
2888
|
-
SELECT * FROM tasks
|
|
2889
|
-
WHERE tenant_id = ? AND workspace_id = ? AND reference_number = ?
|
|
2890
|
-
LIMIT 1
|
|
2891
|
-
`).get(this.tenantId, normalizedWorkspaceId, normalizedReferenceNumber);
|
|
2892
|
-
if (!row)
|
|
2893
|
-
return null;
|
|
2894
|
-
return this.rowToTask(row);
|
|
2903
|
+
return getTaskByReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), referenceNumber, workspaceId);
|
|
2904
|
+
}
|
|
2905
|
+
getTaskByLocalReferenceNumber(referenceNumber, workspaceId) {
|
|
2906
|
+
return getTaskByLocalReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), referenceNumber, workspaceId);
|
|
2895
2907
|
}
|
|
2896
2908
|
resolveTaskIdentifier(value, workspaceId) {
|
|
2897
|
-
|
|
2898
|
-
if (!normalizedValue)
|
|
2899
|
-
return null;
|
|
2900
|
-
const referenceNumber = parseTaskReference(normalizedValue);
|
|
2901
|
-
if (referenceNumber) {
|
|
2902
|
-
const exactReferenceMatch = this.getTaskByReferenceNumber(referenceNumber, workspaceId);
|
|
2903
|
-
if (exactReferenceMatch)
|
|
2904
|
-
return exactReferenceMatch;
|
|
2905
|
-
}
|
|
2906
|
-
return this.getTask(normalizedValue, workspaceId);
|
|
2909
|
+
return resolveTaskIdentifierPolicy(this.getTaskReferencePolicyDeps(), value, workspaceId);
|
|
2907
2910
|
}
|
|
2908
2911
|
getInitiative(id, workspaceId) {
|
|
2909
2912
|
const normalizedId = String(id || '').trim();
|
|
@@ -3234,42 +3237,35 @@ export class TaskforceCore {
|
|
|
3234
3237
|
});
|
|
3235
3238
|
return this.listChecklistItems(normalizedTaskId, normalizedWorkspaceId);
|
|
3236
3239
|
}
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
RETURNING next_reference_number
|
|
3267
|
-
`).get(this.tenantId, normalizedWorkspaceId);
|
|
3268
|
-
const referenceNumber = this.sanitizeTaskReferenceNumber(row?.next_reference_number);
|
|
3269
|
-
if (!referenceNumber) {
|
|
3270
|
-
throw new TaskforceRuleError('Failed to allocate task reference number', 500, 'TASK_REFERENCE_ALLOC_FAILED');
|
|
3271
|
-
}
|
|
3272
|
-
return referenceNumber;
|
|
3240
|
+
getTaskReferenceSequenceValue(workspaceId) {
|
|
3241
|
+
return getTaskReferenceSequenceValuePolicy(this.getTaskReferencePolicyDeps(), workspaceId);
|
|
3242
|
+
}
|
|
3243
|
+
getLocalTaskReferenceSequenceValue(workspaceId) {
|
|
3244
|
+
return getLocalTaskReferenceSequenceValuePolicy(this.getTaskReferencePolicyDeps(), workspaceId);
|
|
3245
|
+
}
|
|
3246
|
+
getMaxTaskReferenceNumber(workspaceId, excludeTaskId) {
|
|
3247
|
+
return getMaxTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
|
|
3248
|
+
}
|
|
3249
|
+
getMaxLocalTaskReferenceNumber(workspaceId, excludeTaskId) {
|
|
3250
|
+
return getMaxLocalTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
|
|
3251
|
+
}
|
|
3252
|
+
persistTaskReferenceSequence(workspaceId, referenceNumber) {
|
|
3253
|
+
persistTaskReferenceSequencePolicy(this.getTaskReferencePolicyDeps(), workspaceId, referenceNumber);
|
|
3254
|
+
}
|
|
3255
|
+
persistLocalTaskReferenceSequence(workspaceId, referenceNumber) {
|
|
3256
|
+
persistLocalTaskReferenceSequencePolicy(this.getTaskReferencePolicyDeps(), workspaceId, referenceNumber);
|
|
3257
|
+
}
|
|
3258
|
+
allocateNextTaskReferenceNumber(workspaceId, excludeTaskId) {
|
|
3259
|
+
return allocateNextTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
|
|
3260
|
+
}
|
|
3261
|
+
allocateNextLocalTaskReferenceNumber(workspaceId, excludeTaskId) {
|
|
3262
|
+
return allocateNextLocalTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, excludeTaskId);
|
|
3263
|
+
}
|
|
3264
|
+
relocateTaskReferenceConflict(workspaceId, referenceNumber, claimantTaskId, updatedAt) {
|
|
3265
|
+
return relocateTaskReferenceConflictPolicy(this.getTaskReferencePolicyDeps(), workspaceId, referenceNumber, claimantTaskId, updatedAt);
|
|
3266
|
+
}
|
|
3267
|
+
reserveTaskReferenceNumber(workspaceId, preferredReferenceNumber, currentTaskId) {
|
|
3268
|
+
return reserveTaskReferenceNumberPolicy(this.getTaskReferencePolicyDeps(), workspaceId, preferredReferenceNumber, currentTaskId);
|
|
3273
3269
|
}
|
|
3274
3270
|
reserveGenericReferenceNumber(params) {
|
|
3275
3271
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(params.workspaceId);
|
|
@@ -4060,7 +4056,7 @@ export class TaskforceCore {
|
|
|
4060
4056
|
if (!normalizedUserId || normalizedUserId === 'anonymous')
|
|
4061
4057
|
return null;
|
|
4062
4058
|
const row = this.db.prepare(`
|
|
4063
|
-
SELECT id, email, display_name, system_role, COALESCE(beta_access, 1) AS beta_access
|
|
4059
|
+
SELECT id, email, display_name, avatar_url, system_role, COALESCE(beta_access, 1) AS beta_access
|
|
4064
4060
|
FROM users
|
|
4065
4061
|
WHERE tenant_id = ? AND id = ?
|
|
4066
4062
|
LIMIT 1
|
|
@@ -4071,13 +4067,17 @@ export class TaskforceCore {
|
|
|
4071
4067
|
userId: row.id,
|
|
4072
4068
|
email: row.email,
|
|
4073
4069
|
displayName: row.display_name || null,
|
|
4070
|
+
avatarUrl: row.avatar_url || null,
|
|
4074
4071
|
systemRole: this.normalizeSystemRole(row.system_role, 'user'),
|
|
4075
4072
|
betaAccess: row.beta_access === undefined || row.beta_access === null ? true : Boolean(row.beta_access)
|
|
4076
4073
|
};
|
|
4077
4074
|
}
|
|
4078
|
-
|
|
4075
|
+
updateUserProfile(userId, input) {
|
|
4079
4076
|
const normalizedUserId = String(userId || '').trim();
|
|
4080
|
-
const trimmedDisplayName = String(displayName || '').trim();
|
|
4077
|
+
const trimmedDisplayName = String(input.displayName || '').trim();
|
|
4078
|
+
const normalizedAvatarUrl = input.avatarUrl === undefined
|
|
4079
|
+
? undefined
|
|
4080
|
+
: (typeof input.avatarUrl === 'string' && input.avatarUrl.trim().length > 0 ? input.avatarUrl.trim() : null);
|
|
4081
4081
|
if (!normalizedUserId)
|
|
4082
4082
|
return null;
|
|
4083
4083
|
if (!trimmedDisplayName) {
|
|
@@ -4086,15 +4086,145 @@ export class TaskforceCore {
|
|
|
4086
4086
|
if (trimmedDisplayName.length > 120) {
|
|
4087
4087
|
throw new TaskforceRuleError('Display name must be 120 characters or fewer', 400, 'DISPLAY_NAME_TOO_LONG');
|
|
4088
4088
|
}
|
|
4089
|
-
const
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4089
|
+
const now = new Date().toISOString();
|
|
4090
|
+
const result = normalizedAvatarUrl === undefined
|
|
4091
|
+
? this.db.prepare(`
|
|
4092
|
+
UPDATE users
|
|
4093
|
+
SET display_name = ?, updated_at = ?
|
|
4094
|
+
WHERE tenant_id = ? AND id = ?
|
|
4095
|
+
`).run(trimmedDisplayName, now, this.tenantId, normalizedUserId)
|
|
4096
|
+
: this.db.prepare(`
|
|
4097
|
+
UPDATE users
|
|
4098
|
+
SET display_name = ?, avatar_url = ?, updated_at = ?
|
|
4099
|
+
WHERE tenant_id = ? AND id = ?
|
|
4100
|
+
`).run(trimmedDisplayName, normalizedAvatarUrl, now, this.tenantId, normalizedUserId);
|
|
4094
4101
|
if (Number(result?.changes || 0) <= 0)
|
|
4095
4102
|
return null;
|
|
4096
4103
|
return this.resolveUserIdentity(normalizedUserId);
|
|
4097
4104
|
}
|
|
4105
|
+
updateUserDisplayName(userId, displayName) {
|
|
4106
|
+
return this.updateUserProfile(userId, { displayName });
|
|
4107
|
+
}
|
|
4108
|
+
createUserAvatarUploadDraft(input) {
|
|
4109
|
+
const draftId = String(input.draftId || '').trim();
|
|
4110
|
+
const userId = String(input.userId || '').trim();
|
|
4111
|
+
const storageKey = String(input.storageKey || '').trim();
|
|
4112
|
+
const mimeType = String(input.mimeType || 'application/octet-stream').trim() || 'application/octet-stream';
|
|
4113
|
+
const originalFilename = String(input.originalFilename || '').trim() || 'avatar';
|
|
4114
|
+
const sizeBytes = Number.isFinite(Number(input.sizeBytes)) ? Math.max(0, Math.floor(Number(input.sizeBytes))) : 0;
|
|
4115
|
+
const storageProvider = input.storageProvider === 'r2' || input.storageProvider === 'external' ? input.storageProvider : 'local';
|
|
4116
|
+
if (!draftId)
|
|
4117
|
+
throw new TaskforceRuleError('draftId is required', 400, 'AVATAR_DRAFT_ID_REQUIRED');
|
|
4118
|
+
if (!userId)
|
|
4119
|
+
throw new TaskforceRuleError('userId is required', 400, 'USER_ID_REQUIRED');
|
|
4120
|
+
if (!storageKey)
|
|
4121
|
+
throw new TaskforceRuleError('storageKey is required', 400, 'AVATAR_STORAGE_KEY_REQUIRED');
|
|
4122
|
+
const now = new Date().toISOString();
|
|
4123
|
+
this.db.prepare(`
|
|
4124
|
+
INSERT INTO user_avatar_upload_drafts (
|
|
4125
|
+
draft_id, tenant_id, user_id, storage_key, mime_type, original_filename, size_bytes, storage_provider,
|
|
4126
|
+
finalized_at, consumed_at, discarded_at, created_at, updated_at
|
|
4127
|
+
)
|
|
4128
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?)
|
|
4129
|
+
`).run(draftId, this.tenantId, userId, storageKey, mimeType, originalFilename, sizeBytes, storageProvider, now, now);
|
|
4130
|
+
return this.getUserAvatarUploadDraft(userId, draftId);
|
|
4131
|
+
}
|
|
4132
|
+
getUserAvatarUploadDraft(userId, draftId) {
|
|
4133
|
+
const normalizedUserId = String(userId || '').trim();
|
|
4134
|
+
const normalizedDraftId = String(draftId || '').trim();
|
|
4135
|
+
if (!normalizedUserId || !normalizedDraftId)
|
|
4136
|
+
return null;
|
|
4137
|
+
const row = this.db.prepare(`
|
|
4138
|
+
SELECT draft_id, user_id, storage_key, mime_type, original_filename, size_bytes, storage_provider,
|
|
4139
|
+
finalized_at, consumed_at, discarded_at, created_at, updated_at
|
|
4140
|
+
FROM user_avatar_upload_drafts
|
|
4141
|
+
WHERE tenant_id = ? AND user_id = ? AND draft_id = ?
|
|
4142
|
+
LIMIT 1
|
|
4143
|
+
`).get(this.tenantId, normalizedUserId, normalizedDraftId);
|
|
4144
|
+
if (!row?.draft_id)
|
|
4145
|
+
return null;
|
|
4146
|
+
return {
|
|
4147
|
+
draftId: String(row.draft_id),
|
|
4148
|
+
userId: String(row.user_id || normalizedUserId),
|
|
4149
|
+
storageKey: String(row.storage_key || ''),
|
|
4150
|
+
mimeType: String(row.mime_type || 'application/octet-stream'),
|
|
4151
|
+
originalFilename: String(row.original_filename || 'avatar'),
|
|
4152
|
+
sizeBytes: Number(row.size_bytes || 0),
|
|
4153
|
+
storageProvider: String(row.storage_provider || 'local') === 'r2'
|
|
4154
|
+
? 'r2'
|
|
4155
|
+
: String(row.storage_provider || 'local') === 'external'
|
|
4156
|
+
? 'external'
|
|
4157
|
+
: 'local',
|
|
4158
|
+
finalizedAt: row.finalized_at ? String(row.finalized_at) : null,
|
|
4159
|
+
consumedAt: row.consumed_at ? String(row.consumed_at) : null,
|
|
4160
|
+
discardedAt: row.discarded_at ? String(row.discarded_at) : null,
|
|
4161
|
+
createdAt: String(row.created_at || ''),
|
|
4162
|
+
updatedAt: String(row.updated_at || '')
|
|
4163
|
+
};
|
|
4164
|
+
}
|
|
4165
|
+
markUserAvatarUploadDraftFinalized(userId, draftId) {
|
|
4166
|
+
const existing = this.getUserAvatarUploadDraft(userId, draftId);
|
|
4167
|
+
if (!existing || existing.discardedAt)
|
|
4168
|
+
return null;
|
|
4169
|
+
const now = new Date().toISOString();
|
|
4170
|
+
this.db.prepare(`
|
|
4171
|
+
UPDATE user_avatar_upload_drafts
|
|
4172
|
+
SET finalized_at = COALESCE(finalized_at, ?), updated_at = ?
|
|
4173
|
+
WHERE tenant_id = ? AND user_id = ? AND draft_id = ? AND discarded_at IS NULL
|
|
4174
|
+
`).run(now, now, this.tenantId, existing.userId, existing.draftId);
|
|
4175
|
+
return this.getUserAvatarUploadDraft(existing.userId, existing.draftId);
|
|
4176
|
+
}
|
|
4177
|
+
markUserAvatarUploadDraftConsumed(userId, draftId) {
|
|
4178
|
+
const existing = this.getUserAvatarUploadDraft(userId, draftId);
|
|
4179
|
+
if (!existing || existing.discardedAt)
|
|
4180
|
+
return null;
|
|
4181
|
+
const now = new Date().toISOString();
|
|
4182
|
+
this.db.prepare(`
|
|
4183
|
+
UPDATE user_avatar_upload_drafts
|
|
4184
|
+
SET consumed_at = COALESCE(consumed_at, ?), updated_at = ?
|
|
4185
|
+
WHERE tenant_id = ? AND user_id = ? AND draft_id = ? AND discarded_at IS NULL
|
|
4186
|
+
`).run(now, now, this.tenantId, existing.userId, existing.draftId);
|
|
4187
|
+
return this.getUserAvatarUploadDraft(existing.userId, existing.draftId);
|
|
4188
|
+
}
|
|
4189
|
+
discardUserAvatarUploadDraft(userId, draftId) {
|
|
4190
|
+
const existing = this.getUserAvatarUploadDraft(userId, draftId);
|
|
4191
|
+
if (!existing || existing.discardedAt)
|
|
4192
|
+
return existing;
|
|
4193
|
+
const now = new Date().toISOString();
|
|
4194
|
+
this.db.prepare(`
|
|
4195
|
+
UPDATE user_avatar_upload_drafts
|
|
4196
|
+
SET discarded_at = ?, updated_at = ?
|
|
4197
|
+
WHERE tenant_id = ? AND user_id = ? AND draft_id = ? AND discarded_at IS NULL
|
|
4198
|
+
`).run(now, now, this.tenantId, existing.userId, existing.draftId);
|
|
4199
|
+
return this.getUserAvatarUploadDraft(existing.userId, existing.draftId);
|
|
4200
|
+
}
|
|
4201
|
+
cleanupStaleUserAvatarUploadDrafts(userId, olderThanIso) {
|
|
4202
|
+
const normalizedUserId = String(userId || '').trim();
|
|
4203
|
+
const cutoff = String(olderThanIso || '').trim();
|
|
4204
|
+
if (!normalizedUserId || !cutoff)
|
|
4205
|
+
return [];
|
|
4206
|
+
const rows = this.db.prepare(`
|
|
4207
|
+
SELECT draft_id
|
|
4208
|
+
FROM user_avatar_upload_drafts
|
|
4209
|
+
WHERE tenant_id = ?
|
|
4210
|
+
AND user_id = ?
|
|
4211
|
+
AND discarded_at IS NULL
|
|
4212
|
+
AND consumed_at IS NULL
|
|
4213
|
+
AND created_at < ?
|
|
4214
|
+
`).all(this.tenantId, normalizedUserId, cutoff);
|
|
4215
|
+
const staleDrafts = rows
|
|
4216
|
+
.map((row) => this.getUserAvatarUploadDraft(normalizedUserId, String(row?.draft_id || '')))
|
|
4217
|
+
.filter((row) => Boolean(row && !row.discardedAt));
|
|
4218
|
+
if (staleDrafts.length === 0)
|
|
4219
|
+
return [];
|
|
4220
|
+
const now = new Date().toISOString();
|
|
4221
|
+
this.db.prepare(`
|
|
4222
|
+
UPDATE user_avatar_upload_drafts
|
|
4223
|
+
SET discarded_at = ?, updated_at = ?
|
|
4224
|
+
WHERE tenant_id = ? AND user_id = ? AND discarded_at IS NULL AND consumed_at IS NULL AND created_at < ?
|
|
4225
|
+
`).run(now, now, this.tenantId, normalizedUserId, cutoff);
|
|
4226
|
+
return staleDrafts;
|
|
4227
|
+
}
|
|
4098
4228
|
getSystemRole(userId) {
|
|
4099
4229
|
const normalizedUserId = String(userId || '').trim();
|
|
4100
4230
|
if (!normalizedUserId || normalizedUserId === 'anonymous')
|
|
@@ -5770,6 +5900,9 @@ export class TaskforceCore {
|
|
|
5770
5900
|
this.assertRequiredTaskTaxonomies(normalizedInput.taxonomies, normalizedWorkspaceId);
|
|
5771
5901
|
const incomingId = String(normalizedInput.id || '').trim();
|
|
5772
5902
|
const incomingReferenceNumber = this.sanitizeTaskReferenceNumber(normalizedInput.referenceNumber);
|
|
5903
|
+
const useProvisionalLocalReference = options?.provisionalLocalReference === true
|
|
5904
|
+
&& options?.allowClientProvidedId !== true
|
|
5905
|
+
&& !incomingReferenceNumber;
|
|
5773
5906
|
if (incomingId && !allowClientProvidedId) {
|
|
5774
5907
|
throw new TaskforceRuleError('Client-supplied task id is not allowed for this operation.', 400, 'TASK_ID_NOT_ALLOWED');
|
|
5775
5908
|
}
|
|
@@ -5777,7 +5910,15 @@ export class TaskforceCore {
|
|
|
5777
5910
|
throw new TaskforceRuleError('Client-supplied task reference number is not allowed for this operation.', 400, 'TASK_REFERENCE_NOT_ALLOWED');
|
|
5778
5911
|
}
|
|
5779
5912
|
const id = incomingId || this.generateId();
|
|
5780
|
-
|
|
5913
|
+
if (incomingReferenceNumber && options?.preferIncomingReferenceNumberOnConflict === true) {
|
|
5914
|
+
this.relocateTaskReferenceConflict(normalizedWorkspaceId, incomingReferenceNumber, id, options?.syncUpdatedAt);
|
|
5915
|
+
}
|
|
5916
|
+
const referenceNumber = useProvisionalLocalReference
|
|
5917
|
+
? null
|
|
5918
|
+
: this.reserveTaskReferenceNumber(normalizedWorkspaceId, incomingReferenceNumber, id);
|
|
5919
|
+
const localReferenceNumber = useProvisionalLocalReference
|
|
5920
|
+
? this.allocateNextLocalTaskReferenceNumber(normalizedWorkspaceId, id)
|
|
5921
|
+
: null;
|
|
5781
5922
|
const normalizedCategory = normalizedInput.category || TaskforceCore.DEFAULT_CATEGORY_VALUE;
|
|
5782
5923
|
const normalizedType = normalizedInput.type || this.resolveDefaultTypeValue(normalizedWorkspaceId);
|
|
5783
5924
|
const createdByInput = String(normalizedInput.createdBy || '').trim() || undefined;
|
|
@@ -5816,7 +5957,8 @@ export class TaskforceCore {
|
|
|
5816
5957
|
const task = {
|
|
5817
5958
|
id,
|
|
5818
5959
|
referenceNumber,
|
|
5819
|
-
|
|
5960
|
+
localReferenceNumber,
|
|
5961
|
+
referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
|
|
5820
5962
|
title: normalizedInput.title || 'Untitled Task',
|
|
5821
5963
|
description: normalizedDescription || null,
|
|
5822
5964
|
category: normalizedCategory,
|
|
@@ -5863,6 +6005,7 @@ export class TaskforceCore {
|
|
|
5863
6005
|
id, tenant_id, title, description, status, priority, complexity, type, category,
|
|
5864
6006
|
workspace_id,
|
|
5865
6007
|
reference_number,
|
|
6008
|
+
local_reference_number,
|
|
5866
6009
|
approach, created_at, updated_at, is_archived, taxonomies, created_by, assignee,
|
|
5867
6010
|
scheduled_date, due_date, scheduled_week_key, order_in_day,
|
|
5868
6011
|
workstream_id
|
|
@@ -5870,6 +6013,7 @@ export class TaskforceCore {
|
|
|
5870
6013
|
@id, @tenantId, @title, @description, @status, @priority, @complexity, @type, @category,
|
|
5871
6014
|
@workspaceId,
|
|
5872
6015
|
@referenceNumber,
|
|
6016
|
+
@localReferenceNumber,
|
|
5873
6017
|
@approach, @createdAt, @updatedAt, 0, @taxonomies, @createdBy, @assignee,
|
|
5874
6018
|
@scheduledDate, @dueDate, @scheduledWeekKey, @orderInDay,
|
|
5875
6019
|
@workstreamId
|
|
@@ -5946,7 +6090,8 @@ export class TaskforceCore {
|
|
|
5946
6090
|
created.push(this.createTask(batch[i], {
|
|
5947
6091
|
persistBackup: false,
|
|
5948
6092
|
workspaceId: options?.workspaceId,
|
|
5949
|
-
allowClientProvidedId: options?.allowClientProvidedId
|
|
6093
|
+
allowClientProvidedId: options?.allowClientProvidedId,
|
|
6094
|
+
provisionalLocalReference: options?.provisionalLocalReference
|
|
5950
6095
|
}));
|
|
5951
6096
|
}
|
|
5952
6097
|
if (validateOnly) {
|
|
@@ -5973,7 +6118,8 @@ export class TaskforceCore {
|
|
|
5973
6118
|
this.createTask(draft, {
|
|
5974
6119
|
persistBackup: false,
|
|
5975
6120
|
workspaceId: options?.workspaceId,
|
|
5976
|
-
allowClientProvidedId: options?.allowClientProvidedId
|
|
6121
|
+
allowClientProvidedId: options?.allowClientProvidedId,
|
|
6122
|
+
provisionalLocalReference: options?.provisionalLocalReference
|
|
5977
6123
|
});
|
|
5978
6124
|
throw validateOnlyRollbackError;
|
|
5979
6125
|
})(input);
|
|
@@ -5982,7 +6128,8 @@ export class TaskforceCore {
|
|
|
5982
6128
|
created.push(this.createTask(input, {
|
|
5983
6129
|
persistBackup: false,
|
|
5984
6130
|
workspaceId: options?.workspaceId,
|
|
5985
|
-
allowClientProvidedId: options?.allowClientProvidedId
|
|
6131
|
+
allowClientProvidedId: options?.allowClientProvidedId,
|
|
6132
|
+
provisionalLocalReference: options?.provisionalLocalReference
|
|
5986
6133
|
}));
|
|
5987
6134
|
}
|
|
5988
6135
|
}
|
|
@@ -6075,7 +6222,19 @@ export class TaskforceCore {
|
|
|
6075
6222
|
}
|
|
6076
6223
|
}
|
|
6077
6224
|
if (Object.prototype.hasOwnProperty.call(patch, 'referenceNumber')) {
|
|
6078
|
-
|
|
6225
|
+
const requestedReferenceNumber = this.sanitizeTaskReferenceNumber(patch.referenceNumber);
|
|
6226
|
+
if (requestedReferenceNumber && options?.preferIncomingReferenceNumberOnConflict === true) {
|
|
6227
|
+
this.relocateTaskReferenceConflict(normalizedWorkspaceId, requestedReferenceNumber, taskId, options?.syncUpdatedAt);
|
|
6228
|
+
}
|
|
6229
|
+
patch.referenceNumber = requestedReferenceNumber
|
|
6230
|
+
? this.reserveTaskReferenceNumber(normalizedWorkspaceId, requestedReferenceNumber, taskId)
|
|
6231
|
+
: requestedReferenceNumber;
|
|
6232
|
+
if (requestedReferenceNumber) {
|
|
6233
|
+
patch.localReferenceNumber = null;
|
|
6234
|
+
}
|
|
6235
|
+
}
|
|
6236
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'localReferenceNumber')) {
|
|
6237
|
+
patch.localReferenceNumber = this.sanitizeTaskReferenceNumber(patch.localReferenceNumber);
|
|
6079
6238
|
}
|
|
6080
6239
|
if (patch.status !== undefined) {
|
|
6081
6240
|
patch.status = this.normalizeStatusValue(patch.status);
|
|
@@ -6093,7 +6252,10 @@ export class TaskforceCore {
|
|
|
6093
6252
|
const updatedTask = {
|
|
6094
6253
|
...existing,
|
|
6095
6254
|
...patch,
|
|
6096
|
-
referenceLabel:
|
|
6255
|
+
referenceLabel: deriveTaskReferenceLabel({
|
|
6256
|
+
referenceNumber: patch.referenceNumber ?? existing.referenceNumber,
|
|
6257
|
+
localReferenceNumber: patch.localReferenceNumber ?? existing.localReferenceNumber
|
|
6258
|
+
}),
|
|
6097
6259
|
updatedAt: options?.syncUpdatedAt || patch.updatedAt || new Date().toISOString()
|
|
6098
6260
|
};
|
|
6099
6261
|
const fields = [];
|
|
@@ -6101,13 +6263,14 @@ export class TaskforceCore {
|
|
|
6101
6263
|
const mappableFields = [
|
|
6102
6264
|
'title', 'description', 'status', 'completedAt', 'priority', 'complexity', 'type', 'category',
|
|
6103
6265
|
'approach', 'canceledReason', 'createdBy', 'assignee', 'scheduledDate', 'dueDate',
|
|
6104
|
-
'scheduledWeekKey', 'orderInDay', 'workstreamId', 'referenceNumber'
|
|
6266
|
+
'scheduledWeekKey', 'orderInDay', 'workstreamId', 'referenceNumber', 'localReferenceNumber'
|
|
6105
6267
|
];
|
|
6106
6268
|
const propertyToColumn = {
|
|
6107
6269
|
canceledReason: 'canceled_reason',
|
|
6108
6270
|
completedAt: 'completed_at',
|
|
6109
6271
|
createdBy: 'created_by',
|
|
6110
6272
|
referenceNumber: 'reference_number',
|
|
6273
|
+
localReferenceNumber: 'local_reference_number',
|
|
6111
6274
|
scheduledDate: 'scheduled_date',
|
|
6112
6275
|
dueDate: 'due_date',
|
|
6113
6276
|
scheduledWeekKey: 'scheduled_week_key',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { TaskforceCore } from './Taskforce';
|
|
6
|
+
function makeProjectRoot() {
|
|
7
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-user-avatar-'));
|
|
8
|
+
}
|
|
9
|
+
function seedPlans(core) {
|
|
10
|
+
core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
|
|
11
|
+
core.updatePlanVersion({
|
|
12
|
+
planId: 'starter',
|
|
13
|
+
versionNumber: 1,
|
|
14
|
+
seatLimit: 1
|
|
15
|
+
});
|
|
16
|
+
core.updateGlobalSettings({
|
|
17
|
+
auth: {
|
|
18
|
+
signupMonetizationStrategy: 'auto_assign_default',
|
|
19
|
+
defaultSignupPlanVersionId: 'starter-v1'
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function openCore(projectRoot) {
|
|
24
|
+
const core = new TaskforceCore({
|
|
25
|
+
projectRoot,
|
|
26
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
27
|
+
});
|
|
28
|
+
seedPlans(core);
|
|
29
|
+
return core;
|
|
30
|
+
}
|
|
31
|
+
describe('TaskforceCore user profile avatar drafts', () => {
|
|
32
|
+
it('persists avatarUrl on profile updates and exposes it in resolved identity', () => {
|
|
33
|
+
const projectRoot = makeProjectRoot();
|
|
34
|
+
const core = openCore(projectRoot);
|
|
35
|
+
try {
|
|
36
|
+
const created = core.registerPasswordUser({
|
|
37
|
+
email: 'avatar-user@example.com',
|
|
38
|
+
password: 'password123'
|
|
39
|
+
});
|
|
40
|
+
const updated = core.updateUserProfile(created.userId, {
|
|
41
|
+
displayName: 'Avatar User',
|
|
42
|
+
avatarUrl: `/api/taskforce/context/${encodeURIComponent(`users/${created.userId}/profile/avatar/headshot.png`)}`
|
|
43
|
+
});
|
|
44
|
+
expect(updated).toMatchObject({
|
|
45
|
+
userId: created.userId,
|
|
46
|
+
displayName: 'Avatar User',
|
|
47
|
+
avatarUrl: `/api/taskforce/context/${encodeURIComponent(`users/${created.userId}/profile/avatar/headshot.png`)}`
|
|
48
|
+
});
|
|
49
|
+
expect(core.resolveUserIdentity(created.userId)?.avatarUrl).toBe(`/api/taskforce/context/${encodeURIComponent(`users/${created.userId}/profile/avatar/headshot.png`)}`);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
core.close();
|
|
53
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
it('tracks, finalizes, discards, and cleans up stale avatar upload drafts', () => {
|
|
57
|
+
const projectRoot = makeProjectRoot();
|
|
58
|
+
const core = openCore(projectRoot);
|
|
59
|
+
try {
|
|
60
|
+
const created = core.registerPasswordUser({
|
|
61
|
+
email: 'draft-user@example.com',
|
|
62
|
+
password: 'password123'
|
|
63
|
+
});
|
|
64
|
+
const draft = core.createUserAvatarUploadDraft({
|
|
65
|
+
userId: created.userId,
|
|
66
|
+
draftId: 'avatar-draft-1',
|
|
67
|
+
storageKey: `users/${created.userId}/profile/avatar/avatar-draft-1-headshot.png`,
|
|
68
|
+
mimeType: 'image/png',
|
|
69
|
+
originalFilename: 'headshot.png',
|
|
70
|
+
sizeBytes: 1024,
|
|
71
|
+
storageProvider: 'r2'
|
|
72
|
+
});
|
|
73
|
+
expect(draft.finalizedAt).toBeNull();
|
|
74
|
+
expect(core.markUserAvatarUploadDraftFinalized(created.userId, draft.draftId)?.finalizedAt).toEqual(expect.any(String));
|
|
75
|
+
expect(core.markUserAvatarUploadDraftConsumed(created.userId, draft.draftId)?.consumedAt).toEqual(expect.any(String));
|
|
76
|
+
expect(core.discardUserAvatarUploadDraft(created.userId, draft.draftId)?.discardedAt).toEqual(expect.any(String));
|
|
77
|
+
core.createUserAvatarUploadDraft({
|
|
78
|
+
userId: created.userId,
|
|
79
|
+
draftId: 'avatar-draft-stale',
|
|
80
|
+
storageKey: `users/${created.userId}/profile/avatar/avatar-draft-stale-headshot.png`,
|
|
81
|
+
mimeType: 'image/png',
|
|
82
|
+
originalFilename: 'stale.png',
|
|
83
|
+
sizeBytes: 512,
|
|
84
|
+
storageProvider: 'r2'
|
|
85
|
+
});
|
|
86
|
+
const db = core.db;
|
|
87
|
+
db.prepare(`
|
|
88
|
+
UPDATE user_avatar_upload_drafts
|
|
89
|
+
SET created_at = ?, updated_at = ?
|
|
90
|
+
WHERE tenant_id = ? AND user_id = ? AND draft_id = ?
|
|
91
|
+
`).run('2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', core.getTenantId(), created.userId, 'avatar-draft-stale');
|
|
92
|
+
const stale = core.cleanupStaleUserAvatarUploadDrafts(created.userId, '2026-02-01T00:00:00.000Z');
|
|
93
|
+
expect(stale).toHaveLength(1);
|
|
94
|
+
expect(stale[0]?.draftId).toBe('avatar-draft-stale');
|
|
95
|
+
expect(core.getUserAvatarUploadDraft(created.userId, 'avatar-draft-stale')?.discardedAt).toEqual(expect.any(String));
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
core.close();
|
|
99
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../storage/databaseAdapter.js';
|
|
2
|
+
import type { TaskItem } from './Taskforce.js';
|
|
3
|
+
interface TaskReferencePolicyDeps<TTask> {
|
|
4
|
+
db: DatabaseAdapter;
|
|
5
|
+
tenantId: string;
|
|
6
|
+
normalizeWorkspaceId: (workspaceId?: string) => string;
|
|
7
|
+
sanitizeTaskReferenceNumber: (value: unknown) => number | null;
|
|
8
|
+
rowToTask: (row: any) => TTask;
|
|
9
|
+
getTask: (id: string, workspaceId?: string) => TTask | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function getTaskReferenceSequenceValue<TTask>(deps: TaskReferencePolicyDeps<TTask>, workspaceId: string): number;
|
|
12
|
+
export declare function getLocalTaskReferenceSequenceValue<TTask>(deps: TaskReferencePolicyDeps<TTask>, workspaceId: string): number;
|
|
13
|
+
export declare function getMaxTaskReferenceNumber<TTask>(deps: TaskReferencePolicyDeps<TTask>, workspaceId: string, excludeTaskId?: string | null): number;
|
|
14
|
+
export declare function getMaxLocalTaskReferenceNumber<TTask>(deps: TaskReferencePolicyDeps<TTask>, workspaceId: string, excludeTaskId?: string | null): number;
|
|
15
|
+
export declare function persistTaskReferenceSequence<TTask>(deps: TaskReferencePolicyDeps<TTask>, workspaceId: string, referenceNumber: number): void;
|
|
16
|
+
export declare function persistLocalTaskReferenceSequence<TTask>(deps: TaskReferencePolicyDeps<TTask>, workspaceId: string, referenceNumber: number): void;
|
|
17
|
+
export declare function allocateNextTaskReferenceNumber<TTask>(deps: TaskReferencePolicyDeps<TTask>, workspaceId: string, excludeTaskId?: string | null): number;
|
|
18
|
+
export declare function allocateNextLocalTaskReferenceNumber<TTask>(deps: TaskReferencePolicyDeps<TTask>, workspaceId: string, excludeTaskId?: string | null): number;
|
|
19
|
+
export declare function relocateTaskReferenceConflict<TTask>(deps: TaskReferencePolicyDeps<TTask>, workspaceId: string, referenceNumber: number, claimantTaskId?: string | null, updatedAt?: string | null): {
|
|
20
|
+
taskId: string;
|
|
21
|
+
referenceNumber: number;
|
|
22
|
+
} | null;
|
|
23
|
+
export declare function reserveTaskReferenceNumber<TTask>(deps: TaskReferencePolicyDeps<TTask>, workspaceId: string, preferredReferenceNumber?: number | null, currentTaskId?: string | null): number;
|
|
24
|
+
export declare function getTaskByReferenceNumber<TTask>(deps: TaskReferencePolicyDeps<TTask>, referenceNumber: number, workspaceId?: string): TTask | null;
|
|
25
|
+
export declare function getTaskByLocalReferenceNumber<TTask>(deps: TaskReferencePolicyDeps<TTask>, referenceNumber: number, workspaceId?: string): TTask | null;
|
|
26
|
+
export declare function resolveTaskIdentifier(deps: TaskReferencePolicyDeps<TaskItem>, value: string, workspaceId?: string): TaskItem | null;
|
|
27
|
+
export declare function deriveTaskReferenceLabel(task: Pick<TaskItem, 'referenceNumber' | 'localReferenceNumber'>): string;
|
|
28
|
+
export {};
|