feat: support hide weekends in both forecast or events view

This commit is contained in:
Quorafind 2025-06-16 15:11:29 +08:00
parent 325096d4b6
commit f83052749b
19 changed files with 1210 additions and 34 deletions

71
debug-settings.js Normal file
View file

@ -0,0 +1,71 @@
// Debug script to check weekend hiding settings
// Run this in the browser console when the plugin is loaded
function debugWeekendSettings() {
console.log("=== Weekend Hiding Debug ===");
// Try to find the plugin instance
const plugin = app.plugins.plugins['task-progress-bar'];
if (!plugin) {
console.error("Plugin not found!");
return;
}
console.log("Plugin found:", plugin);
console.log("Plugin settings:", plugin.settings);
// Check view configurations
const viewConfigs = plugin.settings.viewConfiguration;
console.log("All view configurations:", viewConfigs);
// Find calendar view config
const calendarConfig = viewConfigs.find(v => v.id === 'calendar');
console.log("Calendar view config:", calendarConfig);
if (calendarConfig && calendarConfig.specificConfig) {
console.log("Calendar specific config:", calendarConfig.specificConfig);
console.log("hideWeekends setting:", calendarConfig.specificConfig.hideWeekends);
} else {
console.log("No calendar specific config found!");
}
// Check DOM elements
const calendarContainers = document.querySelectorAll('.full-calendar-container');
console.log("Found calendar containers:", calendarContainers.length);
calendarContainers.forEach((container, index) => {
console.log(`Container ${index}:`, container);
console.log(`Has hide-weekends class:`, container.classList.contains('hide-weekends'));
const weekdayHeaders = container.querySelectorAll('.calendar-weekday');
console.log(`Weekday headers count:`, weekdayHeaders.length);
const dayCells = container.querySelectorAll('.calendar-day-cell');
console.log(`Day cells count:`, dayCells.length);
// Check for weekend day cells
const weekendCells = Array.from(dayCells).filter(cell => {
const dateStr = cell.getAttribute('data-date');
if (dateStr) {
const date = new Date(dateStr);
const dayOfWeek = date.getDay();
return dayOfWeek === 0 || dayOfWeek === 6; // Sunday or Saturday
}
return false;
});
console.log(`Weekend cells found:`, weekendCells.length);
});
console.log("=== End Debug ===");
}
// Auto-run if in browser console
if (typeof window !== 'undefined') {
debugWeekendSettings();
}
// Export for Node.js if needed
if (typeof module !== 'undefined') {
module.exports = debugWeekendSettings;
}

View file

@ -1,6 +1,6 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testEnvironment: "jsdom",
testMatch: ["**/__tests__/**/*.test.ts"],
moduleNameMapper: {
"^obsidian$": "<rootDir>/src/__mocks__/obsidian.ts",
@ -20,4 +20,5 @@ module.exports = {
],
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
setupFilesAfterEnv: ["<rootDir>/src/test-setup.ts"],
};

226
manual-weekend-test.html Normal file
View file

@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weekend Hiding Test</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.test-container {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.calendar-container {
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
.calendar-weekday-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
background-color: #f8f9fa;
border-bottom: 1px solid #ddd;
}
.calendar-weekday-header.hide-weekends {
grid-template-columns: repeat(5, 1fr);
}
.calendar-weekday {
padding: 10px;
text-align: center;
font-weight: bold;
border-right: 1px solid #ddd;
}
.calendar-weekday:last-child {
border-right: none;
}
.calendar-month-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background-color: #ddd;
}
.calendar-month-grid.hide-weekends {
grid-template-columns: repeat(5, 1fr);
}
.calendar-day-cell {
background: white;
padding: 10px;
min-height: 60px;
border: 1px solid #eee;
display: flex;
flex-direction: column;
}
.calendar-day-number {
font-weight: bold;
margin-bottom: 5px;
}
.test-result {
margin-top: 10px;
padding: 10px;
border-radius: 4px;
font-weight: bold;
}
.test-pass {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.test-fail {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<h1>Weekend Hiding Test</h1>
<div class="test-container">
<h2>Test 1: 7-Day Calendar (Weekends Shown)</h2>
<button onclick="renderCalendar('test1', false)">Render 7-Day Calendar</button>
<div id="test1" class="calendar-container"></div>
<div id="test1-result"></div>
</div>
<div class="test-container">
<h2>Test 2: 5-Day Calendar (Weekends Hidden)</h2>
<button onclick="renderCalendar('test2', true)">Render 5-Day Calendar</button>
<div id="test2" class="calendar-container"></div>
<div id="test2-result"></div>
</div>
<script>
function renderCalendar(containerId, hideWeekends) {
const container = document.getElementById(containerId);
const resultDiv = document.getElementById(containerId + '-result');
// Clear container
container.innerHTML = '';
// Create weekday header
const header = document.createElement('div');
header.className = 'calendar-weekday-header';
if (hideWeekends) {
header.classList.add('hide-weekends');
}
// Weekday names
const allWeekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const workdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
const weekdaysToShow = hideWeekends ? workdays : allWeekdays;
weekdaysToShow.forEach(day => {
const dayEl = document.createElement('div');
dayEl.className = 'calendar-weekday';
dayEl.textContent = day;
header.appendChild(dayEl);
});
container.appendChild(header);
// Create month grid
const grid = document.createElement('div');
grid.className = 'calendar-month-grid';
if (hideWeekends) {
grid.classList.add('hide-weekends');
}
// Generate sample days (first week of January 2024)
// January 1, 2024 is a Monday
const sampleDays = [
{ date: 1, isWeekend: false }, // Mon
{ date: 2, isWeekend: false }, // Tue
{ date: 3, isWeekend: false }, // Wed
{ date: 4, isWeekend: false }, // Thu
{ date: 5, isWeekend: false }, // Fri
{ date: 6, isWeekend: true }, // Sat
{ date: 7, isWeekend: true }, // Sun
{ date: 8, isWeekend: false }, // Mon
{ date: 9, isWeekend: false }, // Tue
{ date: 10, isWeekend: false }, // Wed
{ date: 11, isWeekend: false }, // Thu
{ date: 12, isWeekend: false }, // Fri
{ date: 13, isWeekend: true }, // Sat
{ date: 14, isWeekend: true }, // Sun
];
sampleDays.forEach(day => {
// Skip weekend days if hideWeekends is true
if (hideWeekends && day.isWeekend) {
return;
}
const dayCell = document.createElement('div');
dayCell.className = 'calendar-day-cell';
const dayNumber = document.createElement('div');
dayNumber.className = 'calendar-day-number';
dayNumber.textContent = day.date;
dayCell.appendChild(dayNumber);
grid.appendChild(dayCell);
});
container.appendChild(grid);
// Test results
const headerCells = header.children.length;
const dayCells = grid.children.length;
let testResult = '';
let testPassed = false;
if (hideWeekends) {
testPassed = headerCells === 5 && dayCells <= 10; // Should have 5 headers and only work days
testResult = `Expected: 5 headers, work days only. Got: ${headerCells} headers, ${dayCells} day cells. ${testPassed ? 'PASS' : 'FAIL'}`;
} else {
testPassed = headerCells === 7 && dayCells === 14; // Should have 7 headers and all days
testResult = `Expected: 7 headers, 14 day cells. Got: ${headerCells} headers, ${dayCells} day cells. ${testPassed ? 'PASS' : 'FAIL'}`;
}
resultDiv.innerHTML = `<div class="test-result ${testPassed ? 'test-pass' : 'test-fail'}">${testResult}</div>`;
}
// Auto-run tests on page load
window.onload = function() {
renderCalendar('test1', false);
renderCalendar('test2', true);
};
</script>
</body>
</html>

30
run-weekend-tests.js Normal file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env node
/**
* Script to run weekend hiding tests specifically
*/
const { execSync } = require('child_process');
const path = require('path');
console.log('🧪 Running Weekend Hiding Tests...\n');
try {
// Run the specific test files for weekend hiding
const testCommand = 'npx jest src/components/calendar/views/__tests__/month-view.test.ts src/components/calendar/views/__tests__/year-view.test.ts --verbose';
console.log('Running command:', testCommand);
console.log('─'.repeat(50));
const result = execSync(testCommand, {
stdio: 'inherit',
cwd: process.cwd()
});
console.log('\n✅ All weekend hiding tests passed!');
} catch (error) {
console.error('\n❌ Some tests failed:');
console.error(error.message);
process.exit(1);
}

View file

@ -0,0 +1,61 @@
/**
* DOM helpers for testing Obsidian components
*/
// Extend HTMLElement to include Obsidian-specific methods
declare global {
interface HTMLElement {
empty(): void;
addClass(className: string): void;
removeClass(className: string): void;
createDiv(className?: string): HTMLElement;
createEl(tagName: string, options?: { cls?: string; attr?: Record<string, string>; text?: string }): HTMLElement;
}
}
// Add Obsidian-specific methods to HTMLElement prototype
if (typeof HTMLElement !== 'undefined') {
HTMLElement.prototype.empty = function() {
this.innerHTML = '';
};
HTMLElement.prototype.addClass = function(className: string) {
this.classList.add(className);
};
HTMLElement.prototype.removeClass = function(className: string) {
this.classList.remove(className);
};
HTMLElement.prototype.createDiv = function(className?: string) {
const div = document.createElement('div');
if (className) {
div.className = className;
}
this.appendChild(div);
return div;
};
HTMLElement.prototype.createEl = function(tagName: string, options?: { cls?: string; attr?: Record<string, string>; text?: string }) {
const el = document.createElement(tagName);
if (options?.cls) {
el.className = options.cls;
}
if (options?.attr) {
Object.entries(options.attr).forEach(([key, value]) => {
el.setAttribute(key, value);
});
}
if (options?.text) {
el.textContent = options.text;
}
this.appendChild(el);
return el;
};
}
export {};

View file

@ -305,6 +305,10 @@ function momentFn(input?: any) {
return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
};
(momentFn as any).weekdaysMin = function (localeData?: boolean) {
return ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
};
export const moment = momentFn as any;
// Mock Component class

View file

@ -113,6 +113,7 @@ export interface KanbanColumnConfig {
export interface CalendarSpecificConfig {
viewType: "calendar"; // Discriminator
firstDayOfWeek?: number; // 0=Sun, 1=Mon, ..., 6=Sat; undefined=locale default
hideWeekends?: boolean; // Whether to hide weekend columns/cells in calendar views
}
export interface GanttSpecificConfig {
@ -124,6 +125,7 @@ export interface GanttSpecificConfig {
export interface ForecastSpecificConfig {
viewType: "forecast"; // Discriminator
firstDayOfWeek?: number; // 0=Sun, 1=Mon, ..., 6=Sat; undefined=locale default
hideWeekends?: boolean; // Whether to hide weekend columns/cells in forecast calendar
}
export interface TwoColumnSpecificConfig {
@ -736,6 +738,11 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
hideCompletedAndAbandonedTasks: true,
filterRules: {},
filterBlanks: false,
specificConfig: {
viewType: "forecast",
firstDayOfWeek: undefined, // Use locale default initially
hideWeekends: false, // Show weekends by default
} as ForecastSpecificConfig,
},
{
id: "projects",
@ -789,6 +796,7 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
specificConfig: {
viewType: "calendar",
firstDayOfWeek: undefined, // Use locale default initially
hideWeekends: false, // Show weekends by default
} as CalendarSpecificConfig,
},
{

View file

@ -323,6 +323,30 @@ export class ViewConfigModal extends Modal {
this.checkForChanges();
});
});
// Add weekend hiding toggle for calendar view
new Setting(contentEl)
.setName(t("Hide weekends"))
.setDesc(t("Hide weekend columns (Saturday and Sunday) in calendar views."))
.addToggle((toggle) => {
const currentValue = (this.viewConfig.specificConfig as CalendarSpecificConfig)?.hideWeekends ?? false;
toggle.setValue(currentValue);
toggle.onChange((value) => {
if (
!this.viewConfig.specificConfig ||
this.viewConfig.specificConfig.viewType !== "calendar"
) {
this.viewConfig.specificConfig = {
viewType: "calendar",
firstDayOfWeek: undefined,
hideWeekends: value,
};
} else {
(this.viewConfig.specificConfig as CalendarSpecificConfig).hideWeekends = value;
}
this.checkForChanges();
});
});
} else if (isKanbanView) {
new Setting(contentEl)
.setName(t("Group by"))
@ -761,6 +785,30 @@ export class ViewConfigModal extends Modal {
this.checkForChanges();
});
});
// Add weekend hiding toggle for forecast view
new Setting(contentEl)
.setName(t("Hide weekends"))
.setDesc(t("Hide weekend columns (Saturday and Sunday) in forecast calendar."))
.addToggle((toggle) => {
const currentValue = (this.viewConfig.specificConfig as ForecastSpecificConfig)?.hideWeekends ?? false;
toggle.setValue(currentValue);
toggle.onChange((value) => {
if (
!this.viewConfig.specificConfig ||
this.viewConfig.specificConfig.viewType !== "forecast"
) {
this.viewConfig.specificConfig = {
viewType: "forecast",
firstDayOfWeek: undefined,
hideWeekends: value,
};
} else {
(this.viewConfig.specificConfig as ForecastSpecificConfig).hideWeekends = value;
}
this.checkForChanges();
});
});
}
// Two Column View specific config

View file

@ -0,0 +1,236 @@
import { App, moment } from "obsidian";
import { MonthView } from "../month-view";
import { CalendarEvent } from "../../index";
import TaskProgressBarPlugin from "../../../../index";
import "../../../../__mocks__/dom-helpers";
// Mock dependencies
jest.mock("obsidian", () => ({
App: jest.fn(),
Component: class MockComponent {
addChild = jest.fn();
registerDomEvent = jest.fn();
onunload = jest.fn();
},
moment: require("moment"),
debounce: (fn: Function) => fn,
}));
jest.mock("../../../../index", () => ({
default: jest.fn(),
}));
jest.mock("../../rendering/event-renderer", () => ({
renderCalendarEvent: jest.fn(() => ({
eventEl: document.createElement("div"),
component: { onunload: jest.fn() },
})),
}));
jest.mock("../../../../common/setting-definition", () => ({
getViewSettingOrDefault: jest.fn(),
CalendarSpecificConfig: {},
}));
jest.mock("sortablejs", () => ({
default: {
create: jest.fn(() => ({
destroy: jest.fn(),
})),
},
}));
describe("MonthView Weekend Hiding", () => {
let mockApp: App;
let mockPlugin: TaskProgressBarPlugin;
let containerEl: HTMLElement;
let monthView: MonthView;
beforeEach(() => {
// Setup DOM environment
document.body.innerHTML = "";
containerEl = document.createElement("div");
document.body.appendChild(containerEl);
// Mock app and plugin
mockApp = {} as App;
mockPlugin = {
settings: {
viewConfiguration: [
{
id: "calendar",
specificConfig: {
firstDayOfWeek: 1, // Monday
hideWeekends: true,
},
},
],
},
} as any;
// Create test events
const testEvents: CalendarEvent[] = [
{
id: "test-1",
title: "Test Event 1",
start: moment("2024-01-15").toDate(), // Monday
metadata: {
dueDate: moment("2024-01-15").valueOf(),
},
},
{
id: "test-2",
title: "Test Event 2",
start: moment("2024-01-20").toDate(), // Saturday (weekend)
metadata: {
dueDate: moment("2024-01-20").valueOf(),
},
},
];
// Create MonthView instance
monthView = new MonthView(
mockApp,
mockPlugin,
containerEl,
"calendar",
moment("2024-01-15"), // January 2024
testEvents,
{
onDayClick: jest.fn(),
onEventClick: jest.fn(),
}
);
});
afterEach(() => {
monthView.onunload();
document.body.innerHTML = "";
});
test("should render only 5 weekday headers when hideWeekends is true", () => {
// Render the month view
monthView.render();
// Check weekday headers
const weekdayHeaders = containerEl.querySelectorAll(".calendar-weekday");
expect(weekdayHeaders.length).toBe(5);
// Verify the headers are work days only (Mon-Fri)
const headerTexts = Array.from(weekdayHeaders).map(
(el) => el.textContent
);
expect(headerTexts).not.toContain("Sat");
expect(headerTexts).not.toContain("Sun");
expect(headerTexts).toContain("Mon");
expect(headerTexts).toContain("Tue");
expect(headerTexts).toContain("Wed");
expect(headerTexts).toContain("Thu");
expect(headerTexts).toContain("Fri");
});
test("should have hide-weekends CSS class when hideWeekends is true", () => {
monthView.render();
expect(containerEl.classList.contains("hide-weekends")).toBe(true);
});
test("should not create day cells for weekend dates", () => {
monthView.render();
// Get all day cells
const dayCells = containerEl.querySelectorAll(".calendar-day-cell");
// Check that no weekend dates are created
Array.from(dayCells).forEach((cell) => {
const dateStr = cell.getAttribute("data-date");
if (dateStr) {
const date = moment(dateStr);
const dayOfWeek = date.day();
// Should not be Saturday (6) or Sunday (0)
expect(dayOfWeek).not.toBe(0);
expect(dayOfWeek).not.toBe(6);
}
});
});
test("should render only work days in a typical week", () => {
monthView.render();
// Get all day cells for a specific week
const dayCells = containerEl.querySelectorAll(".calendar-day-cell");
const datesInView = Array.from(dayCells).map((cell) =>
cell.getAttribute("data-date")
);
// Find a complete work week (e.g., Jan 15-19, 2024 is Mon-Fri)
const workWeekDates = [
"2024-01-15", // Monday
"2024-01-16", // Tuesday
"2024-01-17", // Wednesday
"2024-01-18", // Thursday
"2024-01-19", // Friday
];
// All work days should be present
workWeekDates.forEach((date) => {
expect(datesInView).toContain(date);
});
// Weekend days should not be present
const weekendDates = [
"2024-01-13", // Saturday
"2024-01-14", // Sunday
"2024-01-20", // Saturday
"2024-01-21", // Sunday
];
weekendDates.forEach((date) => {
expect(datesInView).not.toContain(date);
});
});
test("should have correct grid layout with 5 columns when weekends hidden", () => {
monthView.render();
// Check that the container has the hide-weekends class
expect(containerEl.classList.contains("hide-weekends")).toBe(true);
// The CSS should set grid-template-columns to repeat(5, 1fr)
// We can't directly test CSS, but we can verify the class is applied
const monthGrid = containerEl.querySelector(".calendar-month-grid");
expect(monthGrid).toBeTruthy();
const weekdayHeader = containerEl.querySelector(
".calendar-weekday-header"
);
expect(weekdayHeader).toBeTruthy();
});
test("should render 7 days when hideWeekends is false", () => {
// Update plugin settings to show weekends
mockPlugin.settings.viewConfiguration[0].specificConfig.hideWeekends = false;
// Create new instance with weekends enabled
const monthViewWithWeekends = new MonthView(
mockApp,
mockPlugin,
containerEl,
"calendar",
moment("2024-01-15"),
[],
{}
);
monthViewWithWeekends.render();
// Should have 7 weekday headers
const weekdayHeaders = containerEl.querySelectorAll(".calendar-weekday");
expect(weekdayHeaders.length).toBe(7);
// Should not have hide-weekends class
expect(containerEl.classList.contains("hide-weekends")).toBe(false);
monthViewWithWeekends.onunload();
});
});

View file

@ -0,0 +1,256 @@
import { App, moment } from "obsidian";
import { YearView } from "../year-view";
import { CalendarEvent } from "../../index";
import TaskProgressBarPlugin from "../../../../index";
import "../../../../__mocks__/dom-helpers";
// Mock dependencies
jest.mock("obsidian", () => ({
App: jest.fn(),
Component: class MockComponent {
addChild = jest.fn();
registerDomEvent = jest.fn();
onunload = jest.fn();
},
moment: require("moment"),
debounce: (fn: Function) => fn,
}));
jest.mock("../../../../index", () => ({
default: jest.fn(),
}));
jest.mock("../../../../common/setting-definition", () => ({
getViewSettingOrDefault: jest.fn(() => ({
specificConfig: {
firstDayOfWeek: 1, // Monday
hideWeekends: true,
},
})),
CalendarSpecificConfig: {},
}));
describe("YearView Weekend Hiding", () => {
let mockApp: App;
let mockPlugin: TaskProgressBarPlugin;
let containerEl: HTMLElement;
let yearView: YearView;
beforeEach(() => {
// Setup DOM environment
document.body.innerHTML = "";
containerEl = document.createElement("div");
document.body.appendChild(containerEl);
// Mock app and plugin
mockApp = {} as App;
mockPlugin = {
settings: {
viewConfiguration: [
{
id: "calendar",
specificConfig: {
firstDayOfWeek: 1, // Monday
hideWeekends: true,
},
},
],
},
} as any;
// Create test events
const testEvents: CalendarEvent[] = [
{
id: "test-1",
title: "Test Event 1",
start: moment("2024-01-15").toDate(), // Monday
metadata: {
dueDate: moment("2024-01-15").valueOf(),
},
},
{
id: "test-2",
title: "Test Event 2",
start: moment("2024-06-15").toDate(), // Saturday (weekend)
metadata: {
dueDate: moment("2024-06-15").valueOf(),
},
},
];
// Create YearView instance
yearView = new YearView(
mockApp,
mockPlugin,
containerEl,
moment("2024-01-01"), // Year 2024
testEvents,
{
onDayClick: jest.fn(),
onMonthClick: jest.fn(),
}
);
});
afterEach(() => {
yearView.onunload();
document.body.innerHTML = "";
});
test("should render only 5 weekday headers in mini-months when hideWeekends is true", () => {
// Render the year view
yearView.render();
// Check mini-month weekday headers
const miniWeekdayHeaders = containerEl.querySelectorAll(".mini-weekday");
// Each mini-month should have 5 weekday headers (one for each month)
// Since we have 12 months, we should have 12 * 5 = 60 mini-weekday headers
expect(miniWeekdayHeaders.length).toBe(60);
// Verify no weekend day names are present
const headerTexts = Array.from(miniWeekdayHeaders).map(
(el) => el.textContent
);
// Should not contain Saturday or Sunday abbreviations
expect(headerTexts.filter(text => text === "Sa" || text === "Su")).toHaveLength(0);
// Should contain work day abbreviations
expect(headerTexts.filter(text => text === "Mo")).toHaveLength(12); // 12 months
expect(headerTexts.filter(text => text === "Tu")).toHaveLength(12);
expect(headerTexts.filter(text => text === "We")).toHaveLength(12);
expect(headerTexts.filter(text => text === "Th")).toHaveLength(12);
expect(headerTexts.filter(text => text === "Fr")).toHaveLength(12);
});
test("should have hide-weekends CSS class when hideWeekends is true", () => {
yearView.render();
expect(containerEl.classList.contains("hide-weekends")).toBe(true);
});
test("should not create day cells for weekend dates in mini-months", () => {
yearView.render();
// Get all mini-day cells from all mini-months
const miniDayCells = containerEl.querySelectorAll(".mini-day-cell");
// Check that no weekend dates are created
Array.from(miniDayCells).forEach((cell) => {
const dateStr = cell.getAttribute("data-date");
if (dateStr) {
const date = moment(dateStr);
const dayOfWeek = date.day();
// Should not be Saturday (6) or Sunday (0)
expect(dayOfWeek).not.toBe(0);
expect(dayOfWeek).not.toBe(6);
}
});
});
test("should render 12 mini-months", () => {
yearView.render();
// Should have 12 mini-month containers
const miniMonths = containerEl.querySelectorAll(".calendar-mini-month");
expect(miniMonths.length).toBe(12);
// Each should have a header with month name
const monthHeaders = containerEl.querySelectorAll(".mini-month-header");
expect(monthHeaders.length).toBe(12);
// Verify month names are present
const monthNames = Array.from(monthHeaders).map(
(el) => el.textContent
);
expect(monthNames).toContain("January");
expect(monthNames).toContain("December");
});
test("should render only work days in January 2024 mini-month", () => {
yearView.render();
// Get the first mini-month (January)
const miniMonths = containerEl.querySelectorAll(".calendar-mini-month");
const januaryMonth = miniMonths[0];
// Get day cells from January mini-month
const januaryDayCells = januaryMonth.querySelectorAll(".mini-day-cell");
const datesInJanuary = Array.from(januaryDayCells).map((cell) =>
cell.getAttribute("data-date")
);
// Check that work days are present and weekend days are not
// January 1, 2024 is a Monday
const workDays = [
"2024-01-01", // Monday
"2024-01-02", // Tuesday
"2024-01-03", // Wednesday
"2024-01-04", // Thursday
"2024-01-05", // Friday
];
const weekendDays = [
"2024-01-06", // Saturday
"2024-01-07", // Sunday
];
// Work days should be present
workDays.forEach((date) => {
expect(datesInJanuary).toContain(date);
});
// Weekend days should not be present
weekendDays.forEach((date) => {
expect(datesInJanuary).not.toContain(date);
});
});
test("should have correct grid layout with 5 columns for mini-months when weekends hidden", () => {
yearView.render();
// Check that the container has the hide-weekends class
expect(containerEl.classList.contains("hide-weekends")).toBe(true);
// All mini-month grids should be present
const miniMonthGrids = containerEl.querySelectorAll(".mini-month-grid");
expect(miniMonthGrids.length).toBe(12);
// Each mini-month should have weekday headers
const miniWeekdayHeaders = containerEl.querySelectorAll(".mini-weekday-header");
expect(miniWeekdayHeaders.length).toBe(12);
});
test("should render 7 days when hideWeekends is false", () => {
// Mock getViewSettingOrDefault to return hideWeekends: false
const { getViewSettingOrDefault } = require("../../../../common/setting-definition");
getViewSettingOrDefault.mockReturnValue({
specificConfig: {
firstDayOfWeek: 1,
hideWeekends: false,
},
});
// Create new instance with weekends enabled
const yearViewWithWeekends = new YearView(
mockApp,
mockPlugin,
containerEl,
moment("2024-01-01"),
[],
{}
);
yearViewWithWeekends.render();
// Should have 7 * 12 = 84 weekday headers (7 per month, 12 months)
const miniWeekdayHeaders = containerEl.querySelectorAll(".mini-weekday");
expect(miniWeekdayHeaders.length).toBe(84);
// Should not have hide-weekends class
expect(containerEl.classList.contains("hide-weekends")).toBe(false);
yearViewWithWeekends.onunload();
});
});

View file

@ -46,11 +46,12 @@ export class MonthView extends CalendarViewComponent {
}
render(): void {
// Get view settings, including the first day of the week override
// Get view settings, including the first day of the week override and weekend hiding
const viewConfig = this.plugin.settings.viewConfiguration.find(
(v) => v.id === this.currentViewId
)?.specificConfig as CalendarSpecificConfig; // Assuming 'calendar' view for settings lookup, adjust if needed
const firstDayOfWeekSetting = viewConfig?.firstDayOfWeek;
const hideWeekends = viewConfig?.hideWeekends ?? false;
// Default to Sunday (0) if the setting is undefined, following 0=Sun, 1=Mon, ..., 6=Sat
const effectiveFirstDay =
firstDayOfWeekSetting === undefined ? 0 : firstDayOfWeekSetting;
@ -63,17 +64,50 @@ export class MonthView extends CalendarViewComponent {
// Calculate grid end based on the week containing the end of the month, adjusted for the effective first day
let gridEnd = endOfMonth.clone().weekday(effectiveFirstDay + 6); // moment handles wrapping correctly
// Ensure grid covers at least 6 weeks (42 days) for consistent layout
// This logic should still work fine with custom start/end days
if (gridEnd.diff(gridStart, "days") + 1 < 42) {
// Add full weeks until at least 42 days are covered
const daysToAdd = 42 - (gridEnd.diff(gridStart, "days") + 1);
gridEnd.add(daysToAdd, "days");
// Adjust grid coverage based on whether weekends are hidden
if (hideWeekends) {
// When weekends are hidden, we need fewer days to fill the grid
// Calculate how many work days we need (approximately 6 weeks * 5 work days = 30 days)
let workDaysCount = 0;
let tempIter = gridStart.clone();
// Count existing work days in the current range
while (tempIter.isSameOrBefore(gridEnd, "day")) {
const isWeekend = tempIter.day() === 0 || tempIter.day() === 6;
if (!isWeekend) {
workDaysCount++;
}
tempIter.add(1, "day");
}
// Ensure we have at least 30 work days (6 weeks * 5 days) for consistent layout
while (workDaysCount < 30) {
gridEnd.add(1, "day");
const isWeekend = gridEnd.day() === 0 || gridEnd.day() === 6;
if (!isWeekend) {
workDaysCount++;
}
}
} else {
// Original logic for when weekends are shown
// Ensure grid covers at least 6 weeks (42 days) for consistent layout
if (gridEnd.diff(gridStart, "days") + 1 < 42) {
// Add full weeks until at least 42 days are covered
const daysToAdd = 42 - (gridEnd.diff(gridStart, "days") + 1);
gridEnd.add(daysToAdd, "days");
}
}
this.containerEl.empty();
this.containerEl.addClass("view-month"); // Add class for styling
// Add hide-weekends class if weekend hiding is enabled
if (hideWeekends) {
this.containerEl.addClass("hide-weekends");
} else {
this.containerEl.removeClass("hide-weekends");
}
// 2. Add weekday headers, rotated according to effective first day
const headerRow = this.containerEl.createDiv("calendar-weekday-header");
const weekdays = moment.weekdaysShort(true); // Gets locale-aware short weekdays
@ -81,7 +115,17 @@ export class MonthView extends CalendarViewComponent {
...weekdays.slice(effectiveFirstDay),
...weekdays.slice(0, effectiveFirstDay),
];
rotatedWeekdays.forEach((day) => {
// Filter out weekends if hideWeekends is enabled
const filteredWeekdays = hideWeekends
? rotatedWeekdays.filter((_, index) => {
// Calculate the actual day of week for this header position
const dayOfWeek = (effectiveFirstDay + index) % 7;
return dayOfWeek !== 0 && dayOfWeek !== 6; // Exclude Sunday (0) and Saturday (6)
})
: rotatedWeekdays;
filteredWeekdays.forEach((day) => {
const weekdayEl = headerRow.createDiv("calendar-weekday");
weekdayEl.textContent = day;
});
@ -92,6 +136,14 @@ export class MonthView extends CalendarViewComponent {
let currentDayIter = gridStart.clone();
while (currentDayIter.isSameOrBefore(gridEnd, "day")) {
const isWeekend = currentDayIter.day() === 0 || currentDayIter.day() === 6; // Sunday or Saturday
// Skip weekend days if hideWeekends is enabled
if (hideWeekends && isWeekend) {
currentDayIter.add(1, "day");
continue;
}
const cell = gridContainer.createEl("div", {
cls: "calendar-day-cell",
attr: {
@ -113,9 +165,9 @@ export class MonthView extends CalendarViewComponent {
if (currentDayIter.isSame(moment(), "day")) {
cell.addClass("is-today");
}
// Weekend check might need adjustment depending on visual definition of weekend with custom start day
if (currentDayIter.day() === 0 || currentDayIter.day() === 6) {
// Sunday or Saturday
// Note: We don't add is-weekend class when hideWeekends is enabled
// because weekend cells are not created at all
if (!hideWeekends && isWeekend) {
cell.addClass("is-weekend");
}

View file

@ -38,7 +38,7 @@ export class WeekView extends CalendarViewComponent {
}
render(): void {
// Get view settings, including the first day of the week override
// Get view settings, including the first day of the week override and weekend hiding
const viewConfig = getViewSettingOrDefault(
this.plugin,
this.currentViewId
@ -47,6 +47,7 @@ export class WeekView extends CalendarViewComponent {
const firstDayOfWeekSetting = (
viewConfig.specificConfig as CalendarSpecificConfig
).firstDayOfWeek;
const hideWeekends = (viewConfig.specificConfig as CalendarSpecificConfig)?.hideWeekends ?? false;
// Default to Sunday (0) if the setting is undefined, following 0=Sun, 1=Mon, ..., 6=Sat
const effectiveFirstDay =
firstDayOfWeekSetting === undefined ? 0 : firstDayOfWeekSetting;
@ -58,6 +59,13 @@ export class WeekView extends CalendarViewComponent {
this.containerEl.empty();
this.containerEl.addClass("view-week");
// Add hide-weekends class if weekend hiding is enabled
if (hideWeekends) {
this.containerEl.addClass("hide-weekends");
} else {
this.containerEl.removeClass("hide-weekends");
}
// 1. Render Header Row (Days of the week + Dates)
const headerRow = this.containerEl.createDiv("calendar-week-header");
const dayHeaderCells: { [key: string]: HTMLElement } = {};
@ -69,14 +77,32 @@ export class WeekView extends CalendarViewComponent {
...weekdays.slice(effectiveFirstDay),
...weekdays.slice(0, effectiveFirstDay),
];
// Filter out weekends if hideWeekends is enabled
const filteredWeekdays = hideWeekends
? rotatedWeekdays.filter((_, index) => {
// Calculate the actual day of week for this header position
const dayOfWeek = (effectiveFirstDay + index) % 7;
return dayOfWeek !== 0 && dayOfWeek !== 6; // Exclude Sunday (0) and Saturday (6)
})
: rotatedWeekdays;
let dayIndex = 0;
while (currentDayIter.isSameOrBefore(endOfWeek, "day")) {
const isWeekend = currentDayIter.day() === 0 || currentDayIter.day() === 6; // Sunday or Saturday
// Skip weekend days if hideWeekends is enabled
if (hideWeekends && isWeekend) {
currentDayIter.add(1, "day");
continue; // Don't increment dayIndex for skipped days
}
const dateStr = currentDayIter.format("YYYY-MM-DD");
const headerCell = headerRow.createDiv("calendar-header-cell");
dayHeaderCells[dateStr] = headerCell; // Store header cell if needed
const weekdayEl = headerCell.createDiv("calendar-weekday");
weekdayEl.textContent = rotatedWeekdays[dayIndex % 7]; // Use rotated weekday name
weekdayEl.textContent = filteredWeekdays[dayIndex]; // Use filtered weekday names
const dayNumEl = headerCell.createDiv("calendar-day-number");
dayNumEl.textContent = currentDayIter.format("D"); // Date number
@ -96,6 +122,14 @@ export class WeekView extends CalendarViewComponent {
currentDayIter = startOfWeek.clone();
while (currentDayIter.isSameOrBefore(endOfWeek, "day")) {
const isWeekend = currentDayIter.day() === 0 || currentDayIter.day() === 6; // Sunday or Saturday
// Skip weekend days if hideWeekends is enabled
if (hideWeekends && isWeekend) {
currentDayIter.add(1, "day");
continue;
}
const dateStr = currentDayIter.format("YYYY-MM-DD");
const dayCell = weekGrid.createEl("div", {
cls: "calendar-day-column",
@ -110,7 +144,7 @@ export class WeekView extends CalendarViewComponent {
if (currentDayIter.isSame(moment(), "day")) {
dayCell.addClass("is-today"); // Apply to the main day cell
}
if (currentDayIter.day() === 0 || currentDayIter.day() === 6) {
if (isWeekend) {
// This weekend check is based on Sun/Sat, might need adjustment if start day changes weekend definition visually
dayCell.addClass("is-weekend"); // Apply to the main day cell
}

View file

@ -37,6 +37,7 @@ export class YearView extends CalendarViewComponent {
const year = this.currentDate.year();
this.containerEl.empty();
this.containerEl.addClass("view-year");
console.log(
`YearView: Rendering year ${year}. Total events received: ${this.events.length}`
); // Log total events
@ -68,6 +69,14 @@ export class YearView extends CalendarViewComponent {
const firstDayOfWeekSetting = (
viewConfig.specificConfig as CalendarSpecificConfig
).firstDayOfWeek;
const hideWeekends = (viewConfig.specificConfig as CalendarSpecificConfig)?.hideWeekends ?? false;
// Add hide-weekends class if weekend hiding is enabled
if (hideWeekends) {
this.containerEl.addClass("hide-weekends");
} else {
this.containerEl.removeClass("hide-weekends");
}
// Default to Sunday (0) if the setting is undefined, following 0=Sun, 1=Mon, ..., 6=Sat
const effectiveFirstDay =
firstDayOfWeekSetting === undefined ? 0 : firstDayOfWeekSetting;
@ -111,7 +120,8 @@ export class YearView extends CalendarViewComponent {
monthBody,
monthMoment,
daysWithEvents,
effectiveFirstDay
effectiveFirstDay,
hideWeekends
);
}
@ -169,7 +179,8 @@ export class YearView extends CalendarViewComponent {
container: HTMLElement,
monthMoment: moment.Moment,
daysWithEvents: Set<number>,
effectiveFirstDay: number // Pass the effective first day
effectiveFirstDay: number, // Pass the effective first day
hideWeekends: boolean // Pass the weekend hiding setting
) {
container.empty(); // Clear placeholder
container.addClass("mini-month-grid");
@ -181,25 +192,67 @@ export class YearView extends CalendarViewComponent {
...weekdays.slice(effectiveFirstDay),
...weekdays.slice(0, effectiveFirstDay),
];
rotatedWeekdays.forEach((day) => {
// Filter out weekends if hideWeekends is enabled
const filteredWeekdays = hideWeekends
? rotatedWeekdays.filter((_, index) => {
// Calculate the actual day of week for this header position
const dayOfWeek = (effectiveFirstDay + index) % 7;
return dayOfWeek !== 0 && dayOfWeek !== 6; // Exclude Sunday (0) and Saturday (6)
})
: rotatedWeekdays;
filteredWeekdays.forEach((day) => {
headerRow.createDiv("mini-weekday").textContent = day;
});
// Calculate grid boundaries using effective first day
const monthStart = monthMoment.clone().startOf("month");
const daysToSubtractStart =
(monthStart.weekday() - effectiveFirstDay + 7) % 7;
const gridStart = monthStart
.clone()
.subtract(daysToSubtractStart, "days");
const monthEnd = monthMoment.clone().endOf("month");
const daysToAddEnd =
(effectiveFirstDay + 6 - monthEnd.weekday() + 7) % 7;
const gridEnd = monthEnd.clone().add(daysToAddEnd, "days");
let gridStart: moment.Moment;
let gridEnd: moment.Moment;
if (hideWeekends) {
// When weekends are hidden, adjust grid to start and end on work days
// Find the first work day of the week containing the start of month
gridStart = monthStart.clone();
const daysToSubtractStart = (monthStart.weekday() - effectiveFirstDay + 7) % 7;
gridStart.subtract(daysToSubtractStart, "days");
// Ensure gridStart is not a weekend
while (gridStart.day() === 0 || gridStart.day() === 6) {
gridStart.add(1, "day");
}
// Find the last work day of the week containing the end of month
gridEnd = monthEnd.clone();
const daysToAddEnd = (effectiveFirstDay + 4 - monthEnd.weekday() + 7) % 7; // 4 = Friday in work week
gridEnd.add(daysToAddEnd, "days");
// Ensure gridEnd is not a weekend
while (gridEnd.day() === 0 || gridEnd.day() === 6) {
gridEnd.subtract(1, "day");
}
} else {
// Original logic for when weekends are shown
const daysToSubtractStart = (monthStart.weekday() - effectiveFirstDay + 7) % 7;
gridStart = monthStart.clone().subtract(daysToSubtractStart, "days");
const daysToAddEnd = (effectiveFirstDay + 6 - monthEnd.weekday() + 7) % 7;
gridEnd = monthEnd.clone().add(daysToAddEnd, "days");
}
let currentDayIter = gridStart.clone();
while (currentDayIter.isSameOrBefore(gridEnd, "day")) {
const isWeekend = currentDayIter.day() === 0 || currentDayIter.day() === 6; // Sunday or Saturday
// Skip weekend days if hideWeekends is enabled
if (hideWeekends && isWeekend) {
currentDayIter.add(1, "day");
continue;
}
const cell = container.createEl("div", {
cls: "mini-day-cell",
attr: {

View file

@ -60,6 +60,11 @@ export class CalendarComponent extends Component {
cls: "mini-calendar-container",
});
// Add hide-weekends class if weekend hiding is enabled
if (!this.options.showWeekends) {
this.containerEl.addClass("hide-weekends");
}
// Create header with navigation
this.createCalendarHeader();
@ -142,14 +147,19 @@ export class CalendarComponent extends Component {
}
}
// Filter out weekend headers if showWeekends is false
const filteredDayNames = this.options.showWeekends
? sortedDayNames
: sortedDayNames.filter(day => day !== "Sat" && day !== "Sun");
// Add day header cells
sortedDayNames.forEach((day) => {
filteredDayNames.forEach((day) => {
const dayHeaderEl = this.calendarGridEl.createDiv({
cls: "calendar-day-header",
text: day,
});
// Highlight weekend headers
// Highlight weekend headers (only if they're shown)
if (
(day === "Sat" || day === "Sun") &&
!this.options.showWeekends
@ -263,6 +273,12 @@ export class CalendarComponent extends Component {
isFuture: boolean,
isThisMonth: boolean = false
) {
// Skip weekend days if showWeekends is false
const isWeekend = date.getDay() === 0 || date.getDay() === 6; // Sunday or Saturday
if (!this.options.showWeekends && isWeekend) {
return; // Skip creating this day
}
// Filter tasks for this day
const dayTasks = this.getTasksForDate(date);

View file

@ -6,7 +6,7 @@ import {
setIcon,
} from "obsidian";
import { Task } from "../../types/task";
import { CalendarComponent } from "./calendar";
import { CalendarComponent, CalendarOptions } from "./calendar";
import { TaskListItemComponent } from "./listItem";
import { t } from "../../translations/helper";
import "../../styles/forecast.css";
@ -269,11 +269,20 @@ export class ForecastComponent extends Component {
});
// Create and initialize calendar component
const forecastConfig = this.plugin.settings.viewConfiguration.find(
(view) => view.id === "forecast"
)?.specificConfig as ForecastSpecificConfig;
// Convert ForecastSpecificConfig to CalendarOptions
const calendarOptions: Partial<CalendarOptions> = {
firstDayOfWeek: forecastConfig?.firstDayOfWeek ?? 0,
showWeekends: !(forecastConfig?.hideWeekends ?? false), // Invert hideWeekends to showWeekends
showTaskCounts: true,
};
this.calendarComponent = new CalendarComponent(
this.calendarContainerEl,
this.plugin.settings.viewConfiguration.find(
(view) => view.id === "forecast"
)?.specificConfig as ForecastSpecificConfig
calendarOptions
);
this.addChild(this.calendarComponent);
this.calendarComponent.load();

View file

@ -88,6 +88,14 @@
color: var(--text-accent);
}
/* Adjust grid layout when weekends are hidden in mini calendar */
.task-genius-view .mini-calendar-container.hide-weekends .calendar-grid {
grid-template-columns: repeat(5, 1fr); /* 5 columns instead of 7 when weekends hidden */
}
/* Note: Weekend elements are not created when hideWeekends is enabled,
so no hiding rules are needed. The grid layout adjustments above are sufficient. */
.task-genius-view .mini-calendar-container .calendar-day {
aspect-ratio: 1;
border-radius: 4px;

View file

@ -409,6 +409,30 @@
background-color: var(--background-secondary);
}
/* Adjust grid layouts when weekends are hidden */
.full-calendar-container .calendar-view-container.hide-weekends .calendar-weekday-header {
grid-template-columns: repeat(5, 1fr) !important; /* 5 columns instead of 7 when weekends hidden */
}
.full-calendar-container .calendar-view-container.hide-weekends .calendar-month-grid {
grid-template-columns: repeat(5, 1fr) !important; /* 5 columns instead of 7 when weekends hidden */
}
.full-calendar-container .calendar-view-container.hide-weekends .calendar-week-header {
grid-template-columns: repeat(5, 1fr) !important; /* 5 columns instead of 7 when weekends hidden */
}
.full-calendar-container .calendar-view-container.hide-weekends .calendar-week-grid {
grid-template-columns: repeat(5, 1fr) !important; /* 5 columns instead of 7 when weekends hidden */
}
.full-calendar-container .calendar-view-container.hide-weekends .mini-month-grid {
grid-template-columns: repeat(5, 1fr) !important; /* 5 columns instead of 7 when weekends hidden */
}
/* Note: Weekend elements are not created when hideWeekends is enabled,
so no hiding rules are needed. The grid layout adjustments above are sufficient. */
.full-calendar-container .calendar-day-events-container {
/* Renamed class */
/* This container might not be strictly necessary if .calendar-day-column uses flex */

21
src/test-setup.ts Normal file
View file

@ -0,0 +1,21 @@
/**
* Test setup file for Jest
*/
import './__mocks__/dom-helpers';
// jsdom is already set up by jest-environment-jsdom
// Just need to ensure our DOM helpers are loaded
// Mock console methods to reduce noise in tests
global.console = {
...console,
log: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
// Setup performance mock
global.performance = {
now: jest.fn(() => Date.now()),
} as any;

File diff suppressed because one or more lines are too long