- Introduced a comprehensive FAQ section in the README to address common user queries regarding app installation, code handling, agent communication, and project management. - Enhanced cross-platform keyboard shortcut handling in the Electron app for better user experience on macOS and Windows/Linux. - Updated signal handling in the standalone process to ensure proper shutdown behavior across platforms. - Improved WSL user resolution logic to support default user retrieval for better compatibility. - Enhanced notification handling to support cross-platform features and improve user feedback. - Refactored SSH connection management to include additional key file types and improve authentication handling. - Updated team management services to ensure consistent process termination across platforms. - Improved project path handling in team provisioning to accommodate different operating systems. - Enhanced editor components to utilize shared utility functions for path management, improving code maintainability.
151 lines
4.6 KiB
TypeScript
151 lines
4.6 KiB
TypeScript
import { normalizePath } from '@renderer/utils/pathNormalize';
|
|
import { splitPath } from '@shared/utils/platformPath';
|
|
import { differenceInDays, isToday, isYesterday } from 'date-fns';
|
|
|
|
import { DATE_CATEGORY_ORDER } from '../types/tabs';
|
|
|
|
import type { DateCategory } from '../types/tabs';
|
|
import type { GlobalTask } from '@shared/types';
|
|
|
|
export type DateGroupedTasks = Record<DateCategory, GlobalTask[]>;
|
|
|
|
export interface ProjectTaskGroup {
|
|
projectKey: string;
|
|
projectLabel: string;
|
|
tasks: GlobalTask[];
|
|
}
|
|
|
|
/** Returns updatedAt if available, otherwise createdAt. */
|
|
function getEffectiveDate(task: GlobalTask): string | undefined {
|
|
return task.updatedAt ?? task.createdAt;
|
|
}
|
|
|
|
function getEffectiveTs(task: GlobalTask): number {
|
|
const d = getEffectiveDate(task);
|
|
return d ? new Date(d).getTime() : 0;
|
|
}
|
|
|
|
/**
|
|
* Build a map: teamName → max effective timestamp among its tasks.
|
|
* Used to sort team sub-groups by most recent activity (not alphabetically).
|
|
*/
|
|
function buildTeamMaxTs(tasks: GlobalTask[]): Map<string, number> {
|
|
const m = new Map<string, number>();
|
|
for (const t of tasks) {
|
|
const ts = getEffectiveTs(t);
|
|
const cur = m.get(t.teamName) ?? 0;
|
|
if (ts > cur) m.set(t.teamName, ts);
|
|
}
|
|
return m;
|
|
}
|
|
|
|
/**
|
|
* Sort comparator: teams ordered by most recent task (desc),
|
|
* within the same team — by individual task date (desc).
|
|
*/
|
|
function compareByTeamFreshness(
|
|
a: GlobalTask,
|
|
b: GlobalTask,
|
|
teamMaxTs: Map<string, number>
|
|
): number {
|
|
if (a.teamName !== b.teamName) {
|
|
const teamTsA = teamMaxTs.get(a.teamName) ?? 0;
|
|
const teamTsB = teamMaxTs.get(b.teamName) ?? 0;
|
|
return teamTsB - teamTsA;
|
|
}
|
|
return getEffectiveTs(b) - getEffectiveTs(a);
|
|
}
|
|
|
|
function getDateCategory(dateStr: string | undefined): DateCategory {
|
|
if (!dateStr) return 'Older';
|
|
const d = new Date(dateStr);
|
|
if (isNaN(d.getTime())) return 'Older';
|
|
if (isToday(d)) return 'Today';
|
|
if (isYesterday(d)) return 'Yesterday';
|
|
if (differenceInDays(new Date(), d) <= 7) return 'Previous 7 Days';
|
|
return 'Older';
|
|
}
|
|
|
|
export function groupTasksByDate(tasks: GlobalTask[]): DateGroupedTasks {
|
|
const groups: DateGroupedTasks = {
|
|
Today: [],
|
|
Yesterday: [],
|
|
'Previous 7 Days': [],
|
|
Older: [],
|
|
};
|
|
|
|
for (const task of tasks) {
|
|
const cat = getDateCategory(getEffectiveDate(task));
|
|
groups[cat].push(task);
|
|
}
|
|
|
|
for (const cat of DATE_CATEGORY_ORDER) {
|
|
const teamTs = buildTeamMaxTs(groups[cat]);
|
|
groups[cat].sort((a, b) => compareByTeamFreshness(a, b, teamTs));
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
export function getNonEmptyTaskCategories(groups: DateGroupedTasks): DateCategory[] {
|
|
return DATE_CATEGORY_ORDER.filter((cat) => groups[cat].length > 0);
|
|
}
|
|
|
|
const NO_PROJECT_KEY = '__no_project__';
|
|
const NO_PROJECT_LABEL = 'Without project';
|
|
|
|
function trimTrailingPathSep(p: string): string {
|
|
let s = p;
|
|
while (s.length > 0 && (s.endsWith('/') || s.endsWith('\\'))) s = s.slice(0, -1);
|
|
return s;
|
|
}
|
|
|
|
export function projectLabelFromPath(path: string): string {
|
|
const normalized = trimTrailingPathSep(path);
|
|
const segments = splitPath(normalized);
|
|
return segments.length > 0 ? segments[segments.length - 1] : path || NO_PROJECT_LABEL;
|
|
}
|
|
|
|
/**
|
|
* Flat sort: newest (by updatedAt/createdAt) first, no grouping.
|
|
* Within the same team, tasks are ordered by freshness.
|
|
* Teams with more recent activity appear first.
|
|
*/
|
|
export function sortTasksByFreshness(tasks: GlobalTask[]): GlobalTask[] {
|
|
const teamTs = buildTeamMaxTs(tasks);
|
|
return [...tasks].sort((a, b) => compareByTeamFreshness(a, b, teamTs));
|
|
}
|
|
|
|
export function groupTasksByProject(tasks: GlobalTask[]): ProjectTaskGroup[] {
|
|
const byKey = new Map<string, { path: string; tasks: GlobalTask[] }>();
|
|
|
|
for (const task of tasks) {
|
|
const path = task.projectPath?.trim() ?? '';
|
|
const key = path ? normalizePath(trimTrailingPathSep(path)) : NO_PROJECT_KEY;
|
|
let entry = byKey.get(key);
|
|
if (!entry) {
|
|
entry = { path: path || '', tasks: [] };
|
|
byKey.set(key, entry);
|
|
}
|
|
entry.tasks.push(task);
|
|
}
|
|
|
|
for (const entry of byKey.values()) {
|
|
const teamTs = buildTeamMaxTs(entry.tasks);
|
|
entry.tasks.sort((a, b) => compareByTeamFreshness(a, b, teamTs));
|
|
}
|
|
|
|
const groups: ProjectTaskGroup[] = [];
|
|
for (const [key, { path, tasks: list }] of byKey) {
|
|
const projectLabel = key === NO_PROJECT_KEY ? NO_PROJECT_LABEL : projectLabelFromPath(path);
|
|
groups.push({ projectKey: key, projectLabel, tasks: list });
|
|
}
|
|
|
|
groups.sort((a, b) => {
|
|
const tsA = Math.max(...a.tasks.map(getEffectiveTs));
|
|
const tsB = Math.max(...b.tasks.map(getEffectiveTs));
|
|
return tsB - tsA;
|
|
});
|
|
|
|
return groups;
|
|
}
|