Add HelpPanel and TodoItemTouch components, extend task commands, and refactor task and view logic
- Introduced `HelpPanel.vue` for displaying keyboard shortcuts and command descriptions. - Added `TodoItemTouch.vue`, a mobile-friendly task item component with updated bindings and improved actions. - Extended task commands with support for tagging, due date parsing, and dynamic text formatting. - Implemented `useActions` utility for parsing and executing command-based task modifications. - Streamlined task editing and creation in `useTasks` for consistency and API integration. - Updated `ListScreen` to support collapsible, categorized task lists with visual enhancements. - Refactored `App.vue` for adaptive input handling on mobile versus desktop views. - Enhanced API communication in `useApi` with cleaner header generation and error handling.
This commit is contained in:
403
src/utils/actions.ts
Normal file
403
src/utils/actions.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
import type { Task } from '../types.ts'
|
||||
import type { Command } from './parser.ts'
|
||||
import { TaskStatus } from '../types.ts'
|
||||
import { parseDueDate, stopWorkLogging } from './helpers.ts'
|
||||
|
||||
export function moveCommand(tasks: Task[], ids: Task['id_'][], cmd: Command) {
|
||||
return tasks.filter(t => ids.includes(t.id_) && t.tag && cmd?.tag).map(t => ({
|
||||
...t,
|
||||
tag: cmd?.tag,
|
||||
} as Task))
|
||||
}
|
||||
|
||||
export function beginCommand(tasks: Task[], ids: Task['id_'][]) {
|
||||
return tasks.filter(t => ids.includes(t.id_) && t.status !== TaskStatus.WIP).map((t) => {
|
||||
t.status = TaskStatus.WIP
|
||||
t.logs = (t.logs || []).concat({
|
||||
start: Date.now(),
|
||||
end: 0,
|
||||
})
|
||||
return t
|
||||
})
|
||||
}
|
||||
|
||||
export function checkCommand(tasks: Task[], ids: Task['id_'][]) {
|
||||
return tasks.filter(t => ids.includes(t.id_)).map((t) => {
|
||||
t.status
|
||||
= t.status === TaskStatus.DONE ? TaskStatus.WAIT : TaskStatus.DONE
|
||||
if (t.status === TaskStatus.DONE) {
|
||||
t = stopWorkLogging(t)
|
||||
}
|
||||
return t
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
|
||||
if (ids.length === 0) {
|
||||
// Delete by tag
|
||||
const tag = (cmd?.id?.match(/^(@.*)/) || []).pop()
|
||||
if (tag) {
|
||||
return tasks.filter(t => t.tag === tag).map(t => ({
|
||||
...t,
|
||||
status: TaskStatus.NONE,
|
||||
} as Task))
|
||||
}
|
||||
// Delete by status
|
||||
const status = (
|
||||
cmd?.id?.match(/^(finished|done|flag|ongoing|wip|wait|pending)/)
|
||||
|| []
|
||||
).pop()
|
||||
if (status) {
|
||||
let taskStatus = null
|
||||
switch (status) {
|
||||
case 'finished':
|
||||
case 'done':
|
||||
taskStatus = TaskStatus.DONE
|
||||
break
|
||||
case 'flag':
|
||||
case 'flagged':
|
||||
taskStatus = TaskStatus.FLAG
|
||||
break
|
||||
case 'ongoing':
|
||||
case 'wip':
|
||||
taskStatus = TaskStatus.WIP
|
||||
break
|
||||
case 'wait':
|
||||
case 'pending':
|
||||
taskStatus = TaskStatus.WAIT
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
if (taskStatus) {
|
||||
return tasks.filter(t => t.status === taskStatus && !t.archived).map(t => ({
|
||||
...t,
|
||||
status: TaskStatus.NONE,
|
||||
} as Task))
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
else {
|
||||
// Delete by id
|
||||
return tasks.filter(t => ids.includes(t.id_)).map(t => ({
|
||||
...t,
|
||||
status: TaskStatus.NONE,
|
||||
} as Task))
|
||||
}
|
||||
}
|
||||
|
||||
export function flagCommand(tasks: Task[], ids: Task['id_'][]) {
|
||||
return tasks.filter(t => ids.includes(t.id_)).map((t) => {
|
||||
t.status
|
||||
= t.status === TaskStatus.FLAG ? TaskStatus.WAIT : TaskStatus.FLAG
|
||||
t = stopWorkLogging(t)
|
||||
|
||||
return t
|
||||
})
|
||||
}
|
||||
|
||||
export function stopCommand(tasks: Task[], ids: Task['id_'][]) {
|
||||
return tasks.filter(t => ids.includes(t.id_) && t.status === TaskStatus.WIP).map((t) => {
|
||||
t = stopWorkLogging(t)
|
||||
return {
|
||||
...t,
|
||||
status: TaskStatus.WAIT,
|
||||
} as Task
|
||||
})
|
||||
}
|
||||
|
||||
export function switchCommand(tasks: Task[], ids: Task['id_'][]) {
|
||||
if (ids.length === 2) {
|
||||
const stopId = ids[0]
|
||||
const startId = ids[1]
|
||||
return tasks.filter(t => ids.includes(t.id_)).map((t) => {
|
||||
if (t.id_ === stopId && t.status === TaskStatus.WIP) {
|
||||
return stopCommand([t], [stopId])[0]
|
||||
}
|
||||
if (t.id_ === startId && t.status !== TaskStatus.WIP) {
|
||||
return beginCommand([t], [startId])[0]
|
||||
}
|
||||
return t
|
||||
})
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function archiveCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
|
||||
if (ids.length === 0) {
|
||||
// Archive by tag
|
||||
const tag = (cmd?.id?.match(/^(@.*)/) || []).pop()
|
||||
if (tag) {
|
||||
return tasks.filter(t => t.tag === tag).map(t => ({
|
||||
...t,
|
||||
archived: true,
|
||||
} as Task))
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Archive by Ids
|
||||
return tasks.filter(t => ids.includes(t.id_)).map(t => ({
|
||||
...t,
|
||||
archived: true,
|
||||
} as Task))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function restoreCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
|
||||
if (ids.length === 0) {
|
||||
// Archive by tag
|
||||
const tag = (cmd?.id?.match(/^(@.*)/) || []).pop()
|
||||
if (tag) {
|
||||
return tasks.filter(t => t.tag === tag).map(t => ({
|
||||
...t,
|
||||
archived: false,
|
||||
} as Task))
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Archive by Ids
|
||||
return tasks.filter(t => ids.includes(t.id)).map(t => ({
|
||||
...t,
|
||||
archived: false,
|
||||
} as Task))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function insertTaskCommand(cmd: Command) {
|
||||
const tag = cmd?.tag || '@uncategorized'
|
||||
const task = cmd?.text
|
||||
if (task && task.length) {
|
||||
return {
|
||||
tag,
|
||||
title: task,
|
||||
dueDate: null,
|
||||
} as Pick<Task, 'title' | 'dueDate' | 'tag'>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function editTaskCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
|
||||
const id = ids[0]
|
||||
const task = cmd?.text
|
||||
if (task && task.length) {
|
||||
return tasks.filter(t => t.id === id).map(t => ({
|
||||
...t,
|
||||
title: task,
|
||||
} as Task))
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export function dueCommand(ids: Task['id_'][], cmd: Command, tasks: Task[]) {
|
||||
const id = ids && ids.length ? ids[0] : null
|
||||
const text = (cmd?.text || '').trim()
|
||||
if (id) {
|
||||
return tasks.filter(t => t.id_ === id).map((t: Task) => {
|
||||
if (/^(?:clear|none|remove)$/i.test(text)) {
|
||||
t.dueDate = null
|
||||
}
|
||||
else if (text && text.length) {
|
||||
t.dueDate = parseDueDate(text)
|
||||
}
|
||||
return t
|
||||
})
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function tagRenameCommand(cmd: Command, tasks: Task[]) {
|
||||
const [from, to] = cmd?.tag?.split(' ') || []
|
||||
if (!from || !to)
|
||||
return []
|
||||
return tasks.filter(t => t.tag.match(from)).map(t => ({
|
||||
...t,
|
||||
tag: to,
|
||||
} as Task))
|
||||
}
|
||||
|
||||
// export function hideCommand(updateCandidate, cmd) {
|
||||
// updateCandidate = (() => {
|
||||
// switch (cmd.text) {
|
||||
// case 'finished':
|
||||
// case 'done':
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// taskVisibility: {
|
||||
// ...updateCandidate.taskVisibility,
|
||||
// done: false,
|
||||
// },
|
||||
// }
|
||||
// case 'flag':
|
||||
// case 'flagged':
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// taskVisibility: {
|
||||
// ...updateCandidate.taskVisibility,
|
||||
// flagged: false,
|
||||
// },
|
||||
// }
|
||||
// case 'ongoing':
|
||||
// case 'wip':
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// taskVisibility: {
|
||||
// ...updateCandidate.taskVisibility,
|
||||
// wip: false,
|
||||
// },
|
||||
// }
|
||||
// case 'pending':
|
||||
// case 'wait':
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// taskVisibility: {
|
||||
// ...updateCandidate.taskVisibility,
|
||||
// wait: false,
|
||||
// },
|
||||
// }
|
||||
// default:
|
||||
// return updateCandidate
|
||||
// }
|
||||
// })()
|
||||
// return updateCandidate
|
||||
// }
|
||||
//
|
||||
// export function showCommand(updateCandidate, cmd) {
|
||||
// updateCandidate = (() => {
|
||||
// switch (cmd.text) {
|
||||
// case 'finished':
|
||||
// case 'done':
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// taskVisibility: {
|
||||
// ...updateCandidate.taskVisibility,
|
||||
// done: true,
|
||||
// },
|
||||
// }
|
||||
// case 'flag':
|
||||
// case 'flagged':
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// taskVisibility: {
|
||||
// ...updateCandidate.taskVisibility,
|
||||
// flagged: true,
|
||||
// },
|
||||
// }
|
||||
// case 'wip':
|
||||
// case 'ongoing':
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// taskVisibility: {
|
||||
// ...updateCandidate.taskVisibility,
|
||||
// wip: true,
|
||||
// },
|
||||
// }
|
||||
// case 'pending':
|
||||
// case 'wait':
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// taskVisibility: {
|
||||
// ...updateCandidate.taskVisibility,
|
||||
// wait: true,
|
||||
// },
|
||||
// }
|
||||
// default:
|
||||
// return updateCandidate
|
||||
// }
|
||||
// })()
|
||||
// return updateCandidate
|
||||
// }
|
||||
//
|
||||
// export function searchCommand(updateCandidate: any, cmd) {
|
||||
// if (cmd.command.match(/search/i)) {
|
||||
// updateCandidate = {
|
||||
// ...updateCandidate,
|
||||
// filterBy: cmd.text,
|
||||
// }
|
||||
// }
|
||||
// return updateCandidate
|
||||
// }
|
||||
//
|
||||
// export function otherCommand(updateCandidate, cmd, tasks: Task[]) {
|
||||
// updateCandidate = (() => {
|
||||
// const commandText = cmd.command.toLowerCase()
|
||||
// if (commandText === 'help') {
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// showHelp: true,
|
||||
// }
|
||||
// }
|
||||
// else if (commandText === 'quickhelp') {
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// showQuickHelp: true,
|
||||
// }
|
||||
// }
|
||||
// else if (commandText === 'today') {
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// showToday: !state.showToday,
|
||||
// }
|
||||
// }
|
||||
// else if (commandText === 'dark') {
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// darkMode: true,
|
||||
// }
|
||||
// }
|
||||
// else if (commandText === 'light') {
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// darkMode: false,
|
||||
// }
|
||||
// }
|
||||
// else if (commandText === 'setting') {
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// showSettings: true,
|
||||
// }
|
||||
// }
|
||||
// else if (commandText === 'customize') {
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// showCustomCSS: !updateCandidate.showCustomCSS,
|
||||
// }
|
||||
// }
|
||||
// else if (commandText === 'list-archived') {
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// showArchived: !updateCandidate.showArchived,
|
||||
// }
|
||||
// }
|
||||
// else if (commandText === 'login') {
|
||||
// // OK, Let me explain the weird @demo stuff here:
|
||||
// // If the user is already has their data on another machine, and
|
||||
// // they opened this app on a new machine, then login right away,
|
||||
// // the tasks in the range of 1..12 will be conflict with the demo
|
||||
// // tasks. So, we will explicitly remove these demo tasks if they're
|
||||
// // actually a demo, when login.
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// tasks: updateCandidate.tasks.filter(t =>
|
||||
// (t.id - 1) * (t.id - 12) <= 0 ? t.tag !== '@demo' : true,
|
||||
// ),
|
||||
// userWantToLogin: true,
|
||||
// }
|
||||
// }
|
||||
// else if (commandText === 'logout') {
|
||||
// return {
|
||||
// ...updateCandidate,
|
||||
// authToken: '',
|
||||
// userName: '',
|
||||
// userWantToLogin: true,
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// return updateCandidate
|
||||
// }
|
||||
// })()
|
||||
// return updateCandidate
|
||||
// }
|
||||
27
src/utils/helpers.ts
Normal file
27
src/utils/helpers.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Task } from '../types.ts'
|
||||
import Sherlock from 'sherlockjs'
|
||||
|
||||
export function parseDueDate(text: string): number | null {
|
||||
try {
|
||||
const parsed = Sherlock.parse(text)
|
||||
if (parsed && parsed.startDate) {
|
||||
return parsed.startDate.getTime()
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
const ts = Date.parse(text)
|
||||
return Number.isNaN(ts) ? null : ts
|
||||
}
|
||||
|
||||
export function stopWorkLogging(t: Task) {
|
||||
if (t.logs && t.logs.length) {
|
||||
const lastLog = t.logs[t.logs.length - 1]
|
||||
if (lastLog.start && !lastLog.end) {
|
||||
lastLog.end = Date.now()
|
||||
}
|
||||
}
|
||||
else {
|
||||
t.logs = []
|
||||
}
|
||||
return t
|
||||
}
|
||||
212
src/utils/parser.ts
Normal file
212
src/utils/parser.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
export type Command = {
|
||||
command: string
|
||||
tag?: string
|
||||
text?: string
|
||||
id?: string
|
||||
} | null
|
||||
function parseTaskCommand(str: string) {
|
||||
return str.match(/^(t(?:ask)?)\s(@\S*[0-9a-z'-])?([\s\S]*)/i)
|
||||
}
|
||||
function parseEditCommand(str: string) {
|
||||
return str.match(/^(e(?:dit)?)\s(\d+)([\s\S]*)/i)
|
||||
}
|
||||
const parseDueCommand = (str: string) => str.match(/^(due)\s(\d+)([\s\S]*)/i)
|
||||
function parseMoveCommand(str: string) {
|
||||
return str.match(/^(mv|move)\s(?:(\d+)\s)+(@\S*[0-9a-z'-])/i)
|
||||
}
|
||||
function parseCheckCommand(str: string) {
|
||||
return str.match(/^(c(?:heck)?)\s([\s\S]*)/i)
|
||||
}
|
||||
function parseBeginCommand(str: string) {
|
||||
return str.match(/^(b(?:egin)?)\s([\s\S]*)/i)
|
||||
}
|
||||
function parseDeleteCommand(str: string) {
|
||||
return str.match(/^(d(?:elete)?)\s([\s\S]*)/i)
|
||||
}
|
||||
const parseFlagCommand = (str: string) => str.match(/^(fl(?:ag)?)\s([\s\S]*)/i)
|
||||
const parseStopCommand = (str: string) => str.match(/^(st(?:op)?)\s([\s\S]*)/i)
|
||||
function parseSwitchCommand(str: string) {
|
||||
return str.match(/^(sw(?:itch)?)\s([\s\S]*)/i)
|
||||
}
|
||||
function parseArchiveCommand(str: string) {
|
||||
return str.match(/^(a(?:rchive)?)\s([\s\S]*)/i)
|
||||
}
|
||||
function parseRestoreCommand(str: string) {
|
||||
return str.match(/^(re(?:store)?)\s([\s\S]*)/i)
|
||||
}
|
||||
function parseTagRenameCommand(str: string) {
|
||||
return str.match(
|
||||
/^(tr|tagre|tagrename)\s(@\S*[0-9a-z'-])\s(@\S*[0-9a-z'-])/i,
|
||||
)
|
||||
}
|
||||
function parseVisibilityCommand(str: string) {
|
||||
return str.match(
|
||||
/^(hide|show)\s(done|finished|wait|pending|ongoing|wip|flag|flagged)\b/i,
|
||||
)
|
||||
}
|
||||
function parseOtherCommand(str: string) {
|
||||
return str.match(
|
||||
/^(help|quickhelp|today|dark|light|setting|customize|list-archived|login|logout)/i,
|
||||
)
|
||||
}
|
||||
const parseTextFallback = (str: string) => str.match(/(\b\w*\b\S*\s)/)
|
||||
const parseSearch = (str: string) => str.match(/^\/.*/)
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
||||
function compileTaskCommand(input: string) {
|
||||
const matchTask = parseTaskCommand(input)
|
||||
if (matchTask) {
|
||||
return {
|
||||
command: matchTask[1],
|
||||
tag: matchTask[2],
|
||||
text: matchTask[3].trim(),
|
||||
} as Command
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function compileEditCommand(input: string) {
|
||||
const matchEdit = parseEditCommand(input)
|
||||
if (matchEdit) {
|
||||
return {
|
||||
command: matchEdit[1],
|
||||
id: matchEdit[2],
|
||||
text: matchEdit[3].trim(),
|
||||
} as Command
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function compileDueCommand(input: string) {
|
||||
const matchDue = parseDueCommand(input)
|
||||
if (matchDue) {
|
||||
return {
|
||||
command: matchDue[1],
|
||||
id: matchDue[2],
|
||||
text: matchDue[3].trim(),
|
||||
} as Command
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function compileMoveCommand(input: string) {
|
||||
const matchMove = parseMoveCommand(input)
|
||||
if (matchMove) {
|
||||
return {
|
||||
command: matchMove[1],
|
||||
id: matchMove[2],
|
||||
tag: matchMove[3],
|
||||
} as Command
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function compileTagReCommand(input: string) {
|
||||
const matchTagRe = parseTagRenameCommand(input)
|
||||
if (matchTagRe) {
|
||||
return {
|
||||
command: matchTagRe[1],
|
||||
tag: `${matchTagRe[2]} ${matchTagRe[3]}`,
|
||||
} as Command
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function compileMathOtherCommand(input: string) {
|
||||
const matchOther
|
||||
= parseCheckCommand(input)
|
||||
|| parseBeginCommand(input)
|
||||
|| parseDeleteCommand(input)
|
||||
|| parseFlagCommand(input)
|
||||
|| parseStopCommand(input)
|
||||
|| parseSwitchCommand(input)
|
||||
|| parseArchiveCommand(input)
|
||||
|| parseRestoreCommand(input)
|
||||
if (matchOther) {
|
||||
return {
|
||||
command: matchOther[1],
|
||||
id: matchOther[2],
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function compileVisibilityCommand(input: string) {
|
||||
const matchVisibility = parseVisibilityCommand(input)
|
||||
if (matchVisibility) {
|
||||
return {
|
||||
command: matchVisibility[1],
|
||||
text: matchVisibility[2],
|
||||
} as Command
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function compileHelpCommand(input: string) {
|
||||
const matchHelp = parseOtherCommand(input)
|
||||
if (matchHelp) {
|
||||
return {
|
||||
command: matchHelp[1],
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function compileSearchCommand(input: string) {
|
||||
if (parseSearch(input)) {
|
||||
return {
|
||||
command: 'search',
|
||||
text: input.replace(/^\//, ''),
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
||||
export function parseCommand(input: string): Command {
|
||||
let ret
|
||||
|
||||
ret = compileSearchCommand(input)
|
||||
if (ret)
|
||||
return ret
|
||||
|
||||
ret = compileTaskCommand(input)
|
||||
if (ret)
|
||||
return ret
|
||||
|
||||
ret = compileEditCommand(input)
|
||||
if (ret)
|
||||
return ret
|
||||
|
||||
ret = compileDueCommand(input)
|
||||
if (ret)
|
||||
return ret
|
||||
|
||||
ret = compileMoveCommand(input)
|
||||
if (ret)
|
||||
return ret
|
||||
|
||||
ret = compileTagReCommand(input)
|
||||
if (ret)
|
||||
return ret
|
||||
|
||||
ret = compileMathOtherCommand(input)
|
||||
if (ret)
|
||||
return ret
|
||||
|
||||
ret = compileVisibilityCommand(input)
|
||||
if (ret)
|
||||
return ret
|
||||
|
||||
ret = compileHelpCommand(input)
|
||||
if (ret)
|
||||
return ret
|
||||
|
||||
if (parseTextFallback(input)) {
|
||||
return compileTaskCommand(`task ${input}`)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user