mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 21:11:30 +08:00 
			
		
		
		
	
						commit
						58a8821c22
					
				
							
								
								
									
										4
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							| @ -5,8 +5,8 @@ | |||||||
|     { |     { | ||||||
|       "console": "integratedTerminal", |       "console": "integratedTerminal", | ||||||
|       "internalConsoleOptions": "neverOpen", |       "internalConsoleOptions": "neverOpen", | ||||||
|       "name": "nodemon start-server", |       "name": "nodemon server:start", | ||||||
|       "program": "${workspaceFolder}/src/www", |       "program": "${workspaceFolder}/src/main", | ||||||
|       "request": "launch", |       "request": "launch", | ||||||
|       "restart": true, |       "restart": true, | ||||||
|       "runtimeExecutable": "nodemon", |       "runtimeExecutable": "nodemon", | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								db/migrations/0229__tasks.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								db/migrations/0229__tasks.sql
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | ); | ||||||
| @ -132,3 +132,14 @@ CREATE INDEX IDX_attachments_ownerId_role | |||||||
| CREATE INDEX IDX_notes_blobId on notes (blobId); | CREATE INDEX IDX_notes_blobId on notes (blobId); | ||||||
| CREATE INDEX IDX_revisions_blobId on revisions (blobId); | CREATE INDEX IDX_revisions_blobId on revisions (blobId); | ||||||
| CREATE INDEX IDX_attachments_blobId on attachments (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 | ||||||
|  | ); | ||||||
| @ -12,6 +12,7 @@ import type { AttachmentRow, BlobRow, RevisionRow } from "./entities/rows.js"; | |||||||
| import BBlob from "./entities/bblob.js"; | import BBlob from "./entities/bblob.js"; | ||||||
| import BRecentNote from "./entities/brecent_note.js"; | import BRecentNote from "./entities/brecent_note.js"; | ||||||
| import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; | import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; | ||||||
|  | import type BTask from "./entities/btask.js"; | ||||||
| 
 | 
 | ||||||
| interface AttachmentOpts { | interface AttachmentOpts { | ||||||
|     includeContentLength?: boolean; |     includeContentLength?: boolean; | ||||||
| @ -32,6 +33,7 @@ export default class Becca { | |||||||
|     attributeIndex!: Record<string, BAttribute[]>; |     attributeIndex!: Record<string, BAttribute[]>; | ||||||
|     options!: Record<string, BOption>; |     options!: Record<string, BOption>; | ||||||
|     etapiTokens!: Record<string, BEtapiToken>; |     etapiTokens!: Record<string, BEtapiToken>; | ||||||
|  |     tasks!: Record<string, BTask>; | ||||||
| 
 | 
 | ||||||
|     allNoteSetCache: NoteSet | null; |     allNoteSetCache: NoteSet | null; | ||||||
| 
 | 
 | ||||||
| @ -48,6 +50,7 @@ export default class Becca { | |||||||
|         this.attributeIndex = {}; |         this.attributeIndex = {}; | ||||||
|         this.options = {}; |         this.options = {}; | ||||||
|         this.etapiTokens = {}; |         this.etapiTokens = {}; | ||||||
|  |         this.tasks = {}; | ||||||
| 
 | 
 | ||||||
|         this.dirtyNoteSetCache(); |         this.dirtyNoteSetCache(); | ||||||
| 
 | 
 | ||||||
| @ -213,6 +216,14 @@ export default class Becca { | |||||||
|         return this.etapiTokens[etapiTokenId]; |         return this.etapiTokens[etapiTokenId]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     getTasks(): BTask[] { | ||||||
|  |         return Object.values(this.tasks); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getTask(taskId: string): BTask | null { | ||||||
|  |         return this.tasks[taskId]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     getEntity<T extends AbstractBeccaEntity<T>>(entityName: string, entityId: string): AbstractBeccaEntity<T> | null { |     getEntity<T extends AbstractBeccaEntity<T>>(entityName: string, entityId: string): AbstractBeccaEntity<T> | null { | ||||||
|         if (!entityName || !entityId) { |         if (!entityName || !entityId) { | ||||||
|             return null; |             return null; | ||||||
|  | |||||||
| @ -11,9 +11,10 @@ import BOption from "./entities/boption.js"; | |||||||
| import BEtapiToken from "./entities/betapi_token.js"; | import BEtapiToken from "./entities/betapi_token.js"; | ||||||
| import cls from "../services/cls.js"; | import cls from "../services/cls.js"; | ||||||
| import entityConstructor from "../becca/entity_constructor.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 type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; | ||||||
| import ws from "../services/ws.js"; | import ws from "../services/ws.js"; | ||||||
|  | import BTask from "./entities/btask.js"; | ||||||
| 
 | 
 | ||||||
| const beccaLoaded = new Promise<void>(async (res, rej) => { | const beccaLoaded = new Promise<void>(async (res, rej) => { | ||||||
|     const sqlInit = (await import("../services/sql_init.js")).default; |     const sqlInit = (await import("../services/sql_init.js")).default; | ||||||
| @ -63,6 +64,10 @@ function load() { | |||||||
|         for (const row of sql.getRows<EtapiTokenRow>(`SELECT etapiTokenId, name, tokenHash, utcDateCreated, utcDateModified FROM etapi_tokens WHERE isDeleted = 0`)) { |         for (const row of sql.getRows<EtapiTokenRow>(`SELECT etapiTokenId, name, tokenHash, utcDateCreated, utcDateModified FROM etapi_tokens WHERE isDeleted = 0`)) { | ||||||
|             new BEtapiToken(row); |             new BEtapiToken(row); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         for (const row of sql.getRows<TaskRow>(`SELECT taskId, parentNoteId, title, dueDate, isDone, isDeleted FROM tasks WHERE isDeleted = 0`)) { | ||||||
|  |             new BTask(row); | ||||||
|  |         } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     for (const noteId in becca.notes) { |     for (const noteId in becca.notes) { | ||||||
|  | |||||||
							
								
								
									
										84
									
								
								src/becca/entities/btask.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/becca/entities/btask.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -136,3 +136,13 @@ export interface NoteRow { | |||||||
|     utcDateModified: string; |     utcDateModified: string; | ||||||
|     content?: string | Buffer; |     content?: string | Buffer; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export interface TaskRow { | ||||||
|  |     taskId?: string; | ||||||
|  |     parentNoteId: string; | ||||||
|  |     title: string; | ||||||
|  |     dueDate?: string; | ||||||
|  |     isDone?: boolean; | ||||||
|  |     isDeleted?: boolean; | ||||||
|  |     utcDateModified?: string; | ||||||
|  | } | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import BNote from "./entities/bnote.js"; | |||||||
| import BOption from "./entities/boption.js"; | import BOption from "./entities/boption.js"; | ||||||
| import BRecentNote from "./entities/brecent_note.js"; | import BRecentNote from "./entities/brecent_note.js"; | ||||||
| import BRevision from "./entities/brevision.js"; | import BRevision from "./entities/brevision.js"; | ||||||
|  | import BTask from "./entities/btask.js"; | ||||||
| 
 | 
 | ||||||
| type EntityClass = new (row?: any) => AbstractBeccaEntity<any>; | type EntityClass = new (row?: any) => AbstractBeccaEntity<any>; | ||||||
| 
 | 
 | ||||||
| @ -21,7 +22,8 @@ const ENTITY_NAME_TO_ENTITY: Record<string, ConstructorData<any> & EntityClass> | |||||||
|     notes: BNote, |     notes: BNote, | ||||||
|     options: BOption, |     options: BOption, | ||||||
|     recent_notes: BRecentNote, |     recent_notes: BRecentNote, | ||||||
|     revisions: BRevision |     revisions: BRevision, | ||||||
|  |     tasks: BTask | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function getEntityFromEntityName(entityName: keyof typeof ENTITY_NAME_TO_ENTITY) { | function getEntityFromEntityName(entityName: keyof typeof ENTITY_NAME_TO_ENTITY) { | ||||||
|  | |||||||
| @ -28,7 +28,8 @@ const NOTE_TYPE_ICONS = { | |||||||
|     doc: "bx bxs-file-doc", |     doc: "bx bxs-file-doc", | ||||||
|     contentWidget: "bx bxs-widget", |     contentWidget: "bx bxs-widget", | ||||||
|     mindMap: "bx bx-sitemap", |     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 |  * end user. Those types should be used only for checking against, they are | ||||||
|  * not for direct use. |  * 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 { | export interface NotePathRecord { | ||||||
|     isArchived: boolean; |     isArchived: boolean; | ||||||
|  | |||||||
							
								
								
									
										34
									
								
								src/public/app/entities/ftask.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/public/app/entities/ftask.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -6,6 +6,8 @@ import appContext from "../components/app_context.js"; | |||||||
| import FBlob, { type FBlobRow } from "../entities/fblob.js"; | import FBlob, { type FBlobRow } from "../entities/fblob.js"; | ||||||
| import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; | import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; | ||||||
| import type { Froca } from "./froca-interface.js"; | import type { Froca } from "./froca-interface.js"; | ||||||
|  | import FTask from "../entities/ftask.js"; | ||||||
|  | import type { FTaskRow } from "../entities/ftask.js"; | ||||||
| 
 | 
 | ||||||
| interface SubtreeResponse { | interface SubtreeResponse { | ||||||
|     notes: FNoteRow[]; |     notes: FNoteRow[]; | ||||||
| @ -37,6 +39,7 @@ class FrocaImpl implements Froca { | |||||||
|     attributes!: Record<string, FAttribute>; |     attributes!: Record<string, FAttribute>; | ||||||
|     attachments!: Record<string, FAttachment>; |     attachments!: Record<string, FAttachment>; | ||||||
|     blobPromises!: Record<string, Promise<void | FBlob> | null>; |     blobPromises!: Record<string, Promise<void | FBlob> | null>; | ||||||
|  |     tasks!: Record<string, FTask>; | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         this.initializedPromise = this.loadInitialTree(); |         this.initializedPromise = this.loadInitialTree(); | ||||||
| @ -52,6 +55,7 @@ class FrocaImpl implements Froca { | |||||||
|         this.attributes = {}; |         this.attributes = {}; | ||||||
|         this.attachments = {}; |         this.attachments = {}; | ||||||
|         this.blobPromises = {}; |         this.blobPromises = {}; | ||||||
|  |         this.tasks = {}; | ||||||
| 
 | 
 | ||||||
|         this.addResp(resp); |         this.addResp(resp); | ||||||
|     } |     } | ||||||
| @ -368,6 +372,20 @@ class FrocaImpl implements Froca { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async getTasks(parentNoteId: string) { | ||||||
|  |         const taskRows = await server.get<FTaskRow[]>(`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) { |     async getBlob(entityType: string, entityId: string) { | ||||||
|         // I'm not sure why we're not using blobIds directly, it would save us this composite key ...
 |         // 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
 |         // perhaps one benefit is that we're always requesting the latest blob, not relying on perhaps faulty/slow
 | ||||||
|  | |||||||
| @ -8,6 +8,8 @@ import FAttribute, { type FAttributeRow } from "../entities/fattribute.js"; | |||||||
| import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; | import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; | ||||||
| import type { default as FNote, FNoteRow } from "../entities/fnote.js"; | import type { default as FNote, FNoteRow } from "../entities/fnote.js"; | ||||||
| import type { EntityChange } from "../server_types.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[]) { | async function processEntityChanges(entityChanges: EntityChange[]) { | ||||||
|     const loadResults = new LoadResults(entityChanges); |     const loadResults = new LoadResults(entityChanges); | ||||||
| @ -37,6 +39,8 @@ async function processEntityChanges(entityChanges: EntityChange[]) { | |||||||
|                 processAttachment(loadResults, ec); |                 processAttachment(loadResults, ec); | ||||||
|             } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") { |             } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") { | ||||||
|                 // NOOP
 |                 // NOOP
 | ||||||
|  |             } else if (ec.entityName === "tasks") { | ||||||
|  |                 processTaskChange(loadResults, ec); | ||||||
|             } else { |             } else { | ||||||
|                 throw new Error(`Unknown entityName '${ec.entityName}'`); |                 throw new Error(`Unknown entityName '${ec.entityName}'`); | ||||||
|             } |             } | ||||||
| @ -306,6 +310,35 @@ function processAttachment(loadResults: LoadResults, ec: EntityChange) { | |||||||
|     loadResults.addAttachmentRow(attachmentEntity); |     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 { | export default { | ||||||
|     processEntityChanges |     processEntityChanges | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | import type { TaskRow } from "../../../becca/entities/rows.js"; | ||||||
| import type { AttributeType } from "../entities/fattribute.js"; | import type { AttributeType } from "../entities/fattribute.js"; | ||||||
| import type { EntityChange } from "../server_types.js"; | import type { EntityChange } from "../server_types.js"; | ||||||
| 
 | 
 | ||||||
| @ -69,6 +70,7 @@ export default class LoadResults { | |||||||
|     private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[]; |     private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[]; | ||||||
|     private optionNames: string[]; |     private optionNames: string[]; | ||||||
|     private attachmentRows: AttachmentRow[]; |     private attachmentRows: AttachmentRow[]; | ||||||
|  |     private taskRows: TaskRow[]; | ||||||
| 
 | 
 | ||||||
|     constructor(entityChanges: EntityChange[]) { |     constructor(entityChanges: EntityChange[]) { | ||||||
|         const entities: Record<string, Record<string, any>> = {}; |         const entities: Record<string, Record<string, any>> = {}; | ||||||
| @ -97,6 +99,8 @@ export default class LoadResults { | |||||||
|         this.optionNames = []; |         this.optionNames = []; | ||||||
| 
 | 
 | ||||||
|         this.attachmentRows = []; |         this.attachmentRows = []; | ||||||
|  | 
 | ||||||
|  |         this.taskRows = []; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     getEntityRow<T extends EntityRowNames>(entityName: T, entityId: string): EntityRowMappings[T] { |     getEntityRow<T extends EntityRowNames>(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); |         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) { |     addOption(name: string) { | ||||||
|         this.optionNames.push(name); |         this.optionNames.push(name); | ||||||
|     } |     } | ||||||
| @ -199,6 +211,14 @@ export default class LoadResults { | |||||||
|         return this.attachmentRows; |         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) |      * @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 |      *          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.revisionRows.length === 0 && | ||||||
|             this.contentNoteIdToComponentId.length === 0 && |             this.contentNoteIdToComponentId.length === 0 && | ||||||
|             this.optionNames.length === 0 && |             this.optionNames.length === 0 && | ||||||
|             this.attachmentRows.length === 0 |             this.attachmentRows.length === 0 && | ||||||
|  |             this.taskRows.length === 0 | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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.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.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.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<string[]>("search-templates"); |     const templateNoteIds = await server.get<string[]>("search-templates"); | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								src/public/app/services/tasks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/public/app/services/tasks.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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`); | ||||||
|  | } | ||||||
| @ -27,7 +27,8 @@ const byNoteType: Record<Exclude<NoteType, "book">, string | null> = { | |||||||
|     render: null, |     render: null, | ||||||
|     search: null, |     search: null, | ||||||
|     text: null, |     text: null, | ||||||
|     webView: null |     webView: null, | ||||||
|  |     taskList: null | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const byBookType: Record<ViewTypeOptions, string | null> = { | const byBookType: Record<ViewTypeOptions, string | null> = { | ||||||
|  | |||||||
| @ -35,6 +35,7 @@ import GeoMapTypeWidget from "./type_widgets/geo_map.js"; | |||||||
| import utils from "../services/utils.js"; | import utils from "../services/utils.js"; | ||||||
| import type { NoteType } from "../entities/fnote.js"; | import type { NoteType } from "../entities/fnote.js"; | ||||||
| import type TypeWidget from "./type_widgets/type_widget.js"; | import type TypeWidget from "./type_widgets/type_widget.js"; | ||||||
|  | import TaskListWidget from "./type_widgets/task_list.js"; | ||||||
| 
 | 
 | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div class="note-detail"> | <div class="note-detail"> | ||||||
| @ -72,7 +73,8 @@ const typeWidgetClasses = { | |||||||
|     attachmentDetail: AttachmentDetailTypeWidget, |     attachmentDetail: AttachmentDetailTypeWidget, | ||||||
|     attachmentList: AttachmentListTypeWidget, |     attachmentList: AttachmentListTypeWidget, | ||||||
|     mindMap: MindMapWidget, |     mindMap: MindMapWidget, | ||||||
|     geoMap: GeoMapTypeWidget |     geoMap: GeoMapTypeWidget, | ||||||
|  |     taskList: TaskListWidget | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | |||||||
| @ -48,7 +48,8 @@ const NOTE_TYPES: NoteTypeMapping[] = [ | |||||||
|     { type: "image", title: t("note_types.image"), selectable: false }, |     { type: "image", title: t("note_types.image"), selectable: false }, | ||||||
|     { type: "launcher", mime: "", title: t("note_types.launcher"), selectable: false }, |     { type: "launcher", mime: "", title: t("note_types.launcher"), selectable: false }, | ||||||
|     { type: "noteMap", mime: "", title: t("note_types.note-map"), 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); | const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => !nt.selectable).map((nt) => nt.type); | ||||||
|  | |||||||
							
								
								
									
										129
									
								
								src/public/app/widgets/type_widgets/task_list.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								src/public/app/widgets/type_widgets/task_list.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -1418,7 +1418,8 @@ | |||||||
|     "widget": "Widget", |     "widget": "Widget", | ||||||
|     "confirm-change": "It is not recommended to change note type when note content is not empty. Do you want to continue anyway?", |     "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", |     "geo-map": "Geo Map", | ||||||
|     "beta-feature": "Beta" |     "beta-feature": "Beta", | ||||||
|  |     "task-list": "To-Do List" | ||||||
|   }, |   }, | ||||||
|   "protect_note": { |   "protect_note": { | ||||||
|     "toggle-on": "Protect the note", |     "toggle-on": "Protect the note", | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								src/routes/api/tasks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/routes/api/tasks.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||||
|  | } | ||||||
| @ -72,6 +72,7 @@ import etapiSpecRoute from "../etapi/spec.js"; | |||||||
| import etapiBackupRoute from "../etapi/backup.js"; | import etapiBackupRoute from "../etapi/backup.js"; | ||||||
| 
 | 
 | ||||||
| import apiDocsRoute from "./api_docs.js"; | import apiDocsRoute from "./api_docs.js"; | ||||||
|  | import * as tasksRoute from "./api/tasks.js"; | ||||||
| 
 | 
 | ||||||
| const MAX_ALLOWED_FILE_SIZE_MB = 250; | const MAX_ALLOWED_FILE_SIZE_MB = 250; | ||||||
| const GET = "get", | const GET = "get", | ||||||
| @ -279,6 +280,10 @@ function register(app: express.Application) { | |||||||
|     apiRoute(PATCH, "/api/etapi-tokens/:etapiTokenId", etapiTokensApiRoutes.patchToken); |     apiRoute(PATCH, "/api/etapi-tokens/:etapiTokenId", etapiTokensApiRoutes.patchToken); | ||||||
|     apiRoute(DEL, "/api/etapi-tokens/:etapiTokenId", etapiTokensApiRoutes.deleteToken); |     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
 |     // in case of local electron, local calls are allowed unauthenticated, for server they need auth
 | ||||||
|     const clipperMiddleware = isElectron ? [] : [auth.checkEtapiToken]; |     const clipperMiddleware = isElectron ? [] : [auth.checkEtapiToken]; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,8 +5,8 @@ import build from "./build.js"; | |||||||
| import packageJson from "../../package.json" with { type: "json" }; | import packageJson from "../../package.json" with { type: "json" }; | ||||||
| import dataDir from "./data_dir.js"; | import dataDir from "./data_dir.js"; | ||||||
| 
 | 
 | ||||||
| const APP_DB_VERSION = 228; | const APP_DB_VERSION = 229; | ||||||
| const SYNC_VERSION = 34; | const SYNC_VERSION = 35; | ||||||
| const CLIPPER_PROTOCOL_VERSION = "1.0"; | const CLIPPER_PROTOCOL_VERSION = "1.0"; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|  | |||||||
| @ -888,7 +888,7 @@ class ConsistencyChecks { | |||||||
|             return `${tableName}: ${count}`; |             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(", ")}`); |         log.info(`Table counts: ${tables.map((tableName) => getTableRowCount(tableName)).join(", ")}`); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -15,7 +15,8 @@ const noteTypes = [ | |||||||
|     { type: "doc", defaultMime: "" }, |     { type: "doc", defaultMime: "" }, | ||||||
|     { type: "contentWidget", defaultMime: "" }, |     { type: "contentWidget", defaultMime: "" }, | ||||||
|     { type: "mindMap", defaultMime: "application/json" }, |     { type: "mindMap", defaultMime: "application/json" }, | ||||||
|     { type: "geoMap", defaultMime: "application/json" } |     { type: "geoMap", defaultMime: "application/json" }, | ||||||
|  |     { type: "taskList", defaultMime: "" } | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| function getDefaultMimeForNoteType(typeName: string) { | function getDefaultMimeForNoteType(typeName: string) { | ||||||
|  | |||||||
							
								
								
									
										28
									
								
								src/services/tasks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/services/tasks.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||||
|  | } | ||||||
| @ -188,6 +188,12 @@ function fillInAdditionalProperties(entityChange: EntityChange) { | |||||||
|                                                 WHERE attachmentId = ?`,
 |                                                 WHERE attachmentId = ?`,
 | ||||||
|             [entityChange.entityId] |             [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) { |     if (entityChange.entity instanceof AbstractBeccaEntity) { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran