mirror of https://github.com/TriliumNext/Notes
commit
58a8821c22
@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "tasks"
|
||||||
|
(
|
||||||
|
"taskId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"parentNoteId" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL DEFAULT "",
|
||||||
|
"dueDate" INTEGER,
|
||||||
|
"isDone" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"isDeleted" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"utcDateModified" TEXT NOT NULL
|
||||||
|
);
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
import date_utils from "../../services/date_utils.js";
|
||||||
|
import AbstractBeccaEntity from "./abstract_becca_entity.js";
|
||||||
|
import type BOption from "./boption.js";
|
||||||
|
import type { TaskRow } from "./rows.js";
|
||||||
|
|
||||||
|
export default class BTask extends AbstractBeccaEntity<BOption> {
|
||||||
|
|
||||||
|
static get entityName() {
|
||||||
|
return "tasks";
|
||||||
|
}
|
||||||
|
|
||||||
|
static get primaryKeyName() {
|
||||||
|
return "taskId";
|
||||||
|
}
|
||||||
|
|
||||||
|
static get hashedProperties() {
|
||||||
|
return [ "taskId", "parentNoteId", "title", "dueDate", "isDone", "isDeleted" ];
|
||||||
|
}
|
||||||
|
|
||||||
|
taskId?: string;
|
||||||
|
parentNoteId!: string;
|
||||||
|
title!: string;
|
||||||
|
dueDate?: string;
|
||||||
|
isDone!: boolean;
|
||||||
|
private _isDeleted?: boolean;
|
||||||
|
|
||||||
|
constructor(row?: TaskRow) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateFromRow(row);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDeleted() {
|
||||||
|
return !!this._isDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromRow(row: TaskRow) {
|
||||||
|
this.taskId = row.taskId;
|
||||||
|
this.parentNoteId = row.parentNoteId;
|
||||||
|
this.title = row.title;
|
||||||
|
this.dueDate = row.dueDate;
|
||||||
|
this.isDone = !!row.isDone;
|
||||||
|
this._isDeleted = !!row.isDeleted;
|
||||||
|
this.utcDateModified = row.utcDateModified;
|
||||||
|
|
||||||
|
if (this.taskId) {
|
||||||
|
this.becca.tasks[this.taskId] = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.taskId) {
|
||||||
|
this.becca.tasks[this.taskId] = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected beforeSaving(opts?: {}): void {
|
||||||
|
super.beforeSaving();
|
||||||
|
|
||||||
|
this.utcDateModified = date_utils.utcNowDateTime();
|
||||||
|
|
||||||
|
if (this.taskId) {
|
||||||
|
this.becca.tasks[this.taskId] = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPojo() {
|
||||||
|
return {
|
||||||
|
taskId: this.taskId,
|
||||||
|
parentNoteId: this.parentNoteId,
|
||||||
|
title: this.title,
|
||||||
|
dueDate: this.dueDate,
|
||||||
|
isDone: this.isDone,
|
||||||
|
isDeleted: this.isDeleted,
|
||||||
|
utcDateModified: this.utcDateModified
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import type { Froca } from "../services/froca-interface.js";
|
||||||
|
|
||||||
|
export interface FTaskRow {
|
||||||
|
taskId: string;
|
||||||
|
parentNoteId: string;
|
||||||
|
title: string;
|
||||||
|
dueDate?: string;
|
||||||
|
isDone?: boolean;
|
||||||
|
utcDateModified: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class FTask {
|
||||||
|
private froca: Froca;
|
||||||
|
taskId!: string;
|
||||||
|
parentNoteId!: string;
|
||||||
|
title!: string;
|
||||||
|
dueDate?: string;
|
||||||
|
isDone!: boolean;
|
||||||
|
utcDateModified!: string;
|
||||||
|
|
||||||
|
constructor(froca: Froca, row: FTaskRow) {
|
||||||
|
this.froca = froca;
|
||||||
|
this.update(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(row: FTaskRow) {
|
||||||
|
this.taskId = row.taskId;
|
||||||
|
this.parentNoteId = row.parentNoteId;
|
||||||
|
this.title = row.title;
|
||||||
|
this.dueDate = row.dueDate;
|
||||||
|
this.isDone = !!row.isDone;
|
||||||
|
this.utcDateModified = row.utcDateModified;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import server from "./server.js";
|
||||||
|
|
||||||
|
interface CreateNewTasksOpts {
|
||||||
|
parentNoteId: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createNewTask({ parentNoteId, title }: CreateNewTasksOpts) {
|
||||||
|
await server.post(`tasks`, {
|
||||||
|
parentNoteId,
|
||||||
|
title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function toggleTaskDone(taskId: string) {
|
||||||
|
await server.post(`tasks/${taskId}/toggle`);
|
||||||
|
}
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
import type FTask from "../../entities/ftask.js";
|
||||||
|
import froca from "../../services/froca.js";
|
||||||
|
import TypeWidget from "./type_widget.js";
|
||||||
|
import * as taskService from "../../services/tasks.js";
|
||||||
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
|
||||||
|
const TPL = `
|
||||||
|
<div class="note-detail-task-list note-detail-printable">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<input type="text" placeholder="Add a new task" class="add-new-task" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ol class="task-container">
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.note-detail-task-list {
|
||||||
|
height: 100%;
|
||||||
|
contain: none;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-task-list header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
background-color: var(--main-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-task-list .add-new-task {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-task-list .task-container {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: var(--bs-border-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-task-list .task-container li {
|
||||||
|
background: var(--input-background-color);
|
||||||
|
border-bottom: 1px solid var(--main-background-color);
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-task-list .task-container li .check {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function buildTask(task: FTask) {
|
||||||
|
return `\
|
||||||
|
<li class="task">
|
||||||
|
<input type="checkbox" class="check" data-task-id="${task.taskId}" ${task.isDone ? "checked" : ""} /> ${task.title}
|
||||||
|
</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TaskListWidget extends TypeWidget {
|
||||||
|
|
||||||
|
private $taskContainer!: JQuery<HTMLElement>;
|
||||||
|
private $addNewTask!: JQuery<HTMLElement>;
|
||||||
|
|
||||||
|
static getType() { return "taskList" }
|
||||||
|
|
||||||
|
doRender() {
|
||||||
|
this.$widget = $(TPL);
|
||||||
|
this.$addNewTask = this.$widget.find(".add-new-task");
|
||||||
|
this.$taskContainer = this.$widget.find(".task-container");
|
||||||
|
|
||||||
|
this.$addNewTask.on("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
this.#createNewTask(String(this.$addNewTask.val()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$taskContainer.on("change", "input", (e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
const taskId = target.dataset.taskId;
|
||||||
|
|
||||||
|
if (!taskId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskService.toggleTaskDone(taskId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async #createNewTask(title: string) {
|
||||||
|
if (!title || !this.noteId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await taskService.createNewTask({
|
||||||
|
title,
|
||||||
|
parentNoteId: this.noteId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async doRefresh(note: FNote) {
|
||||||
|
this.$widget.show();
|
||||||
|
|
||||||
|
if (!this.note || !this.noteId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$taskContainer.html("");
|
||||||
|
|
||||||
|
const tasks = await froca.getTasks(this.noteId);
|
||||||
|
for (const task of tasks) {
|
||||||
|
this.$taskContainer.append($(buildTask(task)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
|
if (this.noteId && loadResults.isTaskListReloaded(this.noteId)) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import type { Request } from "express";
|
||||||
|
import * as tasksService from "../../services/tasks.js";
|
||||||
|
|
||||||
|
export function getTasks(req: Request) {
|
||||||
|
const { parentNoteId } = req.params;
|
||||||
|
return tasksService.getTasks(parentNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNewTask(req: Request) {
|
||||||
|
return tasksService.createNewTask(req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleTaskDone(req: Request) {
|
||||||
|
const { taskId } = req.params;
|
||||||
|
if (!taskId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasksService.toggleTaskDone(taskId);
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import becca from "../becca/becca.js";
|
||||||
|
import BTask from "../becca/entities/btask.js";
|
||||||
|
|
||||||
|
export function getTasks(parentNoteId: string) {
|
||||||
|
return becca.getTasks()
|
||||||
|
.filter((task) => task.parentNoteId === parentNoteId && !task.isDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateTaskParams {
|
||||||
|
parentNoteId: string;
|
||||||
|
title: string;
|
||||||
|
dueDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNewTask(params: CreateTaskParams) {
|
||||||
|
const task = new BTask(params);
|
||||||
|
task.save();
|
||||||
|
|
||||||
|
return {
|
||||||
|
task
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleTaskDone(taskId: string) {
|
||||||
|
const task = becca.tasks[taskId];
|
||||||
|
task.isDone = !task.isDone;
|
||||||
|
task.save();
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue