Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions PR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# feat/ui-tweaks

## Task card affordances and visible-property inheritance

This change set refines task card affordances (clear click cues) and ensures subtasks inherit the parent card’s visible properties so metadata stays consistent across levels.

Examples (illustrative):

- Inline task cards show a pointer on the title to clarify clickability.
- Project cards keep the chevron visible when it appears on the right.
- Subtasks mirror the parent card’s visible properties.

## Changelog

- Add pointer cursor cues to clickable task card controls and inline titles.
- Mark project task cards and keep right-side chevrons visible.
- Persist resolved visible properties on cards and apply them to subtask rendering.
- Respect an explicitly empty visible-properties list (do not fall back to defaults).

## Tests

- `npm run i18n:sync`
- `npm run lint` (warnings only; matches `main`)
- `npm run typecheck`
- `npm run test:ci -- --verbose` (fails: `due-date-timezone-inconsistency` test, same as `main`)
- `./node_modules/.bin/jest tests/unit/ui/TaskCard.test.ts --runInBand`
- `npm run test:integration`
- `npm run test:performance` (no tests found)
- `npm run build` (missing OAuth IDs warning, same as `main`)
- `npm run test:build`
82 changes: 63 additions & 19 deletions src/ui/TaskCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,37 @@ function getDefaultVisibleProperties(plugin: TaskNotesPlugin): string[] {
return convertInternalToUserProperties(internalDefaults, plugin);
}

function resolveVisibleProperties(
visibleProperties: string[] | undefined,
plugin: TaskNotesPlugin
): string[] {
if (visibleProperties !== undefined) {
return visibleProperties;
}

if (plugin.settings.defaultVisibleProperties) {
return convertInternalToUserProperties(plugin.settings.defaultVisibleProperties, plugin);
}

return getDefaultVisibleProperties(plugin);
}

function getSubtaskVisibleProperties(card: HTMLElement, plugin: TaskNotesPlugin): string[] {
const raw = card?.dataset?.visibleProperties;
if (raw) {
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed) && parsed.length > 0) {
return parsed;
}
} catch (error) {
console.warn("Failed to parse visibleProperties from card dataset", error);
}
}

return resolveVisibleProperties(undefined, plugin);
}

/**
* Property value extractors for better type safety and error handling
*/
Expand Down Expand Up @@ -1333,6 +1364,7 @@ export function createTaskCard(
const todayLocal = new Date();
return new Date(Date.UTC(todayLocal.getFullYear(), todayLocal.getMonth(), todayLocal.getDate()));
})();
const resolvedVisibleProperties = resolveVisibleProperties(visibleProperties, plugin);

// Determine effective status for recurring tasks
const effectiveStatus = task.recurrence
Expand All @@ -1345,6 +1377,7 @@ export function createTaskCard(
// Main container with BEM class structure
// Use span for inline layout to ensure proper inline flow in CodeMirror
const card = document.createElement(layout === "inline" ? "span" : "div");
card.dataset.visibleProperties = JSON.stringify(resolvedVisibleProperties);

// Store task path for circular reference detection
(card as any)._taskPath = task.path;
Expand All @@ -1357,6 +1390,7 @@ export function createTaskCard(
? task.skipped_instances?.includes(formatDateForStorage(targetDate)) || false // Direct check of skipped_instances
: false; // Only recurring tasks can have skipped instances
const isRecurring = !!task.recurrence;
const isProject = plugin.projectSubtasksService?.isTaskUsedAsProjectSync(task.path) || false;

// Build BEM class names
const cardClasses = ["task-card"];
Expand All @@ -1372,6 +1406,7 @@ export function createTaskCard(
if (task.archived) cardClasses.push("task-card--archived");
if (isActivelyTracked) cardClasses.push("task-card--actively-tracked");
if (isRecurring) cardClasses.push("task-card--recurring");
if (isProject) cardClasses.push("task-card--project");

// Add priority modifier
if (task.priority) {
Expand Down Expand Up @@ -1425,8 +1460,7 @@ export function createTaskCard(
let statusDot: HTMLElement | null = null;
const shouldShowStatus =
!opts.hideStatusIndicator &&
(!visibleProperties ||
visibleProperties.some((prop) => isPropertyForField(prop, "status", plugin)));
resolvedVisibleProperties.some((prop) => isPropertyForField(prop, "status", plugin));
if (shouldShowStatus) {
statusDot = mainRow.createEl("span", { cls: "task-card__status-dot" });
if (statusConfig) {
Expand All @@ -1451,8 +1485,7 @@ export function createTaskCard(

// Priority indicator dot (conditional based on visible properties)
const shouldShowPriority =
!visibleProperties ||
visibleProperties.some((prop) => isPropertyForField(prop, "priority", plugin));
resolvedVisibleProperties.some((prop) => isPropertyForField(prop, "priority", plugin));
if (task.priority && priorityConfig && shouldShowPriority) {
const priorityDot = mainRow.createEl("span", {
cls: "task-card__priority-dot",
Expand Down Expand Up @@ -1584,11 +1617,7 @@ export function createTaskCard(
const metadataElements: HTMLElement[] = [];

// Get properties to display
const propertiesToShow =
visibleProperties ||
(plugin.settings.defaultVisibleProperties
? convertInternalToUserProperties(plugin.settings.defaultVisibleProperties, plugin)
: getDefaultVisibleProperties(plugin));
const propertiesToShow = resolvedVisibleProperties;

// Render each visible property
for (const propertyId of propertiesToShow) {
Expand Down Expand Up @@ -1785,6 +1814,7 @@ export function updateTaskCard(
const todayLocal = new Date();
return new Date(Date.UTC(todayLocal.getFullYear(), todayLocal.getMonth(), todayLocal.getDate()));
})();
const resolvedVisibleProperties = resolveVisibleProperties(visibleProperties, plugin);

// Update effective status
const effectiveStatus = task.recurrence
Expand Down Expand Up @@ -1827,6 +1857,7 @@ export function updateTaskCard(
}

element.className = cardClasses.join(" ");
element.dataset.visibleProperties = JSON.stringify(resolvedVisibleProperties);
element.dataset.status = effectiveStatus;

// Get the main row container
Expand Down Expand Up @@ -1858,21 +1889,33 @@ export function updateTaskCard(

// Update status dot (conditional based on visible properties)
const shouldShowStatus =
!visibleProperties ||
visibleProperties.some((prop) => isPropertyForField(prop, "status", plugin));
!opts.hideStatusIndicator &&
resolvedVisibleProperties.some((prop) => isPropertyForField(prop, "status", plugin));
const statusDot = element.querySelector(".task-card__status-dot") as HTMLElement;

if (shouldShowStatus) {
if (statusDot) {
// Update existing dot
if (statusConfig) {
statusDot.style.borderColor = statusConfig.color;
if (statusConfig.icon) {
statusDot.addClass("task-card__status-dot--icon");
statusDot.empty();
setIcon(statusDot, statusConfig.icon);
} else {
statusDot.removeClass("task-card__status-dot--icon");
statusDot.empty();
}
}
} else if (mainRow) {
// Add missing dot
const newStatusDot = mainRow.createEl("span", { cls: "task-card__status-dot" });
if (statusConfig) {
newStatusDot.style.borderColor = statusConfig.color;
if (statusConfig.icon) {
newStatusDot.addClass("task-card__status-dot--icon");
setIcon(newStatusDot, statusConfig.icon);
}
}

// Add click handler to cycle through statuses
Expand Down Expand Up @@ -1910,6 +1953,10 @@ export function updateTaskCard(
if (task.priority) cardClasses.push(`task-card--priority-${task.priority}`);
if (newEffectiveStatus)
cardClasses.push(`task-card--status-${newEffectiveStatus}`);
if (plugin.settings?.subtaskChevronPosition === "left")
cardClasses.push("task-card--chevron-left");
if (plugin.projectSubtasksService?.isTaskUsedAsProjectSync(task.path))
cardClasses.push("task-card--project");

element.className = cardClasses.join(" ");
element.dataset.status = newEffectiveStatus;
Expand Down Expand Up @@ -1967,8 +2014,7 @@ export function updateTaskCard(

// Update priority indicator (conditional based on visible properties)
const shouldShowPriority =
!visibleProperties ||
visibleProperties.some((prop) => isPropertyForField(prop, "priority", plugin));
resolvedVisibleProperties.some((prop) => isPropertyForField(prop, "priority", plugin));
const existingPriorityDot = element.querySelector(".task-card__priority-dot") as HTMLElement;

if (shouldShowPriority && task.priority && priorityConfig) {
Expand Down Expand Up @@ -2071,6 +2117,7 @@ export function updateTaskCard(
plugin.projectSubtasksService
.isTaskUsedAsProject(task.path)
.then((isProject: boolean) => {
element.classList.toggle("task-card--project", isProject);
// Remove old placeholders if they exist
element.querySelector(".task-card__project-indicator-placeholder")?.remove();
element.querySelector(".task-card__chevron-placeholder")?.remove();
Expand Down Expand Up @@ -2178,11 +2225,7 @@ export function updateTaskCard(
const metadataElements: HTMLElement[] = [];

// Get properties to display
const propertiesToShow =
visibleProperties ||
(plugin.settings.defaultVisibleProperties
? convertInternalToUserProperties(plugin.settings.defaultVisibleProperties, plugin)
: getDefaultVisibleProperties(plugin));
const propertiesToShow = resolvedVisibleProperties;

for (const propertyId of propertiesToShow) {
// Skip status and priority as they're rendered separately
Expand Down Expand Up @@ -2430,6 +2473,7 @@ export async function toggleSubtasks(

// Sort subtasks
const sortedSubtasks = plugin.projectSubtasksService.sortTasks(subtasks);
const parentVisibleProperties = getSubtaskVisibleProperties(card, plugin);

// Build parent chain by traversing up the DOM hierarchy
const buildParentChain = (element: HTMLElement): string[] => {
Expand Down Expand Up @@ -2461,7 +2505,7 @@ export async function toggleSubtasks(
continue;
}

const subtaskCard = createTaskCard(subtask, plugin, undefined);
const subtaskCard = createTaskCard(subtask, plugin, parentVisibleProperties);

// Add subtask modifier class
subtaskCard.classList.add("task-card--subtask");
Expand Down
21 changes: 21 additions & 0 deletions styles/task-card-bem.css
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@
transition: all var(--tn-transition-fast);
position: relative;
box-shadow: none;
cursor: pointer;
}

.tasknotes-plugin .task-card__status-dot:hover {
Expand Down Expand Up @@ -366,6 +367,7 @@
transition: all var(--tn-transition-fast);
border-radius: var(--tn-radius-sm);
flex-shrink: 0;
cursor: pointer;
}

.tasknotes-plugin .task-card__recurring-indicator:hover {
Expand Down Expand Up @@ -401,6 +403,7 @@
justify-content: center;
border-radius: var(--tn-radius-sm);
flex-shrink: 0;
cursor: pointer;
}

.tasknotes-plugin .task-card__reminder-indicator:hover {
Expand All @@ -426,6 +429,7 @@
border-radius: var(--tn-radius-sm);
transition: all var(--tn-transition-fast);
flex-shrink: 0;
cursor: pointer;
}

.tasknotes-plugin .task-card__project-indicator:hover {
Expand All @@ -451,6 +455,13 @@
border-radius: var(--tn-radius-sm);
transition: all var(--tn-transition-fast);
flex-shrink: 0;
cursor: pointer;
order: 10;
}

/* Always show chevron for project tasks when on the right */
.tasknotes-plugin .task-card--project:not(.task-card--chevron-left) .task-card__chevron {
opacity: 1;
}

.tasknotes-plugin .task-card__chevron:hover {
Expand Down Expand Up @@ -496,6 +507,7 @@
transition: color var(--tn-transition-fast), background var(--tn-transition-fast), opacity var(--tn-transition-fast);
border-radius: var(--tn-radius-sm);
flex-shrink: 0;
cursor: pointer;
}

.tasknotes-plugin .task-card__blocking-toggle:hover {
Expand Down Expand Up @@ -644,6 +656,7 @@
border-radius: var(--tn-radius-sm);
flex-shrink: 0;
margin-left: auto;
cursor: pointer;
}

.tasknotes-plugin .task-card__context-menu:hover {
Expand Down Expand Up @@ -675,6 +688,7 @@
.tasknotes-plugin .task-card__priority-dot {
transition: all var(--tn-transition-fast);
box-shadow: none;
cursor: pointer;
}

.tasknotes-plugin .task-card__priority-dot:hover {
Expand All @@ -688,6 +702,7 @@
padding: 3px 6px;
margin: -3px -6px;
position: relative;
cursor: pointer;
}

.tasknotes-plugin .task-card__metadata-date:hover {
Expand Down Expand Up @@ -1139,6 +1154,12 @@
vertical-align: baseline;
}

/* Inline layout: indicate the title is clickable (opens task dialog) */
.tasknotes-plugin .task-card--layout-inline .task-card__title,
.tasknotes-plugin .task-card--layout-inline .task-card__title-text {
cursor: pointer;
}

/* Metadata in inline mode - stays on same line, scrolls if needed */
.tasknotes-plugin .task-card--layout-inline .task-card__metadata {
display: inline-block !important;
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/ui/TaskCard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ describe('TaskCard Component', () => {
app: mockApp,
selectedDate: new Date('2025-01-15'),
fieldMapper: {
isPropertyForField: jest.fn(() => false),
isPropertyForField: jest.fn((propertyId: string, internalField: string) => {
return propertyId === internalField;
}),
toUserField: jest.fn((field) => field),
toInternalField: jest.fn((field) => field),
getMapping: jest.fn(() => ({
Expand Down
Loading