From 8ef2d06c48db20308e56c9233c22dd7a16c1a9ce Mon Sep 17 00:00:00 2001 From: Danko Aleksejevs Date: Wed, 4 Jan 2023 22:32:42 +0200 Subject: [PATCH] Optionally show notebook tag and title on each card --- src/board.ts | 1 + src/gui/Card.tsx | 33 +++++++++++++-- src/gui/index.tsx | 97 +++++++++++++++++++++++++-------------------- src/noteData.ts | 36 +++++++++++------ src/rules.ts | 4 ++ src/types.ts | 15 ++++++- tests/board.test.ts | 7 ++++ 7 files changed, 134 insertions(+), 59 deletions(-) diff --git a/src/board.ts b/src/board.ts index 8d17498..8c1cf3b 100644 --- a/src/board.ts +++ b/src/board.ts @@ -217,6 +217,7 @@ export default class Board { name: this.boardName, messages: [], hiddenTags: [], + displayConfig: this.parsedConfig?.display ?? {}, }; if (this.isValid) { diff --git a/src/gui/Card.tsx b/src/gui/Card.tsx index fa0a6b7..ea5fd79 100644 --- a/src/gui/Card.tsx +++ b/src/gui/Card.tsx @@ -1,16 +1,20 @@ -import React from "react"; +import React, { useContext } from "react"; import styled from "styled-components"; -import { IoMdPricetag } from "react-icons/io"; +import { IoMdFolder, IoMdPricetag } from "react-icons/io"; import { IoCalendarOutline } from "react-icons/io5"; import moment from "moment"; import type { NoteData } from "../types"; +import { BoardContext } from "./index"; const dateFmt = document.getElementById('date-fmt')?.innerHTML || "" export default React.forwardRef( ({ note }, ref) => { - const { title, tags, due } = note; + + const board = useContext(BoardContext); + + const { title, tags, due, notebookData: notebook } = note; const renderExtra = ( key: number, @@ -23,10 +27,28 @@ export default React.forwardRef( {text} ); + const extras: [React.ReactNode, string, string?][] = tags.map((tag) => [ , tag, ]); + + if (board.displayConfig.showNotebookTag) { + const { icon } = notebook; + extras.unshift([ + icon ? ( + icon.dataUrl ? ( + + ) : ( + {icon.emoji} + ) + ) : ( + + ), + notebook.title, + ]); + } + if (due > 0) { const dueDate = new Date(due); const dateStr = moment(dueDate).format(dateFmt); @@ -83,3 +105,8 @@ const ExtraItem = styled.div<{ color: string }>(({ color }) => ({ const IconCont = styled.span({ marginRight: "4px", }); + +const DataIcon = styled.img({ + maxHeight: "1rem", + maxWidth: "1rem", +}); diff --git a/src/gui/index.tsx b/src/gui/index.tsx index bea85eb..e7d9848 100644 --- a/src/gui/index.tsx +++ b/src/gui/index.tsx @@ -1,16 +1,17 @@ import React from "react"; import { render } from "react-dom"; import styled from "styled-components"; -import { IoMdSettings, IoMdAdd } from "react-icons/io"; +import { IoMdSettings, IoMdAdd, IoMdEye } from "react-icons/io"; import { capitalize } from "../utils"; import { DispatchFn, useRemoteBoard } from "./hooks"; import { DragDropContext } from "./DragDrop"; import Column from "./Column"; -import type { Message } from "../types"; +import type { BoardState, Message } from "../types"; export const DispatchContext = React.createContext(async () => {}); export const IsWaitingContext = React.createContext(false); +export const BoardContext = React.createContext({} as BoardState); function MessageBox({ message, @@ -57,49 +58,59 @@ function App() { })); const cont = board ? ( - -
- {board.name} - - dispatch({ type: "settings", payload: { target: "filters" } }) - } - > - - - - - dispatch({ type: "settings", payload: { target: "columnnew" } }) - } - > - - -
- - - {board.messages.map((msg, idx) => ( - - dispatch({ - type: "messageAction", - payload: { actionName: action, messageId: msg.id }, - }) + + +
+ {board.name} + + dispatch({ type: "settings", payload: { target: "filters" } }) } - /> - ))} - - - {board.columns && ( - - {notesToShow?.map(({ name, notes }) => ( - + > + + + + + dispatch({ type: "settings", payload: { target: "display" } }) + } + > + + + + + dispatch({ type: "settings", payload: { target: "columnnew" } }) + } + > + + +
+ + + {board.messages.map((msg, idx) => ( + + dispatch({ + type: "messageAction", + payload: { actionName: action, messageId: msg.id }, + }) + } + /> ))} - - )} -
+
+ + {board.columns && ( + + {notesToShow?.map(({ name, notes }) => ( + + ))} + + )} +
+ ) : (

Loading...

); diff --git a/src/noteData.ts b/src/noteData.ts index 30dd18d..21439a1 100644 --- a/src/noteData.ts +++ b/src/noteData.ts @@ -1,6 +1,6 @@ import joplin from "api"; import { getUpdatedConfigNote } from "./parser"; -import { NoteData, ConfigNote, UpdateQuery } from "./types"; +import { ConfigNote, Folder, NoteData, UpdateQuery } from "./types"; /** * Execute the given search query and return all matching notes in the kanban format. @@ -35,6 +35,11 @@ async function search(query: string): Promise { has_more: boolean; }; + const allNotebooks = await getAllNotebooks(); + const notebookIndex = Object.fromEntries( + allNotebooks.map((folder) => [folder.id, folder]) + ); + let allNotes: any[] = []; let page = 1; while (true) { @@ -71,6 +76,7 @@ async function search(query: string): Promise { isTodo: !!note.is_todo, isCompleted: !!note.todo_completed, notebookId: note.parent_id, + notebookData: notebookIndex[note.parent_id], due: note.todo_due, order: note.order === 0 ? note.created_time : note.order, createdTime: note.created_time, @@ -272,28 +278,34 @@ export async function findAllChildrenNotebook( return children; } -type Folder = { - id: string; - title: string; - parent_id: string; -}; - /** * Get a list of all notebooks, with id, title, and parent_id. */ export async function getAllNotebooks(): Promise { + const fields = ["id", "title", "parent_id", "icon"]; + let folders: Folder[] = []; let page = 1; + while (true) { console.log("data call get", ["folders"], { page }); const { - items: newFolders, + items, has_more: hasMore, - }: { items: Folder[]; has_more: boolean } = await joplin.data.get( - ["folders"], - { page } - ); + }: { items: (Folder & { icon: string })[]; has_more: boolean } = + await joplin.data.get(["folders"], { page, fields }); + const newFolders = items.map((folder) => { + let icon = folder.icon || null; + if (icon) + try { + // Is it always JSON? I'm not sure. + icon = JSON.parse(icon); + } catch (e) { + console.error(e); + } + return { ...folder, icon }; + }); folders = [...folders, ...newFolders]; if (!hasMore) break; diff --git a/src/rules.ts b/src/rules.ts index 48ac1c5..2d880db 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -151,10 +151,14 @@ const editorTypes = { completed: "checkbox", backlog: "checkbox", }, + display: { + showNotebookTag: "checkbox", + }, }; export const getRuleEditorTypes = (targetPath: string) => { if (targetPath === "filters") return editorTypes.filters; + if (targetPath === "display") return editorTypes.display; if (targetPath.startsWith("column")) return editorTypes.columns; throw new Error(`Unkown target path ${targetPath}`); }; diff --git a/src/types.ts b/src/types.ts index 0fab0f7..10bc621 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,7 +31,8 @@ export interface Config { backlog?: boolean; }[]; display: { - markdown: string; + markdown?: string; + showNotebookTag?: boolean; }; } @@ -79,6 +80,7 @@ export interface BoardState { }[]; hiddenTags: string[]; messages: Message[]; + displayConfig: Config["display"]; } // Joplin API related types @@ -96,11 +98,22 @@ export interface ConfigNote { body: string; } +export interface Folder { + id: string; + title: string; + parent_id: string; + icon: null | { + emoji: string; + dataUrl?: string; + }; +} + export interface NoteData { id: string; title: string; tags: string[]; notebookId: string; + notebookData: Folder; isTodo: boolean; isCompleted: boolean; due: number; diff --git a/tests/board.test.ts b/tests/board.test.ts index 9d9d48b..1b3244b 100644 --- a/tests/board.test.ts +++ b/tests/board.test.ts @@ -66,6 +66,12 @@ const note = (args: Partial = {}): NoteData => ({ id: "id", title: "title", notebookId: parentNb, + notebookData: { + id: parentNb, + title: "notebook", + icon: { emoji: "☢️" }, + parent_id: "" + }, tags: ["task"], createdTime: mockTime, due: 0, @@ -90,6 +96,7 @@ const state = ( { name: "Working", notes: working }, { name: "Done", notes: done }, ], + displayConfig: {}, }); describe("Board", () => {