├── .gitignore
├── .github
├── FUNDING.yml
└── workflows
│ └── releases.yml
├── screenshots
├── plugin-panel.png
└── plugin-settings.png
├── src
├── state
│ ├── input.ts
│ ├── visible.ts
│ ├── filter.ts
│ ├── settings.ts
│ ├── theme.ts
│ ├── user-configs.ts
│ └── tasks.ts
├── hooks
│ ├── useRefreshAll.ts
│ └── useHotKey.ts
├── style.css
├── querys
│ ├── next-n-days.ts
│ ├── scheduled.ts
│ ├── anytime.ts
│ └── today.ts
├── utils.ts
├── components
│ ├── TaskInput.tsx
│ ├── TaskSection.tsx
│ ├── TaskFilter.tsx
│ └── TaskItem.tsx
├── main.tsx
├── models
│ └── TaskEntity.ts
├── settings.ts
├── api.ts
└── App.tsx
├── windi.config.ts
├── .envrc
├── devbox.json
├── renovate.json
├── index.html
├── vite.config.ts
├── tsconfig.json
├── .eslintrc.json
├── release.config.js
├── LICENSE
├── logo.svg
├── readme.md
├── package.json
├── devbox.lock
└── CHANGELOG.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [ahonn]
4 |
5 |
--------------------------------------------------------------------------------
/screenshots/plugin-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahonn/logseq-plugin-todo/HEAD/screenshots/plugin-panel.png
--------------------------------------------------------------------------------
/screenshots/plugin-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahonn/logseq-plugin-todo/HEAD/screenshots/plugin-settings.png
--------------------------------------------------------------------------------
/src/state/input.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 |
3 | export const inputState = atom({
4 | key: 'input',
5 | default: '',
6 | });
7 |
--------------------------------------------------------------------------------
/windi.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | darkMode: 'class',
3 | plugins: [
4 | require('windicss/plugin/line-clamp'),
5 | require('windicss/plugin/scroll-snap'),
6 | ],
7 | }
8 |
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Automatically sets up your devbox environment whenever you cd into this
4 | # directory via our direnv integration:
5 |
6 | eval "$(devbox generate direnv --print-envrc)"
7 |
8 | # check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/
9 | # for more details
10 |
--------------------------------------------------------------------------------
/devbox.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": [
3 | "nodejs@18.17.1"
4 | ],
5 | "env": {
6 | "DEVBOX_COREPACK_ENABLED": "true"
7 | },
8 | "shell": {
9 | "init_hook": [
10 | "echo 'Welcome to devbox!' > /dev/null"
11 | ],
12 | "scripts": {
13 | "test": [
14 | "echo \"Error: no test specified\" && exit 1"
15 | ]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/hooks/useRefreshAll.ts:
--------------------------------------------------------------------------------
1 | import { useRecoilCallback } from "recoil";
2 |
3 | export function useRefreshAll() {
4 | const refreshAll = useRecoilCallback(
5 | ({ snapshot, refresh }) =>
6 | () => {
7 | for (const node of snapshot.getNodes_UNSTABLE()) {
8 | refresh(node);
9 | }
10 | },
11 | [],
12 | );
13 |
14 | return refreshAll;
15 | }
16 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "packageRules": [
4 | {
5 | "matchUpdateTypes": ["minor", "patch"],
6 | "automerge": true,
7 | "requiredStatusChecks": null
8 | },
9 | {
10 | "matchPackageNames": ["@logseq/libs"],
11 | "ignoreUnstable": false,
12 | "automerge": false,
13 | "requiredStatusChecks": null
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Logseq Plugin
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | .priority-a {
2 | .rc-checkbox-inner {
3 | @apply border-red-500 !important;
4 | }
5 | }
6 |
7 | .priority-b {
8 | .rc-checkbox-inner {
9 | @apply border-yellow-500 !important;
10 | }
11 | }
12 |
13 | .priority-c {
14 | .rc-checkbox-inner {
15 | @apply border-blue-500 !important;
16 | }
17 | }
18 |
19 | .priority-none {
20 | .rc-checkbox-inner {
21 | @apply border-gray-500 !important;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import reactPlugin from '@vitejs/plugin-react';
2 | import { defineConfig } from 'vite';
3 | import logseqDevPlugin from 'vite-plugin-logseq';
4 | import WindiCSS from 'vite-plugin-windicss';
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [
9 | logseqDevPlugin(),
10 | reactPlugin(),
11 | WindiCSS(),
12 | ],
13 | // Makes HMR available for development
14 | build: {
15 | target: 'esnext',
16 | minify: 'esbuild',
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/src/state/visible.ts:
--------------------------------------------------------------------------------
1 | import { atom, AtomEffect } from 'recoil';
2 |
3 | const visibleChangedEffect: AtomEffect = ({ setSelf }) => {
4 | const eventName = 'ui:visible:changed';
5 | const listener = ({ visible }: { visible: boolean }) => {
6 | setSelf(visible);
7 | };
8 | logseq.on(eventName, listener);
9 | return () => {
10 | logseq.off(eventName, listener);
11 | };
12 | };
13 |
14 | export const visibleState = atom({
15 | key: 'visible',
16 | default: logseq.isMainUIVisible,
17 | effects: [visibleChangedEffect],
18 | });
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "types": ["vite/client"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react"
18 | },
19 | "include": ["./src"]
20 | }
21 |
--------------------------------------------------------------------------------
/src/querys/next-n-days.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 |
3 | export default function getNextNDaysTaskQuery(days: number) {
4 | const start = dayjs().format('YYYYMMDD');
5 | const next = dayjs().add(days, 'd').format('YYYYMMDD');
6 |
7 | const query = `
8 | [:find (pull ?b [*])
9 | :where
10 | [?b :block/marker ?marker]
11 | [(contains? #{"NOW" "LATER" "TODO" "DOING"} ?marker)]
12 | [?b :block/page ?p]
13 | (or
14 | [?b :block/scheduled ?d]
15 | [?b :block/deadline ?d])
16 | [(> ?d ${start})]]
17 | [(> ?d ${next})]]
18 | `;
19 | return query;
20 | }
21 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": [
4 | "eslint:recommended",
5 | "plugin:react/recommended",
6 | "plugin:@typescript-eslint/eslint-recommended",
7 | "plugin:@typescript-eslint/recommended"
8 | ],
9 | "plugins": ["@typescript-eslint", "react-hooks"],
10 | "parser": "@typescript-eslint/parser",
11 | "rules": {
12 | "react-hooks/rules-of-hooks": "error",
13 | "react-hooks/exhaustive-deps": "warn",
14 | "import/prefer-default-export": "off",
15 | "@typescript-eslint/ban-ts-comment": "off",
16 | "@typescript-eslint/no-non-null-assertion": "off",
17 | "@typescript-eslint/explicit-module-boundary-types": "off"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/hooks/useHotKey.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import Mousetrap from 'mousetrap';
3 | import 'mousetrap-global-bind';
4 |
5 | export function useHotKey(hotkey: string) {
6 | useEffect(() => {
7 | if (!hotkey) {
8 | return;
9 | }
10 |
11 | // @ts-ignore
12 | Mousetrap.bindGlobal(
13 | hotkey,
14 | () => window.logseq.hideMainUI(),
15 | 'keydown',
16 | );
17 |
18 | // @ts-ignore
19 | Mousetrap.bindGlobal(
20 | 'Esc',
21 | () => window.logseq.hideMainUI(),
22 | 'keydown',
23 | );
24 |
25 | return () => {
26 | // @ts-ignore
27 | Mousetrap.unbindGlobal(hotkey, 'keydown');
28 | // @ts-ignore
29 | Mousetrap.unbindGlobal('Esc', 'keydown');
30 | };
31 | }, [hotkey]);
32 | }
33 |
--------------------------------------------------------------------------------
/src/state/filter.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "recoil";
2 | import { TaskPriority } from "../models/TaskEntity";
3 |
4 | export const DEFAULT_OPTION = {
5 | label: 'ALL',
6 | value: '',
7 | }
8 |
9 | export const markerState = atom({
10 | key: 'filter/marker',
11 | default: DEFAULT_OPTION,
12 | });
13 |
14 |
15 | export const PRIORITY_OPTIONS = [
16 | TaskPriority.HIGH,
17 | TaskPriority.MEDIUM,
18 | TaskPriority.LOW,
19 | TaskPriority.NONE,
20 | ];
21 |
22 | export const priorityState = atom({
23 | key: 'filter/priority',
24 | default: DEFAULT_OPTION,
25 | });
26 |
27 | export enum SortType {
28 | Asc = 'ASC',
29 | Desc = 'DESC',
30 | }
31 |
32 | export const sortState = atom({
33 | key: 'filter/sort',
34 | default: {
35 | label: 'DESC',
36 | value: SortType.Desc,
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/release.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | branches: ["master"],
3 | plugins: [
4 | [
5 | "@semantic-release/commit-analyzer",
6 | {
7 | preset: "conventionalcommits",
8 | },
9 | ],
10 | "@semantic-release/release-notes-generator",
11 | "@semantic-release/changelog",
12 | [
13 | "@semantic-release/npm",
14 | {
15 | npmPublish: false,
16 | },
17 | ],
18 | "@semantic-release/git",
19 | [
20 | "@semantic-release/exec",
21 | {
22 | prepareCmd:
23 | "zip -qq -r logseq-plugin-todo-${nextRelease.version}.zip dist readme.md LICENSE package.json logo.svg",
24 | },
25 | ],
26 | [
27 | "@semantic-release/github",
28 | {
29 | assets: "logseq-plugin-todo-*.zip",
30 | },
31 | ],
32 | ],
33 | };
34 |
--------------------------------------------------------------------------------
/src/querys/scheduled.ts:
--------------------------------------------------------------------------------
1 | import dayjs, { Dayjs } from 'dayjs';
2 |
3 | export default function getScheduledTaskQuery(
4 | treatJournalEntriesAsScheduled = true,
5 | startDate: Dayjs | Date = new Date(),
6 | ) {
7 | const start = dayjs(startDate).format('YYYYMMDD');
8 |
9 | const journalEntryCond = treatJournalEntriesAsScheduled ? `
10 | (and
11 | [?p :block/journal? true]
12 | [?p :block/journal-day ?d])` : '';
13 |
14 | const query = `
15 | [:find (pull ?b [*])
16 | :where
17 | [?b :block/marker ?marker]
18 | [(contains? #{"NOW" "LATER" "TODO" "DOING"} ?marker)]
19 | [?b :block/page ?p]
20 | (or
21 | (or
22 | [?b :block/scheduled ?d]
23 | [?b :block/deadline ?d])
24 | ${journalEntryCond})
25 | [(> ?d ${start})]]
26 | `;
27 |
28 | return query;
29 | }
30 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { BlockEntity } from '@logseq/libs/dist/LSPlugin';
2 |
3 | export function getBlockUUID(block: BlockEntity) {
4 | if (typeof block.uuid === 'string') {
5 | return block.uuid;
6 | }
7 | // @ts-ignore
8 | return block.uuid.$uuid$;
9 | }
10 |
11 | export function fixPreferredDateFormat(preferredDateFormat: string) {
12 | const format = preferredDateFormat
13 | .replace('yyyy', 'YYYY')
14 | .replace('dd', 'DD')
15 | .replace('do', 'Do')
16 | .replace('EEEE', 'dddd')
17 | .replace('EEE', 'ddd')
18 | .replace('EE', 'dd')
19 | .replace('E', 'dd');
20 | return format;
21 | }
22 |
23 | // https://github.com/logseq/logseq/blob/master/libs/src/helpers.ts#L122
24 | export function isValidUUID(s: string) {
25 | return (
26 | typeof s === 'string' &&
27 | s.length === 36 &&
28 | /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi.test(
29 | s,
30 | )
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022-current
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.github/workflows/releases.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Releases
4 |
5 | env:
6 | PLUGIN_NAME: logseq-plugin-todo
7 |
8 | # Controls when the action will run.
9 | on:
10 | # push:
11 | # branches:
12 | # - "master"
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | release:
19 | # The type of runner that the job will run on
20 | runs-on: ubuntu-latest
21 |
22 | # Steps represent a sequence of tasks that will be executed as part of the job
23 | steps:
24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
25 | - uses: actions/checkout@v3
26 | - uses: actions/setup-node@v3
27 | with:
28 | node-version: "22"
29 | - uses: pnpm/action-setup@v4
30 | with:
31 | version: 9
32 | - run: pnpm install
33 | - run: pnpm build
34 | - name: Install zip
35 | uses: montudor/action-zip@v1
36 | - name: Release
37 | run: npx semantic-release
38 | env:
39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 |
--------------------------------------------------------------------------------
/src/querys/anytime.ts:
--------------------------------------------------------------------------------
1 | export default function getAnytimeTaskQuery(
2 | customMarkers: string[] = [],
3 | treatJournalEntriesAsScheduled = true,
4 | ) {
5 | const markers = customMarkers.map((m) => '"' + m + '"').join(' ');
6 | const excludeJournalEntries = treatJournalEntriesAsScheduled ? `
7 | (not [?p :block/journal? true])
8 | (not [?p :block/journalDay])
9 | ` : '';
10 | const cond =
11 | customMarkers.length > 0
12 | ? `
13 | (or
14 | (and
15 | [(contains? #{"NOW" "LATER" "TODO" "DOING"} ?marker)]
16 | [?b :block/page ?p]
17 | ${excludeJournalEntries}
18 | (not [?b :block/scheduled])
19 | (not [?b :block/deadline]))
20 | (and
21 | [(contains? #{${markers}} ?marker)]
22 | [?b :block/page ?p]
23 | ${excludeJournalEntries}
24 | (not [?b :block/scheduled])
25 | (not [?b :block/deadline])))
26 | `
27 | : `
28 | [(contains? #{"NOW" "LATER" "TODO" "DOING"} ?marker)]
29 | [?b :block/page ?p]
30 | ${excludeJournalEntries}
31 | (not [?b :block/scheduled])
32 | (not [?b :block/deadline])
33 | `;
34 |
35 | const query = `
36 | [:find (pull ?b [*])
37 | :where
38 | [?b :block/marker ?marker]
39 | ${cond}]
40 | `;
41 | return query;
42 | }
43 |
--------------------------------------------------------------------------------
/src/state/settings.ts:
--------------------------------------------------------------------------------
1 | import { atom, AtomEffect } from 'recoil';
2 | import { TaskMarker, TaskPriority } from '../models/TaskEntity';
3 | import settings from '../settings';
4 |
5 | interface IPluginSettings {
6 | hotkey: string;
7 | defaultMarker: TaskMarker;
8 | customMarkers: string;
9 | defaultPriority: TaskPriority;
10 | showNextNDaysTask: boolean;
11 | numberOfNextNDays: number;
12 | lightPrimaryBackgroundColor: string;
13 | lightSecondaryBackgroundColor: string;
14 | darkPrimaryBackgroundColor: string;
15 | darkSecondaryBackgroundColor: string;
16 | useDefaultColors: boolean;
17 | sectionTitleColor: string;
18 | openInRightSidebar: boolean;
19 | whereToPlaceNewTask: string;
20 | treatJournalEntriesAsScheduled: boolean;
21 | }
22 |
23 | const settingsChangedEffect: AtomEffect = ({ setSelf }) => {
24 | setSelf({ ...logseq.settings } as unknown as IPluginSettings);
25 | const unlisten = logseq.onSettingsChanged((newSettings) => {
26 | setSelf(newSettings);
27 | });
28 | return () => unlisten();
29 | };
30 |
31 | export const settingsState = atom({
32 | key: 'settings',
33 | default: settings.reduce((result, item) => ({ ...result, [item.key]: item.default }), {}) as IPluginSettings,
34 | effects: [settingsChangedEffect],
35 | });
36 |
--------------------------------------------------------------------------------
/src/querys/today.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 |
3 | export default function getTodayTaskQuery(
4 | customMarkers: string[] = [],
5 | treatJournalEntriesAsScheduled = true,
6 | ) {
7 | const today = dayjs().format('YYYYMMDD');
8 | const markers = customMarkers.map((m) => '"' + m + '"').join(' ');
9 |
10 | const journalEntryCond = treatJournalEntriesAsScheduled ? `
11 | (and
12 | [(contains? #{"NOW" "LATER" "TODO" "DOING"} ?marker)]
13 | [?p :block/journal? true]
14 | [?p :block/journal-day ?d]
15 | (not [?b :block/scheduled])
16 | (not [?b :block/deadline])
17 | [(<= ?d ${today})])
18 | ` : '';
19 |
20 | const customMarkerCond = customMarkers.length > 0 ? `
21 | (and
22 | [(contains? #{${markers}} ?marker)]
23 | (or
24 | [?b :block/scheduled ?d]
25 | [?b :block/deadline ?d])
26 | [(<= ?d ${today})])
27 | ` : '';
28 |
29 | const query = `
30 | [:find (pull ?b [*])
31 | :where
32 | [?b :block/marker ?marker]
33 | [?b :block/page ?p]
34 | (or
35 | (and
36 | [(contains? #{"NOW" "LATER" "TODO" "DOING"} ?marker)]
37 | (or
38 | [?b :block/scheduled ?d]
39 | [?b :block/deadline ?d])
40 | [(<= ?d ${today})])
41 | ${journalEntryCond}
42 | ${customMarkerCond})]
43 | `;
44 |
45 | return query;
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Logseq Todo Plugin
2 |
3 | A simple to-do list plugin for logseq
4 |
5 | > This plugin relies solely on the Logseq Plugin API to access local data, and does not store it externally.
6 |
7 |
8 |
9 | ### Features
10 | - Quickly add new to-do items to today's journal page.
11 | - View all of today's to-do items (include scheduled & today's journal page).
12 | - View all to-do items without a schedule.
13 | - Ignore to-do items on a specified page.
14 |
15 | 
16 |
17 | 
18 |
19 | ## Install
20 |
21 | ### Option 1: directly install via Marketplace
22 |
23 | ### Option 2: manually load
24 |
25 | - turn on Logseq developer mode
26 | - [download the prebuilt package here](https://github.com/ahonn/logseq-plugin-todo/releases)
27 | - unzip the zip file and load from Logseq plugins page
28 |
29 | ## How to use
30 |
31 | - Pin the plugin to the top bar
32 | - Now To-do items can be easily created or edited from the menu bar through the dedicated icon.
33 | - To set task priority (options are `A`=HIGH, `B`=MEDIUM, `C`=LOW), add `[#A]` to your marker. For example, `TODO [#C] text`.
34 |
35 | ## Page Properties
36 |
37 | - `todo-ignore`: Whether to hide the todo task in the current page. see [How to use todo-ignore #8](https://github.com/ahonn/logseq-plugin-todo/issues/8)
38 |
39 | ## Contribution
40 | Issues and PRs are welcome!
41 |
42 | ## Licence
43 | MIT
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "logseq-plugin-todo",
3 | "version": "1.22.0",
4 | "main": "dist/index.html",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "tsc && vite build",
8 | "preinstall": "npx only-allow pnpm"
9 | },
10 | "license": "MIT",
11 | "dependencies": {
12 | "@logseq/libs": "^0.0.17",
13 | "classnames": "^2.3.2",
14 | "dayjs": "^1.11.6",
15 | "less": "^4.1.3",
16 | "lodash-es": "^4.17.21",
17 | "mousetrap": "^1.6.5",
18 | "mousetrap-global-bind": "^1.1.0",
19 | "rc-checkbox": "^2.3.2",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0",
22 | "react-error-boundary": "^3.1.4",
23 | "react-select": "^5.7.0",
24 | "react-use": "^17.4.0",
25 | "recoil": "^0.7.6",
26 | "remove-markdown": "^0.5.0",
27 | "swr": "^1.3.0",
28 | "tabler-icons-react": "^1.55.0"
29 | },
30 | "devDependencies": {
31 | "@semantic-release/changelog": "6.0.1",
32 | "@semantic-release/exec": "6.0.3",
33 | "@semantic-release/git": "10.0.1",
34 | "@semantic-release/npm": "9.0.1",
35 | "@types/lodash-es": "^4.17.6",
36 | "@types/mousetrap": "^1.6.10",
37 | "@types/node": "18.11.9",
38 | "@types/react": "18.0.25",
39 | "@types/react-dom": "18.0.8",
40 | "@types/remove-markdown": "^0.3.1",
41 | "@typescript-eslint/eslint-plugin": "5.42.1",
42 | "@typescript-eslint/parser": "5.42.1",
43 | "@vitejs/plugin-react": "2.2.0",
44 | "conventional-changelog-conventionalcommits": "5.0.0",
45 | "eslint": "8.27.0",
46 | "eslint-plugin-react": "7.31.10",
47 | "eslint-plugin-react-hooks": "^4.6.0",
48 | "node-fetch": "3.3.0",
49 | "semantic-release": "19.0.5",
50 | "typescript": "4.8.4",
51 | "vite": "3.2.3",
52 | "vite-plugin-logseq": "1.1.2",
53 | "vite-plugin-windicss": "1.8.8",
54 | "windicss": "3.5.6"
55 | },
56 | "logseq": {
57 | "id": "logseq-plugin-todo",
58 | "title": "Todo list",
59 | "icon": "./logo.svg"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/state/theme.ts:
--------------------------------------------------------------------------------
1 | import { selector } from 'recoil';
2 | import { settingsState } from './settings';
3 | import { userConfigsState } from './user-configs';
4 |
5 | export const themeModeState = selector({
6 | key: 'themeMode',
7 | get: ({ get }) => {
8 | const userConfigs = get(userConfigsState);
9 | return userConfigs.preferredThemeMode;
10 | }
11 | });
12 |
13 | const getStyleVariable = (variableName: string) => {
14 | const bodyElement = window.parent.document.body;
15 | if (bodyElement) {
16 | return getComputedStyle(bodyElement).getPropertyValue(variableName);
17 | } else {
18 | return null;
19 | }
20 | }
21 |
22 | export const themeColorsState = selector({
23 | key: 'themeColors',
24 | get: () => {
25 | const themeColors = {
26 | primaryBackgroundColor: getStyleVariable('--ls-primary-background-color')!,
27 | secondaryBackgroundColor: getStyleVariable('--ls-secondary-background-color')!,
28 | sectionTitleColor: getStyleVariable('--ls-link-text-color')!,
29 | }
30 |
31 | if (Object.values(themeColors).some((value) => value === null)) return null
32 | return themeColors
33 | }
34 | });
35 |
36 | export const themeStyleState = selector({
37 | key: 'themeStyle',
38 | get: ({ get }) => {
39 | const settings = get(settingsState);
40 | const themeMode = get(themeModeState);
41 | const themeColors = get(themeColorsState);
42 |
43 | const isLightMode = themeMode === 'light';
44 |
45 | if (settings.useDefaultColors && themeColors) return themeColors;
46 |
47 | const primaryBackgroundColor = isLightMode
48 | ? settings.lightPrimaryBackgroundColor
49 | : settings.darkPrimaryBackgroundColor;
50 |
51 | const secondaryBackgroundColor = isLightMode
52 | ? settings.lightSecondaryBackgroundColor
53 | : settings.darkSecondaryBackgroundColor;
54 |
55 | return {
56 | primaryBackgroundColor,
57 | secondaryBackgroundColor,
58 | sectionTitleColor: settings.sectionTitleColor,
59 | };
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/src/state/user-configs.ts:
--------------------------------------------------------------------------------
1 | import { AppUserConfigs } from '@logseq/libs/dist/LSPlugin';
2 | import { atom, AtomEffect, selector } from 'recoil';
3 | import { logseq as plugin } from '../../package.json';
4 | import { TaskMarker } from '../models/TaskEntity';
5 | import { settingsState } from './settings';
6 |
7 | export const USER_CONFIGS_KEY = `${plugin.id}#userConfigs`;
8 |
9 | export const DEFAULT_USER_CONFIGS: Partial = {
10 | preferredLanguage: 'en',
11 | preferredThemeMode: 'light',
12 | preferredFormat: 'markdown',
13 | preferredWorkflow: 'now',
14 | preferredTodo: 'LATER',
15 | preferredDateFormat: 'MMM do, yyyy',
16 | };
17 |
18 | const themeModeChangeEffect: AtomEffect = ({ onSet }) => {
19 | onSet(({ preferredThemeMode }) => {
20 | if (preferredThemeMode === 'dark') {
21 | document.documentElement.classList.add('dark');
22 | document.documentElement.classList.remove('light');
23 | } else {
24 | document.documentElement.classList.add('light');
25 | document.documentElement.classList.remove('dark');
26 | }
27 | });
28 | };
29 |
30 | const localStorageEffect: AtomEffect = ({ setSelf, onSet }) => {
31 | const savedValue = localStorage.getItem(USER_CONFIGS_KEY);
32 | if (savedValue != null) {
33 | setSelf(JSON.parse(savedValue));
34 | }
35 |
36 | onSet((newValue, _, isReset) => {
37 | isReset
38 | ? localStorage.removeItem(USER_CONFIGS_KEY)
39 | : localStorage.setItem(USER_CONFIGS_KEY, JSON.stringify(newValue));
40 | });
41 | };
42 |
43 | export const userConfigsState = atom({
44 | key: 'userConfigs',
45 | default: DEFAULT_USER_CONFIGS as AppUserConfigs,
46 | effects: [localStorageEffect, themeModeChangeEffect],
47 | });
48 |
49 | export const taskMarkersState = selector<(TaskMarker | string)[]>({
50 | key: 'taskMarkers',
51 | get: ({ get }) => {
52 | const { preferredWorkflow } = get(userConfigsState);
53 | const settings = get(settingsState);
54 | const customMarkers =
55 | settings.customMarkers === '' ? [] : settings.customMarkers.split(',');
56 | if (preferredWorkflow === 'now') {
57 | return [TaskMarker.LATER, TaskMarker.NOW, ...customMarkers];
58 | }
59 | return [TaskMarker.TODO, TaskMarker.DOING, ...customMarkers];
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/src/components/TaskInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useImperativeHandle, useRef } from 'react';
2 | import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
3 | import { CirclePlus } from 'tabler-icons-react';
4 | import { inputState } from '../state/input';
5 | import { themeStyleState } from '../state/theme';
6 | import { visibleState } from '../state/visible';
7 |
8 | export interface ITaskInputRef {
9 | focus: () => void;
10 | }
11 |
12 | export interface ITaskInputProps {
13 | onCreateTask(content: string): Promise;
14 | }
15 |
16 | const TaskInput: React.ForwardRefRenderFunction<
17 | ITaskInputRef,
18 | ITaskInputProps
19 | > = (props, ref) => {
20 | const [input, setInput] = useRecoilState(inputState);
21 | const setVisible = useSetRecoilState(visibleState);
22 | const inputRef = useRef(null);
23 | const themeStyle = useRecoilValue(themeStyleState);
24 |
25 | const focus = () => {
26 | if (inputRef.current) {
27 | inputRef.current.focus();
28 | }
29 | };
30 |
31 | useImperativeHandle(ref, () => ({
32 | focus,
33 | }));
34 |
35 | return (
36 |
37 |
43 |
44 | setInput(e.target.value)}
50 | placeholder="Type to search, enter to create"
51 | onKeyPress={async (e) => {
52 | if (e.key === 'Enter' && input.trim() !== '') {
53 | e.preventDefault();
54 | await props.onCreateTask(input);
55 | setInput('');
56 | // HACK: Force focus to solve the problem that the focus cannot be
57 | // focused after creating the task on current page
58 | setVisible(false);
59 | setTimeout(() => {
60 | setVisible(true);
61 | });
62 | }
63 | }}
64 | />
65 |
66 |
67 | );
68 | };
69 |
70 | export default React.forwardRef(TaskInput);
71 |
--------------------------------------------------------------------------------
/devbox.lock:
--------------------------------------------------------------------------------
1 | {
2 | "lockfile_version": "1",
3 | "packages": {
4 | "nodejs@18.17.1": {
5 | "last_modified": "2023-09-15T06:49:28Z",
6 | "plugin_version": "0.0.2",
7 | "resolved": "github:NixOS/nixpkgs/46688f8eb5cd6f1298d873d4d2b9cf245e09e88e#nodejs_18",
8 | "source": "devbox-search",
9 | "version": "18.17.1",
10 | "systems": {
11 | "aarch64-darwin": {
12 | "outputs": [
13 | {
14 | "name": "out",
15 | "path": "/nix/store/nk77k4d3fj6i2fbd9nmqm5bqqdjy3knb-nodejs-18.17.1",
16 | "default": true
17 | },
18 | {
19 | "name": "libv8",
20 | "path": "/nix/store/495wci9zkck1q1v5akdkh1pljghmqmb3-nodejs-18.17.1-libv8"
21 | }
22 | ],
23 | "store_path": "/nix/store/nk77k4d3fj6i2fbd9nmqm5bqqdjy3knb-nodejs-18.17.1"
24 | },
25 | "aarch64-linux": {
26 | "outputs": [
27 | {
28 | "name": "out",
29 | "path": "/nix/store/kgzwqvksqjms49jlfz9nzz2cjxsh8ani-nodejs-18.17.1",
30 | "default": true
31 | },
32 | {
33 | "name": "libv8",
34 | "path": "/nix/store/r4yp4071a23wcjni822ppw89fac7q5wf-nodejs-18.17.1-libv8"
35 | }
36 | ],
37 | "store_path": "/nix/store/kgzwqvksqjms49jlfz9nzz2cjxsh8ani-nodejs-18.17.1"
38 | },
39 | "x86_64-darwin": {
40 | "outputs": [
41 | {
42 | "name": "out",
43 | "path": "/nix/store/x01bx6r10w0835xmhxyjqn9khamdh9pj-nodejs-18.17.1",
44 | "default": true
45 | },
46 | {
47 | "name": "libv8",
48 | "path": "/nix/store/vad99zm2ila2lgidx478j3xjvi861v03-nodejs-18.17.1-libv8"
49 | }
50 | ],
51 | "store_path": "/nix/store/x01bx6r10w0835xmhxyjqn9khamdh9pj-nodejs-18.17.1"
52 | },
53 | "x86_64-linux": {
54 | "outputs": [
55 | {
56 | "name": "out",
57 | "path": "/nix/store/fg86njc0q2djbyfaqvnaq7x0khpc6sf4-nodejs-18.17.1",
58 | "default": true
59 | },
60 | {
61 | "name": "libv8",
62 | "path": "/nix/store/0zm665zwnqhk6wjpypl77cbwbhdzwi5x-nodejs-18.17.1-libv8"
63 | }
64 | ],
65 | "store_path": "/nix/store/fg86njc0q2djbyfaqvnaq7x0khpc6sf4-nodejs-18.17.1"
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import '@logseq/libs';
2 | import React from 'react';
3 | import * as ReactDOM from 'react-dom/client';
4 | import { RecoilRoot } from 'recoil';
5 | import { logseq as plugin } from '../package.json';
6 | import App from './App';
7 | import settings from './settings';
8 |
9 | type Rect = {
10 | top: number;
11 | right: number;
12 | left: number;
13 | bottom: number;
14 | };
15 |
16 | let cachedRect: Rect | undefined = undefined;
17 |
18 | async function openTaskPanel(e?: { rect: Rect }) {
19 | const taskPanel = document.querySelector('#' + plugin.id)!;
20 | let rect = e?.rect ?? cachedRect;
21 |
22 | if (!rect) {
23 | try {
24 | const elRect = await logseq.UI.queryElementRect('#' + plugin.id);
25 | if (elRect) {
26 | rect = elRect;
27 | cachedRect = elRect;
28 | }
29 | } catch (e) {
30 | console.error(e);
31 | }
32 | }
33 |
34 | if (rect) {
35 | cachedRect = rect;
36 | }
37 |
38 | const position = rect
39 | ? {
40 | top: `${rect.top + 40}px`,
41 | left: rect.left + 'px',
42 | }
43 | : {
44 | top: '40px',
45 | right: '200px',
46 | };
47 |
48 | // @ts-ignore
49 | Object.assign(taskPanel.style, {
50 | position: 'fixed',
51 | ...position,
52 | });
53 |
54 | logseq.showMainUI();
55 | }
56 |
57 | function createModel() {
58 | return {
59 | openTaskPanel,
60 | };
61 | }
62 |
63 | const registeredHotKeySet = new Set();
64 | function registerHotKey(binding: string) {
65 | if (!binding || registeredHotKeySet.has(binding)) {
66 | return;
67 | }
68 |
69 | logseq.App.registerCommandShortcut({ binding }, openTaskPanel);
70 | registeredHotKeySet.add(binding);
71 | }
72 |
73 | function main() {
74 | logseq.setMainUIInlineStyle({
75 | position: 'fixed',
76 | zIndex: 11,
77 | });
78 |
79 | logseq.App.registerUIItem('toolbar', {
80 | key: plugin.id,
81 | template: `
82 |
83 |
84 |
85 | `,
86 | });
87 |
88 | if (logseq.settings?.hotkey) {
89 | registerHotKey(logseq.settings?.hotkey as string);
90 | }
91 | logseq.onSettingsChanged((settings) => {
92 | registerHotKey(settings?.hotkey);
93 | });
94 |
95 | logseq.App.registerCommandPalette(
96 | {
97 | key: 'logseq-plugin-todo',
98 | label: 'Open todo list',
99 | },
100 | () => {
101 | openTaskPanel();
102 | },
103 | );
104 |
105 | const root = ReactDOM.createRoot(document.getElementById('app')!);
106 | root.render(
107 |
108 |
109 |
110 |
111 | ,
112 | );
113 | }
114 |
115 | logseq.useSettingsSchema(settings).ready(createModel()).then(main).catch(console.error);
116 |
--------------------------------------------------------------------------------
/src/models/TaskEntity.ts:
--------------------------------------------------------------------------------
1 | import removeMarkdown from 'remove-markdown';
2 | import { BlockEntity, PageEntity } from '@logseq/libs/dist/LSPlugin.user';
3 | import { getBlockUUID } from '../utils';
4 |
5 | export enum TaskMarker {
6 | LATER = 'LATER',
7 | NOW = 'NOW',
8 | TODO = 'TODO',
9 | DOING = 'DOING',
10 | DONE = 'DONE',
11 | WAITING = 'WAITING',
12 | }
13 |
14 | export enum TaskPriority {
15 | HIGH = 'A',
16 | MEDIUM = 'B',
17 | LOW = 'C',
18 | NONE = 'NONE'
19 | }
20 |
21 | export const TASK_PRIORITY_WEIGHT = {
22 | [TaskPriority.HIGH]: 100,
23 | [TaskPriority.MEDIUM]: 50,
24 | [TaskPriority.LOW]: 10,
25 | [TaskPriority.NONE]: 0,
26 | };
27 |
28 | export interface TaskEntityObject {
29 | uuid: string;
30 | content: string;
31 | rawContent: string;
32 | marker: TaskMarker;
33 | priority: TaskPriority;
34 | scheduled: number | undefined;
35 | repeated: boolean;
36 | completed: boolean;
37 | page: {
38 | name: string;
39 | uuid: string;
40 | journalDay: number | undefined;
41 | updatedAt: number | undefined;
42 | }
43 | }
44 |
45 | class TaskEntity {
46 | private block: BlockEntity;
47 | private page: PageEntity;
48 | private _content: string;
49 |
50 | constructor(block: BlockEntity, page: PageEntity) {
51 | this.block = block;
52 | this.page = page;
53 | this._content = this.trimContent(block.content);
54 | }
55 |
56 | public get uuid(): string {
57 | return getBlockUUID(this.block);
58 | }
59 |
60 | public get content(): string {
61 | return this._content;
62 | }
63 |
64 | public set content(value: string) {
65 | this._content = this.trimContent(value);
66 | }
67 |
68 | public trimContent(rawContent: string): string {
69 | let content = rawContent;
70 | content = content.replace(this.block.marker as string, '');
71 | content = content.replace(`[#${this.block.priority}]`, '');
72 | content = content.replace(/SCHEDULED: <[^>]+>/, '');
73 | content = content.replace(/DEADLINE: <[^>]+>/, '');
74 | content = content.replace(/(:LOGBOOK:)|(\*\s.*)|(:END:)|(CLOCK:.*)/gm, '');
75 | content = content.replace(/id::[^:]+/, '');
76 | content = removeMarkdown(content);
77 | return content.trim();
78 | }
79 |
80 | public get rawContent(): string {
81 | return this.block.content;
82 | }
83 |
84 | public get marker(): TaskMarker {
85 | return this.block.marker as TaskMarker;
86 | }
87 |
88 | public get priority(): TaskPriority {
89 | return (this.block.priority ?? TaskPriority.NONE) as TaskPriority;
90 | }
91 |
92 | public get scheduled(): number | undefined {
93 | return (this.block.scheduled ?? this.block.deadline ?? this.page.journalDay) as number;
94 | }
95 |
96 | public get repeated(): boolean {
97 | return !!this.block.repeated;
98 | }
99 |
100 | public get completed(): boolean {
101 | return this.marker === TaskMarker.DONE;
102 | }
103 |
104 | public getPageProperty(key: string): T {
105 | // @ts-ignore
106 | return this.page.properties?.[key];
107 | }
108 |
109 | public toObject(): TaskEntityObject {
110 | return {
111 | uuid: this.uuid,
112 | content: this._content,
113 | rawContent: this.rawContent,
114 | marker: this.marker,
115 | priority: this.priority,
116 | scheduled: this.scheduled,
117 | repeated: this.repeated,
118 | completed: this.completed,
119 | page: {
120 | name: this.page.name,
121 | uuid: this.page.uuid,
122 | journalDay: this.page['journalDay'],
123 | updatedAt: this.page.updatedAt,
124 | },
125 | };
126 | }
127 | }
128 |
129 | export default TaskEntity;
130 |
--------------------------------------------------------------------------------
/src/settings.ts:
--------------------------------------------------------------------------------
1 | import { SettingSchemaDesc } from '@logseq/libs/dist/LSPlugin';
2 |
3 | const settings: SettingSchemaDesc[] = [
4 | {
5 | key: 'hotkey',
6 | type: 'string',
7 | title: 'Quick Open Hotkey',
8 | description: 'Use this hotkey to quickly open the task panel',
9 | default: 'mod+shift+t',
10 | },
11 | {
12 | key: 'defaultMarker',
13 | type: 'string',
14 | title: 'Default Marker',
15 | description:
16 | 'Assign a default marker to new to-do items and filter your to-do list by markers',
17 | default: '',
18 | },
19 | {
20 | key: 'customMarkers',
21 | type: 'string',
22 | title: 'Custom Markers',
23 | description: 'Custom Markers, separate multiple tags with commas',
24 | default: 'WAITING',
25 | },
26 | {
27 | key: 'defaultPriority',
28 | type: 'string',
29 | title: 'Default Priority',
30 | description:
31 | 'Assign a default priority to new to-do items and filter your to-do list by priority',
32 | default: '',
33 | },
34 | {
35 | key: 'whereToPlaceNewTask',
36 | type: 'string',
37 | title: 'Where to Place New Tasks',
38 | description: 'Choose where new task will be placed on the journal page',
39 | default: '',
40 | },
41 | {
42 | key: 'showNextNDaysTask',
43 | type: 'boolean',
44 | title: 'Show Next N Days Task Section',
45 | description: 'Display a section for the next N days of tasks after today',
46 | default: false,
47 | },
48 | {
49 | key: 'numberOfNextNDays',
50 | type: 'number',
51 | title: 'Number of Next N Days Tasks',
52 | description:
53 | 'Set the number of days in the "Next N Days" section of the task list',
54 | default: 14,
55 | },
56 | {
57 | key: 'treatJournalEntriesAsScheduled',
58 | type: 'boolean',
59 | title: 'Treat journal entries as scheduled',
60 | description: 'Treat entries an a journal page as scheduled on that day',
61 | default: true,
62 | },
63 | {
64 | key: 'openInRightSidebar',
65 | type: 'boolean',
66 | title: 'Open Task in Right Sidebar',
67 | description: 'Open task in the right sidebar',
68 | default: false,
69 | },
70 | {
71 | key: 'useDefaultColors',
72 | type: 'boolean',
73 | title: 'Use Default Colors',
74 | description: 'Use the colors from your current Logseq theme',
75 | default: false,
76 | },
77 | {
78 | key: 'sectionTitleColor',
79 | type: 'string',
80 | title: 'Section Title Color',
81 | description: 'Set the color of task section titles',
82 | default: '#106ba3',
83 | inputAs: 'color',
84 | },
85 | {
86 | key: 'lightPrimaryBackgroundColor',
87 | type: 'string',
88 | title: 'Light Mode Primary Background Color',
89 | description: 'Set the primary background color for light mode',
90 | default: '#ffffff',
91 | inputAs: 'color',
92 | },
93 | {
94 | key: 'lightSecondaryBackgroundColor',
95 | type: 'string',
96 | title: 'Light Mode Secondary Background Color',
97 | description: 'Set the secondary background color for light mode',
98 | default: '#f7f7f7',
99 | inputAs: 'color',
100 | },
101 | {
102 | key: 'darkPrimaryBackgroundColor',
103 | type: 'string',
104 | title: 'Dark Mode Primary Background Color',
105 | description: 'Set the primary background color for dark mode',
106 | default: '#023643',
107 | inputAs: 'color',
108 | },
109 | {
110 | key: 'darkSecondaryBackgroundColor',
111 | type: 'string',
112 | title: 'Dark Mode Secondary Background Color',
113 | description: 'Set the secondary background color for dark mode',
114 | default: '#002B37',
115 | inputAs: 'color',
116 | },
117 | ];
118 |
119 | export default settings;
120 |
--------------------------------------------------------------------------------
/src/state/tasks.ts:
--------------------------------------------------------------------------------
1 | import { BlockEntity, PageEntity } from '@logseq/libs/dist/LSPlugin';
2 | import { selectorFamily } from 'recoil';
3 | import TaskEntity, {
4 | TaskEntityObject,
5 | TASK_PRIORITY_WEIGHT,
6 | } from '../models/TaskEntity';
7 | import { getBlockUUID, isValidUUID } from '../utils';
8 | import { markerState, priorityState, sortState, SortType } from './filter';
9 |
10 | async function getTaskEntitiesByQuery(query: string) {
11 | const collections = await window.logseq.DB.datascriptQuery(
12 | query,
13 | );
14 | const tasks = await Promise.all(
15 | (collections ?? []).map(async ([item]) => {
16 | const uuid = getBlockUUID(item);
17 | const block = await window.logseq.Editor.getBlock(uuid, {
18 | includeChildren: true,
19 | });
20 | if (block === undefined) {
21 | return null;
22 | }
23 |
24 | const page = await window.logseq.Editor.getPage(
25 | (block?.page as PageEntity).name,
26 | );
27 | if (page === undefined) {
28 | return null;
29 | }
30 |
31 | if (page?.journalDay) {
32 | const blocksTree = await window.logseq.Editor.getPageBlocksTree(page!.uuid);
33 | page.properties = Object.assign(page.properties ?? {}, blocksTree[0].properties)
34 | }
35 |
36 | const taskEntity = new TaskEntity(block!, page!);
37 | if (
38 | taskEntity.content.startsWith('((') &&
39 | taskEntity.content.endsWith('))')
40 | ) {
41 | const uuid = taskEntity.content.slice(2, -2);
42 | if (isValidUUID(uuid)) {
43 | const block = await window.logseq.Editor.getBlock(uuid, {
44 | includeChildren: true,
45 | });
46 | if (block) {
47 | taskEntity.content = block.content;
48 | }
49 | }
50 | }
51 | return taskEntity;
52 | }),
53 | );
54 |
55 | return (
56 | tasks
57 | // @ts-ignore
58 | .filter((task) => {
59 | return task && !task.getPageProperty('todoIgnore');
60 | })
61 | .map((task) => task!.toObject())
62 | .sort((a, b) => {
63 | if (a.scheduled !== undefined || b.scheduled !== undefined) {
64 | if (a.scheduled === b.scheduled) {
65 | return (
66 | TASK_PRIORITY_WEIGHT[b.priority] -
67 | TASK_PRIORITY_WEIGHT[a.priority]
68 | );
69 | }
70 | return (b.scheduled ?? 0) - (a.scheduled ?? 0);
71 | }
72 |
73 | if (a.page.updatedAt !== undefined || b.page.updatedAt !== undefined) {
74 | if (a.page.updatedAt === b.page.updatedAt) {
75 | return (
76 | TASK_PRIORITY_WEIGHT[b.priority] -
77 | TASK_PRIORITY_WEIGHT[a.priority]
78 | );
79 | }
80 | return (b.page.updatedAt ?? 0) - (a.page.updatedAt ?? 0);
81 | }
82 |
83 | return 0;
84 | })
85 | );
86 | }
87 |
88 | export const tasksState = selectorFamily({
89 | key: 'tasks',
90 | get: (query: string) => () => getTaskEntitiesByQuery(query),
91 | cachePolicy_UNSTABLE: {
92 | eviction: 'most-recent',
93 | },
94 | });
95 |
96 | export const filterdTasksState = selectorFamily({
97 | key: 'filterd-tasks',
98 | get:
99 | (query: string) =>
100 | ({ get }) => {
101 | const tasks = get(tasksState(query));
102 | const marker = get(markerState);
103 | const priority = get(priorityState);
104 | const sort = get(sortState);
105 |
106 | return tasks.filter((task: TaskEntityObject) => {
107 | if (marker.value && task.marker !== marker.value) {
108 | return false;
109 | }
110 |
111 | if (priority.value && task.priority !== priority.value) {
112 | return false;
113 | }
114 |
115 | return true;
116 | }).sort((a, b) => {
117 | if (a.scheduled === undefined || b.scheduled === undefined) {
118 | return 0;
119 | }
120 | if (sort.value === SortType.Asc) {
121 | return a.scheduled - b.scheduled;
122 | }
123 | return b.scheduled - a.scheduled;
124 | });
125 | },
126 | });
127 |
--------------------------------------------------------------------------------
/src/components/TaskSection.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useState } from 'react';
2 | import {
3 | useRecoilCallback,
4 | useRecoilValue,
5 | useRecoilValueLoadable,
6 | } from 'recoil';
7 | import { groupBy } from 'lodash-es';
8 | import { TaskEntityObject } from '../models/TaskEntity';
9 | import { filterdTasksState } from '../state/tasks';
10 | import { themeStyleState } from '../state/theme';
11 | import { visibleState } from '../state/visible';
12 | import TaskItem from './TaskItem';
13 | import { settingsState } from '../state/settings';
14 | import { openTask, openTaskPage } from '../api';
15 | import { inputState } from '../state/input';
16 | import { ChevronsRight } from 'tabler-icons-react';
17 |
18 | export enum GroupBy {
19 | Page,
20 | Tag,
21 | Namespace,
22 | }
23 |
24 | export interface ITaskSectionProps {
25 | title: string;
26 | query: string;
27 | groupBy?: GroupBy;
28 | }
29 |
30 | const TaskSection: React.FC = (props) => {
31 | const { title, query } = props;
32 | const [tasks, setTasks] = useState([]);
33 | const visible = useRecoilValue(visibleState);
34 | const tasksLoadable = useRecoilValueLoadable(filterdTasksState(query));
35 | const themeStyle = useRecoilValue(themeStyleState);
36 | const { openInRightSidebar } = useRecoilValue(settingsState);
37 | const input = useRecoilValue(inputState);
38 |
39 | const refreshAll = useRecoilCallback(
40 | ({ snapshot, refresh }) =>
41 | () => {
42 | for (const node of snapshot.getNodes_UNSTABLE()) {
43 | refresh(node);
44 | }
45 | },
46 | [],
47 | );
48 |
49 | useEffect(() => {
50 | switch (tasksLoadable.state) {
51 | case 'hasValue': {
52 | const tasks = tasksLoadable.contents.filter(
53 | (task: TaskEntityObject) => {
54 | return task.content.toLowerCase().includes(input.toLowerCase());
55 | },
56 | );
57 | setTasks(tasks);
58 | break;
59 | }
60 | case 'hasError':
61 | throw tasksLoadable.contents;
62 | }
63 | }, [tasksLoadable.state, tasksLoadable.contents, input]);
64 |
65 | useEffect(() => {
66 | if (visible) {
67 | refreshAll();
68 | }
69 | }, [visible, refreshAll]);
70 |
71 | const taskGroups = useMemo(() => {
72 | switch (props.groupBy) {
73 | case GroupBy.Page:
74 | return groupBy(tasks, (task: TaskEntityObject) => task.page.name);
75 | default:
76 | return { '': tasks };
77 | }
78 | }, [props.groupBy, tasks]);
79 |
80 | const openTaskGroups = React.useCallback(() => {
81 | const tasksCopy = [...tasks];
82 | tasksCopy.reverse();
83 |
84 | tasksCopy.forEach((task) => {
85 | openTask(task, {
86 | openInRightSidebar: true,
87 | });
88 | });
89 | window.logseq.hideMainUI();
90 | }, [tasks]);
91 |
92 | if (tasks.length === 0) {
93 | return null;
94 | }
95 |
96 | return (
97 |
98 |
99 |
103 | {title}
104 |
105 |
106 |
110 |
111 |
112 |
113 | {(Object.entries(taskGroups) ?? []).map(([name, tasks]) => {
114 | const [{ page }] = tasks;
115 | return (
116 |
117 | {name && (
118 |
openTaskPage(page, { openInRightSidebar })}
121 | >
122 | {name}
123 |
124 | )}
125 | {(tasks ?? []).map((task) => (
126 |
127 | ))}
128 |
129 | );
130 | })}
131 |
132 |
133 | );
134 | };
135 |
136 | export default TaskSection;
137 |
--------------------------------------------------------------------------------
/src/api.ts:
--------------------------------------------------------------------------------
1 | import { BlockEntity } from '@logseq/libs/dist/LSPlugin.user';
2 | import dayjs from 'dayjs';
3 | import { TaskEntityObject, TaskMarker, TaskPriority } from './models/TaskEntity';
4 |
5 | export const MARKER_GROUPS: Record = {
6 | [TaskMarker.TODO]: [TaskMarker.TODO, TaskMarker.DOING],
7 | [TaskMarker.LATER]: [TaskMarker.LATER, TaskMarker.NOW],
8 | };
9 |
10 | export interface ITaskOptions {
11 | marker?: TaskMarker | string;
12 | markerGroup?: (TaskMarker | string)[];
13 | priority?: TaskPriority;
14 | whereToPlaceNewTask?: string;
15 | }
16 |
17 | export function isTodayTask(task: TaskEntityObject) {
18 | const { scheduled } = task;
19 | if (!scheduled) return false;
20 | return dayjs(new Date()).format('YYYYMMDD') === scheduled.toString();
21 | }
22 |
23 | export async function createNewTask(date: string, content: string, opts: ITaskOptions) {
24 | const { marker, priority, whereToPlaceNewTask } = opts;
25 | const rawContent = `${marker} ${priority ? `[#${priority}]` : ''} ${content}`;
26 | let page = await window.logseq.Editor.getPage(date);
27 | if (page === null) {
28 | page = await window.logseq.Editor.createPage(date, {
29 | journal: true,
30 | redirect: false,
31 | });
32 | }
33 | const blocksTree = await window.logseq.Editor.getPageBlocksTree(date);
34 |
35 | if (whereToPlaceNewTask) {
36 | let parentBlock = blocksTree.find(
37 | (block: BlockEntity) => block.content === whereToPlaceNewTask,
38 | );
39 | if (parentBlock === undefined) {
40 | parentBlock = (await window.logseq.Editor.appendBlockInPage(
41 | page!.name,
42 | whereToPlaceNewTask,
43 | )) as BlockEntity;
44 | }
45 | await window.logseq.Editor.insertBlock(parentBlock!.uuid, rawContent);
46 | } else {
47 | await window.logseq.Editor.appendBlockInPage(page!.name, rawContent);
48 | }
49 |
50 | if (blocksTree.length === 1 && blocksTree[0].content === '') {
51 | await window.logseq.Editor.removeBlock(blocksTree[0].uuid);
52 | }
53 | }
54 |
55 | export async function toggleTaskStatus(
56 | task: TaskEntityObject,
57 | options: ITaskOptions & Required>,
58 | ) {
59 | const { uuid, completed, marker } = task;
60 | const nextMarker = completed ? options.marker : TaskMarker.DONE;
61 | await window.logseq.Editor.updateBlock(uuid, task.rawContent.replace(marker, nextMarker));
62 | }
63 |
64 | interface IOpenTaskOptions {
65 | openInRightSidebar?: boolean;
66 | }
67 |
68 | export function openTask(task: TaskEntityObject, opts?: IOpenTaskOptions) {
69 | const { uuid } = task;
70 | if (opts?.openInRightSidebar) {
71 | return window.logseq.Editor.openInRightSidebar(uuid);
72 | }
73 | return window.logseq.Editor.scrollToBlockInPage(task.page.name, uuid);
74 | }
75 |
76 | export function openTaskPage(page: TaskEntityObject['page'], opts?: IOpenTaskOptions) {
77 | if (opts?.openInRightSidebar) {
78 | return window.logseq.Editor.openInRightSidebar(page.uuid);
79 | }
80 | return window.logseq.Editor.scrollToBlockInPage(page.name, page.uuid);
81 | }
82 |
83 | export async function toggleTaskMarker(
84 | task: TaskEntityObject,
85 | options: ITaskOptions & Required>,
86 | ) {
87 | const { uuid, rawContent, marker } = task;
88 | const { markerGroup } = options;
89 | const currentMarkIndex = markerGroup.findIndex((m) => m === marker);
90 | const newMarker = markerGroup[(currentMarkIndex + 1) % markerGroup.length];
91 |
92 | const newRawContent = rawContent.replace(new RegExp(`^${marker}`), newMarker);
93 | await window.logseq.Editor.updateBlock(uuid, newRawContent);
94 | }
95 |
96 | export async function setTaskScheduled(task: TaskEntityObject, date: Date | null) {
97 | const { uuid, rawContent } = task;
98 | let newRawContent = task.rawContent;
99 | if (date === null) {
100 | newRawContent = rawContent.replace(/SCHEDULED: <[^>]+>/, '');
101 | await window.logseq.Editor.updateBlock(uuid, newRawContent);
102 | return;
103 | }
104 |
105 | const scheduledString = `SCHEDULED: <${dayjs(date).format('YYYY-MM-DD ddd')}>`;
106 | if (rawContent.includes('SCHEDULED')) {
107 | newRawContent = rawContent.replace(/SCHEDULED: <[^>]+>/, scheduledString);
108 | } else {
109 | const lines = rawContent.split('\n');
110 | lines.splice(1, 0, scheduledString);
111 | newRawContent = lines.join('\n');
112 | }
113 |
114 | await window.logseq.Editor.updateBlock(uuid, newRawContent);
115 | }
116 |
--------------------------------------------------------------------------------
/src/components/TaskFilter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Select, { Theme } from 'react-select';
3 | import { useRecoilState, useRecoilValue } from 'recoil';
4 | import { CircleOff } from 'tabler-icons-react';
5 | import { taskMarkersState } from '../state/user-configs';
6 | import {
7 | DEFAULT_OPTION,
8 | markerState,
9 | priorityState,
10 | PRIORITY_OPTIONS,
11 | sortState,
12 | SortType,
13 | } from '../state/filter';
14 | import { themeStyleState } from '../state/theme';
15 | import { settingsState } from '../state/settings';
16 |
17 | const TaskFilter: React.FC = () => {
18 | const [marker, setMarker] = useRecoilState(markerState);
19 | const [priority, setPriority] = useRecoilState(priorityState);
20 | const [sort, setSort] = useRecoilState(sortState);
21 | const taskMarkers = useRecoilValue(taskMarkersState);
22 | const themeStyle = useRecoilValue(themeStyleState);
23 | const settings = useRecoilValue(settingsState);
24 |
25 | const markerOptions = React.useMemo(() => {
26 | return taskMarkers.reduce(
27 | (options, marker) => {
28 | return [...options, { label: marker, value: marker }];
29 | },
30 | [DEFAULT_OPTION],
31 | );
32 | }, [taskMarkers]);
33 |
34 | const priorityOptions = React.useMemo(() => {
35 | return PRIORITY_OPTIONS.reduce(
36 | (options, marker) => {
37 | return [...options, { label: marker, value: marker }];
38 | },
39 | [DEFAULT_OPTION],
40 | );
41 | }, []);
42 |
43 | const selectClassNames = React.useMemo(
44 | () => ({
45 | container: () => 'text-xs',
46 | control: () => '!h-6 !min-h-6 w-12 !border-none !shadow-none !bg-transparent ',
47 | valueContainer: () => '!py-0 !px-1 cursor-pointer bg-transparent',
48 | singleValue: () => `!text-gray-500 !dark:text-gray-300`,
49 | indicatorsContainer: () => '!hidden',
50 | menu: () => `!-mt-0.5`,
51 | option: () => `!py-1 !px-2`,
52 | }),
53 | [],
54 | );
55 |
56 | const selectTheme = React.useCallback(
57 | (theme: Theme) => ({
58 | ...theme,
59 | colors: {
60 | ...theme.colors,
61 | primary: themeStyle.sectionTitleColor,
62 | primary25: themeStyle.secondaryBackgroundColor,
63 | neutral0: themeStyle.primaryBackgroundColor,
64 | },
65 | }),
66 | [themeStyle],
67 | );
68 |
69 | React.useEffect(() => {
70 | const marker = markerOptions.find((marker) => marker.value === settings.defaultMarker);
71 | if (marker) {
72 | setMarker(marker);
73 | }
74 |
75 | const priority = priorityOptions.find(
76 | (priority) => priority.value === settings.defaultPriority,
77 | );
78 | if (priority) {
79 | setPriority(priority);
80 | }
81 | }, [settings, markerOptions, priorityOptions, setMarker, setPriority]);
82 |
83 | const handleReset = () => {
84 | setMarker(DEFAULT_OPTION);
85 | setPriority(DEFAULT_OPTION);
86 | };
87 |
88 | return (
89 |
95 |
96 |
97 | Marker:
98 |
107 |
108 | Priority:
109 |
118 |
119 | Sort:
120 |
138 |
139 | {(marker.value || priority.value) && (
140 |
145 | )}
146 |
147 | );
148 | };
149 |
150 | export default TaskFilter;
151 |
--------------------------------------------------------------------------------
/src/components/TaskItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import classnames from 'classnames';
3 | import dayjs from 'dayjs';
4 | import Checkbox from 'rc-checkbox';
5 | import { ArrowDownCircle, BrightnessUp } from 'tabler-icons-react';
6 | import { TaskEntityObject } from '../models/TaskEntity';
7 | import 'rc-checkbox/assets/index.css';
8 | import { useRecoilValue } from 'recoil';
9 | import { taskMarkersState, userConfigsState } from '../state/user-configs';
10 | import { themeStyleState } from '../state/theme';
11 | import {
12 | isTodayTask,
13 | openTask,
14 | toggleTaskMarker,
15 | setTaskScheduled,
16 | toggleTaskStatus,
17 | } from '../api';
18 | import { settingsState } from '../state/settings';
19 | import { fixPreferredDateFormat } from '../utils';
20 |
21 | export interface ITaskItemProps {
22 | task: TaskEntityObject;
23 | onChange(): void;
24 | }
25 |
26 | const TaskItem: React.FC = (props) => {
27 | const { task, onChange } = props;
28 | const themeStyle = useRecoilValue(themeStyleState);
29 | const { preferredDateFormat, preferredTodo } = useRecoilValue(userConfigsState);
30 | const taskMarkers = useRecoilValue(taskMarkersState);
31 | const settings = useRecoilValue(settingsState);
32 | const [checked, setChecked] = React.useState(task.completed);
33 |
34 | const isExpiredTask = useMemo(() => {
35 | if (!task.scheduled) {
36 | return false;
37 | }
38 | const date = dayjs(task.scheduled.toString(), 'YYYYMMDD');
39 | return date.isBefore(dayjs(), 'day');
40 | }, [task.scheduled]);
41 |
42 | const openTaskBlock = (e: React.MouseEvent) => {
43 | const openInRightSidebar = e.nativeEvent.shiftKey
44 | ? !settings.openInRightSidebar
45 | : settings.openInRightSidebar;
46 | openTask(task, {
47 | openInRightSidebar,
48 | });
49 | window.logseq.hideMainUI();
50 | };
51 |
52 | const toggleStatus = async () => {
53 | await toggleTaskStatus(task, { marker: preferredTodo as string });
54 | setChecked(!checked);
55 | };
56 |
57 | const toggleMarker = () => {
58 | toggleTaskMarker(task, { markerGroup: taskMarkers });
59 | onChange();
60 | };
61 |
62 | const contentClassName = classnames('mb-1 line-clamp-3 cursor-pointer', {
63 | 'line-through': checked,
64 | 'text-gray-400': checked,
65 | });
66 |
67 | return (
68 |
72 |
73 |
78 |
79 |
80 |
81 |
82 |
83 |
88 | {task.marker}
89 |
90 |
91 | {task.content}
92 |
93 |
94 |
95 | {task.scheduled && (
96 |
106 | )}
107 |
108 |
109 | {isTodayTask(task) ? (
110 |
{
113 | setTaskScheduled(task, null)
114 | onChange();
115 | }}
116 | >
117 |
124 |
125 | ) : (
126 |
{
129 | setTaskScheduled(task, new Date());
130 | onChange();
131 | }}
132 | >
133 |
137 |
138 | )}
139 |
140 |
141 |
142 | );
143 | };
144 |
145 | export default TaskItem;
146 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import 'virtual:windi.css';
2 | import React, { useEffect, useRef } from 'react';
3 | import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
4 | import dayjs from 'dayjs';
5 | import advancedFormat from 'dayjs/plugin/advancedFormat';
6 | import TaskInput, { ITaskInputRef } from './components/TaskInput';
7 | import TaskSection, { GroupBy } from './components/TaskSection';
8 | import TaskFilter from './components/TaskFilter';
9 | import { logseq as plugin } from '../package.json';
10 | import { useRecoilState, useRecoilValue } from 'recoil';
11 | import { visibleState } from './state/visible';
12 | import { userConfigsState } from './state/user-configs';
13 | import { themeStyleState } from './state/theme';
14 | import getTodayTaskQuery from './querys/today';
15 | import getScheduledTaskQuery from './querys/scheduled';
16 | import getAnytimeTaskQuery from './querys/anytime';
17 | import { settingsState } from './state/settings';
18 | import * as api from './api';
19 | import getNextNDaysTaskQuery from './querys/next-n-days';
20 | import { fixPreferredDateFormat } from './utils';
21 | import './style.css';
22 | import { markerState, priorityState } from './state/filter';
23 | import { TaskMarker, TaskPriority } from './models/TaskEntity'
24 | import { useRefreshAll } from './hooks/useRefreshAll';
25 | import { useHotKey } from './hooks/useHotKey';
26 |
27 | dayjs.extend(advancedFormat);
28 |
29 | function ErrorFallback({ error }: FallbackProps) {
30 | return (
31 |
32 |
Todo list failed to render.
33 |
Can you re-index your graph and try again?
34 |
[Error]: {error.message}
35 |
36 | );
37 | }
38 |
39 | function App() {
40 | const innerRef = useRef(null);
41 | const inputRef = useRef(null);
42 | const visible = useRecoilValue(visibleState);
43 | const [userConfigs, setUserConfigs] = useRecoilState(userConfigsState);
44 | const themeStyle = useRecoilValue(themeStyleState);
45 | const settings = useRecoilValue(settingsState);
46 | const marker = useRecoilValue(markerState);
47 | const priority = useRecoilValue(priorityState);
48 | const refreshAll = useRefreshAll();
49 |
50 | useHotKey(settings.hotkey);
51 |
52 | useEffect(() => {
53 | if (visible) {
54 | setTimeout(() => {
55 | inputRef.current?.focus();
56 | }, 0);
57 | refreshAll();
58 | }
59 |
60 | window.logseq.App.getUserConfigs().then(setUserConfigs);
61 | }, [visible, refreshAll, setUserConfigs]);
62 |
63 | const handleClickOutside = (e: React.MouseEvent) => {
64 | if (!innerRef.current?.contains(e.target as unknown as Node)) {
65 | window.logseq.hideMainUI();
66 | }
67 | };
68 |
69 | const createNewTask = async (content: string) => {
70 | const { preferredDateFormat, preferredTodo} = userConfigs!;
71 | const { whereToPlaceNewTask } = settings;
72 | const date = dayjs().format(fixPreferredDateFormat(preferredDateFormat!));
73 | await api.createNewTask(date, content, {
74 | marker: marker.value || preferredTodo as TaskMarker,
75 | priority: priority.value as TaskPriority,
76 | whereToPlaceNewTask,
77 | });
78 | refreshAll();
79 | };
80 |
81 | const customMarkers = settings.customMarkers.split(',');
82 | const treatJournalEntriesAsScheduled = settings.treatJournalEntriesAsScheduled;
83 |
84 | return (
85 |
89 |
90 |
97 |
98 |
99 |
100 |
101 |
110 | {settings.showNextNDaysTask && (
111 |
115 | )}
116 |
127 |
137 |
138 |
139 |
140 |
141 |
142 | );
143 | }
144 |
145 | export default App;
146 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # [1.22.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.21.0...v1.22.0) (2025-02-27)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * fix type and add devbox ([62a9ee9](https://github.com/ahonn/logseq-plugin-todo/commit/62a9ee95fb073b8d52cc8a1b8de54e45afcae168))
7 |
8 |
9 | ### Features
10 |
11 | * optionally do not treat journal entries as scheduled ([020bf6f](https://github.com/ahonn/logseq-plugin-todo/commit/020bf6f9e486c201aedc9cb5e9fad5cf0de0e87c))
12 |
13 | # [1.21.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.20.2...v1.21.0) (2024-02-04)
14 |
15 |
16 | ### Bug Fixes
17 |
18 | * fixed journals template properties missing, [#63](https://github.com/ahonn/logseq-plugin-todo/issues/63) ([450cdf9](https://github.com/ahonn/logseq-plugin-todo/commit/450cdf9616d37a4be375f2eee275526bb9d1eb14))
19 |
20 |
21 | ### Features
22 |
23 | * refactor task creation process, force focus after create new task, [#68](https://github.com/ahonn/logseq-plugin-todo/issues/68) ([c7dc80e](https://github.com/ahonn/logseq-plugin-todo/commit/c7dc80e9f0b23c027a7dcecdd1fd5fdd60d6fcfd))
24 | * support task scheduled time sorting ([9570e68](https://github.com/ahonn/logseq-plugin-todo/commit/9570e682cd4857131a276be118184f9f69cc12a3))
25 |
26 | ## [1.20.2](https://github.com/ahonn/logseq-plugin-todo/compare/v1.20.1...v1.20.2) (2024-01-09)
27 |
28 |
29 | ### Bug Fixes
30 |
31 | * types and apis ([797f99b](https://github.com/ahonn/logseq-plugin-todo/commit/797f99b7acf671b5369e3de3aeb83f0197463cca))
32 |
33 | ## [1.20.1](https://github.com/ahonn/logseq-plugin-todo/compare/v1.20.0...v1.20.1) (2023-12-24)
34 |
35 |
36 | ### Bug Fixes
37 |
38 | * fix queryElementRect issue and add fallback position, [#64](https://github.com/ahonn/logseq-plugin-todo/issues/64) ([bb3a551](https://github.com/ahonn/logseq-plugin-todo/commit/bb3a55100cb6ad31923383fd30e3f57b44e5efbc))
39 |
40 | # [1.20.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.19.0...v1.20.0) (2023-11-24)
41 |
42 |
43 | ### Features
44 |
45 | * show ref block task content ([ec6659c](https://github.com/ahonn/logseq-plugin-todo/commit/ec6659c057744e0e5226e4431ff7d3204f0bbf39))
46 |
47 | # [1.19.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.18.0...v1.19.0) (2023-04-23)
48 |
49 |
50 | ### Features
51 |
52 | * including TODOs from future daily note pages, [#52](https://github.com/ahonn/logseq-plugin-todo/issues/52) ([035ebac](https://github.com/ahonn/logseq-plugin-todo/commit/035ebaca809445b57cdb87a0ceb4ff7cc5cb3baf))
53 |
54 | # [1.18.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.17.0...v1.18.0) (2023-04-23)
55 |
56 |
57 | ### Features
58 |
59 | * add custom markers setting support and adjust query logic ([d1714e1](https://github.com/ahonn/logseq-plugin-todo/commit/d1714e1f6da1e76809c1a9e6c3a2b50751e661a4))
60 |
61 | # [1.17.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.16.5...v1.17.0) (2023-03-28)
62 |
63 |
64 | ### Features
65 |
66 | * add shift click hotkey to open task in right sidebar, [#47](https://github.com/ahonn/logseq-plugin-todo/issues/47) ([1b5fcd7](https://github.com/ahonn/logseq-plugin-todo/commit/1b5fcd7848027dd2fcb15b786cca2c28c6c3ebde))
67 | * support trigger the opposite behavior of openInRightSidebar, [#47](https://github.com/ahonn/logseq-plugin-todo/issues/47) ([196d5e1](https://github.com/ahonn/logseq-plugin-todo/commit/196d5e13730bb1b679b297808bcd3fac778382d5))
68 |
69 | ## [1.16.5](https://github.com/ahonn/logseq-plugin-todo/compare/v1.16.4...v1.16.5) (2023-03-14)
70 |
71 |
72 | ### Bug Fixes
73 |
74 | * refactor with new hooks, and fix theme mode issue ([65d5706](https://github.com/ahonn/logseq-plugin-todo/commit/65d5706736a7f815446f596f772fa9099cf07f51))
75 |
76 | ## [1.16.4](https://github.com/ahonn/logseq-plugin-todo/compare/v1.16.3...v1.16.4) (2023-03-09)
77 |
78 |
79 | ### Bug Fixes
80 |
81 | * update userConfigsState to use getUserConfigs() as default value, [#31](https://github.com/ahonn/logseq-plugin-todo/issues/31) ([a1a80eb](https://github.com/ahonn/logseq-plugin-todo/commit/a1a80eb53b5296d715f9f2bdc923da53db16d6cc))
82 |
83 | ## [1.16.3](https://github.com/ahonn/logseq-plugin-todo/compare/v1.16.2...v1.16.3) (2023-03-01)
84 |
85 |
86 | ### Bug Fixes
87 |
88 | * fixed task item work break issue, [#41](https://github.com/ahonn/logseq-plugin-todo/issues/41) ([e624f03](https://github.com/ahonn/logseq-plugin-todo/commit/e624f0380181b5979743d9f4310f274efa2cc1ce))
89 |
90 | ## [1.16.2](https://github.com/ahonn/logseq-plugin-todo/compare/v1.16.1...v1.16.2) (2023-02-28)
91 |
92 |
93 | ### Bug Fixes
94 |
95 | * update logseq sdk ([b1f4138](https://github.com/ahonn/logseq-plugin-todo/commit/b1f413812e6cb66116545bee1f2a52f8ac2f47f5))
96 |
97 | ## [1.16.1](https://github.com/ahonn/logseq-plugin-todo/compare/v1.16.0...v1.16.1) (2023-02-26)
98 |
99 |
100 | ### Bug Fixes
101 |
102 | * make the font color of the filter more comfortable ([f37aceb](https://github.com/ahonn/logseq-plugin-todo/commit/f37aceb49b156e93756d4b1587e1196ac3734ce6))
103 |
104 | # [1.16.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.15.0...v1.16.0) (2023-02-26)
105 |
106 |
107 | ### Features
108 |
109 | * add button for open section tasks in the right side ([8c37636](https://github.com/ahonn/logseq-plugin-todo/commit/8c37636a6681ed2fcb45b5d809cb28018600d921))
110 |
111 | # [1.15.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.14.1...v1.15.0) (2023-02-26)
112 |
113 |
114 | ### Bug Fixes
115 |
116 | * fix type error ([705af59](https://github.com/ahonn/logseq-plugin-todo/commit/705af594f9133e6f1543beedaa1f827759b38cf3))
117 |
118 |
119 | ### Features
120 |
121 | * add defaultMarker and defaultPriority settings ([137dbc7](https://github.com/ahonn/logseq-plugin-todo/commit/137dbc7a848f5def1165b040ed7ec87ece31c474))
122 | * add task filter component ([eb8c41b](https://github.com/ahonn/logseq-plugin-todo/commit/eb8c41bc0c286b7958bbb168ec3cd3ea1456102f))
123 | * add the ability to filter tasks by marker or priority level. ([412415f](https://github.com/ahonn/logseq-plugin-todo/commit/412415f10a7c8e003781b9f42f8737a9d253317c))
124 |
125 | ## [1.14.1](https://github.com/ahonn/logseq-plugin-todo/compare/v1.14.0...v1.14.1) (2023-01-17)
126 |
127 |
128 | ### Bug Fixes
129 |
130 | * label should not let task be removed from the list, [#37](https://github.com/ahonn/logseq-plugin-todo/issues/37) ([a17cad7](https://github.com/ahonn/logseq-plugin-todo/commit/a17cad761c155555a781794d5bdb5137490e2450))
131 |
132 | # [1.14.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.13.0...v1.14.0) (2023-01-10)
133 |
134 |
135 | ### Features
136 |
137 | * exit editing mode when create task where today journal page ([ae96a9f](https://github.com/ahonn/logseq-plugin-todo/commit/ae96a9f302417862eb2b99a3a1f9b65acc90a2b5))
138 |
139 | # [1.13.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.12.1...v1.13.0) (2023-01-08)
140 |
141 |
142 | ### Features
143 |
144 | * add next n days task section and add setting item for enable/disable it ([ead0825](https://github.com/ahonn/logseq-plugin-todo/commit/ead0825bfcecffb060e26499a2d5872a8f72086f))
145 | * sort unscheduled tasks with page's updatedAt property. ([12f1461](https://github.com/ahonn/logseq-plugin-todo/commit/12f1461ba46b25c2066175474c9b832f7c464d52))
146 |
147 | ## [1.12.1](https://github.com/ahonn/logseq-plugin-todo/compare/v1.12.0...v1.12.1) (2022-11-14)
148 |
149 |
150 | ### Bug Fixes
151 |
152 | * fixed first load not follow current theme mode, [#31](https://github.com/ahonn/logseq-plugin-todo/issues/31) ([2a8b515](https://github.com/ahonn/logseq-plugin-todo/commit/2a8b5155eacdbf1b06cdaf2cbc6b3b268f690a66))
153 |
154 | # [1.12.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.11.1...v1.12.0) (2022-11-14)
155 |
156 |
157 | ### Bug Fixes
158 |
159 | * fix not refresh when set task schedule ([9d46240](https://github.com/ahonn/logseq-plugin-todo/commit/9d462408927a2fca00c1a75549379a3a494a391f))
160 |
161 |
162 | ### Features
163 |
164 | * let the input box can be search tasks by title ([1a4ba2d](https://github.com/ahonn/logseq-plugin-todo/commit/1a4ba2dfeab93a49006d423dc8f279d6734a21ec))
165 |
166 | ## [1.11.1](https://github.com/ahonn/logseq-plugin-todo/compare/v1.11.0...v1.11.1) (2022-10-19)
167 |
168 |
169 | ### Bug Fixes
170 |
171 | * avoid duplicate binding hotkey ([9f9ab25](https://github.com/ahonn/logseq-plugin-todo/commit/9f9ab25ab651899897b53e065e970c5156e6b422))
172 |
173 | # [1.11.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.10.0...v1.11.0) (2022-10-19)
174 |
175 |
176 | ### Features
177 |
178 | * add whereToPlaceNewTask setting item, [#24](https://github.com/ahonn/logseq-plugin-todo/issues/24) ([a42ed45](https://github.com/ahonn/logseq-plugin-todo/commit/a42ed456a7bee33042f4dcf07e0387e56b7f6948))
179 |
180 | # [1.10.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.9.1...v1.10.0) (2022-10-14)
181 |
182 |
183 | ### Features
184 |
185 | * add quick open hotkey and toggle panel with same key ([56404cd](https://github.com/ahonn/logseq-plugin-todo/commit/56404cda5b1aa0b186eac1547ec88a3ce46fb5e0))
186 | * suport WAITING task status, [#17](https://github.com/ahonn/logseq-plugin-todo/issues/17) ([f0869b3](https://github.com/ahonn/logseq-plugin-todo/commit/f0869b3a2d878242db5af52efdc7fe045ddfea4f))
187 |
188 | ## [1.9.1](https://github.com/ahonn/logseq-plugin-todo/compare/v1.9.0...v1.9.1) (2022-10-14)
189 |
190 |
191 | ### Bug Fixes
192 |
193 | * fixed cannot read map of null errors ([8866d0b](https://github.com/ahonn/logseq-plugin-todo/commit/8866d0b284bf1da2194913e3237efe1331ffe1cb))
194 |
195 | # [1.9.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.8.0...v1.9.0) (2022-10-13)
196 |
197 |
198 | ### Bug Fixes
199 |
200 | * fix not showing the page title when click, [#25](https://github.com/ahonn/logseq-plugin-todo/issues/25) ([c37f1c0](https://github.com/ahonn/logseq-plugin-todo/commit/c37f1c0e98630c0963e182493f150f5d8b08af00))
201 |
202 |
203 | ### Features
204 |
205 | * support task section title clickable & add openInRightSidebar setting ([e995860](https://github.com/ahonn/logseq-plugin-todo/commit/e99586090c98bf030ffbe7659b9f4d984c8477e5))
206 |
207 | # [1.8.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.7.0...v1.8.0) (2022-09-19)
208 |
209 |
210 | ### Bug Fixes
211 |
212 | * fixed reading map property error, reslove [#23](https://github.com/ahonn/logseq-plugin-todo/issues/23) ([1278fc7](https://github.com/ahonn/logseq-plugin-todo/commit/1278fc7753c0471952ce33d72599f62e49cf99c2))
213 |
214 |
215 | ### Features
216 |
217 | * support anytime tasks group by page name, [#21](https://github.com/ahonn/logseq-plugin-todo/issues/21) ([8e42dc2](https://github.com/ahonn/logseq-plugin-todo/commit/8e42dc2aed413ee71b5e52833b3a3c34cef082f3))
218 |
219 | # [1.7.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.6.0...v1.7.0) (2022-09-13)
220 |
221 |
222 | ### Bug Fixes
223 |
224 | * fixed theme mode not work ([db4b7e2](https://github.com/ahonn/logseq-plugin-todo/commit/db4b7e2177090d5ae9dc3565e4e37381288f867c))
225 |
226 |
227 | ### Features
228 |
229 | * add scheduled/anytime tasks back ([4a41a7e](https://github.com/ahonn/logseq-plugin-todo/commit/4a41a7ea138742aa7ac6bd0a628638a83de7f66b))
230 | * create task api and remove useTaskManager ([48fa9e2](https://github.com/ahonn/logseq-plugin-todo/commit/48fa9e2cf7168103301d7a1c0cfa4d05c15544ac))
231 | * use useRecoilValueLoadable to load tasks ([b158a5b](https://github.com/ahonn/logseq-plugin-todo/commit/b158a5bf98dd8f10a3508be3a8634b258eb6953b))
232 |
233 | # [1.6.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.5.0...v1.6.0) (2022-08-06)
234 |
235 |
236 | ### Features
237 |
238 | * add plugin settings to customize theme colors, [#18](https://github.com/ahonn/logseq-plugin-todo/issues/18) ([3f0d862](https://github.com/ahonn/logseq-plugin-todo/commit/3f0d86292305ebf543c5a149b378f15c49b414b1))
239 |
240 | # [1.5.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.4.0...v1.5.0) (2022-06-06)
241 |
242 |
243 | ### Features
244 |
245 | * support quick open task panel ([c648e2f](https://github.com/ahonn/logseq-plugin-todo/commit/c648e2fc1329edbf81e87a66f808b5b96c5522af))
246 |
247 | # [1.4.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.3.1...v1.4.0) (2022-06-06)
248 |
249 |
250 | ### Features
251 |
252 | * show task marker and toggle when click marker label, [#11](https://github.com/ahonn/logseq-plugin-todo/issues/11) ([34e4a86](https://github.com/ahonn/logseq-plugin-todo/commit/34e4a868a9938e7398f74ff5a0f1b8574392b860))
253 |
254 | ## [1.3.1](https://github.com/ahonn/logseq-plugin-todo/compare/v1.3.0...v1.3.1) (2022-05-06)
255 |
256 |
257 | ### Bug Fixes
258 |
259 | * clean content id property when task has ref id ([61b3afd](https://github.com/ahonn/logseq-plugin-todo/commit/61b3afd1b15f0262719baf3ceec0b7977e0e5833))
260 | * fix cursor pointer styling ([1eefd41](https://github.com/ahonn/logseq-plugin-todo/commit/1eefd413fa745d3eb4ced25aace34465ec5b45a1))
261 |
262 | # [1.3.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.2.1...v1.3.0) (2022-05-04)
263 |
264 |
265 | ### Bug Fixes
266 |
267 | * fix none priority task error ([48a32cc](https://github.com/ahonn/logseq-plugin-todo/commit/48a32ccc12c26fc10d70c22eaf0cf974a59435a7))
268 |
269 |
270 | ### Features
271 |
272 | * show priority color to make task more visually appealing ([573cb7d](https://github.com/ahonn/logseq-plugin-todo/commit/573cb7d227ead7e92e18bb09578cca5832ae7289))
273 | * sort by priority when same scheduled task ([98d4398](https://github.com/ahonn/logseq-plugin-todo/commit/98d439805b53cc0ac7336e10687e11826b1bec39))
274 |
275 | ## [1.2.1](https://github.com/ahonn/logseq-plugin-todo/compare/v1.2.0...v1.2.1) (2022-04-30)
276 |
277 |
278 | ### Bug Fixes
279 |
280 | * fix missing logo.svg after build dist ([6d228dc](https://github.com/ahonn/logseq-plugin-todo/commit/6d228dc9238acfef1f0e66f16e63ff04a0049816))
281 |
282 | # [1.2.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.1.4...v1.2.0) (2022-04-27)
283 |
284 |
285 | ### Features
286 |
287 | * add button to schedule/cancel to-do items to today ([5909096](https://github.com/ahonn/logseq-plugin-todo/commit/590909691172622750e57c8bb39a39cb9aac7c7d))
288 |
289 | ## [1.1.4](https://github.com/ahonn/logseq-plugin-todo/compare/v1.1.3...v1.1.4) (2022-04-27)
290 |
291 |
292 | ### Bug Fixes
293 |
294 | * fix anytime task query ([294b77a](https://github.com/ahonn/logseq-plugin-todo/commit/294b77a6ae2370713d7a8c56abfee1b844c87ff5))
295 |
296 | ## [1.1.3](https://github.com/ahonn/logseq-plugin-todo/compare/v1.1.2...v1.1.3) (2022-04-27)
297 |
298 |
299 | ### Bug Fixes
300 |
301 | * fix task panel position ([ff2ff95](https://github.com/ahonn/logseq-plugin-todo/commit/ff2ff95d072b7a529800d09eaa3a7887c3f7e088))
302 |
303 | ## [1.1.2](https://github.com/ahonn/logseq-plugin-todo/compare/v1.1.1...v1.1.2) (2022-04-26)
304 |
305 |
306 | ### Bug Fixes
307 |
308 | * add error bounday and show error message when throw error ([7f59e7f](https://github.com/ahonn/logseq-plugin-todo/commit/7f59e7f816ec05687ee94ac002d680113a99aff5))
309 |
310 | ## [1.1.1](https://github.com/ahonn/logseq-plugin-todo/compare/v1.1.0...v1.1.1) (2022-04-26)
311 |
312 |
313 | ### Bug Fixes
314 |
315 | * fix panel position style and auto focus task input when plugin visible ([f79562b](https://github.com/ahonn/logseq-plugin-todo/commit/f79562bddda0535a4bc0dcb453c7c295b984ccfc))
316 | * fix panel position when right sidebar opend ([eaa8a4e](https://github.com/ahonn/logseq-plugin-todo/commit/eaa8a4e2245768571003d8c662c72f1ebeb92fce))
317 |
318 | # [1.1.0](https://github.com/ahonn/logseq-plugin-todo/compare/v1.0.0...v1.1.0) (2022-04-26)
319 |
320 |
321 | ### Features
322 |
323 | * add line-clamp style ([2968e7d](https://github.com/ahonn/logseq-plugin-todo/commit/2968e7daa2c71fc846f714d419dbcf5a929b82ee))
324 | * add TaskEntity ([f235703](https://github.com/ahonn/logseq-plugin-todo/commit/f23570302cc43749f10ec06e2ea35c3a419d9ae5))
325 |
326 | # 1.0.0 (2022-04-26)
327 |
328 |
329 | ### Bug Fixes
330 |
331 | * update task list after add new task ([5137884](https://github.com/ahonn/logseq-plugin-todo/commit/5137884c3b6ef8a790d9517e3b986734d7f22139))
332 |
333 |
334 | ### Features
335 |
336 | * add basic task list ([fbc070f](https://github.com/ahonn/logseq-plugin-todo/commit/fbc070fcc72c7f4f1093feb67c9432d902841797))
337 | * add expired/scheduled/no scheduled task section ([87526ef](https://github.com/ahonn/logseq-plugin-todo/commit/87526ef469ffb27741730992bc5302e87f372c23))
338 | * add useTask hook and adjust task sections ([70d280c](https://github.com/ahonn/logseq-plugin-todo/commit/70d280c0ac78abcc4e74d87a5efaac87c47bd33f))
339 | * add withUserConfigs ([c451693](https://github.com/ahonn/logseq-plugin-todo/commit/c4516939eb91c1f6f5c3e2c0ddd1974af8cbc5b1))
340 | * modify task section title font weight ([d6a81e6](https://github.com/ahonn/logseq-plugin-todo/commit/d6a81e67471ec17a0952e6f16896d058a55ee901))
341 | * show task deadline as scheduled date ([6f04571](https://github.com/ahonn/logseq-plugin-todo/commit/6f045717195f7e08947683a110ecd36f296feaa8))
342 | * show task scheduled time and open task in right sidebar ([b48e08e](https://github.com/ahonn/logseq-plugin-todo/commit/b48e08ec8da0fd92e4810ef2b18091659437f395))
343 |
--------------------------------------------------------------------------------