From c0e42e23a692ce5f0baf38a9d23fa610fe55e982 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 18 Feb 2025 18:42:26 +0200 Subject: [PATCH 01/27] feat(tasks): create backend model for task --- src/becca/becca_loader.ts | 7 +++- src/becca/entities/btask.ts | 58 ++++++++++++++++++++++++++++++ src/becca/entities/rows.ts | 9 +++++ src/becca/entity_constructor.ts | 4 ++- src/services/consistency_checks.ts | 2 +- 5 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/becca/entities/btask.ts diff --git a/src/becca/becca_loader.ts b/src/becca/becca_loader.ts index 8397b2dec..862e3e8be 100644 --- a/src/becca/becca_loader.ts +++ b/src/becca/becca_loader.ts @@ -11,9 +11,10 @@ import BOption from "./entities/boption.js"; import BEtapiToken from "./entities/betapi_token.js"; import cls from "../services/cls.js"; import entityConstructor from "../becca/entity_constructor.js"; -import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from "./entities/rows.js"; +import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow, TaskRow } from "./entities/rows.js"; import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import ws from "../services/ws.js"; +import BTask from "./entities/btask.js"; const beccaLoaded = new Promise(async (res, rej) => { const sqlInit = (await import("../services/sql_init.js")).default; @@ -63,6 +64,10 @@ function load() { for (const row of sql.getRows(`SELECT etapiTokenId, name, tokenHash, utcDateCreated, utcDateModified FROM etapi_tokens WHERE isDeleted = 0`)) { new BEtapiToken(row); } + + for (const row of sql.getRows(`SELECT taskId, parentNoteId, title, dueDate, isDone, isDeleted FROM tasks WHERE isDeleted = 0`)) { + new BTask(row); + } }); for (const noteId in becca.notes) { diff --git a/src/becca/entities/btask.ts b/src/becca/entities/btask.ts new file mode 100644 index 000000000..a0c8ce232 --- /dev/null +++ b/src/becca/entities/btask.ts @@ -0,0 +1,58 @@ +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 { + + 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); + } + + 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._isDeleted = !!row.isDeleted; + } + + getPojo() { + return { + taskId: this.taskId, + parentNoteId: this.parentNoteId, + title: this.title, + dueDate: this.dueDate, + isDeleted: this.isDeleted + }; + } + +} diff --git a/src/becca/entities/rows.ts b/src/becca/entities/rows.ts index 89fa36953..08a296c7a 100644 --- a/src/becca/entities/rows.ts +++ b/src/becca/entities/rows.ts @@ -136,3 +136,12 @@ export interface NoteRow { utcDateModified: string; content?: string | Buffer; } + +export interface TaskRow { + taskId: string; + parentNoteId: string; + title: string; + dueDate?: string; + isDone: boolean; + isDeleted: boolean; +} diff --git a/src/becca/entity_constructor.ts b/src/becca/entity_constructor.ts index 18f7a14c7..5e9d3e013 100644 --- a/src/becca/entity_constructor.ts +++ b/src/becca/entity_constructor.ts @@ -9,6 +9,7 @@ import BNote from "./entities/bnote.js"; import BOption from "./entities/boption.js"; import BRecentNote from "./entities/brecent_note.js"; import BRevision from "./entities/brevision.js"; +import BTask from "./entities/btask.js"; type EntityClass = new (row?: any) => AbstractBeccaEntity; @@ -21,7 +22,8 @@ const ENTITY_NAME_TO_ENTITY: Record & EntityClass> notes: BNote, options: BOption, recent_notes: BRecentNote, - revisions: BRevision + revisions: BRevision, + tasks: BTask }; function getEntityFromEntityName(entityName: keyof typeof ENTITY_NAME_TO_ENTITY) { diff --git a/src/services/consistency_checks.ts b/src/services/consistency_checks.ts index 8e7da9c9e..ae678f807 100644 --- a/src/services/consistency_checks.ts +++ b/src/services/consistency_checks.ts @@ -888,7 +888,7 @@ class ConsistencyChecks { return `${tableName}: ${count}`; } - const tables = ["notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens", "blobs"]; + const tables = ["notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens", "blobs", "tasks"]; log.info(`Table counts: ${tables.map((tableName) => getTableRowCount(tableName)).join(", ")}`); } From 98dff6130506c437e46f514809b3890b8c915e95 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 18 Feb 2025 19:06:02 +0200 Subject: [PATCH 02/27] feat(tasks): add GET API --- src/becca/becca-interface.ts | 7 +++++++ src/becca/entities/btask.ts | 11 +++++++++++ src/routes/api/tasks.ts | 5 +++++ src/routes/routes.ts | 3 +++ src/services/tasks.ts | 5 +++++ 5 files changed, 31 insertions(+) create mode 100644 src/routes/api/tasks.ts create mode 100644 src/services/tasks.ts diff --git a/src/becca/becca-interface.ts b/src/becca/becca-interface.ts index cdee5d1cd..8da95c39f 100644 --- a/src/becca/becca-interface.ts +++ b/src/becca/becca-interface.ts @@ -12,6 +12,7 @@ import type { AttachmentRow, BlobRow, RevisionRow } from "./entities/rows.js"; import BBlob from "./entities/bblob.js"; import BRecentNote from "./entities/brecent_note.js"; import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; +import type BTask from "./entities/btask.js"; interface AttachmentOpts { includeContentLength?: boolean; @@ -32,6 +33,7 @@ export default class Becca { attributeIndex!: Record; options!: Record; etapiTokens!: Record; + tasks!: Record; allNoteSetCache: NoteSet | null; @@ -48,6 +50,7 @@ export default class Becca { this.attributeIndex = {}; this.options = {}; this.etapiTokens = {}; + this.tasks = {}; this.dirtyNoteSetCache(); @@ -213,6 +216,10 @@ export default class Becca { return this.etapiTokens[etapiTokenId]; } + getTasks(): BTask[] { + return Object.values(this.tasks); + } + getEntity>(entityName: string, entityId: string): AbstractBeccaEntity | null { if (!entityName || !entityId) { return null; diff --git a/src/becca/entities/btask.ts b/src/becca/entities/btask.ts index a0c8ce232..d84d09e7e 100644 --- a/src/becca/entities/btask.ts +++ b/src/becca/entities/btask.ts @@ -31,6 +31,7 @@ export default class BTask extends AbstractBeccaEntity { } this.updateFromRow(row); + this.init(); } get isDeleted() { @@ -43,6 +44,16 @@ export default class BTask extends AbstractBeccaEntity { this.title = row.title; this.dueDate = row.dueDate; this._isDeleted = !!row.isDeleted; + + if (this.taskId) { + this.becca.tasks[this.taskId] = this; + } + } + + init() { + if (this.taskId) { + this.becca.tasks[this.taskId] = this; + } } getPojo() { diff --git a/src/routes/api/tasks.ts b/src/routes/api/tasks.ts new file mode 100644 index 000000000..ac4d0a5b8 --- /dev/null +++ b/src/routes/api/tasks.ts @@ -0,0 +1,5 @@ +import * as tasksService from "../../services/tasks.js"; + +export function getTasks() { + return tasksService.getTasks(); +} diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 05c7612f2..8eaa1f96b 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -72,6 +72,7 @@ import etapiSpecRoute from "../etapi/spec.js"; import etapiBackupRoute from "../etapi/backup.js"; import apiDocsRoute from "./api_docs.js"; +import * as tasksRoute from "./api/tasks.js"; const MAX_ALLOWED_FILE_SIZE_MB = 250; const GET = "get", @@ -279,6 +280,8 @@ function register(app: express.Application) { apiRoute(PATCH, "/api/etapi-tokens/:etapiTokenId", etapiTokensApiRoutes.patchToken); apiRoute(DEL, "/api/etapi-tokens/:etapiTokenId", etapiTokensApiRoutes.deleteToken); + apiRoute(GET, "/api/tasks", tasksRoute.getTasks); + // in case of local electron, local calls are allowed unauthenticated, for server they need auth const clipperMiddleware = isElectron ? [] : [auth.checkEtapiToken]; diff --git a/src/services/tasks.ts b/src/services/tasks.ts new file mode 100644 index 000000000..58fa6d8bb --- /dev/null +++ b/src/services/tasks.ts @@ -0,0 +1,5 @@ +import becca from "../becca/becca.js"; + +export function getTasks() { + return becca.getTasks(); +} From 17f9fa7e895d1de35c6fe60986279375a10114bf Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 18 Feb 2025 19:30:02 +0200 Subject: [PATCH 03/27] feat(tasks): add POST API --- src/becca/entities/btask.ts | 3 ++- src/becca/entities/rows.ts | 6 +++--- src/routes/api/tasks.ts | 5 +++++ src/routes/routes.ts | 1 + src/services/tasks.ts | 16 ++++++++++++++++ 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/becca/entities/btask.ts b/src/becca/entities/btask.ts index d84d09e7e..64ae3d954 100644 --- a/src/becca/entities/btask.ts +++ b/src/becca/entities/btask.ts @@ -16,7 +16,7 @@ export default class BTask extends AbstractBeccaEntity { return [ "taskId", "parentNoteId", "title", "dueDate", "isDone", "isDeleted" ]; } - taskId!: string; + taskId?: string; parentNoteId!: string; title!: string; dueDate?: string; @@ -43,6 +43,7 @@ export default class BTask extends AbstractBeccaEntity { this.parentNoteId = row.parentNoteId; this.title = row.title; this.dueDate = row.dueDate; + this.isDone = !!row.isDeleted; this._isDeleted = !!row.isDeleted; if (this.taskId) { diff --git a/src/becca/entities/rows.ts b/src/becca/entities/rows.ts index 08a296c7a..c9e67de56 100644 --- a/src/becca/entities/rows.ts +++ b/src/becca/entities/rows.ts @@ -138,10 +138,10 @@ export interface NoteRow { } export interface TaskRow { - taskId: string; + taskId?: string; parentNoteId: string; title: string; dueDate?: string; - isDone: boolean; - isDeleted: boolean; + isDone?: boolean; + isDeleted?: boolean; } diff --git a/src/routes/api/tasks.ts b/src/routes/api/tasks.ts index ac4d0a5b8..33516ef28 100644 --- a/src/routes/api/tasks.ts +++ b/src/routes/api/tasks.ts @@ -1,5 +1,10 @@ +import type { Request } from "express"; import * as tasksService from "../../services/tasks.js"; export function getTasks() { return tasksService.getTasks(); } + +export function createNewTask(req: Request) { + return tasksService.createNewTask(req.body); +} diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 8eaa1f96b..6cff1e033 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -281,6 +281,7 @@ function register(app: express.Application) { apiRoute(DEL, "/api/etapi-tokens/:etapiTokenId", etapiTokensApiRoutes.deleteToken); apiRoute(GET, "/api/tasks", tasksRoute.getTasks); + apiRoute(PST, "/api/tasks", tasksRoute.createNewTask); // in case of local electron, local calls are allowed unauthenticated, for server they need auth const clipperMiddleware = isElectron ? [] : [auth.checkEtapiToken]; diff --git a/src/services/tasks.ts b/src/services/tasks.ts index 58fa6d8bb..6a2f93710 100644 --- a/src/services/tasks.ts +++ b/src/services/tasks.ts @@ -1,5 +1,21 @@ import becca from "../becca/becca.js"; +import BTask from "../becca/entities/btask.js"; export function getTasks() { return becca.getTasks(); } + +interface CreateTaskParams { + parentNoteId: string; + title: string; + dueDate?: string; +} + +export function createNewTask(params: CreateTaskParams) { + const task = new BTask(params); + task.save(); + + return { + task + } +} From 1024733252915319e20b27ddc72bbd292db5309f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 18 Feb 2025 19:42:27 +0200 Subject: [PATCH 04/27] feat(client/tasks): create task list note type --- src/public/app/entities/fnote.ts | 5 ++-- src/public/app/services/note_types.ts | 1 + src/public/app/widgets/note_detail.ts | 4 ++- src/public/app/widgets/note_type.ts | 3 ++- .../app/widgets/type_widgets/task_list.ts | 26 +++++++++++++++++++ src/public/translations/en/translation.json | 3 ++- src/services/note_types.ts | 3 ++- 7 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 src/public/app/widgets/type_widgets/task_list.ts diff --git a/src/public/app/entities/fnote.ts b/src/public/app/entities/fnote.ts index 2e0293112..ec200bdfa 100644 --- a/src/public/app/entities/fnote.ts +++ b/src/public/app/entities/fnote.ts @@ -28,7 +28,8 @@ const NOTE_TYPE_ICONS = { doc: "bx bxs-file-doc", contentWidget: "bx bxs-widget", mindMap: "bx bx-sitemap", - geoMap: "bx bx-map-alt" + geoMap: "bx bx-map-alt", + taskList: "bx bx-list-check" }; /** @@ -36,7 +37,7 @@ const NOTE_TYPE_ICONS = { * end user. Those types should be used only for checking against, they are * not for direct use. */ -export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap"; +export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap" | "taskList"; export interface NotePathRecord { isArchived: boolean; diff --git a/src/public/app/services/note_types.ts b/src/public/app/services/note_types.ts index 7aebd48ff..fc6e7bed6 100644 --- a/src/public/app/services/note_types.ts +++ b/src/public/app/services/note_types.ts @@ -20,6 +20,7 @@ async function getNoteTypeItems(command?: NoteTypeCommandNames) { { title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" }, { title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" }, { title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" }, + { title: t("note_types.task-list"), command, type: "taskList", uiIcon: "bx bx-list-check" } ]; const templateNoteIds = await server.get("search-templates"); diff --git a/src/public/app/widgets/note_detail.ts b/src/public/app/widgets/note_detail.ts index afd51a6ae..a16de3ece 100644 --- a/src/public/app/widgets/note_detail.ts +++ b/src/public/app/widgets/note_detail.ts @@ -35,6 +35,7 @@ import GeoMapTypeWidget from "./type_widgets/geo_map.js"; import utils from "../services/utils.js"; import type { NoteType } from "../entities/fnote.js"; import type TypeWidget from "./type_widgets/type_widget.js"; +import TaskListWidget from "./type_widgets/task_list.js"; const TPL = `
@@ -72,7 +73,8 @@ const typeWidgetClasses = { attachmentDetail: AttachmentDetailTypeWidget, attachmentList: AttachmentListTypeWidget, mindMap: MindMapWidget, - geoMap: GeoMapTypeWidget + geoMap: GeoMapTypeWidget, + taskList: TaskListWidget }; /** diff --git a/src/public/app/widgets/note_type.ts b/src/public/app/widgets/note_type.ts index 9b3b90665..1824974fd 100644 --- a/src/public/app/widgets/note_type.ts +++ b/src/public/app/widgets/note_type.ts @@ -48,7 +48,8 @@ const NOTE_TYPES: NoteTypeMapping[] = [ { type: "image", title: t("note_types.image"), selectable: false }, { type: "launcher", mime: "", title: t("note_types.launcher"), selectable: false }, { type: "noteMap", mime: "", title: t("note_types.note-map"), selectable: false }, - { type: "search", title: t("note_types.saved-search"), selectable: false } + { type: "search", title: t("note_types.saved-search"), selectable: false }, + { type: "taskList", title: t("note_types.task-list"), selectable: false } ]; const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => !nt.selectable).map((nt) => nt.type); diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts new file mode 100644 index 000000000..30a5572a2 --- /dev/null +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -0,0 +1,26 @@ +import type FNote from "../../entities/fnote.js"; +import TypeWidget from "./type_widget.js"; + +const TPL = ` +
+ Task list goes here. +
+`; + +export default class TaskListWidget extends TypeWidget { + + static getType() { return "taskList" } + + doRender() { + this.$widget = $(TPL); + } + + async doRefresh(note: FNote) { + this.$widget.show(); + + if (!this.note) { + return; + } + } + +} diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 1a5d92b32..feaba40ef 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1416,7 +1416,8 @@ "widget": "Widget", "confirm-change": "It is not recommended to change note type when note content is not empty. Do you want to continue anyway?", "geo-map": "Geo Map", - "beta-feature": "Beta" + "beta-feature": "Beta", + "task-list": "To-Do List" }, "protect_note": { "toggle-on": "Protect the note", diff --git a/src/services/note_types.ts b/src/services/note_types.ts index 3e086acf4..46a2e01a9 100644 --- a/src/services/note_types.ts +++ b/src/services/note_types.ts @@ -15,7 +15,8 @@ const noteTypes = [ { type: "doc", defaultMime: "" }, { type: "contentWidget", defaultMime: "" }, { type: "mindMap", defaultMime: "application/json" }, - { type: "geoMap", defaultMime: "application/json" } + { type: "geoMap", defaultMime: "application/json" }, + { type: "taskList", defaultMime: "" } ]; function getDefaultMimeForNoteType(typeName: string) { From 7cba5a7c7d87761f3dda9b0c66cb1644517998fe Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 18 Feb 2025 19:58:00 +0200 Subject: [PATCH 05/27] feat(client/tasks): display tasks --- src/public/app/entities/ftask.ts | 31 +++++++++++++++++++ src/public/app/services/froca.ts | 18 +++++++++++ .../app/widgets/type_widgets/task_list.ts | 19 +++++++++++- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/public/app/entities/ftask.ts diff --git a/src/public/app/entities/ftask.ts b/src/public/app/entities/ftask.ts new file mode 100644 index 000000000..b6e70b00b --- /dev/null +++ b/src/public/app/entities/ftask.ts @@ -0,0 +1,31 @@ +import type { Froca } from "../services/froca-interface.js"; + +export interface FTaskRow { + taskId: string; + parentNoteId: string; + title: string; + dueDate?: string; + isDone?: boolean; +} + +export default class FTask { + private froca: Froca; + taskId!: string; + parentNoteId!: string; + title!: string; + dueDate?: string; + isDone!: boolean; + + 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; + } +} diff --git a/src/public/app/services/froca.ts b/src/public/app/services/froca.ts index 8849d6331..36e25a5a0 100644 --- a/src/public/app/services/froca.ts +++ b/src/public/app/services/froca.ts @@ -6,6 +6,8 @@ import appContext from "../components/app_context.js"; import FBlob, { type FBlobRow } from "../entities/fblob.js"; import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; import type { Froca } from "./froca-interface.js"; +import FTask from "../entities/ftask.js"; +import type { FTaskRow } from "../entities/ftask.js"; interface SubtreeResponse { notes: FNoteRow[]; @@ -37,6 +39,7 @@ class FrocaImpl implements Froca { attributes!: Record; attachments!: Record; blobPromises!: Record | null>; + tasks!: Record; constructor() { this.initializedPromise = this.loadInitialTree(); @@ -52,6 +55,7 @@ class FrocaImpl implements Froca { this.attributes = {}; this.attachments = {}; this.blobPromises = {}; + this.tasks = {}; this.addResp(resp); } @@ -368,6 +372,20 @@ class FrocaImpl implements Froca { }); } + async getTasks() { + const taskRows = await server.get(`tasks`); + return this.processTaskRow(taskRows); + } + + processTaskRow(taskRows: FTaskRow[]): FTask[] { + return taskRows.map((taskRow) => { + const task = new FTask(this, taskRow); + this.tasks[task.taskId] = task; + + return task; + }); + } + async getBlob(entityType: string, entityId: string) { // I'm not sure why we're not using blobIds directly, it would save us this composite key ... // perhaps one benefit is that we're always requesting the latest blob, not relying on perhaps faulty/slow diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts index 30a5572a2..41abe01e2 100644 --- a/src/public/app/widgets/type_widgets/task_list.ts +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -1,18 +1,28 @@ 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"; const TPL = `
- Task list goes here. +
+
`; +function buildTask(task: FTask) { + return `
${task.title}
`; +} + export default class TaskListWidget extends TypeWidget { + private $taskContainer!: JQuery; + static getType() { return "taskList" } doRender() { this.$widget = $(TPL); + this.$taskContainer = this.$widget.find(".task-container"); } async doRefresh(note: FNote) { @@ -21,6 +31,13 @@ export default class TaskListWidget extends TypeWidget { if (!this.note) { return; } + + this.$taskContainer.clearQueue(); + + const tasks = await froca.getTasks(); + for (const task of tasks) { + this.$taskContainer.append($(buildTask(task))); + } } } From fc1ee7c6f0a512e1cec7cd5541fe2a2aab100700 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 18 Feb 2025 20:01:04 +0200 Subject: [PATCH 06/27] feat(client/tasks): add a text box for adding a new task --- .../app/widgets/type_widgets/task_list.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts index 41abe01e2..b3bb90da9 100644 --- a/src/public/app/widgets/type_widgets/task_list.ts +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -5,8 +5,28 @@ import TypeWidget from "./type_widget.js"; const TPL = `
+ +
+ +
+
+ +
`; From c00505cd7b738b6524eb14a77b86fe3ed06d4e4e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 18 Feb 2025 21:06:51 +0200 Subject: [PATCH 07/27] feat(client/tasks): create flow for creating a task --- src/public/app/services/tasks.ts | 7 +++++++ .../app/widgets/type_widgets/task_list.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/public/app/services/tasks.ts diff --git a/src/public/app/services/tasks.ts b/src/public/app/services/tasks.ts new file mode 100644 index 000000000..a5a6a9ceb --- /dev/null +++ b/src/public/app/services/tasks.ts @@ -0,0 +1,7 @@ +import server from "./server.js"; + +export async function createNewTask(title: string) { + await server.post(`tasks`, { + title + }); +} diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts index b3bb90da9..faf9a5687 100644 --- a/src/public/app/widgets/type_widgets/task_list.ts +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -2,6 +2,7 @@ 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"; const TPL = `
@@ -37,12 +38,28 @@ function buildTask(task: FTask) { export default class TaskListWidget extends TypeWidget { private $taskContainer!: JQuery; + private $addNewTask!: JQuery; 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())); + } + }); + } + + async #createNewTask(title: string) { + if (!title) { + return; + } + + await taskService.createNewTask(title); } async doRefresh(note: FNote) { From 373e0b45f2a92efc009ed2549a8c0371149f47d8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 18 Feb 2025 21:07:06 +0200 Subject: [PATCH 08/27] fix(server/tasks): missing utcDateModified causing errors on create --- src/becca/entities/btask.ts | 11 ++++++++++- src/becca/entities/rows.ts | 1 + src/public/app/entities/ftask.ts | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/becca/entities/btask.ts b/src/becca/entities/btask.ts index 64ae3d954..85bcca46a 100644 --- a/src/becca/entities/btask.ts +++ b/src/becca/entities/btask.ts @@ -1,3 +1,4 @@ +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"; @@ -45,6 +46,7 @@ export default class BTask extends AbstractBeccaEntity { this.dueDate = row.dueDate; this.isDone = !!row.isDeleted; this._isDeleted = !!row.isDeleted; + this.utcDateModified = row.utcDateModified; if (this.taskId) { this.becca.tasks[this.taskId] = this; @@ -57,13 +59,20 @@ export default class BTask extends AbstractBeccaEntity { } } + protected beforeSaving(opts?: {}): void { + super.beforeSaving(); + + this.utcDateModified = date_utils.utcNowDateTime(); + } + getPojo() { return { taskId: this.taskId, parentNoteId: this.parentNoteId, title: this.title, dueDate: this.dueDate, - isDeleted: this.isDeleted + isDeleted: this.isDeleted, + utcDateModified: this.utcDateModified }; } diff --git a/src/becca/entities/rows.ts b/src/becca/entities/rows.ts index c9e67de56..85b9e1667 100644 --- a/src/becca/entities/rows.ts +++ b/src/becca/entities/rows.ts @@ -144,4 +144,5 @@ export interface TaskRow { dueDate?: string; isDone?: boolean; isDeleted?: boolean; + utcDateModified: string; } diff --git a/src/public/app/entities/ftask.ts b/src/public/app/entities/ftask.ts index b6e70b00b..abca83f06 100644 --- a/src/public/app/entities/ftask.ts +++ b/src/public/app/entities/ftask.ts @@ -6,6 +6,7 @@ export interface FTaskRow { title: string; dueDate?: string; isDone?: boolean; + utcDateModified: string; } export default class FTask { @@ -15,6 +16,7 @@ export default class FTask { title!: string; dueDate?: string; isDone!: boolean; + utcDateModified!: string; constructor(froca: Froca, row: FTaskRow) { this.froca = froca; @@ -27,5 +29,6 @@ export default class FTask { this.title = row.title; this.dueDate = row.dueDate; this.isDone = !!row.isDone; + this.utcDateModified = row.utcDateModified; } } From 0b8e3b976f08636f8b20373eced423f339e49b7b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 18 Feb 2025 21:16:32 +0200 Subject: [PATCH 09/27] fix(client/tasks): error due to froca update --- src/public/app/services/froca_updater.ts | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/public/app/services/froca_updater.ts b/src/public/app/services/froca_updater.ts index 37f9d2814..617c4c277 100644 --- a/src/public/app/services/froca_updater.ts +++ b/src/public/app/services/froca_updater.ts @@ -8,6 +8,8 @@ import FAttribute, { type FAttributeRow } from "../entities/fattribute.js"; import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; import type { default as FNote, FNoteRow } from "../entities/fnote.js"; import type { EntityChange } from "../server_types.js"; +import type { FTaskRow } from "../entities/ftask.js"; +import FTask from "../entities/ftask.js"; async function processEntityChanges(entityChanges: EntityChange[]) { const loadResults = new LoadResults(entityChanges); @@ -37,6 +39,8 @@ async function processEntityChanges(entityChanges: EntityChange[]) { processAttachment(loadResults, ec); } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") { // NOOP + } else if (ec.entityName === "tasks") { + processTaskChange(loadResults, ec); } else { throw new Error(`Unknown entityName '${ec.entityName}'`); } @@ -306,6 +310,33 @@ function processAttachment(loadResults: LoadResults, ec: EntityChange) { loadResults.addAttachmentRow(attachmentEntity); } +function processTaskChange(loadResults: LoadResults, ec: EntityChange) { + if (ec.isErased && ec.entityId in froca.tasks) { + utils.reloadFrontendApp(`${ec.entityName} '${ec.entityId}' is erased, need to do complete reload.`); + return; + } + + let task = froca.tasks[ec.entityId]; + const taskEntity = ec.entity as FTaskRow; + + if (ec.isErased || (ec.entity as any)?.isDeleted) { + if (task) { + delete froca.tasks[ec.entityId]; + } + + return; + } + + if (ec.entity) { + if (task) { + task.update(ec.entity as FTaskRow); + } else { + task = new FTask(froca, ec.entity as FTaskRow); + froca.tasks[task.taskId] = task; + } + } +} + export default { processEntityChanges }; From 575ef5e10ed5d01b19042135368b2b2d683f8345 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 18:14:49 +0200 Subject: [PATCH 10/27] fix(build): missing note type in help button --- src/public/app/widgets/floating_buttons/help_button.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/public/app/widgets/floating_buttons/help_button.ts b/src/public/app/widgets/floating_buttons/help_button.ts index c2147c75d..a6e2cb94e 100644 --- a/src/public/app/widgets/floating_buttons/help_button.ts +++ b/src/public/app/widgets/floating_buttons/help_button.ts @@ -27,7 +27,8 @@ const byNoteType: Record, string | null> = { render: null, search: null, text: null, - webView: null + webView: null, + taskList: null }; const byBookType: Record = { From 0baa8045448980ff903072d7acc316a3c43637f6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 18:18:20 +0200 Subject: [PATCH 11/27] fix(server/tasks): becca not updating on task creation --- src/becca/entities/btask.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/becca/entities/btask.ts b/src/becca/entities/btask.ts index 85bcca46a..e04f9b8ab 100644 --- a/src/becca/entities/btask.ts +++ b/src/becca/entities/btask.ts @@ -63,6 +63,10 @@ export default class BTask extends AbstractBeccaEntity { super.beforeSaving(); this.utcDateModified = date_utils.utcNowDateTime(); + + if (this.taskId) { + this.becca.tasks[this.taskId] = this; + } } getPojo() { From 35af12b6e7a7d81126e80156a2e81b8e14bc5f88 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 18:18:28 +0200 Subject: [PATCH 12/27] fix(vscode): F5 to start server --- .vscode/launch.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index f8d4780a1..cf21b9ce1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,8 +5,8 @@ { "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", - "name": "nodemon start-server", - "program": "${workspaceFolder}/src/www", + "name": "nodemon server:start", + "program": "${workspaceFolder}/src/main", "request": "launch", "restart": true, "runtimeExecutable": "nodemon", From ad492619f52235137a3230e5351172d7d9be7848 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 18:30:33 +0200 Subject: [PATCH 13/27] style(tasks): floating header --- src/public/app/widgets/type_widgets/task_list.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts index faf9a5687..711253b22 100644 --- a/src/public/app/widgets/type_widgets/task_list.ts +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -16,11 +16,18 @@ const TPL = `
`; function buildTask(task: FTask) { - return `
${task.title}
`; + return `
  • ${task.title}
  • `; } export default class TaskListWidget extends TypeWidget { From 7c0b43db85f7f2f16a35ba85703aca44d40e8cb4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 19:22:38 +0200 Subject: [PATCH 15/27] feat(tasks): mark tasks as completed --- src/becca/entities/btask.ts | 1 + src/public/app/services/tasks.ts | 4 ++++ .../app/widgets/type_widgets/task_list.ts | 20 ++++++++++++++++++- src/routes/api/tasks.ts | 9 +++++++++ src/routes/routes.ts | 1 + src/services/tasks.ts | 6 ++++++ 6 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/becca/entities/btask.ts b/src/becca/entities/btask.ts index e04f9b8ab..6271384b3 100644 --- a/src/becca/entities/btask.ts +++ b/src/becca/entities/btask.ts @@ -75,6 +75,7 @@ export default class BTask extends AbstractBeccaEntity { parentNoteId: this.parentNoteId, title: this.title, dueDate: this.dueDate, + isDone: this.isDone, isDeleted: this.isDeleted, utcDateModified: this.utcDateModified }; diff --git a/src/public/app/services/tasks.ts b/src/public/app/services/tasks.ts index a5a6a9ceb..400e94712 100644 --- a/src/public/app/services/tasks.ts +++ b/src/public/app/services/tasks.ts @@ -5,3 +5,7 @@ export async function createNewTask(title: string) { title }); } + +export async function toggleTaskDone(taskId: string) { + await server.post(`tasks/${taskId}/toggle`); +} diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts index 573e7de63..0f71092e9 100644 --- a/src/public/app/widgets/type_widgets/task_list.ts +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -48,12 +48,19 @@ const TPL = ` border-bottom: 1px solid var(--main-background-color); padding: 0.5em 1em; } + + .note-detail-task-list .task-container li .check { + margin-right: 0.5em; + }
    `; function buildTask(task: FTask) { - return `
  • ${task.title}
  • `; + return `\ +
  • + ${task.title} +
  • `; } export default class TaskListWidget extends TypeWidget { @@ -73,6 +80,17 @@ export default class TaskListWidget extends TypeWidget { 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) { diff --git a/src/routes/api/tasks.ts b/src/routes/api/tasks.ts index 33516ef28..a9942ffa6 100644 --- a/src/routes/api/tasks.ts +++ b/src/routes/api/tasks.ts @@ -8,3 +8,12 @@ export function getTasks() { 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); +} diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 6cff1e033..563cd21d1 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -282,6 +282,7 @@ function register(app: express.Application) { apiRoute(GET, "/api/tasks", tasksRoute.getTasks); apiRoute(PST, "/api/tasks", tasksRoute.createNewTask); + apiRoute(PST, "/api/tasks/:taskId/toggle", tasksRoute.toggleTaskDone); // in case of local electron, local calls are allowed unauthenticated, for server they need auth const clipperMiddleware = isElectron ? [] : [auth.checkEtapiToken]; diff --git a/src/services/tasks.ts b/src/services/tasks.ts index 6a2f93710..dda9fc4b7 100644 --- a/src/services/tasks.ts +++ b/src/services/tasks.ts @@ -19,3 +19,9 @@ export function createNewTask(params: CreateTaskParams) { task } } + +export function toggleTaskDone(taskId: string) { + const task = becca.tasks[taskId]; + task.isDone = !task.isDone; + task.save(); +} From 2a3546edd54d95382974d47630eaf29daad1180b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 19:27:04 +0200 Subject: [PATCH 16/27] feat(client/tasks): display completed tasks --- src/public/app/widgets/type_widgets/task_list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts index 0f71092e9..95ffa5bf9 100644 --- a/src/public/app/widgets/type_widgets/task_list.ts +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -59,7 +59,7 @@ const TPL = ` function buildTask(task: FTask) { return `\
  • - ${task.title} + ${task.title}
  • `; } From 2de46eb5d2fe6e7d3b07d347141adf7e60731d5e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 19:37:07 +0200 Subject: [PATCH 17/27] fix(build): task row should be optional --- src/becca/entities/rows.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/becca/entities/rows.ts b/src/becca/entities/rows.ts index 85b9e1667..ba9189190 100644 --- a/src/becca/entities/rows.ts +++ b/src/becca/entities/rows.ts @@ -144,5 +144,5 @@ export interface TaskRow { dueDate?: string; isDone?: boolean; isDeleted?: boolean; - utcDateModified: string; + utcDateModified?: string; } From 7f0df441b5d4120b7f82daa9749c9e4bc979a713 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 19:45:23 +0200 Subject: [PATCH 18/27] fix(tasks): initial state for checkbox --- src/becca/entities/btask.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/becca/entities/btask.ts b/src/becca/entities/btask.ts index 6271384b3..6f2b89373 100644 --- a/src/becca/entities/btask.ts +++ b/src/becca/entities/btask.ts @@ -44,7 +44,7 @@ export default class BTask extends AbstractBeccaEntity { this.parentNoteId = row.parentNoteId; this.title = row.title; this.dueDate = row.dueDate; - this.isDone = !!row.isDeleted; + this.isDone = !!row.isDone; this._isDeleted = !!row.isDeleted; this.utcDateModified = row.utcDateModified; From 9ed075b6757d4ad945235fa460e5ad1bbd869f0e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 20:02:43 +0200 Subject: [PATCH 19/27] fix(tasks): task list not clearing properly --- src/public/app/widgets/type_widgets/task_list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts index 95ffa5bf9..861ebe634 100644 --- a/src/public/app/widgets/type_widgets/task_list.ts +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -108,7 +108,7 @@ export default class TaskListWidget extends TypeWidget { return; } - this.$taskContainer.clearQueue(); + this.$taskContainer.html(""); const tasks = await froca.getTasks(); for (const task of tasks) { From f743f634b4777f65b5c2ceac6109593f882521dd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 20:33:20 +0200 Subject: [PATCH 20/27] feat(tasks): hide completed tasks for now --- src/becca/becca-interface.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/becca/becca-interface.ts b/src/becca/becca-interface.ts index 8da95c39f..16d81d3df 100644 --- a/src/becca/becca-interface.ts +++ b/src/becca/becca-interface.ts @@ -217,7 +217,9 @@ export default class Becca { } getTasks(): BTask[] { - return Object.values(this.tasks); + return Object + .values(this.tasks) + .filter((task) => !task.isDone); } getEntity>(entityName: string, entityId: string): AbstractBeccaEntity | null { From 034b93c99c293f81314df85de2927c2c6bf994f1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 21:27:02 +0200 Subject: [PATCH 21/27] feat(tasks): support entities reloaded properly --- src/becca/becca-interface.ts | 4 ++++ src/public/app/services/froca_updater.ts | 2 ++ src/public/app/services/load_results.ts | 15 ++++++++++++++- src/public/app/widgets/type_widgets/task_list.ts | 5 +++++ src/services/ws.ts | 6 ++++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/becca/becca-interface.ts b/src/becca/becca-interface.ts index 16d81d3df..82970907b 100644 --- a/src/becca/becca-interface.ts +++ b/src/becca/becca-interface.ts @@ -222,6 +222,10 @@ export default class Becca { .filter((task) => !task.isDone); } + getTask(taskId: string): BTask | null { + return this.tasks[taskId]; + } + getEntity>(entityName: string, entityId: string): AbstractBeccaEntity | null { if (!entityName || !entityId) { return null; diff --git a/src/public/app/services/froca_updater.ts b/src/public/app/services/froca_updater.ts index 617c4c277..cf9dbbad0 100644 --- a/src/public/app/services/froca_updater.ts +++ b/src/public/app/services/froca_updater.ts @@ -335,6 +335,8 @@ function processTaskChange(loadResults: LoadResults, ec: EntityChange) { froca.tasks[task.taskId] = task; } } + + loadResults.addTaskRow(taskEntity); } export default { diff --git a/src/public/app/services/load_results.ts b/src/public/app/services/load_results.ts index 5ebfc7be5..e91dfde86 100644 --- a/src/public/app/services/load_results.ts +++ b/src/public/app/services/load_results.ts @@ -1,3 +1,4 @@ +import type { TaskRow } from "../../../becca/entities/rows.js"; import type { AttributeType } from "../entities/fattribute.js"; import type { EntityChange } from "../server_types.js"; @@ -69,6 +70,7 @@ export default class LoadResults { private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[]; private optionNames: string[]; private attachmentRows: AttachmentRow[]; + private taskRows: TaskRow[]; constructor(entityChanges: EntityChange[]) { const entities: Record> = {}; @@ -97,6 +99,8 @@ export default class LoadResults { this.optionNames = []; this.attachmentRows = []; + + this.taskRows = []; } getEntityRow(entityName: T, entityId: string): EntityRowMappings[T] { @@ -199,6 +203,14 @@ export default class LoadResults { return this.attachmentRows; } + addTaskRow(task: TaskRow) { + this.taskRows.push(task); + } + + getTaskRows() { + return this.taskRows; + } + /** * @returns {boolean} true if there are changes which could affect the attributes (including inherited ones) * notably changes in note itself should not have any effect on attributes @@ -216,7 +228,8 @@ export default class LoadResults { this.revisionRows.length === 0 && this.contentNoteIdToComponentId.length === 0 && this.optionNames.length === 0 && - this.attachmentRows.length === 0 + this.attachmentRows.length === 0 && + this.taskRows.length === 0 ); } diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts index 861ebe634..b54ee35c7 100644 --- a/src/public/app/widgets/type_widgets/task_list.ts +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -3,6 +3,7 @@ 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 = `
    @@ -116,4 +117,8 @@ export default class TaskListWidget extends TypeWidget { } } + entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { + console.log("Update", loadResults); + } + } diff --git a/src/services/ws.ts b/src/services/ws.ts index e163e25d7..f46a9b3f7 100644 --- a/src/services/ws.ts +++ b/src/services/ws.ts @@ -188,6 +188,12 @@ function fillInAdditionalProperties(entityChange: EntityChange) { WHERE attachmentId = ?`, [entityChange.entityId] ); + } else if (entityChange.entityName === "tasks") { + entityChange.entity = becca.getTask(entityChange.entity); + + if (!entityChange.entity) { + entityChange.entity = sql.getRow(`SELECT * FROM tasks WHERE taskId = ?`, [entityChange.entityId]); + } } if (entityChange.entity instanceof AbstractBeccaEntity) { From bb822126cda4393a5f70e264b3e125f476902928 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 21:52:33 +0200 Subject: [PATCH 22/27] feat(tasks): store parent note ID --- src/public/app/services/tasks.ts | 8 +++++++- src/public/app/widgets/type_widgets/task_list.ts | 7 +++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/public/app/services/tasks.ts b/src/public/app/services/tasks.ts index 400e94712..becf7fd25 100644 --- a/src/public/app/services/tasks.ts +++ b/src/public/app/services/tasks.ts @@ -1,7 +1,13 @@ import server from "./server.js"; -export async function createNewTask(title: string) { +interface CreateNewTasksOpts { + parentNoteId: string; + title: string; +} + +export async function createNewTask({ parentNoteId, title }: CreateNewTasksOpts) { await server.post(`tasks`, { + parentNoteId, title }); } diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts index b54ee35c7..a91d9879e 100644 --- a/src/public/app/widgets/type_widgets/task_list.ts +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -95,11 +95,14 @@ export default class TaskListWidget extends TypeWidget { } async #createNewTask(title: string) { - if (!title) { + if (!title || !this.noteId) { return; } - await taskService.createNewTask(title); + await taskService.createNewTask({ + title, + parentNoteId: this.noteId + }); } async doRefresh(note: FNote) { From c0d3e8d834c8b58f5074c5dd41509ffba0f3fc0b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 22:13:13 +0200 Subject: [PATCH 23/27] feat(tasks): filter by parent note --- src/becca/becca-interface.ts | 4 +--- src/public/app/services/froca.ts | 4 ++-- src/public/app/widgets/type_widgets/task_list.ts | 4 ++-- src/routes/api/tasks.ts | 5 +++-- src/routes/routes.ts | 2 +- src/services/tasks.ts | 5 +++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/becca/becca-interface.ts b/src/becca/becca-interface.ts index 82970907b..11d6ae506 100644 --- a/src/becca/becca-interface.ts +++ b/src/becca/becca-interface.ts @@ -217,9 +217,7 @@ export default class Becca { } getTasks(): BTask[] { - return Object - .values(this.tasks) - .filter((task) => !task.isDone); + return Object.values(this.tasks); } getTask(taskId: string): BTask | null { diff --git a/src/public/app/services/froca.ts b/src/public/app/services/froca.ts index 36e25a5a0..d787fbcf0 100644 --- a/src/public/app/services/froca.ts +++ b/src/public/app/services/froca.ts @@ -372,8 +372,8 @@ class FrocaImpl implements Froca { }); } - async getTasks() { - const taskRows = await server.get(`tasks`); + async getTasks(parentNoteId: string) { + const taskRows = await server.get(`tasks/${parentNoteId}`); return this.processTaskRow(taskRows); } diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts index a91d9879e..037ec5739 100644 --- a/src/public/app/widgets/type_widgets/task_list.ts +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -108,13 +108,13 @@ export default class TaskListWidget extends TypeWidget { async doRefresh(note: FNote) { this.$widget.show(); - if (!this.note) { + if (!this.note || !this.noteId) { return; } this.$taskContainer.html(""); - const tasks = await froca.getTasks(); + const tasks = await froca.getTasks(this.noteId); for (const task of tasks) { this.$taskContainer.append($(buildTask(task))); } diff --git a/src/routes/api/tasks.ts b/src/routes/api/tasks.ts index a9942ffa6..102de086d 100644 --- a/src/routes/api/tasks.ts +++ b/src/routes/api/tasks.ts @@ -1,8 +1,9 @@ import type { Request } from "express"; import * as tasksService from "../../services/tasks.js"; -export function getTasks() { - return tasksService.getTasks(); +export function getTasks(req: Request) { + const { parentNoteId } = req.params; + return tasksService.getTasks(parentNoteId); } export function createNewTask(req: Request) { diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 563cd21d1..da37854d3 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -280,7 +280,7 @@ function register(app: express.Application) { apiRoute(PATCH, "/api/etapi-tokens/:etapiTokenId", etapiTokensApiRoutes.patchToken); apiRoute(DEL, "/api/etapi-tokens/:etapiTokenId", etapiTokensApiRoutes.deleteToken); - apiRoute(GET, "/api/tasks", tasksRoute.getTasks); + apiRoute(GET, "/api/tasks/:parentNoteId", tasksRoute.getTasks); apiRoute(PST, "/api/tasks", tasksRoute.createNewTask); apiRoute(PST, "/api/tasks/:taskId/toggle", tasksRoute.toggleTaskDone); diff --git a/src/services/tasks.ts b/src/services/tasks.ts index dda9fc4b7..92d2b25db 100644 --- a/src/services/tasks.ts +++ b/src/services/tasks.ts @@ -1,8 +1,9 @@ import becca from "../becca/becca.js"; import BTask from "../becca/entities/btask.js"; -export function getTasks() { - return becca.getTasks(); +export function getTasks(parentNoteId: string) { + return becca.getTasks() + .filter((task) => task.parentNoteId === parentNoteId && !task.isDone); } interface CreateTaskParams { From 62c96fc95ec245f33e3429d8a3cc72e413cbbe87 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 19 Feb 2025 22:34:52 +0200 Subject: [PATCH 24/27] feat(tasks): implement basic refresh support --- src/public/app/services/load_results.ts | 8 ++++++++ src/public/app/widgets/type_widgets/task_list.ts | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/public/app/services/load_results.ts b/src/public/app/services/load_results.ts index e91dfde86..25f3b30ec 100644 --- a/src/public/app/services/load_results.ts +++ b/src/public/app/services/load_results.ts @@ -183,6 +183,14 @@ export default class LoadResults { return this.contentNoteIdToComponentId.find((l) => l.noteId === noteId && l.componentId !== componentId); } + isTaskListReloaded(parentNoteId: string) { + if (!parentNoteId) { + return false; + } + + return !!this.taskRows.find((tr) => tr.parentNoteId === parentNoteId); + } + addOption(name: string) { this.optionNames.push(name); } diff --git a/src/public/app/widgets/type_widgets/task_list.ts b/src/public/app/widgets/type_widgets/task_list.ts index 037ec5739..6c4f1aeda 100644 --- a/src/public/app/widgets/type_widgets/task_list.ts +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -121,7 +121,9 @@ export default class TaskListWidget extends TypeWidget { } entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - console.log("Update", loadResults); + if (this.noteId && loadResults.isTaskListReloaded(this.noteId)) { + this.refresh(); + } } } From a433c9c18962fd3962308640d87738b8f9bd8032 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 20 Feb 2025 12:07:10 +0200 Subject: [PATCH 25/27] feat(tasks): add SQL migration --- db/migrations/0229__tasks.sql | 10 ++++++++++ src/services/app_info.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 db/migrations/0229__tasks.sql diff --git a/db/migrations/0229__tasks.sql b/db/migrations/0229__tasks.sql new file mode 100644 index 000000000..4d0381db2 --- /dev/null +++ b/db/migrations/0229__tasks.sql @@ -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 +); \ No newline at end of file diff --git a/src/services/app_info.ts b/src/services/app_info.ts index 72b1f0fca..8af0e6be1 100644 --- a/src/services/app_info.ts +++ b/src/services/app_info.ts @@ -5,7 +5,7 @@ import build from "./build.js"; import packageJson from "../../package.json" with { type: "json" }; import dataDir from "./data_dir.js"; -const APP_DB_VERSION = 228; +const APP_DB_VERSION = 229; const SYNC_VERSION = 34; const CLIPPER_PROTOCOL_VERSION = "1.0"; From 0b11f4d9c72b5e35ccb39acf09e7e9f0fca4409b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Feb 2025 14:34:44 +0200 Subject: [PATCH 26/27] chore(server): bump sync version --- src/services/app_info.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/app_info.ts b/src/services/app_info.ts index 8af0e6be1..63c4edfcd 100644 --- a/src/services/app_info.ts +++ b/src/services/app_info.ts @@ -6,7 +6,7 @@ import packageJson from "../../package.json" with { type: "json" }; import dataDir from "./data_dir.js"; const APP_DB_VERSION = 229; -const SYNC_VERSION = 34; +const SYNC_VERSION = 35; const CLIPPER_PROTOCOL_VERSION = "1.0"; export default { From fc27c4fc7be6f38f4b0a3bd8b2fb9bdf0cbe59f6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Feb 2025 14:36:15 +0200 Subject: [PATCH 27/27] feat(db): create task database in schema --- db/schema.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/db/schema.sql b/db/schema.sql index 1b4c46321..77361f5ed 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -132,3 +132,14 @@ CREATE INDEX IDX_attachments_ownerId_role CREATE INDEX IDX_notes_blobId on notes (blobId); CREATE INDEX IDX_revisions_blobId on revisions (blobId); CREATE INDEX IDX_attachments_blobId on attachments (blobId); + +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 +); \ No newline at end of file