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", 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/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 diff --git a/src/becca/becca-interface.ts b/src/becca/becca-interface.ts index cdee5d1cd..11d6ae506 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,14 @@ export default class Becca { return this.etapiTokens[etapiTokenId]; } + getTasks(): BTask[] { + return Object.values(this.tasks); + } + + 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/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..6f2b89373 --- /dev/null +++ b/src/becca/entities/btask.ts @@ -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 { + + 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 + }; + } + +} diff --git a/src/becca/entities/rows.ts b/src/becca/entities/rows.ts index 89fa36953..ba9189190 100644 --- a/src/becca/entities/rows.ts +++ b/src/becca/entities/rows.ts @@ -136,3 +136,13 @@ export interface NoteRow { utcDateModified: string; content?: string | Buffer; } + +export interface TaskRow { + taskId?: string; + parentNoteId: string; + title: string; + dueDate?: string; + isDone?: boolean; + isDeleted?: boolean; + utcDateModified?: string; +} 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/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/entities/ftask.ts b/src/public/app/entities/ftask.ts new file mode 100644 index 000000000..abca83f06 --- /dev/null +++ b/src/public/app/entities/ftask.ts @@ -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; + } +} diff --git a/src/public/app/services/froca.ts b/src/public/app/services/froca.ts index 8849d6331..d787fbcf0 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(parentNoteId: string) { + const taskRows = await server.get(`tasks/${parentNoteId}`); + 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/services/froca_updater.ts b/src/public/app/services/froca_updater.ts index 37f9d2814..cf9dbbad0 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,35 @@ 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; + } + } + + loadResults.addTaskRow(taskEntity); +} + export default { processEntityChanges }; diff --git a/src/public/app/services/load_results.ts b/src/public/app/services/load_results.ts index 5ebfc7be5..25f3b30ec 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] { @@ -179,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); } @@ -199,6 +211,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 +236,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/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/services/tasks.ts b/src/public/app/services/tasks.ts new file mode 100644 index 000000000..becf7fd25 --- /dev/null +++ b/src/public/app/services/tasks.ts @@ -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`); +} 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 = { 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..6c4f1aeda --- /dev/null +++ b/src/public/app/widgets/type_widgets/task_list.ts @@ -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 = ` +
+ +
+ +
+ +
    +
+ + +
+`; + +function buildTask(task: FTask) { + return `\ +
  • + ${task.title} +
  • `; +} + +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())); + } + }); + + 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(); + } + } + +} diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index a915f3cca..9e7cd6454 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1418,7 +1418,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/routes/api/tasks.ts b/src/routes/api/tasks.ts new file mode 100644 index 000000000..102de086d --- /dev/null +++ b/src/routes/api/tasks.ts @@ -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); +} diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 05c7612f2..da37854d3 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,10 @@ 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/:parentNoteId", 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/app_info.ts b/src/services/app_info.ts index 72b1f0fca..63c4edfcd 100644 --- a/src/services/app_info.ts +++ b/src/services/app_info.ts @@ -5,8 +5,8 @@ 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 SYNC_VERSION = 34; +const APP_DB_VERSION = 229; +const SYNC_VERSION = 35; const CLIPPER_PROTOCOL_VERSION = "1.0"; export default { 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(", ")}`); } 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) { diff --git a/src/services/tasks.ts b/src/services/tasks.ts new file mode 100644 index 000000000..92d2b25db --- /dev/null +++ b/src/services/tasks.ts @@ -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(); +} 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) {