Rename due_date to date across codebase
The kebab menu and docs referred to a task "due date" but the field was just a date; this change renames due_date/group_by_due_date and related identifiers to date/group_by_date across Rust, TypeScript, Svelte, CLI, docs and tests to keep terminology consistent. Updates include API/command names, storage/models, repository methods, UI text, JS variables and builder/parse functions so code, tests and documentation all use "date" semantics. Preserve old "group_by_due_date" field name Add serde alias to ListMetadata.group_by_date so older .listdata.json files that used the previous field name (group_by_due_date) can still be deserialized correctly. This fixes serialization/deserialization issues encountered at /home/trztn/Documents/Onyx and /var/home/trztn/Nextcloud/Onyx. the frontmatter due should be date... I don't want due anywhere - Renamed `TaskFrontmatter.due` → `TaskFrontmatter.date`; YAML key on disk is now `date:` instead of `due:` - Added `#[serde(alias = "due")]` so existing task files with `due:` frontmatter still deserialize correctly - Updated google_tasks.rs to write `date:` instead of `due:` in generated YAML - Renamed CLI `--due` flag to `--date`; updated function signature and display string "Due:" → "Date:" Remove serde aliases and rename due→date Drop backwards-compat aliases and update frontmatter/metadata to use the canonical "date"/"group_by_date" fields. The aliases for group_by_due_date and due were removed to avoid maintaining backward-compatibility and to reflect the current schema. Updated storage types to rename the fields and adjusted serialization attributes so YAML/JSON frontmatter and .listdata.json files now use group_by_date and date consistently.
This commit is contained in:
parent
a0c183df82
commit
9ed84690ac
|
|
@ -56,7 +56,7 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`).
|
||||||
- **Task animations**: Grid-rows `0fr`/`1fr` trick for smooth collapse/expand. Module-level `animateInIds` Set coordinates expand-in after toggle.
|
- **Task animations**: Grid-rows `0fr`/`1fr` trick for smooth collapse/expand. Module-level `animateInIds` Set coordinates expand-in after toggle.
|
||||||
- **Inline editing**: Click task to edit, auto-save on blur. `debouncedSave` snapshots task before timer to prevent stale-reference errors on component destroy.
|
- **Inline editing**: Click task to edit, auto-save on blur. `debouncedSave` snapshots task before timer to prevent stale-reference errors on component destroy.
|
||||||
- **Kebab menus**: Tasks and lists use kebab menus with custom `ConfirmDialog` component (not native `confirm()`). "Move to..." is inline in the menu (not a submenu) to avoid overflow.
|
- **Kebab menus**: Tasks and lists use kebab menus with custom `ConfirmDialog` component (not native `confirm()`). "Move to..." is inline in the menu (not a submenu) to avoid overflow.
|
||||||
- **Main panel header**: Hamburger + window controls in top bar; list name (large, bold) + kebab below divider (matching task detail layout). Kebab has Rename, Group by due date, Delete completed, Delete list.
|
- **Main panel header**: Hamburger + window controls in top bar; list name (large, bold) + kebab below divider (matching task detail layout). Kebab has Rename, Group by date, Delete completed, Delete list.
|
||||||
- **New task**: FAB button opens bottom toast sheet (outside sliding container for fixed positioning).
|
- **New task**: FAB button opens bottom toast sheet (outside sliding container for fixed positioning).
|
||||||
|
|
||||||
### Development phase
|
### Development phase
|
||||||
|
|
@ -81,10 +81,10 @@ Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to
|
||||||
- Workspace switcher drop-up with add/remove
|
- Workspace switcher drop-up with add/remove
|
||||||
- Per-workspace theme system (System default, Light, Dark, Nord, Dracula, Solarized Dark) via CSS `data-theme` attribute
|
- Per-workspace theme system (System default, Light, Dark, Nord, Dracula, Solarized Dark) via CSS `data-theme` attribute
|
||||||
- Completed tasks section with animated show/hide
|
- Completed tasks section with animated show/hide
|
||||||
- Due date picker/editor (DateTimePicker in new task + task detail); `has_time: bool` field tracks whether time is set
|
- Date picker/editor (DateTimePicker in new task + task detail); `has_time: bool` field tracks whether time is set
|
||||||
- Move task between lists (inline list in kebab menu, no submenu)
|
- Move task between lists (inline list in kebab menu, no submenu)
|
||||||
- List rename (inline input in main panel header via kebab)
|
- List rename (inline input in main panel header via kebab)
|
||||||
- Group-by-due-date toggle per list (main panel kebab; persists flag but display sorting not yet implemented)
|
- Group-by-date toggle per list (main panel kebab; persists flag but display sorting not yet implemented)
|
||||||
- Delete completed tasks (main panel kebab + subtask kebab, with confirmation dialogs)
|
- Delete completed tasks (main panel kebab + subtask kebab, with confirmation dialogs)
|
||||||
- Keyboard shortcuts (Escape priority chain: settings → detail → list menu → drawer → menus)
|
- Keyboard shortcuts (Escape priority chain: settings → detail → list menu → drawer → menus)
|
||||||
- Setup screen with 2-step mode selection (Local Folder vs WebDAV Server), window dragging, "Open Existing Folder" option, remote folder browsing
|
- Setup screen with 2-step mode selection (Local Folder vs WebDAV Server), window dragging, "Open Existing Folder" option, remote folder browsing
|
||||||
|
|
|
||||||
|
|
@ -488,7 +488,7 @@ fn rename_list(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn set_group_by_due_date(
|
fn set_group_by_date(
|
||||||
list_id: String,
|
list_id: String,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
state: State<'_, Mutex<AppState>>,
|
state: State<'_, Mutex<AppState>>,
|
||||||
|
|
@ -498,12 +498,12 @@ fn set_group_by_due_date(
|
||||||
mute_watcher(&mut s);
|
mute_watcher(&mut s);
|
||||||
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
repo_mut(&mut s)?
|
repo_mut(&mut s)?
|
||||||
.set_group_by_due_date(id, enabled)
|
.set_group_by_date(id, enabled)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_group_by_due_date(
|
fn get_group_by_date(
|
||||||
list_id: String,
|
list_id: String,
|
||||||
state: State<'_, Mutex<AppState>>,
|
state: State<'_, Mutex<AppState>>,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
|
|
@ -511,7 +511,7 @@ fn get_group_by_due_date(
|
||||||
ensure_repo(&mut s)?;
|
ensure_repo(&mut s)?;
|
||||||
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?;
|
||||||
repo_ref(&s)?
|
repo_ref(&s)?
|
||||||
.get_group_by_due_date(id)
|
.get_group_by_date(id)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -924,8 +924,8 @@ pub fn run() {
|
||||||
reorder_task,
|
reorder_task,
|
||||||
move_task,
|
move_task,
|
||||||
rename_list,
|
rename_list,
|
||||||
set_group_by_due_date,
|
set_group_by_date,
|
||||||
get_group_by_due_date,
|
get_group_by_date,
|
||||||
set_webdav_config,
|
set_webdav_config,
|
||||||
set_workspace_theme,
|
set_workspace_theme,
|
||||||
set_sync_interval,
|
set_sync_interval,
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,21 @@
|
||||||
|
|
||||||
let title = $state("");
|
let title = $state("");
|
||||||
let description = $state("");
|
let description = $state("");
|
||||||
let dueDate = $state<string | null>(null);
|
let date = $state<string | null>(null);
|
||||||
let dueDateHasTime = $state(false);
|
let dateHasTime = $state(false);
|
||||||
let inputEl = $state<HTMLInputElement | null>(null);
|
let inputEl = $state<HTMLInputElement | null>(null);
|
||||||
let showDatePicker = $state(false);
|
let showDatePicker = $state(false);
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!title.trim()) return;
|
if (!title.trim()) return;
|
||||||
const created = await app.createTask(title.trim(), description.trim() || undefined);
|
const created = await app.createTask(title.trim(), description.trim() || undefined);
|
||||||
if (dueDate && created) {
|
if (date && created) {
|
||||||
await app.updateTask({ ...created, due_date: dueDate, has_time: dueDateHasTime });
|
await app.updateTask({ ...created, date: date, has_time: dateHasTime });
|
||||||
}
|
}
|
||||||
title = "";
|
title = "";
|
||||||
description = "";
|
description = "";
|
||||||
dueDate = null;
|
date = null;
|
||||||
dueDateHasTime = false;
|
dateHasTime = false;
|
||||||
newTaskState.open = false;
|
newTaskState.open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,14 +31,14 @@
|
||||||
newTaskState.open = false;
|
newTaskState.open = false;
|
||||||
title = "";
|
title = "";
|
||||||
description = "";
|
description = "";
|
||||||
dueDate = null;
|
date = null;
|
||||||
dueDateHasTime = false;
|
dateHasTime = false;
|
||||||
showDatePicker = false;
|
showDatePicker = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDateChange(iso: string | null, hasTime: boolean = false) {
|
function handleDateChange(iso: string | null, hasTime: boolean = false) {
|
||||||
dueDate = iso;
|
date = iso;
|
||||||
dueDateHasTime = hasTime;
|
dateHasTime = hasTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateChip(iso: string): string {
|
function formatDateChip(iso: string): string {
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
const day = dayNames[d.getDay()];
|
const day = dayNames[d.getDay()];
|
||||||
const pad = (n: number) => String(n).padStart(2, "0");
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
const timePart = dueDateHasTime ? `, ${pad(d.getHours())}:${pad(d.getMinutes())}` : "";
|
const timePart = dateHasTime ? `, ${pad(d.getHours())}:${pad(d.getMinutes())}` : "";
|
||||||
if (d.toDateString() === today.toDateString()) return `Today${timePart}`;
|
if (d.toDateString() === today.toDateString()) return `Today${timePart}`;
|
||||||
return `${day}, ${pad(d.getDate())}/${pad(d.getMonth() + 1)}${timePart}`;
|
return `${day}, ${pad(d.getDate())}/${pad(d.getMonth() + 1)}${timePart}`;
|
||||||
}
|
}
|
||||||
|
|
@ -102,12 +102,12 @@
|
||||||
<svg class="h-5 w-5 shrink-0 opacity-40" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 shrink-0 opacity-40" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
{#if dueDate}
|
{#if date}
|
||||||
<div class="flex items-center gap-1.5 rounded-full border border-border-light bg-black/5 px-3 py-1 text-sm dark:border-border-dark dark:bg-white/10">
|
<div class="flex items-center gap-1.5 rounded-full border border-border-light bg-black/5 px-3 py-1 text-sm dark:border-border-dark dark:bg-white/10">
|
||||||
<button type="button" onclick={() => (showDatePicker = true)} class="hover:opacity-70">
|
<button type="button" onclick={() => (showDatePicker = true)} class="hover:opacity-70">
|
||||||
{formatDateChip(dueDate)}
|
{formatDateChip(date)}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onclick={() => (dueDate = null)} class="opacity-40 hover:opacity-80">
|
<button type="button" onclick={() => (date = null)} class="opacity-40 hover:opacity-80">
|
||||||
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
|
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDateChange(iso: string | null, hasTime: boolean = false) {
|
function handleDateChange(iso: string | null, hasTime: boolean = false) {
|
||||||
app.updateTask({ ...task, due_date: iso, has_time: hasTime });
|
app.updateTask({ ...task, date: iso, has_time: hasTime });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleToggle() {
|
async function handleToggle() {
|
||||||
|
|
@ -237,10 +237,10 @@
|
||||||
<svg class="h-5 w-5 shrink-0 opacity-40" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 shrink-0 opacity-40" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
{#if task.due_date}
|
{#if task.date}
|
||||||
<div class="flex items-center gap-1.5 rounded-full border border-border-light bg-black/5 px-3 py-1 text-sm dark:border-border-dark dark:bg-white/10">
|
<div class="flex items-center gap-1.5 rounded-full border border-border-light bg-black/5 px-3 py-1 text-sm dark:border-border-dark dark:bg-white/10">
|
||||||
<button onclick={() => (showDatePicker = true)} class="hover:opacity-70">
|
<button onclick={() => (showDatePicker = true)} class="hover:opacity-70">
|
||||||
{formatDateChip(task.due_date)}
|
{formatDateChip(task.date)}
|
||||||
</button>
|
</button>
|
||||||
<button onclick={() => handleDateChange(null)} class="opacity-40 hover:opacity-80">
|
<button onclick={() => handleDateChange(null)} class="opacity-40 hover:opacity-80">
|
||||||
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
|
@ -388,7 +388,7 @@
|
||||||
<!-- Date picker overlay -->
|
<!-- Date picker overlay -->
|
||||||
{#if showDatePicker}
|
{#if showDatePicker}
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={task.due_date}
|
value={task.date}
|
||||||
has_time={task.has_time}
|
has_time={task.has_time}
|
||||||
onchange={handleDateChange}
|
onchange={handleDateChange}
|
||||||
onclose={() => (showDatePicker = false)}
|
onclose={() => (showDatePicker = false)}
|
||||||
|
|
|
||||||
|
|
@ -150,14 +150,14 @@
|
||||||
{#if task.description}
|
{#if task.description}
|
||||||
<p class="mt-0.5 text-xs opacity-40 line-clamp-1">{task.description}</p>
|
<p class="mt-0.5 text-xs opacity-40 line-clamp-1">{task.description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if task.due_date && dateChipStyle !== "hidden"}
|
{#if task.date && dateChipStyle !== "hidden"}
|
||||||
{#if dateChipStyle === "overdue"}
|
{#if dateChipStyle === "overdue"}
|
||||||
<span class="mt-1 inline-block rounded-full border border-danger px-2 py-0.5 text-xs text-danger opacity-80">
|
<span class="mt-1 inline-block rounded-full border border-danger px-2 py-0.5 text-xs text-danger opacity-80">
|
||||||
{formatDate(task.due_date)}
|
{formatDate(task.date)}
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="mt-1 inline-block rounded-full border border-border-light px-2 py-0.5 text-xs opacity-50 dark:border-border-dark">
|
<span class="mt-1 inline-block rounded-full border border-border-light px-2 py-0.5 text-xs opacity-50 dark:border-border-dark">
|
||||||
{formatDate(task.due_date)}
|
{formatDate(task.date)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ let completedTasks = $derived(tasks.filter((t) => t.status === "completed" && !t
|
||||||
type TaskGroup = { label: string; tasks: Task[]; date: Date | null };
|
type TaskGroup = { label: string; tasks: Task[]; date: Date | null };
|
||||||
|
|
||||||
let groupedPendingTasks = $derived.by((): TaskGroup[] | null => {
|
let groupedPendingTasks = $derived.by((): TaskGroup[] | null => {
|
||||||
if (!activeList?.group_by_due_date) return null;
|
if (!activeList?.group_by_date) return null;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
const tomorrowStart = new Date(todayStart);
|
const tomorrowStart = new Date(todayStart);
|
||||||
|
|
@ -68,10 +68,10 @@ let groupedPendingTasks = $derived.by((): TaskGroup[] | null => {
|
||||||
const noDate: Task[] = [];
|
const noDate: Task[] = [];
|
||||||
|
|
||||||
for (const task of pendingTasks) {
|
for (const task of pendingTasks) {
|
||||||
if (!task.due_date) {
|
if (!task.date) {
|
||||||
noDate.push(task);
|
noDate.push(task);
|
||||||
} else {
|
} else {
|
||||||
const d = new Date(task.due_date);
|
const d = new Date(task.date);
|
||||||
const dayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
const dayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||||||
if (dayStart < todayStart) overdue.push(task);
|
if (dayStart < todayStart) overdue.push(task);
|
||||||
else if (dayStart.getTime() === todayStart.getTime()) today.push(task);
|
else if (dayStart.getTime() === todayStart.getTime()) today.push(task);
|
||||||
|
|
@ -86,7 +86,7 @@ let groupedPendingTasks = $derived.by((): TaskGroup[] | null => {
|
||||||
|
|
||||||
const taskOrderIndex = new Map(pendingTasks.map((t, i) => [t.id, i]));
|
const taskOrderIndex = new Map(pendingTasks.map((t, i) => [t.id, i]));
|
||||||
const sortByDue = (a: Task, b: Task) => {
|
const sortByDue = (a: Task, b: Task) => {
|
||||||
const dateDiff = new Date(a.due_date!).getTime() - new Date(b.due_date!).getTime();
|
const dateDiff = new Date(a.date!).getTime() - new Date(b.date!).getTime();
|
||||||
if (dateDiff !== 0) return dateDiff;
|
if (dateDiff !== 0) return dateDiff;
|
||||||
return (taskOrderIndex.get(a.id) ?? 0) - (taskOrderIndex.get(b.id) ?? 0);
|
return (taskOrderIndex.get(a.id) ?? 0) - (taskOrderIndex.get(b.id) ?? 0);
|
||||||
};
|
};
|
||||||
|
|
@ -395,11 +395,11 @@ async function renameList(listId: string, newName: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setGroupByDueDate(listId: string, enabled: boolean) {
|
async function setGroupByDate(listId: string, enabled: boolean) {
|
||||||
try {
|
try {
|
||||||
await invoke("set_group_by_due_date", { listId, enabled });
|
await invoke("set_group_by_date", { listId, enabled });
|
||||||
lists = lists.map((l) =>
|
lists = lists.map((l) =>
|
||||||
l.id === listId ? { ...l, group_by_due_date: enabled } : l,
|
l.id === listId ? { ...l, group_by_date: enabled } : l,
|
||||||
);
|
);
|
||||||
if (listId === activeListId) await loadTasks();
|
if (listId === activeListId) await loadTasks();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -656,7 +656,7 @@ export const app = {
|
||||||
deleteTask,
|
deleteTask,
|
||||||
moveTask,
|
moveTask,
|
||||||
renameList,
|
renameList,
|
||||||
setGroupByDueDate,
|
setGroupByDate,
|
||||||
triggerSync,
|
triggerSync,
|
||||||
startAutoSync,
|
startAutoSync,
|
||||||
stopAutoSync,
|
stopAutoSync,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ export interface Task {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
status: "backlog" | "completed";
|
status: "backlog" | "completed";
|
||||||
due_date: string | null;
|
date: string | null;
|
||||||
has_time: boolean;
|
has_time: boolean;
|
||||||
version: number;
|
version: number;
|
||||||
parent_id: string | null;
|
parent_id: string | null;
|
||||||
|
|
@ -15,7 +15,7 @@ export interface TaskList {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
group_by_due_date: boolean;
|
group_by_date: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkspaceMode = "local" | "webdav";
|
export type WorkspaceMode = "local" | "webdav";
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ pub fn enable(list_name: String, workspace: Option<String>) -> Result<()> {
|
||||||
.find(|l| l.title == list_name)
|
.find(|l| l.title == list_name)
|
||||||
.ok_or_else(|| anyhow::anyhow!("List '{}' not found", list_name))?;
|
.ok_or_else(|| anyhow::anyhow!("List '{}' not found", list_name))?;
|
||||||
|
|
||||||
repo.set_group_by_due_date(list.id, true)
|
repo.set_group_by_date(list.id, true)
|
||||||
.context("Failed to enable grouping")?;
|
.context("Failed to enable grouping")?;
|
||||||
|
|
||||||
output::success(&format!("Enabled group-by-due-date for list \"{}\"", list_name));
|
output::success(&format!("Enabled group-by-date for list \"{}\"", list_name));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -30,10 +30,10 @@ pub fn disable(list_name: String, workspace: Option<String>) -> Result<()> {
|
||||||
.find(|l| l.title == list_name)
|
.find(|l| l.title == list_name)
|
||||||
.ok_or_else(|| anyhow::anyhow!("List '{}' not found", list_name))?;
|
.ok_or_else(|| anyhow::anyhow!("List '{}' not found", list_name))?;
|
||||||
|
|
||||||
repo.set_group_by_due_date(list.id, false)
|
repo.set_group_by_date(list.id, false)
|
||||||
.context("Failed to disable grouping")?;
|
.context("Failed to disable grouping")?;
|
||||||
|
|
||||||
output::success(&format!("Disabled group-by-due-date for list \"{}\"", list_name));
|
output::success(&format!("Disabled group-by-date for list \"{}\"", list_name));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ fn print_tasks(tasks: &[Task]) {
|
||||||
}
|
}
|
||||||
for task in tasks {
|
for task in tasks {
|
||||||
let checkbox = if task.status == TaskStatus::Completed { "[✓]".green() } else { "[ ]".normal() };
|
let checkbox = if task.status == TaskStatus::Completed { "[✓]".green() } else { "[ ]".normal() };
|
||||||
let due_str = task.due_date.map(|d| format!(" (due: {})", d.format("%Y-%m-%d")).yellow().to_string()).unwrap_or_default();
|
let due_str = task.date.map(|d| format!(" ({})", d.format("%Y-%m-%d")).yellow().to_string()).unwrap_or_default();
|
||||||
output::item(&format!("{} {}{} {}", checkbox, task.title, due_str, task.id.to_string().dimmed()));
|
output::item(&format!("{} {}{} {}", checkbox, task.title, due_str, task.id.to_string().dimmed()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use uuid::Uuid;
|
||||||
use crate::output;
|
use crate::output;
|
||||||
use crate::commands::get_repository;
|
use crate::commands::get_repository;
|
||||||
|
|
||||||
pub fn add(title: String, list_name: Option<String>, due_str: Option<String>, workspace: Option<String>) -> Result<()> {
|
pub fn add(title: String, list_name: Option<String>, date_str: Option<String>, workspace: Option<String>) -> Result<()> {
|
||||||
let (mut repo, _workspace_name) = get_repository(workspace)?;
|
let (mut repo, _workspace_name) = get_repository(workspace)?;
|
||||||
|
|
||||||
// Get lists
|
// Get lists
|
||||||
|
|
@ -29,18 +29,18 @@ pub fn add(title: String, list_name: Option<String>, due_str: Option<String>, wo
|
||||||
// Create task
|
// Create task
|
||||||
let mut task = Task::new(title.clone());
|
let mut task = Task::new(title.clone());
|
||||||
|
|
||||||
// Parse due date if provided
|
// Parse date if provided
|
||||||
if let Some(due_str) = due_str {
|
if let Some(due_str) = date_str {
|
||||||
let due_date = parse_due_date(&due_str)?;
|
let date = parse_date(&due_str)?;
|
||||||
task.due_date = Some(due_date);
|
task.date = Some(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save task
|
// Save task
|
||||||
repo.create_task(list.id, task.clone())
|
repo.create_task(list.id, task.clone())
|
||||||
.context("Failed to create task")?;
|
.context("Failed to create task")?;
|
||||||
|
|
||||||
let due_info = if let Some(due) = task.due_date {
|
let due_info = if let Some(due) = task.date {
|
||||||
format!("\n Due: {}", due.format("%Y-%m-%d"))
|
format!("\n Date: {}", due.format("%Y-%m-%d"))
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
@ -204,7 +204,7 @@ pub fn edit(task_id_str: String, workspace: Option<String>) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_due_date(s: &str) -> Result<DateTime<Utc>> {
|
fn parse_date(s: &str) -> Result<DateTime<Utc>> {
|
||||||
// Try parsing as date only (YYYY-MM-DD)
|
// Try parsing as date only (YYYY-MM-DD)
|
||||||
if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
|
if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
|
||||||
let naive_datetime = naive_date.and_hms_opt(0, 0, 0)
|
let naive_datetime = naive_date.and_hms_opt(0, 0, 0)
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,9 @@ enum Commands {
|
||||||
/// List to add task to
|
/// List to add task to
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
list: Option<String>,
|
list: Option<String>,
|
||||||
/// Due date (ISO 8601 format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)
|
/// Date (ISO 8601 format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
due: Option<String>,
|
date: Option<String>,
|
||||||
/// Workspace to use
|
/// Workspace to use
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
workspace: Option<String>,
|
workspace: Option<String>,
|
||||||
|
|
@ -74,7 +74,7 @@ enum Commands {
|
||||||
workspace: Option<String>,
|
workspace: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Toggle group-by-due-date for a list
|
/// Toggle group-by-date for a list
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
Group(GroupCommands),
|
Group(GroupCommands),
|
||||||
|
|
||||||
|
|
@ -176,7 +176,7 @@ enum ListCommands {
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum GroupCommands {
|
enum GroupCommands {
|
||||||
/// Enable group-by-due-date for a list
|
/// Enable group-by-date for a list
|
||||||
Enable {
|
Enable {
|
||||||
/// Name of the list
|
/// Name of the list
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
|
|
@ -186,7 +186,7 @@ enum GroupCommands {
|
||||||
workspace: Option<String>,
|
workspace: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Disable group-by-due-date for a list
|
/// Disable group-by-date for a list
|
||||||
Disable {
|
Disable {
|
||||||
/// Name of the list
|
/// Name of the list
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
|
|
@ -235,8 +235,8 @@ fn main() -> Result<()> {
|
||||||
list::delete(name, workspace)?;
|
list::delete(name, workspace)?;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Commands::Add { title, list, due, workspace } => {
|
Commands::Add { title, list, date, workspace } => {
|
||||||
task::add(title, list, due, workspace)?;
|
task::add(title, list, date, workspace)?;
|
||||||
}
|
}
|
||||||
Commands::Complete { task_id, workspace } => {
|
Commands::Complete { task_id, workspace } => {
|
||||||
task::complete(task_id, workspace)?;
|
task::complete(task_id, workspace)?;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ pub struct Task {
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub status: TaskStatus,
|
pub status: TaskStatus,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub due_date: Option<DateTime<Utc>>,
|
pub date: Option<DateTime<Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub has_time: bool,
|
pub has_time: bool,
|
||||||
pub version: u64,
|
pub version: u64,
|
||||||
|
|
@ -31,7 +31,7 @@ impl Task {
|
||||||
title,
|
title,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
status: TaskStatus::Backlog,
|
status: TaskStatus::Backlog,
|
||||||
due_date: None,
|
date: None,
|
||||||
has_time: false,
|
has_time: false,
|
||||||
version: 0,
|
version: 0,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
|
|
@ -43,8 +43,8 @@ impl Task {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_due_date(mut self, due_date: DateTime<Utc>) -> Self {
|
pub fn with_date(mut self, date: DateTime<Utc>) -> Self {
|
||||||
self.due_date = Some(due_date);
|
self.date = Some(date);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ pub struct TaskList {
|
||||||
pub tasks: Vec<Task>,
|
pub tasks: Vec<Task>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
pub group_by_due_date: bool,
|
pub group_by_date: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaskList {
|
impl TaskList {
|
||||||
|
|
@ -81,7 +81,7 @@ impl TaskList {
|
||||||
tasks: Vec::new(),
|
tasks: Vec::new(),
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
group_by_due_date: false,
|
group_by_date: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,7 +152,7 @@ mod tests {
|
||||||
assert_eq!(task.title, "My Task");
|
assert_eq!(task.title, "My Task");
|
||||||
assert_eq!(task.description, "");
|
assert_eq!(task.description, "");
|
||||||
assert_eq!(task.status, TaskStatus::Backlog);
|
assert_eq!(task.status, TaskStatus::Backlog);
|
||||||
assert!(task.due_date.is_none());
|
assert!(task.date.is_none());
|
||||||
assert!(!task.has_time);
|
assert!(!task.has_time);
|
||||||
assert_eq!(task.version, 0);
|
assert_eq!(task.version, 0);
|
||||||
assert!(task.parent_id.is_none());
|
assert!(task.parent_id.is_none());
|
||||||
|
|
@ -197,12 +197,12 @@ mod tests {
|
||||||
let dt = Utc::now();
|
let dt = Utc::now();
|
||||||
let task = Task::new("Chained".to_string())
|
let task = Task::new("Chained".to_string())
|
||||||
.with_description("Desc".to_string())
|
.with_description("Desc".to_string())
|
||||||
.with_due_date(dt)
|
.with_date(dt)
|
||||||
.with_parent(parent_id);
|
.with_parent(parent_id);
|
||||||
|
|
||||||
assert_eq!(task.title, "Chained");
|
assert_eq!(task.title, "Chained");
|
||||||
assert_eq!(task.description, "Desc");
|
assert_eq!(task.description, "Desc");
|
||||||
assert_eq!(task.due_date, Some(dt));
|
assert_eq!(task.date, Some(dt));
|
||||||
assert_eq!(task.parent_id, Some(parent_id));
|
assert_eq!(task.parent_id, Some(parent_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,7 +231,7 @@ mod tests {
|
||||||
fn test_task_serde_skips_none_fields() {
|
fn test_task_serde_skips_none_fields() {
|
||||||
let task = Task::new("Minimal".to_string());
|
let task = Task::new("Minimal".to_string());
|
||||||
let json = serde_json::to_string(&task).unwrap();
|
let json = serde_json::to_string(&task).unwrap();
|
||||||
assert!(!json.contains("due_date"));
|
assert!(!json.contains("\"date\""));
|
||||||
assert!(!json.contains("parent_id"));
|
assert!(!json.contains("parent_id"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -118,17 +118,17 @@ impl TaskRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grouping preference
|
// Grouping preference
|
||||||
pub fn set_group_by_due_date(&mut self, list_id: Uuid, enabled: bool) -> Result<()> {
|
pub fn set_group_by_date(&mut self, list_id: Uuid, enabled: bool) -> Result<()> {
|
||||||
let mut metadata = self.storage.read_list_metadata(list_id)?;
|
let mut metadata = self.storage.read_list_metadata(list_id)?;
|
||||||
metadata.group_by_due_date = enabled;
|
metadata.group_by_date = enabled;
|
||||||
metadata.updated_at = chrono::Utc::now();
|
metadata.updated_at = chrono::Utc::now();
|
||||||
self.storage.write_list_metadata(&metadata)?;
|
self.storage.write_list_metadata(&metadata)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_group_by_due_date(&self, list_id: Uuid) -> Result<bool> {
|
pub fn get_group_by_date(&self, list_id: Uuid) -> Result<bool> {
|
||||||
let metadata = self.storage.read_list_metadata(list_id)?;
|
let metadata = self.storage.read_list_metadata(list_id)?;
|
||||||
Ok(metadata.group_by_due_date)
|
Ok(metadata.group_by_date)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,19 +214,19 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_group_by_due_date() {
|
fn test_group_by_date() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap();
|
let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap();
|
||||||
|
|
||||||
let list = repo.create_list("Test List".to_string()).unwrap();
|
let list = repo.create_list("Test List".to_string()).unwrap();
|
||||||
|
|
||||||
assert!(!repo.get_group_by_due_date(list.id).unwrap());
|
assert!(!repo.get_group_by_date(list.id).unwrap());
|
||||||
|
|
||||||
repo.set_group_by_due_date(list.id, true).unwrap();
|
repo.set_group_by_date(list.id, true).unwrap();
|
||||||
assert!(repo.get_group_by_due_date(list.id).unwrap());
|
assert!(repo.get_group_by_date(list.id).unwrap());
|
||||||
|
|
||||||
repo.set_group_by_due_date(list.id, false).unwrap();
|
repo.set_group_by_date(list.id, false).unwrap();
|
||||||
assert!(!repo.get_group_by_due_date(list.id).unwrap());
|
assert!(!repo.get_group_by_date(list.id).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Error path tests ---
|
// --- Error path tests ---
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ pub struct ListMetadata {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
pub group_by_due_date: bool,
|
pub group_by_date: bool,
|
||||||
pub task_order: Vec<Uuid>,
|
pub task_order: Vec<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ impl ListMetadata {
|
||||||
id,
|
id,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
group_by_due_date: false,
|
group_by_date: false,
|
||||||
task_order: Vec::new(),
|
task_order: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +88,7 @@ pub struct TaskFrontmatter {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub status: TaskStatus,
|
pub status: TaskStatus,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub due: Option<DateTime<Utc>>,
|
pub date: Option<DateTime<Utc>>,
|
||||||
#[serde(default, skip_serializing_if = "is_false")]
|
#[serde(default, skip_serializing_if = "is_false")]
|
||||||
pub has_time: bool,
|
pub has_time: bool,
|
||||||
#[serde(default = "default_version")]
|
#[serde(default = "default_version")]
|
||||||
|
|
@ -360,7 +360,7 @@ impl Storage for FileSystemStorage {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
status: frontmatter.status,
|
status: frontmatter.status,
|
||||||
due_date: frontmatter.due,
|
date: frontmatter.date,
|
||||||
has_time: frontmatter.has_time,
|
has_time: frontmatter.has_time,
|
||||||
version: frontmatter.version,
|
version: frontmatter.version,
|
||||||
parent_id: frontmatter.parent,
|
parent_id: frontmatter.parent,
|
||||||
|
|
@ -456,7 +456,7 @@ impl Storage for FileSystemStorage {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
status: frontmatter.status,
|
status: frontmatter.status,
|
||||||
due_date: frontmatter.due,
|
date: frontmatter.date,
|
||||||
has_time: frontmatter.has_time,
|
has_time: frontmatter.has_time,
|
||||||
version: frontmatter.version,
|
version: frontmatter.version,
|
||||||
parent_id: frontmatter.parent,
|
parent_id: frontmatter.parent,
|
||||||
|
|
@ -547,7 +547,7 @@ impl Storage for FileSystemStorage {
|
||||||
tasks: Vec::new(),
|
tasks: Vec::new(),
|
||||||
created_at: list_metadata.created_at,
|
created_at: list_metadata.created_at,
|
||||||
updated_at: list_metadata.updated_at,
|
updated_at: list_metadata.updated_at,
|
||||||
group_by_due_date: list_metadata.group_by_due_date,
|
group_by_date: list_metadata.group_by_date,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(task_list)
|
Ok(task_list)
|
||||||
|
|
@ -582,7 +582,7 @@ impl Storage for FileSystemStorage {
|
||||||
tasks,
|
tasks,
|
||||||
created_at: list_metadata.created_at,
|
created_at: list_metadata.created_at,
|
||||||
updated_at: list_metadata.updated_at,
|
updated_at: list_metadata.updated_at,
|
||||||
group_by_due_date: list_metadata.group_by_due_date,
|
group_by_date: list_metadata.group_by_date,
|
||||||
};
|
};
|
||||||
|
|
||||||
lists.push(task_list);
|
lists.push(task_list);
|
||||||
|
|
@ -761,7 +761,7 @@ mod tests {
|
||||||
|
|
||||||
let content = "---\nid: 550e8400-e29b-41d4-a716-446655440000\nstatus: backlog\ndue: 2026-06-15T12:00:00Z\nversion: 2\nparent: 660e8400-e29b-41d4-a716-446655440001\n---\n\nNotes";
|
let content = "---\nid: 550e8400-e29b-41d4-a716-446655440000\nstatus: backlog\ndue: 2026-06-15T12:00:00Z\nversion: 2\nparent: 660e8400-e29b-41d4-a716-446655440001\n---\n\nNotes";
|
||||||
let (fm, _) = storage.parse_markdown_with_frontmatter(content).unwrap();
|
let (fm, _) = storage.parse_markdown_with_frontmatter(content).unwrap();
|
||||||
assert!(fm.due.is_some());
|
assert!(fm.date.is_some());
|
||||||
assert!(fm.parent.is_some());
|
assert!(fm.parent.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue