mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 08:56:41 +00:00
* fix(e2e): stabilize undo task delete sync test Two flakiness sources fixed: - Click on task element could activate title inline editor, causing Backspace to edit text instead of triggering delete. Now clicks the drag handle which calls focusSelf() without entering edit mode. - Replaced deleteTask helper with inline sequence to avoid wasting 2s of the 5s undo snackbar window on dialog-detection timeout. * refactor: address code review findings from 2026-02-03 - Extract getBreakCycle helper to replace error-prone `cycle - 1 || 1` pattern at 3 call sites - Add clarifying comment on intentionally broad 'timed out' match - Reduce Pomodoro E2E test from 9 to 5 sessions (sufficient coverage) - Remove dead _isTransientNetworkError wrapper from DropboxApi - Extract stubWindowConfirm helper in task reducer tests * fix(sync): prevent Formly from clearing provider config on show (#6345) resetOnHide: true caused Formly to reset field values when provider fieldGroups transitioned from hidden to visible, discarding user input if sync was enabled before selecting a provider. * fix(tasks): fix huge space between emoji and text in tag/project menus Use matMenuItemIcon attribute on emoji spans so they project into the icon slot of mat-menu-item instead of the text slot. Update emoji icon sizing to 24x24px to match mat-icon and add overflow: hidden. Closes #5977 * fix(tasks): guard against undefined task entities in selectors and archive (#6359) Prevent TypeError crashes (reading 'dueWithTime', 'dueDay', 'issueProviderId') caused by orphaned IDs in NgRx state. Fix archive merge to deduplicate IDs, filter orphans, and use correct entity precedence (young over old). Add defensive null guards to selectors and archive/task service methods. * fix(tasks): guard against undefined task in mainListTasksInProject$ (#6360) * fix(tasks): detect and sanitize orphaned task IDs to prevent startup crashes (#6359, #6360) Orphaned task IDs (entries in task.ids without matching entities) caused TypeError on app startup. Fix addresses three layers: validation now flags orphaned IDs instead of silently skipping them, loadAllData sanitizes IDs on load as a safety net, and data repair no longer crashes when encountering orphaned IDs it's trying to fix. * fix(sync): prevent recurring task duplication across clients Remove SuperSync special-case that bypassed initial sync wait, causing repeatable task effects to fire before sync completed. Add post-sync cleanup effect that detects and removes stale duplicate repeat instances when multiple active instances exist for the same repeat config. * fix(sync): restore WebDAV provider compatibility warning text * feat(sync): mark WebDAV and LocalFile sync options as experimental * feat(plugins): add UI Kit with inject-first CSS strategy for iframe plugins Introduce a lightweight CSS reset (UI Kit) that auto-styles basic HTML elements in plugin iframes to match the host app theme. Injected after <head> so plugin styles always win by source order. UI Kit provides: element resets (body, headings, buttons, inputs, tables, links, code, lists, hr), .btn-primary/.btn-outline button variants, and .card/.card-clickable components. All bundled plugins updated to use UI Kit classes, removing redundant custom CSS (-542 lines net). Pico CSS removed from automations plugin. sync-md converted from hardcoded colors to host theme variables. * feat(plugins): extract shared CSS utilities into UI Kit Move .text-muted, .text-primary, .page-fade and @keyframes fadeIn from plugin CSS into the UI Kit so all iframe plugins get them automatically. Add box-shadow focus ring to input:focus for better accessibility. Remove per-plugin focus overrides now covered by the UI Kit.
390 lines
11 KiB
HTML
390 lines
11 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1.0"
|
|
/>
|
|
<title>Yesterday's Tasks</title>
|
|
<style>
|
|
/* Theme vars + UI Kit are injected automatically before this style block.
|
|
Only plugin-specific layout rules are needed here. */
|
|
|
|
body {
|
|
padding: var(--s3);
|
|
}
|
|
|
|
.container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.date {
|
|
margin-bottom: var(--s3);
|
|
color: var(--text-color-muted);
|
|
}
|
|
|
|
.loading,
|
|
.no-tasks {
|
|
text-align: center;
|
|
padding: var(--s4);
|
|
color: var(--text-color-muted);
|
|
}
|
|
|
|
.task-item {
|
|
padding: var(--s) 0;
|
|
border-bottom: 1px solid var(--divider-color);
|
|
display: flex;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.task-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.task-title {
|
|
flex: 1;
|
|
margin-right: var(--s2);
|
|
}
|
|
|
|
.task-time {
|
|
white-space: nowrap;
|
|
min-width: 60px;
|
|
text-align: right;
|
|
}
|
|
|
|
.project-tag {
|
|
display: inline-block;
|
|
padding: var(--s-quarter) var(--s);
|
|
margin-left: var(--s);
|
|
font-size: 0.85em;
|
|
color: var(--text-color-muted);
|
|
border: 1px solid var(--divider-color);
|
|
border-radius: 12px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div
|
|
class="date"
|
|
id="yesterday-date"
|
|
></div>
|
|
|
|
<div id="content">
|
|
<div class="loading">Loading tasks...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Format date to YYYY-MM-DD in local timezone
|
|
// Note: This matches the getDbDateStr() utility used in the main app
|
|
function formatDateStr(date) {
|
|
const d = date || new Date();
|
|
const year = d.getFullYear();
|
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
// Helper function to get date range for a specific day
|
|
function getDateRange(daysBack = 1) {
|
|
const now = new Date();
|
|
const targetDate = new Date(now);
|
|
targetDate.setDate(targetDate.getDate() - daysBack);
|
|
|
|
// Set to start of day (00:00:00)
|
|
const startOfDay = new Date(targetDate);
|
|
startOfDay.setHours(0, 0, 0, 0);
|
|
|
|
// Set to end of day (23:59:59)
|
|
const endOfDay = new Date(targetDate);
|
|
endOfDay.setHours(23, 59, 59, 999);
|
|
|
|
return {
|
|
start: startOfDay.getTime(),
|
|
end: endOfDay.getTime(),
|
|
date: targetDate,
|
|
dateKey: formatDateStr(targetDate),
|
|
};
|
|
}
|
|
|
|
// Helper function to get yesterday's date range (for backward compatibility)
|
|
function getYesterdayRange() {
|
|
return getDateRange(1);
|
|
}
|
|
|
|
// Function to format duration
|
|
function formatDuration(ms) {
|
|
if (!ms || ms === 0) return '0m';
|
|
|
|
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
|
|
|
|
if (hours > 0) {
|
|
return hours + 'h ' + minutes + 'm';
|
|
}
|
|
return minutes + 'm';
|
|
}
|
|
|
|
// Wait for plugin API to be available
|
|
function waitForPlugin() {
|
|
if (typeof PluginAPI !== 'undefined') {
|
|
init();
|
|
} else {
|
|
setTimeout(waitForPlugin, 100);
|
|
}
|
|
}
|
|
|
|
// Function to get tasks for a specific date
|
|
function getTasksForDate(combinedTasks, range, dateKey) {
|
|
// Filter tasks that have time entries from the target date
|
|
const dateTasks = combinedTasks.filter((task) => {
|
|
if (!task.timeSpentOnDay) return false;
|
|
|
|
// Check if date key exists
|
|
if (task.timeSpentOnDay[dateKey]) {
|
|
return true;
|
|
}
|
|
|
|
// Also check timestamp-based entries
|
|
for (const taskDateKey in task.timeSpentOnDay) {
|
|
const entries = task.timeSpentOnDay[taskDateKey];
|
|
if (Array.isArray(entries)) {
|
|
for (const entry of entries) {
|
|
if (entry && entry.s >= range.start && entry.s <= range.end) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Calculate time spent on target date for each task
|
|
const tasksWithTime = dateTasks.map((task) => {
|
|
let timeSpent = 0;
|
|
|
|
if (task.timeSpentOnDay) {
|
|
// First check for direct date key (YYYY-MM-DD format)
|
|
if (task.timeSpentOnDay[dateKey]) {
|
|
const timeValue = task.timeSpentOnDay[dateKey];
|
|
if (typeof timeValue === 'number') {
|
|
timeSpent = timeValue;
|
|
} else if (Array.isArray(timeValue)) {
|
|
// Handle array format
|
|
for (const entry of timeValue) {
|
|
if (entry && entry.e && entry.s) {
|
|
timeSpent += entry.e - entry.s;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Check timestamp-based entries
|
|
for (const taskDateKey in task.timeSpentOnDay) {
|
|
const entries = task.timeSpentOnDay[taskDateKey];
|
|
if (Array.isArray(entries)) {
|
|
for (const entry of entries) {
|
|
if (entry && entry.s >= range.start && entry.s <= range.end) {
|
|
timeSpent += entry.e - entry.s || 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
...task,
|
|
dateTimeSpent: timeSpent,
|
|
formattedTime: formatDuration(timeSpent),
|
|
};
|
|
});
|
|
|
|
// Sort by time spent (descending)
|
|
tasksWithTime.sort((a, b) => b.dateTimeSpent - a.dateTimeSpent);
|
|
|
|
return tasksWithTime;
|
|
}
|
|
|
|
async function getLastAvailableTasks() {
|
|
try {
|
|
const allTasks = await PluginAPI.getTasks();
|
|
const archivedTasks = await PluginAPI.getArchivedTasks();
|
|
|
|
// Combine all tasks
|
|
const combinedTasks = [...allTasks, ...archivedTasks];
|
|
|
|
console.log('Total tasks found:', combinedTasks.length);
|
|
|
|
// Try to find tasks starting from yesterday and going backwards
|
|
for (let daysBack = 1; daysBack <= 30; daysBack++) {
|
|
const range = getDateRange(daysBack);
|
|
console.log(`Checking ${daysBack} days back:`, range.dateKey);
|
|
|
|
const tasks = getTasksForDate(combinedTasks, range, range.dateKey);
|
|
|
|
if (tasks.length > 0) {
|
|
console.log(`Found ${tasks.length} tasks from ${range.dateKey}`);
|
|
return {
|
|
tasks,
|
|
date: range.date,
|
|
dateKey: range.dateKey,
|
|
daysBack,
|
|
};
|
|
}
|
|
}
|
|
|
|
// No tasks found in the last 30 days
|
|
console.log('No tasks found in the last 30 days');
|
|
return {
|
|
tasks: [],
|
|
date: new Date(),
|
|
dateKey: '',
|
|
daysBack: 0,
|
|
};
|
|
} catch (error) {
|
|
console.error('Error fetching tasks:', error);
|
|
return {
|
|
tasks: [],
|
|
date: new Date(),
|
|
dateKey: '',
|
|
daysBack: 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
async function init() {
|
|
try {
|
|
const result = await getLastAvailableTasks();
|
|
const projects = await PluginAPI.getAllProjects();
|
|
|
|
// Update the date display based on what was found
|
|
const dateEl = document.getElementById('yesterday-date');
|
|
if (result.tasks.length > 0) {
|
|
const dateText = result.date.toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
|
|
if (result.daysBack === 1) {
|
|
dateEl.textContent = dateText + ' (Yesterday)';
|
|
} else {
|
|
dateEl.textContent = dateText + ` (${result.daysBack} days ago)`;
|
|
}
|
|
} else {
|
|
dateEl.textContent = 'No recent tasks found';
|
|
}
|
|
|
|
displayTasks(result.tasks, projects);
|
|
} catch (error) {
|
|
console.error('Error loading tasks:', error);
|
|
document.getElementById('content').innerHTML =
|
|
'<div class="no-tasks">Error loading tasks. Please try again.</div>';
|
|
}
|
|
}
|
|
|
|
function displayTasks(tasks, projects = []) {
|
|
const contentEl = document.getElementById('content');
|
|
|
|
if (tasks.length === 0) {
|
|
contentEl.innerHTML =
|
|
'<div class="no-tasks">No tasks tracked in the last 30 days.</div>';
|
|
return;
|
|
}
|
|
|
|
// Create project lookup map
|
|
const projectMap = {};
|
|
projects.forEach((project) => {
|
|
projectMap[project.id] = project.title;
|
|
});
|
|
|
|
// Build task list HTML
|
|
let html = '';
|
|
|
|
tasks.forEach((task) => {
|
|
const isDone = task.isDone ? 'task-done' : '';
|
|
const projectTitle =
|
|
task.projectId && projectMap[task.projectId]
|
|
? projectMap[task.projectId]
|
|
: task.projectId;
|
|
const projectTag = projectTitle
|
|
? `<span class="project-tag">${escapeHtml(projectTitle)}</span>`
|
|
: '';
|
|
|
|
html += `
|
|
<div class="task-item">
|
|
<div class="task-title ${isDone}">
|
|
${escapeHtml(task.title)}${projectTag}
|
|
</div>
|
|
<div class="task-time">${task.formattedTime}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
contentEl.innerHTML = html;
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Refresh function for hooks
|
|
async function refreshTasks() {
|
|
const result = await getLastAvailableTasks();
|
|
const projects = await PluginAPI.getAllProjects();
|
|
|
|
// Update the date display
|
|
const dateEl = document.getElementById('yesterday-date');
|
|
if (result.tasks.length > 0) {
|
|
const dateText = result.date.toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
|
|
if (result.daysBack === 1) {
|
|
dateEl.textContent = dateText + ' (Yesterday)';
|
|
} else {
|
|
dateEl.textContent = dateText + ` (${result.daysBack} days ago)`;
|
|
}
|
|
} else {
|
|
dateEl.textContent = 'No recent tasks found';
|
|
}
|
|
|
|
displayTasks(result.tasks, projects);
|
|
}
|
|
|
|
// Register hooks to refresh on task changes
|
|
function setupHooks() {
|
|
PluginAPI.registerHook(PluginAPI.Hooks.TASK_UPDATE, (taskData) => {
|
|
refreshTasks();
|
|
});
|
|
PluginAPI.registerHook(PluginAPI.Hooks.TASK_COMPLETE, (taskData) => {
|
|
refreshTasks();
|
|
});
|
|
PluginAPI.registerHook(PluginAPI.Hooks.TASK_DELETE, (taskData) => {
|
|
refreshTasks();
|
|
});
|
|
}
|
|
|
|
// Start
|
|
function waitForPlugin() {
|
|
if (typeof PluginAPI !== 'undefined') {
|
|
init();
|
|
setupHooks();
|
|
} else {
|
|
setTimeout(waitForPlugin, 100);
|
|
}
|
|
}
|
|
|
|
waitForPlugin();
|
|
</script>
|
|
</body>
|
|
</html>
|