mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(kanban): add cycle selector and Other column for unmatched tasks
- Add cycle selector button in kanban controls to filter by specific status cycle - Display 'Other' column for tasks not in selected cycle - Prevent status updates when dragging to Other column (returns null) - Save cycle selection to localStorage per view - Add getUnmatchedTasks() to collect tasks outside current cycle - Update getTasksForStatus() to respect cycle selection - Add getStatusMarkForColumn() to handle Other column drag prevention - Add translations for cycle selector UI (en, zh-cn) - Update kanban styles for cycle selector button - Update Fluent UI components and styles - Update status switcher and task grouping logic - Update read mode text mark handling
This commit is contained in:
parent
bf36bf2ff8
commit
36d834091a
12 changed files with 5680 additions and 4077 deletions
|
|
@ -24,7 +24,7 @@ export interface CustomNavButton {
|
|||
|
||||
export function isCompletedMark(
|
||||
plugin: TaskProgressBarPlugin,
|
||||
mark: string
|
||||
mark: string,
|
||||
): boolean {
|
||||
if (!mark) return false;
|
||||
try {
|
||||
|
|
@ -48,7 +48,13 @@ export class TopNavigation extends Component {
|
|||
private availableModes: ViewMode[] = ["list", "kanban", "tree", "calendar"];
|
||||
private viewTabsContainer: HTMLElement | null = null;
|
||||
private customButtonsContainer: HTMLElement | null = null;
|
||||
private customButtons: Map<string, { buttonEl: HTMLElement; config: CustomNavButton }> = new Map();
|
||||
private customButtons: Map<
|
||||
string,
|
||||
{ buttonEl: HTMLElement; config: CustomNavButton }
|
||||
> = new Map();
|
||||
private cycleSelectorContainer: HTMLElement | null = null;
|
||||
private selectedCycleId: string | null = null;
|
||||
private onCycleChange: ((cycleId: string | null) => void) | null = null;
|
||||
|
||||
constructor(
|
||||
containerEl: HTMLElement,
|
||||
|
|
@ -59,7 +65,7 @@ export class TopNavigation extends Component {
|
|||
private onSortClick: () => void,
|
||||
private onSettingsClick: () => void,
|
||||
availableModes?: ViewMode[],
|
||||
private onToggleSidebar?: () => void
|
||||
private onToggleSidebar?: () => void,
|
||||
) {
|
||||
super();
|
||||
this.containerEl = containerEl;
|
||||
|
|
@ -87,14 +93,14 @@ export class TopNavigation extends Component {
|
|||
this.registerEvent(
|
||||
on(this.plugin.app, Events.CACHE_READY, () => {
|
||||
this.updateNotificationCount();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Listen for task cache updates
|
||||
this.registerEvent(
|
||||
on(this.plugin.app, Events.TASK_CACHE_UPDATED, () => {
|
||||
this.updateNotificationCount();
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -151,14 +157,14 @@ export class TopNavigation extends Component {
|
|||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
this.notificationCount = tasks.filter((task: Task) =>
|
||||
this.isOverdueTask(task, today)
|
||||
this.isOverdueTask(task, today),
|
||||
).length;
|
||||
|
||||
this.updateNotificationBadge();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[FluentTopNavigation] Failed to update notification count:",
|
||||
error
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -170,9 +176,12 @@ export class TopNavigation extends Component {
|
|||
// Hide entire navigation if no view modes are available
|
||||
if (this.availableModes.length === 0) {
|
||||
this.containerEl.hide();
|
||||
this.containerEl.toggleClass("other-view", true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.containerEl.toggleClass("other-view", false);
|
||||
|
||||
// Show navigation when modes are available
|
||||
this.containerEl.show();
|
||||
|
||||
|
|
@ -212,6 +221,12 @@ export class TopNavigation extends Component {
|
|||
cls: "fluent-nav-custom-buttons",
|
||||
});
|
||||
|
||||
// Cycle selector container (hidden by default)
|
||||
this.cycleSelectorContainer = rightSection.createDiv({
|
||||
cls: "fluent-nav-cycle-selector-wrapper",
|
||||
});
|
||||
this.cycleSelectorContainer.hide();
|
||||
|
||||
// Notification button
|
||||
const notificationBtn = rightSection.createDiv({
|
||||
cls: "fluent-nav-icon-button",
|
||||
|
|
@ -227,7 +242,7 @@ export class TopNavigation extends Component {
|
|||
badge.show();
|
||||
}
|
||||
this.registerDomEvent(notificationBtn, "click", (e) =>
|
||||
this.showNotifications(e)
|
||||
this.showNotifications(e),
|
||||
);
|
||||
|
||||
// Settings button
|
||||
|
|
@ -236,7 +251,7 @@ export class TopNavigation extends Component {
|
|||
});
|
||||
setIcon(settingsBtn, "settings");
|
||||
this.registerDomEvent(settingsBtn, "click", () =>
|
||||
this.onSettingsClick()
|
||||
this.onSettingsClick(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +259,7 @@ export class TopNavigation extends Component {
|
|||
container: HTMLElement,
|
||||
mode: ViewMode,
|
||||
icon: string,
|
||||
label: string
|
||||
label: string,
|
||||
) {
|
||||
const tab = container.createEl("button", {
|
||||
cls: ["fluent-view-tab", "clickable-icon"],
|
||||
|
|
@ -275,7 +290,7 @@ export class TopNavigation extends Component {
|
|||
});
|
||||
|
||||
const activeTab = this.containerEl.querySelector(
|
||||
`[data-mode="${mode}"]`
|
||||
`[data-mode="${mode}"]`,
|
||||
);
|
||||
if (activeTab) {
|
||||
activeTab.addClass("is-active");
|
||||
|
|
@ -296,7 +311,7 @@ export class TopNavigation extends Component {
|
|||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const overdueTasks = tasks.filter((task: Task) =>
|
||||
this.isOverdueTask(task, today)
|
||||
this.isOverdueTask(task, today),
|
||||
);
|
||||
|
||||
if (overdueTasks.length === 0) {
|
||||
|
|
@ -306,7 +321,7 @@ export class TopNavigation extends Component {
|
|||
} else {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(
|
||||
`${overdueTasks.length} overdue tasks`
|
||||
`${overdueTasks.length} overdue tasks`,
|
||||
).setDisabled(true);
|
||||
});
|
||||
|
||||
|
|
@ -340,7 +355,7 @@ export class TopNavigation extends Component {
|
|||
|
||||
private updateNotificationBadge() {
|
||||
const badge = this.containerEl.querySelector(
|
||||
".fluent-notification-badge"
|
||||
".fluent-notification-badge",
|
||||
);
|
||||
if (badge instanceof HTMLElement) {
|
||||
badge.textContent = String(this.notificationCount);
|
||||
|
|
@ -386,7 +401,7 @@ export class TopNavigation extends Component {
|
|||
this.viewTabsContainer,
|
||||
mode,
|
||||
config.icon,
|
||||
config.label
|
||||
config.label,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -399,9 +414,12 @@ export class TopNavigation extends Component {
|
|||
// Hide entire navigation if no modes available
|
||||
if (modes.length === 0) {
|
||||
this.containerEl.hide();
|
||||
this.containerEl.toggleClass("other-view", true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.containerEl.toggleClass("other-view", false);
|
||||
|
||||
// If transitioning from empty to non-empty, need to re-render entire UI
|
||||
if (wasEmpty && modes.length > 0) {
|
||||
this.render();
|
||||
|
|
@ -420,7 +438,7 @@ export class TopNavigation extends Component {
|
|||
|
||||
// Update center section visibility (this should always be visible now since we handle empty modes above)
|
||||
const centerSection = this.containerEl.querySelector(
|
||||
".fluent-nav-center"
|
||||
".fluent-nav-center",
|
||||
) as HTMLElement;
|
||||
if (centerSection) {
|
||||
centerSection.show();
|
||||
|
|
@ -443,13 +461,17 @@ export class TopNavigation extends Component {
|
|||
*/
|
||||
public registerCustomButton(config: CustomNavButton): void {
|
||||
if (!this.customButtonsContainer) {
|
||||
console.warn("[FluentTopNavigation] Custom buttons container not initialized");
|
||||
console.warn(
|
||||
"[FluentTopNavigation] Custom buttons container not initialized",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if button already exists
|
||||
if (this.customButtons.has(config.id)) {
|
||||
console.warn(`[FluentTopNavigation] Button with id "${config.id}" already registered`);
|
||||
console.warn(
|
||||
`[FluentTopNavigation] Button with id "${config.id}" already registered`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -470,7 +492,9 @@ export class TopNavigation extends Component {
|
|||
config: config,
|
||||
});
|
||||
|
||||
console.log(`[FluentTopNavigation] Registered custom button: ${config.id}`);
|
||||
console.log(
|
||||
`[FluentTopNavigation] Registered custom button: ${config.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -509,4 +533,165 @@ export class TopNavigation extends Component {
|
|||
|
||||
console.log("[FluentTopNavigation] Cleared all custom buttons");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback to be called when cycle selection changes
|
||||
*/
|
||||
public setCycleChangeCallback(
|
||||
callback: (cycleId: string | null) => void,
|
||||
): void {
|
||||
this.onCycleChange = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the cycle selector in the navigation bar
|
||||
* @param selectedCycleId - Currently selected cycle ID (null for "All Cycles")
|
||||
*/
|
||||
public showCycleSelector(selectedCycleId: string | null = null): void {
|
||||
if (!this.cycleSelectorContainer) {
|
||||
console.warn(
|
||||
"[FluentTopNavigation] Cycle selector container not initialized",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedCycleId = selectedCycleId;
|
||||
this.renderCycleSelector();
|
||||
this.cycleSelectorContainer.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the cycle selector from the navigation bar
|
||||
*/
|
||||
public hideCycleSelector(): void {
|
||||
if (this.cycleSelectorContainer) {
|
||||
this.cycleSelectorContainer.hide();
|
||||
this.cycleSelectorContainer.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the selected cycle ID and refresh the UI
|
||||
*/
|
||||
public setSelectedCycleId(cycleId: string | null): void {
|
||||
this.selectedCycleId = cycleId;
|
||||
if (
|
||||
this.cycleSelectorContainer &&
|
||||
this.cycleSelectorContainer.isShown()
|
||||
) {
|
||||
this.renderCycleSelector();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected cycle ID
|
||||
*/
|
||||
public getSelectedCycleId(): string | null {
|
||||
return this.selectedCycleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the cycle selector dropdown button
|
||||
*/
|
||||
private renderCycleSelector(): void {
|
||||
if (!this.cycleSelectorContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cycleSelectorContainer.empty();
|
||||
|
||||
// Create cycle selector button
|
||||
const cycleButton = this.cycleSelectorContainer.createEl("button", {
|
||||
cls: "fluent-nav-cycle-button clickable-icon",
|
||||
attr: {
|
||||
"aria-label": t("kanban.cycleSelector"),
|
||||
},
|
||||
});
|
||||
|
||||
// Add icon
|
||||
const iconDiv = cycleButton.createDiv({ cls: "fluent-nav-cycle-icon" });
|
||||
setIcon(iconDiv, "layers");
|
||||
|
||||
// Add label
|
||||
const labelSpan = cycleButton.createSpan({
|
||||
cls: "fluent-nav-cycle-label",
|
||||
});
|
||||
|
||||
// Set label based on selected cycle
|
||||
if (this.selectedCycleId) {
|
||||
const selectedCycle = (
|
||||
this.plugin.settings.statusCycles || []
|
||||
).find((c) => c.id === this.selectedCycleId);
|
||||
labelSpan.textContent =
|
||||
selectedCycle?.name || t("kanban.allCycles");
|
||||
} else {
|
||||
labelSpan.textContent = t("kanban.allCycles");
|
||||
}
|
||||
|
||||
// Register click event to show menu
|
||||
this.registerDomEvent(cycleButton, "click", (event: MouseEvent) => {
|
||||
this.showCycleMenu(event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the cycle selection menu
|
||||
*/
|
||||
private showCycleMenu(event: MouseEvent): void {
|
||||
const menu = new Menu();
|
||||
|
||||
// "All Cycles" option
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(t("kanban.allCycles"))
|
||||
.setChecked(this.selectedCycleId === null)
|
||||
.onClick(() => {
|
||||
this.selectedCycleId = null;
|
||||
this.renderCycleSelector();
|
||||
if (this.onCycleChange) {
|
||||
this.onCycleChange(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
// Get enabled cycles, sorted by priority
|
||||
const enabledCycles = (this.plugin.settings.statusCycles || [])
|
||||
.filter((c) => c.enabled)
|
||||
.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
if (enabledCycles.length === 0) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(t("kanban.noCyclesAvailable")).setDisabled(true);
|
||||
});
|
||||
} else {
|
||||
for (const cycle of enabledCycles) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(cycle.name)
|
||||
.setChecked(this.selectedCycleId === cycle.id)
|
||||
.onClick(() => {
|
||||
this.selectedCycleId = cycle.id;
|
||||
this.renderCycleSelector();
|
||||
if (this.onCycleChange) {
|
||||
this.onCycleChange(cycle.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
menu.addItem((i) => {
|
||||
i.setTitle(t("Open status cycle settings"));
|
||||
i.onClick(() => {
|
||||
this.plugin.app.setting.open();
|
||||
this.plugin.app.setting.openTabById(this.plugin.manifest.id);
|
||||
|
||||
this.plugin.settingTab.openTab("task-status");
|
||||
});
|
||||
});
|
||||
|
||||
menu.showAtMouseEvent(event);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -438,6 +438,11 @@ export class FluentComponentManager extends Component {
|
|||
this.listContainer?.hide();
|
||||
this.treeContainer?.hide();
|
||||
}
|
||||
|
||||
// Hide cycle selector when hiding all components
|
||||
if (this.topNavigation) {
|
||||
this.topNavigation.hideCycleSelector();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -619,8 +624,13 @@ export class FluentComponentManager extends Component {
|
|||
this.currentVisibleComponent = targetComponent;
|
||||
|
||||
// Set TopNavigation reference for ContentComponent to enable custom buttons
|
||||
if (targetComponent === this.contentComponent && this.topNavigation) {
|
||||
console.log("[FluentComponent] Setting TopNavigation reference for ContentComponent");
|
||||
if (
|
||||
targetComponent === this.contentComponent &&
|
||||
this.topNavigation
|
||||
) {
|
||||
console.log(
|
||||
"[FluentComponent] Setting TopNavigation reference for ContentComponent",
|
||||
);
|
||||
this.contentComponent.setTopNavigation(this.topNavigation);
|
||||
}
|
||||
|
||||
|
|
@ -724,6 +734,8 @@ export class FluentComponentManager extends Component {
|
|||
// Clear custom buttons before switching view modes
|
||||
if (this.topNavigation) {
|
||||
this.topNavigation.clearCustomButtons();
|
||||
// Hide cycle selector when switching away from kanban
|
||||
this.topNavigation.hideCycleSelector();
|
||||
}
|
||||
|
||||
// Based on the current view mode, show the appropriate component
|
||||
|
|
@ -757,6 +769,31 @@ export class FluentComponentManager extends Component {
|
|||
|
||||
this.kanbanComponent.containerEl.show();
|
||||
|
||||
// Show cycle selector in TopNavigation for kanban view
|
||||
if (this.topNavigation) {
|
||||
// Load saved cycle selection from kanban component
|
||||
const savedCycleId =
|
||||
this.kanbanComponent["selectedCycleId"] || null;
|
||||
|
||||
// Set cycle change callback
|
||||
this.topNavigation.setCycleChangeCallback(
|
||||
(cycleId: string | null) => {
|
||||
console.log(
|
||||
"[FluentComponent] Cycle changed to:",
|
||||
cycleId,
|
||||
);
|
||||
// Update kanban component's selected cycle
|
||||
this.kanbanComponent["selectedCycleId"] = cycleId;
|
||||
this.kanbanComponent["saveCycleSelection"]();
|
||||
// Re-render columns with new cycle
|
||||
this.kanbanComponent["renderColumns"]();
|
||||
},
|
||||
);
|
||||
|
||||
// Show cycle selector with current selection
|
||||
this.topNavigation.showCycleSelector(savedCycleId);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[FluentComponent] Setting",
|
||||
filteredTasks.length,
|
||||
|
|
@ -888,7 +925,10 @@ export class FluentComponentManager extends Component {
|
|||
/**
|
||||
* Render error state with detailed context
|
||||
*/
|
||||
renderErrorState(context: ErrorContext | string, onRetry: () => void): void {
|
||||
renderErrorState(
|
||||
context: ErrorContext | string,
|
||||
onRetry: () => void,
|
||||
): void {
|
||||
if (!this.contentArea) return;
|
||||
|
||||
// Parse context: support both new (ErrorContext) and old (string) formats
|
||||
|
|
@ -952,7 +992,7 @@ export class FluentComponentManager extends Component {
|
|||
*/
|
||||
private createErrorContext(
|
||||
errorEl: HTMLElement,
|
||||
errorContext: ErrorContext
|
||||
errorContext: ErrorContext,
|
||||
): void {
|
||||
if (
|
||||
!errorContext.viewId &&
|
||||
|
|
@ -994,7 +1034,7 @@ export class FluentComponentManager extends Component {
|
|||
*/
|
||||
private createErrorMessage(
|
||||
errorEl: HTMLElement,
|
||||
errorContext: ErrorContext
|
||||
errorContext: ErrorContext,
|
||||
): void {
|
||||
const userMessage =
|
||||
errorContext.userMessage ||
|
||||
|
|
@ -1012,7 +1052,7 @@ export class FluentComponentManager extends Component {
|
|||
*/
|
||||
private createTechnicalDetails(
|
||||
errorEl: HTMLElement,
|
||||
errorContext: ErrorContext
|
||||
errorContext: ErrorContext,
|
||||
): void {
|
||||
if (!errorContext.originalError) return;
|
||||
|
||||
|
|
@ -1045,10 +1085,7 @@ export class FluentComponentManager extends Component {
|
|||
/**
|
||||
* Create retry button
|
||||
*/
|
||||
private createRetryButton(
|
||||
errorEl: HTMLElement,
|
||||
onRetry: () => void
|
||||
): void {
|
||||
private createRetryButton(errorEl: HTMLElement, onRetry: () => void): void {
|
||||
const retryBtn = errorEl.createEl("button", {
|
||||
cls: "tg-fluent-button tg-fluent-button-primary",
|
||||
text: t("Retry"),
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ export class KanbanComponent extends Component {
|
|||
};
|
||||
private hideEmptyColumns = false;
|
||||
private configOverride: Partial<KanbanSpecificConfig> | null = null; // Configuration override from Bases view
|
||||
private selectedCycleId: string | null = null; // Currently selected cycle ID for filtering
|
||||
private readonly CYCLE_STORAGE_KEY_PREFIX = "kanban-cycle-"; // localStorage key prefix for cycle selection
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
|
|
@ -136,6 +138,7 @@ export class KanbanComponent extends Component {
|
|||
|
||||
// Load configuration settings
|
||||
this.loadKanbanConfig();
|
||||
this.loadCycleSelection();
|
||||
|
||||
this.filterContainerEl = this.containerEl.createDiv({
|
||||
cls: "tg-kanban-filters",
|
||||
|
|
@ -276,6 +279,78 @@ export class KanbanComponent extends Component {
|
|||
|
||||
menu.showAtMouseEvent(event);
|
||||
});
|
||||
|
||||
// Cycle selector (only for status grouping)
|
||||
this.renderCycleSelector(controlsContainer);
|
||||
}
|
||||
|
||||
private renderCycleSelector(container: HTMLElement): void {
|
||||
// Only show cycle selector in status grouping mode
|
||||
const kanbanConfig = this.getEffectiveKanbanConfig();
|
||||
if (kanbanConfig?.groupBy !== "status") {
|
||||
return;
|
||||
}
|
||||
|
||||
const cycleContainer = container.createDiv({
|
||||
cls: "tg-kanban-cycle-container",
|
||||
});
|
||||
|
||||
const cycleButton = cycleContainer.createEl(
|
||||
"button",
|
||||
{
|
||||
cls: "tg-kanban-cycle-button clickable-icon",
|
||||
attr: {
|
||||
"aria-label": t("kanban.cycleSelector"),
|
||||
},
|
||||
},
|
||||
(el) => {
|
||||
setIcon(el, "layers");
|
||||
},
|
||||
);
|
||||
|
||||
this.registerDomEvent(cycleButton, "click", (event: MouseEvent) => {
|
||||
const menu = new Menu();
|
||||
|
||||
// "All Cycles" option
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(t("kanban.allCycles"))
|
||||
.setChecked(this.selectedCycleId === null)
|
||||
.onClick(() => {
|
||||
this.selectedCycleId = null;
|
||||
this.saveCycleSelection();
|
||||
this.renderColumns();
|
||||
});
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
// Get enabled cycles, sorted by priority
|
||||
const enabledCycles = (this.plugin.settings.statusCycles || [])
|
||||
.filter((c) => c.enabled)
|
||||
.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
if (enabledCycles.length === 0) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(t("kanban.noCyclesAvailable")).setDisabled(
|
||||
true,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
for (const cycle of enabledCycles) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(cycle.name)
|
||||
.setChecked(this.selectedCycleId === cycle.id)
|
||||
.onClick(() => {
|
||||
this.selectedCycleId = cycle.id;
|
||||
this.saveCycleSelection();
|
||||
this.renderColumns();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
menu.showAtMouseEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
private renderFilterControls(containerEl: HTMLElement) {
|
||||
|
|
@ -555,12 +630,42 @@ export class KanbanComponent extends Component {
|
|||
}
|
||||
|
||||
private renderStatusColumns() {
|
||||
const { cycle: statusCycle, excludeMarksFromCycle } =
|
||||
getTaskStatusConfig(this.plugin.settings);
|
||||
let statusNames =
|
||||
statusCycle.length > 0
|
||||
? statusCycle
|
||||
: ["Todo", "In Progress", "Done"];
|
||||
// Determine which statuses to render based on cycle selection
|
||||
let statusNames: string[];
|
||||
let excludeMarksFromCycle: string[] = [];
|
||||
|
||||
if (this.selectedCycleId) {
|
||||
// Find the selected cycle
|
||||
const selectedCycle = (
|
||||
this.plugin.settings.statusCycles || []
|
||||
).find((c) => c.id === this.selectedCycleId && c.enabled);
|
||||
|
||||
if (selectedCycle) {
|
||||
// Use only the statuses from the selected cycle
|
||||
statusNames = [...selectedCycle.cycle];
|
||||
} else {
|
||||
// Cycle no longer exists or is disabled, reset to all
|
||||
console.warn(
|
||||
`[Kanban] Selected cycle ${this.selectedCycleId} not found, resetting`,
|
||||
);
|
||||
this.selectedCycleId = null;
|
||||
this.saveCycleSelection();
|
||||
const config = getTaskStatusConfig(this.plugin.settings);
|
||||
statusNames =
|
||||
config.cycle.length > 0
|
||||
? config.cycle
|
||||
: ["Todo", "In Progress", "Done"];
|
||||
excludeMarksFromCycle = config.excludeMarksFromCycle || [];
|
||||
}
|
||||
} else {
|
||||
// Show all cycles (existing behavior)
|
||||
const config = getTaskStatusConfig(this.plugin.settings);
|
||||
statusNames =
|
||||
config.cycle.length > 0
|
||||
? config.cycle
|
||||
: ["Todo", "In Progress", "Done"];
|
||||
excludeMarksFromCycle = config.excludeMarksFromCycle || [];
|
||||
}
|
||||
|
||||
const spaceStatus: string[] = [];
|
||||
const xStatus: string[] = [];
|
||||
|
|
@ -614,6 +719,50 @@ export class KanbanComponent extends Component {
|
|||
this.addChild(column);
|
||||
this.columns.push(column);
|
||||
});
|
||||
|
||||
// Render "Other" column for unmatched tasks (only when a cycle is selected)
|
||||
if (this.selectedCycleId) {
|
||||
const unmatchedTasks = this.getUnmatchedTasks();
|
||||
if (unmatchedTasks.length > 0) {
|
||||
const otherColumn = new KanbanColumnComponent(
|
||||
this.app,
|
||||
this.plugin,
|
||||
this.columnContainerEl,
|
||||
t("kanban.otherColumn"),
|
||||
unmatchedTasks,
|
||||
{
|
||||
...this.params,
|
||||
onTaskStatusUpdate: (
|
||||
taskId: string,
|
||||
newStatusMark: string,
|
||||
) => this.handleStatusUpdate(taskId, newStatusMark),
|
||||
onFilterApply: this.handleFilterApply,
|
||||
},
|
||||
);
|
||||
this.addChild(otherColumn);
|
||||
this.columns.push(otherColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getUnmatchedTasks(): Task[] {
|
||||
if (!this.selectedCycleId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const selectedCycle = (this.plugin.settings.statusCycles || []).find(
|
||||
(c) => c.id === this.selectedCycleId,
|
||||
);
|
||||
|
||||
if (!selectedCycle) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Collect all marks from the selected cycle
|
||||
const cycleMarks = new Set(Object.values(selectedCycle.marks));
|
||||
|
||||
// Return tasks whose status is not in the cycle
|
||||
return this.tasks.filter((task) => !cycleMarks.has(task.status || " "));
|
||||
}
|
||||
|
||||
private renderCustomColumns(
|
||||
|
|
@ -809,14 +958,31 @@ export class KanbanComponent extends Component {
|
|||
}
|
||||
|
||||
private getTasksForStatus(statusName: string): Task[] {
|
||||
// Prefer multi-mark mapping from settings.taskStatuses when available
|
||||
const allowed = this.getAllowedMarksForStatusName(statusName);
|
||||
const statusMark = this.resolveStatusMark(statusName) ?? " ";
|
||||
let allowedMarks: Set<string>;
|
||||
|
||||
if (this.selectedCycleId) {
|
||||
// Use the selected cycle's marks mapping
|
||||
const selectedCycle = (
|
||||
this.plugin.settings.statusCycles || []
|
||||
).find((c) => c.id === this.selectedCycleId);
|
||||
|
||||
if (selectedCycle) {
|
||||
const mark = selectedCycle.marks[statusName];
|
||||
allowedMarks = mark ? new Set([mark]) : new Set();
|
||||
} else {
|
||||
allowedMarks = new Set();
|
||||
}
|
||||
} else {
|
||||
// Use multi-cycle logic (existing behavior)
|
||||
const allowed = this.getAllowedMarksForStatusName(statusName);
|
||||
const statusMark = this.resolveStatusMark(statusName) ?? " ";
|
||||
allowedMarks = allowed || new Set([statusMark]);
|
||||
}
|
||||
|
||||
// Filter from the already filtered list
|
||||
const tasksForStatus = this.tasks.filter((task) => {
|
||||
const mark = task.status || " ";
|
||||
return allowed ? allowed.has(mark) : mark === statusMark;
|
||||
return allowedMarks.has(mark);
|
||||
});
|
||||
|
||||
// Sort tasks within the status column based on selected sort option
|
||||
|
|
@ -943,18 +1109,21 @@ export class KanbanComponent extends Component {
|
|||
this.getEffectiveKanbanConfig()?.groupBy || groupByPlugin;
|
||||
|
||||
if (groupBy === "status") {
|
||||
// Handle status-based grouping (original logic)
|
||||
const targetStatusMark = this.resolveStatusMark(
|
||||
// Handle status-based grouping
|
||||
const targetStatusMark = this.getStatusMarkForColumn(
|
||||
(targetColumnTitle || "").trim(),
|
||||
);
|
||||
if (targetStatusMark !== undefined) {
|
||||
if (
|
||||
targetStatusMark !== undefined &&
|
||||
targetStatusMark !== null
|
||||
) {
|
||||
console.log(
|
||||
`Kanban requesting status update for task ${taskId} to status ${targetColumnTitle} (mark: ${targetStatusMark})`,
|
||||
);
|
||||
await this.handleStatusUpdate(taskId, targetStatusMark);
|
||||
} else {
|
||||
console.warn(
|
||||
`Could not find status mark for status name: ${targetColumnTitle}`,
|
||||
`Could not find status mark for status name: ${targetColumnTitle} or drag to Other column is not allowed`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1017,6 +1186,36 @@ export class KanbanComponent extends Component {
|
|||
this.loadColumnOrder();
|
||||
}
|
||||
|
||||
private getCycleStorageKey(): string {
|
||||
return `${this.CYCLE_STORAGE_KEY_PREFIX}${this.currentViewId}`;
|
||||
}
|
||||
|
||||
private loadCycleSelection(): void {
|
||||
const key = this.getCycleStorageKey();
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved) {
|
||||
// Verify that the cycle still exists and is enabled
|
||||
const cycles = this.plugin.settings.statusCycles || [];
|
||||
const cycleExists = cycles.some((c) => c.id === saved && c.enabled);
|
||||
this.selectedCycleId = cycleExists ? saved : null;
|
||||
if (!cycleExists && saved) {
|
||||
console.log(
|
||||
`[Kanban] Saved cycle ${saved} no longer exists or is disabled, resetting to null`,
|
||||
);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private saveCycleSelection(): void {
|
||||
const key = this.getCycleStorageKey();
|
||||
if (this.selectedCycleId) {
|
||||
localStorage.setItem(key, this.selectedCycleId);
|
||||
} else {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
private getSortOptionLabel(field: string, order: string): string {
|
||||
const fieldLabels: Record<string, string> = {
|
||||
priority: t("Priority"),
|
||||
|
|
@ -1034,6 +1233,36 @@ export class KanbanComponent extends Component {
|
|||
* Accepts either configured status names (e.g., "Abandoned")
|
||||
* or raw marks (e.g., "-", "x", "/").
|
||||
*/
|
||||
/**
|
||||
* Get status mark for a column title, respecting cycle selection.
|
||||
* Returns null if dragging to "Other" column (not allowed).
|
||||
*/
|
||||
private getStatusMarkForColumn(
|
||||
columnTitle: string,
|
||||
): string | null | undefined {
|
||||
if (!columnTitle) return undefined;
|
||||
|
||||
// Check if this is the "Other" column
|
||||
if (columnTitle === t("kanban.otherColumn")) {
|
||||
// Dragging to "Other" column is not allowed
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.selectedCycleId) {
|
||||
// Use selected cycle's marks mapping
|
||||
const selectedCycle = (
|
||||
this.plugin.settings.statusCycles || []
|
||||
).find((c) => c.id === this.selectedCycleId);
|
||||
|
||||
if (selectedCycle) {
|
||||
return selectedCycle.marks[columnTitle] || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to existing logic for multi-cycle or no cycle selected
|
||||
return this.resolveStatusMark(columnTitle);
|
||||
}
|
||||
|
||||
private resolveStatusMark(titleOrMark: string): string | undefined {
|
||||
if (!titleOrMark) return undefined;
|
||||
const trimmed = titleOrMark.trim();
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ class TaskTextMark extends Component {
|
|||
// Get cycle configuration from plugin settings
|
||||
const { cycle, marks, excludeMarksFromCycle } = getTaskStatusConfig(
|
||||
this.plugin.settings,
|
||||
this.currentMark,
|
||||
);
|
||||
|
||||
// Filter out excluded marks
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import {
|
|||
getTaskStatusConfig,
|
||||
} from "@/utils/status-cycle-resolver";
|
||||
|
||||
import { t } from "@/translations/helper";
|
||||
|
||||
export type TaskState = string;
|
||||
export const taskStatusChangeAnnotation = Annotation.define();
|
||||
|
||||
|
|
@ -51,6 +53,7 @@ class TaskStatusWidget extends WidgetType {
|
|||
readonly from: number,
|
||||
readonly to: number,
|
||||
readonly currentState: TaskState,
|
||||
readonly currentMark: string,
|
||||
readonly listPrefix: string,
|
||||
) {
|
||||
super();
|
||||
|
|
@ -193,7 +196,7 @@ class TaskStatusWidget extends WidgetType {
|
|||
if (applicableCycles.length > 0) {
|
||||
// Show each applicable cycle with its next status
|
||||
menu.addItem((item) => {
|
||||
item.setTitle("Switch using cycle:");
|
||||
item.setTitle(t("Cycle to next:"));
|
||||
item.setDisabled(true);
|
||||
});
|
||||
|
||||
|
|
@ -205,16 +208,16 @@ class TaskStatusWidget extends WidgetType {
|
|||
|
||||
if (nextStatusResult) {
|
||||
menu.addItem((item) => {
|
||||
const priorityIndicator =
|
||||
cycle.priority === 0 ? "★ " : "";
|
||||
const priorityIndicator = cycle.priority === 0;
|
||||
item.setTitle(
|
||||
`${priorityIndicator}${cycle.name}: → ${nextStatusResult.statusName}`,
|
||||
`${cycle.name}: ${nextStatusResult.statusName}`,
|
||||
);
|
||||
item.onClick(() => {
|
||||
this.setTaskState(
|
||||
nextStatusResult.statusName,
|
||||
);
|
||||
});
|
||||
item.setChecked(priorityIndicator);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -223,7 +226,7 @@ class TaskStatusWidget extends WidgetType {
|
|||
|
||||
// Add "Choose any status" section
|
||||
menu.addItem((item) => {
|
||||
item.setTitle("Choose any status:");
|
||||
item.setTitle(t("Or choose any:"));
|
||||
item.setDisabled(true);
|
||||
});
|
||||
|
||||
|
|
@ -234,11 +237,9 @@ class TaskStatusWidget extends WidgetType {
|
|||
for (const statusName of Array.from(allStatusNames)) {
|
||||
menu.addItem((item) => {
|
||||
const isCurrent = statusName === this.currentState;
|
||||
item.setTitle(
|
||||
isCurrent
|
||||
? `${statusName} (current)`
|
||||
: statusName,
|
||||
);
|
||||
item.setTitle(statusName);
|
||||
item.setDisabled(isCurrent);
|
||||
item.setChecked(isCurrent);
|
||||
item.onClick(() => {
|
||||
this.setTaskState(statusName);
|
||||
});
|
||||
|
|
@ -346,7 +347,7 @@ class TaskStatusWidget extends WidgetType {
|
|||
};
|
||||
}
|
||||
|
||||
return getTaskStatusConfig(this.plugin.settings);
|
||||
return getTaskStatusConfig(this.plugin.settings, this.currentMark);
|
||||
}
|
||||
|
||||
// Cycle through task states
|
||||
|
|
@ -440,7 +441,7 @@ export function taskStatusSwitcherExtension(
|
|||
const checkbox = checkboxWithSpace.trim();
|
||||
const isLivePreview = this.isLivePreview(view.state);
|
||||
const { cycle, marks, excludeMarksFromCycle } =
|
||||
getTaskStatusConfig(plugin.settings);
|
||||
getTaskStatusConfig(plugin.settings, mark);
|
||||
const remainingCycle = cycle.filter(
|
||||
(state) => !excludeMarksFromCycle.includes(state),
|
||||
);
|
||||
|
|
@ -476,6 +477,7 @@ export function taskStatusSwitcherExtension(
|
|||
checkboxStart,
|
||||
checkboxEnd,
|
||||
currentState,
|
||||
mark,
|
||||
bulletText,
|
||||
),
|
||||
}),
|
||||
|
|
@ -499,6 +501,7 @@ export function taskStatusSwitcherExtension(
|
|||
bulletWithSpace.length +
|
||||
checkbox.length,
|
||||
currentState,
|
||||
mark,
|
||||
bulletText,
|
||||
),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -533,6 +533,54 @@ button.fluent-new-task-btn:hover {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
/* Cycle selector styles */
|
||||
.fluent-nav-cycle-selector-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.fluent-nav-cycle-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-ui-small);
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fluent-nav-cycle-button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
border-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.fluent-nav-cycle-button:active {
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
.fluent-nav-cycle-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.fluent-nav-cycle-label {
|
||||
font-weight: 500;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fluent-nav-icon-button {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
|
|
@ -573,7 +621,9 @@ button.fluent-new-task-btn:hover {
|
|||
|
||||
.bases-view .tg-kanban-filters,
|
||||
.tg-fluent-content .content-header,
|
||||
.tg-fluent-content .tg-kanban-filters {
|
||||
.tg-fluent-main-container:has(.fluent-top-navigation:not(.other-view))
|
||||
.tg-fluent-content
|
||||
.tg-kanban-filters {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -637,7 +687,10 @@ button.fluent-new-task-btn:hover {
|
|||
|
||||
/* ===== Collapsed Sidebar (Rail) ===== */
|
||||
.tg-fluent-sidebar-container {
|
||||
transition: width 0.2s ease, min-width 0.2s ease, max-width 0.2s ease;
|
||||
transition:
|
||||
width 0.2s ease,
|
||||
min-width 0.2s ease,
|
||||
max-width 0.2s ease;
|
||||
}
|
||||
|
||||
/* When the sidebar element itself is collapsed */
|
||||
|
|
@ -668,7 +721,9 @@ button.fluent-new-task-btn:hover {
|
|||
border-radius: var(--radius-s);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.fluent-rail-btn:hover {
|
||||
|
|
@ -832,7 +887,9 @@ button.fluent-new-task-btn:hover {
|
|||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-primary);
|
||||
width: calc(100% - 360px);
|
||||
}
|
||||
|
||||
.forecast-task-list {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,38 @@
|
|||
border-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
/* Cycle selector styles */
|
||||
.tg-kanban-cycle-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tg-kanban-cycle-button {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: var(--font-ui-small);
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.tg-kanban-cycle-button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
border-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.tg-kanban-cycle-button:active {
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
.tg-kanban-toggle-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -115,7 +147,7 @@
|
|||
background-color: var(--background-secondary);
|
||||
border-radius: var(--radius-m);
|
||||
height: 100%; /* Fill container height */
|
||||
max-height: 100%; /* Prevent exceeding container */
|
||||
max-height: calc(100vh - 170px); /* Prevent exceeding container */
|
||||
overflow: hidden; /* Hide overflow within the column */
|
||||
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
|
|
@ -175,7 +207,9 @@
|
|||
border: 1px solid var(--background-modifier-border);
|
||||
font-size: var(--font-ui-small);
|
||||
cursor: grab;
|
||||
transition: box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out;
|
||||
transition:
|
||||
box-shadow 0.2s ease-in-out,
|
||||
background-color 0.2s ease-in-out;
|
||||
|
||||
/* Ensure card fits within column and handles content */
|
||||
max-width: 100%; /* Prevent card from exceeding parent width */
|
||||
|
|
@ -313,14 +347,18 @@
|
|||
margin-top: 10px; /* Increased margin */
|
||||
border-top: 2px dashed var(--interactive-accent); /* Indicator */
|
||||
/* padding-top: 20px; */ /* Removed padding */
|
||||
transition: margin-top 0.1s ease-out, border-top 0.1s ease-out; /* Updated transition */
|
||||
transition:
|
||||
margin-top 0.1s ease-out,
|
||||
border-top 0.1s ease-out; /* Updated transition */
|
||||
}
|
||||
|
||||
.tg-kanban-card--drop-indicator-after {
|
||||
margin-bottom: 10px; /* Increased margin */
|
||||
border-bottom: 2px dashed var(--interactive-accent); /* Indicator */
|
||||
/* padding-bottom: 20px; */ /* Removed padding */
|
||||
transition: margin-bottom 0.1s ease-out, border-bottom 0.1s ease-out; /* Updated transition */
|
||||
transition:
|
||||
margin-bottom 0.1s ease-out,
|
||||
border-bottom 0.1s ease-out; /* Updated transition */
|
||||
}
|
||||
|
||||
/* Optional: Style for dropping into an empty column */
|
||||
|
|
@ -335,9 +373,13 @@
|
|||
/* Ensure transitions are smooth when classes are removed */
|
||||
.tg-kanban-card {
|
||||
/* Ensure existing transitions don't conflict, or add base transition */
|
||||
transition: margin 0.1s ease-out, padding 0.1s ease-out,
|
||||
border 0.1s ease-out, transform 0.2s ease-out,
|
||||
box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out; /* Merged transitions */
|
||||
transition:
|
||||
margin 0.1s ease-out,
|
||||
padding 0.1s ease-out,
|
||||
border 0.1s ease-out,
|
||||
transform 0.2s ease-out,
|
||||
box-shadow 0.2s ease-in-out,
|
||||
background-color 0.2s ease-in-out; /* Merged transitions */
|
||||
}
|
||||
|
||||
.drop-target-active {
|
||||
|
|
@ -373,7 +415,9 @@
|
|||
cursor: pointer;
|
||||
font-size: var(--font-ui-small);
|
||||
text-align: left;
|
||||
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
|
||||
transition:
|
||||
background-color 0.2s ease-in-out,
|
||||
color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.tg-kanban-add-card-button:hover {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -680,7 +680,8 @@ export function groupTasksByStatus(
|
|||
const groupMap = new Map<string, Task[]>();
|
||||
|
||||
tasks.forEach((task) => {
|
||||
const status = task.status || "TODO";
|
||||
// task.status is the mark character (e.g., "x", " ", "r", "-")
|
||||
const status = task.status || " ";
|
||||
|
||||
if (!groupMap.has(status)) {
|
||||
groupMap.set(status, []);
|
||||
|
|
@ -688,14 +689,47 @@ export function groupTasksByStatus(
|
|||
groupMap.get(status)!.push(task);
|
||||
});
|
||||
|
||||
// Define status order
|
||||
const statusOrder = ["TODO", "IN_PROGRESS", "WAITING", "DONE", "CANCELLED"];
|
||||
// Build mark order from user's cycle configuration
|
||||
// This ensures marks are sorted according to user's defined cycles
|
||||
const markOrder: string[] = [];
|
||||
|
||||
if (settings?.statusCycles && settings.statusCycles.length > 0) {
|
||||
// Get marks from all enabled cycles, ordered by priority
|
||||
const enabledCycles = settings.statusCycles
|
||||
.filter((c) => c.enabled)
|
||||
.sort((a, b) => a.priority - b.priority);
|
||||
|
||||
for (const cycle of enabledCycles) {
|
||||
for (const statusName of cycle.cycle) {
|
||||
const mark = cycle.marks[statusName];
|
||||
if (mark && !markOrder.includes(mark)) {
|
||||
markOrder.push(mark);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (settings?.taskStatusCycle && settings?.taskStatusMarks) {
|
||||
// Legacy single-cycle mode
|
||||
for (const statusName of settings.taskStatusCycle) {
|
||||
const mark = settings.taskStatusMarks[statusName];
|
||||
if (mark && !markOrder.includes(mark)) {
|
||||
markOrder.push(mark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback order for common marks not in user config
|
||||
const fallbackOrder = [" ", "/", ">", "x", "X", "-", "?", "!"];
|
||||
for (const mark of fallbackOrder) {
|
||||
if (!markOrder.includes(mark)) {
|
||||
markOrder.push(mark);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to TaskGroup array and sort by defined order
|
||||
const groups: TaskGroup[] = [];
|
||||
const sortedKeys = Array.from(groupMap.keys()).sort((a, b) => {
|
||||
const indexA = statusOrder.indexOf(a);
|
||||
const indexB = statusOrder.indexOf(b);
|
||||
const indexA = markOrder.indexOf(a);
|
||||
const indexB = markOrder.indexOf(b);
|
||||
|
||||
// If both are in the order array, sort by index
|
||||
if (indexA !== -1 && indexB !== -1) {
|
||||
|
|
|
|||
71
styles.css
71
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue