mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
refactor: improve error handling and status mark display
- Add ErrorContext type for structured error information with view ID, component name, operation, and file path details - Enhance FluentComponentManager error rendering with collapsible technical details and context display - Add status mark to display name mapping with comprehensive defaults and user configuration support - Improve FileSource status mapping with textual-to-symbol conversion - Clean up code formatting and trailing commas in ConfigurableTaskParser
This commit is contained in:
parent
4a075ae35d
commit
37606e0bcf
7 changed files with 555 additions and 157 deletions
|
|
@ -22,6 +22,7 @@ import { QuickCaptureModal } from "@/components/features/quick-capture/modals/Qu
|
|||
import { t } from "@/translations/helper";
|
||||
import { ViewMode, TopNavigation } from "../components/FluentTopNavigation";
|
||||
import { TaskSelectionManager } from "@/components/features/task/selection/TaskSelectionManager";
|
||||
import { ErrorContext } from "@/types/fluent-types";
|
||||
|
||||
type ManagedViewComponent = {
|
||||
containerEl: HTMLElement;
|
||||
|
|
@ -51,6 +52,18 @@ const VIEW_MODE_CONFIG: Record<string, ViewMode[]> = {
|
|||
kanban: [],
|
||||
};
|
||||
|
||||
// CSS class names for error state display
|
||||
const ERROR_CSS_CLASSES = {
|
||||
STATE: "tg-fluent-error-state",
|
||||
ICON: "tg-fluent-error-icon",
|
||||
TITLE: "tg-fluent-error-title",
|
||||
CONTEXT: "tg-fluent-error-context",
|
||||
CONTEXT_ITEM: "tg-fluent-error-context-item",
|
||||
MESSAGE: "tg-fluent-error-message",
|
||||
DETAILS: "tg-fluent-error-details",
|
||||
STACK: "tg-fluent-error-stack",
|
||||
};
|
||||
|
||||
/**
|
||||
* FluentComponentManager - Manages view component lifecycle
|
||||
*
|
||||
|
|
@ -873,39 +886,206 @@ export class FluentComponentManager extends Component {
|
|||
}
|
||||
|
||||
/**
|
||||
* Render error state
|
||||
* Render error state with detailed context
|
||||
*/
|
||||
renderErrorState(errorMessage: string, onRetry: () => void): void {
|
||||
if (this.contentArea) {
|
||||
this.contentArea
|
||||
.querySelectorAll(".tg-fluent-error-state")
|
||||
.forEach((el) => el.remove());
|
||||
renderErrorState(context: ErrorContext | string, onRetry: () => void): void {
|
||||
if (!this.contentArea) return;
|
||||
|
||||
const errorEl = this.contentArea.createDiv({
|
||||
cls: "tg-fluent-error-state",
|
||||
});
|
||||
const errorIcon = errorEl.createDiv({
|
||||
cls: "tg-fluent-error-icon",
|
||||
});
|
||||
setIcon(errorIcon, "alert-triangle");
|
||||
// Parse context: support both new (ErrorContext) and old (string) formats
|
||||
const errorContext = this.parseErrorContext(context);
|
||||
|
||||
errorEl.createDiv({
|
||||
cls: "tg-fluent-error-title",
|
||||
text: t("Failed to load tasks"),
|
||||
});
|
||||
// Remove existing error overlays
|
||||
this.contentArea
|
||||
.querySelectorAll(`.${ERROR_CSS_CLASSES.STATE}`)
|
||||
.forEach((el) => el.remove());
|
||||
|
||||
errorEl.createDiv({
|
||||
cls: "tg-fluent-error-message",
|
||||
text: errorMessage || t("An unexpected error occurred"),
|
||||
});
|
||||
// Create error container
|
||||
const errorEl = this.contentArea.createDiv({
|
||||
cls: ERROR_CSS_CLASSES.STATE,
|
||||
});
|
||||
|
||||
const retryBtn = errorEl.createEl("button", {
|
||||
cls: "tg-fluent-button tg-fluent-button-primary",
|
||||
text: t("Retry"),
|
||||
});
|
||||
// Build error UI components
|
||||
this.createErrorIcon(errorEl);
|
||||
this.createErrorTitle(errorEl);
|
||||
this.createErrorContext(errorEl, errorContext);
|
||||
this.createErrorMessage(errorEl, errorContext);
|
||||
this.createTechnicalDetails(errorEl, errorContext);
|
||||
this.createRetryButton(errorEl, onRetry);
|
||||
}
|
||||
|
||||
retryBtn.addEventListener("click", onRetry);
|
||||
/**
|
||||
* Parse error context from various input formats
|
||||
*/
|
||||
private parseErrorContext(context: ErrorContext | string): ErrorContext {
|
||||
if (typeof context === "string") {
|
||||
// Backward compatibility: convert string to ErrorContext
|
||||
return {
|
||||
userMessage: context,
|
||||
originalError: new Error(context),
|
||||
};
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create error icon element
|
||||
*/
|
||||
private createErrorIcon(errorEl: HTMLElement): void {
|
||||
const errorIcon = errorEl.createDiv({
|
||||
cls: ERROR_CSS_CLASSES.ICON,
|
||||
});
|
||||
setIcon(errorIcon, "alert-triangle");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create error title element
|
||||
*/
|
||||
private createErrorTitle(errorEl: HTMLElement): void {
|
||||
errorEl.createDiv({
|
||||
cls: ERROR_CSS_CLASSES.TITLE,
|
||||
text: t("Failed to load tasks"),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create error context information (view, component, operation)
|
||||
*/
|
||||
private createErrorContext(
|
||||
errorEl: HTMLElement,
|
||||
errorContext: ErrorContext
|
||||
): void {
|
||||
if (
|
||||
!errorContext.viewId &&
|
||||
!errorContext.componentName &&
|
||||
!errorContext.operation
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contextEl = errorEl.createDiv({
|
||||
cls: ERROR_CSS_CLASSES.CONTEXT,
|
||||
});
|
||||
|
||||
if (errorContext.viewId) {
|
||||
const viewLabel = this.getViewLabel(errorContext.viewId);
|
||||
contextEl.createDiv({
|
||||
cls: ERROR_CSS_CLASSES.CONTEXT_ITEM,
|
||||
text: `${t("View")}: ${viewLabel} (${errorContext.viewId})`,
|
||||
});
|
||||
}
|
||||
|
||||
if (errorContext.componentName) {
|
||||
contextEl.createDiv({
|
||||
cls: ERROR_CSS_CLASSES.CONTEXT_ITEM,
|
||||
text: `${t("Component")}: ${errorContext.componentName}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (errorContext.operation) {
|
||||
contextEl.createDiv({
|
||||
cls: ERROR_CSS_CLASSES.CONTEXT_ITEM,
|
||||
text: `${t("Operation")}: ${errorContext.operation}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create user-friendly error message
|
||||
*/
|
||||
private createErrorMessage(
|
||||
errorEl: HTMLElement,
|
||||
errorContext: ErrorContext
|
||||
): void {
|
||||
const userMessage =
|
||||
errorContext.userMessage ||
|
||||
errorContext.originalError?.message ||
|
||||
t("An unexpected error occurred");
|
||||
|
||||
errorEl.createDiv({
|
||||
cls: ERROR_CSS_CLASSES.MESSAGE,
|
||||
text: userMessage,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create collapsible technical details section
|
||||
*/
|
||||
private createTechnicalDetails(
|
||||
errorEl: HTMLElement,
|
||||
errorContext: ErrorContext
|
||||
): void {
|
||||
if (!errorContext.originalError) return;
|
||||
|
||||
const detailsEl = errorEl.createEl("details", {
|
||||
cls: ERROR_CSS_CLASSES.DETAILS,
|
||||
});
|
||||
|
||||
detailsEl.createEl("summary", {
|
||||
text: t("View technical details"),
|
||||
});
|
||||
|
||||
const stackEl = detailsEl.createEl("pre", {
|
||||
cls: ERROR_CSS_CLASSES.STACK,
|
||||
});
|
||||
|
||||
// Build technical details
|
||||
let technicalInfo = "";
|
||||
if (errorContext.filePath) {
|
||||
technicalInfo += `File: ${errorContext.filePath}\n\n`;
|
||||
}
|
||||
if (errorContext.originalError.stack) {
|
||||
technicalInfo += errorContext.originalError.stack;
|
||||
} else {
|
||||
technicalInfo += errorContext.originalError.message;
|
||||
}
|
||||
|
||||
stackEl.textContent = technicalInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create retry button
|
||||
*/
|
||||
private createRetryButton(
|
||||
errorEl: HTMLElement,
|
||||
onRetry: () => void
|
||||
): void {
|
||||
const retryBtn = errorEl.createEl("button", {
|
||||
cls: "tg-fluent-button tg-fluent-button-primary",
|
||||
text: t("Retry"),
|
||||
});
|
||||
|
||||
retryBtn.addEventListener("click", onRetry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get localized view label
|
||||
*/
|
||||
private getViewLabel(viewId: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
inbox: t("Inbox"),
|
||||
today: t("Today"),
|
||||
upcoming: t("Upcoming"),
|
||||
flagged: t("Flagged"),
|
||||
projects: t("Projects"),
|
||||
tags: t("Tags"),
|
||||
forecast: t("Forecast"),
|
||||
review: t("Review"),
|
||||
habit: t("Habit"),
|
||||
calendar: t("Calendar"),
|
||||
kanban: t("Kanban"),
|
||||
gantt: t("Gantt"),
|
||||
};
|
||||
return labels[viewId] || viewId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current visible component name
|
||||
*/
|
||||
public getCurrentComponentName(): string | undefined {
|
||||
if (!this.currentVisibleComponent) {
|
||||
return undefined;
|
||||
}
|
||||
return this.currentVisibleComponent.constructor?.name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -315,7 +315,8 @@ export class ContentComponent extends Component {
|
|||
} else {
|
||||
this.taskGroups = groupTasksBy(
|
||||
this.filteredTasks,
|
||||
this.groupByDimension
|
||||
this.groupByDimension,
|
||||
this.plugin.settings.taskStatusMarks
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class MarkdownTaskParser {
|
|||
|
||||
constructor(
|
||||
config: TaskParserConfig,
|
||||
timeParsingService?: TimeParsingService,
|
||||
timeParsingService?: TimeParsingService
|
||||
) {
|
||||
this.config = config;
|
||||
// Extract custom date formats if available
|
||||
|
|
@ -46,7 +46,7 @@ export class MarkdownTaskParser {
|
|||
|
||||
// Public alias for extractMetadataAndTags
|
||||
public extractMetadataAndTags(
|
||||
content: string,
|
||||
content: string
|
||||
): [string, Record<string, string>, string[]] {
|
||||
return this.extractMetadataAndTagsInternal(content);
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ export class MarkdownTaskParser {
|
|||
static createWithStatusMapping(
|
||||
config: TaskParserConfig,
|
||||
statusMapping: Record<string, string>,
|
||||
timeParsingService?: TimeParsingService,
|
||||
timeParsingService?: TimeParsingService
|
||||
): MarkdownTaskParser {
|
||||
const newConfig = { ...config, statusMapping };
|
||||
return new MarkdownTaskParser(newConfig, timeParsingService);
|
||||
|
|
@ -71,7 +71,7 @@ export class MarkdownTaskParser {
|
|||
filePath = "",
|
||||
fileMetadata?: Record<string, any>,
|
||||
projectConfigData?: Record<string, any>,
|
||||
tgProject?: TgProject,
|
||||
tgProject?: TgProject
|
||||
): EnhancedTask[] {
|
||||
this.reset();
|
||||
this.fileMetadata = fileMetadata;
|
||||
|
|
@ -90,7 +90,7 @@ export class MarkdownTaskParser {
|
|||
parseIteration++;
|
||||
if (parseIteration > this.config.maxParseIterations) {
|
||||
console.warn(
|
||||
"Warning: Maximum parse iterations reached, stopping to prevent infinite loop",
|
||||
"Warning: Maximum parse iterations reached, stopping to prevent infinite loop"
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
|
@ -142,13 +142,13 @@ export class MarkdownTaskParser {
|
|||
const isSubtask = parentId !== undefined;
|
||||
const inheritedMetadata = this.inheritFileMetadata(
|
||||
metadata,
|
||||
isSubtask,
|
||||
isSubtask
|
||||
);
|
||||
|
||||
// Extract time components from task content using enhanced time parsing
|
||||
const enhancedMetadata = this.extractTimeComponents(
|
||||
taskContent,
|
||||
inheritedMetadata,
|
||||
inheritedMetadata
|
||||
);
|
||||
|
||||
// Process inherited tags and merge with task's own tags
|
||||
|
|
@ -156,7 +156,7 @@ export class MarkdownTaskParser {
|
|||
if (inheritedMetadata.tags) {
|
||||
try {
|
||||
const inheritedTags = JSON.parse(
|
||||
inheritedMetadata.tags,
|
||||
inheritedMetadata.tags
|
||||
);
|
||||
if (Array.isArray(inheritedTags)) {
|
||||
finalTags = this.mergeTags(tags, inheritedTags);
|
||||
|
|
@ -179,8 +179,8 @@ export class MarkdownTaskParser {
|
|||
? this.extractMultilineComment(
|
||||
lines,
|
||||
i + 1,
|
||||
actualSpaces,
|
||||
)
|
||||
actualSpaces
|
||||
)
|
||||
: [undefined, 0];
|
||||
|
||||
i += linesToSkip;
|
||||
|
|
@ -216,23 +216,23 @@ export class MarkdownTaskParser {
|
|||
priority: extractedPriority,
|
||||
startDate: this.extractLegacyDate(
|
||||
enhancedMetadata,
|
||||
"startDate",
|
||||
"startDate"
|
||||
),
|
||||
dueDate: this.extractLegacyDate(
|
||||
enhancedMetadata,
|
||||
"dueDate",
|
||||
"dueDate"
|
||||
),
|
||||
scheduledDate: this.extractLegacyDate(
|
||||
enhancedMetadata,
|
||||
"scheduledDate",
|
||||
"scheduledDate"
|
||||
),
|
||||
completedDate: this.extractLegacyDate(
|
||||
enhancedMetadata,
|
||||
"completedDate",
|
||||
"completedDate"
|
||||
),
|
||||
createdDate: this.extractLegacyDate(
|
||||
enhancedMetadata,
|
||||
"createdDate",
|
||||
"createdDate"
|
||||
),
|
||||
recurrence: enhancedMetadata.recurrence,
|
||||
project: enhancedMetadata.project,
|
||||
|
|
@ -241,7 +241,7 @@ export class MarkdownTaskParser {
|
|||
|
||||
if (parentId && this.tasks.length > 0) {
|
||||
const parentTask = this.tasks.find(
|
||||
(t) => t.id === parentId,
|
||||
(t) => t.id === parentId
|
||||
);
|
||||
if (parentTask) {
|
||||
parentTask.childrenIds.push(taskId);
|
||||
|
|
@ -267,14 +267,14 @@ export class MarkdownTaskParser {
|
|||
filePath: string = "",
|
||||
fileMetadata?: Record<string, any>,
|
||||
projectConfigData?: Record<string, any>,
|
||||
tgProject?: TgProject,
|
||||
tgProject?: TgProject
|
||||
): Task[] {
|
||||
const enhancedTasks = this.parse(
|
||||
input,
|
||||
filePath,
|
||||
fileMetadata,
|
||||
projectConfigData,
|
||||
tgProject,
|
||||
tgProject
|
||||
);
|
||||
return enhancedTasks.map((task) => this.convertToLegacyTask(task));
|
||||
}
|
||||
|
|
@ -299,7 +299,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private extractTaskLine(
|
||||
line: string,
|
||||
line: string
|
||||
): [number, number, string, string] | null {
|
||||
// Preserve trailing spaces to allow parsing of empty-content tasks like "- [ ] "
|
||||
const trimmed = line.trimStart();
|
||||
|
|
@ -361,7 +361,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private extractMetadataAndTagsInternal(
|
||||
content: string,
|
||||
content: string
|
||||
): [string, Record<string, string>, string[]] {
|
||||
const metadata: Record<string, string> = {};
|
||||
const tags: string[] = [];
|
||||
|
|
@ -513,7 +513,7 @@ export class MarkdownTaskParser {
|
|||
*/
|
||||
private extractTimeComponents(
|
||||
taskContent: string,
|
||||
existingMetadata: Record<string, string>,
|
||||
existingMetadata: Record<string, string>
|
||||
): EnhancedStandardTaskMetadata {
|
||||
if (!this.timeParsingService) {
|
||||
// Return existing metadata as EnhancedStandardTaskMetadata without time components
|
||||
|
|
@ -539,7 +539,7 @@ export class MarkdownTaskParser {
|
|||
// Swallow JSON.parse or format errors from time parsing; continue without time components
|
||||
console.warn(
|
||||
"[MarkdownTaskParser] timeParsingService.parseTimeComponents failed, continuing without time components:",
|
||||
innerErr,
|
||||
innerErr
|
||||
);
|
||||
timeComponents = {};
|
||||
errors = [];
|
||||
|
|
@ -550,7 +550,7 @@ export class MarkdownTaskParser {
|
|||
if (warnings.length > 0) {
|
||||
console.warn(
|
||||
`[MarkdownTaskParser] Time parsing warnings for "${taskContent}":`,
|
||||
warnings,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -558,7 +558,7 @@ export class MarkdownTaskParser {
|
|||
if (errors.length > 0) {
|
||||
console.warn(
|
||||
`[MarkdownTaskParser] Time parsing errors for "${taskContent}":`,
|
||||
errors,
|
||||
errors
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -582,7 +582,7 @@ export class MarkdownTaskParser {
|
|||
scheduledDate: existingMetadata.scheduledDate,
|
||||
completedDate: existingMetadata.completedDate,
|
||||
},
|
||||
timeComponents,
|
||||
timeComponents
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -590,7 +590,7 @@ export class MarkdownTaskParser {
|
|||
} catch (error) {
|
||||
console.error(
|
||||
`[MarkdownTaskParser] Failed to extract time components from "${taskContent}":`,
|
||||
error,
|
||||
error
|
||||
);
|
||||
// Return existing metadata without time components on error
|
||||
return {
|
||||
|
|
@ -611,7 +611,7 @@ export class MarkdownTaskParser {
|
|||
scheduledDate?: number | string;
|
||||
completedDate?: number | string;
|
||||
},
|
||||
timeComponents: EnhancedStandardTaskMetadata["timeComponents"],
|
||||
timeComponents: EnhancedStandardTaskMetadata["timeComponents"]
|
||||
): EnhancedStandardTaskMetadata["enhancedDates"] {
|
||||
if (!timeComponents) {
|
||||
return undefined;
|
||||
|
|
@ -622,7 +622,7 @@ export class MarkdownTaskParser {
|
|||
// Helper function to combine date and time component
|
||||
const combineDateTime = (
|
||||
dateValue: number | string | undefined,
|
||||
timeComponent: TimeComponent | undefined,
|
||||
timeComponent: TimeComponent | undefined
|
||||
): Date | undefined => {
|
||||
if (!dateValue || !timeComponent) {
|
||||
return undefined;
|
||||
|
|
@ -652,7 +652,7 @@ export class MarkdownTaskParser {
|
|||
date.getDate(),
|
||||
timeComponent.hour,
|
||||
timeComponent.minute,
|
||||
timeComponent.second || 0,
|
||||
timeComponent.second || 0
|
||||
);
|
||||
|
||||
return combinedDate;
|
||||
|
|
@ -662,7 +662,7 @@ export class MarkdownTaskParser {
|
|||
if (dates.startDate && timeComponents.startTime) {
|
||||
enhancedDates.startDateTime = combineDateTime(
|
||||
dates.startDate,
|
||||
timeComponents.startTime,
|
||||
timeComponents.startTime
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -670,7 +670,7 @@ export class MarkdownTaskParser {
|
|||
if (dates.dueDate && timeComponents.dueTime) {
|
||||
enhancedDates.dueDateTime = combineDateTime(
|
||||
dates.dueDate,
|
||||
timeComponents.dueTime,
|
||||
timeComponents.dueTime
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -678,7 +678,7 @@ export class MarkdownTaskParser {
|
|||
if (dates.scheduledDate && timeComponents.scheduledTime) {
|
||||
enhancedDates.scheduledDateTime = combineDateTime(
|
||||
dates.scheduledDate,
|
||||
timeComponents.scheduledTime,
|
||||
timeComponents.scheduledTime
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -686,7 +686,7 @@ export class MarkdownTaskParser {
|
|||
if (dates.startDate && timeComponents.endTime) {
|
||||
enhancedDates.endDateTime = combineDateTime(
|
||||
dates.startDate,
|
||||
timeComponents.endTime,
|
||||
timeComponents.endTime
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -699,7 +699,7 @@ export class MarkdownTaskParser {
|
|||
) {
|
||||
enhancedDates.dueDateTime = combineDateTime(
|
||||
dates.dueDate,
|
||||
timeComponents.scheduledTime,
|
||||
timeComponents.scheduledTime
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -712,7 +712,7 @@ export class MarkdownTaskParser {
|
|||
) {
|
||||
enhancedDates.scheduledDateTime = combineDateTime(
|
||||
dates.scheduledDate,
|
||||
timeComponents.dueTime,
|
||||
timeComponents.dueTime
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -722,7 +722,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private extractDataviewMetadata(
|
||||
content: string,
|
||||
content: string
|
||||
): [string, string, string] | null {
|
||||
const start = content.indexOf("[");
|
||||
if (start === -1) return null;
|
||||
|
|
@ -763,7 +763,7 @@ export class MarkdownTaskParser {
|
|||
// We need to reverse lookup: find prefix that maps to standard metadata keys
|
||||
const lowerKey = key.toLowerCase();
|
||||
for (const [prefix, metadataType] of Object.entries(
|
||||
this.config.specialTagPrefixes || {},
|
||||
this.config.specialTagPrefixes || {}
|
||||
)) {
|
||||
if (prefix.toLowerCase() === lowerKey) {
|
||||
key = metadataType; // Map to the target metadata field (project, context, area)
|
||||
|
|
@ -784,7 +784,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private extractEmojiMetadata(
|
||||
content: string,
|
||||
content: string
|
||||
): [string, string, string, string] | null {
|
||||
// Find the earliest emoji
|
||||
let earliestEmoji: { pos: number; emoji: string; key: string } | null =
|
||||
|
|
@ -803,7 +803,7 @@ export class MarkdownTaskParser {
|
|||
|
||||
const beforeEmoji = content.substring(0, earliestEmoji.pos);
|
||||
const afterEmoji = content.substring(
|
||||
earliestEmoji.pos + earliestEmoji.emoji.length,
|
||||
earliestEmoji.pos + earliestEmoji.emoji.length
|
||||
);
|
||||
|
||||
// Extract value after emoji
|
||||
|
|
@ -817,7 +817,7 @@ export class MarkdownTaskParser {
|
|||
// Check if we encounter other emojis or special characters
|
||||
if (
|
||||
Object.keys(this.config.emojiMapping).some((e) =>
|
||||
valuePart.substring(i).startsWith(e),
|
||||
valuePart.substring(i).startsWith(e)
|
||||
) ||
|
||||
char === "["
|
||||
) {
|
||||
|
|
@ -1126,7 +1126,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private extractTagsOnly(
|
||||
content: string,
|
||||
content: string
|
||||
): [string, Record<string, string>, string[]] {
|
||||
const metadata: Record<string, string> = {};
|
||||
const tags: string[] = [];
|
||||
|
|
@ -1232,7 +1232,7 @@ export class MarkdownTaskParser {
|
|||
}
|
||||
|
||||
private findParentAndLevel(
|
||||
actualSpaces: number,
|
||||
actualSpaces: number
|
||||
): [string | undefined, number] {
|
||||
if (this.indentStack.length === 0 || actualSpaces === 0) {
|
||||
return [undefined, 0];
|
||||
|
|
@ -1255,7 +1255,7 @@ export class MarkdownTaskParser {
|
|||
private updateIndentStack(
|
||||
taskId: string,
|
||||
indentLevel: number,
|
||||
actualSpaces: number,
|
||||
actualSpaces: number
|
||||
): void {
|
||||
let stackOperations = 0;
|
||||
|
||||
|
|
@ -1263,7 +1263,7 @@ export class MarkdownTaskParser {
|
|||
stackOperations++;
|
||||
if (stackOperations > this.config.maxStackOperations) {
|
||||
console.warn(
|
||||
"Warning: Maximum stack operations reached, clearing stack",
|
||||
"Warning: Maximum stack operations reached, clearing stack"
|
||||
);
|
||||
this.indentStack = [];
|
||||
break;
|
||||
|
|
@ -1280,7 +1280,7 @@ export class MarkdownTaskParser {
|
|||
if (this.indentStack.length >= this.config.maxStackSize) {
|
||||
this.indentStack.splice(
|
||||
0,
|
||||
this.indentStack.length - this.config.maxStackSize + 1,
|
||||
this.indentStack.length - this.config.maxStackSize + 1
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1290,7 +1290,7 @@ export class MarkdownTaskParser {
|
|||
private getStatusFromMapping(rawStatus: string): string | undefined {
|
||||
// Find status name corresponding to raw character
|
||||
for (const [statusName, mappedChar] of Object.entries(
|
||||
this.config.statusMapping,
|
||||
this.config.statusMapping
|
||||
)) {
|
||||
if (mappedChar === rawStatus) {
|
||||
return statusName;
|
||||
|
|
@ -1327,7 +1327,7 @@ export class MarkdownTaskParser {
|
|||
private extractMultilineComment(
|
||||
lines: string[],
|
||||
startIndex: number,
|
||||
actualSpaces: number,
|
||||
actualSpaces: number
|
||||
): [string | undefined, number] {
|
||||
const commentLines: string[] = [];
|
||||
let i = startIndex;
|
||||
|
|
@ -1359,7 +1359,7 @@ export class MarkdownTaskParser {
|
|||
|
||||
// Legacy compatibility methods
|
||||
private extractLegacyPriority(
|
||||
metadata: Record<string, string>,
|
||||
metadata: Record<string, string>
|
||||
): number | undefined {
|
||||
if (!metadata.priority) return undefined;
|
||||
|
||||
|
|
@ -1401,14 +1401,14 @@ export class MarkdownTaskParser {
|
|||
|
||||
private extractLegacyDate(
|
||||
metadata: Record<string, string>,
|
||||
key: string,
|
||||
key: string
|
||||
): number | undefined {
|
||||
const dateStr = metadata[key];
|
||||
if (!dateStr) return undefined;
|
||||
|
||||
// Check cache first to avoid repeated date parsing
|
||||
const cacheKey = `${dateStr}_${(this.customDateFormats || []).join(
|
||||
",",
|
||||
","
|
||||
)}`;
|
||||
const cachedDate = MarkdownTaskParser.dateCache.get(cacheKey);
|
||||
if (cachedDate !== undefined) {
|
||||
|
|
@ -1452,7 +1452,7 @@ export class MarkdownTaskParser {
|
|||
filePath: enhancedTask.filePath,
|
||||
line: enhancedTask.line,
|
||||
completed: enhancedTask.completed,
|
||||
status: enhancedTask.rawStatus,
|
||||
status: enhancedTask.rawStatus || " ", // Default to " " if no raw status
|
||||
originalMarkdown: enhancedTask.originalMarkdown,
|
||||
children: enhancedTask.children || [],
|
||||
metadata: {
|
||||
|
|
@ -1494,8 +1494,8 @@ export class MarkdownTaskParser {
|
|||
heading: Array.isArray(enhancedTask.heading)
|
||||
? enhancedTask.heading
|
||||
: enhancedTask.heading
|
||||
? [enhancedTask.heading]
|
||||
: [],
|
||||
? [enhancedTask.heading]
|
||||
: [],
|
||||
parent: enhancedTask.parentId,
|
||||
tgProject: enhancedTask.tgProject,
|
||||
},
|
||||
|
|
@ -1668,10 +1668,10 @@ export class MarkdownTaskParser {
|
|||
private mergeTags(baseTags: string[], inheritedTags: string[]): string[] {
|
||||
// Normalize all tags before merging
|
||||
const normalizedBaseTags = baseTags.map((tag) =>
|
||||
this.normalizeTag(tag),
|
||||
this.normalizeTag(tag)
|
||||
);
|
||||
const normalizedInheritedTags = inheritedTags.map((tag) =>
|
||||
this.normalizeTag(tag),
|
||||
this.normalizeTag(tag)
|
||||
);
|
||||
|
||||
const merged = [...normalizedBaseTags];
|
||||
|
|
@ -1696,7 +1696,7 @@ export class MarkdownTaskParser {
|
|||
*/
|
||||
private inheritFileMetadata(
|
||||
taskMetadata: Record<string, string>,
|
||||
isSubtask: boolean = false,
|
||||
isSubtask: boolean = false
|
||||
): Record<string, string> {
|
||||
// Helper function to convert priority values to numbers
|
||||
const convertPriorityValue = (value: any): string => {
|
||||
|
|
@ -1827,7 +1827,7 @@ export class MarkdownTaskParser {
|
|||
this.fileMetadata[configuredProjectKey] !== undefined &&
|
||||
this.fileMetadata[configuredProjectKey] !== null &&
|
||||
String(
|
||||
this.fileMetadata[configuredProjectKey],
|
||||
this.fileMetadata[configuredProjectKey]
|
||||
).trim() !== ""
|
||||
) {
|
||||
if (
|
||||
|
|
@ -1836,7 +1836,7 @@ export class MarkdownTaskParser {
|
|||
inherited.project === ""
|
||||
) {
|
||||
inherited.project = String(
|
||||
this.fileMetadata[configuredProjectKey],
|
||||
this.fileMetadata[configuredProjectKey]
|
||||
).trim();
|
||||
}
|
||||
}
|
||||
|
|
@ -1851,7 +1851,7 @@ export class MarkdownTaskParser {
|
|||
|
||||
// Merge extracted metadata from tags
|
||||
for (const [tagKey, tagValue] of Object.entries(
|
||||
tagMetadata,
|
||||
tagMetadata
|
||||
)) {
|
||||
if (
|
||||
!nonInheritableFields.has(tagKey) &&
|
||||
|
|
@ -1880,7 +1880,7 @@ export class MarkdownTaskParser {
|
|||
) {
|
||||
// Normalize tags before storing
|
||||
const normalizedTags = value.map((tag) =>
|
||||
this.normalizeTag(tag),
|
||||
this.normalizeTag(tag)
|
||||
);
|
||||
inherited["tags"] = JSON.stringify(normalizedTags);
|
||||
}
|
||||
|
|
@ -1911,7 +1911,7 @@ export class MarkdownTaskParser {
|
|||
// LEGACY: Inherit from project configuration data if available
|
||||
if (this.projectConfigCache) {
|
||||
for (const [key, value] of Object.entries(
|
||||
this.projectConfigCache,
|
||||
this.projectConfigCache
|
||||
)) {
|
||||
// Only inherit if:
|
||||
// 1. The field is not in the non-inheritable list
|
||||
|
|
@ -1947,7 +1947,7 @@ export class MarkdownTaskParser {
|
|||
export class ConfigurableTaskParser extends MarkdownTaskParser {
|
||||
constructor(
|
||||
config?: Partial<TaskParserConfig>,
|
||||
timeParsingService?: TimeParsingService,
|
||||
timeParsingService?: TimeParsingService
|
||||
) {
|
||||
// Default configuration
|
||||
const defaultConfig: TaskParserConfig = {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,25 @@ import { FileFilterManager } from "@/managers/file-filter-manager";
|
|||
* following the established dataflow patterns.
|
||||
*/
|
||||
export class FileSource {
|
||||
/**
|
||||
* Default status mappings from textual values to task symbols
|
||||
* Provides fallback mappings when user configuration doesn't specify a mapping
|
||||
*/
|
||||
private static readonly DEFAULT_STATUS_MAPPINGS: Record<string, string> = {
|
||||
completed: "x",
|
||||
done: "x",
|
||||
finished: "x",
|
||||
"in-progress": "/",
|
||||
"in progress": "/",
|
||||
doing: "/",
|
||||
planned: "?",
|
||||
todo: "?",
|
||||
cancelled: "-",
|
||||
canceled: "-",
|
||||
"not-started": " ",
|
||||
"not started": " ",
|
||||
};
|
||||
|
||||
private config: FileSourceConfig;
|
||||
private isInitialized = false;
|
||||
private lastUpdateSeq = 0;
|
||||
|
|
@ -621,21 +640,37 @@ export class FileSource {
|
|||
: filePath.split("/").pop() || filePath;
|
||||
|
||||
// Extract metadata from frontmatter
|
||||
const metadata = this.extractTaskMetadata(
|
||||
const metadataResult = this.extractTaskMetadata(
|
||||
filePath,
|
||||
fileContent,
|
||||
fileCache,
|
||||
strategy
|
||||
);
|
||||
|
||||
// Separate rawStatus from metadata (rawStatus is not part of FileSourceTaskMetadata)
|
||||
const { rawStatus, ...metadata } = metadataResult;
|
||||
|
||||
// Compute status symbol from raw frontmatter value (BaseTask layer)
|
||||
const status = this.computeStatusSymbol(
|
||||
rawStatus,
|
||||
config.fileTaskProperties.defaultStatus
|
||||
);
|
||||
|
||||
// Log status mapping if conversion occurred
|
||||
if (rawStatus && status !== rawStatus) {
|
||||
console.log(
|
||||
`[FileSource] Mapped status '${rawStatus}' to '${status}' for ${filePath}`
|
||||
);
|
||||
}
|
||||
|
||||
// Create the file task
|
||||
const fileTask: Task<FileSourceTaskMetadata> = {
|
||||
id: `file-source:${filePath}`,
|
||||
content: safeContent,
|
||||
filePath,
|
||||
line: 0, // File tasks are at line 0
|
||||
completed: metadata.status === "x" || metadata.status === "X",
|
||||
status: metadata.status || config.fileTaskProperties.defaultStatus,
|
||||
completed: status === "x" || status === "X",
|
||||
status: status,
|
||||
originalMarkdown: `**${safeContent}**`,
|
||||
metadata: {
|
||||
...metadata,
|
||||
|
|
@ -801,15 +836,47 @@ export class FileSource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute task status symbol from raw metadata value
|
||||
* Maps textual status values (e.g., "completed", "in-progress") to task status symbols (e.g., "x", "/")
|
||||
*/
|
||||
private computeStatusSymbol(
|
||||
rawStatus: string | undefined,
|
||||
defaultStatus: string
|
||||
): string {
|
||||
if (!rawStatus) return defaultStatus;
|
||||
// Already a single-character mark
|
||||
if (rawStatus.length === 1) return rawStatus;
|
||||
|
||||
const sm = this.config.getConfig().statusMapping;
|
||||
const target = sm.caseSensitive
|
||||
? rawStatus
|
||||
: String(rawStatus).toLowerCase();
|
||||
|
||||
// Try configured metadata->symbol table first
|
||||
for (const [k, sym] of Object.entries(sm.metadataToSymbol || {})) {
|
||||
const key = sm.caseSensitive ? k : k.toLowerCase();
|
||||
if (key === target) return sym;
|
||||
}
|
||||
|
||||
// Fallback to common defaults to be robust
|
||||
const norm = String(rawStatus).toLowerCase();
|
||||
const defaultMapping = FileSource.DEFAULT_STATUS_MAPPINGS[norm];
|
||||
if (defaultMapping !== undefined) return defaultMapping;
|
||||
|
||||
return defaultStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract task metadata from file
|
||||
* Returns metadata along with rawStatus for status symbol computation
|
||||
*/
|
||||
private extractTaskMetadata(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
fileCache: CachedMetadata | null,
|
||||
strategy: { name: RecognitionStrategy; criteria: string }
|
||||
): Partial<FileSourceTaskMetadata> {
|
||||
): Partial<FileSourceTaskMetadata> & { rawStatus?: string } {
|
||||
const config = this.config.getConfig();
|
||||
const frontmatter = fileCache?.frontmatter || {};
|
||||
const resolvedFrontmatter = this.applyMetadataMappings(
|
||||
|
|
@ -817,46 +884,9 @@ export class FileSource {
|
|||
config.metadataMappings
|
||||
);
|
||||
|
||||
// Derive status from frontmatter and eagerly map textual metadata to a symbol
|
||||
const rawStatus = resolvedFrontmatter.status ?? "";
|
||||
const toSymbol = (val: string): string => {
|
||||
if (!val) return config.fileTaskProperties.defaultStatus;
|
||||
// Already a single-character mark
|
||||
if (val.length === 1) return val;
|
||||
const sm = this.config.getConfig().statusMapping;
|
||||
const target = sm.caseSensitive ? val : String(val).toLowerCase();
|
||||
// Try configured metadata->symbol table first
|
||||
for (const [k, sym] of Object.entries(sm.metadataToSymbol || {})) {
|
||||
const key = sm.caseSensitive ? k : k.toLowerCase();
|
||||
if (key === target) return sym;
|
||||
}
|
||||
// Fallback to common defaults to be robust
|
||||
const defaults: Record<string, string> = {
|
||||
completed: "x",
|
||||
done: "x",
|
||||
finished: "x",
|
||||
"in-progress": "/",
|
||||
"in progress": "/",
|
||||
doing: "/",
|
||||
planned: "?",
|
||||
todo: "?",
|
||||
cancelled: "-",
|
||||
canceled: "-",
|
||||
"not-started": " ",
|
||||
"not started": " ",
|
||||
};
|
||||
const norm = String(val).toLowerCase();
|
||||
if (defaults[norm] !== undefined) return defaults[norm];
|
||||
return config.fileTaskProperties.defaultStatus;
|
||||
};
|
||||
let status = rawStatus
|
||||
? toSymbol(rawStatus)
|
||||
: config.fileTaskProperties.defaultStatus;
|
||||
if (rawStatus && status !== rawStatus) {
|
||||
console.log(
|
||||
`[FileSource] Mapped status '${rawStatus}' to '${status}' for ${filePath}`
|
||||
);
|
||||
}
|
||||
// Extract raw status value from frontmatter for later symbol mapping
|
||||
const rawStatusVal = resolvedFrontmatter.status ?? "";
|
||||
const rawStatus: string = typeof rawStatusVal === "string" ? rawStatusVal : String(rawStatusVal ?? "");
|
||||
|
||||
// TODO: Future enhancement - read frontmatter.repeat and map into FileSourceTaskMetadata (e.g., as recurrence)
|
||||
|
||||
|
|
@ -876,7 +906,11 @@ export class FileSource {
|
|||
// 1. Direct frontmatter field (project: xxx) - highest priority
|
||||
// 2. Metadata mapping (e.g., custom_project mapped to project via metadataMappings)
|
||||
// 3. Tag extraction (#project/xxx) - lowest priority, only if frontmatter has nothing
|
||||
const projectValue = resolvedFrontmatter.project ?? projectFromTags;
|
||||
const projectVal = resolvedFrontmatter.project ?? projectFromTags;
|
||||
const projectValue: string | undefined =
|
||||
projectVal !== undefined && projectVal !== null
|
||||
? String(projectVal)
|
||||
: undefined;
|
||||
|
||||
const shouldStripProjectTags =
|
||||
!resolvedFrontmatter.project &&
|
||||
|
|
@ -905,21 +939,31 @@ export class FileSource {
|
|||
recurrence:
|
||||
typeof resolvedFrontmatter.recurrence === "string"
|
||||
? resolvedFrontmatter.recurrence
|
||||
: undefined,
|
||||
: (resolvedFrontmatter.recurrence != null
|
||||
? String(resolvedFrontmatter.recurrence)
|
||||
: undefined),
|
||||
priority:
|
||||
this.parsePriority(resolvedFrontmatter.priority) ??
|
||||
config.fileTaskProperties.defaultPriority,
|
||||
// Project: frontmatter (direct or mapped) > tags (#project/xxx)
|
||||
project: projectValue,
|
||||
// Context and area: ONLY from frontmatter (direct or mapped via metadataMappings)
|
||||
context: resolvedFrontmatter.context,
|
||||
area: resolvedFrontmatter.area,
|
||||
context:
|
||||
resolvedFrontmatter.context !== undefined &&
|
||||
resolvedFrontmatter.context !== null
|
||||
? String(resolvedFrontmatter.context)
|
||||
: undefined,
|
||||
area:
|
||||
resolvedFrontmatter.area !== undefined &&
|
||||
resolvedFrontmatter.area !== null
|
||||
? String(resolvedFrontmatter.area)
|
||||
: undefined,
|
||||
tags: tagsForMetadata,
|
||||
status: status,
|
||||
children: [],
|
||||
};
|
||||
|
||||
return metadata;
|
||||
// Return metadata with rawStatus for status computation in buildFileTask
|
||||
return { ...metadata, rawStatus };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
TopNavigation,
|
||||
ViewMode,
|
||||
} from "@/components/features/fluent/components/FluentTopNavigation";
|
||||
import { FluentTaskViewState } from "@/types/fluent-types";
|
||||
import { FluentTaskViewState, ErrorContext } from "@/types/fluent-types";
|
||||
import {
|
||||
onWorkspaceSwitched,
|
||||
onWorkspaceOverridesSaved,
|
||||
|
|
@ -482,9 +482,16 @@ export class FluentTaskView extends ItemView {
|
|||
if (error) {
|
||||
this.loadError = error;
|
||||
this.isLoading = false;
|
||||
this.componentManager?.renderErrorState(error, () => {
|
||||
this.dataManager.loadTasks();
|
||||
});
|
||||
this.componentManager?.renderErrorState(
|
||||
this.createErrorContext(
|
||||
error,
|
||||
t("Loading tasks"),
|
||||
"src/components/features/fluent/managers/FluentComponentManager.ts"
|
||||
),
|
||||
() => {
|
||||
this.dataManager.loadTasks();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.tasks = tasks;
|
||||
this.loadError = null;
|
||||
|
|
@ -1182,9 +1189,16 @@ export class FluentTaskView extends ItemView {
|
|||
|
||||
// Show error state
|
||||
if (this.loadError) {
|
||||
this.componentManager.renderErrorState(this.loadError, () => {
|
||||
this.dataManager.loadTasks();
|
||||
});
|
||||
this.componentManager.renderErrorState(
|
||||
this.createErrorContext(
|
||||
this.loadError,
|
||||
t("Updating view"),
|
||||
"src/pages/FluentTaskView.ts"
|
||||
),
|
||||
() => {
|
||||
this.dataManager.loadTasks();
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1263,6 +1277,25 @@ export class FluentTaskView extends ItemView {
|
|||
/**
|
||||
* Set state (for debugging)
|
||||
*/
|
||||
/**
|
||||
* Create error context object for structured error display
|
||||
*/
|
||||
private createErrorContext(
|
||||
error: string | Error,
|
||||
operation: string,
|
||||
filePath: string
|
||||
): ErrorContext {
|
||||
return {
|
||||
viewId: this.currentViewId,
|
||||
componentName: this.componentManager?.getCurrentComponentName(),
|
||||
operation: operation,
|
||||
filePath: filePath,
|
||||
userMessage: typeof error === "string" ? error : error?.message,
|
||||
originalError:
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
};
|
||||
}
|
||||
|
||||
async setState(state: any, result: any) {
|
||||
// Restore state if needed
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,3 +19,21 @@ export type FluentTaskNavigationItem = {
|
|||
action?: () => void;
|
||||
badge?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Error context for structured error display
|
||||
*/
|
||||
export interface ErrorContext {
|
||||
/** View ID (inbox, today, projects, etc.) */
|
||||
viewId?: string;
|
||||
/** Component name (ContentComponent, KanbanComponent, etc.) */
|
||||
componentName?: string;
|
||||
/** Operation description (e.g., "Loading tasks", "Switching view") */
|
||||
operation?: string;
|
||||
/** File path where error occurred */
|
||||
filePath?: string;
|
||||
/** Original error object */
|
||||
originalError?: Error;
|
||||
/** User-friendly error message */
|
||||
userMessage?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,132 @@ import { Task } from "@/types/task";
|
|||
import { GroupByDimension, TaskGroup } from "@/types/groupBy";
|
||||
import { t } from "@/translations/helper";
|
||||
|
||||
/**
|
||||
* Default status mark to display name mapping
|
||||
* Based on common task management conventions and Tasks plugin compatibility
|
||||
* Used as fallback when status mark is not found in user's taskStatusMarks configuration
|
||||
*/
|
||||
const DEFAULT_STATUS_NAMES: Record<string, string> = {
|
||||
// Standard statuses
|
||||
" ": "Todo", // Incomplete/Not started
|
||||
"x": "Done", // Completed (lowercase)
|
||||
"X": "Done", // Completed (uppercase)
|
||||
"-": "Cancelled", // Cancelled/Abandoned
|
||||
|
||||
// Progress indicators
|
||||
"/": "In Progress", // Half-done/In progress
|
||||
">": "Forwarded", // Deferred/Forwarded
|
||||
"<": "Scheduled", // Scheduled for later
|
||||
|
||||
// Priority/importance markers
|
||||
"!": "Important", // Important/High priority
|
||||
"*": "Star", // Starred/Favorite
|
||||
|
||||
// Question/tentative
|
||||
"?": "Question", // Uncertain/Question
|
||||
|
||||
// Extended status marks (from various task management systems)
|
||||
"i": "Info", // Information
|
||||
"I": "Idea", // Idea
|
||||
"S": "Started", // Started
|
||||
"p": "Pro", // Pro/Professional
|
||||
"c": "Choice", // Choice
|
||||
"l": "Location", // Location-based
|
||||
"b": "Bookmark", // Bookmarked
|
||||
"f": "Fire", // Urgent/Fire
|
||||
"k": "Key", // Key task
|
||||
"w": "Win", // Won/Achieved
|
||||
"u": "Up", // Ongoing/Up
|
||||
"d": "Down", // Deprecated/Down
|
||||
'"': "Quote", // Quoted/Referenced
|
||||
"B": "Bug", // Bug/Issue
|
||||
|
||||
// Numeric progress indicators (0-5 scale)
|
||||
"0": "0/5",
|
||||
"1": "1/5",
|
||||
"2": "2/5",
|
||||
"3": "3/5",
|
||||
"4": "4/5",
|
||||
"5": "5/5",
|
||||
|
||||
// Other
|
||||
"n": "Note", // Note
|
||||
};
|
||||
|
||||
/**
|
||||
* Format status key to readable text
|
||||
* Handles camelCase, snake_case, and kebab-case
|
||||
* @example formatStatusKey("IN_PROGRESS") => "In Progress"
|
||||
* @example formatStatusKey("inProgress") => "In Progress"
|
||||
*/
|
||||
function formatStatusKey(key: string): string {
|
||||
return key
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2") // camelCase -> camel Case
|
||||
.replace(/[-_]+/g, " ") // snake_case/kebab-case -> spaces
|
||||
.split(" ")
|
||||
.filter(Boolean)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert status mark to display name using task status marks mapping
|
||||
* Performs reverse lookup in taskStatusMarks to find the configured status name
|
||||
* Falls back to default status names, then formatting if no mapping is found
|
||||
*
|
||||
* Lookup priority:
|
||||
* 1. User-configured taskStatusMarks (highest priority)
|
||||
* 2. DEFAULT_STATUS_NAMES (common status marks)
|
||||
* 3. formatStatusKey (generic formatting fallback)
|
||||
*
|
||||
* @param mark - Status mark from task (e.g., "x", ">", "-", " ")
|
||||
* @param taskStatusMarks - Status marks mapping from plugin settings
|
||||
* @returns Display name (e.g., "Completed", "In Progress", "Abandoned")
|
||||
*
|
||||
* @example
|
||||
* // With taskStatusMarks = { "Completed": "x", "In Progress": ">", "Abandoned": "-" }
|
||||
* getStatusDisplayName("x", taskStatusMarks) => "Completed"
|
||||
* getStatusDisplayName(">", taskStatusMarks) => "In Progress"
|
||||
* getStatusDisplayName("/", taskStatusMarks) => "In Progress" (from DEFAULT_STATUS_NAMES)
|
||||
* getStatusDisplayName("unknown", taskStatusMarks) => "Unknown" (from formatStatusKey)
|
||||
*/
|
||||
function getStatusDisplayName(
|
||||
mark: string,
|
||||
taskStatusMarks?: Record<string, string>
|
||||
): string {
|
||||
// Priority 1: User-configured taskStatusMarks
|
||||
if (taskStatusMarks) {
|
||||
for (const [statusName, statusMark] of Object.entries(taskStatusMarks)) {
|
||||
// Match both exact and case-insensitive
|
||||
if (statusMark === mark ||
|
||||
(statusMark && statusMark.toLowerCase() === mark.toLowerCase())) {
|
||||
return statusName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Check default status names mapping
|
||||
if (mark in DEFAULT_STATUS_NAMES) {
|
||||
return DEFAULT_STATUS_NAMES[mark];
|
||||
}
|
||||
|
||||
// Priority 3: Fallback formatting for unknown marks
|
||||
// Handle empty/whitespace marks
|
||||
if (!mark || mark.trim().length === 0) {
|
||||
return DEFAULT_STATUS_NAMES[" "] || "Todo";
|
||||
}
|
||||
|
||||
// Use generic formatting for completely unknown marks
|
||||
return formatStatusKey(mark);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main grouping function - dispatches to specific grouping strategies
|
||||
*/
|
||||
export function groupTasksBy(
|
||||
tasks: Task[],
|
||||
dimension: GroupByDimension
|
||||
dimension: GroupByDimension,
|
||||
taskStatusMarks?: Record<string, string>
|
||||
): TaskGroup[] {
|
||||
switch (dimension) {
|
||||
case "none":
|
||||
|
|
@ -28,7 +148,7 @@ export function groupTasksBy(
|
|||
case "tags":
|
||||
return groupTasksByTags(tasks);
|
||||
case "status":
|
||||
return groupTasksByStatus(tasks);
|
||||
return groupTasksByStatus(tasks, taskStatusMarks);
|
||||
default:
|
||||
return groupTasksNone(tasks);
|
||||
}
|
||||
|
|
@ -539,8 +659,16 @@ export function groupTasksByTags(tasks: Task[]): TaskGroup[] {
|
|||
|
||||
/**
|
||||
* Group tasks by status
|
||||
* Uses taskStatusMarks mapping to display user-configured status names
|
||||
*
|
||||
* @param tasks - Array of tasks to group
|
||||
* @param taskStatusMarks - Optional status marks mapping from plugin settings
|
||||
* @returns Array of task groups organized by status
|
||||
*/
|
||||
export function groupTasksByStatus(tasks: Task[]): TaskGroup[] {
|
||||
export function groupTasksByStatus(
|
||||
tasks: Task[],
|
||||
taskStatusMarks?: Record<string, string>
|
||||
): TaskGroup[] {
|
||||
const groupMap = new Map<string, Task[]>();
|
||||
|
||||
tasks.forEach((task) => {
|
||||
|
|
@ -573,14 +701,8 @@ export function groupTasksByStatus(tasks: Task[]): TaskGroup[] {
|
|||
});
|
||||
|
||||
sortedKeys.forEach((key, index) => {
|
||||
// Format status for display
|
||||
const displayStatus = key
|
||||
.split("_")
|
||||
.map(
|
||||
(word) =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||||
)
|
||||
.join(" ");
|
||||
// Get display name using status marks mapping or fallback formatting
|
||||
const displayStatus = getStatusDisplayName(key, taskStatusMarks);
|
||||
|
||||
groups.push({
|
||||
title: displayStatus,
|
||||
|
|
|
|||
Loading…
Reference in a new issue