mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 10:02:59 +08:00
Revert "Merge pull request #1234 from TriliumNext/feature/task_list"
This reverts commit 58a8821c229898c45551da16476d44c010c345ef, reversing changes made to 50d491b432ce811c4d5e597e952eb18a89ae6c19.
This commit is contained in:
parent
ee7b97ae56
commit
00e576b052
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@ -5,8 +5,8 @@
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"name": "nodemon server:start",
|
||||
"program": "${workspaceFolder}/src/main",
|
||||
"name": "nodemon start-server",
|
||||
"program": "${workspaceFolder}/src/www",
|
||||
"request": "launch",
|
||||
"restart": true,
|
||||
"runtimeExecutable": "nodemon",
|
||||
|
@ -132,14 +132,3 @@ 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
|
||||
);
|
@ -12,7 +12,6 @@ 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;
|
||||
@ -33,7 +32,6 @@ export default class Becca {
|
||||
attributeIndex!: Record<string, BAttribute[]>;
|
||||
options!: Record<string, BOption>;
|
||||
etapiTokens!: Record<string, BEtapiToken>;
|
||||
tasks!: Record<string, BTask>;
|
||||
|
||||
allNoteSetCache: NoteSet | null;
|
||||
|
||||
@ -50,7 +48,6 @@ export default class Becca {
|
||||
this.attributeIndex = {};
|
||||
this.options = {};
|
||||
this.etapiTokens = {};
|
||||
this.tasks = {};
|
||||
|
||||
this.dirtyNoteSetCache();
|
||||
|
||||
@ -216,14 +213,6 @@ 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<T extends AbstractBeccaEntity<T>>(entityName: string, entityId: string): AbstractBeccaEntity<T> | null {
|
||||
if (!entityName || !entityId) {
|
||||
return null;
|
||||
|
@ -11,10 +11,9 @@ 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, TaskRow } from "./entities/rows.js";
|
||||
import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } 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<void>(async (res, rej) => {
|
||||
const sqlInit = (await import("../services/sql_init.js")).default;
|
||||
@ -64,17 +63,6 @@ function load() {
|
||||
for (const row of sql.getRows<EtapiTokenRow>(`SELECT etapiTokenId, name, tokenHash, utcDateCreated, utcDateModified FROM etapi_tokens WHERE isDeleted = 0`)) {
|
||||
new BEtapiToken(row);
|
||||
}
|
||||
|
||||
try {
|
||||
for (const row of sql.getRows<TaskRow>(`SELECT taskId, parentNoteId, title, dueDate, isDone, isDeleted FROM tasks WHERE isDeleted = 0`)) {
|
||||
new BTask(row);
|
||||
}
|
||||
} catch (e: any) {
|
||||
// Some older migrations trigger becca which would fail since the "tasks" table is not yet defined (didn't reach the right migration).
|
||||
if (!(e.message.includes("no such table"))) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const noteId in becca.notes) {
|
||||
|
@ -1,84 +0,0 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -139,13 +139,3 @@ 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;
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ 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<any>;
|
||||
|
||||
@ -22,8 +21,7 @@ const ENTITY_NAME_TO_ENTITY: Record<string, ConstructorData<any> & EntityClass>
|
||||
notes: BNote,
|
||||
options: BOption,
|
||||
recent_notes: BRecentNote,
|
||||
revisions: BRevision,
|
||||
tasks: BTask
|
||||
revisions: BRevision
|
||||
};
|
||||
|
||||
function getEntityFromEntityName(entityName: keyof typeof ENTITY_NAME_TO_ENTITY) {
|
||||
|
@ -28,8 +28,7 @@ const NOTE_TYPE_ICONS = {
|
||||
doc: "bx bxs-file-doc",
|
||||
contentWidget: "bx bxs-widget",
|
||||
mindMap: "bx bx-sitemap",
|
||||
geoMap: "bx bx-map-alt",
|
||||
taskList: "bx bx-list-check"
|
||||
geoMap: "bx bx-map-alt"
|
||||
};
|
||||
|
||||
/**
|
||||
@ -37,25 +36,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"
|
||||
| "taskList";
|
||||
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap";
|
||||
|
||||
export interface NotePathRecord {
|
||||
isArchived: boolean;
|
||||
|
@ -1,34 +0,0 @@
|
||||
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,8 +6,6 @@ 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[];
|
||||
@ -39,7 +37,6 @@ class FrocaImpl implements Froca {
|
||||
attributes!: Record<string, FAttribute>;
|
||||
attachments!: Record<string, FAttachment>;
|
||||
blobPromises!: Record<string, Promise<void | FBlob> | null>;
|
||||
tasks!: Record<string, FTask>;
|
||||
|
||||
constructor() {
|
||||
this.initializedPromise = this.loadInitialTree();
|
||||
@ -55,7 +52,6 @@ class FrocaImpl implements Froca {
|
||||
this.attributes = {};
|
||||
this.attachments = {};
|
||||
this.blobPromises = {};
|
||||
this.tasks = {};
|
||||
|
||||
this.addResp(resp);
|
||||
}
|
||||
@ -372,24 +368,6 @@ class FrocaImpl implements Froca {
|
||||
});
|
||||
}
|
||||
|
||||
getTask(taskId: string) {
|
||||
return this.tasks[taskId];
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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
|
||||
|
@ -8,8 +8,6 @@ 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);
|
||||
@ -39,8 +37,6 @@ 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}'`);
|
||||
}
|
||||
@ -310,35 +306,6 @@ 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
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { TaskRow, AttachmentRow } from "../../../becca/entities/rows.js";
|
||||
import type { AttachmentRow } from "../../../becca/entities/rows.js";
|
||||
import type { AttributeType } from "../entities/fattribute.js";
|
||||
import type { EntityChange } from "../server_types.js";
|
||||
|
||||
@ -71,7 +71,6 @@ export default class LoadResults {
|
||||
private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[];
|
||||
private optionNames: string[];
|
||||
private attachmentRows: AttachmentRow[];
|
||||
private taskRows: TaskRow[];
|
||||
|
||||
constructor(entityChanges: EntityChange[]) {
|
||||
const entities: Record<string, Record<string, any>> = {};
|
||||
@ -100,8 +99,6 @@ export default class LoadResults {
|
||||
this.optionNames = [];
|
||||
|
||||
this.attachmentRows = [];
|
||||
|
||||
this.taskRows = [];
|
||||
}
|
||||
|
||||
getEntityRow<T extends EntityRowNames>(entityName: T, entityId: string): EntityRowMappings[T] {
|
||||
@ -184,14 +181,6 @@ 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);
|
||||
}
|
||||
@ -212,14 +201,6 @@ 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
|
||||
@ -237,8 +218,7 @@ export default class LoadResults {
|
||||
this.revisionRows.length === 0 &&
|
||||
this.contentNoteIdToComponentId.length === 0 &&
|
||||
this.optionNames.length === 0 &&
|
||||
this.attachmentRows.length === 0 &&
|
||||
this.taskRows.length === 0
|
||||
this.attachmentRows.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,6 @@ async function getNoteTypeItems(command?: TreeCommandNames) {
|
||||
{ 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<string[]>("search-templates");
|
||||
|
@ -1,30 +0,0 @@
|
||||
import type FTask from "../entities/ftask.js";
|
||||
import server from "./server.js";
|
||||
|
||||
interface CreateNewTasksOpts {
|
||||
parentNoteId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export async function createNewTask({ parentNoteId, title }: CreateNewTasksOpts) {
|
||||
await server.post(`tasks`, {
|
||||
parentNoteId,
|
||||
title: title.trim()
|
||||
});
|
||||
}
|
||||
|
||||
export async function toggleTaskDone(taskId: string) {
|
||||
await server.post(`tasks/${taskId}/toggle`);
|
||||
}
|
||||
|
||||
export async function updateTask(task: FTask) {
|
||||
if (!task.taskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await server.patch(`tasks/${task.taskId}/`, {
|
||||
taskId: task.taskId,
|
||||
dueDate: task.dueDate,
|
||||
isDone: task.isDone
|
||||
});
|
||||
}
|
@ -28,8 +28,7 @@ const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
render: null,
|
||||
search: null,
|
||||
text: null,
|
||||
webView: null,
|
||||
taskList: null
|
||||
webView: null
|
||||
};
|
||||
|
||||
const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||
|
@ -35,7 +35,6 @@ 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 = `
|
||||
<div class="note-detail">
|
||||
@ -73,8 +72,7 @@ const typeWidgetClasses = {
|
||||
attachmentDetail: AttachmentDetailTypeWidget,
|
||||
attachmentList: AttachmentListTypeWidget,
|
||||
mindMap: MindMapWidget,
|
||||
geoMap: GeoMapTypeWidget,
|
||||
taskList: TaskListWidget
|
||||
geoMap: GeoMapTypeWidget
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -49,8 +49,7 @@ 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: "taskList", title: t("note_types.task-list"), selectable: false }
|
||||
{ type: "search", title: t("note_types.saved-search"), selectable: false }
|
||||
];
|
||||
|
||||
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => !nt.selectable).map((nt) => nt.type);
|
||||
|
@ -1,268 +0,0 @@
|
||||
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";
|
||||
import dayjs from "dayjs";
|
||||
import calendarTime from "dayjs/plugin/calendar.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
dayjs.extend(calendarTime);
|
||||
|
||||
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;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.note-detail-task-list .task-container li:hover {
|
||||
background: var(--input-hover-background);
|
||||
transition: background 250ms ease-in-out;
|
||||
}
|
||||
|
||||
.note-detail-task-list .task-container li > header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.note-detail-task-list .task-container li .check {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.note-detail-task-list .task-container li .title {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.note-detail-task-list .task-container li .due-date {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.note-detail-task-list .task-container li.overdue .due-date {
|
||||
color: #fd8282;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function buildTasks(tasks: FTask[]) {
|
||||
let html = "";
|
||||
|
||||
const now = dayjs();
|
||||
const dateFormat = "DD-MM-YYYY";
|
||||
for (const task of tasks) {
|
||||
const classes = ["task"];
|
||||
|
||||
if (task.dueDate && dayjs(task.dueDate).isBefore(now, "days")) {
|
||||
classes.push("overdue");
|
||||
}
|
||||
|
||||
html += `<li class="${classes.join(" ")}" data-task-id="${task.taskId}">`;
|
||||
html += "<header>";
|
||||
html += '<span class="title">';
|
||||
html += `<input type="checkbox" class="check" ${task.isDone ? "checked" : ""} />`;
|
||||
html += `${task.title}</span>`;
|
||||
html += '</span>';
|
||||
if (task.dueDate) {
|
||||
html += `<span class="due-date">`;
|
||||
html += `<span class="bx bx-calendar"></span> `;
|
||||
html += dayjs(task.dueDate).calendar(null, {
|
||||
sameDay: `[${t("tasks.due.today")}]`,
|
||||
nextDay: `[${t("tasks.due.tomorrow")}]`,
|
||||
nextWeek: "dddd",
|
||||
lastDay: `[${t("tasks.due.yesterday")}]`,
|
||||
lastWeek: dateFormat,
|
||||
sameElse: dateFormat
|
||||
});
|
||||
html += "</span>";
|
||||
}
|
||||
html += "</header>";
|
||||
html += `<div class="edit-container"></div>`;
|
||||
html += `</li>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function buildEditContainer(task: FTask) {
|
||||
return `\
|
||||
<label>Due date:</label>
|
||||
<input type="date" data-tasks-role="due-date" value="${task.dueDate ?? ""}" />
|
||||
`;
|
||||
}
|
||||
|
||||
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.$addNewTask.val("");
|
||||
}
|
||||
});
|
||||
|
||||
this.$taskContainer.on("change", "input.check", (e) => {
|
||||
const $target = $(e.target);
|
||||
const taskId = $target.closest("li")[0].dataset.taskId;
|
||||
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
taskService.toggleTaskDone(taskId);
|
||||
});
|
||||
|
||||
this.$taskContainer.on("click", "li", (e) => {
|
||||
if ((e.target as HTMLElement).tagName === "INPUT") {
|
||||
return;
|
||||
}
|
||||
|
||||
const $target = $(e.target);
|
||||
|
||||
// Clear existing edit containers.
|
||||
const $existingContainers = this.$taskContainer.find(".edit-container");
|
||||
|
||||
$existingContainers.html("");
|
||||
|
||||
// Add the new edit container.
|
||||
const $editContainer = $target.closest("li").find(".edit-container");
|
||||
const task = this.#getCorrespondingTask($target);
|
||||
if (task) {
|
||||
$editContainer.html(buildEditContainer(task));
|
||||
}
|
||||
});
|
||||
|
||||
this.$taskContainer.on("change", "input:not(.check)", async (e) => {
|
||||
const $target = $(e.target);
|
||||
const task = this.#getCorrespondingTask($target);
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
const role = $target.data("tasks-role");
|
||||
const value = String($target.val());
|
||||
|
||||
switch (role) {
|
||||
case "due-date":
|
||||
task.dueDate = value;
|
||||
break;
|
||||
}
|
||||
|
||||
await taskService.updateTask(task);
|
||||
});
|
||||
}
|
||||
|
||||
#getCorrespondingTask($target: JQuery<HTMLElement>) {
|
||||
const $parentEl = $target.closest("li");
|
||||
if (!$parentEl.length) {
|
||||
return;
|
||||
}
|
||||
const taskId = $parentEl[0].dataset.taskId;
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return froca.getTask(taskId);
|
||||
}
|
||||
|
||||
async #createNewTask(title: string) {
|
||||
if (!title || !this.noteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await taskService.createNewTask({
|
||||
title,
|
||||
parentNoteId: this.noteId
|
||||
});
|
||||
}
|
||||
|
||||
async #getTasks() {
|
||||
if (!this.noteId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (await froca.getTasks(this.noteId)).toSorted((a, b) => {
|
||||
// Sort by due date, closest date first.
|
||||
if (!a.dueDate) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!b.dueDate) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return a.dueDate.localeCompare(b.dueDate, "en");
|
||||
});
|
||||
}
|
||||
|
||||
async doRefresh(note: FNote) {
|
||||
this.$widget.show();
|
||||
|
||||
if (!this.note || !this.noteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tasks = await this.#getTasks();
|
||||
const tasksHtml = buildTasks(tasks);
|
||||
this.$taskContainer.html(tasksHtml);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (this.noteId && loadResults.isTaskListReloaded(this.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1422,8 +1422,7 @@
|
||||
"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",
|
||||
"task-list": "To-Do List"
|
||||
"beta-feature": "Beta"
|
||||
},
|
||||
"protect_note": {
|
||||
"toggle-on": "Protect the note",
|
||||
|
@ -1,24 +0,0 @@
|
||||
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 updateTask(req: Request) {
|
||||
return tasksService.updateTask(req.params.taskId, req.body);
|
||||
}
|
||||
|
||||
export function toggleTaskDone(req: Request) {
|
||||
const { taskId } = req.params;
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return tasksService.toggleTaskDone(taskId);
|
||||
}
|
@ -72,7 +72,6 @@ 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",
|
||||
@ -280,11 +279,6 @@ 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);
|
||||
apiRoute(PATCH, "/api/tasks/:taskId", tasksRoute.updateTask);
|
||||
|
||||
// in case of local electron, local calls are allowed unauthenticated, for server they need auth
|
||||
const clipperMiddleware = isElectron ? [] : [auth.checkEtapiToken];
|
||||
|
||||
|
@ -6,7 +6,7 @@ import packageJson from "../../package.json" with { type: "json" };
|
||||
import dataDir from "./data_dir.js";
|
||||
|
||||
const APP_DB_VERSION = 228;
|
||||
const SYNC_VERSION = 35;
|
||||
const SYNC_VERSION = 34;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
export default {
|
||||
|
@ -888,7 +888,7 @@ class ConsistencyChecks {
|
||||
return `${tableName}: ${count}`;
|
||||
}
|
||||
|
||||
const tables = ["notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens", "blobs", "tasks"];
|
||||
const tables = ["notes", "revisions", "attachments", "branches", "attributes", "etapi_tokens", "blobs"];
|
||||
|
||||
log.info(`Table counts: ${tables.map((tableName) => getTableRowCount(tableName)).join(", ")}`);
|
||||
}
|
||||
|
@ -15,8 +15,7 @@ const noteTypes = [
|
||||
{ type: "doc", defaultMime: "" },
|
||||
{ type: "contentWidget", defaultMime: "" },
|
||||
{ type: "mindMap", defaultMime: "application/json" },
|
||||
{ type: "geoMap", defaultMime: "application/json" },
|
||||
{ type: "taskList", defaultMime: "" }
|
||||
{ type: "geoMap", defaultMime: "application/json" }
|
||||
];
|
||||
|
||||
function getDefaultMimeForNoteType(typeName: string) {
|
||||
|
@ -1,35 +0,0 @@
|
||||
import becca from "../becca/becca.js";
|
||||
import BTask from "../becca/entities/btask.js";
|
||||
import type { TaskRow } from "../becca/entities/rows.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();
|
||||
}
|
||||
|
||||
export function updateTask(taskId: string, content: TaskRow) {
|
||||
const task = becca.tasks[taskId];
|
||||
task.isDone = !!content.isDone;
|
||||
task.dueDate = content.dueDate;
|
||||
task.save();
|
||||
}
|
@ -188,12 +188,6 @@ 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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user