Add blocked/blocking task metadata pills

This commit is contained in:
callumalpass 2025-09-28 08:28:29 +10:00
parent 40855bb71f
commit 299cf7f178
14 changed files with 212 additions and 75 deletions

View file

@ -239,7 +239,7 @@ export async function renderTaskNotesInBasesView(
// Use plugin default properties if no Bases properties available
if (!visibleProperties || visibleProperties.length === 0) {
visibleProperties = plugin.settings.defaultVisibleProperties || [
'due', 'scheduled', 'projects', 'contexts', 'tags'
'due', 'scheduled', 'projects', 'contexts', 'tags', 'blocked', 'blocking'
];
}
@ -322,4 +322,4 @@ export function renderBasesDataItem(container: HTMLElement, item: BasesDataItem,
itemEl.appendChild(rawDataEl);
container.appendChild(itemEl);
}
}

View file

@ -733,7 +733,9 @@ export const de: TranslationTree = {
modifiedDate: 'Änderungsdatum',
projects: 'Projekte',
contexts: 'Kontexte',
tags: 'Tags'
tags: 'Tags',
blocked: 'Blockiert',
blocking: 'Blockierend'
}
},
taskFilenames: {
@ -1869,6 +1871,16 @@ export const de: TranslationTree = {
dailyBadge: 'Täglich',
dailyTooltip: 'Tägliche Notiz'
},
taskCard: {
blockedBadge: 'Blockiert',
blockedBadgeTooltip: 'Diese Aufgabe wartet auf eine andere Aufgabe',
blockingBadge: 'Blockierend',
blockingBadgeTooltip: 'Diese Aufgabe blockiert eine andere Aufgabe',
blockingToggle: 'Blockiert {count} Aufgaben',
loadingDependencies: 'Abhängigkeiten werden geladen…',
blockingEmpty: 'Keine abhängigen Aufgaben',
blockingLoadError: 'Abhängigkeiten konnten nicht geladen werden'
},
filterHeading: {
allViewName: 'Alle'
},
@ -1986,7 +1998,9 @@ export const de: TranslationTree = {
modifiedDate: 'Änderungsdatum',
projects: 'Projekte',
contexts: 'Kontexte',
tags: 'Tags'
tags: 'Tags',
blocked: 'Blockiert',
blocking: 'Blockierend'
}
},
reminderContextMenu: {
@ -2074,4 +2088,4 @@ export const de: TranslationTree = {
}
};
export type DeTranslationSchema = typeof de;
export type DeTranslationSchema = typeof de;

View file

@ -734,7 +734,9 @@ export const en: TranslationTree = {
modifiedDate: 'Modified Date',
projects: 'Projects',
contexts: 'Contexts',
tags: 'Tags'
tags: 'Tags',
blocked: 'Blocked',
blocking: 'Blocking'
}
},
taskFilenames: {
@ -1912,6 +1914,8 @@ export const en: TranslationTree = {
taskCard: {
blockedBadge: 'Blocked',
blockedBadgeTooltip: 'This task is waiting on another task',
blockingBadge: 'Blocking',
blockingBadgeTooltip: 'This task is blocking another task',
blockingToggle: 'Blocking {count} tasks',
loadingDependencies: 'Loading dependencies...',
blockingEmpty: 'No dependent tasks',
@ -2034,7 +2038,9 @@ export const en: TranslationTree = {
modifiedDate: 'Modified Date',
projects: 'Projects',
contexts: 'Contexts',
tags: 'Tags'
tags: 'Tags',
blocked: 'Blocked',
blocking: 'Blocking'
}
},
reminderContextMenu: {

View file

@ -733,7 +733,9 @@ export const es: TranslationTree = {
modifiedDate: 'Fecha de modificación',
projects: 'Proyectos',
contexts: 'Contextos',
tags: 'Etiquetas'
tags: 'Etiquetas',
blocked: 'Bloqueada',
blocking: 'Bloqueando'
}
},
taskFilenames: {
@ -1869,6 +1871,16 @@ export const es: TranslationTree = {
dailyBadge: 'Diario',
dailyTooltip: 'Nota diaria'
},
taskCard: {
blockedBadge: 'Bloqueada',
blockedBadgeTooltip: 'Esta tarea está esperando otra tarea',
blockingBadge: 'Bloqueando',
blockingBadgeTooltip: 'Esta tarea bloquea otra tarea',
blockingToggle: 'Bloqueando {count} tareas',
loadingDependencies: 'Cargando dependencias...',
blockingEmpty: 'Sin tareas dependientes',
blockingLoadError: 'No se pudieron cargar las dependencias'
},
filterHeading: {
allViewName: 'Todos'
},
@ -1986,7 +1998,9 @@ export const es: TranslationTree = {
modifiedDate: 'Fecha de modificación',
projects: 'Proyectos',
contexts: 'Contextos',
tags: 'Etiquetas'
tags: 'Etiquetas',
blocked: 'Bloqueada',
blocking: 'Bloqueando'
}
},
reminderContextMenu: {
@ -2074,4 +2088,4 @@ export const es: TranslationTree = {
}
};
export type EsTranslationSchema = typeof es;
export type EsTranslationSchema = typeof es;

View file

@ -733,7 +733,9 @@ export const fr: TranslationTree = {
modifiedDate: 'Date de modification',
projects: 'Projets',
contexts: 'Contextes',
tags: 'Étiquettes'
tags: 'Étiquettes',
blocked: 'Bloqué',
blocking: 'Bloquant'
}
},
taskFilenames: {
@ -1902,6 +1904,8 @@ export const fr: TranslationTree = {
taskCard: {
blockedBadge: 'Bloqué',
blockedBadgeTooltip: 'Cette tâche attend une autre tâche',
blockingBadge: 'Bloquant',
blockingBadgeTooltip: 'Cette tâche bloque une autre tâche',
blockingToggle: 'Bloque {count} tâches',
loadingDependencies: 'Chargement des dépendances…',
blockingEmpty: 'Aucune tâche dépendante',
@ -2024,7 +2028,9 @@ export const fr: TranslationTree = {
modifiedDate: 'Date de modification',
projects: 'Projets',
contexts: 'Contextes',
tags: 'Étiquettes'
tags: 'Étiquettes',
blocked: 'Bloqué',
blocking: 'Bloquant'
}
},
reminderContextMenu: {

View file

@ -733,7 +733,9 @@ export const ja: TranslationTree = {
modifiedDate: '変更日',
projects: 'プロジェクト',
contexts: 'コンテキスト',
tags: 'タグ'
tags: 'タグ',
blocked: 'ブロック中',
blocking: 'ブロックしている'
}
},
taskFilenames: {
@ -1869,6 +1871,16 @@ export const ja: TranslationTree = {
dailyBadge: 'デイリー',
dailyTooltip: 'デイリーノート'
},
taskCard: {
blockedBadge: 'ブロック中',
blockedBadgeTooltip: 'このタスクは他のタスクを待っています',
blockingBadge: 'ブロックしている',
blockingBadgeTooltip: 'このタスクは他のタスクをブロックしています',
blockingToggle: '{count} 件のタスクをブロック',
loadingDependencies: '依存関係を読み込み中…',
blockingEmpty: '依存タスクはありません',
blockingLoadError: '依存関係の読み込みに失敗しました'
},
filterHeading: {
allViewName: 'すべて'
},
@ -1986,7 +1998,9 @@ export const ja: TranslationTree = {
modifiedDate: '変更日',
projects: 'プロジェクト',
contexts: 'コンテキスト',
tags: 'タグ'
tags: 'タグ',
blocked: 'ブロック中',
blocking: 'ブロックしている'
}
},
reminderContextMenu: {
@ -2074,4 +2088,4 @@ export const ja: TranslationTree = {
}
};
export type JaTranslationSchema = typeof ja;
export type JaTranslationSchema = typeof ja;

View file

@ -733,7 +733,9 @@ export const ru: TranslationTree = {
modifiedDate: 'Дата изменения',
projects: 'Проекты',
contexts: 'Контексты',
tags: 'Теги'
tags: 'Теги',
blocked: 'Заблокирована',
blocking: 'Блокирует'
}
},
taskFilenames: {
@ -1902,6 +1904,8 @@ export const ru: TranslationTree = {
taskCard: {
blockedBadge: 'Заблокирована',
blockedBadgeTooltip: 'Эта задача ожидает другую задачу',
blockingBadge: 'Блокирует',
blockingBadgeTooltip: 'Эта задача блокирует другую задачу',
blockingToggle: 'Блокирует {count} задач',
loadingDependencies: 'Загрузка зависимостей…',
blockingEmpty: 'Нет зависимых задач',
@ -2024,7 +2028,9 @@ export const ru: TranslationTree = {
modifiedDate: 'Дата изменения',
projects: 'Проекты',
contexts: 'Контексты',
tags: 'Теги'
tags: 'Теги',
blocked: 'Заблокирована',
blocking: 'Блокирует'
}
},
reminderContextMenu: {

View file

@ -733,7 +733,9 @@ export const zh: TranslationTree = {
modifiedDate: '修改日期',
projects: '项目',
contexts: '上下文',
tags: '标签'
tags: '标签',
blocked: '已阻塞',
blocking: '阻塞中'
}
},
taskFilenames: {
@ -1869,6 +1871,16 @@ export const zh: TranslationTree = {
dailyBadge: '日记',
dailyTooltip: '日记'
},
taskCard: {
blockedBadge: '已阻塞',
blockedBadgeTooltip: '此任务正在等待其他任务',
blockingBadge: '阻塞中',
blockingBadgeTooltip: '此任务正在阻塞其他任务',
blockingToggle: '阻塞 {count} 个任务',
loadingDependencies: '正在加载依赖…',
blockingEmpty: '没有依赖的任务',
blockingLoadError: '无法加载依赖'
},
filterHeading: {
allViewName: '全部'
},
@ -1986,7 +1998,9 @@ export const zh: TranslationTree = {
modifiedDate: '修改日期',
projects: '项目',
contexts: '上下文',
tags: '标签'
tags: '标签',
blocked: '已阻塞',
blocking: '阻塞中'
}
},
reminderContextMenu: {
@ -2072,4 +2086,4 @@ export const zh: TranslationTree = {
}
}
}
};
};

View file

@ -276,7 +276,9 @@ export const DEFAULT_SETTINGS: TaskNotesSettings = {
'scheduled', // Scheduled date
'projects', // Projects
'contexts', // Contexts
'tags' // Tags
'tags', // Tags
'blocked', // Blocked indicator
'blocking' // Blocking indicator
],
// Bases integration defaults
enableBases: true,

View file

@ -35,6 +35,8 @@ export function renderAppearanceTab(container: HTMLElement, plugin: TaskNotesPlu
core: [
{ key: 'status', label: translate('settings.appearance.taskCards.properties.status') },
{ key: 'priority', label: translate('settings.appearance.taskCards.properties.priority') },
{ key: 'blocked', label: translate('settings.appearance.taskCards.properties.blocked') },
{ key: 'blocking', label: translate('settings.appearance.taskCards.properties.blocking') },
{ key: 'due', label: translate('settings.appearance.taskCards.properties.due') },
{ key: 'scheduled', label: translate('settings.appearance.taskCards.properties.scheduled') },
{ key: 'timeEstimate', label: translate('settings.appearance.taskCards.properties.timeEstimate') },

View file

@ -1968,7 +1968,7 @@ export class FilterBar extends EventEmitter {
* Get default fallback properties when no configuration exists
*/
private getDefaultFallbackProperties(): string[] {
return ['status', 'priority', 'due', 'scheduled', 'projects', 'contexts', 'tags'];
return ['status', 'priority', 'due', 'scheduled', 'projects', 'contexts', 'tags', 'blocked', 'blocking'];
}
/**

View file

@ -104,6 +104,8 @@ export class PropertyVisibilityDropdown {
properties.push(...[
{ id: 'status', name: this.plugin.i18n.translate('components.propertyVisibilityDropdown.properties.statusDot'), category: 'core' as const },
{ id: 'priority', name: this.plugin.i18n.translate('components.propertyVisibilityDropdown.properties.priorityDot'), category: 'core' as const },
{ id: 'blocked', name: this.plugin.i18n.translate('components.propertyVisibilityDropdown.properties.blocked'), category: 'core' as const },
{ id: 'blocking', name: this.plugin.i18n.translate('components.propertyVisibilityDropdown.properties.blocking'), category: 'core' as const },
{ id: 'due', name: this.plugin.i18n.translate('components.propertyVisibilityDropdown.properties.dueDate'), category: 'core' as const },
{ id: 'scheduled', name: this.plugin.i18n.translate('components.propertyVisibilityDropdown.properties.scheduledDate'), category: 'core' as const },
{ id: 'timeEstimate', name: this.plugin.i18n.translate('components.propertyVisibilityDropdown.properties.timeEstimate'), category: 'core' as const },
@ -203,4 +205,4 @@ export class PropertyVisibilityDropdown {
export function clearPropertiesCache(): void {
propertiesCache = null;
cacheValidForPlugin = null;
}
}

View file

@ -96,7 +96,9 @@ function getDefaultVisibleProperties(): string[] {
'scheduled', // Scheduled date
'projects', // Projects
'contexts', // Contexts
'tags' // Tags
'tags', // Tags
'blocked', // Blocked indicator
'blocking' // Blocking indicator
];
}
@ -109,6 +111,8 @@ const PROPERTY_EXTRACTORS: Record<string, (task: TaskInfo) => any> = {
'projects': (task) => task.projects,
'contexts': (task) => task.contexts,
'tags': (task) => task.tags,
'blocked': (task) => task.isBlocked,
'blocking': (task) => task.isBlocking,
'timeEstimate': (task) => task.timeEstimate,
'totalTrackedTime': (task) => task.totalTrackedTime,
'recurrence': (task) => task.recurrence,
@ -1112,15 +1116,6 @@ export function createTaskCard(task: TaskInfo, plugin: TaskNotesPlugin, visibleP
titleTextEl.classList.add('completed');
}
if (task.isBlocked) {
const blockedBadge = titleEl.createSpan({
cls: 'task-card__badge task-card__badge--blocked',
attr: { 'aria-label': plugin.i18n.translate('ui.taskCard.blockedBadge') }
});
setIcon(blockedBadge, 'octagon');
setTooltip(blockedBadge, plugin.i18n.translate('ui.taskCard.blockedBadgeTooltip'), { placement: 'top' });
}
// Second line: Metadata (dynamic based on visible properties)
const metadataLine = contentContainer.createEl('div', { cls: 'task-card__metadata' });
const metadataElements: HTMLElement[] = [];
@ -1132,19 +1127,52 @@ export function createTaskCard(task: TaskInfo, plugin: TaskNotesPlugin, visibleP
// Render each visible property
for (const propertyId of propertiesToShow) {
// Skip status and priority - they're shown as dots
if (propertyId === 'status' || propertyId === 'priority') continue;
if (propertyId === 'blocked') {
if (task.isBlocked) {
const blockedLabel = plugin.i18n.translate('ui.taskCard.blockedBadge');
const blockedCount = task.blockedBy?.length ?? 0;
const pillText = blockedCount > 0
? `${blockedLabel} (${blockedCount})`
: blockedLabel;
const blockedPill = metadataLine.createSpan({
cls: 'task-card__metadata-pill task-card__metadata-pill--blocked',
text: pillText
});
setTooltip(blockedPill, plugin.i18n.translate('ui.taskCard.blockedBadgeTooltip'), { placement: 'top' });
metadataElements.push(blockedPill);
}
continue;
}
if (propertyId === 'blocking') {
if (task.isBlocking) {
const blockingLabel = plugin.i18n.translate('ui.taskCard.blockingBadge');
const blockingCount = task.blocking?.length ?? 0;
const pillText = blockingCount > 0
? `${blockingLabel} (${blockingCount})`
: blockingLabel;
const blockingPill = metadataLine.createSpan({
cls: 'task-card__metadata-pill task-card__metadata-pill--blocking',
text: pillText
});
setTooltip(blockingPill, plugin.i18n.translate('ui.taskCard.blockingBadgeTooltip'), { placement: 'top' });
metadataElements.push(blockingPill);
}
continue;
}
const element = renderPropertyMetadata(metadataLine, propertyId, task, plugin);
if (element) {
metadataElements.push(element);
}
}
// Add separators between metadata elements
addMetadataSeparators(metadataLine, metadataElements);
// Add click handlers with single/double click distinction
const { clickHandler, dblclickHandler } = createTaskClickHandler({
task,
@ -1668,23 +1696,11 @@ export function updateTaskCard(element: HTMLElement, task: TaskInfo, plugin: Tas
titleContainer.classList.toggle('completed', titleIsCompleted);
}
const existingBadge = element.querySelector('.task-card__badge--blocked') as HTMLElement;
if (task.isBlocked) {
if (!existingBadge) {
const titleContainer = element.querySelector('.task-card__title') as HTMLElement;
if (titleContainer) {
const badge = titleContainer.createSpan({
cls: 'task-card__badge task-card__badge--blocked',
attr: { 'aria-label': plugin.i18n.translate('ui.taskCard.blockedBadge') }
});
setIcon(badge, 'octagon');
setTooltip(badge, plugin.i18n.translate('ui.taskCard.blockedBadgeTooltip'), { placement: 'top' });
}
}
} else if (existingBadge) {
existingBadge.remove();
const legacyBlockedBadge = element.querySelector('.task-card__badge--blocked');
if (legacyBlockedBadge) {
legacyBlockedBadge.remove();
}
// Update metadata line
const metadataLine = element.querySelector('.task-card__metadata') as HTMLElement;
if (metadataLine) {
@ -1697,11 +1713,43 @@ export function updateTaskCard(element: HTMLElement, task: TaskInfo, plugin: Tas
plugin.settings.defaultVisibleProperties ||
getDefaultVisibleProperties();
// Render each visible property
for (const propertyId of propertiesToShow) {
// Skip status and priority - they're shown as dots
if (propertyId === 'status' || propertyId === 'priority') continue;
if (propertyId === 'blocked') {
if (task.isBlocked) {
const blockedLabel = plugin.i18n.translate('ui.taskCard.blockedBadge');
const blockedCount = task.blockedBy?.length ?? 0;
const pillText = blockedCount > 0
? `${blockedLabel} (${blockedCount})`
: blockedLabel;
const blockedPill = metadataLine.createSpan({
cls: 'task-card__metadata-pill task-card__metadata-pill--blocked',
text: pillText
});
setTooltip(blockedPill, plugin.i18n.translate('ui.taskCard.blockedBadgeTooltip'), { placement: 'top' });
metadataElements.push(blockedPill);
}
continue;
}
if (propertyId === 'blocking') {
if (task.isBlocking) {
const blockingLabel = plugin.i18n.translate('ui.taskCard.blockingBadge');
const blockingCount = task.blocking?.length ?? 0;
const pillText = blockingCount > 0
? `${blockingLabel} (${blockingCount})`
: blockingLabel;
const blockingPill = metadataLine.createSpan({
cls: 'task-card__metadata-pill task-card__metadata-pill--blocking',
text: pillText
});
setTooltip(blockingPill, plugin.i18n.translate('ui.taskCard.blockingBadgeTooltip'), { placement: 'top' });
metadataElements.push(blockingPill);
}
continue;
}
const element = renderPropertyMetadata(metadataLine, propertyId, task, plugin);
if (element) {
metadataElements.push(element);

View file

@ -93,24 +93,6 @@
min-width: 0;
}
.tasknotes-plugin .task-card__badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--color-red);
color: var(--background-primary);
font-size: 11px;
box-shadow: 0 0 0 1px rgba(0,0,0,0.1);
}
.tasknotes-plugin .task-card__badge svg {
width: 14px;
height: 14px;
}
.tasknotes-plugin .task-card__metadata {
font-size: var(--tn-font-size-md);
color: var(--tn-text-muted);
@ -131,6 +113,33 @@
white-space: nowrap;
}
.tasknotes-plugin .task-card__metadata-pill {
display: inline-flex;
align-items: center;
gap: var(--tn-spacing-xs);
padding: 2px var(--tn-spacing-sm);
border-radius: var(--tn-radius-lg);
font-size: var(--tn-font-size-sm);
line-height: 1.2;
font-weight: 500;
background: var(--tn-bg-secondary);
color: var(--tn-text-muted);
border: 1px solid var(--tn-border-subtle);
}
.tasknotes-plugin .task-card__metadata-pill--blocked {
background: color-mix(in srgb, var(--tn-color-error) 75%, var(--tn-bg-primary));
color: var(--tn-bg-primary);
border-color: color-mix(in srgb, var(--tn-color-error) 85%, var(--tn-bg-primary));
padding-inline: var(--tn-spacing-md);
}
.tasknotes-plugin .task-card__metadata-pill--blocking {
background: transparent;
color: var(--tn-color-error);
border-color: color-mix(in srgb, var(--tn-color-error) 70%, transparent);
}
.tasknotes-plugin .task-card__metadata-separator {
color: var(--tn-text-faint);
opacity: 0.5;