chore(team): checkpoint follow-up frontend edits
This commit is contained in:
parent
651cfa1846
commit
88b3ea2358
2 changed files with 47 additions and 9 deletions
|
|
@ -66,19 +66,36 @@ function normalizeActorKey(value) {
|
||||||
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '';
|
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeTimestampForInterval(interval, timestamp) {
|
||||||
|
const startedAtMs = Date.parse(interval.startedAt);
|
||||||
|
const timestampMs = Date.parse(timestamp);
|
||||||
|
if (Number.isFinite(startedAtMs) && Number.isFinite(timestampMs) && timestampMs < startedAtMs) {
|
||||||
|
return interval.startedAt;
|
||||||
|
}
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
function openReviewInterval(task, reviewer, timestamp = new Date().toISOString()) {
|
function openReviewInterval(task, reviewer, timestamp = new Date().toISOString()) {
|
||||||
const reviewerName = typeof reviewer === 'string' && reviewer.trim() ? reviewer.trim() : '';
|
const reviewerName = typeof reviewer === 'string' && reviewer.trim() ? reviewer.trim() : '';
|
||||||
if (!reviewerName) return false;
|
if (!reviewerName) return false;
|
||||||
const reviewerKey = normalizeActorKey(reviewerName);
|
const reviewerKey = normalizeActorKey(reviewerName);
|
||||||
const intervals = Array.isArray(task.reviewIntervals) ? [...task.reviewIntervals] : [];
|
const intervals = Array.isArray(task.reviewIntervals) ? [...task.reviewIntervals] : [];
|
||||||
const hasOpenForReviewer = intervals.some(
|
let changed = false;
|
||||||
(interval) => !interval.completedAt && normalizeActorKey(interval.reviewer) === reviewerKey
|
let hasOpenForReviewer = false;
|
||||||
);
|
const nextIntervals = intervals.map((interval) => {
|
||||||
|
if (interval.completedAt) return interval;
|
||||||
|
if (normalizeActorKey(interval.reviewer) === reviewerKey) {
|
||||||
|
hasOpenForReviewer = true;
|
||||||
|
return interval;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
return { ...interval, completedAt: closeTimestampForInterval(interval, timestamp) };
|
||||||
|
});
|
||||||
if (hasOpenForReviewer) {
|
if (hasOpenForReviewer) {
|
||||||
task.reviewIntervals = intervals;
|
task.reviewIntervals = nextIntervals;
|
||||||
return false;
|
return changed;
|
||||||
}
|
}
|
||||||
task.reviewIntervals = [...intervals, { reviewer: reviewerName, startedAt: timestamp }];
|
task.reviewIntervals = [...nextIntervals, { reviewer: reviewerName, startedAt: timestamp }];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,7 +105,7 @@ function closeReviewIntervals(task, timestamp = new Date().toISOString()) {
|
||||||
task.reviewIntervals = task.reviewIntervals.map((interval) => {
|
task.reviewIntervals = task.reviewIntervals.map((interval) => {
|
||||||
if (interval.completedAt) return interval;
|
if (interval.completedAt) return interval;
|
||||||
changed = true;
|
changed = true;
|
||||||
return { ...interval, completedAt: timestamp };
|
return { ...interval, completedAt: closeTimestampForInterval(interval, timestamp) };
|
||||||
});
|
});
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
@ -315,7 +332,7 @@ function startReview(context, taskId, flags = {}) {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
tasks.updateTask(context, task.id, (t) => {
|
tasks.updateTask(context, task.id, (t) => {
|
||||||
openReviewInterval(t, existingActor);
|
openReviewInterval(t, existingActor, latestReviewEvent.timestamp);
|
||||||
return t;
|
return t;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { getTasksBasePath } from '@main/utils/pathDecoder';
|
import { getTasksBasePath, getTeamsBasePath } from '@main/utils/pathDecoder';
|
||||||
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import { withFileLockSync } from './fileLock';
|
||||||
import { TeamTaskReader } from './TeamTaskReader';
|
import { TeamTaskReader } from './TeamTaskReader';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -20,6 +22,7 @@ type MutableTeamTask = TeamTask & {
|
||||||
};
|
};
|
||||||
|
|
||||||
const CRASH_REPAIR_GRACE_MS = 5_000;
|
const CRASH_REPAIR_GRACE_MS = 5_000;
|
||||||
|
const logger = createLogger('Service:TeamTaskActivityIntervalService');
|
||||||
|
|
||||||
function normalizeMemberName(value: string | null | undefined): string {
|
function normalizeMemberName(value: string | null | undefined): string {
|
||||||
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '';
|
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '';
|
||||||
|
|
@ -142,6 +145,23 @@ export class TeamTaskActivityIntervalService {
|
||||||
private mutateTeamTasks(
|
private mutateTeamTasks(
|
||||||
teamName: string,
|
teamName: string,
|
||||||
mutate: (task: MutableTeamTask) => boolean
|
mutate: (task: MutableTeamTask) => boolean
|
||||||
|
): ActivityIntervalResult {
|
||||||
|
const lockScope = path.join(getTeamsBasePath(), teamName, 'board-state');
|
||||||
|
try {
|
||||||
|
return withFileLockSync(lockScope, () => this.mutateTeamTasksUnlocked(teamName, mutate));
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`[${teamName}] Failed to update task activity intervals: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return { changedTasks: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mutateTeamTasksUnlocked(
|
||||||
|
teamName: string,
|
||||||
|
mutate: (task: MutableTeamTask) => boolean
|
||||||
): ActivityIntervalResult {
|
): ActivityIntervalResult {
|
||||||
const tasksDir = path.join(getTasksBasePath(), teamName);
|
const tasksDir = path.join(getTasksBasePath(), teamName);
|
||||||
let entries: string[];
|
let entries: string[];
|
||||||
|
|
@ -216,6 +236,7 @@ export class TeamTaskActivityIntervalService {
|
||||||
|
|
||||||
const activeReviewer = getActiveReviewActor(task);
|
const activeReviewer = getActiveReviewActor(task);
|
||||||
if (
|
if (
|
||||||
|
task.status === 'completed' &&
|
||||||
activeReviewer &&
|
activeReviewer &&
|
||||||
normalizeMemberName(activeReviewer) === memberKey &&
|
normalizeMemberName(activeReviewer) === memberKey &&
|
||||||
!hasOpenReviewInterval(task, activeReviewer)
|
!hasOpenReviewInterval(task, activeReviewer)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue