Browse Source

Merge branch 'master' into enhance/apis-improvements

charlie 2 năm trước cách đây
mục cha
commit
b3c8eb2c8f
91 tập tin đã thay đổi với 1794 bổ sung1102 xóa
  1. 2 0
      .carve/ignore
  2. 1 0
      .clj-kondo/config.edn
  3. 1 1
      deps/graph-parser/src/logseq/graph_parser/property.cljs
  4. 1 1
      e2e-tests/editor.spec.ts
  5. 8 8
      e2e-tests/whiteboards.spec.ts
  6. 2 2
      package.json
  7. 19 1
      src/main/frontend/commands.cljs
  8. 67 45
      src/main/frontend/components/block.cljs
  9. 37 10
      src/main/frontend/components/block.css
  10. 8 1
      src/main/frontend/components/container.css
  11. 21 7
      src/main/frontend/components/content.cljs
  12. 2 2
      src/main/frontend/components/datetime.cljs
  13. 12 10
      src/main/frontend/components/header.cljs
  14. 5 0
      src/main/frontend/components/header.css
  15. 25 16
      src/main/frontend/components/page.cljs
  16. 1 1
      src/main/frontend/components/page.css
  17. 12 10
      src/main/frontend/components/page_menu.cljs
  18. 10 9
      src/main/frontend/components/plugins.cljs
  19. 3 2
      src/main/frontend/components/query.cljs
  20. 3 3
      src/main/frontend/components/query_table.cljs
  21. 1 0
      src/main/frontend/components/shortcut.cljs
  22. 3 1
      src/main/frontend/components/whiteboard.cljs
  23. 2 9
      src/main/frontend/db/model.cljs
  24. 1 1
      src/main/frontend/db/react.cljs
  25. 117 6
      src/main/frontend/dicts.cljc
  26. 2 1
      src/main/frontend/extensions/pdf/assets.cljs
  27. 2 1
      src/main/frontend/extensions/srs.cljs
  28. 8 1
      src/main/frontend/extensions/tldraw.cljs
  29. 2 2
      src/main/frontend/fs/watcher_handler.cljs
  30. 37 8
      src/main/frontend/handler/block.cljs
  31. 2 1
      src/main/frontend/handler/dnd.cljs
  32. 132 173
      src/main/frontend/handler/editor.cljs
  33. 141 0
      src/main/frontend/handler/editor/property.cljs
  34. 22 3
      src/main/frontend/handler/events.cljs
  35. 3 1
      src/main/frontend/handler/paste.cljs
  36. 73 53
      src/main/frontend/modules/editor/undo_redo.cljs
  37. 59 34
      src/main/frontend/modules/outliner/core.cljs
  38. 9 4
      src/main/frontend/modules/outliner/datascript.cljc
  39. 33 9
      src/main/frontend/modules/outliner/pipeline.cljs
  40. 3 2
      src/main/frontend/modules/outliner/transaction.cljc
  41. 135 6
      src/main/frontend/modules/shortcut/config.cljs
  42. 93 1
      src/main/frontend/modules/shortcut/dicts.cljc
  43. 1 0
      src/main/frontend/schema/handler/common_config.cljc
  44. 3 1
      src/main/frontend/state.cljs
  45. 1 0
      src/main/frontend/ui.cljs
  46. 5 0
      src/main/frontend/util.cljc
  47. 1 1
      src/main/frontend/util/thingatpt.cljs
  48. 25 1
      src/main/frontend/utils.js
  49. 12 10
      src/main/logseq/api.cljs
  50. 16 0
      src/test/frontend/db/model_test.cljs
  51. 58 1
      src/test/frontend/modules/outliner/core_test.cljs
  52. 54 0
      src/test/frontend/modules/outliner/pipeline_test.cljs
  53. 227 164
      templates/config.edn
  54. 6 0
      tldraw/apps/tldraw-logseq/build.mjs
  55. 2 2
      tldraw/apps/tldraw-logseq/package.json
  56. 16 15
      tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx
  57. 15 59
      tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx
  58. 16 0
      tldraw/apps/tldraw-logseq/src/components/KeyboardShortcut/KeyboardShortcut.tsx
  59. 1 0
      tldraw/apps/tldraw-logseq/src/components/KeyboardShortcut/index.ts
  60. 52 19
      tldraw/apps/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx
  61. 30 30
      tldraw/apps/tldraw-logseq/src/components/ToolButton/ToolButton.tsx
  62. 7 25
      tldraw/apps/tldraw-logseq/src/components/ZoomMenu/ZoomMenu.tsx
  63. 1 0
      tldraw/apps/tldraw-logseq/src/components/inputs/SelectInput.tsx
  64. 1 1
      tldraw/apps/tldraw-logseq/src/components/inputs/ToggleInput.tsx
  65. 3 0
      tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts
  66. 3 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/IFrameShape.tsx
  67. 0 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx
  68. 1 1
      tldraw/apps/tldraw-logseq/src/lib/shapes/TweetShape.tsx
  69. 4 2
      tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx
  70. 1 1
      tldraw/apps/tldraw-logseq/src/lib/tools/BoxTool.tsx
  71. 1 1
      tldraw/apps/tldraw-logseq/src/lib/tools/EllipseTool.tsx
  72. 1 1
      tldraw/apps/tldraw-logseq/src/lib/tools/EraseTool.tsx
  73. 1 1
      tldraw/apps/tldraw-logseq/src/lib/tools/HighlighterTool.tsx
  74. 1 1
      tldraw/apps/tldraw-logseq/src/lib/tools/LineTool.tsx
  75. 1 1
      tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/LogseqPortalTool.tsx
  76. 1 1
      tldraw/apps/tldraw-logseq/src/lib/tools/PencilTool.tsx
  77. 1 1
      tldraw/apps/tldraw-logseq/src/lib/tools/TextTool.tsx
  78. 1 1
      tldraw/apps/tldraw-logseq/src/styles.css
  79. 2 2
      tldraw/package.json
  80. 4 13
      tldraw/packages/core/src/lib/TLApi/TLApi.ts
  81. 5 170
      tldraw/packages/core/src/lib/TLApp/TLApp.ts
  82. 0 21
      tldraw/packages/core/src/lib/TLState.ts
  83. 1 1
      tldraw/packages/core/src/lib/tools/TLMoveTool/TLMoveTool.ts
  84. 1 1
      tldraw/packages/core/src/lib/tools/TLSelectTool/TLSelectTool.tsx
  85. 3 13
      tldraw/packages/core/src/types/types.ts
  86. 0 32
      tldraw/packages/core/src/utils/KeyUtils.ts
  87. 7 7
      tldraw/packages/core/src/utils/index.ts
  88. 0 1
      tldraw/packages/react/src/hooks/useAppSetup.ts
  89. 1 5
      tldraw/packages/react/src/hooks/useSetup.ts
  90. 0 2
      tldraw/packages/react/src/types/TLReactSubscriptions.tsx
  91. 80 43
      tldraw/yarn.lock

+ 2 - 0
.carve/ignore

@@ -82,3 +82,5 @@ logseq.graph-parser.nbb-test-runner/run-tests
 ;; For debugging
 ;; For debugging
 frontend.fs.sync/debug-print-sync-events-loop
 frontend.fs.sync/debug-print-sync-events-loop
 frontend.fs.sync/stop-debug-print-sync-events-loop
 frontend.fs.sync/stop-debug-print-sync-events-loop
+;; Used in macro
+frontend.state/get-current-edit-block-and-position

+ 1 - 0
.clj-kondo/config.edn

@@ -64,6 +64,7 @@
              frontend.handler.common.plugin plugin-common-handler
              frontend.handler.common.plugin plugin-common-handler
              frontend.handler.common.developer dev-common-handler
              frontend.handler.common.developer dev-common-handler
              frontend.handler.config config-handler
              frontend.handler.config config-handler
+             frontend.handler.editor.property editor-property
              frontend.handler.events events
              frontend.handler.events events
              frontend.handler.global-config global-config-handler
              frontend.handler.global-config global-config-handler
              frontend.handler.ui ui-handler
              frontend.handler.ui ui-handler

+ 1 - 1
deps/graph-parser/src/logseq/graph_parser/property.cljs

@@ -64,7 +64,7 @@
      :created-at :updated-at :last-modified-at :created_at :last_modified_at
      :created-at :updated-at :last-modified-at :created_at :last_modified_at
      :query-table :query-properties :query-sort-by :query-sort-desc :ls-type
      :query-table :query-properties :query-sort-by :query-sort-desc :ls-type
      :hl-type :hl-page :hl-stamp :hl-color :logseq.macro-name :logseq.macro-arguments
      :hl-type :hl-page :hl-stamp :hl-color :logseq.macro-name :logseq.macro-arguments
-     :logseq.tldraw.page :logseq.tldraw.shape
+     :logseq.order-list-type :logseq.tldraw.page :logseq.tldraw.shape
      ; task markers
      ; task markers
      :todo :doing :now :later :done}
      :todo :doing :now :later :done}
    @built-in-extended-properties))
    @built-in-extended-properties))

+ 1 - 1
e2e-tests/editor.spec.ts

@@ -260,7 +260,7 @@ test('undo and redo after starting an action should not destroy text #6267', asy
 
 
   // And it should keep what was undone as a redo action
   // And it should keep what was undone as a redo action
   await page.keyboard.press(modKey + '+Shift+z')
   await page.keyboard.press(modKey + '+Shift+z')
-  await expect(page.locator('text="text2"')).toHaveCount(1)
+  await expect(page.locator('text="text1 text2 [[]]"')).toHaveCount(1)
 })
 })
 
 
 test('undo after starting an action should close the action menu #6269', async ({ page, block }) => {
 test('undo after starting an action should close the action menu #6269', async ({ page, block }) => {

+ 8 - 8
e2e-tests/whiteboards.spec.ts

@@ -83,7 +83,7 @@ test('draw a rectangle', async ({ page }) => {
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const bounds = (await canvas.boundingBox())!
   const bounds = (await canvas.boundingBox())!
 
 
-  await page.keyboard.press('r')
+  await page.keyboard.type('wr')
 
 
   await page.mouse.move(bounds.x + 5, bounds.y + 5)
   await page.mouse.move(bounds.x + 5, bounds.y + 5)
   await page.mouse.down()
   await page.mouse.down()
@@ -130,7 +130,7 @@ test('connect rectangles with an arrow', async ({ page }) => {
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const bounds = (await canvas.boundingBox())!
   const bounds = (await canvas.boundingBox())!
 
 
-  await page.keyboard.press('c')
+  await page.keyboard.type('wc')
 
 
   await page.mouse.move(bounds.x + 20, bounds.y + 20)
   await page.mouse.move(bounds.x + 20, bounds.y + 20)
   await page.mouse.down()
   await page.mouse.down()
@@ -205,7 +205,7 @@ test('create a block', async ({ page }) => {
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const bounds = (await canvas.boundingBox())!
   const bounds = (await canvas.boundingBox())!
 
 
-  await page.keyboard.press('s')
+  await page.keyboard.type('ws')
   await page.mouse.dblclick(bounds.x + 5, bounds.y + 5)
   await page.mouse.dblclick(bounds.x + 5, bounds.y + 5)
   await page.waitForTimeout(100)
   await page.waitForTimeout(100)
 
 
@@ -240,7 +240,7 @@ test('copy/paste url to create an iFrame shape', async ({ page }) => {
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const bounds = (await canvas.boundingBox())!
   const bounds = (await canvas.boundingBox())!
 
 
-  await page.keyboard.press('t')
+  await page.keyboard.type('wt')
   await page.mouse.move(bounds.x + 5, bounds.y + 5)
   await page.mouse.move(bounds.x + 5, bounds.y + 5)
   await page.mouse.down()
   await page.mouse.down()
   await page.waitForTimeout(100)
   await page.waitForTimeout(100)
@@ -259,7 +259,7 @@ test('copy/paste twitter status url to create a Tweet shape', async ({ page }) =
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const bounds = (await canvas.boundingBox())!
   const bounds = (await canvas.boundingBox())!
 
 
-  await page.keyboard.press('t')
+  await page.keyboard.type('wt')
   await page.mouse.move(bounds.x + 5, bounds.y + 5)
   await page.mouse.move(bounds.x + 5, bounds.y + 5)
   await page.mouse.down()
   await page.mouse.down()
   await page.waitForTimeout(100)
   await page.waitForTimeout(100)
@@ -278,7 +278,7 @@ test('copy/paste youtube video url to create a Youtube shape', async ({ page })
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const bounds = (await canvas.boundingBox())!
   const bounds = (await canvas.boundingBox())!
 
 
-  await page.keyboard.press('t')
+  await page.keyboard.type('wt')
   await page.mouse.move(bounds.x + 5, bounds.y + 5)
   await page.mouse.move(bounds.x + 5, bounds.y + 5)
   await page.mouse.down()
   await page.mouse.down()
   await page.waitForTimeout(100)
   await page.waitForTimeout(100)
@@ -296,7 +296,7 @@ test('copy/paste youtube video url to create a Youtube shape', async ({ page })
 test('zoom in', async ({ page }) => {
 test('zoom in', async ({ page }) => {
   await page.keyboard.press('Shift+0') // reset zoom
   await page.keyboard.press('Shift+0') // reset zoom
   await page.waitForTimeout(1500) // wait for the zoom animation to finish
   await page.waitForTimeout(1500) // wait for the zoom animation to finish
-  await page.keyboard.press(`${modKey}++`)
+  await page.keyboard.press('Shift+=')
   await page.waitForTimeout(1500) // wait for the zoom animation to finish
   await page.waitForTimeout(1500) // wait for the zoom animation to finish
   await expect(page.locator('#tl-zoom')).toContainText('125%')
   await expect(page.locator('#tl-zoom')).toContainText('125%')
 })
 })
@@ -304,7 +304,7 @@ test('zoom in', async ({ page }) => {
 test('zoom out', async ({ page }) => {
 test('zoom out', async ({ page }) => {
   await page.keyboard.press('Shift+0')
   await page.keyboard.press('Shift+0')
   await page.waitForTimeout(1500) // wait for the zoom animation to finish
   await page.waitForTimeout(1500) // wait for the zoom animation to finish
-  await page.keyboard.press(`${modKey}+-`)
+  await page.keyboard.press('Shift+-')
   await page.waitForTimeout(1500) // wait for the zoom animation to finish
   await page.waitForTimeout(1500) // wait for the zoom animation to finish
   await expect(page.locator('#tl-zoom')).toContainText('80%')
   await expect(page.locator('#tl-zoom')).toContainText('80%')
 })
 })

+ 2 - 2
package.json

@@ -71,7 +71,7 @@
         "cljs:lint": "clojure -M:clj-kondo --parallel --lint src --cache false",
         "cljs:lint": "clojure -M:clj-kondo --parallel --lint src --cache false",
         "ios:dev": "cross-env PLATFORM=ios gulp cap",
         "ios:dev": "cross-env PLATFORM=ios gulp cap",
         "android:dev": "cross-env PLATFORM=android gulp cap",
         "android:dev": "cross-env PLATFORM=android gulp cap",
-        "tldraw:build": "cd tldraw && yarn install",
+        "tldraw:build": "yarn --cwd tldraw install",
         "postinstall": "yarn tldraw:build"
         "postinstall": "yarn tldraw:build"
     },
     },
     "dependencies": {
     "dependencies": {
@@ -156,4 +156,4 @@
         "pixi-graph-fork/@pixi/mixin-get-child-by-name": "6.2.0",
         "pixi-graph-fork/@pixi/mixin-get-child-by-name": "6.2.0",
         "pixi-graph-fork/@pixi/math": "6.2.0"
         "pixi-graph-fork/@pixi/math": "6.2.0"
     }
     }
-}
+}

+ 19 - 1
src/main/frontend/commands.cljs

@@ -147,6 +147,7 @@
    "Tomorrow" "TIME & DATE"
    "Tomorrow" "TIME & DATE"
    "LATER" "TASK"
    "LATER" "TASK"
    "A" "PRIORITY"
    "A" "PRIORITY"
+   "Number list" "LIST TYPE"
    "Query" "ADVANCED"
    "Query" "ADVANCED"
    "Quote" "ORG-MODE"})
    "Quote" "ORG-MODE"})
 
 
@@ -250,9 +251,14 @@
      ["Current time" #(date/get-current-time) "Insert current time"]
      ["Current time" #(date/get-current-time) "Insert current time"]
      ["Date picker" [[:editor/show-date-picker]] "Pick a date and insert here"]]
      ["Date picker" [[:editor/show-date-picker]] "Pick a date and insert here"]]
 
 
+    ;; order list
+    [["Number list" [[:editor/clear-current-slash]
+                     [:editor/toggle-own-number-list]] "Number list"]
+     ["Number children" [[:editor/clear-current-slash]
+                         [:editor/toggle-children-number-list]] "Number children"]]
+
     ;; task management
     ;; task management
     (get-preferred-workflow)
     (get-preferred-workflow)
-
     [["DONE" (->marker "DONE")]
     [["DONE" (->marker "DONE")]
      ["WAITING" (->marker "WAITING")]
      ["WAITING" (->marker "WAITING")]
      ["CANCELED" (->marker "CANCELED")]
      ["CANCELED" (->marker "CANCELED")]
@@ -654,6 +660,18 @@
         macro (youtube/gen-youtube-ts-macro)]
         macro (youtube/gen-youtube-ts-macro)]
     (insert! input-id macro {})))
     (insert! input-id macro {})))
 
 
+(defmethod handle-step :editor/toggle-children-number-list [[_]]
+  (when-let [block (state/get-edit-block)]
+    (state/pub-event! [:editor/toggle-children-number-list block])))
+
+(defmethod handle-step :editor/toggle-own-number-list [[_]]
+  (when-let [block (state/get-edit-block)]
+    (state/pub-event! [:editor/toggle-own-number-list block])))
+
+(defmethod handle-step :editor/remove-own-number-list [[_]]
+  (when-let [block (state/get-edit-block)]
+    (state/pub-event! [:editor/remove-own-number-list block])))
+
 (defmethod handle-step :editor/show-date-picker [[_ type]]
 (defmethod handle-step :editor/show-date-picker [[_ type]]
   (if (and
   (if (and
        (contains? #{:scheduled :deadline} type)
        (contains? #{:scheduled :deadline} type)

+ 67 - 45
src/main/frontend/components/block.cljs

@@ -927,8 +927,7 @@
         [:span.warning.mr-1 {:title "Block ref invalid"}
         [:span.warning.mr-1 {:title "Block ref invalid"}
          (block-ref/->block-ref id)]))
          (block-ref/->block-ref id)]))
   [:span.warning.mr-1 {:title "Block ref invalid"}
   [:span.warning.mr-1 {:title "Block ref invalid"}
-    (block-ref/->block-ref id)]
-))
+    (block-ref/->block-ref id)]))
 
 
 (defn inline-text
 (defn inline-text
   ([format v]
   ([format v]
@@ -1693,10 +1692,10 @@
 
 
 (rum/defc block-children < rum/reactive
 (rum/defc block-children < rum/reactive
   [config block children collapsed?]
   [config block children collapsed?]
-  (let [ref? (:ref? config)
-        query? (:custom-query? config)
-        children (when (coll? children)
-                   (remove nil? children))]
+  (let [ref?        (:ref? config)
+        query?      (:custom-query? config)
+        children    (when (coll? children)
+                      (remove nil? children))]
     (when (and (coll? children)
     (when (and (coll? children)
                (seq children)
                (seq children)
                (not collapsed?))
                (not collapsed?))
@@ -1730,13 +1729,18 @@
 
 
 (rum/defcs block-control < rum/reactive
 (rum/defcs block-control < rum/reactive
   [state config block uuid block-id collapsed? *control-show? edit?]
   [state config block uuid block-id collapsed? *control-show? edit?]
-  (let [doc-mode? (state/sub :document/mode?)
-        control-show? (util/react *control-show?)
-        ref? (:ref? config)
-        empty-content? (block-content-empty? block)
+  (let [doc-mode?          (state/sub :document/mode?)
+        control-show?      (util/react *control-show?)
+        ref?               (:ref? config)
+        empty-content?     (block-content-empty? block)
         fold-button-right? (state/enable-fold-button-right?)
         fold-button-right? (state/enable-fold-button-right?)
-        collapsable? (editor-handler/collapsable? uuid {:semantic? true})]
+        own-number-list?   (:own-order-number-list? config)
+        order-list?        (boolean own-number-list?)
+        order-list-idx     (:own-order-list-index config)
+        collapsable?       (editor-handler/collapsable? uuid {:semantic? true})]
     [:div.block-control-wrap.mr-1.flex.flex-row.items-center.sm:mr-2
     [:div.block-control-wrap.mr-1.flex.flex-row.items-center.sm:mr-2
+     {:class (util/classnames [{:is-order-list order-list?
+                                :bullet-closed collapsed?}])}
      (when (or (not fold-button-right?) collapsable?)
      (when (or (not fold-button-right?) collapsable?)
        [:a.block-control
        [:a.block-control
         {:id       (str "control-" uuid)
         {:id       (str "control-" uuid)
@@ -1748,13 +1752,15 @@
                        (if collapsed?
                        (if collapsed?
                          (editor-handler/expand-block! uuid)
                          (editor-handler/expand-block! uuid)
                          (editor-handler/collapse-block! uuid))))}
                          (editor-handler/collapse-block! uuid))))}
-        [:span {:class (if (and control-show?
-                                (or collapsed?
-                                    (editor-handler/collapsable? uuid {:semantic? true}))) "control-show cursor-pointer" "control-hide")}
+        [:span {:class (if (or (and control-show?
+                                    (or collapsed?
+                                        (editor-handler/collapsable? uuid {:semantic? true})))
+                               (and collapsed? order-list?))
+                         "control-show cursor-pointer"
+                         "control-hide")}
          (ui/rotating-arrow collapsed?)]])
          (ui/rotating-arrow collapsed?)]])
 
 
-     (let [bullet [:a {:on-click (fn [event]
-                                   (bullet-on-click event block uuid))}
+     (let [bullet [:a.bullet-link-wrap {:on-click #(bullet-on-click % block uuid)}
                    [:span.bullet-container.cursor
                    [:span.bullet-container.cursor
                     {:id (str "dot-" uuid)
                     {:id (str "dot-" uuid)
                      :draggable true
                      :draggable true
@@ -1762,11 +1768,14 @@
                                       (bullet-drag-start event block uuid block-id))
                                       (bullet-drag-start event block uuid block-id))
                      :blockid (str uuid)
                      :blockid (str uuid)
                      :class (str (when collapsed? "bullet-closed")
                      :class (str (when collapsed? "bullet-closed")
-                                 " "
                                  (when (and (:document/mode? config)
                                  (when (and (:document/mode? config)
                                             (not collapsed?))
                                             (not collapsed?))
-                                   "hide-inner-bullet"))}
-                    [:span.bullet {:blockid (str uuid)}]]]]
+                                   " hide-inner-bullet")
+                                 (when order-list? " as-order-list typed-list"))}
+
+                    [:span.bullet {:blockid (str uuid)}
+                     (when order-list?
+                       [:label (str order-list-idx ".")])]]]]
        (cond
        (cond
          (and (or (mobile-util/native-platform?)
          (and (or (mobile-util/native-platform?)
                   (:ui/show-empty-bullets? (state/get-config)))
                   (:ui/show-empty-bullets? (state/get-config)))
@@ -1774,14 +1783,14 @@
          bullet
          bullet
 
 
          (or
          (or
-          (and empty-content?
-               (not edit?)
-               (not (:block/top? block))
-               (not (:block/bottom? block))
-               (not (util/react *control-show?)))
-          (and doc-mode?
-               (not collapsed?)
-               (not (util/react *control-show?))))
+           (and empty-content?
+                (not edit?)
+                (not (:block/top? block))
+                (not (:block/bottom? block))
+                (not (util/react *control-show?)))
+           (and doc-mode?
+                (not collapsed?)
+                (not (util/react *control-show?))))
          ;; hidden
          ;; hidden
          [:span.bullet-container]
          [:span.bullet-container]
 
 
@@ -2768,6 +2777,7 @@
         config (if (nil? (:query-result config))
         config (if (nil? (:query-result config))
                  (assoc config :query-result (atom nil))
                  (assoc config :query-result (atom nil))
                  config)
                  config)
+        config (if ref? (block-handler/attach-order-list-state config block) config)
         heading? (:heading properties)
         heading? (:heading properties)
         *control-show? (get state ::control-show?)
         *control-show? (get state ::control-show?)
         db-collapsed? (util/collapsed? block)
         db-collapsed? (util/collapsed? block)
@@ -2868,6 +2878,12 @@
 
 
      (dnd-separator-wrapper block block-id slide? false false)]))
      (dnd-separator-wrapper block block-id slide? false false)]))
 
 
+(defn- attach-order-list-state!
+  [cp-state]
+  (let [args (:rum/args cp-state)]
+    (assoc cp-state
+      :rum/args (assoc (vec args) 0 (block-handler/attach-order-list-state (first args) (second args))))))
+
 (rum/defcs block-container < rum/reactive
 (rum/defcs block-container < rum/reactive
   (rum/local false ::show-block-left-menu?)
   (rum/local false ::show-block-left-menu?)
   (rum/local false ::show-block-right-menu?)
   (rum/local false ::show-block-right-menu?)
@@ -2884,20 +2900,26 @@
 
 
                :else
                :else
                nil)
                nil)
-             (assoc state
-                    ::control-show? (atom false)
-                    ::navigating-block (atom (:block/uuid block)))))
+             (-> (assoc state
+                   ::control-show? (atom false)
+                   ::navigating-block (atom (:block/uuid block)))
+                 (attach-order-list-state!))))
+
+   :will-remount (fn [_old-state new-state]
+                   (-> new-state
+                       (attach-order-list-state!)))
+
    :should-update (fn [old-state new-state]
    :should-update (fn [old-state new-state]
-                    (let [compare-keys [:block/uuid :block/content :block/parent :block/collapsed?
-                                        :block/properties :block/left :block/children :block/_refs :block/bottom? :block/top?]
-                          config-compare-keys [:show-cloze?]
-                          b1 (second (:rum/args old-state))
-                          b2 (second (:rum/args new-state))
-                          result (or
-                                  (not= (select-keys b1 compare-keys)
-                                        (select-keys b2 compare-keys))
-                                  (not= (select-keys (first (:rum/args old-state)) config-compare-keys)
-                                        (select-keys (first (:rum/args new-state)) config-compare-keys)))]
+                    (let [compare-keys        [:block/uuid :block/content :block/parent :block/collapsed?
+                                               :block/properties :block/left :block/children :block/_refs :block/bottom? :block/top?]
+                          config-compare-keys [:show-cloze? :own-order-list-type :own-order-list-index]
+                          b1                  (second (:rum/args old-state))
+                          b2                  (second (:rum/args new-state))
+                          result              (or
+                                                (not= (select-keys b1 compare-keys)
+                                                      (select-keys b2 compare-keys))
+                                                (not= (select-keys (first (:rum/args old-state)) config-compare-keys)
+                                                      (select-keys (first (:rum/args new-state)) config-compare-keys)))]
                       (boolean result)))
                       (boolean result)))
    :will-unmount (fn [state]
    :will-unmount (fn [state]
                    ;; restore root block's collapsed state
                    ;; restore root block's collapsed state
@@ -2907,8 +2929,8 @@
                        (state/set-collapsed-block! block-id nil)))
                        (state/set-collapsed-block! block-id nil)))
                    state)}
                    state)}
   [state config block]
   [state config block]
-  (let [repo (state/get-current-repo)
-        ref? (:ref? config)
+  (let [repo          (state/get-current-repo)
+        ref?          (:ref? config)
         custom-query? (boolean (:custom-query? config))]
         custom-query? (boolean (:custom-query? config))]
     (if (and (or ref? custom-query?) (not (:ref-query-child? config)))
     (if (and (or ref? custom-query?) (not (:ref-query-child? config)))
       (ui/lazy-visible
       (ui/lazy-visible
@@ -2918,11 +2940,11 @@
 
 
 (defn divide-lists
 (defn divide-lists
   [[f & l]]
   [[f & l]]
-  (loop [l l
+  (loop [l        l
          ordered? (:ordered f)
          ordered? (:ordered f)
-         result [[f]]]
+         result   [[f]]]
     (if (seq l)
     (if (seq l)
-      (let [cur (first l)
+      (let [cur          (first l)
             cur-ordered? (:ordered cur)]
             cur-ordered? (:ordered cur)]
         (if (= ordered? cur-ordered?)
         (if (= ordered? cur-ordered?)
           (recur
           (recur

+ 37 - 10
src/main/frontend/components/block.css

@@ -116,6 +116,7 @@
       position: relative;
       position: relative;
       top: 4px;
       top: 4px;
     }
     }
+
     a {
     a {
       color: var(--ls-primary-text-color);
       color: var(--ls-primary-text-color);
 
 
@@ -202,6 +203,10 @@
 .block-control-wrap {
 .block-control-wrap {
   height: 24px;
   height: 24px;
   margin-top: 0;
   margin-top: 0;
+
+  &.is-order-list {
+    @apply relative right-0 mr-0;
+  }
 }
 }
 
 
 .block-control, .block-control:hover {
 .block-control, .block-control:hover {
@@ -525,25 +530,47 @@
     background-color: var(--ls-block-bullet-color, #8fbc8f);
     background-color: var(--ls-block-bullet-color, #8fbc8f);
   }
   }
 
 
+  &.as-order-list {
+    @apply w-[28px] whitespace-nowrap justify-start;
+  }
+
   .bullet {
   .bullet {
-    border-radius: 50%;
-    width: 6px;
-    height: 6px;
+    @apply rounded-full w-[6px] h-[6px];
+
     background-color: var(--ls-block-bullet-color, #394b59);
     background-color: var(--ls-block-bullet-color, #394b59);
     transition: transform 0.2s;
     transition: transform 0.2s;
+
+    > * {
+      @apply cursor-pointer;
+    }
   }
   }
 
 
-  &.bullet-closed {
-    background-color: var(--ls-block-bullet-border-color, #ced9e0);
+  &:not(.typed-list) {
+    &.bullet-closed {
+      background-color: var(--ls-block-bullet-border-color, #ced9e0);
+    }
   }
   }
-}
 
 
-a:hover > .bullet-container .bullet {
-  transform: scale(1.4);
+  &.typed-list:not(:focus-within) {
+    .bullet {
+      background-color: unset;
+      height: unset;
+      width: unset;
+      box-shadow: none;
+    }
+  }
 }
 }
 
 
-a:hover > .bullet-container {
-  background-color: var(--ls-block-bullet-border-color, #ced9e0);
+.bullet-link-wrap {
+  color: var(--ls-primary-text-color);
+
+  &:hover > .bullet-container .bullet {
+    transform: scale(1.4);
+  }
+
+  &:hover > .bullet-container:not(.typed-list) {
+    background-color: var(--ls-block-bullet-border-color, #ced9e0);
+  }
 }
 }
 
 
 .content.doc-mode {
 .content.doc-mode {

+ 8 - 1
src/main/frontend/components/container.css

@@ -226,7 +226,7 @@
         li {
         li {
           margin: 0;
           margin: 0;
         }
         }
-        
+
         a {
         a {
           width: 100%;
           width: 100%;
           padding: 4px 24px;
           padding: 4px 24px;
@@ -624,6 +624,13 @@ html[data-theme='dark'] {
   }
   }
 }
 }
 
 
+/* Workaround for Linux Intel GPU text rendering issue GH#7233 */
+.is-electron.is-linux .cp__menubar-repos {
+  #repo-switch, .nav-header .flex-1 {
+    position: relative;
+  }
+}
+
 @supports not (overflow-y: overlay) {
 @supports not (overflow-y: overlay) {
   .scrollbar-spacing {
   .scrollbar-spacing {
     overflow-y: auto;
     overflow-y: auto;

+ 21 - 7
src/main/frontend/components/content.cljs

@@ -10,6 +10,7 @@
             [frontend.extensions.srs :as srs]
             [frontend.extensions.srs :as srs]
             [frontend.handler.common :as common-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.editor.property :as editor-property]
             [frontend.handler.image :as image-handler]
             [frontend.handler.image :as image-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.notification :as notification]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
@@ -31,8 +32,8 @@
 (rum/defc custom-context-menu-content
 (rum/defc custom-context-menu-content
   []
   []
   [:.menu-links-wrapper
   [:.menu-links-wrapper
-   (ui/menu-background-color #(editor-handler/batch-add-block-property! (state/get-selection-block-ids) :background-color %)
-                             #(editor-handler/batch-remove-block-property! (state/get-selection-block-ids) :background-color))
+   (ui/menu-background-color #(editor-property/batch-add-block-property! (state/get-selection-block-ids) :background-color %)
+                             #(editor-property/batch-remove-block-property! (state/get-selection-block-ids) :background-color))
 
 
    (ui/menu-heading #(editor-handler/batch-set-heading! (state/get-selection-block-ids) %)
    (ui/menu-heading #(editor-handler/batch-set-heading! (state/get-selection-block-ids) %)
                     #(editor-handler/batch-set-heading! (state/get-selection-block-ids) true)
                     #(editor-handler/batch-set-heading! (state/get-selection-block-ids) true)
@@ -84,6 +85,12 @@
       (t :context-menu/make-a-flashcard)
       (t :context-menu/make-a-flashcard)
       nil))
       nil))
 
 
+   (ui/menu-link
+     {:key "Toggle number list"
+      :on-click #(state/pub-event! [:editor/toggle-own-number-list (state/get-selection-block-ids)])}
+     (t :context-menu/toggle-number-list)
+     nil)
+
    (ui/menu-link
    (ui/menu-link
     {:key "cycle todos"
     {:key "cycle todos"
      :on-click editor-handler/cycle-todos!}
      :on-click editor-handler/cycle-todos!}
@@ -151,9 +158,9 @@
                                         [:p (t :context-menu/template-exists-warning)]
                                         [:p (t :context-menu/template-exists-warning)]
                                         :error)
                                         :error)
                                        (do
                                        (do
-                                         (editor-handler/set-block-property! block-id :template title)
+                                         (editor-property/set-block-property! block-id :template title)
                                          (when (false? template-including-parent?)
                                          (when (false? template-including-parent?)
-                                           (editor-handler/set-block-property! block-id :template-including-parent false))
+                                           (editor-property/set-block-property! block-id :template-including-parent false))
                                          (state/hide-custom-context-menu!)))))))]
                                          (state/hide-custom-context-menu!)))))))]
          [:hr.menu-separator]])
          [:hr.menu-separator]])
       (ui/menu-link
       (ui/menu-link
@@ -170,8 +177,8 @@
     (when-let [block (db/entity [:block/uuid block-id])]
     (when-let [block (db/entity [:block/uuid block-id])]
       (let [heading (-> block :block/properties :heading (or false))]
       (let [heading (-> block :block/properties :heading (or false))]
         [:.menu-links-wrapper
         [:.menu-links-wrapper
-         (ui/menu-background-color #(editor-handler/set-block-property! block-id :background-color %)
-                                   #(editor-handler/remove-block-property! block-id :background-color))
+         (ui/menu-background-color #(editor-property/set-block-property! block-id :background-color %)
+                                   #(editor-property/remove-block-property! block-id :background-color))
 
 
          (ui/menu-heading heading
          (ui/menu-heading heading
                           #(editor-handler/set-heading! block-id %)
                           #(editor-handler/set-heading! block-id %)
@@ -255,6 +262,12 @@
            :else
            :else
            nil)
            nil)
 
 
+         (ui/menu-link
+           {:key "Toggle number list"
+            :on-click #(state/pub-event! [:editor/toggle-own-number-list (state/get-selection-block-ids)])}
+           (t :context-menu/toggle-number-list)
+           nil)
+
          [:hr.menu-separator]
          [:hr.menu-separator]
 
 
          (ui/menu-link
          (ui/menu-link
@@ -353,7 +366,8 @@
      (mixins/listen state js/window "contextmenu"
      (mixins/listen state js/window "contextmenu"
                     (fn [e]
                     (fn [e]
                       (let [target (gobj/get e "target")
                       (let [target (gobj/get e "target")
-                            block-id (d/attr target "blockid")
+                            block-el (.closest target ".bullet-container[blockid]")
+                            block-id (some-> block-el (.getAttribute "blockid"))
                             {:keys [block block-ref]} (state/sub :block-ref/context)
                             {:keys [block block-ref]} (state/sub :block-ref/context)
                             {:keys [page]} (state/sub :page-title/context)]
                             {:keys [page]} (state/sub :page-title/context)]
                         (cond
                         (cond

+ 2 - 2
src/main/frontend/components/datetime.cljs

@@ -46,7 +46,7 @@
   (let [show? (rum/react *show-repeater?)]
   (let [show? (rum/react *show-repeater?)]
     (if (or show? (and num duration kind))
     (if (or show? (and num duration kind))
       [:div.w.full.flex.flex-row.justify-left
       [:div.w.full.flex.flex-row.justify-left
-       [:input#repeater-num.form-input.mt-1.w-8.px-1.sm:w-20.sm:px-2.text-center
+       [:input#repeater-num.form-input.w-8.mr-2.px-1.sm:w-20.sm:px-2.text-center
         {:default-value num
         {:default-value num
          :on-change (fn [event]
          :on-change (fn [event]
                       (let [value (util/evalue event)]
                       (let [value (util/evalue event)]
@@ -66,7 +66,7 @@
           (swap! *timestamp assoc-in [:repeater :duration] value))
           (swap! *timestamp assoc-in [:repeater :duration] value))
         nil)
         nil)
 
 
-       [:a.ml-1.self-center {:on-click (fn []
+       [:a.ml-2.self-center {:on-click (fn []
                                          (reset! *show-repeater? false)
                                          (reset! *show-repeater? false)
                                          (swap! *timestamp assoc :repeater {}))}
                                          (swap! *timestamp assoc :repeater {}))}
         svg/close]]
         svg/close]]

+ 12 - 10
src/main/frontend/components/header.cljs

@@ -114,17 +114,19 @@
           :options {:href (rfe/href :import)}
           :options {:href (rfe/href :import)}
           :icon (ui/icon "file-upload")})
           :icon (ui/icon "file-upload")})
 
 
-       {:title [:div.flex-row.flex.justify-between.items-center
-                [:span (t :join-community)]]
-        :options {:href "https://discuss.logseq.com"
-                  :title (t :discourse-title)
-                  :target "_blank"}
-        :icon (ui/icon "brand-discord")}
+       (when-not config/publishing? 
+         {:title [:div.flex-row.flex.justify-between.items-center
+                  [:span (t :join-community)]]
+          :options {:href "https://discuss.logseq.com"
+                    :title (t :discourse-title)
+                    :target "_blank"}
+          :icon (ui/icon "brand-discord")})
 
 
-       {:title [:div.flex-row.flex.justify-between.items-center
-                [:span (t :help/bug)]]
-        :options {:href (rfe/href :bug-report)}
-        :icon (ui/icon "bug")}
+       (when-not config/publishing?
+         {:title [:div.flex-row.flex.justify-between.items-center
+                  [:span (t :help/bug)]]
+          :options {:href (rfe/href :bug-report)}
+          :icon (ui/icon "bug")})
 
 
        (when (and (state/sub :auth/id-token) (user-handler/logged-in?))
        (when (and (state/sub :auth/id-token) (user-handler/logged-in?))
          {:title (str (t :logout) " (" (user-handler/email) ")")
          {:title (str (t :logout) " (" (user-handler/email) ")")

+ 5 - 0
src/main/frontend/components/header.css

@@ -128,6 +128,11 @@
   padding-left: 1rem;
   padding-left: 1rem;
 }
 }
 
 
+/* Workaround for Linux Intel GPU text rendering issue GH#7233 */
+.is-electron.is-linux .cp__header .dropdown-wrapper .title-wrap {
+  position: relative;
+}
+
 .cp__header a, .cp__header svg {
 .cp__header a, .cp__header svg {
   -webkit-app-region: no-drag;
   -webkit-app-region: no-drag;
 }
 }

+ 25 - 16
src/main/frontend/components/page.cljs

@@ -37,7 +37,9 @@
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
             [medley.core :as medley]
             [medley.core :as medley]
             [reitit.frontend.easy :as rfe]
             [reitit.frontend.easy :as rfe]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [logseq.graph-parser.util.page-ref :as page-ref]
+            [logseq.graph-parser.mldoc :as gp-mldoc]))
 
 
 (defn- get-page-name
 (defn- get-page-name
   [state]
   [state]
@@ -295,7 +297,8 @@
            (assoc state ::title-value (atom (nth (:rum/args state) 2))))}
            (assoc state ::title-value (atom (nth (:rum/args state) 2))))}
   [state page-name icon title _format fmt-journal?]
   [state page-name icon title _format fmt-journal?]
   (when title
   (when title
-    (let [*title-value (get state ::title-value)
+    (let [page (when page-name (db/entity [:block/name page-name]))
+          *title-value (get state ::title-value)
           *edit? (get state ::edit?)
           *edit? (get state ::edit?)
           *input-value (get state ::input-value)
           *input-value (get state ::input-value)
           repo (state/get-current-repo)
           repo (state/get-current-repo)
@@ -304,7 +307,9 @@
           untitled? (and whiteboard-page? (parse-uuid page-name)) ;; normal page cannot be untitled right?
           untitled? (and whiteboard-page? (parse-uuid page-name)) ;; normal page cannot be untitled right?
           title (if hls-page?
           title (if hls-page?
                   [:a.asset-ref (pdf-utils/fix-local-asset-pagename title)]
                   [:a.asset-ref (pdf-utils/fix-local-asset-pagename title)]
-                  (if fmt-journal? (date/journal-title->custom-format title) title))
+                  (if fmt-journal?
+                    (date/journal-title->custom-format title)
+                    title))
           old-name (or title page-name)]
           old-name (or title page-name)]
       [:h1.page-title.flex.cursor-pointer.gap-1.w-full
       [:h1.page-title.flex.cursor-pointer.gap-1.w-full
        {:class (when-not whiteboard-page? "title")
        {:class (when-not whiteboard-page? "title")
@@ -312,16 +317,16 @@
                          (when (util/right-click? e)
                          (when (util/right-click? e)
                            (state/set-state! :page-title/context {:page page-name})))
                            (state/set-state! :page-title/context {:page page-name})))
         :on-click (fn [e]
         :on-click (fn [e]
-                    (.preventDefault e)
-                    (if (gobj/get e "shiftKey")
-                      (when-let [page (db/pull repo '[*] [:block/name page-name])]
-                        (state/sidebar-add-block!
-                         repo
-                         (:db/id page)
-                         :page))
-                      (when (and (not hls-page?) (not fmt-journal?) (not config/publishing?))
-                        (reset! *input-value (if untitled? "" old-name))
-                        (reset! *edit? true))))}
+                       (.preventDefault e)
+                       (if (gobj/get e "shiftKey")
+                         (when-let [page (db/pull repo '[*] [:block/name page-name])]
+                           (state/sidebar-add-block!
+                            repo
+                            (:db/id page)
+                            :page))
+                         (when (and (not hls-page?) (not fmt-journal?) (not config/publishing?))
+                           (reset! *input-value (if untitled? "" old-name))
+                           (reset! *edit? true))))}
        (when (not= icon "") [:span.page-icon icon])
        (when (not= icon "") [:span.page-icon icon])
        [:div.page-title-sizer-wrapper.relative
        [:div.page-title-sizer-wrapper.relative
         (when @*edit?
         (when @*edit?
@@ -337,9 +342,13 @@
          {:data-value @*input-value
          {:data-value @*input-value
           :data-ref   page-name
           :data-ref   page-name
           :style      {:opacity (when @*edit? 0)}}
           :style      {:opacity (when @*edit? 0)}}
-         (cond @*edit? [:span {:style {:white-space "pre"}} (rum/react *input-value)]
-               untitled? [:span.opacity-50 (t :untitled)]
-               :else title)]]])))
+         (let [nested? (and (string/includes? title page-ref/left-brackets)
+                            (string/includes? title page-ref/right-brackets))]
+           (cond @*edit? [:span {:style {:white-space "pre"}} (rum/react *input-value)]
+                 untitled? [:span.opacity-50 (t :untitled)]
+                 nested? (component-block/map-inline {} (gp-mldoc/inline->edn title (gp-mldoc/default-config
+                                                                                     (:block/format page))))
+                 :else title))]]])))
 
 
 (defn- page-mouse-over
 (defn- page-mouse-over
   [e *control-show? *all-collapsed?]
   [e *control-show? *all-collapsed?]

+ 1 - 1
src/main/frontend/components/page.css

@@ -276,7 +276,7 @@ a.page-title {
   }
   }
 
 
   > .title {
   > .title {
-    @apply w-full pointer-events-none overflow-hidden overflow-ellipsis;
+    @apply w-full overflow-hidden overflow-ellipsis;
   }
   }
 
 
   .edit-input {
   .edit-input {

+ 12 - 10
src/main/frontend/components/page_menu.cljs

@@ -80,15 +80,16 @@
                                     (file-sync-handler/get-current-graph-uuid))]
                                     (file-sync-handler/get-current-graph-uuid))]
       (when (and page (not block?))
       (when (and page (not block?))
         (->>
         (->>
-         [{:title   (if favorited?
-                      (t :page/unfavorite)
-                      (t :page/add-to-favorites))
-           :options {:on-click
-                     (fn []
-                       (if favorited?
-                         (page-handler/unfavorite-page! page-original-name)
-                         (page-handler/favorite-page! page-original-name)))}}
-
+         [(when-not config/publishing?
+            {:title   (if favorited?
+                        (t :page/unfavorite)
+                        (t :page/add-to-favorites))
+             :options {:on-click
+                       (fn []
+                         (if favorited?
+                           (page-handler/unfavorite-page! page-original-name)
+                           (page-handler/favorite-page! page-original-name)))}})
+          
           (when (or (util/electron?) file-sync-graph-uuid)
           (when (or (util/electron?) file-sync-graph-uuid)
             {:title   (t :page/version-history)
             {:title   (t :page/version-history)
              :options {:on-click
              :options {:on-click
@@ -109,7 +110,8 @@
             {:title   (t :page/copy-page-url)
             {:title   (t :page/copy-page-url)
              :options {:on-click #(page-handler/copy-page-url page-original-name)}})
              :options {:on-click #(page-handler/copy-page-url page-original-name)}})
 
 
-          (when-not contents?
+          (when-not (or contents?
+                        config/publishing?)
             {:title   (t :page/delete)
             {:title   (t :page/delete)
              :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
              :options {:on-click #(state/set-modal! (delete-page-dialog page-name))}})
 
 

+ 10 - 9
src/main/frontend/components/plugins.cljs

@@ -1104,16 +1104,17 @@
         [:div.ui-items-container
         [:div.ui-items-container
          {:data-type (name type)}
          {:data-type (name type)}
 
 
-         ;; manage plugin buttons
-         (when toolbar?
-           [:<>
-            (header-ui-items-list-wrap
-              (for [[_ {:keys [key pinned?] :as opts} pid] items]
-                (when (or (not toolbar?)
-                          (not (set? pinned-items)) pinned?)
-                  (rum/with-key (ui-item-renderer pid type opts) key))))
+         [:<>
+          (header-ui-items-list-wrap
+            (for [[_ {:keys [key pinned?] :as opts} pid] items]
+              (when (or (not toolbar?)
+                        (not (set? pinned-items)) pinned?)
+                (rum/with-key (ui-item-renderer pid type opts) key))))
+
+          ;; manage plugin buttons
+          (when toolbar?
             (let [updates-coming (state/sub :plugin/updates-coming)]
             (let [updates-coming (state/sub :plugin/updates-coming)]
-              (toolbar-plugins-manager-list updates-coming items))])]))))
+              (toolbar-plugins-manager-list updates-coming items)))]]))))
 
 
 (rum/defcs hook-ui-fenced-code < rum/reactive
 (rum/defcs hook-ui-fenced-code < rum/reactive
   [_state content {:keys [render edit] :as _opts}]
   [_state content {:keys [render edit] :as _opts}]

+ 3 - 2
src/main/frontend/components/query.cljs

@@ -12,6 +12,7 @@
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
             [frontend.extensions.sci :as sci]
             [frontend.extensions.sci :as sci]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.editor.property :as editor-property]
             [logseq.graph-parser.util :as gp-util]))
             [logseq.graph-parser.util :as gp-util]))
 
 
 (defn built-in-custom-query?
 (defn built-in-custom-query?
@@ -206,12 +207,12 @@
                (when (and current-block (not view-f) (nil? table-view?) (not page-list?))
                (when (and current-block (not view-f) (nil? table-view?) (not page-list?))
                  (if table?
                  (if table?
                    [:a.flex.ml-1.fade-link {:title "Switch to list view"
                    [:a.flex.ml-1.fade-link {:title "Switch to list view"
-                                            :on-click (fn [] (editor-handler/set-block-property! current-block-uuid
+                                            :on-click (fn [] (editor-property/set-block-property! current-block-uuid
                                                                                                  "query-table"
                                                                                                  "query-table"
                                                                                                  false))}
                                                                                                  false))}
                     (ui/icon "list" {:style {:font-size 20}})]
                     (ui/icon "list" {:style {:font-size 20}})]
                    [:a.flex.ml-1.fade-link {:title "Switch to table view"
                    [:a.flex.ml-1.fade-link {:title "Switch to table view"
-                                            :on-click (fn [] (editor-handler/set-block-property! current-block-uuid
+                                            :on-click (fn [] (editor-property/set-block-property! current-block-uuid
                                                                                                  "query-table"
                                                                                                  "query-table"
                                                                                                  true))}
                                                                                                  true))}
                     (ui/icon "table" {:style {:font-size 20}})]))
                     (ui/icon "table" {:style {:font-size 20}})]))

+ 3 - 3
src/main/frontend/components/query_table.cljs

@@ -4,7 +4,7 @@
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.db.query-dsl :as query-dsl]
             [frontend.db.query-dsl :as query-dsl]
             [frontend.handler.common :as common-handler]
             [frontend.handler.common :as common-handler]
-            [frontend.handler.editor :as editor-handler]
+            [frontend.handler.editor.property :as editor-property]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util :as util]
             [frontend.util.clock :as clock]
             [frontend.util.clock :as clock]
@@ -80,8 +80,8 @@
   [title column {:keys [sort-by-column sort-desc?]} block-id]
   [title column {:keys [sort-by-column sort-desc?]} block-id]
   [:th.whitespace-nowrap
   [:th.whitespace-nowrap
    [:a {:on-click (fn []
    [:a {:on-click (fn []
-                    (editor-handler/set-block-property! block-id :query-sort-by (name column))
-                    (editor-handler/set-block-property! block-id :query-sort-desc (not sort-desc?)))}
+                    (editor-property/set-block-property! block-id :query-sort-by (name column))
+                    (editor-property/set-block-property! block-id :query-sort-desc (not sort-desc?)))}
     [:div.flex.items-center
     [:div.flex.items-center
      [:span.mr-1 title]
      [:span.mr-1 title]
      (when (= sort-by-column column)
      (when (= sort-by-column column)

+ 1 - 0
src/main/frontend/components/shortcut.cljs

@@ -181,4 +181,5 @@
    (shortcut-table :shortcut.category/block-selection true)
    (shortcut-table :shortcut.category/block-selection true)
    (shortcut-table :shortcut.category/formatting true)
    (shortcut-table :shortcut.category/formatting true)
    (shortcut-table :shortcut.category/toggle true)
    (shortcut-table :shortcut.category/toggle true)
+   (when (state/enable-whiteboards?) (shortcut-table :shortcut.category/whiteboard true))
    (shortcut-table :shortcut.category/others true)])
    (shortcut-table :shortcut.category/others true)])

+ 3 - 1
src/main/frontend/components/whiteboard.cljs

@@ -11,6 +11,7 @@
             [frontend.handler.common :as common-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
+            [frontend.modules.shortcut.core :as shortcut]
             [frontend.rum :refer [use-bounding-client-rect use-breakpoint
             [frontend.rum :refer [use-bounding-client-rect use-breakpoint
                                   use-click-outside]]
                                   use-click-outside]]
             [frontend.state :as state]
             [frontend.state :as state]
@@ -290,7 +291,8 @@
                                                                       {:extension? true})])})]]
                                                                       {:extension? true})])})]]
      (tldraw-app page-name block-id)]))
      (tldraw-app page-name block-id)]))
 
 
-(rum/defc whiteboard-route
+(rum/defc whiteboard-route <
+(shortcut/mixin :shortcut.handler/whiteboard)
   [route-match]
   [route-match]
   (let [name (get-in route-match [:parameters :path :name])
   (let [name (get-in route-match [:parameters :path :name])
         {:keys [block-id]} (get-in route-match [:parameters :query])]
         {:keys [block-id]} (get-in route-match [:parameters :query])]

+ 2 - 9
src/main/frontend/db/model.cljs

@@ -957,15 +957,8 @@ independent of format as format specific heading characters are stripped"
   "Doesn't include nested children."
   "Doesn't include nested children."
   [repo block-uuid]
   [repo block-uuid]
   (when-let [db (conn/get-db repo)]
   (when-let [db (conn/get-db repo)]
-    (-> (d/q
-         '[:find [(pull ?b [*]) ...]
-           :in $ ?parent-id
-           :where
-           [?parent :block/uuid ?parent-id]
-           [?b :block/parent ?parent]]
-         db
-         block-uuid)
-        (sort-by-left (db-utils/entity [:block/uuid block-uuid])))))
+    (when-let [parent (db-utils/entity repo [:block/uuid block-uuid])]
+      (sort-by-left (:block/_parent parent) parent))))
 
 
 (defn get-block-children
 (defn get-block-children
   "Including nested children."
   "Including nested children."

+ 1 - 1
src/main/frontend/db/react.cljs

@@ -16,7 +16,7 @@
 ;;; keywords specs for reactive query, used by `react/q` calls
 ;;; keywords specs for reactive query, used by `react/q` calls
 ;; ::block
 ;; ::block
 ;; pull-block react-query
 ;; pull-block react-query
-(s/def ::block (s/tuple #(= ::block %) uuid?))
+(s/def ::block (s/tuple #(= ::block %) int?))
 ;; ::page-blocks
 ;; ::page-blocks
 ;; get page-blocks react-query
 ;; get page-blocks react-query
 (s/def ::page-blocks (s/tuple #(= ::page-blocks %) int?))
 (s/def ::page-blocks (s/tuple #(= ::page-blocks %) int?))

+ 117 - 6
src/main/frontend/dicts.cljc

@@ -171,6 +171,7 @@
         :content/open-in-sidebar "Open in sidebar"
         :content/open-in-sidebar "Open in sidebar"
         :content/click-to-edit "Click to edit"
         :content/click-to-edit "Click to edit"
         :context-menu/make-a-flashcard "Make a Flashcard"
         :context-menu/make-a-flashcard "Make a Flashcard"
+        :context-menu/toggle-number-list "Toggle number list"
         :context-menu/preview-flashcard "Preview Flashcard"
         :context-menu/preview-flashcard "Preview Flashcard"
         :context-menu/make-a-template "Make a Template"
         :context-menu/make-a-template "Make a Template"
         :context-menu/input-template-name "What's the template's name?"
         :context-menu/input-template-name "What's the template's name?"
@@ -318,7 +319,7 @@
         :plugin/found-updates "New updates"
         :plugin/found-updates "New updates"
         :plugin/found-n-updates "Found {1} updates"
         :plugin/found-n-updates "Found {1} updates"
         :plugin/update-all-selected "Update all of selected"
         :plugin/update-all-selected "Update all of selected"
-        :plugin/updates-downloading "Downloading for the updates"
+        :plugin/updates-downloading "Downloading updates"
         :plugin/refresh-lists "Refresh lists"
         :plugin/refresh-lists "Refresh lists"
         :plugin/enabled "Enabled"
         :plugin/enabled "Enabled"
         :plugin/disabled "Disabled"
         :plugin/disabled "Disabled"
@@ -2726,7 +2727,116 @@
            :asset/maximize "Maksimer bilde"
            :asset/maximize "Maksimer bilde"
            :asset/open-in-browser "Åpne bilde i nettleser"
            :asset/open-in-browser "Åpne bilde i nettleser"
            :asset/show-in-folder "Vis bilde i mappe"
            :asset/show-in-folder "Vis bilde i mappe"
-           :linked-references/filter-search "Søk i lenkede referanser"}
+           :linked-references/filter-search "Søk i lenkede referanser"
+           :all-whiteboards "Alle whiteboard"
+           :auto-heading "Automatisk overskrift"
+           :heading "Overskrift {1}"
+           :new-whiteboard "Nytt whiteboard"
+           :remove-heading "Fjern overskrift"
+           :untitled "Uten navn"
+           :accessibility/skip-to-main-content "Hopp til hovedinnhold"
+           :color/blue "Blå"
+           :color/gray "Grå"
+           :color/green "Grønn"
+           :color/pink "Rosa"
+           :color/purple "Lilla"
+           :color/red "Rød"
+           :color/yellow "Gul"
+           :content/copy-block-url "Kopier blokk URL"
+           :content/copy-export-as "Kopier / Eksporter som.."
+           :content/copy-ref "Kopier denne referansen"
+           :content/delete-ref "Slett denne referansen"
+           :content/replace-with-embed "Erstatt med innebygging"
+           :content/replace-with-text "Erstatt med tekst"
+           :context-menu/input-template-name "Hva heter malen?"
+           :context-menu/make-a-flashcard "Lag et Flashcard"
+           :context-menu/make-a-template "Lag en Mal"
+           :context-menu/preview-flashcard "Forhåndsvis Flashcard"
+           :context-menu/template-exists-warning "Malen eksisterer allerde!"
+           :context-menu/template-include-parent-block "Inkluder overordnet blokk i malen?"
+           :context-menu/toggle-number-list "Veksle nummerliste"
+           :dev/show-block-ast "(Dev) Vis blokk AST"
+           :dev/show-block-data "(Dev) Vis blokk data"
+           :dev/show-page-ast "(Dev) Vis side AST"
+           :dev/show-page-data "(Dev) Vis side data"
+           :editor/collapse-block-children "Skjul alle"
+           :editor/cycle-todo "Roterer TODO statusen for gjeldende element"
+           :editor/delete-selection "Slett valgte blokker"
+           :editor/expand-block-children "Utvid alle"
+           :file/validate-existing-file-error "Siden eksisterer allerede i en annen fil: {1}, nåværen..."
+           :file-rn/all-action "Utfør alle Handlinger!"
+           :file-rn/apply-rename "Utfør omdøping av filen"
+           :file-rn/close-panel "Lukk Panel"
+           :file-rn/confirm-proceed "Oppdater format!"
+           :file-rn/filename-desc-1 "Denne innstillingen konfigurerer hvordan en side blir lagret til en ..."
+           :file-rn/filename-desc-2 "Noen tegn som \"/\" eller \"?\" er ikke gyldige for en..."
+           :file-rn/filename-desc-3 "Logseq erstatter ugyldige tegn med deres URL ..."
+           :file-rn/filename-desc-4 "Skilletegnet for navnerom \"/\" brukes også av \"_..."
+           :file-rn/format-deprecated "Du bruker for øyeblikket et utdatert format. Oppdat..."
+           :file-rn/instruct-1 "Det er en to-trinns prosess å oppdatere formatet for filnavn:"
+           :file-rn/instruct-2 "1. Klikk "
+           :file-rn/instruct-3 "2. Følg instruksjonene under for å gi filen et nytt navn..."
+           :file-rn/legend "🟢 Valgfri omdøping; 🟡 Omdøping kreves..."
+           :file-rn/need-action "Omdøping av fil er anbefalt for å matche de nye..."
+           :file-rn/no-action "Bra jobba! Well done! Ingen ytterligere tiltak kreves."
+           :file-rn/optional-rename "Forslag: "
+           :file-rn/or-select-actions " eller gi filer nytt navn individuelt under, så "
+           :file-rn/or-select-actions-2 ". Disse handlingene er ikke tilgjengelige når du lukker ..."
+           :file-rn/otherwise-breaking "Eller tittelen vil bli"
+           :file-rn/re-index "Re-indksering er sterkt anbefalt etter at filene er..."
+           :file-rn/rename "Omdøp fil \"{1}\" til \"{2}\""
+           :file-rn/select-confirm-proceed "Dev: skriv format"
+           :file-rn/select-format "(Uviklermodus Operasjon, Farlig!) Velg filenav..."
+           :file-rn/suggest-rename "Handling kreves: "
+           :file-rn/unreachable-title "Advarsel! Navnet på siden vil bli {1} under nåvære.."
+           :left-side-bar/create "Opprett"
+           :left-side-bar/new-whiteboard "Nytt whiteboard"
+           :notification/clear-all "Fjern alt"
+           :on-boarding/tour-whiteboard-home "{1} Hjem for dine whiteboards"
+           :on-boarding/tour-whiteboard-home-description "Whiteboards har sin egen seksjon i appen hvo..."
+           :on-boarding/tour-whiteboard-new "{1} Lag nytt whiteboard"
+           :on-boarding/tour-whiteboard-new-description "Det er mange måter å lage et nytt whiteboard på..."
+           :on-boarding/welcome-whiteboard-modal-description "Whiteboards er et fantastisk verktøy for brainstorming og ..."
+           :on-boarding/welcome-whiteboard-modal-skip "Hopp over"
+           :on-boarding/welcome-whiteboard-modal-start "Start med whiteboard"
+           :on-boarding/welcome-whiteboard-modal-title "Et nytt lerret for dine tanker."
+           :page/logseq-is-having-a-problem "Logseq har et problem. Prøver å få den tilbake ..."
+           :page/show-whiteboards "Vis whiteboards"
+           :page/something-went-wrong "Noe gikk galt"
+           :page/step "Steg {1}"
+           :page/try "Prøv"
+           :pdf/doc-metadata "Dokument metadata"
+           :pdf/hl-block-colored "Farget merkelapp for å  label for utheve blokk"
+           :plugin/found-n-updates "Fant {1} oppdatering"
+           :plugin/found-updates "Nye oppateringer"
+           :plugin/update-all-selected "Oppdater alle valgte"
+           :plugin/updates-downloading "Laster ned oppdateringer"
+           :plugin.install-from-file/menu-title "Installer fra plugins.edn"
+           :plugin.install-from-file/notice "Følgende plugins vil erstatte dine plugins:"
+           :plugin.install-from-file/success "Alle plugins er installert!"
+           :plugin.install-from-file/title "Installer plugins fra plugins.edn"
+           :right-side-bar/history "(Dev) Angre/Gjør om logg"
+           :right-side-bar/whiteboards "Whiteboards"
+           :search/items "elementer"
+           :search-item/block "Blokk"
+           :search-item/file "Fil"
+           :search-item/page "Side"
+           :search-item/whiteboard "Whiteboard"
+           :select/default-select-multiple "Velg en eller flere"
+           :settings-page/alpha-features "Alpha funksjoner"
+           :settings-page/auto-expand-block-refs "Utvid blokkreferanser automatisk når zoomet inn..."
+           :settings-page/beta-features "Beta funksjoner"
+           :settings-page/clear-cache-warning "Tømming av hurtigbufferen vil forkaste dine åpne grafer. Du m..."
+           :settings-page/custom-date-format-warning "Re-indeksering kreves! Eksisterernde dagbokreferanse vi..."
+           :settings-page/disable-sentry-desc "Logseq vil aldri samle inn dine lokale graf sin databas..."
+           :settings-page/edit-setting "Rediger"
+           :settings-page/enable-whiteboards "Whiteboards"
+           :settings-page/filename-format "Filnavn format"
+           :settings-page/login-prompt "For å få tilgang til nye funksjoner før alle andre må du..."
+           :settings-page/preferred-pasting-file "Foretrekk innliming av fil"
+           :settings-page/show-full-blocks "Vis alle linjer av en blokkreferanse"
+           :settings-page/tab-assets "Ressurser"
+           :whiteboard/link-whiteboard-or-block "Lenk whiteboard/side/blokk"}
 
 
    :pt-BR {:on-boarding/demo-graph "Esse é um grafo de demonstração, mudanças não serão salvas enquanto uma pasta local não for aberta."
    :pt-BR {:on-boarding/demo-graph "Esse é um grafo de demonstração, mudanças não serão salvas enquanto uma pasta local não for aberta."
            :on-boarding/add-graph "Adicionar grafo"
            :on-boarding/add-graph "Adicionar grafo"
@@ -3568,7 +3678,7 @@
         :settings-page/disable-sentry "Отправлять данные использования и диагностики в Logseq"
         :settings-page/disable-sentry "Отправлять данные использования и диагностики в Logseq"
         :settings-page/disable-sentry-desc "Logseq никогда не будет собирать вашу локальную базу данных графов или продавать ваши данные"
         :settings-page/disable-sentry-desc "Logseq никогда не будет собирать вашу локальную базу данных графов или продавать ваши данные"
         :settings-page/preferred-outdenting "Логические отступы"
         :settings-page/preferred-outdenting "Логические отступы"
-        :settings-page/show-full-blocks "Показать все ссылки на блок"
+        :settings-page/show-full-blocks "Показать все строки в ссылке на блок"
         :settings-page/auto-expand-block-refs "Автоматически раскрывать ссылки на блок при увеличении масштаба"
         :settings-page/auto-expand-block-refs "Автоматически раскрывать ссылки на блок при увеличении масштаба"
         :settings-page/custom-date-format "Формат даты"
         :settings-page/custom-date-format "Формат даты"
         :settings-page/custom-date-format-warning "Требуется переиндексация! Существующие ссылки на журналы будут нарушены!"
         :settings-page/custom-date-format-warning "Требуется переиндексация! Существующие ссылки на журналы будут нарушены!"
@@ -3725,11 +3835,11 @@
         :updater/new-version-install "Была загружена новая версия"
         :updater/new-version-install "Была загружена новая версия"
         :updater/quit-and-install "Перезапустить для установки"
         :updater/quit-and-install "Перезапустить для установки"
 
 
-        :paginates/pages "Всего {1} страница"
+        :paginates/pages "Всего {1} страниц(а)"
         :paginates/prev "Предыдущая"
         :paginates/prev "Предыдущая"
         :paginates/next "Следующая"
         :paginates/next "Следующая"
 
 
-        :tips/all-done "Все готово"
+        :tips/all-done "Всё готово"
 
 
         :command-palette/prompt "Введите команду"
         :command-palette/prompt "Введите команду"
         :select/default-prompt "Выберите"
         :select/default-prompt "Выберите"
@@ -3741,7 +3851,7 @@
         :file-sync/other-user-graph "Текущий локальный граф привязан к удаленному графу другого пользователя. Поэтому синхронизацию начать нельзя."
         :file-sync/other-user-graph "Текущий локальный граф привязан к удаленному графу другого пользователя. Поэтому синхронизацию начать нельзя."
         :file-sync/graph-deleted "Текущий удаленный граф был удален"
         :file-sync/graph-deleted "Текущий удаленный граф был удален"
 
 
-        :notification/clear-all "Очистить все"}
+        :notification/clear-all "Очистить всё"}
 
 
    :ja {:tutorial/text #?(:cljs (rc/inline "tutorial-ja.md")
    :ja {:tutorial/text #?(:cljs (rc/inline "tutorial-ja.md")
                           :default "tutorial-ja.md")
                           :default "tutorial-ja.md")
@@ -4408,6 +4518,7 @@
         :content/open-in-sidebar "Kenar çubuğunda aç"
         :content/open-in-sidebar "Kenar çubuğunda aç"
         :content/click-to-edit "Düzenlemek için tıklayın"
         :content/click-to-edit "Düzenlemek için tıklayın"
         :context-menu/make-a-flashcard "Bilgi Kartı Oluştur"
         :context-menu/make-a-flashcard "Bilgi Kartı Oluştur"
+        :context-menu/toggle-number-list "Numaralı liste olarak değiştir"
         :context-menu/preview-flashcard "Bilgi Kartını Önizle"
         :context-menu/preview-flashcard "Bilgi Kartını Önizle"
         :context-menu/make-a-template "Bir Şablon Oluştur"
         :context-menu/make-a-template "Bir Şablon Oluştur"
         :context-menu/input-template-name "Şablonun adı nedir?"
         :context-menu/input-template-name "Şablonun adı nedir?"

+ 2 - 1
src/main/frontend/extensions/pdf/assets.cljs

@@ -6,6 +6,7 @@
             [frontend.db.utils :as db-utils]
             [frontend.db.utils :as db-utils]
             [frontend.fs :as fs]
             [frontend.fs :as fs]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.editor.property :as editor-property]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.notification :as notification]
@@ -132,7 +133,7 @@
                                (get-in highlight [:content :image])
                                (get-in highlight [:content :image])
                                (js/Date.now))
                                (js/Date.now))
                    :hl-color (get-in highlight [:properties :color])}]
                    :hl-color (get-in highlight [:properties :color])}]
-      (editor-handler/set-block-property! (:block/uuid block) k v))))
+      (editor-property/set-block-property! (:block/uuid block) k v))))
 
 
 (defn unlink-hl-area-image$
 (defn unlink-hl-area-image$
   [^js _viewer current hl]
   [^js _viewer current hl]

+ 2 - 1
src/main/frontend/extensions/srs.cljs

@@ -13,6 +13,7 @@
             [frontend.db-mixins :as db-mixins]
             [frontend.db-mixins :as db-mixins]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.editor.property :as editor-property]
             [frontend.components.block :as component-block]
             [frontend.components.block :as component-block]
             [frontend.components.macro :as component-macro]
             [frontend.components.macro :as component-macro]
             [frontend.components.select :as component-select]
             [frontend.components.select :as component-select]
@@ -483,7 +484,7 @@
                                    :on-click   (fn []
                                    :on-click   (fn []
                                                  (score-and-next-card 1 card card-index finished? phase review-records cb)
                                                  (score-and-next-card 1 card card-index finished? phase review-records cb)
                                                  (let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
                                                  (let [tomorrow (tc/to-string (t/plus (t/today) (t/days 1)))]
-                                                   (editor-handler/set-block-property! root-block-id card-next-schedule-property tomorrow)))})
+                                                   (editor-property/set-block-property! root-block-id card-next-schedule-property tomorrow)))})
 
 
                (btn-with-shortcut {:btn-text (if (util/mobile?) "Hard" "Took a while to recall")
                (btn-with-shortcut {:btn-text (if (util/mobile?) "Hard" "Took a while to recall")
                                    :shortcut "t"
                                    :shortcut "t"

+ 8 - 1
src/main/frontend/extensions/tldraw.cljs

@@ -10,6 +10,7 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
             [frontend.handler.history :as history]
             [frontend.handler.history :as history]
+            [frontend.modules.shortcut.data-helper :as shortcut-helper]
             [frontend.rum :as r]
             [frontend.rum :as r]
             [frontend.search :as search]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.state :as state]
@@ -75,13 +76,19 @@
   (apply whiteboard/references-count
   (apply whiteboard/references-count
          (map (fn [k] (js->clj (gobj/get props k) {:keywordize-keys true})) ["id" "className" "options"])))
          (map (fn [k] (js->clj (gobj/get props k) {:keywordize-keys true})) ["id" "className" "options"])))
 
 
+(rum/defc keyboard-shortcut
+  [props]
+  (let [shortcut (shortcut-helper/gen-shortcut-seq (keyword (gobj/get props "action")))]
+    (ui/render-keyboard-shortcut shortcut)))
+
 (def tldraw-renderers {:Page page-cp
 (def tldraw-renderers {:Page page-cp
                        :Block block-cp
                        :Block block-cp
                        :Breadcrumb breadcrumb
                        :Breadcrumb breadcrumb
                        :Tweet tweet
                        :Tweet tweet
                        :PageName page-name-link
                        :PageName page-name-link
                        :BacklinksCount references-count
                        :BacklinksCount references-count
-                       :BlockReference block-reference})
+                       :BlockReference block-reference
+                       :KeyboardShortcut keyboard-shortcut})
 
 
 (def undo (fn [] (history/undo! nil)))
 (def undo (fn [] (history/undo! nil)))
 (def redo (fn [] (history/redo! nil)))
 (def redo (fn [] (history/redo! nil)))

+ 2 - 2
src/main/frontend/fs/watcher_handler.cljs

@@ -7,7 +7,7 @@
             [frontend.db.model :as model]
             [frontend.db.model :as model]
             [frontend.fs :as fs]
             [frontend.fs :as fs]
             [logseq.common.path :as path]
             [logseq.common.path :as path]
-            [frontend.handler.editor :as editor]
+            [frontend.handler.editor.property :as editor-property]
             [frontend.handler.file :as file-handler]
             [frontend.handler.file :as file-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.ui :as ui-handler]
@@ -33,7 +33,7 @@
                            nil))]
                            nil))]
         (let [id-property (:id (:block/properties block))]
         (let [id-property (:id (:block/properties block))]
           (when-not (= (str id-property) (str block-id))
           (when-not (= (str id-property) (str block-id))
-            (editor/set-block-property! block-id "id" block-id)))))))
+            (editor-property/set-block-property! block-id "id" block-id)))))))
 
 
 (defn- handle-add-and-change!
 (defn- handle-add-and-change!
   [repo path content db-content mtime backup?]
   [repo path content db-content mtime backup?]

+ 37 - 8
src/main/frontend/handler/block.cljs

@@ -6,7 +6,6 @@
    [frontend.db :as db]
    [frontend.db :as db]
    [frontend.db.model :as db-model]
    [frontend.db.model :as db-model]
    [frontend.db.react :as react]
    [frontend.db.react :as react]
-   [frontend.db.utils :as db-utils]
    [frontend.mobile.haptics :as haptics]
    [frontend.mobile.haptics :as haptics]
    [frontend.modules.outliner.core :as outliner-core]
    [frontend.modules.outliner.core :as outliner-core]
    [frontend.modules.outliner.transaction :as outliner-tx]
    [frontend.modules.outliner.transaction :as outliner-tx]
@@ -70,14 +69,9 @@
                                    (util/distinct-by :db/id))))))
                                    (util/distinct-by :db/id))))))
 
 
 (defn indentable?
 (defn indentable?
-  [{:block/keys [parent] :as block}]
+  [{:block/keys [parent left]}]
   (when parent
   (when parent
-    (let [parent-block (db-utils/pull (:db/id parent))
-          first-child (first
-                       (db-model/get-block-immediate-children
-                        (state/get-current-repo)
-                        (:block/uuid parent-block)))]
-      (not= (:db/id block) (:db/id first-child)))))
+    (not= parent left)))
 
 
 (defn outdentable?
 (defn outdentable?
   [{:block/keys [level] :as _block}]
   [{:block/keys [level] :as _block}]
@@ -291,3 +285,38 @@
                               (recur parent (conj result parent))
                               (recur parent (conj result parent))
                               result))))]
                               result))))]
       (distinct (mapcat get-parents filtered-ref-blocks)))))
       (distinct (mapcat get-parents filtered-ref-blocks)))))
+
+(defn get-idx-of-order-list-block
+  [block order-list-type]
+  (let [order-block-fn? #(some-> % :block/properties :logseq.order-list-type (= order-list-type))
+        prev-block-fn   #(some->> (:db/id %) (db-model/get-prev-sibling (state/get-current-repo)))
+        prev-block      (prev-block-fn block)]
+    (letfn [(page-fn? [b] (some-> b :block/name some?))
+            (order-sibling-list [b]
+              (lazy-seq
+                (when (and (not (page-fn? b)) (order-block-fn? b))
+                  (cons b (order-sibling-list (prev-block-fn b))))))
+            (order-parent-list [b]
+              (lazy-seq
+                (when (and (not (page-fn? b)) (order-block-fn? b))
+                  (cons b (order-parent-list (db-model/get-block-parent (:block/uuid b)))))))]
+      (let [idx           (if prev-block
+                            (count (order-sibling-list block)) 1)
+            order-parents-count (dec (count (order-parent-list block)))
+            delta (if (neg? order-parents-count) 0 (mod order-parents-count 3))]
+        (cond
+          (zero? delta) idx
+
+          (= delta 1)
+          (some-> (util/convert-to-letters idx) util/safe-lower-case)
+
+          :else
+          (util/convert-to-roman idx))))))
+
+(defn attach-order-list-state
+  [config block]
+  (let [own-order-list-type  (some-> block :block/properties :logseq.order-list-type str string/lower-case)
+        own-order-list-index (some->> own-order-list-type (get-idx-of-order-list-block block))]
+    (assoc config :own-order-list-type own-order-list-type
+                  :own-order-list-index own-order-list-index
+                  :own-order-number-list? (= own-order-list-type "number"))))

+ 2 - 1
src/main/frontend/handler/dnd.cljs

@@ -1,6 +1,7 @@
 (ns frontend.handler.dnd
 (ns frontend.handler.dnd
   "Provides fns for drag n drop"
   "Provides fns for drag n drop"
   (:require [frontend.handler.editor :as editor-handler]
   (:require [frontend.handler.editor :as editor-handler]
+            [frontend.handler.editor.property :as editor-property]
             [frontend.modules.outliner.core :as outliner-core]
             [frontend.modules.outliner.core :as outliner-core]
             [frontend.modules.outliner.tree :as tree]
             [frontend.modules.outliner.tree :as tree]
             [frontend.modules.outliner.transaction :as outliner-tx]
             [frontend.modules.outliner.transaction :as outliner-tx]
@@ -20,7 +21,7 @@
       ;; alt pressed, make a block-ref
       ;; alt pressed, make a block-ref
       (and alt-key? (= (count blocks) 1))
       (and alt-key? (= (count blocks) 1))
       (do
       (do
-        (editor-handler/set-block-property! (:block/uuid first-block)
+        (editor-property/set-block-property! (:block/uuid first-block)
                                             :id
                                             :id
                                             (str (:block/uuid first-block)))
                                             (str (:block/uuid first-block)))
         (editor-handler/api-insert-new-block!
         (editor-handler/api-insert-new-block!

+ 132 - 173
src/main/frontend/handler/editor.cljs

@@ -19,6 +19,7 @@
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.common :as common-handler]
             [frontend.handler.common :as common-handler]
+            [frontend.handler.editor.property :as editor-property]
             [frontend.handler.export.html :as export-html]
             [frontend.handler.export.html :as export-html]
             [frontend.handler.export.text :as export-text]
             [frontend.handler.export.text :as export-text]
             [frontend.handler.notification :as notification]
             [frontend.handler.notification :as notification]
@@ -62,6 +63,41 @@
 (defonce *asset-uploading? (atom false))
 (defonce *asset-uploading? (atom false))
 (defonce *asset-uploading-process (atom 0))
 (defonce *asset-uploading-process (atom 0))
 
 
+(def clear-selection! editor-property/clear-selection!)
+(def edit-block! editor-property/edit-block!)
+
+(defn get-block-own-order-list-type
+  [block]
+  (some-> block :block/properties :logseq.order-list-type))
+
+(defn set-block-own-order-list-type!
+  [block type]
+  (when-let [uuid (:block/uuid block)]
+    (editor-property/set-block-property! uuid :logseq.order-list-type (name type))))
+
+(defn remove-block-own-order-list-type!
+  [block]
+  (when-let [uuid (:block/uuid block)]
+    (editor-property/remove-block-property! uuid :logseq.order-list-type)))
+
+(defn own-order-number-list?
+  [block]
+  (= (get-block-own-order-list-type block) "number"))
+
+(defn make-block-as-own-order-list!
+  [block]
+  (some-> block (set-block-own-order-list-type! "number")))
+
+(defn toggle-blocks-as-own-order-list!
+  [blocks]
+  (when (seq blocks)
+    (let [has-ordered?    (some own-order-number-list? blocks)
+          blocks-uuids    (some->> blocks (map :block/uuid) (remove nil?))
+          order-list-prop :logseq.order-list-type]
+      (if has-ordered?
+        (editor-property/batch-remove-block-property! blocks-uuids order-list-prop)
+        (editor-property/batch-add-block-property! blocks-uuids order-list-prop "number")))))
+
 (defn get-selection-and-format
 (defn get-selection-and-format
   []
   []
   (when-let [block (state/get-edit-block)]
   (when-let [block (state/get-edit-block)]
@@ -197,69 +233,7 @@
     (doseq [block blocks]
     (doseq [block blocks]
       (gdom-classes/remove block "block-highlight"))))
       (gdom-classes/remove block "block-highlight"))))
 
 
-(defn- get-edit-input-id-with-block-id
-  [block-id]
-  (when-let [first-block (util/get-first-block-by-id block-id)]
-    (string/replace (gobj/get first-block "id")
-                    "ls-block"
-                    "edit-block")))
-
-(defn clear-selection!
-  []
-  (state/clear-selection!))
-
-(defn- text-range-by-lst-fst-line [content [direction pos]]
-  (case direction
-    :up
-    (let [last-new-line (or (string/last-index-of content \newline) -1)
-          end (+ last-new-line pos 1)]
-      (subs content 0 end))
-    :down
-    (-> (string/split-lines content)
-        first
-        (or "")
-        (subs 0 pos))))
-
 ;; id: block dom id, "ls-block-counter-uuid"
 ;; id: block dom id, "ls-block-counter-uuid"
-(defn edit-block!
-  ([block pos id]
-   (edit-block! block pos id nil))
-  ([block pos id {:keys [custom-content tail-len move-cursor? retry-times]
-                  :or {tail-len 0
-                       move-cursor? true
-                       retry-times 0}
-                  :as opts}]
-   (when-not (> retry-times 2)
-     (when-not config/publishing?
-       (when-let [block-id (:block/uuid block)]
-         (let [block (or (db/pull [:block/uuid block-id]) block)
-               edit-input-id (if (uuid? id)
-                               (get-edit-input-id-with-block-id id)
-                               (-> (str (subs id 0 (- (count id) 36)) block-id)
-                                   (string/replace "ls-block" "edit-block")))
-               content (or custom-content (:block/content block) "")
-               content-length (count content)
-               text-range (cond
-                            (vector? pos)
-                            (text-range-by-lst-fst-line content pos)
-
-                            (and (> tail-len 0) (>= (count content) tail-len))
-                            (subs content 0 (- (count content) tail-len))
-
-                            (or (= :max pos) (<= content-length pos))
-                            content
-
-                            :else
-                            (subs content 0 pos))
-               content (-> (property/remove-built-in-properties (:block/format block)
-                                                                content)
-                           (drawer/remove-logbook))]
-           (clear-selection!)
-           (if edit-input-id
-             (state/set-editing! edit-input-id content block text-range move-cursor?)
-             ;; Block may not be rendered yet
-             (js/setTimeout (fn [] (edit-block! block pos id (update opts :retry-times inc))) 10))))))))
-
 (defn- another-block-with-same-id-exists?
 (defn- another-block-with-same-id-exists?
   [current-id block-id]
   [current-id block-id]
   (when-let [id (and (string? block-id) (parse-uuid block-id))]
   (when-let [id (and (string? block-id) (parse-uuid block-id))]
@@ -779,30 +753,28 @@
        (outliner-core/delete-blocks! [block] {:children? children?})))))
        (outliner-core/delete-blocks! [block] {:children? children?})))))
 
 
 (defn- move-to-prev-block
 (defn- move-to-prev-block
-  ([repo sibling-block format id value]
-   (move-to-prev-block repo sibling-block format id value true))
-  ([repo sibling-block format id value edit?]
-   (when (and repo sibling-block)
-     (when-let [sibling-block-id (dom/attr sibling-block "blockid")]
-       (when-let [block (db/pull repo '[*] [:block/uuid (uuid sibling-block-id)])]
-         (let [original-content (util/trim-safe (:block/content block))
-               value' (-> (property/remove-built-in-properties format original-content)
-                          (drawer/remove-logbook))
-               new-value (str value' value)
-               tail-len (count value)
-               pos (max
-                    (if original-content
-                      (gobj/get (utf8/encode original-content) "length")
-                      0)
-                    0)]
-           (when edit?
-             (edit-block! block pos id
-                          {:custom-content new-value
-                           :tail-len tail-len
-                           :move-cursor? false}))
-           {:prev-block block
-            :new-content new-value
-            :pos pos}))))))
+  [repo sibling-block format id value move?]
+  (when (and repo sibling-block)
+    (when-let [sibling-block-id (dom/attr sibling-block "blockid")]
+      (when-let [block (db/pull repo '[*] [:block/uuid (uuid sibling-block-id)])]
+        (let [original-content (util/trim-safe (:block/content block))
+              value' (-> (property/remove-built-in-properties format original-content)
+                         (drawer/remove-logbook))
+              new-value (str value' value)
+              tail-len (count value)
+              pos (max
+                   (if original-content
+                     (gobj/get (utf8/encode original-content) "length")
+                     0)
+                   0)
+              f (fn [] (edit-block! block pos id
+                                    {:custom-content new-value
+                                     :tail-len tail-len
+                                     :move-cursor? false}))]
+          (when move? (f))
+          {:prev-block block
+           :new-content new-value
+           :move-fn f})))))
 
 
 (declare save-block!)
 (declare save-block!)
 
 
@@ -828,7 +800,7 @@
                (when block-parent-id
                (when block-parent-id
                  (let [block-parent (gdom/getElement block-parent-id)
                  (let [block-parent (gdom/getElement block-parent-id)
                        sibling-block (util/get-prev-block-non-collapsed-non-embed block-parent)
                        sibling-block (util/get-prev-block-non-collapsed-non-embed block-parent)
-                       {:keys [prev-block new-content]} (move-to-prev-block repo sibling-block format id value)
+                       {:keys [prev-block new-content move-fn]} (move-to-prev-block repo sibling-block format id value false)
                        concat-prev-block? (boolean (and prev-block new-content))
                        concat-prev-block? (boolean (and prev-block new-content))
                        transact-opts (cond->
                        transact-opts (cond->
                                        {:outliner-op :delete-block}
                                        {:outliner-op :delete-block}
@@ -838,7 +810,8 @@
                    (outliner-tx/transact! transact-opts
                    (outliner-tx/transact! transact-opts
                      (when concat-prev-block?
                      (when concat-prev-block?
                        (save-block! repo prev-block new-content))
                        (save-block! repo prev-block new-content))
-                     (delete-block-aux! block delete-children?))))))))))
+                     (delete-block-aux! block delete-children?))
+                   (move-fn)))))))))
    (state/set-editor-op! nil)))
    (state/set-editor-op! nil)))
 
 
 (defn delete-blocks!
 (defn delete-blocks!
@@ -855,7 +828,8 @@
         (move-to-prev-block repo sibling-block
         (move-to-prev-block repo sibling-block
                             (:block/format block)
                             (:block/format block)
                             (dom/attr sibling-block "id")
                             (dom/attr sibling-block "id")
-                            "")))))
+                            ""
+                            true)))))
 
 
 (defn- set-block-property-aux!
 (defn- set-block-property-aux!
   [block-or-id key value]
   [block-or-id key value]
@@ -877,72 +851,6 @@
        :block/properties-order (or (keys properties) [])
        :block/properties-order (or (keys properties) [])
        :block/content content})))
        :block/content content})))
 
 
-(defn- batch-set-block-property!
-  "col: a collection of [block-id property-key property-value]."
-  [col]
-  #_:clj-kondo/ignore
-  (when-let [repo (state/get-current-repo)]
-    (let [col' (group-by first col)]
-      (outliner-tx/transact!
-       {:outliner-op :save-block}
-        (doseq [[block-id items] col']
-          (let [block-id (if (string? block-id) (uuid block-id) block-id)
-                new-properties (zipmap (map second items)
-                                (map last items))]
-            (when-let [block (db/entity [:block/uuid block-id])]
-              (let [format (:block/format block)
-                    content (:block/content block)
-                    properties (:block/properties block)
-                    properties-text-values (:block/properties-text-values block)
-                    properties (-> (merge properties new-properties)
-                                   gp-util/remove-nils-non-nested)
-                    properties-text-values (-> (merge properties-text-values new-properties)
-                                               gp-util/remove-nils-non-nested)
-                    property-ks (->> (concat (:block/properties-order block)
-                                             (map second items))
-                                     (filter (set (keys properties)))
-                                     distinct
-                                     vec)
-                    content (property/remove-properties format content)
-                    kvs (for [key property-ks] [key (or (get properties-text-values key)
-                                                        (get properties key))])
-                    content (property/insert-properties format content kvs)
-                    content (property/remove-empty-properties content)
-                    block {:block/uuid block-id
-                           :block/properties properties
-                           :block/properties-order property-ks
-                           :block/properties-text-values properties-text-values
-                           :block/content content}]
-                (outliner-core/save-block! block)))))))
-
-    (let [block-id (ffirst col)
-          block-id (if (string? block-id) (uuid block-id) block-id)
-          input-pos (or (state/get-edit-pos) :max)]
-      ;; update editing input content
-      (when-let [editing-block (state/get-edit-block)]
-        (when (= (:block/uuid editing-block) block-id)
-          (edit-block! editing-block
-                       input-pos
-                       (state/get-edit-input-id)))))))
-
-(defn batch-add-block-property!
-  [block-ids property-key property-value]
-  (batch-set-block-property! (map #(vector % property-key property-value) block-ids)))
-
-(defn batch-remove-block-property!
-  [block-ids property-key]
-  (batch-set-block-property! (map #(vector % property-key nil) block-ids)))
-
-(defn remove-block-property!
-  [block-id key]
-  (let [key (keyword key)]
-    (batch-set-block-property! [[block-id key nil]])))
-
-(defn set-block-property!
-  [block-id key value]
-  (let [key (keyword key)]
-    (batch-set-block-property! [[block-id key value]])))
-
 (defn set-block-query-properties!
 (defn set-block-query-properties!
   [block-id all-properties key add?]
   [block-id all-properties key add?]
   (when-let [block (db/entity [:block/uuid block-id])]
   (when-let [block (db/entity [:block/uuid block-id])]
@@ -956,8 +864,8 @@
                              (remove #{key} query-properties))
                              (remove #{key} query-properties))
           query-properties (vec query-properties)]
           query-properties (vec query-properties)]
       (if (seq query-properties)
       (if (seq query-properties)
-        (set-block-property! block-id :query-properties (str query-properties))
-        (remove-block-property! block-id :query-properties)))))
+        (editor-property/set-block-property! block-id :query-properties (str query-properties))
+        (editor-property/remove-block-property! block-id :query-properties)))))
 
 
 (defn set-block-timestamp!
 (defn set-block-timestamp!
   [block-id key value]
   [block-id key value]
@@ -1001,7 +909,7 @@
                        [block-id :id (str block-id)])))
                        [block-id :id (str block-id)])))
                  block-ids)
                  block-ids)
         col (remove nil? col)]
         col (remove nil? col)]
-    (batch-set-block-property! col)))
+    (editor-property/batch-set-block-property! col)))
 
 
 (defn copy-block-ref!
 (defn copy-block-ref!
   ([block-id]
   ([block-id]
@@ -1910,14 +1818,23 @@
 
 
 (defn handle-last-input []
 (defn handle-last-input []
   (let [input           (state/get-input)
   (let [input           (state/get-input)
+        input-id        (state/get-edit-input-id)
+        edit-block      (state/get-edit-block)
         pos             (cursor/pos input)
         pos             (cursor/pos input)
-        last-input-char (util/nth-safe (.-value input) (dec pos))
-        last-prev-input-char (util/nth-safe (.-value input) (dec (dec pos)))
-        prev-prev-input-char (util/nth-safe (.-value input) (- pos 3))]
+        content         (.-value input)
+        last-input-char (util/nth-safe content (dec pos))
+        last-prev-input-char (util/nth-safe content (dec (dec pos)))
+        prev-prev-input-char (util/nth-safe content (- pos 3))]
 
 
     ;; TODO: is it cross-browser compatible?
     ;; TODO: is it cross-browser compatible?
     ;; (not= (gobj/get native-e "inputType") "insertFromPaste")
     ;; (not= (gobj/get native-e "inputType") "insertFromPaste")
     (cond
     (cond
+      (and (= content "1. ") (= last-input-char " ") input-id edit-block
+           (not (own-order-number-list? edit-block)))
+      (do
+        (state/pub-event! [:editor/toggle-own-number-list edit-block])
+        (state/set-edit-content! input-id ""))
+
       (and (= last-input-char (state/get-editor-command-trigger))
       (and (= last-input-char (state/get-editor-command-trigger))
            (or (re-find #"(?m)^/" (str (.-value input))) (start-of-new-word? input pos)))
            (or (re-find #"(?m)^/" (str (.-value input))) (start-of-new-word? input pos)))
       (do
       (do
@@ -1985,9 +1902,9 @@
                         :command :block-ref})
                         :command :block-ref})
 
 
       ;; Save it so it'll be parsed correctly in the future
       ;; Save it so it'll be parsed correctly in the future
-      (set-block-property! (:block/uuid chosen)
-                           :id
-                           uuid-string)
+      (editor-property/set-block-property! (:block/uuid chosen)
+                                           :id
+                                           uuid-string)
 
 
       (when-let [input (gdom/getElement id)]
       (when-let [input (gdom/getElement id)]
         (.focus input)))))
         (.focus input)))))
@@ -2045,7 +1962,7 @@
                   keep-uuid?]
                   keep-uuid?]
            :or {exclude-properties []}}]
            :or {exclude-properties []}}]
   (let [editing-block (when-let [editing-block (state/get-edit-block)]
   (let [editing-block (when-let [editing-block (state/get-edit-block)]
-                        (some-> (db/pull (:db/id editing-block))
+                        (some-> (db/pull [:block/uuid (:block/uuid editing-block)])
                                 (assoc :block/content (state/get-edit-content))))
                                 (assoc :block/content (state/get-edit-content))))
         has-unsaved-edits (and editing-block
         has-unsaved-edits (and editing-block
                                (not= (:block/content (db/pull (:db/id editing-block)))
                                (not= (:block/content (db/pull (:db/id editing-block)))
@@ -2450,6 +2367,7 @@
     (let [{:keys [block config]} (get-state)]
     (let [{:keys [block config]} (get-state)]
       (when block
       (when block
         (let [input (state/get-input)
         (let [input (state/get-input)
+              config (assoc config :keydown-new-block true)
               content (gobj/get input "value")
               content (gobj/get input "value")
               pos (cursor/pos input)
               pos (cursor/pos input)
               current-node (outliner-core/block block)
               current-node (outliner-core/block block)
@@ -2492,6 +2410,12 @@
               "list-item" (dwim-in-list)
               "list-item" (dwim-in-list)
               "properties-drawer" (dwim-in-properties state))
               "properties-drawer" (dwim-in-properties state))
 
 
+            (and (string/blank? content)
+                 (own-order-number-list? block)
+                 (not (some-> (db-model/get-block-parent (:block/uuid block))
+                              (own-order-number-list?))))
+            (remove-block-own-order-list-type! block)
+
             (and
             (and
              (string/blank? content)
              (string/blank? content)
              (not has-right?)
              (not has-right?)
@@ -2501,8 +2425,9 @@
             :else
             :else
             (profile
             (profile
              "Insert block"
              "Insert block"
-             (do (save-current-block!)
-                 (insert-new-block! state)))))))))
+             (outliner-tx/transact! {:outliner-op :insert-blocks}
+               (save-current-block!)
+               (insert-new-block! state)))))))))
 
 
 (defn- inside-of-single-block
 (defn- inside-of-single-block
   "When we are in a single block wrapper, we should always insert a new line instead of new block"
   "When we are in a single block wrapper, we should always insert a new line instead of new block"
@@ -2656,15 +2581,18 @@
         ^js input (state/get-input)
         ^js input (state/get-input)
         current-pos (cursor/pos input)
         current-pos (cursor/pos input)
         value (gobj/get input "value")
         value (gobj/get input "value")
-        right (outliner-core/get-right-node (outliner-core/block current-block))
+        right (outliner-core/get-right-sibling (:db/id current-block))
         current-block-has-children? (db/has-children? (:block/uuid current-block))
         current-block-has-children? (db/has-children? (:block/uuid current-block))
         collapsed? (util/collapsed? current-block)
         collapsed? (util/collapsed? current-block)
         first-child (:data (tree/-get-down (outliner-core/block current-block)))
         first-child (:data (tree/-get-down (outliner-core/block current-block)))
         next-block (if (or collapsed? (not current-block-has-children?))
         next-block (if (or collapsed? (not current-block-has-children?))
-                     (:data right)
+                     (when right (db/pull (:db/id right)))
                      first-child)]
                      first-child)]
     (cond
     (cond
-      (and collapsed? right (db/has-children? (tree/-get-id right)))
+      (nil? next-block)
+      nil
+
+      (and collapsed? right (db/has-children? (:block/uuid right)))
       nil
       nil
 
 
       (and (not collapsed?) first-child (db/has-children? (:block/uuid first-child)))
       (and (not collapsed?) first-child (db/has-children? (:block/uuid first-child)))
@@ -2739,7 +2667,11 @@
                    (not root-block?)
                    (not root-block?)
                    (not single-block?)
                    (not single-block?)
                    (not custom-query?))
                    (not custom-query?))
-          (delete-block! repo false)))
+          (if (own-order-number-list? block)
+            (do
+              (save-current-block!)
+              (remove-block-own-order-list-type! block))
+            (delete-block! repo false))))
 
 
       (and (> current-pos 1)
       (and (> current-pos 1)
            (= (util/nth-safe value (dec current-pos)) (state/get-editor-command-trigger)))
            (= (util/nth-safe value (dec current-pos)) (state/get-editor-command-trigger)))
@@ -3159,6 +3091,11 @@
       :else
       :else
       (js/document.execCommand "copy"))))
       (js/document.execCommand "copy"))))
 
 
+(defn whiteboard?
+  []
+  (and (state/whiteboard-route?)
+       (.closest (.-activeElement js/document) ".logseq-tldraw")))
+
 (defn shortcut-cut
 (defn shortcut-cut
   "shortcut cut action:
   "shortcut cut action:
   * when in selection mode, cut selected blocks
   * when in selection mode, cut selected blocks
@@ -3173,13 +3110,23 @@
                            (gdom/getElement (state/get-edit-input-id))))
                            (gdom/getElement (state/get-edit-input-id))))
     (keydown-backspace-handler true e)
     (keydown-backspace-handler true e)
 
 
+    (whiteboard?)
+    (.cut (state/active-tldraw-app))
+
     :else
     :else
     nil))
     nil))
 
 
 (defn delete-selection
 (defn delete-selection
   [e]
   [e]
-  (when (state/selection?)
-    (shortcut-delete-selection e)))
+  (cond
+    (state/selection?)
+    (shortcut-delete-selection e)
+
+    (and (whiteboard?) (not (state/editing?)))
+    (.deleteShapes (.-api ^js (state/active-tldraw-app)))
+
+    :else
+    nil))
 
 
 (defn editor-delete
 (defn editor-delete
   [_state e]
   [_state e]
@@ -3479,6 +3426,10 @@
                        expand-block!)))
                        expand-block!)))
             doall)
             doall)
        (and clear-selection? (clear-selection!)))
        (and clear-selection? (clear-selection!)))
+
+     (whiteboard?)
+     (.setCollapsed (.-api ^js (state/active-tldraw-app)) false)
+
      :else
      :else
      ;; expand one level
      ;; expand one level
      (let [blocks-with-level (all-blocks-with-level {})
      (let [blocks-with-level (all-blocks-with-level {})
@@ -3513,6 +3464,9 @@
             doall)
             doall)
        (and clear-selection? (clear-selection!)))
        (and clear-selection? (clear-selection!)))
 
 
+     (whiteboard?)
+     (.setCollapsed (.-api ^js (state/active-tldraw-app)) true)
+
      :else
      :else
      ;; collapse by one level from outside
      ;; collapse by one level from outside
      (let [blocks-with-level
      (let [blocks-with-level
@@ -3608,6 +3562,11 @@
         edit-block (state/get-edit-block)
         edit-block (state/get-edit-block)
         target-element (.-nodeName (.-target e))]
         target-element (.-nodeName (.-target e))]
     (cond
     (cond
+      (whiteboard?)
+      (do
+        (util/stop e)
+        (.selectAll (.-api ^js (state/active-tldraw-app))))
+
       ;; editing block fully selected
       ;; editing block fully selected
       (and edit-block edit-input
       (and edit-block edit-input
            (= (util/get-selected-text) (.-value edit-input)))
            (= (util/get-selected-text) (.-value edit-input)))

+ 141 - 0
src/main/frontend/handler/editor/property.cljs

@@ -0,0 +1,141 @@
+(ns frontend.handler.editor.property
+  "Property related fns for the editor"
+  (:require [clojure.string :as string]
+            [frontend.config :as config]
+            [frontend.db :as db]
+            [frontend.modules.outliner.core :as outliner-core]
+            [frontend.modules.outliner.transaction :as outliner-tx]
+            [frontend.state :as state]
+            [frontend.util :as util]
+            [frontend.util.drawer :as drawer]
+            [frontend.util.property :as property]
+            [goog.object :as gobj]
+            [logseq.graph-parser.util :as gp-util]))
+
+(defn clear-selection!
+  []
+  (state/clear-selection!))
+
+(defn- get-edit-input-id-with-block-id
+  [block-id]
+  (when-let [first-block (util/get-first-block-by-id block-id)]
+    (string/replace (gobj/get first-block "id")
+                    "ls-block"
+                    "edit-block")))
+
+(defn- text-range-by-lst-fst-line [content [direction pos]]
+  (case direction
+    :up
+    (let [last-new-line (or (string/last-index-of content \newline) -1)
+          end (+ last-new-line pos 1)]
+      (subs content 0 end))
+    :down
+    (-> (string/split-lines content)
+        first
+        (or "")
+        (subs 0 pos))))
+
+(defn edit-block!
+  ([block pos id]
+   (edit-block! block pos id nil))
+  ([block pos id {:keys [custom-content tail-len move-cursor? retry-times]
+                  :or {tail-len 0
+                       move-cursor? true
+                       retry-times 0}
+                  :as opts}]
+   (when-not (> retry-times 2)
+     (when-not config/publishing?
+       (when-let [block-id (:block/uuid block)]
+         (let [block (or (db/pull [:block/uuid block-id]) block)
+               edit-input-id (if (uuid? id)
+                               (get-edit-input-id-with-block-id id)
+                               (-> (str (subs id 0 (- (count id) 36)) block-id)
+                                   (string/replace "ls-block" "edit-block")))
+               content (or custom-content (:block/content block) "")
+               content-length (count content)
+               text-range (cond
+                            (vector? pos)
+                            (text-range-by-lst-fst-line content pos)
+
+                            (and (> tail-len 0) (>= (count content) tail-len))
+                            (subs content 0 (- (count content) tail-len))
+
+                            (or (= :max pos) (<= content-length pos))
+                            content
+
+                            :else
+                            (subs content 0 pos))
+               content (-> (property/remove-built-in-properties (:block/format block)
+                                                                content)
+                           (drawer/remove-logbook))]
+           (clear-selection!)
+           (if edit-input-id
+             (state/set-editing! edit-input-id content block text-range move-cursor?)
+             ;; Block may not be rendered yet
+             (js/setTimeout (fn [] (edit-block! block pos id (update opts :retry-times inc))) 10))))))))
+
+(defn batch-set-block-property!
+  "col: a collection of [block-id property-key property-value]."
+  [col]
+  #_:clj-kondo/ignore
+  (when-let [repo (state/get-current-repo)]
+    (let [col' (group-by first col)]
+      (outliner-tx/transact!
+       {:outliner-op :save-block}
+       (doseq [[block-id items] col']
+         (let [block-id (if (string? block-id) (uuid block-id) block-id)
+               new-properties (zipmap (map second items)
+                                      (map last items))]
+           (when-let [block (db/entity [:block/uuid block-id])]
+             (let [format (:block/format block)
+                   content (:block/content block)
+                   properties (:block/properties block)
+                   properties-text-values (:block/properties-text-values block)
+                   properties (-> (merge properties new-properties)
+                                  gp-util/remove-nils-non-nested)
+                   properties-text-values (-> (merge properties-text-values new-properties)
+                                              gp-util/remove-nils-non-nested)
+                   property-ks (->> (concat (:block/properties-order block)
+                                            (map second items))
+                                    (filter (set (keys properties)))
+                                    distinct
+                                    vec)
+                   content (property/remove-properties format content)
+                   kvs (for [key property-ks] [key (or (get properties-text-values key)
+                                                       (get properties key))])
+                   content (property/insert-properties format content kvs)
+                   content (property/remove-empty-properties content)
+                   block {:block/uuid block-id
+                          :block/properties properties
+                          :block/properties-order property-ks
+                          :block/properties-text-values properties-text-values
+                          :block/content content}]
+               (outliner-core/save-block! block)))))))
+
+    (let [block-id (ffirst col)
+          block-id (if (string? block-id) (uuid block-id) block-id)
+          input-pos (or (state/get-edit-pos) :max)]
+      ;; update editing input content
+      (when-let [editing-block (state/get-edit-block)]
+        (when (= (:block/uuid editing-block) block-id)
+          (edit-block! editing-block
+                       input-pos
+                       (state/get-edit-input-id)))))))
+
+(defn batch-add-block-property!
+  [block-ids property-key property-value]
+  (batch-set-block-property! (map #(vector % property-key property-value) block-ids)))
+
+(defn batch-remove-block-property!
+  [block-ids property-key]
+  (batch-set-block-property! (map #(vector % property-key nil) block-ids)))
+
+(defn remove-block-property!
+  [block-id key]
+  (let [key (keyword key)]
+    (batch-set-block-property! [[block-id key nil]])))
+
+(defn set-block-property!
+  [block-id key value]
+  (let [key (keyword key)]
+    (batch-set-block-property! [[block-id key value]])))

+ 22 - 3
src/main/frontend/handler/events.cljs

@@ -608,10 +608,9 @@
               (plugin/open-waiting-updates-modal!))
               (plugin/open-waiting-updates-modal!))
             (plugin-handler/set-auto-checking! false))))))
             (plugin-handler/set-auto-checking! false))))))
 
 
-(defmethod handle :plugin/hook-db-tx [[_ {:keys [blocks tx-data tx-meta] :as payload}]]
+(defmethod handle :plugin/hook-db-tx [[_ {:keys [blocks tx-data] :as payload}]]
   (when-let [payload (and (seq blocks)
   (when-let [payload (and (seq blocks)
-                          (merge payload {:tx-data (map #(into [] %) tx-data)
-                                          :tx-meta (dissoc tx-meta :editor-cursor)}))]
+                          (merge payload {:tx-data (map #(into [] %) tx-data)}))]
     (plugin-handler/hook-plugin-db :changed payload)
     (plugin-handler/hook-plugin-db :changed payload)
     (plugin-handler/hook-plugin-block-changes payload)))
     (plugin-handler/hook-plugin-block-changes payload)))
 
 
@@ -948,6 +947,26 @@
 (defmethod handle :editor/quick-capture [[_ args]]
 (defmethod handle :editor/quick-capture [[_ args]]
   (quick-capture/quick-capture args))
   (quick-capture/quick-capture args))
 
 
+(defmethod handle :editor/toggle-own-number-list [[_ blocks]]
+  (let [batch? (sequential? blocks)
+        blocks (cond->> blocks
+                  batch?
+                  (map #(cond-> % (or (uuid? %) (string? %)) (db-model/get-block-by-uuid))))]
+    (if (and batch? (> (count blocks) 1))
+      (editor-handler/toggle-blocks-as-own-order-list! blocks)
+      (when-let [block (cond-> blocks batch? (first))]
+        (if (editor-handler/own-order-number-list? block)
+          (editor-handler/remove-block-own-order-list-type! block)
+          (editor-handler/make-block-as-own-order-list! block))))))
+
+(defmethod handle :editor/remove-own-number-list [[_ block]]
+  (when (some-> block (editor-handler/own-order-number-list?))
+    (editor-handler/remove-block-own-order-list-type! block)))
+
+(defmethod handle :editor/toggle-children-number-list [[_ block]]
+  (when-let [blocks (and block (db-model/get-block-immediate-children (state/get-current-repo) (:block/uuid block)))]
+    (editor-handler/toggle-blocks-as-own-order-list! blocks)))
+
 (defn run!
 (defn run!
   []
   []
   (let [chan (state/get-events-chan)]
   (let [chan (state/get-events-chan)]

+ 3 - 1
src/main/frontend/handler/paste.cljs

@@ -84,7 +84,9 @@
 ;; See https://developer.chrome.com/blog/web-custom-formats-for-the-async-clipboard-api/
 ;; See https://developer.chrome.com/blog/web-custom-formats-for-the-async-clipboard-api/
 ;; for a similar example
 ;; for a similar example
 (defn get-copied-blocks []
 (defn get-copied-blocks []
-  (p/let [clipboard-items (when (and js/window (gobj/get js/window "navigator") js/navigator.clipboard)
+  ;; NOTE: Avoid using navigator clipboard API on Android, it will report a permission error
+  (p/let [clipboard-items (when (and (not (mobile-util/native-android?))
+                                     js/window (gobj/get js/window "navigator") js/navigator.clipboard)
                             (js/navigator.clipboard.read))
                             (js/navigator.clipboard.read))
           blocks-blob ^js (when clipboard-items
           blocks-blob ^js (when clipboard-items
                             (let [types (.-types ^js (first clipboard-items))]
                             (let [types (.-types ^js (first clipboard-items))]

+ 73 - 53
src/main/frontend/modules/editor/undo_redo.cljs

@@ -44,7 +44,7 @@
     [txs]
     [txs]
     (filterv (fn [[_ a & y]]
     (filterv (fn [[_ a & y]]
                (= :block/content a))
                (= :block/content a))
-      txs))
+             txs))
 
 
   (defn get-content-from-stack
   (defn get-content-from-stack
     "For test."
     "For test."
@@ -60,22 +60,21 @@
     (when-let [stack @undo-stack]
     (when-let [stack @undo-stack]
       (when (seq stack)
       (when (seq stack)
         (let [removed-e (peek stack)
         (let [removed-e (peek stack)
-              popped-stack (pop stack)
-              prev-e (peek popped-stack)]
+              popped-stack (pop stack)]
           (reset! undo-stack popped-stack)
           (reset! undo-stack popped-stack)
-          [removed-e prev-e])))))
+          removed-e)))))
 
 
 (defn push-redo
 (defn push-redo
   [txs]
   [txs]
   (let [redo-stack (get-redo-stack)]
   (let [redo-stack (get-redo-stack)]
-   (swap! redo-stack conj txs)))
+    (swap! redo-stack conj txs)))
 
 
 (defn pop-redo
 (defn pop-redo
   []
   []
   (let [redo-stack (get-redo-stack)]
   (let [redo-stack (get-redo-stack)]
-   (when-let [removed-e (peek @redo-stack)]
-     (swap! redo-stack pop)
-     removed-e)))
+    (when-let [removed-e (peek @redo-stack)]
+      (swap! redo-stack pop)
+      removed-e)))
 
 
 (defn page-pop-redo
 (defn page-pop-redo
   [page-id]
   [page-id]
@@ -119,7 +118,7 @@
                        (and redo? (not add?)) :db/retract
                        (and redo? (not add?)) :db/retract
                        (and (not redo?) (not add?)) :db/add)]
                        (and (not redo?) (not add?)) :db/add)]
               [op id attr value tx]))
               [op id attr value tx]))
-      txs)))
+          txs)))
 
 
 ;;;; Invokes
 ;;;; Invokes
 
 
@@ -128,7 +127,7 @@
   (let [conn (conn/get-db false)]
   (let [conn (conn/get-db false)]
     (d/transact! conn txs tx-meta)))
     (d/transact! conn txs tx-meta)))
 
 
-(defn page-pop-undo
+(defn- page-pop-undo
   [page-id]
   [page-id]
   (let [undo-stack (get-undo-stack)]
   (let [undo-stack (get-undo-stack)]
     (when-let [stack @undo-stack]
     (when-let [stack @undo-stack]
@@ -144,7 +143,7 @@
                   others (vec (concat before after))]
                   others (vec (concat before after))]
               (reset! undo-stack others)
               (reset! undo-stack others)
               (prn "[debug] undo remove: " (nth stack idx'))
               (prn "[debug] undo remove: " (nth stack idx'))
-              [(nth stack idx') others])))))))
+              (nth stack idx'))))))))
 
 
 (defn- smart-pop-undo
 (defn- smart-pop-undo
   []
   []
@@ -154,56 +153,77 @@
       (pop-undo))
       (pop-undo))
     (pop-undo)))
     (pop-undo)))
 
 
+(defn- set-editor-content!
+  "Prevent block auto-save during undo/redo."
+  []
+  (when-let [block (state/get-edit-block)]
+    (state/set-edit-content! (state/get-edit-input-id)
+                             (:block/content (db/entity (:db/id block))))))
+
+(defn- get-next-tx-editor-cursor
+  [tx-id]
+  (let [result (->> (sort (keys (:history/tx->editor-cursor @state/state)))
+                    (split-with #(not= % tx-id))
+                    second)]
+    (when (> (count result) 1)
+      (when-let [next-tx-id (nth result 1)]
+        (get-in @state/state [:history/tx->editor-cursor next-tx-id])))))
+
+(defn- get-previous-tx-id
+  [tx-id]
+  (let [result (->> (sort (keys (:history/tx->editor-cursor @state/state)))
+                    (split-with #(not= % tx-id))
+                    first)]
+    (when (>= (count result) 1)
+      (last result))))
+
+(defn- get-previous-tx-editor-cursor
+  [tx-id]
+  (when-let [prev-tx-id (get-previous-tx-id tx-id)]
+    (get-in @state/state [:history/tx->editor-cursor prev-tx-id])))
+
 (defn undo
 (defn undo
   []
   []
-  (let [[e prev-e] (smart-pop-undo)]
-    (when e
-      (let [{:keys [txs tx-meta]} e
-            new-txs (get-txs false txs)
-            undo-delete-concat-block? (and (= :delete-block (:outliner-op tx-meta))
-                                           (seq (:concat-data tx-meta)))
-            editor-cursor (cond
-                            undo-delete-concat-block?
-                            (let [data (:concat-data tx-meta)]
-                              (assoc (:editor-cursor e)
-                                     :last-edit-block {:block/uuid (:last-edit-block data)}
-                                     :pos (if (:end? data) :max 0)))
-
-                            ;; same block
-                            (= (get-in e [:editor-cursor :last-edit-block :block/uuid])
-                               (get-in prev-e [:editor-cursor :last-edit-block :block/uuid]))
-                            (:editor-cursor prev-e)
-
-                            :else
-                            (:editor-cursor e))]
-
-        (push-redo e)
-        (transact! new-txs (merge {:undo? true}
-                                  tx-meta
-                                  (select-keys e [:pagination-blocks-range])))
-
-        (when undo-delete-concat-block?
-          (when-let [block (state/get-edit-block)]
-            (state/set-edit-content! (state/get-edit-input-id)
-                                     (:block/content (db/entity (:db/id block))))))
-
-        (when (:whiteboard/transact? tx-meta)
-          (state/pub-event! [:whiteboard/undo e]))
-        (assoc e
-               :txs-op new-txs
-               :editor-cursor editor-cursor)))))
+  (when-let [e (smart-pop-undo)]
+    (let [{:keys [txs tx-meta tx-id]} e
+          new-txs (get-txs false txs)
+          current-editor-cursor (get-in @state/state [:history/tx->editor-cursor tx-id])
+          save-block? (= (:outliner-op tx-meta) :save-block)
+          prev-editor-cursor (get-previous-tx-editor-cursor tx-id)
+          editor-cursor (if (and save-block?
+                                 (= (:block/uuid (:last-edit-block prev-editor-cursor))
+                                    (:block/uuid (state/get-edit-block))))
+                          prev-editor-cursor
+                          current-editor-cursor)]
+      (push-redo e)
+      (transact! new-txs (merge {:undo? true}
+                                tx-meta
+                                (select-keys e [:pagination-blocks-range])))
+      (set-editor-content!)
+      (when (:whiteboard/transact? tx-meta)
+        (state/pub-event! [:whiteboard/undo e]))
+      (assoc e
+             :txs-op new-txs
+             :editor-cursor editor-cursor))))
 
 
 (defn redo
 (defn redo
   []
   []
-  (when-let [{:keys [txs tx-meta] :as e} (smart-pop-redo)]
-    (let [new-txs (get-txs true txs)]
+  (when-let [{:keys [txs tx-meta tx-id] :as e} (smart-pop-redo)]
+    (let [new-txs (get-txs true txs)
+          current-editor-cursor (get-in @state/state [:history/tx->editor-cursor tx-id])
+          editor-cursor (if (= (:outliner-op tx-meta) :save-block)
+                          current-editor-cursor
+                          (get-next-tx-editor-cursor tx-id))]
       (push-undo e)
       (push-undo e)
       (transact! new-txs (merge {:redo? true}
       (transact! new-txs (merge {:redo? true}
                                 tx-meta
                                 tx-meta
                                 (select-keys e [:pagination-blocks-range])))
                                 (select-keys e [:pagination-blocks-range])))
+      (set-editor-content!)
       (when (:whiteboard/transact? tx-meta)
       (when (:whiteboard/transact? tx-meta)
         (state/pub-event! [:whiteboard/redo e]))
         (state/pub-event! [:whiteboard/redo e]))
-      (assoc e :txs-op new-txs))))
+      (assoc e
+             :txs-op new-txs
+             :editor-cursor editor-cursor))))
 
 
 (defn toggle-undo-redo-mode!
 (defn toggle-undo-redo-mode!
   []
   []
@@ -231,14 +251,14 @@
                    #{:block/created-at :block/updated-at})))
                    #{:block/created-at :block/updated-at})))
     (reset-redo)
     (reset-redo)
     (if (:replace? tx-meta)
     (if (:replace? tx-meta)
-      (let [[removed-e _prev-e] (pop-undo)
+      (let [removed-e (pop-undo)
             entity (update removed-e :txs concat tx-data)]
             entity (update removed-e :txs concat tx-data)]
         (push-undo entity))
         (push-undo entity))
       (let [updated-blocks (db-report/get-blocks tx-report)
       (let [updated-blocks (db-report/get-blocks tx-report)
-            entity {:blocks updated-blocks
+            entity {:tx-id (get-in tx-report [:tempids :db/current-tx])
+                    :blocks updated-blocks
                     :txs tx-data
                     :txs tx-data
                     :tx-meta tx-meta
                     :tx-meta tx-meta
-                    :editor-cursor (:editor-cursor tx-meta)
                     :pagination-blocks-range (get-in [:ui/pagination-blocks-range (get-in tx-report [:db-after :max-tx])] @state/state)
                     :pagination-blocks-range (get-in [:ui/pagination-blocks-range (get-in tx-report [:db-after :max-tx])] @state/state)
                     :app-state (select-keys @state/state
                     :app-state (select-keys @state/state
                                             [:route-match
                                             [:route-match

+ 59 - 34
src/main/frontend/modules/outliner/core.cljs

@@ -12,6 +12,7 @@
             [frontend.modules.outliner.utils :as outliner-u]
             [frontend.modules.outliner.utils :as outliner-u]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util :as util]
+            [frontend.util.property :as property]
             [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser.util :as gp-util]
             [cljs.spec.alpha :as s]))
             [cljs.spec.alpha :as s]))
 
 
@@ -25,8 +26,14 @@
 
 
 (defn block
 (defn block
   [m]
   [m]
-  (assert (map? m) (util/format "block data must be map, got: %s %s" (type m) m))
-  (->Block m))
+  (assert (or (map? m) (de/entity? m)) (util/format "block data must be map or entity, got: %s %s" (type m) m))
+  (if (de/entity? m)
+    (->Block {:db/id (:db/id m)
+              :block/uuid (:block/uuid m)
+              :block/page (:block/page m)
+              :block/left (:block/left m)
+              :block/parent (:block/parent m)})
+    (->Block m)))
 
 
 (defn get-data
 (defn get-data
   [block]
   [block]
@@ -140,42 +147,49 @@
           m (if (state/enable-block-timestamps?) (block-with-timestamps m) m)
           m (if (state/enable-block-timestamps?) (block-with-timestamps m) m)
           other-tx (:db/other-tx m)
           other-tx (:db/other-tx m)
           id (:db/id (:data this))
           id (:db/id (:data this))
-          block-entity (db/entity id)
-          remove-self-page #(remove (fn [b]
-                                      (= (:db/id b) (:db/id (:block/page block-entity)))) %)
-          old-refs (remove-self-page (:block/refs block-entity))
-          new-refs (remove-self-page (:block/refs m))]
+          block-entity (db/entity id)]
       (when (seq other-tx)
       (when (seq other-tx)
         (swap! txs-state (fn [txs]
         (swap! txs-state (fn [txs]
                            (vec (concat txs other-tx)))))
                            (vec (concat txs other-tx)))))
 
 
       (when id
       (when id
+        ;; Retract attributes to prepare for tx which rewrites block attributes
         (swap! txs-state (fn [txs]
         (swap! txs-state (fn [txs]
                            (vec
                            (vec
                             (concat txs
                             (concat txs
                                     (map (fn [attribute]
                                     (map (fn [attribute]
                                            [:db/retract id attribute])
                                            [:db/retract id attribute])
-                                      db-schema/retract-attributes)))))
+                                         db-schema/retract-attributes)))))
 
 
+        ;; Update block's page attributes
         (when-let [e (:block/page block-entity)]
         (when-let [e (:block/page block-entity)]
-          (let [m' {:db/id (:db/id e)
-                   :block/updated-at (util/time-ms)}
-                m' (if (:block/created-at e)
-                    m'
-                    (assoc m' :block/created-at (util/time-ms)))
-                m' (if (or (:block/pre-block? block-entity)
-                           (:block/pre-block? m))
-                     (let [properties (:block/properties m)
-                           alias (set (:alias properties))
-                           tags (set (:tags properties))
-                           alias (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) alias)
-                           tags (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) tags)]
-                       (assoc m'
-                              :block/alias alias
-                              :block/tags tags
-                              :block/properties properties))
-                     m')]
-            (swap! txs-state conj m'))
+          (let [m' (cond-> {:db/id (:db/id e)
+                            :block/updated-at (util/time-ms)}
+                     (not (:block/created-at e))
+                     (assoc :block/created-at (util/time-ms)))
+                txs (if (or (:block/pre-block? block-entity)
+                            (:block/pre-block? m))
+                      (let [properties (:block/properties m)
+                            alias (set (:alias properties))
+                            tags (set (:tags properties))
+                            alias (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) alias)
+                            tags (map (fn [p] {:block/name (util/page-name-sanity-lc p)}) tags)
+                            deleteable-page-attributes {:block/alias alias
+                                                        :block/tags tags
+                                                        :block/properties properties
+                                                        :block/properties-text-values (:block/properties-text-values m)}
+                            ;; Retract page attributes to allow for deletion of page attributes
+                            page-retractions
+                            (mapv #(vector :db/retract (:db/id e) %) (keys deleteable-page-attributes))]
+                        (conj page-retractions (merge m' deleteable-page-attributes)))
+                      [m'])]
+            (swap! txs-state into txs)))
+
+        ;; Remove orphaned refs from block
+        (let [remove-self-page #(remove (fn [b]
+                                          (= (:db/id b) (:db/id (:block/page block-entity)))) %)
+              old-refs (remove-self-page (:block/refs block-entity))
+              new-refs (remove-self-page (:block/refs m))]
           (remove-orphaned-page-refs! (:db/id block-entity) txs-state old-refs new-refs)))
           (remove-orphaned-page-refs! (:db/id block-entity) txs-state old-refs new-refs)))
 
 
       (swap! txs-state conj (dissoc m :db/other-tx))
       (swap! txs-state conj (dissoc m :db/other-tx))
@@ -223,11 +237,6 @@
           children (db-model/get-block-immediate-children (state/get-current-repo) parent-id)]
           children (db-model/get-block-immediate-children (state/get-current-repo) parent-id)]
       (map block children))))
       (map block children))))
 
 
-(defn get-right-node
-  [node]
-  {:pre [(tree/satisfied-inode? node)]}
-  (tree/-get-right node))
-
 (defn get-right-sibling
 (defn get-right-sibling
   [db-id]
   [db-id]
   (when db-id
   (when db-id
@@ -399,7 +408,7 @@
   (let [level-blocks (blocks-with-level blocks)]
   (let [level-blocks (blocks-with-level blocks)]
     (filter (fn [b] (= 1 (:block/level b))) level-blocks)))
     (filter (fn [b] (= 1 (:block/level b))) level-blocks)))
 
 
-(defn get-right-siblings
+(defn- get-right-siblings
   "Get `node`'s right siblings."
   "Get `node`'s right siblings."
   [node]
   [node]
   {:pre [(tree/satisfied-inode? node)]}
   {:pre [(tree/satisfied-inode? node)]}
@@ -409,6 +418,21 @@
            last
            last
            rest))))
            rest))))
 
 
+(defn blocks-with-ordered-list-props
+  [blocks target-block sibling?]
+  (let [target-block (if sibling? target-block (some-> target-block :db/id db/pull block tree/-get-down :data))]
+    (letfn [(list-type-fn [b] (some-> b :block/properties :logseq.order-list-type))]
+      (if-let [list-type (and target-block (list-type-fn target-block))]
+        (mapv
+          (fn [{:block/keys [content format] :as block}]
+            (cond-> block
+              (and (some? (:block/uuid block))
+                   (nil? (list-type-fn block)))
+              (-> (update :block/properties #(assoc % :logseq.order-list-type list-type))
+                  (assoc :block/content (property/insert-property format content :logseq.order-list-type list-type)))))
+          blocks)
+        blocks))))
+
 ;;; ### insert-blocks, delete-blocks, move-blocks
 ;;; ### insert-blocks, delete-blocks, move-blocks
 
 
 (defn fix-top-level-blocks
 (defn fix-top-level-blocks
@@ -454,7 +478,7 @@
                         (:db/id target-block))
                         (:db/id target-block))
         get-new-id (fn [block lookup]
         get-new-id (fn [block lookup]
                      (cond
                      (cond
-                       (or (map? lookup) (vector? lookup))
+                       (or (map? lookup) (vector? lookup) (de/entity? lookup))
                        (when-let [uuid (if (and (vector? lookup) (= (first lookup) :block/uuid))
                        (when-let [uuid (if (and (vector? lookup) (= (first lookup) :block/uuid))
                                          (get uuids (last lookup))
                                          (get uuids (last lookup))
                                          (get id->new-uuid (:db/id lookup)))]
                                          (get id->new-uuid (:db/id lookup)))]
@@ -519,6 +543,7 @@
                                      (> (count blocks) 1)
                                      (> (count blocks) 1)
                                      (not move?)))
                                      (not move?)))
         blocks' (blocks-with-level blocks)
         blocks' (blocks-with-level blocks)
+        blocks' (blocks-with-ordered-list-props blocks' target-block sibling?)
         blocks' (if (= outliner-op :paste)
         blocks' (if (= outliner-op :paste)
                   (fix-top-level-blocks blocks')
                   (fix-top-level-blocks blocks')
                   blocks')
                   blocks')
@@ -560,7 +585,7 @@
         (when (and replace-empty-target? (state/editing?))
         (when (and replace-empty-target? (state/editing?))
           (state/set-edit-content! (state/get-edit-input-id) (:block/content (first blocks))))
           (state/set-edit-content! (state/get-edit-input-id) (:block/content (first blocks))))
         {:tx-data full-tx
         {:tx-data full-tx
-         :blocks tx}))))
+         :blocks  tx}))))
 
 
 (defn- build-move-blocks-next-tx
 (defn- build-move-blocks-next-tx
   [blocks non-consecutive-blocks?]
   [blocks non-consecutive-blocks?]

+ 9 - 4
src/main/frontend/modules/outliner/datascript.cljc

@@ -45,9 +45,14 @@
                                           v)))
                                           v)))
                        x))))))
                        x))))))
 
 
+#?(:cljs
+   (defn get-tx-id
+     [tx-report]
+     (get-in tx-report [:tempids :db/current-tx])))
+
 #?(:cljs
 #?(:cljs
    (defn transact!
    (defn transact!
-     [txs opts]
+     [txs opts before-editor-cursor]
      (let [txs (remove-nil-from-transaction txs)
      (let [txs (remove-nil-from-transaction txs)
            txs (map (fn [m] (if (map? m)
            txs (map (fn [m] (if (map? m)
                               (dissoc m
                               (dissoc m
@@ -65,9 +70,9 @@
          (try
          (try
            (let [repo (get opts :repo (state/get-current-repo))
            (let [repo (get opts :repo (state/get-current-repo))
                  conn (conn/get-db repo false)
                  conn (conn/get-db repo false)
-                 editor-cursor (state/get-current-edit-block-and-position)
-                 meta (merge opts {:editor-cursor editor-cursor})
-                 rs (d/transact! conn txs (assoc meta :outliner/transact? true))]
+                 rs (d/transact! conn txs (assoc opts :outliner/transact? true))
+                 tx-id (get-tx-id rs)]
+             (swap! state/state assoc-in [:history/tx->editor-cursor tx-id] before-editor-cursor)
              (when true                 ; TODO: add debug flag
              (when true                 ; TODO: add debug flag
                (let [eids (distinct (mapv first (:tx-data rs)))
                (let [eids (distinct (mapv first (:tx-data rs)))
                      left&parent-list (->>
                      left&parent-list (->>

+ 33 - 9
src/main/frontend/modules/outliner/pipeline.cljs

@@ -6,7 +6,8 @@
             [frontend.db.model :as db-model]
             [frontend.db.model :as db-model]
             [frontend.db.react :as react]
             [frontend.db.react :as react]
             [frontend.db :as db]
             [frontend.db :as db]
-            [clojure.set :as set]))
+            [clojure.set :as set]
+            [datascript.core :as d]))
 
 
 (defn updated-page-hook
 (defn updated-page-hook
   [tx-report page]
   [tx-report page]
@@ -24,7 +25,7 @@
 ;; 1. For each changed block, new-refs = its page + :block/refs + parents :block/refs
 ;; 1. For each changed block, new-refs = its page + :block/refs + parents :block/refs
 ;; 2. Its children' block/path-refs might need to be updated too.
 ;; 2. Its children' block/path-refs might need to be updated too.
 (defn compute-block-path-refs
 (defn compute-block-path-refs
-  [{:keys [tx-meta]} blocks]
+  [{:keys [tx-meta db-before]} blocks]
   (let [repo (state/get-current-repo)
   (let [repo (state/get-current-repo)
         blocks (remove :block/name blocks)]
         blocks (remove :block/name blocks)]
     (when (:outliner-op tx-meta)
     (when (:outliner-op tx-meta)
@@ -36,19 +37,42 @@
                       (let [parents (db-model/get-block-parents repo (:block/uuid block))
                       (let [parents (db-model/get-block-parents repo (:block/uuid block))
                             parents-refs (->> (mapcat :block/path-refs parents)
                             parents-refs (->> (mapcat :block/path-refs parents)
                                               (map :db/id))
                                               (map :db/id))
-                            old-refs (set (map :db/id (:block/path-refs block)))
+                            old-refs (if db-before
+                                       (set (map :db/id (:block/path-refs (d/entity db-before (:db/id block)))))
+                                       #{})
                             new-refs (set (util/concat-without-nil
                             new-refs (set (util/concat-without-nil
                                            [(:db/id (:block/page block))]
                                            [(:db/id (:block/page block))]
                                            (map :db/id (:block/refs block))
                                            (map :db/id (:block/refs block))
                                            parents-refs))
                                            parents-refs))
                             refs-changed? (not= old-refs new-refs)
                             refs-changed? (not= old-refs new-refs)
                             children (db-model/get-block-children-ids repo (:block/uuid block))
                             children (db-model/get-block-children-ids repo (:block/uuid block))
-                            children-refs (map (fn [id]
-                                                 (let [entity (db/entity [:block/uuid id])]
-                                                   {:db/id (:db/id entity)
-                                                    :block/path-refs (concat
-                                                                      (map :db/id (:block/path-refs entity))
-                                                                      new-refs)})) children)]
+                            ;; Builds map of children ids to their parent id and :block/refs ids
+                            children-maps (into {}
+                                                (map (fn [id]
+                                                       (let [entity (db/entity [:block/uuid id])]
+                                                         [(:db/id entity)
+                                                          {:parent-id (get-in entity [:block/parent :db/id])
+                                                           :block-ref-ids (map :db/id (:block/refs entity))}]))
+                                                     children))
+                            children-refs (map (fn [[id {:keys [block-ref-ids] :as child-map}]]
+                                                 {:db/id id
+                                                  ;; Recalculate :block/path-refs as db contains stale data for this attribute
+                                                  :block/path-refs
+                                                  (set/union
+                                                   ;; Refs from top-level parent
+                                                   new-refs
+                                                   ;; Refs from current block
+                                                   block-ref-ids
+                                                   ;; Refs from parents in between top-level
+                                                   ;; parent and current block
+                                                   (loop [parent-refs #{}
+                                                          parent-id (:parent-id child-map)]
+                                                     (if-let [parent (children-maps parent-id)]
+                                                       (recur (into parent-refs (:block-ref-ids parent))
+                                                              (:parent-id parent))
+                                                       ;; exits when top-level parent is reached
+                                                       parent-refs)))})
+                                               children-maps)]
                         (swap! *computed-ids set/union (set (cons (:block/uuid block) children)))
                         (swap! *computed-ids set/union (set (cons (:block/uuid block) children)))
                         (util/concat-without-nil
                         (util/concat-without-nil
                          [(when (and (seq new-refs)
                          [(when (and (seq new-refs)

+ 3 - 2
src/main/frontend/modules/outliner/transaction.cljc

@@ -27,7 +27,8 @@
   `(let [transact-data# frontend.modules.outliner.core/*transaction-data*
   `(let [transact-data# frontend.modules.outliner.core/*transaction-data*
          opts# (if transact-data#
          opts# (if transact-data#
                  (assoc ~opts :nested-transaction? true)
                  (assoc ~opts :nested-transaction? true)
-                 ~opts)]
+                 ~opts)
+         before-editor-cursor# (frontend.state/get-current-edit-block-and-position)]
      (if transact-data#
      (if transact-data#
        (do ~@body)
        (do ~@body)
        (binding [frontend.modules.outliner.core/*transaction-data* (transient [])]
        (binding [frontend.modules.outliner.core/*transaction-data* (transient [])]
@@ -40,7 +41,7 @@
                opts## (merge (dissoc opts# :additional-tx) tx-meta#)]
                opts## (merge (dissoc opts# :additional-tx) tx-meta#)]
            (when (seq all-tx#) ;; If it's empty, do nothing
            (when (seq all-tx#) ;; If it's empty, do nothing
              (when-not (:nested-transaction? opts#) ; transact only for the whole transaction
              (when-not (:nested-transaction? opts#) ; transact only for the whole transaction
-               (let [result# (frontend.modules.outliner.datascript/transact! all-tx# opts##)]
+               (let [result# (frontend.modules.outliner.datascript/transact! all-tx# opts## before-editor-cursor#)]
                  {:tx-report result#
                  {:tx-report result#
                   :tx-data all-tx#
                   :tx-data all-tx#
                   :tx-meta tx-meta#}))))))))
                   :tx-meta tx-meta#}))))))))

+ 135 - 6
src/main/frontend/modules/shortcut/config.cljs

@@ -71,6 +71,78 @@
    :pdf/find                     {:binding "alt+f"
    :pdf/find                     {:binding "alt+f"
                                   :fn      pdf-utils/open-finder}
                                   :fn      pdf-utils/open-finder}
 
 
+   :whiteboard/select            {:binding ["1" "w s"]
+                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "select")}
+   
+   :whiteboard/pan               {:binding ["2" "w p"]
+                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "move")}
+   
+   :whiteboard/portal            {:binding ["3" "w b"]
+                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "logseq-portal")}
+
+   :whiteboard/pencil            {:binding ["4" "w d"]
+                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "pencil")}
+
+   :whiteboard/highlighter       {:binding ["5" "w h"]
+                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "highlighter")}
+   
+   :whiteboard/eraser            {:binding ["6" "w e"]
+                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "erase")}
+   
+   :whiteboard/connector         {:binding ["7" "w c"]
+                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "line")}
+   
+   :whiteboard/text              {:binding ["8" "w t"]
+                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "text")}
+
+   :whiteboard/rectangle         {:binding ["9" "w r"]
+                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "box")}
+
+   :whiteboard/ellipse           {:binding ["o" "w o"]
+                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "ellipse")}
+
+   :whiteboard/reset-zoom        {:binding "shift+0"
+                                  :fn      #(.resetZoom (.-api ^js (state/active-tldraw-app)))}
+
+   :whiteboard/zoom-to-fit       {:binding "shift+1"
+                                  :fn      #(.zoomToFit (.-api ^js (state/active-tldraw-app)))}
+
+   :whiteboard/zoom-to-selection {:binding "shift+2"
+                                  :fn      #(.zoomToSelection (.-api ^js (state/active-tldraw-app)))}
+
+   :whiteboard/zoom-out          {:binding "shift+dash"
+                                  :fn      #(.zoomOut (.-api ^js (state/active-tldraw-app)) false)}
+
+   :whiteboard/zoom-in           {:binding "shift+="
+                                  :fn      #(.zoomIn (.-api ^js (state/active-tldraw-app)) false)}
+
+   :whiteboard/send-backward     {:binding "["
+                                  :fn      #(.sendBackward ^js (state/active-tldraw-app))}
+
+   :whiteboard/send-to-back      {:binding "shift+["
+                                  :fn      #(.sendToBack ^js (state/active-tldraw-app))}
+
+   :whiteboard/bring-forward     {:binding "]"
+                                  :fn      #(.bringForward ^js (state/active-tldraw-app))}
+
+   :whiteboard/bring-to-front    {:binding "shift+]"
+                                  :fn      #(.bringToFront ^js (state/active-tldraw-app))}
+
+   :whiteboard/lock              {:binding "mod+l"
+                                  :fn      #(.setLocked ^js (state/active-tldraw-app) true)}
+
+   :whiteboard/unlock            {:binding "mod+shift+l"
+                                  :fn      #(.setLocked ^js (state/active-tldraw-app) false)}
+
+   :whiteboard/group             {:binding "mod+g"
+                                  :fn      #(.doGroup (.-api ^js (state/active-tldraw-app)))}
+
+   :whiteboard/ungroup           {:binding "mod+shift+g"
+                                  :fn      #(.unGroup (.-api ^js (state/active-tldraw-app)))}
+
+   :whiteboard/toggle-grid       {:binding "t g"
+                                  :fn      #(.toggleGrid (.-api ^js (state/active-tldraw-app)))}
+
    :auto-complete/complete       {:binding "enter"
    :auto-complete/complete       {:binding "enter"
                                   :fn      ui-handler/auto-complete-complete}
                                   :fn      ui-handler/auto-complete-complete}
 
 
@@ -259,6 +331,9 @@
                                     :fn      editor-handler/zoom-out!}
                                     :fn      editor-handler/zoom-out!}
 
 
    :editor/toggle-undo-redo-mode   {:fn      undo-redo/toggle-undo-redo-mode!}
    :editor/toggle-undo-redo-mode   {:fn      undo-redo/toggle-undo-redo-mode!}
+   
+   :editor/toggle-number-list      {:binding "t n"
+                                    :fn #(state/pub-event! [:editor/toggle-own-number-list (state/get-selection-block-ids)])}
 
 
    :ui/toggle-brackets             {:binding "mod+c mod+b"
    :ui/toggle-brackets             {:binding "mod+c mod+b"
                                     :fn      config-handler/toggle-ui-show-brackets!}
                                     :fn      config-handler/toggle-ui-show-brackets!}
@@ -338,7 +413,7 @@
 
 
    :graph/re-index                 {:fn (fn []
    :graph/re-index                 {:fn (fn []
                                           (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
                                           (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
-                                                 (state/pub-event! [:graph/ask-for-re-index (atom multiple-windows?) nil])))
+                                            (state/pub-event! [:graph/ask-for-re-index (atom multiple-windows?) nil])))
                                     :binding false}
                                     :binding false}
 
 
    :command/run                    {:binding "mod+shift+1"
    :command/run                    {:binding "mod+shift+1"
@@ -348,7 +423,7 @@
                                                 (state/pub-event! [:command/run]))}
                                                 (state/pub-event! [:command/run]))}
 
 
    :go/home                        {:binding "g h"
    :go/home                        {:binding "g h"
-                                    :fn      route-handler/redirect-to-home!}
+                                    :fn      #(route-handler/redirect-to-home!)}
 
 
    :go/all-pages                   {:binding "g a"
    :go/all-pages                   {:binding "g a"
                                     :fn      route-handler/redirect-to-all-pages!}
                                     :fn      route-handler/redirect-to-all-pages!}
@@ -418,7 +493,7 @@
 
 
    :editor/copy-page-url            {:binding false
    :editor/copy-page-url            {:binding false
                                      :inactive (not (util/electron?))
                                      :inactive (not (util/electron?))
-                                     :fn      page-handler/copy-page-url}
+                                     :fn      #(page-handler/copy-page-url)}
 
 
    :ui/toggle-wide-mode             {:binding "t w"
    :ui/toggle-wide-mode             {:binding "t w"
                                      :fn      ui-handler/toggle-wide-mode!}
                                      :fn      ui-handler/toggle-wide-mode!}
@@ -507,6 +582,33 @@
                              :pdf/find])
                              :pdf/find])
         (with-meta {:before m/enable-when-not-editing-mode!}))
         (with-meta {:before m/enable-when-not-editing-mode!}))
 
 
+    :shortcut.handler/whiteboard
+    (-> (build-category-map [:whiteboard/select
+                             :whiteboard/pan
+                             :whiteboard/portal
+                             :whiteboard/pencil
+                             :whiteboard/highlighter
+                             :whiteboard/eraser
+                             :whiteboard/connector
+                             :whiteboard/text
+                             :whiteboard/rectangle
+                             :whiteboard/ellipse
+                             :whiteboard/reset-zoom
+                             :whiteboard/zoom-to-fit
+                             :whiteboard/zoom-to-selection
+                             :whiteboard/zoom-out
+                             :whiteboard/zoom-in
+                             :whiteboard/send-backward
+                             :whiteboard/send-to-back
+                             :whiteboard/bring-forward
+                             :whiteboard/bring-to-front
+                             :whiteboard/lock
+                             :whiteboard/unlock
+                             :whiteboard/group
+                             :whiteboard/ungroup
+                             :whiteboard/toggle-grid])
+        (with-meta {:before m/enable-when-not-editing-mode!}))
+
     :shortcut.handler/auto-complete
     :shortcut.handler/auto-complete
     (build-category-map [:auto-complete/complete
     (build-category-map [:auto-complete/complete
                          :auto-complete/prev
                          :auto-complete/prev
@@ -552,8 +654,7 @@
 
 
     :shortcut.handler/editor-global
     :shortcut.handler/editor-global
     (->
     (->
-     (build-category-map [
-                          :graph/export-as-html
+     (build-category-map [:graph/export-as-html
                           :graph/open
                           :graph/open
                           :graph/remove
                           :graph/remove
                           :graph/add
                           :graph/add
@@ -590,6 +691,7 @@
                           :editor/zoom-in
                           :editor/zoom-in
                           :editor/zoom-out
                           :editor/zoom-out
                           :editor/toggle-undo-redo-mode
                           :editor/toggle-undo-redo-mode
+                          :editor/toggle-number-list
                           :editor/undo
                           :editor/undo
                           :editor/redo
                           :editor/redo
                           :ui/toggle-brackets
                           :ui/toggle-brackets
@@ -749,6 +851,7 @@
    [:ui/toggle-help
    [:ui/toggle-help
     :editor/toggle-open-blocks
     :editor/toggle-open-blocks
     :editor/toggle-undo-redo-mode
     :editor/toggle-undo-redo-mode
+    :editor/toggle-number-list
     :ui/toggle-wide-mode
     :ui/toggle-wide-mode
     :ui/toggle-cards
     :ui/toggle-cards
     :ui/toggle-document-mode
     :ui/toggle-document-mode
@@ -759,6 +862,33 @@
     :ui/toggle-settings
     :ui/toggle-settings
     :ui/toggle-contents]
     :ui/toggle-contents]
 
 
+   :shortcut.category/whiteboard
+   [:editor/new-whiteboard
+    :whiteboard/select
+    :whiteboard/pan
+    :whiteboard/portal
+    :whiteboard/pencil
+    :whiteboard/highlighter
+    :whiteboard/eraser
+    :whiteboard/connector
+    :whiteboard/text
+    :whiteboard/rectangle
+    :whiteboard/ellipse
+    :whiteboard/reset-zoom
+    :whiteboard/zoom-to-fit
+    :whiteboard/zoom-to-selection
+    :whiteboard/zoom-out
+    :whiteboard/zoom-in
+    :whiteboard/send-backward
+    :whiteboard/send-to-back
+    :whiteboard/bring-forward
+    :whiteboard/bring-to-front
+    :whiteboard/lock
+    :whiteboard/unlock
+    :whiteboard/group
+    :whiteboard/ungroup
+    :whiteboard/toggle-grid]
+   
    :shortcut.category/others
    :shortcut.category/others
    [:pdf/previous-page
    [:pdf/previous-page
     :pdf/next-page
     :pdf/next-page
@@ -781,7 +911,6 @@
     :editor/open-file-in-default-app
     :editor/open-file-in-default-app
     :editor/open-file-in-directory
     :editor/open-file-in-directory
     :editor/copy-page-url
     :editor/copy-page-url
-    :editor/new-whiteboard
     :auto-complete/prev
     :auto-complete/prev
     :auto-complete/next
     :auto-complete/next
     :auto-complete/complete
     :auto-complete/complete

+ 93 - 1
src/main/frontend/modules/shortcut/dicts.cljc

@@ -79,6 +79,31 @@
    :editor/zoom-in                 "Zoom in editing block / Forwards otherwise"
    :editor/zoom-in                 "Zoom in editing block / Forwards otherwise"
    :editor/zoom-out                "Zoom out editing block / Backwards otherwise"
    :editor/zoom-out                "Zoom out editing block / Backwards otherwise"
    :editor/toggle-undo-redo-mode   "Toggle undo redo mode (global or page only)"
    :editor/toggle-undo-redo-mode   "Toggle undo redo mode (global or page only)"
+   :editor/toggle-number-list      "Toggle number list"
+   :whiteboard/select              "Select tool"
+   :whiteboard/pan                 "Pan tool"
+   :whiteboard/portal              "Portal tool"
+   :whiteboard/pencil              "Pencil tool"
+   :whiteboard/highlighter         "Highlighter tool"
+   :whiteboard/eraser              "Eraser tool"
+   :whiteboard/connector           "Connector tool"
+   :whiteboard/text                "Text tool"
+   :whiteboard/rectangle           "Rectangle tool"
+   :whiteboard/ellipse             "Ellipse tool"
+   :whiteboard/reset-zoom          "Reset zoom"
+   :whiteboard/zoom-to-fit         "Zoom to drawing"
+   :whiteboard/zoom-to-selection   "Zoom to fit selection"
+   :whiteboard/zoom-out            "Zoom out"
+   :whiteboard/zoom-in             "Zoom in"
+   :whiteboard/send-backward       "Move backward"
+   :whiteboard/send-to-back        "Move to back"
+   :whiteboard/bring-forward       "Move forward"
+   :whiteboard/bring-to-front      "Move to front"
+   :whiteboard/lock                "Lock selection"
+   :whiteboard/unlock              "Unlock selection"
+   :whiteboard/group               "Group selection"
+   :whiteboard/ungroup             "Ungroup selection"
+   :whiteboard/toggle-grid         "Toggle the canvas grid"
    :ui/toggle-brackets             "Toggle whether to display brackets"
    :ui/toggle-brackets             "Toggle whether to display brackets"
    :go/search-in-page              "Search blocks in the current page"
    :go/search-in-page              "Search blocks in the current page"
    :go/electron-find-in-page       "Find text in page"
    :go/electron-find-in-page       "Find text in page"
@@ -145,6 +170,7 @@
    :shortcut.category/block-command-editing "Block command editing"
    :shortcut.category/block-command-editing "Block command editing"
    :shortcut.category/block-selection "Block selection (press Esc to quit selection)"
    :shortcut.category/block-selection "Block selection (press Esc to quit selection)"
    :shortcut.category/toggle "Toggle"
    :shortcut.category/toggle "Toggle"
+   :shortcut.category/whiteboard "Whiteboard"
    :shortcut.category/others "Others"})
    :shortcut.category/others "Others"})
 
 
 (def ^:large-vars/data-var dicts
 (def ^:large-vars/data-var dicts
@@ -1109,7 +1135,47 @@
              :command.ui/goto-plugins "Gå til dashbord for utvidelser"
              :command.ui/goto-plugins "Gå til dashbord for utvidelser"
              ;;  :command.ui/open-new-window "Åpne et nytt vindu"
              ;;  :command.ui/open-new-window "Åpne et nytt vindu"
              :command.ui/select-theme-color "Velg tilgjengelige temafarger"
              :command.ui/select-theme-color "Velg tilgjengelige temafarger"
-             :command.ui/toggle-cards "Veksle kort"}
+             :command.ui/toggle-cards "Veksle kort"
+             :command.dev/show-block-ast "(Dev) Vis blokk AST"
+             :command.dev/show-block-data "(Dev) Vis blokk data"
+             :command.dev/show-page-ast "(Dev) Vis side AST"
+             :command.dev/show-page-data "(Dev) Vis side data"
+             :command.editor/copy-page-url "Kopier side url"
+             :command.editor/new-whiteboard "Nytt whiteboard"
+             :command.editor/select-parent "Velg overordnet blokk"
+             :command.editor/toggle-number-list "Veksle nummerliste"
+             :command.editor/toggle-undo-redo-mode "Veksle angremodus (global eller kun side)"
+             :command.go/whiteboards "Gå til whiteboards"
+             :command.graph/export-as-html "Eksporter offentlig graf som html"
+             :command.pdf/find "Pdf: Søk tekst i nåværende pdf doc"
+             :command.sidebar/close-top "Lukker øverste objekt i høyre sidestolpe"
+             :command.ui/clear-all-notifications "Fjern alle varsler"
+             :command.ui/install-plugins-from-file "Installer plugins fra plugins.edn"
+             :command.whiteboard/bring-forward "Flytt fremover"
+             :command.whiteboard/bring-to-front "Flytt fremst"
+             :command.whiteboard/connector "Koblingsverktøy"
+             :command.whiteboard/ellipse "Ellipseverktøy"
+             :command.whiteboard/eraser "Sletteverktøy"
+             :command.whiteboard/group "Velg gruppe"
+             :command.whiteboard/highlighter "Merkepenn"
+             :command.whiteboard/lock "Lås seleksjon"
+             :command.whiteboard/pan "Panoreringsverktøy"
+             :command.whiteboard/pencil "Blyantverktøy"
+             :command.whiteboard/portal "Portalverktøy"
+             :command.whiteboard/rectangle "Rektangelverktøy"
+             :command.whiteboard/reset-zoom "Tilbakestill zoom"
+             :command.whiteboard/select "Valg-verktøy"
+             :command.whiteboard/send-backward "Flytt bakover"
+             :command.whiteboard/send-to-back "Flytt bakerst"
+             :command.whiteboard/text "Tekst-verktøy"
+             :command.whiteboard/toggle-grid "Veksle rutenett på lerretet"
+             :command.whiteboard/ungroup "Del opp gruppe"
+             :command.whiteboard/unlock "Lås opp seleksjon"
+             :command.whiteboard/zoom-in "Zoom inn"
+             :command.whiteboard/zoom-out "Zoom ut"
+             :command.whiteboard/zoom-to-fit "Zoom til tegning"
+             :command.whiteboard/zoom-to-selection "Zoom for å passe seleksjonen"
+             :shortcut.category/whiteboard "Whiteboard"}
 
 
    :pt-PT   {:shortcut.category/formatting            "Formatação"
    :pt-PT   {:shortcut.category/formatting            "Formatação"
              :shortcut.category/basics                "Básico"
              :shortcut.category/basics                "Básico"
@@ -1629,6 +1695,7 @@
              :shortcut.category/block-command-editing "Blok düzenleme komutuları"
              :shortcut.category/block-command-editing "Blok düzenleme komutuları"
              :shortcut.category/block-selection "Blok seçimi (seçimden çıkmak için Esc tuşuna basın)"
              :shortcut.category/block-selection "Blok seçimi (seçimden çıkmak için Esc tuşuna basın)"
              :shortcut.category/toggle "Aç/Kapat"
              :shortcut.category/toggle "Aç/Kapat"
+             :shortcut.category/whiteboard "Beyaz tahta"
              :shortcut.category/others "Diğer"
              :shortcut.category/others "Diğer"
              :command.date-picker/complete         "Tarih seçici: Seçilen günü seç"
              :command.date-picker/complete         "Tarih seçici: Seçilen günü seç"
              :command.date-picker/prev-day         "Tarih seçici: Önceki günü seç"
              :command.date-picker/prev-day         "Tarih seçici: Önceki günü seç"
@@ -1701,6 +1768,31 @@
              :command.editor/zoom-in                 "Düzenlenen bloğu yakınlaştır / Aksi takdirde ileri git"
              :command.editor/zoom-in                 "Düzenlenen bloğu yakınlaştır / Aksi takdirde ileri git"
              :command.editor/zoom-out                "Düzenlenen bloğu uzaklaştır / Aksi takdirde geri git"
              :command.editor/zoom-out                "Düzenlenen bloğu uzaklaştır / Aksi takdirde geri git"
              :command.editor/toggle-undo-redo-mode   "Geri alma / yineleme modunu değiştir (yalnızca sayfa veya genel)"
              :command.editor/toggle-undo-redo-mode   "Geri alma / yineleme modunu değiştir (yalnızca sayfa veya genel)"
+             :command.editor/toggle-number-list      "Numaralı liste olarak değiştir"
+             :command.whiteboard/select              "Seçim aracı"
+             :command.whiteboard/pan                 "Kaydırma aracı"
+             :command.whiteboard/portal              "Portal aracı"
+             :command.whiteboard/pencil              "Kalem aracı"
+             :command.whiteboard/highlighter         "Vurgulayıcı aracı"
+             :command.whiteboard/eraser              "Silgi aracı"
+             :command.whiteboard/connector           "Bağlayıcı aracı"
+             :command.whiteboard/text                "Metin aracı"
+             :command.whiteboard/rectangle           "Dikdörtgen aracı"
+             :command.whiteboard/ellipse             "Elips aracı"
+             :command.whiteboard/reset-zoom          "Yakınlaştırmayı sıfırla"
+             :command.whiteboard/zoom-to-fit         "Çizimi yakınlaştır"
+             :command.whiteboard/zoom-to-selection   "Seçimi sığacak kadar yakınlaştır"
+             :command.whiteboard/zoom-out            "Uzaklaştır"
+             :command.whiteboard/zoom-in             "Yakınlaştır"
+             :command.whiteboard/send-backward       "Geriye git"
+             :command.whiteboard/send-to-back        "Geriye taşı"
+             :command.whiteboard/bring-forward       "İleriye git"
+             :command.whiteboard/bring-to-front      "Öne taşı"
+             :command.whiteboard/lock                "Seçimi kilitle"
+             :command.whiteboard/unlock              "Seçimin Kilidini aç"
+             :command.whiteboard/group               "Seçimi gruplandır"
+             :command.whiteboard/ungroup             "Seçimi gruptan çıkar"
+             :command.whiteboard/toggle-grid         "Tuval ızgarasını değiştir"
              :command.ui/toggle-brackets             "Köşeli ayraçların görüntülenip görüntülenmeyeceğini değiştir"
              :command.ui/toggle-brackets             "Köşeli ayraçların görüntülenip görüntülenmeyeceğini değiştir"
              :command.go/search-in-page              "Geçerli sayfada ara"
              :command.go/search-in-page              "Geçerli sayfada ara"
              :command.go/electron-find-in-page       "Sayfada bul"
              :command.go/electron-find-in-page       "Sayfada bul"

+ 1 - 0
src/main/frontend/schema/handler/common_config.cljc

@@ -62,6 +62,7 @@
               :string]]
               :string]]
     [:ref/default-open-blocks-level :int]
     [:ref/default-open-blocks-level :int]
     [:ref/linked-references-collapsed-threshold :int]
     [:ref/linked-references-collapsed-threshold :int]
+    [:graph/settings [:map-of :keyword :boolean]]
     [:favorites [:vector :string]]
     [:favorites [:vector :string]]
     ;; There isn't a :float yet
     ;; There isn't a :float yet
     [:srs/learning-fraction float?]
     [:srs/learning-fraction float?]

+ 3 - 1
src/main/frontend/state.cljs

@@ -277,7 +277,9 @@
      :whiteboard/onboarding-tour?           (or (storage/get :whiteboard-onboarding-tour?) false)
      :whiteboard/onboarding-tour?           (or (storage/get :whiteboard-onboarding-tour?) false)
      :whiteboard/last-persisted-at          {}
      :whiteboard/last-persisted-at          {}
      :whiteboard/pending-tx-data            {}
      :whiteboard/pending-tx-data            {}
-     :history/page-only-mode?               false})))
+     :history/page-only-mode?               false
+     ;; db tx-id -> editor cursor
+     :history/tx->editor-cursor             {}})))
 
 
 ;; Block ast state
 ;; Block ast state
 ;; ===============
 ;; ===============

+ 1 - 0
src/main/frontend/ui.cljs

@@ -353,6 +353,7 @@
     (when config/publishing? (.add cl "is-publish-mode"))
     (when config/publishing? (.add cl "is-publish-mode"))
     (when util/mac? (.add cl "is-mac"))
     (when util/mac? (.add cl "is-mac"))
     (when util/win32? (.add cl "is-win32"))
     (when util/win32? (.add cl "is-win32"))
+    (when util/linux? (.add cl "is-linux"))
     (when (util/electron?) (.add cl "is-electron"))
     (when (util/electron?) (.add cl "is-electron"))
     (when (util/ios?) (.add cl "is-ios"))
     (when (util/ios?) (.add cl "is-ios"))
     (when (util/mobile?) (.add cl "is-mobile"))
     (when (util/mobile?) (.add cl "is-mobile"))

+ 5 - 0
src/main/frontend/util.cljc

@@ -58,6 +58,8 @@
                (gdom/getElementByClass "sidebar-item-list")
                (gdom/getElementByClass "sidebar-item-list")
                (app-scroll-container-node))))))
                (app-scroll-container-node))))))
 #?(:cljs (defonce el-visible-in-viewport? utils/elementIsVisibleInViewport))
 #?(:cljs (defonce el-visible-in-viewport? utils/elementIsVisibleInViewport))
+#?(:cljs (defonce convert-to-roman utils/convertToRoman))
+#?(:cljs (defonce convert-to-letters utils/convertToLetters))
 
 
 (defn string-join-path
 (defn string-join-path
   "Replace all `strings/join` used to construct paths with this function to reduce lint output.
   "Replace all `strings/join` used to construct paths with this function to reduce lint output.
@@ -915,6 +917,9 @@
 (defonce win32? #?(:cljs goog.userAgent/WINDOWS
 (defonce win32? #?(:cljs goog.userAgent/WINDOWS
                    :clj nil))
                    :clj nil))
 
 
+(defonce linux? #?(:cljs goog.userAgent/LINUX
+                   :clj nil))
+
 (defn default-content-with-title
 (defn default-content-with-title
   [text-format]
   [text-format]
   (case (name text-format)
   (case (name text-format)

+ 1 - 1
src/main/frontend/util/thingatpt.cljs

@@ -177,7 +177,7 @@
    :block-ref?       true
    :block-ref?       true
    :page-ref?        true
    :page-ref?        true
    :properties?      true
    :properties?      true
-   :list?            true})
+   :list?            false})
 
 
 (defn get-setting [setting]
 (defn get-setting [setting]
   (let [value (get-in (state/get-config) [:dwim/settings setting])]
   (let [value (get-in (state/get-config) [:dwim/settings setting])]

+ 25 - 1
src/main/frontend/utils.js

@@ -406,4 +406,28 @@ export const elementIsVisibleInViewport = (el, partiallyVisible = false) => {
       (bottom > 0 && bottom < innerHeight)) &&
       (bottom > 0 && bottom < innerHeight)) &&
     ((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth))
     ((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth))
     : top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth
     : top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth
-}
+}
+
+export const convertToLetters = (num) => {
+  if (!+num) return false
+  let s = '', t
+
+  while (num > 0) {
+    t = (num - 1) % 26
+    s = String.fromCharCode(65 + t) + s
+    num = ((num - t) / 26) | 0
+  }
+
+  return s
+}
+
+export const convertToRoman = (num) => {
+  if (!+num) return false
+  const digits = String(+num).split('')
+  const key = ['','C','CC','CCC','CD','D','DC','DCC','DCCC','CM',
+    '','X','XX','XXX','XL','L','LX','LXX','LXXX','XC',
+    '','I','II','III','IV','V','VI','VII','VIII','IX']
+  let roman = '', i = 3
+  while (i--) roman = (key[+digits.pop() + i * 10] || '') + roman
+  return Array(+digits.join('') + 1).join('M') + roman
+}

+ 12 - 10
src/main/logseq/api.cljs

@@ -21,6 +21,7 @@
             [frontend.fs :as fs]
             [frontend.fs :as fs]
             [frontend.handler.dnd :as editor-dnd-handler]
             [frontend.handler.dnd :as editor-dnd-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.editor.property :as editor-property]
             [frontend.handler.export :as export-handler]
             [frontend.handler.export :as export-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.plugin :as plugin-handler]
@@ -687,11 +688,11 @@
                 block (if includeChildren
                 block (if includeChildren
                         ;; nested children results
                         ;; nested children results
                         (first (outliner-tree/blocks->vec-tree
                         (first (outliner-tree/blocks->vec-tree
-                                 (db-model/get-block-and-children repo uuid) uuid))
+                                (db-model/get-block-and-children repo uuid) uuid))
                         ;; attached shallow children
                         ;; attached shallow children
                         (assoc block :block/children
                         (assoc block :block/children
-                                     (map #(list :uuid (get-in % [:data :block/uuid]))
-                                          (db/get-block-immediate-children repo uuid))))]
+                               (map #(list :uuid (:block/uuid %))
+                                 (db/get-block-immediate-children repo uuid))))]
             (bean/->js (sdk-utils/normalize-keyword-for-json block))))))))
             (bean/->js (sdk-utils/normalize-keyword-for-json block))))))))
 
 
 (def ^:export get_current_block
 (def ^:export get_current_block
@@ -714,8 +715,9 @@
 (def ^:export get_next_sibling_block
 (def ^:export get_next_sibling_block
   (fn [block-uuid]
   (fn [block-uuid]
     (when-let [block (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error block-uuid))]
     (when-let [block (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error block-uuid))]
-      (when-let [right-siblings (outliner/get-right-siblings (outliner/->Block block))]
-        (bean/->js (sdk-utils/normalize-keyword-for-json (:data (first right-siblings))))))))
+      (when-let [right-sibling (outliner/get-right-sibling (:db/id block))]
+        (let [block (db/pull (:id right-sibling))]
+          (bean/->js (sdk-utils/normalize-keyword-for-json block)))))))
 
 
 (def ^:export set_block_collapsed
 (def ^:export set_block_collapsed
   (fn [block-uuid ^js opts]
   (fn [block-uuid ^js opts]
@@ -733,11 +735,11 @@
 
 
 (def ^:export upsert_block_property
 (def ^:export upsert_block_property
   (fn [block-uuid key value]
   (fn [block-uuid key value]
-    (editor-handler/set-block-property! (sdk-utils/uuid-or-throw-error block-uuid) key value)))
+    (editor-property/set-block-property! (sdk-utils/uuid-or-throw-error block-uuid) key value)))
 
 
 (def ^:export remove_block_property
 (def ^:export remove_block_property
   (fn [block-uuid key]
   (fn [block-uuid key]
-    (editor-handler/remove-block-property! (sdk-utils/uuid-or-throw-error block-uuid) key)))
+    (editor-property/remove-block-property! (sdk-utils/uuid-or-throw-error block-uuid) key)))
 
 
 (def ^:export get_block_property
 (def ^:export get_block_property
   (fn [block-uuid key]
   (fn [block-uuid key]
@@ -986,14 +988,14 @@
           exist? (page-handler/template-exists? template-name)]
           exist? (page-handler/template-exists? template-name)]
       (if (or (not exist?) (true? overwrite))
       (if (or (not exist?) (true? overwrite))
         (do (when-let [old-target (and exist? (db-model/get-template-by-name template-name))]
         (do (when-let [old-target (and exist? (db-model/get-template-by-name template-name))]
-              (editor-handler/remove-block-property! (:block/uuid old-target) :template))
-            (editor-handler/set-block-property! target-uuid :template template-name))
+              (editor-property/remove-block-property! (:block/uuid old-target) :template))
+            (editor-property/set-block-property! target-uuid :template template-name))
         (throw (js/Error. "Template already exists!"))))))
         (throw (js/Error. "Template already exists!"))))))
 
 
 (defn ^:export remove_template
 (defn ^:export remove_template
   [name]
   [name]
   (when-let [target (db-model/get-template-by-name name)]
   (when-let [target (db-model/get-template-by-name name)]
-    (editor-handler/remove-block-property! (:block/uuid target) :template)))
+    (editor-property/remove-block-property! (:block/uuid target) :template)))
 
 
 ;; search
 ;; search
 (defn ^:export search
 (defn ^:export search

+ 16 - 0
src/test/frontend/db/model_test.cljs

@@ -163,6 +163,22 @@ foo:: bar"}])
                 (catch :default e
                 (catch :default e
                   (ex-message e)))))))
                   (ex-message e)))))))
 
 
+(deftest get-block-immediate-children
+  (load-test-files [{:file/path "pages/page1.md"
+                     :file/content "\n
+- parent
+  - child 1
+    - grandchild 1
+  - child 2
+    - grandchild 2
+  - child 3"}])
+  (let [parent (-> (d/q '[:find (pull ?b [*]) :where [?b :block/content "parent"]]
+                        (conn/get-db test-helper/test-db))
+                   ffirst)]
+    (is (= ["child 1" "child 2" "child 3"]
+           (map :block/content
+                (model/get-block-immediate-children test-helper/test-db (:block/uuid parent)))))))
+
 (deftest get-property-values
 (deftest get-property-values
   (load-test-files [{:file/path "pages/Feature.md"
   (load-test-files [{:file/path "pages/Feature.md"
                      :file/content "type:: [[Class]]"}
                      :file/content "type:: [[Class]]"}

+ 58 - 1
src/test/frontend/modules/outliner/core_test.cljs

@@ -10,7 +10,7 @@
             [clojure.walk :as walk]
             [clojure.walk :as walk]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.block :as gp-block]
             [datascript.core :as d]
             [datascript.core :as d]
-            [frontend.test.helper :as test-helper]
+            [frontend.test.helper :as test-helper :refer [load-test-files]]
             [clojure.set :as set]))
             [clojure.set :as set]))
 
 
 (def test-db test-helper/test-db)
 (def test-db test-helper/test-db)
@@ -440,6 +440,63 @@
          '(16 17)
          '(16 17)
          (map :block/uuid (tree/get-sorted-block-and-children test-db (:db/id (get-block 16))))))))
          (map :block/uuid (tree/get-sorted-block-and-children test-db (:db/id (get-block 16))))))))
 
 
+(defn- save-block!
+  [block]
+  (outliner-tx/transact! {:graph test-db}
+                         (outliner-core/save-block! block)))
+
+(deftest save-test
+  (load-test-files [{:file/path "pages/page1.md"
+                     :file/content "alias:: foo, bar
+tags:: tag1, tag2
+- block #blarg #bar"}])
+  (testing "save deletes a page's tags"
+      (let [conn (db/get-db test-helper/test-db false)
+            pre-block (->> (d/q '[:find (pull ?b [*])
+                                  :where [?b :block/pre-block? true]]
+                                @conn)
+                           ffirst)
+            _ (save-block! (-> pre-block
+                               (update :block/properties dissoc :tags)
+                               (update :block/properties-text-values dissoc :tags)))
+            updated-page (-> (d/q '[:find (pull ?bp [* {:block/alias [*]}])
+                                    :where [?b :block/pre-block? true]
+                                    [?b :block/page ?bp]]
+                                  @conn)
+                             ffirst)]
+        (is (nil? (:block/tags updated-page))
+            "Page's tags are deleted")
+        (is (= #{"foo" "bar"} (set (map :block/name (:block/alias updated-page))))
+            "Page's aliases remain the same")
+        (is (= {:block/properties {:alias #{"foo" "bar"}}
+                :block/properties-text-values {:alias "foo, bar"}}
+               (select-keys updated-page [:block/properties :block/properties-text-values]))
+            "Page property attributes are correct")
+        (is (= {:block/properties {:alias #{"foo" "bar"}}
+                :block/properties-text-values {:alias "foo, bar"}}
+               (-> (d/q '[:find (pull ?b [*])
+                          :where [?b :block/pre-block? true]]
+                        @conn)
+                   ffirst
+                   (select-keys [:block/properties :block/properties-text-values])))
+            "Pre-block property attributes are correct")))
+
+  (testing "save deletes orphaned pages when a block's refs change"
+    (let [conn (db/get-db test-helper/test-db false)
+          pages (set (map first (d/q '[:find ?bn :where [?b :block/name ?bn]] @conn)))
+          _ (assert (set/subset? #{"blarg" "bar"} pages) "Pages from block exist")
+          block-with-refs (ffirst (d/q '[:find (pull ?b [* {:block/refs [*]}])
+                                         :where [?b :block/content "block #blarg #bar"]]
+                                       @conn))
+          _ (save-block! (-> block-with-refs
+                             (assoc :block/content "block"
+                                    :block/refs [])))
+          updated-pages (set (map first (d/q '[:find ?bn :where [?b :block/name ?bn]] @conn)))]
+      (is (not (contains? updated-pages "blarg"))
+          "Deleted, orphaned page no longer exists")
+      (is (contains? updated-pages "bar")
+          "Deleted but not orphaned page still exists"))))
+
 ;;; Fuzzy tests
 ;;; Fuzzy tests
 
 
 (def init-id (atom 100))
 (def init-id (atom 100))

+ 54 - 0
src/test/frontend/modules/outliner/pipeline_test.cljs

@@ -0,0 +1,54 @@
+(ns frontend.modules.outliner.pipeline-test
+  (:require [cljs.test :refer [deftest is use-fixtures testing]]
+            [datascript.core :as d]
+            [frontend.state :as state]
+            [frontend.db :as db]
+            [frontend.modules.outliner.pipeline :as pipeline]
+            [frontend.test.helper :as test-helper :refer [load-test-files]]))
+
+(use-fixtures :each {:before (fn []
+                               ;; Set current-repo explicitly since it's not the default
+                               (state/set-current-repo! test-helper/test-db)
+                               (test-helper/start-test-db!))
+                     :after (fn []
+                              (state/set-current-repo! nil)
+                              (test-helper/destroy-test-db!))})
+
+(defn- get-blocks [db]
+  (->> (d/q '[:find (pull ?b [* {:block/path-refs [:block/name :db/id]}])
+              :in $
+              :where [?b :block/content] [(missing? $ ?b :block/pre-block?)]]
+            db)
+       (map first)))
+
+(deftest compute-block-path-refs
+  (load-test-files [{:file/path "pages/page1.md"
+                     :file/content "prop:: #bar
+- parent #foo
+  - child #baz
+    - grandchild #bing"}])
+  (testing "when a block's :refs change, descendants of block have correct :block/path-refs"
+    (let [conn (db/get-db test-helper/test-db false)
+          blocks (get-blocks @conn)
+          ;; Update parent block to replace #foo with #bar
+          new-tag-id (:db/id (d/entity @conn [:block/name "bar"]))
+          modified-blocks (map #(if (= "parent #foo" (:block/content %))
+                                  (assoc %
+                                         :block/refs [{:db/id new-tag-id}]
+                                         :block/path-refs [{:db/id new-tag-id}])
+                                  %)
+                               blocks)
+          refs-tx (pipeline/compute-block-path-refs {:tx-meta {:outliner-op :save-block}} modified-blocks)
+          _ (d/transact! conn (concat (map (fn [m] [:db/retract (:db/id m) :block/path-refs]) refs-tx)
+                                      refs-tx))
+          updated-blocks (->> (get-blocks @conn)
+                              (map #(hash-map :block/content (:block/content %)
+                                              :path-ref-names (mapv :block/name (:block/path-refs %)))))]
+      (is (= [;; still have old parent content b/c we're only testing :block/path-refs updates
+              {:block/content "parent #foo"
+               :path-ref-names ["page1" "bar"]}
+              {:block/content "child #baz"
+               :path-ref-names ["page1" "bar" "baz"]}
+              {:block/content "grandchild #bing"
+               :path-ref-names ["page1" "bar" "baz" "bing"]}]
+             updated-blocks)))))

+ 227 - 164
templates/config.edn

@@ -1,137 +1,166 @@
 {:meta/version 1
 {:meta/version 1
 
 
- ;; Currently, we support either "Markdown" or "Org".
- ;; This can overwrite your global preference so that
- ;; maybe your personal preferred format is Org but you'd
- ;; need to use Markdown for some projects.
- ;; :preferred-format ""
-
- ;; Preferred workflow style.
- ;; Value is either ":now" for NOW/LATER style,
- ;; or ":todo" for TODO/DOING style.
+ ;; Set the preferred format.
+ ;; Available options:
+ ;; - Markdown (default)
+ ;; - Org
+ ;; :preferred-format "Markdown"
+
+ ;; Set the preferred workflow style.
+ ;; Available options:
+ ;; - :now for NOW/LATER style (default)
+ ;; - :todo for TODO/DOING style
  :preferred-workflow :now
  :preferred-workflow :now
 
 
- ;; The app will ignore those directories or files.
- ;; E.g. :hidden ["/archived" "/test.md" "../assets/archived"]
+ ;; Exclude directories/files.
+ ;; Example usage: 
+ ;; :hidden ["/archived" "/test.md" "../assets/archived"]
  :hidden []
  :hidden []
 
 
- ;; When creating the new journal page, the app will use your template if there is one.
- ;; You only need to input your template name here.
+ ;; Define the default journal page template.
+ ;; Enter the template name between the quotes.
  :default-templates
  :default-templates
  {:journals ""}
  {:journals ""}
 
 
- ;; Set a custom date format for journal page title
- ;; Example:
- ;; :journal/page-title-format "EEE, do MMM yyyy"
+ ;; Set a custom date format for the journal page title.
+ ;; Default value: "MMM do, yyyy"
+ ;; e.g., "Jan 19th, 2038"
+ ;; Example usage e.g., "Tue 19th, Jan 2038"
+ ;; :journal/page-title-format "EEE do, MMM yyyy"
+
+ ;; Specify the journal filename format using a valid date format string.
+ ;; !Warning:
+ ;;   This configuration is not retroactive and affects only new journals.
+ ;;   To show old journal files in the app, manually rename the files in the
+ ;;   journal directory to match the new format.
+ ;; Default value: "yyyy_MM_dd"
+ ;; :journal/file-name-format "yyyy_MM_dd"
 
 
- ;; Whether to enable hover on tooltip preview feature
- ;; Default is true, you can also toggle this via setting page
+ ;; Enable tooltip preview on hover.
+ ;; Default value: true
  :ui/enable-tooltip? true
  :ui/enable-tooltip? true
 
 
- ;; Show brackets around page references
+ ;; Display brackets [[]] around page references.
+ ;; Default value: true
  ;; :ui/show-brackets? true
  ;; :ui/show-brackets? true
 
 
- ;; Enable showing the body of blocks when referencing them.
+ ;; Display all lines of a block when referencing ((block)).
+ ;; Default value: false
  :ui/show-full-blocks? false
  :ui/show-full-blocks? false
 
 
- ;; Expand block references automatically when zoom-in
+ ;; Automatically expand block references when zooming in.
+ ;; Default value: true
  :ui/auto-expand-block-refs? true
  :ui/auto-expand-block-refs? true
 
 
- ;; Enable Block timestamp
+ ;; Enable Block timestamps.
+ ;; Default value: false
  :feature/enable-block-timestamps? false
  :feature/enable-block-timestamps? false
 
 
- ;; Enable remove accents when searching.
- ;; After toggle this option, please remember to rebuild your search index by press (cmd+c cmd+s).
+ ;; Disable accent marks when searching.
+ ;; After changing this setting, rebuild the search index by pressing (^C ^S).
+ ;; Default value: true
  :feature/enable-search-remove-accents? true
  :feature/enable-search-remove-accents? true
 
 
- ;; Enable journals
+ ;; Enable journals.
+ ;; Default value: true
  ;; :feature/enable-journals? true
  ;; :feature/enable-journals? true
 
 
- ;; Enable flashcards
+ ;; Enable flashcards.
+ ;; Default value: true
  ;; :feature/enable-flashcards? true
  ;; :feature/enable-flashcards? true
 
 
- ;; Enable Whiteboards
+ ;; Enable whiteboards.
+ ;; Default value: true
  ;; :feature/enable-whiteboards? true
  ;; :feature/enable-whiteboards? true
 
 
- ;; Disable the built-in Scheduled tasks and deadlines query
- ;; :feature/disable-scheduled-and-deadline-query? true
+ ;; Disable the journal's built-in 'Scheduled tasks and deadlines' query.
+ ;; Default value: false
+ ;; :feature/disable-scheduled-and-deadline-query? false
 
 
- ;; Specify the number of days in the future to display in the
- ;; scheduled tasks and deadlines query, with a default value of 7 which
- ;; displays tasks for the next 7 days.
+ ;; Specify the number of days displayed in the future for
+ ;; the 'scheduled tasks and deadlines' query.
  ;; Example usage:
  ;; Example usage:
- ;; Display all scheduled and deadline blocks for the next 14 days
+ ;; Display all scheduled and deadline blocks for the next 14 days:
  ;; :scheduled/future-days 14
  ;; :scheduled/future-days 14
+ ;; Default value: 7
+ ;; :scheduled/future-days 7
 
 
- ;; Specify the date on which the week starts.
- ;; Goes from 0 to 6 (Monday to Sunday), default to 6
+ ;; Specify the first day of the week.
+ ;; Available options:
+ ;;  - integer from 0 to 6 (Monday to Sunday) 
+ ;; Default value: 6 (Sunday)
  :start-of-week 6
  :start-of-week 6
 
 
- ;; Specify a custom CSS import
- ;; This option take precedence over your local `logseq/custom.css` file
- ;; You may find a list of awesome logseq themes here:
- ;; https://github.com/logseq/awesome-logseq#css-themes
- ;; Example:
+ ;; Specify a custom CSS import.
+ ;; This option takes precedence over the local `logseq/custom.css` file.
+ ;; Example usage:
  ;; :custom-css-url "@import url('https://cdn.jsdelivr.net/gh/dracula/logseq@master/custom.css');"
  ;; :custom-css-url "@import url('https://cdn.jsdelivr.net/gh/dracula/logseq@master/custom.css');"
 
 
- ;; Specify a custom js import
- ;; This option take precedence over your local `logseq/custom.js` file
- ;; :custom-js-url ""
+ ;; Specify a custom JS import.
+ ;; This option takes precedence over the local `logseq/custom.js` file.
+ ;; Example usage:
+ ;; :custom-js-url "https://cdn.logseq.com/custom.js"
 
 
  ;; Set a custom Arweave gateway
  ;; Set a custom Arweave gateway
  ;; Default gateway: https://arweave.net
  ;; Default gateway: https://arweave.net
- ;; :arweave/gateway ""
-
- ;; Set Bullet indentation when exporting
- ;; default option: tab
- ;; Possible options for `:export/bullet-indentation` are
- ;; 1. `:eight-spaces` as eight spaces
- ;; 2. `:four-spaces` as four spaces
- ;; 3. `:two-spaces` as two spaces
+ ;; :arweave/gateway "https://arweave.net"
+
+ ;; Set bullet indentation when exporting
+ ;; Available options:
+ ;;  - `:eight-spaces` as eight spaces
+ ;;  - `:four-spaces` as four spaces
+ ;;  - `:two-spaces` as two spaces
+ ;; - `:tab` as a tab character (default)
  ;; :export/bullet-indentation :tab
  ;; :export/bullet-indentation :tab
 
 
- ;; When :all-pages-public? true, export repo would export all pages within that repo.
- ;; Regardless of whether you've set any page to public or not.
- ;; Example:
- ;; :publishing/all-pages-public? true
-
- ;; Specify default home page and sidebar status for Logseq
- ;; If not specified, Logseq default opens journals page on startup
- ;; value for `:page` is name of page
- ;; Possible options for `:sidebar` are
- ;; 1. `"Contents"` to open up `Contents` in sidebar by default
- ;; 2. `page name` to open up some page in sidebar
- ;; 3. Or multiple pages in an array ["Contents" "Page A" "Page B"]
- ;; If `:sidebar` is not set, sidebar will be hidden
- ;; Example:
- ;; 1. Setup page "Changelog" as home page and "Contents" in sidebar
+ ;; Publish all pages within the Graph
+ ;; regardless of whether individual pages have been marked as public.
+ ;; Default value: false
+ ;; :publishing/all-pages-public? false
+
+ ;; Set the default home page and sidebar status.
+ ;; Define the default home page and sidebar status. 
+ ;; If unspecified, Logseq will load the journal page by default on startup. 
+ ;; The `:page` value represents the name of the page displayed at startup. 
+ ;; Available options for `:sidebar` are: 
+ ;; - "Contents" to display the Contents page in the right sidebar. 
+ ;; - A specific page name to display in the right sidebar. 
+ ;; - An array of multiple pages, e.g., ["Contents" "Page A" "Page B"]. 
+ ;; If `:sidebar` remains unset, the right sidebar will stay hidden.
+ ;; Examples:
+ ;; 1. Set "Changelog" as the home page and display "Contents" in the right sidebar:
  ;; :default-home {:page "Changelog", :sidebar "Contents"}
  ;; :default-home {:page "Changelog", :sidebar "Contents"}
- ;; 2. Setup page "Jun 3rd, 2021" as home page without sidebar
+ ;; 2. Set "Jun 3rd, 2021" as the home page without the right sidebar:
  ;; :default-home {:page "Jun 3rd, 2021"}
  ;; :default-home {:page "Jun 3rd, 2021"}
- ;; 3. Setup page "home" as home page with multiple pages in sidebar
- ;; :default-home {:page "home" :sidebar ["page a" "page b"]}
+ ;; 3. Set "home" as the home page and display multiple pages in the right sidebar:
+ ;; :default-home {:page "home", :sidebar ["Page A" "Page B"]}
+
+ ;; Set the default location for storing notes.
+ ;; Default value: "pages"
+ ;; :pages-directory "pages"
 
 
- ;; Tell logseq to use a specific folder in the repo as a default location for notes
- ;; if not specified, notes are stored in `pages` directory
- ;; :pages-directory "your-directory"
+ ;; Set the default location for storing journals.
+ ;; Default value: "journals"
+ ;; :journals-directory "journals"
 
 
- ;; Tell logseq to use a specific folder in the repo as a default location for journals
- ;; if not specified, journals are stored in `journals` directory
- ;; :journals-directory "your-directory"
+ ;; Set the default location for storing whiteboards.
+ ;; If not specified, whiteboards will be stored in the 'whiteboards' subdirectory.
+ ;; :whiteboards-directory "whiteboards"
 
 
- ;; Set this to true will convert
- ;; `[[Grant Ideas]]` to `[[file:./grant_ideas.org][Grant Ideas]]` for org-mode
- ;; For more, see https://github.com/logseq/logseq/issues/672
- ;; :org-mode/insert-file-link? true
+ ;; Enabling this option converts
+ ;; [[Grant Ideas]] to [[file:./grant_ideas.org][Grant Ideas]] for org-mode.
+ ;; For more information, visit https://github.com/logseq/logseq/issues/672
+ ;; :org-mode/insert-file-link? false
 
 
- ;; Setup custom shortcuts under `:shortcuts` key
+ ;; Configure custom shortcuts.
  ;; Syntax:
  ;; Syntax:
- ;; 1. `+` means keys pressing simultaneously. eg: `ctrl+shift+a`
- ;; 2. ` ` empty space between keys represents key chords. eg: `t s` means press `t` followed by `s`
- ;; 3. `mod` means `Ctrl` for Windows/Linux  and `Command` for Mac
- ;; 4. use `false` to disable particular shortcut
- ;; 5. you can define multiple bindings for one action, eg `["ctrl+j" "down"]`
- ;; full list of configurable shortcuts are available below:
+ ;; 1. + indicates simultaneous key presses, e.g., `Ctrl+Shift+a`.
+ ;; 2. A space between keys represents key chords, e.g., `t s` means 
+ ;;    pressing `t` followed by `s`.
+ ;; 3. mod refers to `Ctrl` for Windows/Linux and `Command` for Mac.
+ ;; 4. Use false to disable a specific shortcut.
+ ;; 5. You can define multiple bindings for a single action, e.g., ["ctrl+j" "down"].
+ ;; The full list of configurable shortcuts is available at:
  ;; https://github.com/logseq/logseq/blob/master/src/main/frontend/modules/shortcut/config.cljs
  ;; https://github.com/logseq/logseq/blob/master/src/main/frontend/modules/shortcut/config.cljs
  ;; Example:
  ;; Example:
  ;; :shortcuts
  ;; :shortcuts
@@ -146,33 +175,37 @@
  ;;  :editor/right           ["ctrl+l" "right"]}
  ;;  :editor/right           ["ctrl+l" "right"]}
  :shortcuts {}
  :shortcuts {}
 
 
- ;; By default, pressing `Enter` in the document mode will create a new line.
- ;; Set this to `true` so that it's the same behaviour as the usual outliner mode.
+ ;; Configure the behavior of pressing Enter in document mode.
+ ;; if set to true, pressing Enter will create a new block.
+ ;; Default value: false
  :shortcut/doc-mode-enter-for-new-block? false
  :shortcut/doc-mode-enter-for-new-block? false
 
 
  ;; Block content larger than `block/content-max-length` will not be searchable
  ;; Block content larger than `block/content-max-length` will not be searchable
  ;; or editable for performance.
  ;; or editable for performance.
+ ;; Default value: 10000
  :block/content-max-length 10000
  :block/content-max-length 10000
 
 
- ;; Whether to show command doc on hover
+ ;; Display command documentation on hover.
+ ;; Default value: true
  :ui/show-command-doc? true
  :ui/show-command-doc? true
 
 
- ;; Whether to show empty bullets for non-document mode (the default mode)
+ ;; Display empty bullet points.
  :ui/show-empty-bullets? false
  :ui/show-empty-bullets? false
 
 
- ;; Pre-defined :view function to use with advanced queries
+ ;; Pre-defined :view function to use with advanced queries.
  :query/views
  :query/views
  {:pprint
  {:pprint
   (fn [r] [:pre.code (pprint r)])}
   (fn [r] [:pre.code (pprint r)])}
 
 
- ;; Pre-defined :result-transform function for use with advanced queries
+ ;; Advanced queries `:result-transform` function.
+ ;; Transform the query result before displaying it.
  :query/result-transforms
  :query/result-transforms
  {:sort-by-priority
  {:sort-by-priority
   (fn [result] (sort-by (fn [h] (get h :block/priority "Z")) result))}
   (fn [result] (sort-by (fn [h] (get h :block/priority "Z")) result))}
 
 
- ;; The app will show those queries in today's journal page,
- ;; the "NOW" query asks the tasks which need to be finished "now",
- ;; the "NEXT" query asks the future tasks.
+ ;; The following queries will be displayed at the bottom of today's journal page.
+ ;; The "NOW" query returns tasks with "NOW" or "DOING" status.
+ ;; The "NEXT" query returns tasks with "NOW", "LATER", or "TODO" status.
  :default-queries
  :default-queries
  {:journals
  {:journals
   [{:title "🔨 NOW"
   [{:title "🔨 NOW"
@@ -207,25 +240,26 @@
     :group-by-page? false
     :group-by-page? false
     :collapsed? false}]}
     :collapsed? false}]}
 
 
- ;; Add your own commands to slash menu to speedup.
- ;; E.g.
+ ;; Add custom commands to the command palette
+ ;; Example usage:
  ;; :commands
  ;; :commands
  ;; [
  ;; [
- ;; ["js" "Javascript"]
- ;; ["md" "Markdown"]
- ;; ]
- :commands
- []
-
- ;; By default, a block can only be collapsed if it has some children.
- ;; `:outliner/block-title-collapse-enabled? true` enables a block with a title
- ;; (multiple lines) can be collapsed too. For example:
+ ;;  ["js" "Javascript"]
+ ;;  ["md" "Markdown"]
+ ;;  ]
+ :commands []
+
+ ;; Enable collapsing blocks with titles but no children.
+ ;; By default, only blocks with children can be collapsed.
+ ;; Setting `:outliner/block-title-collapse-enabled?` to true allows collapsing
+ ;; blocks with titles (multiple lines) and content. For example:
  ;; - block title
  ;; - block title
  ;;   block content
  ;;   block content
+ ;; Default value: false
  :outliner/block-title-collapse-enabled? false
  :outliner/block-title-collapse-enabled? false
 
 
  ;; Macros replace texts and will make you more productive.
  ;; Macros replace texts and will make you more productive.
- ;; For example:
+ ;; Example usage:
  ;; Change the :macros value below to:
  ;; Change the :macros value below to:
  ;; {"poem" "Rose is $1, violet's $2. Life's ordered: Org assists you."}
  ;; {"poem" "Rose is $1, violet's $2. Life's ordered: Org assists you."}
  ;; input "{{poem red,blue}}"
  ;; input "{{poem red,blue}}"
@@ -233,119 +267,148 @@
  ;; Rose is red, violet's blue. Life's ordered: Org assists you.
  ;; Rose is red, violet's blue. Life's ordered: Org assists you.
  :macros {}
  :macros {}
 
 
- ;; The default level to be opened for the linked references.
- ;; For example, if we have some example blocks like this:
+ ;; Configure the default expansion level for linked references.
+ ;; For example, consider the following block hierarchy:
  ;; - a [[page]] (level 1)
  ;; - a [[page]] (level 1)
  ;;   - b        (level 2)
  ;;   - b        (level 2)
  ;;     - c      (level 3)
  ;;     - c      (level 3)
  ;;       - d    (level 4)
  ;;       - d    (level 4)
  ;;
  ;;
- ;; With the default value of level 2, `b` will be collapsed.
- ;; If we set the level's value to 3, `b` will be opened and `c` will be collapsed.
+ ;; With the default value of level 2, block b will be collapsed.
+ ;; If the level's value is set to 3, block c will be collapsed.
+ ;; Default value: 2
  :ref/default-open-blocks-level 2
  :ref/default-open-blocks-level 2
 
 
+ ;; Configure the threshold for linked references before collapsing.
+ ;; Default value: 100
  :ref/linked-references-collapsed-threshold 50
  :ref/linked-references-collapsed-threshold 50
 
 
+ ;; Graph view configuration.
+ ;; Example usage:
+ ;; :graph/settings
+ ;; {:orphan-pages?  true   ; Default value: true
+ ;;  :builtin-pages? false  ; Default value: false
+ ;;  :excluded-pages? false ; Default value: false
+ ;;  :journal?       false} ; Default value: false
+
  ;; Favorites to list on the left sidebar
  ;; Favorites to list on the left sidebar
  :favorites []
  :favorites []
 
 
- ;; any number between 0 and 1 (the greater it is the faster the changes of the next-interval of card reviews) (default 0.5)
+ ;; Set flashcards interval.
+ ;; Expected value:
+ ;; - Float between 0 and 1
+ ;; higher values result in faster changes to the next review interval.
+ ;; Default value: 0.5
  ;; :srs/learning-fraction 0.5
  ;; :srs/learning-fraction 0.5
 
 
- ;; the initial interval after the first successful review of a card (default 4)
+ ;; Set the initial interval after the first successful review of a card.
+ ;; Default value: 4
  ;; :srs/initial-interval 4
  ;; :srs/initial-interval 4
 
 
- ;; hide specific properties for blocks
- ;; E.g. :block-hidden-properties #{:created-at :updated-at}
- ;; :block-hidden-properties #{}
+ ;; Hide specific block properties.
+ ;; Example usage:
+ ;; :block-hidden-properties #{:public :icon}
 
 
- ;; Enable all your properties to have corresponding pages
+ ;; Create a page for all properties.
+ ;; Default value: false
  :property-pages/enabled? true
  :property-pages/enabled? true
 
 
  ;; Properties to exclude from having property pages
  ;; Properties to exclude from having property pages
- ;; E.g.:property-pages/excludelist #{:duration :author}
- ;; :property-pages/excludelist
+ ;; Example usage:
+ ;; :property-pages/excludelist #{:duration :author}
 
 
  ;; By default, property value separated by commas will not be treated as
  ;; By default, property value separated by commas will not be treated as
  ;; page references. You can add properties to enable it.
  ;; page references. You can add properties to enable it.
- ;; E.g. :property/separated-by-commas #{:alias :tags}
- ;; :property/separated-by-commas #{}
+ ;; Example usage:
+ ;; :property/separated-by-commas #{:alias :tags}
 
 
  ;; Properties that are ignored when parsing property values for references
  ;; Properties that are ignored when parsing property values for references
- ;; :ignored-page-references-keywords #{:author :startup}
+ ;; Example usage:
+ ;; :ignored-page-references-keywords #{:author :website}
 
 
- ;; logbook setup
+ ;; logbook configuration.
  ;; :logbook/settings
  ;; :logbook/settings
  ;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated
  ;; {:with-second-support? false ;limit logbook to minutes, seconds will be eliminated
  ;;  :enabled-in-all-blocks true ;display logbook in all blocks after timetracking
  ;;  :enabled-in-all-blocks true ;display logbook in all blocks after timetracking
  ;;  :enabled-in-timestamped-blocks false ;don't display logbook at all
  ;;  :enabled-in-timestamped-blocks false ;don't display logbook at all
  ;; }
  ;; }
 
 
- ;; Mobile photo uploading setup
+ ;; Mobile photo upload configuration.
  ;; :mobile/photo
  ;; :mobile/photo
  ;; {:allow-editing? true
  ;; {:allow-editing? true
- ;;  :quality          80}
+ ;;  :quality        80}
 
 
  ;; Mobile features options
  ;; Mobile features options
  ;; Gestures
  ;; Gestures
- ;; :mobile
+ ;; Example usage:
+ ;; :mobile 
  ;; {:gestures/disabled-in-block-with-tags ["kanban"]}
  ;; {:gestures/disabled-in-block-with-tags ["kanban"]}
 
 
  ;; Extra CodeMirror options
  ;; Extra CodeMirror options
  ;; See https://codemirror.net/5/doc/manual.html#config for possible options
  ;; See https://codemirror.net/5/doc/manual.html#config for possible options
- ;; :editor/extra-codemirror-options {:keyMap "emacs" :lineWrapping true}
+ ;; Example usage:
+ ;; :editor/extra-codemirror-options
+ ;; {:lineWrapping  false  ; Default value: false
+ ;;  :lineNumbers   true   ; Default value: true
+ ;;  :readOnly      false} ; Default value: false
 
 
  ;; Enable logical outdenting
  ;; Enable logical outdenting
- ;; :editor/logical-outdenting? true
+ ;; Default value: false 
+ ;; :editor/logical-outdenting? false
 
 
- ;; When both text and a file are in the clipboard, paste the file
- ;; :editor/preferred-pasting-file? true
+ ;; Prefer pasting the file when text and a file are in the clipboard.
+ ;; Default value: false 
+ ;; :editor/preferred-pasting-file? false
 
 
- ;; Quick capture templates for receiving contents from other apps.
+ ;; Quick capture templates for receiving content from other apps.
  ;; Each template contains three elements {time}, {text} and {url}, which can be auto-expanded
  ;; Each template contains three elements {time}, {text} and {url}, which can be auto-expanded
- ;; by received contents from other apps. Note: the {} cannot be omitted.
+ ;; by receiving content from other apps. Note: the {} cannot be omitted.
  ;; - {time}: capture time
  ;; - {time}: capture time
  ;; - {date}: capture date using current date format, use `[[{date}]]` to get a page reference
  ;; - {date}: capture date using current date format, use `[[{date}]]` to get a page reference
  ;; - {text}: text that users selected before sharing.
  ;; - {text}: text that users selected before sharing.
- ;; - {url}: url or assets path for media files stored in Logseq.
- ;; You can also reorder them, or even only use one or two of them in the template.
- ;; You can also insert or format any text in the template as shown in the following examples.
+ ;; - {url}: URL or assets path for media files stored in Logseq.
+ ;; You can also reorder them or use only one or two of them in the template.
+ ;; You can also insert or format any text in the template, as shown in the following examples.
  ;; :quick-capture-templates
  ;; :quick-capture-templates
  ;; {:text "[[quick capture]] **{time}**: {text} from {url}"
  ;; {:text "[[quick capture]] **{time}**: {text} from {url}"
  ;;  :media "[[quick capture]] **{time}**: {url}"}
  ;;  :media "[[quick capture]] **{time}**: {url}"}
 
 
- ;; Quick capture options
- ;; :quick-capture-options {:insert-today? false :redirect-page? false :default-page "my page"}
+ ;; Quick capture options.
+ ;; - insert-today?   Insert the capture at the end of today's journal page (boolean).
+ ;; - redirect-page?  Redirect to the quick capture page after capturing (boolean).
+ ;; - default-page    The default page to capture to if insert-today? is false (string).
+ ;; :quick-capture-options
+ ;; {:insert-today? false           ;; Default value: true
+ ;;  :redirect-page? false          ;; Default value: false
+ ;;  :default-page "quick capture"} ;; Default page: "quick capture"
 
 
  ;; File sync options
  ;; File sync options
  ;; Ignore these files when syncing, regexp is supported.
  ;; Ignore these files when syncing, regexp is supported.
  ;; :file-sync/ignore-files []
  ;; :file-sync/ignore-files []
 
 
- ;; dwim (do what I mean) for Enter key when editing.
- ;; Context-awareness of Enter key makes editing more easily
- ; :dwim/settings {
- ;   :admonition&src?  true
- ;   :markup?          false
- ;   :block-ref?       true
- ;   :page-ref?        true
- ;   :properties?      true
- ;   :list?            true
- ; }
-
- ;; Decide the way to escape the special characters in the page title.
+ ;; Configure the Enter key behavior for
+ ;; context-aware editing with DWIM (Do What I Mean).
+ ;; context-aware Enter key behavior implies that pressing Enter will
+ ;; have different outcomes based on the context.
+ ;; For instance, pressing Enter within a list generates a new list item,
+ ;; whereas pressing Enter in a block reference opens the referenced block.
+ ;; :dwim/settings
+ ;; {:admonition&src?  true        ;; Default value: true
+ ;;  :markup?          false       ;; Default value: false
+ ;;  :block-ref?       true        ;; Default value: true
+ ;;  :page-ref?        true        ;; Default value: true
+ ;;  :properties?      true        ;; Default value: true
+ ;;  :list?            false}      ;; Default value: false
+
+ ;; Configure the escaping method for special characters in page titles.
  ;; Warning:
  ;; Warning:
- ;;   This is a dangerous operation. If you want to change the setting,
- ;;   should access the setting `Filename format` and follow the instructions.
- ;;   Or you have to rename all the affected files manually then re-index on all
- ;;   clients after the files are synced. Wrong handling may cause page titles
- ;;   containing special characters to be messy.
- ;; Available values:
- ;;   :file/name-format :triple-lowbar
- ;;     ;use triple underscore `___` for slash `/` in page title
- ;;     ;use Percent-encoding for other invalid characters
- :file/name-format :triple-lowbar
-
- ;; specify the format of the filename for journal files
- ;; :journal/file-name-format "yyyy_MM_dd"
-
- }
+ ;;   This is a dangerous operation. To modify the setting,
+ ;;   access the 'Filename format' setting and follow the instructions.
+ ;;   Othwerwise, You may need to manually rename all affected files and
+ ;;   re-index them on all clients after synchronization.
+ ;;   Incorrect handling may result in messy page titles.
+ ;; Available options:
+ ;;   - :triple-lowbar (default)
+ ;;      ;use triple underscore `___` for slash `/` in page title
+ ;;      ;use Percent-encoding for other invalid characters
+ :file/name-format :triple-lowbar}

+ 6 - 0
tldraw/apps/tldraw-logseq/build.mjs

@@ -4,9 +4,15 @@ import 'zx/globals'
 import fs from 'fs'
 import fs from 'fs'
 import path from 'path'
 import path from 'path'
 
 
+if (process.platform === 'win32') {
+  defaults.shell = "cmd.exe";
+  defaults.prefix = "";
+}
+
 // Build with [tsup](https://tsup.egoist.sh)
 // Build with [tsup](https://tsup.egoist.sh)
 await $`npx tsup`
 await $`npx tsup`
 
 
+
 // Prepare package.json file
 // Prepare package.json file
 const packageJson = fs.readFileSync('package.json', 'utf8')
 const packageJson = fs.readFileSync('package.json', 'utf8')
 const glob = JSON.parse(packageJson)
 const glob = JSON.parse(packageJson)

+ 2 - 2
tldraw/apps/tldraw-logseq/package.json

@@ -41,10 +41,10 @@
     "shadow-cljs": "^2.20.11",
     "shadow-cljs": "^2.20.11",
     "tsup": "^6.5.0",
     "tsup": "^6.5.0",
     "typescript": "^4.9.3",
     "typescript": "^4.9.3",
-    "zx": "^7.1.1"
+    "zx": "^7.2.2"
   },
   },
   "peerDependencies": {
   "peerDependencies": {
     "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
     "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
     "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
     "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
   }
   }
-}
+}

+ 16 - 15
tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx

@@ -30,6 +30,7 @@ import {
 import { ToggleInput } from '../inputs/ToggleInput'
 import { ToggleInput } from '../inputs/ToggleInput'
 import { GeometryTools } from '../GeometryTools'
 import { GeometryTools } from '../GeometryTools'
 import { LogseqContext } from '../../lib/logseq-context'
 import { LogseqContext } from '../../lib/logseq-context'
+import { KeyboardShortcut } from '../KeyboardShortcut'
 
 
 export const contextBarActionTypes = [
 export const contextBarActionTypes = [
   // Order matters
   // Order matters
@@ -61,13 +62,7 @@ const contextBarActionMapping = new Map<ContextBarActionType, React.FC>()
 type ShapeType = Shape['props']['type']
 type ShapeType = Shape['props']['type']
 
 
 export const shapeMapping: Record<ShapeType, ContextBarActionType[]> = {
 export const shapeMapping: Record<ShapeType, ContextBarActionType[]> = {
-  'logseq-portal': [
-    'Swatch',
-    'LogseqPortalViewMode',
-    'ScaleLevel',
-    'AutoResizing',
-    'Links',
-  ],
+  'logseq-portal': ['Swatch', 'LogseqPortalViewMode', 'ScaleLevel', 'AutoResizing', 'Links'],
   youtube: ['YoutubeLink', 'Links'],
   youtube: ['YoutubeLink', 'Links'],
   tweet: ['TwitterLink', 'Links'],
   tweet: ['TwitterLink', 'Links'],
   iframe: ['IFrameSource', 'Links'],
   iframe: ['IFrameSource', 'Links'],
@@ -134,13 +129,24 @@ const LogseqPortalViewModeAction = observer(() => {
     return null
     return null
   }
   }
 
 
+  const tooltip = (
+    <div className="flex">
+      {collapsed ? 'Expand' : 'Collapse'}
+      <KeyboardShortcut
+        action={
+          collapsed ? 'editor/expand-block-children' : 'editor/collapse-block-children'
+        }
+      />
+    </div>
+  )
+
   return (
   return (
     <ToggleInput
     <ToggleInput
-      tooltip={collapsed ? 'Expand' : 'Collapse'}
+      tooltip={tooltip}
       toggle={shapes.every(s => s.props.type === 'logseq-portal')}
       toggle={shapes.every(s => s.props.type === 'logseq-portal')}
       className="tl-button"
       className="tl-button"
       pressed={collapsed}
       pressed={collapsed}
-      onPressedChange={() => app.api.setCollapsed(!collapsed) }
+      onPressedChange={() => app.api.setCollapsed(!collapsed)}
     >
     >
       <TablerIcon name={collapsed ? 'object-expanded' : 'object-compact'} />
       <TablerIcon name={collapsed ? 'object-expanded' : 'object-compact'} />
     </ToggleInput>
     </ToggleInput>
@@ -306,12 +312,7 @@ const GeometryAction = observer(() => {
     app.api.convertShapes(type)
     app.api.convertShapes(type)
   }, [])
   }, [])
 
 
-  return (
-    <GeometryTools
-      popoverSide="top"
-      chevron={false}
-      setGeometry={handleSetGeometry}/>
-  )
+  return <GeometryTools popoverSide="top" chevron={false} setGeometry={handleSetGeometry} />
 })
 })
 
 
 const StrokeTypeAction = observer(() => {
 const StrokeTypeAction = observer(() => {

+ 15 - 59
tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx

@@ -4,6 +4,7 @@ import { MOD_KEY, AlignType, DistributeType, isDev, EXPORT_PADDING } from '@tldr
 import { observer } from 'mobx-react-lite'
 import { observer } from 'mobx-react-lite'
 import { TablerIcon } from '../icons'
 import { TablerIcon } from '../icons'
 import { Button } from '../Button'
 import { Button } from '../Button'
+import { KeyboardShortcut } from '../KeyboardShortcut'
 import * as React from 'react'
 import * as React from 'react'
 
 
 import * as ReactContextMenu from '@radix-ui/react-context-menu'
 import * as ReactContextMenu from '@radix-ui/react-context-menu'
@@ -138,11 +139,7 @@ export const ContextMenu = observer(function ContextMenu({
                 onClick={() => runAndTransition(app.api.zoomToSelection)}
                 onClick={() => runAndTransition(app.api.zoomToSelection)}
               >
               >
                 Zoom to fit
                 Zoom to fit
-                <div className="tl-menu-right-slot">
-                  <span className="keyboard-shortcut">
-                    <code>⇧</code> <code>2</code>
-                  </span>
-                </div>
+                <KeyboardShortcut action="whiteboard/zoom-to-fit" />
               </ReactContextMenu.Item>
               </ReactContextMenu.Item>
               <ReactContextMenu.Separator className="menu-separator" />
               <ReactContextMenu.Separator className="menu-separator" />
             </>
             </>
@@ -159,11 +156,7 @@ export const ContextMenu = observer(function ContextMenu({
                   >
                   >
                     <TablerIcon className="tl-menu-icon" name="ungroup" />
                     <TablerIcon className="tl-menu-icon" name="ungroup" />
                     Ungroup
                     Ungroup
-                    <div className="tl-menu-right-slot">
-                      <span className="keyboard-shortcut">
-                        <code>{MOD_KEY}</code> <code>⇧</code> <code>G</code>
-                      </span>
-                    </div>
+                    <KeyboardShortcut action="whiteboard/ungroup" />
                   </ReactContextMenu.Item>
                   </ReactContextMenu.Item>
                 )}
                 )}
                 {app.selectedShapesArray.length > 1 &&
                 {app.selectedShapesArray.length > 1 &&
@@ -174,11 +167,7 @@ export const ContextMenu = observer(function ContextMenu({
                     >
                     >
                       <TablerIcon className="tl-menu-icon" name="group" />
                       <TablerIcon className="tl-menu-icon" name="group" />
                       Group
                       Group
-                      <div className="tl-menu-right-slot">
-                        <span className="keyboard-shortcut">
-                          <code>{MOD_KEY}</code> <code>G</code>
-                        </span>
-                      </div>
+                      <KeyboardShortcut action="whiteboard/group" />
                     </ReactContextMenu.Item>
                     </ReactContextMenu.Item>
                   )}
                   )}
                 <ReactContextMenu.Separator className="menu-separator" />
                 <ReactContextMenu.Separator className="menu-separator" />
@@ -193,11 +182,6 @@ export const ContextMenu = observer(function ContextMenu({
                 >
                 >
                   <TablerIcon className="tl-menu-icon" name="cut" />
                   <TablerIcon className="tl-menu-icon" name="cut" />
                   Cut
                   Cut
-                  <div className="tl-menu-right-slot">
-                    <span className="keyboard-shortcut">
-                      <code>{MOD_KEY}</code> <code>X</code>
-                    </span>
-                  </div>
                 </ReactContextMenu.Item>
                 </ReactContextMenu.Item>
               )}
               )}
               <ReactContextMenu.Item
               <ReactContextMenu.Item
@@ -206,11 +190,7 @@ export const ContextMenu = observer(function ContextMenu({
               >
               >
                 <TablerIcon className="tl-menu-icon" name="copy" />
                 <TablerIcon className="tl-menu-icon" name="copy" />
                 Copy
                 Copy
-                <div className="tl-menu-right-slot">
-                  <span className="keyboard-shortcut">
-                    <code>{MOD_KEY}</code> <code>C</code>
-                  </span>
-                </div>
+                <KeyboardShortcut action="editor/copy" />
               </ReactContextMenu.Item>
               </ReactContextMenu.Item>
             </>
             </>
           )}
           )}
@@ -223,7 +203,7 @@ export const ContextMenu = observer(function ContextMenu({
               Paste
               Paste
               <div className="tl-menu-right-slot">
               <div className="tl-menu-right-slot">
                 <span className="keyboard-shortcut">
                 <span className="keyboard-shortcut">
-                  <code>{MOD_KEY}</code> <code>V</code>
+                  <code>{MOD_KEY}</code> <code>v</code>
                 </span>
                 </span>
               </div>
               </div>
             </ReactContextMenu.Item>
             </ReactContextMenu.Item>
@@ -236,7 +216,7 @@ export const ContextMenu = observer(function ContextMenu({
               Paste as link
               Paste as link
               <div className="tl-menu-right-slot">
               <div className="tl-menu-right-slot">
                 <span className="keyboard-shortcut">
                 <span className="keyboard-shortcut">
-                  <code>{MOD_KEY}</code> <code>⇧</code> <code>V</code>
+                  <code>{MOD_KEY}</code> <code>⇧</code> <code>v</code>
                 </span>
                 </span>
               </div>
               </div>
             </ReactContextMenu.Item>
             </ReactContextMenu.Item>
@@ -272,11 +252,7 @@ export const ContextMenu = observer(function ContextMenu({
             onClick={() => runAndTransition(app.api.selectAll)}
             onClick={() => runAndTransition(app.api.selectAll)}
           >
           >
             Select all
             Select all
-            <div className="tl-menu-right-slot">
-              <span className="keyboard-shortcut">
-                <code>{MOD_KEY}</code> <code>A</code>
-              </span>
-            </div>
+            <KeyboardShortcut action="editor/select-parent" />
           </ReactContextMenu.Item>
           </ReactContextMenu.Item>
           {app.selectedShapes?.size > 1 && (
           {app.selectedShapes?.size > 1 && (
             <ReactContextMenu.Item
             <ReactContextMenu.Item
@@ -286,32 +262,24 @@ export const ContextMenu = observer(function ContextMenu({
               Deselect all
               Deselect all
             </ReactContextMenu.Item>
             </ReactContextMenu.Item>
           )}
           )}
-          {app.selectedShapes?.size > 0 && app.selectedShapesArray?.some(s => !s.props.isLocked) && (
+          {!app.readOnly && app.selectedShapes?.size > 0 && app.selectedShapesArray?.some(s => !s.props.isLocked) && (
             <ReactContextMenu.Item
             <ReactContextMenu.Item
               className="tl-menu-item"
               className="tl-menu-item"
               onClick={() => runAndTransition(() => app.setLocked(true))}
               onClick={() => runAndTransition(() => app.setLocked(true))}
             >
             >
               <TablerIcon className="tl-menu-icon" name="lock" />
               <TablerIcon className="tl-menu-icon" name="lock" />
               Lock
               Lock
-              <div className="tl-menu-right-slot">
-                <span className="keyboard-shortcut">
-                <code>{MOD_KEY}</code> <code>L</code>
-                </span>
-              </div>
+              <KeyboardShortcut action="whiteboard/lock" />
             </ReactContextMenu.Item>
             </ReactContextMenu.Item>
           )}
           )}
-          {app.selectedShapes?.size > 0 && app.selectedShapesArray?.some(s => s.props.isLocked) && (
+          {!app.readOnly && app.selectedShapes?.size > 0 && app.selectedShapesArray?.some(s => s.props.isLocked) && (
             <ReactContextMenu.Item
             <ReactContextMenu.Item
               className="tl-menu-item"
               className="tl-menu-item"
               onClick={() => runAndTransition(() => app.setLocked(false))}
               onClick={() => runAndTransition(() => app.setLocked(false))}
             >
             >
               <TablerIcon className="tl-menu-icon" name="lock-open" />
               <TablerIcon className="tl-menu-icon" name="lock-open" />
               Unlock
               Unlock
-              <div className="tl-menu-right-slot">
-                <span className="keyboard-shortcut">
-                <code>{MOD_KEY}</code> <code>⇧</code> <code>L</code>
-                </span>
-              </div>
+              <KeyboardShortcut action="whiteboard/unlock" />
             </ReactContextMenu.Item>
             </ReactContextMenu.Item>
           )}
           )}
           {app.selectedShapes?.size > 0 &&
           {app.selectedShapes?.size > 0 &&
@@ -324,11 +292,7 @@ export const ContextMenu = observer(function ContextMenu({
                 >
                 >
                   <TablerIcon className="tl-menu-icon" name="backspace" />
                   <TablerIcon className="tl-menu-icon" name="backspace" />
                   Delete
                   Delete
-                  <div className="tl-menu-right-slot">
-                    <span className="keyboard-shortcut">
-                      <code>Del</code>
-                    </span>
-                  </div>
+                  <KeyboardShortcut action="editor/delete" />
                 </ReactContextMenu.Item>
                 </ReactContextMenu.Item>
                 {app.selectedShapes?.size > 1 && !app.readOnly && (
                 {app.selectedShapes?.size > 1 && !app.readOnly && (
                   <>
                   <>
@@ -357,22 +321,14 @@ export const ContextMenu = observer(function ContextMenu({
                       onClick={() => runAndTransition(app.bringToFront)}
                       onClick={() => runAndTransition(app.bringToFront)}
                     >
                     >
                       Move to front
                       Move to front
-                      <div className="tl-menu-right-slot">
-                        <span className="keyboard-shortcut">
-                          <code>⇧</code> <code>]</code>
-                        </span>
-                      </div>
+                      <KeyboardShortcut action="whiteboard/bring-to-front" />
                     </ReactContextMenu.Item>
                     </ReactContextMenu.Item>
                     <ReactContextMenu.Item
                     <ReactContextMenu.Item
                       className="tl-menu-item"
                       className="tl-menu-item"
                       onClick={() => runAndTransition(app.sendToBack)}
                       onClick={() => runAndTransition(app.sendToBack)}
                     >
                     >
                       Move to back
                       Move to back
-                      <div className="tl-menu-right-slot">
-                        <span className="keyboard-shortcut">
-                          <code>⇧</code> <code>[</code>
-                        </span>
-                      </div>
+                      <KeyboardShortcut action="whiteboard/send-to-back" />
                     </ReactContextMenu.Item>
                     </ReactContextMenu.Item>
                   </>
                   </>
                 )}
                 )}

+ 16 - 0
tldraw/apps/tldraw-logseq/src/components/KeyboardShortcut/KeyboardShortcut.tsx

@@ -0,0 +1,16 @@
+import { LogseqContext } from '../../lib/logseq-context'
+import * as React from 'react'
+
+export const KeyboardShortcut = ({
+  action,
+  ...props
+}: { action: string } & React.HTMLAttributes<HTMLElement>) => {
+  const { renderers } = React.useContext(LogseqContext)
+  const Shortcut = renderers?.KeyboardShortcut
+
+  return (
+    <div className="tl-menu-right-slot" {...props}>
+      <Shortcut action={action} />
+    </div>
+  )
+}

+ 1 - 0
tldraw/apps/tldraw-logseq/src/components/KeyboardShortcut/index.ts

@@ -0,0 +1 @@
+export * from './KeyboardShortcut'

+ 52 - 19
tldraw/apps/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx

@@ -15,16 +15,15 @@ export const PrimaryTools = observer(function PrimaryTools() {
     app.api.setColor(color)
     app.api.setColor(color)
   }, [])
   }, [])
 
 
-  const handleToolClick = React.useCallback(
-    (e: React.MouseEvent<HTMLButtonElement>) => {
-      const tool = e.currentTarget.dataset.tool
-      if (tool) app.selectTool(tool)
-    },
-    []
-  )
+  const handleToolClick = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
+    const tool = e.currentTarget.dataset.tool
+    if (tool) app.selectTool(tool)
+  }, [])
 
 
   const [activeGeomId, setActiveGeomId] = React.useState(
   const [activeGeomId, setActiveGeomId] = React.useState(
-    () => (Object.values(Geometry).find((geo: string) => geo === app.selectedTool.id) ?? Object.values(Geometry)[0])
+    () =>
+      Object.values(Geometry).find((geo: string) => geo === app.selectedTool.id) ??
+      Object.values(Geometry)[0]
   )
   )
 
 
   React.useEffect(() => {
   React.useEffect(() => {
@@ -33,25 +32,59 @@ export const PrimaryTools = observer(function PrimaryTools() {
     })
     })
   }, [app.selectedTool.id])
   }, [app.selectedTool.id])
 
 
-
   return (
   return (
     <div className="tl-primary-tools" data-html2canvas-ignore="true">
     <div className="tl-primary-tools" data-html2canvas-ignore="true">
       <div className="tl-toolbar tl-tools-floating-panel">
       <div className="tl-toolbar tl-tools-floating-panel">
-        <ToolButton handleClick={() =>app.selectTool("select")} tooltip="Select" id="select" icon="select-cursor" />
         <ToolButton
         <ToolButton
-         handleClick={() =>app.selectTool("move")}
-          tooltip="Move"
+          handleClick={() => app.selectTool('select')}
+          tooltip="Select"
+          id="select"
+          icon="select-cursor"
+        />
+        <ToolButton
+          handleClick={() => app.selectTool('move')}
+          tooltip="Pan"
           id="move"
           id="move"
           icon={app.isIn('move.panning') ? 'hand-grab' : 'hand-stop'}
           icon={app.isIn('move.panning') ? 'hand-grab' : 'hand-stop'}
         />
         />
         <Separator.Root className="tl-toolbar-separator" orientation="horizontal" />
         <Separator.Root className="tl-toolbar-separator" orientation="horizontal" />
-        <ToolButton handleClick={() =>app.selectTool("logseq-portal")} tooltip="Add block or page" id="logseq-portal" icon="circle-plus" />
-        <ToolButton handleClick={() =>app.selectTool("pencil")} tooltip="Draw" id="pencil" icon="ballpen" />
-        <ToolButton handleClick={() =>app.selectTool("highlighter")} tooltip="Highlight" id="highlighter" icon="highlight" />
-        <ToolButton handleClick={() =>app.selectTool("erase")} tooltip="Eraser" id="erase" icon="eraser" />
-        <ToolButton handleClick={() =>app.selectTool("line")} tooltip="Connector" id="line" icon="connector" />
-        <ToolButton handleClick={() =>app.selectTool("text")} tooltip="Text" id="text" icon="text" />
-        <GeometryTools activeGeometry={activeGeomId} setGeometry={handleToolClick}/>
+        <ToolButton
+          handleClick={() => app.selectTool('logseq-portal')}
+          tooltip="Add block or page"
+          id="logseq-portal"
+          icon="circle-plus"
+        />
+        <ToolButton
+          handleClick={() => app.selectTool('pencil')}
+          tooltip="Draw"
+          id="pencil"
+          icon="ballpen"
+        />
+        <ToolButton
+          handleClick={() => app.selectTool('highlighter')}
+          tooltip="Highlight"
+          id="highlighter"
+          icon="highlight"
+        />
+        <ToolButton
+          handleClick={() => app.selectTool('erase')}
+          tooltip="Eraser"
+          id="erase"
+          icon="eraser"
+        />
+        <ToolButton
+          handleClick={() => app.selectTool('line')}
+          tooltip="Connector"
+          id="line"
+          icon="connector"
+        />
+        <ToolButton
+          handleClick={() => app.selectTool('text')}
+          tooltip="Text"
+          id="text"
+          icon="text"
+        />
+        <GeometryTools activeGeometry={activeGeomId} setGeometry={handleToolClick} />
         <Separator.Root
         <Separator.Root
           className="tl-toolbar-separator"
           className="tl-toolbar-separator"
           orientation="horizontal"
           orientation="horizontal"

+ 30 - 30
tldraw/apps/tldraw-logseq/src/components/ToolButton/ToolButton.tsx

@@ -5,6 +5,7 @@ import { observer } from 'mobx-react-lite'
 import type * as React from 'react'
 import type * as React from 'react'
 import { Button } from '../Button'
 import { Button } from '../Button'
 import { TablerIcon } from '../icons'
 import { TablerIcon } from '../icons'
+import { KeyboardShortcut } from '../KeyboardShortcut'
 
 
 export interface ToolButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
 export interface ToolButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
   id: string
   id: string
@@ -14,37 +15,36 @@ export interface ToolButtonProps extends React.ButtonHTMLAttributes<HTMLButtonEl
   handleClick: (e: React.MouseEvent<HTMLButtonElement>) => void
   handleClick: (e: React.MouseEvent<HTMLButtonElement>) => void
 }
 }
 
 
-export const ToolButton = observer(({ id, icon, tooltip, tooltipSide = "left", handleClick, ...props }: ToolButtonProps) => {
-  const app = useApp()
+export const ToolButton = observer(
+  ({ id, icon, tooltip, tooltipSide = 'left', handleClick, ...props }: ToolButtonProps) => {
+    const app = useApp()
 
 
-  // Tool must exist
-  const Tool = [...app.Tools, TLSelectTool, TLMoveTool]?.find(T => T.id === id)
+    // Tool must exist
+    const Tool = [...app.Tools, TLSelectTool, TLMoveTool]?.find(T => T.id === id)
 
 
-  const shortcuts = (Tool as any)?.['shortcut']
+    const shortcuts = (Tool as any)?.['shortcut']
 
 
-  const tooltipContent = shortcuts && tooltip ? (
-    <>
-      {tooltip}
-      <span className="ml-2 keyboard-shortcut">
-        {shortcuts
-          .map((shortcut: string, idx: number) => <code key={idx}>{shortcut.toUpperCase()}</code>)
-          .reduce((prev: React.ReactNode, curr: React.ReactNode) => [prev, ' | ', curr])}
-      </span>
-    </>
-  ) : (
-    tooltip
-  )
+    const tooltipContent =
+      shortcuts && tooltip ? (
+        <div className="flex">
+          {tooltip}
+          <KeyboardShortcut action={shortcuts} />
+        </div>
+      ) : (
+        tooltip
+      )
 
 
-  return (
-    <Button
-      {...props}
-      tooltipSide={tooltipSide}
-      tooltip={tooltipContent}
-      data-tool={id}
-      data-selected={id === app.selectedTool.id}
-      onClick={handleClick}
-    >
-      {typeof icon === 'string' ? <TablerIcon name={icon} /> : icon}
-    </Button>
-  )
-})
+    return (
+      <Button
+        {...props}
+        tooltipSide={tooltipSide}
+        tooltip={tooltipContent}
+        data-tool={id}
+        data-selected={id === app.selectedTool.id}
+        onClick={handleClick}
+      >
+        {typeof icon === 'string' ? <TablerIcon name={icon} /> : icon}
+      </Button>
+    )
+  }
+)

+ 7 - 25
tldraw/apps/tldraw-logseq/src/components/ZoomMenu/ZoomMenu.tsx

@@ -1,7 +1,9 @@
 import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
 import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
 import { useApp } from '@tldraw/react'
 import { useApp } from '@tldraw/react'
+import { KeyboardShortcut } from '../KeyboardShortcut'
 import { MOD_KEY } from '@tldraw/core'
 import { MOD_KEY } from '@tldraw/core'
 import { observer } from 'mobx-react-lite'
 import { observer } from 'mobx-react-lite'
+import * as React from 'react'
 
 
 export const ZoomMenu = observer(function ZoomMenu(): JSX.Element {
 export const ZoomMenu = observer(function ZoomMenu(): JSX.Element {
   const app = useApp()
   const app = useApp()
@@ -26,11 +28,7 @@ export const ZoomMenu = observer(function ZoomMenu(): JSX.Element {
           onClick={app.api.zoomToFit}
           onClick={app.api.zoomToFit}
         >
         >
           Zoom to drawing
           Zoom to drawing
-          <div className="tl-menu-right-slot">
-            <span className="keyboard-shortcut">
-              <code>⇧</code> <code>1</code>
-            </span>
-          </div>
+          <KeyboardShortcut action="whiteboard/zoom-to-fit" />
         </DropdownMenuPrimitive.Item>
         </DropdownMenuPrimitive.Item>
         <DropdownMenuPrimitive.Item
         <DropdownMenuPrimitive.Item
           className="tl-menu-item"
           className="tl-menu-item"
@@ -39,11 +37,7 @@ export const ZoomMenu = observer(function ZoomMenu(): JSX.Element {
           disabled={app.selectedShapesArray.length === 0}
           disabled={app.selectedShapesArray.length === 0}
         >
         >
           Zoom to fit selection
           Zoom to fit selection
-          <div className="tl-menu-right-slot">
-            <span className="keyboard-shortcut">
-              <code>⇧</code> <code>2</code>
-            </span>
-          </div>
+          <KeyboardShortcut action="whiteboard/zoom-to-selection" />
         </DropdownMenuPrimitive.Item>
         </DropdownMenuPrimitive.Item>
         <DropdownMenuPrimitive.Item
         <DropdownMenuPrimitive.Item
           className="tl-menu-item"
           className="tl-menu-item"
@@ -51,11 +45,7 @@ export const ZoomMenu = observer(function ZoomMenu(): JSX.Element {
           onClick={app.api.zoomIn}
           onClick={app.api.zoomIn}
         >
         >
           Zoom in
           Zoom in
-          <div className="tl-menu-right-slot">
-            <span className="keyboard-shortcut">
-              <code>{MOD_KEY}</code> <code>+</code>
-            </span>
-          </div>
+          <KeyboardShortcut action="whiteboard/zoom-in" />
         </DropdownMenuPrimitive.Item>
         </DropdownMenuPrimitive.Item>
         <DropdownMenuPrimitive.Item
         <DropdownMenuPrimitive.Item
           className="tl-menu-item"
           className="tl-menu-item"
@@ -63,11 +53,7 @@ export const ZoomMenu = observer(function ZoomMenu(): JSX.Element {
           onClick={app.api.zoomOut}
           onClick={app.api.zoomOut}
         >
         >
           Zoom out
           Zoom out
-          <div className="tl-menu-right-slot">
-            <span className="keyboard-shortcut">
-              <code>{MOD_KEY}</code> <code>-</code>
-            </span>
-          </div>
+          <KeyboardShortcut action="whiteboard/zoom-out" />
         </DropdownMenuPrimitive.Item>
         </DropdownMenuPrimitive.Item>
         <DropdownMenuPrimitive.Item
         <DropdownMenuPrimitive.Item
           className="tl-menu-item"
           className="tl-menu-item"
@@ -75,11 +61,7 @@ export const ZoomMenu = observer(function ZoomMenu(): JSX.Element {
           onClick={app.api.resetZoom}
           onClick={app.api.resetZoom}
         >
         >
           Reset zoom
           Reset zoom
-          <div className="tl-menu-right-slot">
-            <span className="keyboard-shortcut">
-              <code>⇧</code> <code>0</code>
-            </span>
-          </div>
+          <KeyboardShortcut action="whiteboard/reset-zoom" />
         </DropdownMenuPrimitive.Item>
         </DropdownMenuPrimitive.Item>
       </DropdownMenuPrimitive.Content>
       </DropdownMenuPrimitive.Content>
     </DropdownMenuPrimitive.Root>
     </DropdownMenuPrimitive.Root>

+ 1 - 0
tldraw/apps/tldraw-logseq/src/components/inputs/SelectInput.tsx

@@ -56,6 +56,7 @@ export function SelectInput({
             position="popper"
             position="popper"
             sideOffset={14}
             sideOffset={14}
             align="center"
             align="center"
+            onKeyDown={e => e.stopPropagation()}
           >
           >
             <Select.ScrollUpButton />
             <Select.ScrollUpButton />
             <Select.Viewport className="tl-select-input-viewport">
             <Select.Viewport className="tl-select-input-viewport">

+ 1 - 1
tldraw/apps/tldraw-logseq/src/components/inputs/ToggleInput.tsx

@@ -4,7 +4,7 @@ import * as Toggle from '@radix-ui/react-toggle'
 interface ToggleInputProps extends React.HTMLAttributes<HTMLElement> {
 interface ToggleInputProps extends React.HTMLAttributes<HTMLElement> {
   toggle?: boolean
   toggle?: boolean
   pressed: boolean
   pressed: boolean
-  tooltip?: string
+  tooltip?: React.ReactNode
   onPressedChange: (value: boolean) => void
   onPressedChange: (value: boolean) => void
 }
 }
 
 

+ 3 - 0
tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts

@@ -37,6 +37,9 @@ export interface LogseqContextValue {
         renderFn?: (open?: boolean, count?: number) => React.ReactNode
         renderFn?: (open?: boolean, count?: number) => React.ReactNode
       }
       }
     }>
     }>
+    KeyboardShortcut: React.FC<{
+      action: string
+    }>
   }
   }
   handlers: {
   handlers: {
     search: (
     search: (

+ 3 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/IFrameShape.tsx

@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import * as React from 'react'
 import * as React from 'react'
 import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
 import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
-import { HTMLContainer, TLComponentProps } from '@tldraw/react'
+import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
 import { action } from 'mobx'
 import { action } from 'mobx'
 import { observer } from 'mobx-react-lite'
 import { observer } from 'mobx-react-lite'
 
 
@@ -37,6 +37,7 @@ export class IFrameShape extends TLBoxShape<IFrameShapeProps> {
 
 
   ReactComponent = observer(({ events, isErasing, isEditing }: TLComponentProps) => {
   ReactComponent = observer(({ events, isErasing, isEditing }: TLComponentProps) => {
     const ref = React.useRef<HTMLIFrameElement>(null)
     const ref = React.useRef<HTMLIFrameElement>(null)
+    const app = useApp<Shape>()
 
 
     return (
     return (
       <HTMLContainer
       <HTMLContainer
@@ -50,7 +51,7 @@ export class IFrameShape extends TLBoxShape<IFrameShapeProps> {
         <div
         <div
           className="tl-iframe-container"
           className="tl-iframe-container"
           style={{
           style={{
-            pointerEvents: isEditing ? 'all' : 'none',
+            pointerEvents: isEditing || app.readOnly ? 'all' : 'none',
             userSelect: 'none',
             userSelect: 'none',
           }}
           }}
         >
         >

+ 0 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/TextShape.tsx

@@ -166,8 +166,6 @@ export class TextShape extends TLTextShape<TextShapeProps> {
             elm.select()
             elm.select()
           }
           }
         })
         })
-      } else {
-        onEditingEnd?.()
       }
       }
     }, [isEditing, onEditingEnd])
     }, [isEditing, onEditingEnd])
 
 

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/shapes/TweetShape.tsx

@@ -85,7 +85,7 @@ export class TweetShape extends TLBoxShape<TweetShapeProps> {
         <div
         <div
           className="rounded-xl w-full h-full relative shadow-xl tl-tweet-container"
           className="rounded-xl w-full h-full relative shadow-xl tl-tweet-container"
           style={{
           style={{
-            pointerEvents: isEditing ? 'all' : 'none',
+            pointerEvents: isEditing || app.readOnly ? 'all' : 'none',
             userSelect: 'none',
             userSelect: 'none',
           }}
           }}
         >
         >

+ 4 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/YouTubeShape.tsx

@@ -1,6 +1,6 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
 import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
-import { HTMLContainer, TLComponentProps } from '@tldraw/react'
+import { HTMLContainer, TLComponentProps, useApp} from '@tldraw/react'
 import { action, computed } from 'mobx'
 import { action, computed } from 'mobx'
 import { observer } from 'mobx-react-lite'
 import { observer } from 'mobx-react-lite'
 import { withClampedStyles } from './style-props'
 import { withClampedStyles } from './style-props'
@@ -45,6 +45,8 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
   }
   }
 
 
   ReactComponent = observer(({ events, isErasing, isEditing, isSelected }: TLComponentProps) => {
   ReactComponent = observer(({ events, isErasing, isEditing, isSelected }: TLComponentProps) => {
+    const app = useApp<Shape>()
+
     return (
     return (
       <HTMLContainer
       <HTMLContainer
         style={{
         style={{
@@ -57,7 +59,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
         <div
         <div
           className="rounded-lg w-full h-full relative overflow-hidden shadow-xl tl-youtube-container"
           className="rounded-lg w-full h-full relative overflow-hidden shadow-xl tl-youtube-container"
           style={{
           style={{
-            pointerEvents: isEditing ? 'all' : 'none',
+            pointerEvents: isEditing || app.readOnly ? 'all' : 'none',
             userSelect: 'none',
             userSelect: 'none',
             background: `url('https://img.youtube.com/vi/${this.embedId}/mqdefault.jpg') no-repeat center/cover`,
             background: `url('https://img.youtube.com/vi/${this.embedId}/mqdefault.jpg') no-repeat center/cover`,
           }}
           }}

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/tools/BoxTool.tsx

@@ -4,6 +4,6 @@ import { BoxShape, type Shape } from '../shapes'
 
 
 export class BoxTool extends TLBoxTool<BoxShape, Shape, TLReactEventMap> {
 export class BoxTool extends TLBoxTool<BoxShape, Shape, TLReactEventMap> {
   static id = 'box'
   static id = 'box'
-  static shortcut = ['9', 'r']
+  static shortcut = 'whiteboard/rectangle'
   Shape = BoxShape
   Shape = BoxShape
 }
 }

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/tools/EllipseTool.tsx

@@ -4,6 +4,6 @@ import { EllipseShape, type Shape } from '../shapes'
 
 
 export class EllipseTool extends TLBoxTool<EllipseShape, Shape, TLReactEventMap> {
 export class EllipseTool extends TLBoxTool<EllipseShape, Shape, TLReactEventMap> {
   static id = 'ellipse'
   static id = 'ellipse'
-  static shortcut = ['o']
+  static shortcut = 'whiteboard/ellipse'
   Shape = EllipseShape
   Shape = EllipseShape
 }
 }

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/tools/EraseTool.tsx

@@ -4,5 +4,5 @@ import type { Shape } from '../shapes'
 
 
 export class NuEraseTool extends TLEraseTool<Shape, TLReactEventMap> {
 export class NuEraseTool extends TLEraseTool<Shape, TLReactEventMap> {
   static id = 'erase'
   static id = 'erase'
-  static shortcut = ['6', 'e']
+  static shortcut = 'whiteboard/eraser'
 }
 }

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/tools/HighlighterTool.tsx

@@ -4,7 +4,7 @@ import { HighlighterShape, type Shape } from '../shapes'
 
 
 export class HighlighterTool extends TLDrawTool<HighlighterShape, Shape, TLReactEventMap> {
 export class HighlighterTool extends TLDrawTool<HighlighterShape, Shape, TLReactEventMap> {
   static id = 'highlighter'
   static id = 'highlighter'
-  static shortcut = ['5', 'h']
+  static shortcut = 'whiteboard/highlighter'
   Shape = HighlighterShape
   Shape = HighlighterShape
   simplify = true
   simplify = true
   simplifyTolerance = 0.618
   simplifyTolerance = 0.618

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/tools/LineTool.tsx

@@ -5,6 +5,6 @@ import { LineShape, type Shape } from '../shapes'
 // @ts-expect-error maybe later
 // @ts-expect-error maybe later
 export class LineTool extends TLLineTool<LineShape, Shape, TLReactEventMap> {
 export class LineTool extends TLLineTool<LineShape, Shape, TLReactEventMap> {
   static id = 'line'
   static id = 'line'
-  static shortcut = ['7', 'c']
+  static shortcut = 'whiteboard/connector'
   Shape = LineShape
   Shape = LineShape
 }
 }

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/tools/LogseqPortalTool/LogseqPortalTool.tsx

@@ -9,7 +9,7 @@ export class LogseqPortalTool extends TLTool<
   TLApp<Shape, TLReactEventMap>
   TLApp<Shape, TLReactEventMap>
 > {
 > {
   static id = 'logseq-portal'
   static id = 'logseq-portal'
-  static shortcut = ['3']
+  static shortcut = 'whiteboard/portal'
   static states = [IdleState, CreatingState]
   static states = [IdleState, CreatingState]
   static initial = 'idle'
   static initial = 'idle'
 
 

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/tools/PencilTool.tsx

@@ -4,7 +4,7 @@ import { PencilShape, type Shape } from '../shapes'
 
 
 export class PencilTool extends TLDrawTool<PencilShape, Shape, TLReactEventMap> {
 export class PencilTool extends TLDrawTool<PencilShape, Shape, TLReactEventMap> {
   static id = 'pencil'
   static id = 'pencil'
-  static shortcut = ['4', 'd']
+  static shortcut = 'whiteboard/pencil'
   Shape = PencilShape
   Shape = PencilShape
   simplify = false
   simplify = false
 }
 }

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/tools/TextTool.tsx

@@ -4,6 +4,6 @@ import { TextShape, type Shape } from '../shapes'
 
 
 export class TextTool extends TLTextTool<TextShape, Shape, TLReactEventMap> {
 export class TextTool extends TLTextTool<TextShape, Shape, TLReactEventMap> {
   static id = 'text'
   static id = 'text'
-  static shortcut = ['8', 't']
+  static shortcut = 'whiteboard/text'
   Shape = TextShape
   Shape = TextShape
 }
 }

+ 1 - 1
tldraw/apps/tldraw-logseq/src/styles.css

@@ -147,7 +147,7 @@ html[data-theme='light'] {
 
 
 .tl-menu-right-slot {
 .tl-menu-right-slot {
   margin-left: auto;
   margin-left: auto;
-  padding-left: 40px;
+  padding-left: 20px;
 
 
   .keyboard-shortcut > code {
   .keyboard-shortcut > code {
     padding: 4px !important;
     padding: 4px !important;

+ 2 - 2
tldraw/package.json

@@ -17,7 +17,7 @@
     "demo"
     "demo"
   ],
   ],
   "scripts": {
   "scripts": {
-    "build": "cd apps/tldraw-logseq && yarn build",
+    "build": "yarn --cwd apps/tldraw-logseq build",
     "postinstall": "yarn build",
     "postinstall": "yarn build",
     "dev": "cd demo && yarn dev",
     "dev": "cd demo && yarn dev",
     "fix:style": "yarn run pretty-quick",
     "fix:style": "yarn run pretty-quick",
@@ -41,4 +41,4 @@
   "dependencies": {
   "dependencies": {
     "@types/uuid": "^8.3.4"
     "@types/uuid": "^8.3.4"
   }
   }
-}
+}

+ 4 - 13
tldraw/packages/core/src/lib/TLApi/TLApi.ts

@@ -43,7 +43,9 @@ export class TLApi<S extends TLShape = TLShape, K extends TLEventMap = TLEventMa
    *
    *
    * @param shapes The serialized shape changes to apply.
    * @param shapes The serialized shape changes to apply.
    */
    */
-  updateShapes = <T extends S>(...shapes: ({ id: string, type: string } & Partial<T['props']>)[]): this => {
+  updateShapes = <T extends S>(
+    ...shapes: ({ id: string; type: string } & Partial<T['props']>)[]
+  ): this => {
     this.app.updateShapes(shapes)
     this.app.updateShapes(shapes)
     return this
     return this
   }
   }
@@ -196,16 +198,6 @@ export class TLApi<S extends TLShape = TLShape, K extends TLEventMap = TLEventMa
     return this
     return this
   }
   }
 
 
-  save = () => {
-    this.app.save()
-    return this
-  }
-
-  saveAs = () => {
-    this.app.save()
-    return this
-  }
-
   undo = () => {
   undo = () => {
     this.app.undo()
     this.app.undo()
     return this
     return this
@@ -445,8 +437,7 @@ export class TLApi<S extends TLShape = TLShape, K extends TLEventMap = TLEventMa
 
 
   setCollapsed = (collapsed: boolean, shapes: S[] = this.app.allSelectedShapesArray) => {
   setCollapsed = (collapsed: boolean, shapes: S[] = this.app.allSelectedShapesArray) => {
     shapes.forEach(shape => {
     shapes.forEach(shape => {
-      if (shape.props.type === 'logseq-portal')
-        shape.setCollapsed(collapsed)
+      if (shape.props.type === 'logseq-portal') shape.setCollapsed(collapsed)
     })
     })
     this.app.persist()
     this.app.persist()
   }
   }

+ 5 - 170
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -17,14 +17,7 @@ import type {
   TLSubscriptionEventName,
   TLSubscriptionEventName,
 } from '../../types'
 } from '../../types'
 import { AlignType, DistributeType } from '../../types'
 import { AlignType, DistributeType } from '../../types'
-import {
-  BoundsUtils,
-  createNewLineBinding,
-  dedupe,
-  isNonNullable,
-  KeyUtils,
-  uniqueId,
-} from '../../utils'
+import { BoundsUtils, createNewLineBinding, dedupe, isNonNullable, uniqueId } from '../../utils'
 import type { TLShape, TLShapeConstructor, TLShapeModel } from '../shapes'
 import type { TLShape, TLShapeConstructor, TLShapeModel } from '../shapes'
 import { TLApi } from '../TLApi'
 import { TLApi } from '../TLApi'
 import { TLCursors } from '../TLCursors'
 import { TLCursors } from '../TLCursors'
@@ -77,7 +70,6 @@ export class TLApp<
     this.notify('mount', null)
     this.notify('mount', null)
   }
   }
 
 
-  keybindingRegistered = false
   uuid = uniqueId()
   uuid = uniqueId()
 
 
   readOnly: boolean | undefined
   readOnly: boolean | undefined
@@ -93,159 +85,6 @@ export class TLApp<
 
 
   Tools: TLToolConstructor<S, K>[] = []
   Tools: TLToolConstructor<S, K>[] = []
 
 
-  dispose() {
-    super.dispose()
-    this.keybindingRegistered = false
-    return this
-  }
-
-  initKeyboardShortcuts() {
-    if (this.keybindingRegistered) {
-      return
-    }
-    const ownShortcuts: TLShortcut<S, K>[] = [
-      {
-        keys: 'shift+0',
-        fn: () => this.api.resetZoom(),
-      },
-      {
-        keys: 'shift+1',
-        fn: () => this.api.zoomToFit(),
-      },
-      {
-        keys: 'shift+2',
-        fn: () => this.api.zoomToSelection(),
-      },
-      {
-        keys: 'mod+up',
-        fn: () => this.api.setCollapsed(true),
-      },
-      {
-        keys: 'mod+down',
-        fn: () => this.api.setCollapsed(false),
-      },
-      {
-        keys: 'mod+-',
-        fn: () => this.api.zoomOut(),
-      },
-      {
-        keys: 'mod+=',
-        fn: () => this.api.zoomIn(),
-      },
-      {
-        keys: 'mod+x',
-        fn: () => this.cut(),
-      },
-      {
-        keys: '[',
-        fn: () => this.sendBackward(),
-      },
-      {
-        keys: 'shift+[',
-        fn: () => this.sendToBack(),
-      },
-      {
-        keys: ']',
-        fn: () => this.bringForward(),
-      },
-      {
-        keys: 'shift+]',
-        fn: () => this.bringToFront(),
-      },
-      {
-        keys: 'mod+a',
-        fn: () => {
-          const { selectedTool } = this
-          if (selectedTool.id !== 'select') {
-            this.selectTool('select')
-          }
-          this.api.selectAll()
-        },
-      },
-      {
-        keys: 'mod+shift+s',
-        fn: () => {
-          this.saveAs()
-          this.notify('saveAs', null)
-        },
-      },
-      {
-        keys: 'mod+shift+v',
-        fn: (_, __, e) => {
-          if (!this.editingShape) {
-            e.preventDefault()
-            this.paste(undefined, true)
-          }
-        },
-      },
-      {
-        keys: ['del', 'backspace'],
-        fn: () => {
-          if (!this.editingShape) {
-            this.api.deleteShapes()
-            this.selectedTool.transition('idle')
-          }
-        },
-      },
-      {
-        keys: 'mod+g',
-        fn: () => {
-          this.api.doGroup()
-        },
-      },
-      {
-        keys: 'mod+shift+g',
-        fn: () => {
-          this.api.unGroup()
-        },
-      },
-      {
-        keys: 'shift+g',
-        fn: () => {
-          this.api.toggleGrid()
-        },
-      },
-      {
-        keys: 'mod+l',
-        fn: () => {
-          this.setLocked(true)
-        },
-      },
-      {
-        keys: 'mod+shift+l',
-        fn: () => {
-          this.setLocked(false)
-        },
-      },
-    ]
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-ignore
-    const shortcuts = (this.constructor['shortcuts'] || []) as TLShortcut<S, K>[]
-    const childrenShortcuts = Array.from(this.children.values())
-      // @ts-expect-error ???
-      .filter(c => c.constructor['shortcut'])
-      .map(child => {
-        return {
-          // @ts-expect-error ???
-          keys: child.constructor['shortcut'] as string | string[],
-          fn: (_: any, __: any, e: KeyboardEvent) => {
-            this.selectTool(child.id)
-            // hack: allows logseq related shortcut combinations to work
-            // fixme?: unsure if it will cause unexpected issues
-            // e.stopPropagation()
-          },
-        }
-      })
-    this._disposables.push(
-      ...[...ownShortcuts, ...shortcuts, ...childrenShortcuts].map(({ keys, fn }) => {
-        return KeyUtils.registerShortcut(keys, e => {
-          fn(this, this, e)
-        })
-      })
-    )
-    this.keybindingRegistered = true
-  }
-
   /* --------------------- History -------------------- */
   /* --------------------- History -------------------- */
 
 
   history = new TLHistory<S, K>(this)
   history = new TLHistory<S, K>(this)
@@ -292,12 +131,6 @@ export class TLApp<
     return this
     return this
   }
   }
 
 
-  saveAs = (): this => {
-    // todo
-    this.notify('saveAs', null)
-    return this
-  }
-
   @computed get serialized(): TLDocumentModel<S> {
   @computed get serialized(): TLDocumentModel<S> {
     return {
     return {
       // currentPageId: this.currentPageId,
       // currentPageId: this.currentPageId,
@@ -355,14 +188,16 @@ export class TLApp<
     return this
     return this
   }
   }
 
 
-  @action updateShapes = <T extends S>(shapes: ({ id: string, type: string } & Partial<T['props']>)[]): this => {
+  @action updateShapes = <T extends S>(
+    shapes: ({ id: string; type: string } & Partial<T['props']>)[]
+  ): this => {
     if (this.readOnly) return this
     if (this.readOnly) return this
 
 
     shapes.forEach(shape => {
     shapes.forEach(shape => {
       const oldShape = this.getShapeById(shape.id)
       const oldShape = this.getShapeById(shape.id)
       oldShape?.update(shape)
       oldShape?.update(shape)
       if (shape.type !== oldShape?.type) {
       if (shape.type !== oldShape?.type) {
-        this.api.convertShapes(shape.type , [oldShape])
+        this.api.convertShapes(shape.type, [oldShape])
       }
       }
     })
     })
     this.persist()
     this.persist()

+ 0 - 21
tldraw/packages/core/src/lib/TLState.ts

@@ -9,7 +9,6 @@ import type {
   TLShortcut,
   TLShortcut,
   TLEvents,
   TLEvents,
 } from '../types'
 } from '../types'
-import { KeyUtils } from '../utils'
 import type { TLShape } from './shapes'
 import type { TLShape } from './shapes'
 
 
 export interface TLStateClass<
 export interface TLStateClass<
@@ -120,7 +119,6 @@ export abstract class TLRootState<S extends TLShape, K extends TLEventMap>
       if (this.currentState) {
       if (this.currentState) {
         prevState._events.onExit({ ...data, toId: id })
         prevState._events.onExit({ ...data, toId: id })
         prevState.dispose()
         prevState.dispose()
-        nextState.registerKeyboardShortcuts()
         this.setCurrentState(nextState)
         this.setCurrentState(nextState)
         this._events.onTransition({ ...data, fromId: prevState.id, toId: id })
         this._events.onTransition({ ...data, fromId: prevState.id, toId: id })
         nextState._events.onEnter({ ...data, fromId: prevState.id })
         nextState._events.onEnter({ ...data, fromId: prevState.id })
@@ -404,34 +402,15 @@ export abstract class TLState<
       }
       }
     }
     }
 
 
-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-ignore
-    const shortcuts = this.constructor['shortcuts'] as TLShortcut<S, K>[]
-    this._shortcuts = shortcuts
-
     makeObservable(this)
     makeObservable(this)
   }
   }
 
 
   static cursor?: TLCursor
   static cursor?: TLCursor
 
 
-  registerKeyboardShortcuts = (): void => {
-    if (!this._shortcuts?.length) return
-
-    this._disposables.push(
-      ...this._shortcuts.map(({ keys, fn }) => {
-        return KeyUtils.registerShortcut(keys, event => {
-          if (!this.isActive) return
-          fn(this.root, this, event)
-        })
-      })
-    )
-  }
-
   /* --------------------- States --------------------- */
   /* --------------------- States --------------------- */
 
 
   protected _root: R
   protected _root: R
   protected _parent: P
   protected _parent: P
-  private _shortcuts: TLShortcut<S, K>[] = []
 
 
   get root() {
   get root() {
     return this._root
     return this._root

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLMoveTool/TLMoveTool.ts

@@ -10,7 +10,7 @@ export class TLMoveTool<
   R extends TLApp<S, K> = TLApp<S, K>
   R extends TLApp<S, K> = TLApp<S, K>
 > extends TLTool<S, K, R> {
 > extends TLTool<S, K, R> {
   static id = 'move'
   static id = 'move'
-  static shortcut = ['2', 'm']
+  static shortcut = 'whiteboard/pan'
 
 
   static states = [IdleState, IdleHoldState, PanningState, PinchingState]
   static states = [IdleState, IdleHoldState, PanningState, PinchingState]
 
 

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLSelectTool/TLSelectTool.tsx

@@ -33,7 +33,7 @@ export class TLSelectTool<
 
 
   static initial = 'idle'
   static initial = 'idle'
 
 
-  static shortcut = ['1', 's']
+  static shortcut = 'whiteboard/select'
 
 
   static states = [
   static states = [
     IdleState,
     IdleState,

+ 3 - 13
tldraw/packages/core/src/types/types.ts

@@ -15,9 +15,9 @@ export enum Color {
 }
 }
 
 
 export enum Geometry {
 export enum Geometry {
-    Box = 'box',
-    Ellipse = 'ellipse',
-    Polygon = 'polygon',
+  Box = 'box',
+  Ellipse = 'ellipse',
+  Polygon = 'polygon',
 }
 }
 
 
 export enum AlignType {
 export enum AlignType {
@@ -163,14 +163,6 @@ export type TLSubscriptionEvent =
       event: 'persist'
       event: 'persist'
       info: { replace: boolean }
       info: { replace: boolean }
     }
     }
-  | {
-      event: 'save'
-      info: null
-    }
-  | {
-      event: 'saveAs'
-      info: null
-    }
   | {
   | {
       event: 'undo'
       event: 'undo'
       info: null
       info: null
@@ -260,8 +252,6 @@ export interface TLCallbacks<
 > {
 > {
   onMount: TLCallback<S, K, R, 'mount'>
   onMount: TLCallback<S, K, R, 'mount'>
   onPersist: TLCallback<S, K, R, 'persist'>
   onPersist: TLCallback<S, K, R, 'persist'>
-  onSave: TLCallback<S, K, R, 'save'>
-  onSaveAs: TLCallback<S, K, R, 'saveAs'>
   onError: TLCallback<S, K, R, 'error'>
   onError: TLCallback<S, K, R, 'error'>
 }
 }
 
 

+ 0 - 32
tldraw/packages/core/src/utils/KeyUtils.ts

@@ -1,32 +0,0 @@
-import Mousetrap from 'mousetrap'
-
-type AvailableTags = 'INPUT' | 'TEXTAREA' | 'SELECT'
-
-const tagFilter = ({ target }: KeyboardEvent, enableOnTags?: AvailableTags[]) => {
-  const targetTagName = target && (target as HTMLElement).tagName
-  return Boolean(
-    targetTagName && enableOnTags && enableOnTags.includes(targetTagName as AvailableTags)
-  )
-}
-
-export class KeyUtils {
-  static registerShortcut(
-    keys: string | string[],
-    callback: (keyboardEvent: Mousetrap.ExtendedKeyboardEvent, combo: string) => void
-  ) {
-    const fn = (keyboardEvent: Mousetrap.ExtendedKeyboardEvent, combo: string): void => {
-      keyboardEvent.preventDefault()
-      if (
-        tagFilter(keyboardEvent, ['INPUT', 'TEXTAREA', 'SELECT']) ||
-        (keyboardEvent.target as HTMLElement)?.isContentEditable
-      ) {
-        return
-      }
-      callback(keyboardEvent, combo)
-    }
-    Mousetrap.bind(keys, fn, 'keydown')
-    return () => {
-      Mousetrap.unbind(keys, 'keydown')
-    }
-  }
-}

+ 7 - 7
tldraw/packages/core/src/utils/index.ts

@@ -1,7 +1,6 @@
 import * as uuid from 'uuid'
 import * as uuid from 'uuid'
 export * from './BoundsUtils'
 export * from './BoundsUtils'
 export * from './PointUtils'
 export * from './PointUtils'
-export * from './KeyUtils'
 export * from './GeomUtils'
 export * from './GeomUtils'
 export * from './PolygonUtils'
 export * from './PolygonUtils'
 export * from './SvgPathUtils'
 export * from './SvgPathUtils'
@@ -81,13 +80,14 @@ export function isDarwin(): boolean {
   return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
   return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
 }
 }
 
 
-export function isDev():boolean {
-  return window?.logseq?.api?.get_state_from_store?.('ui/developer-mode?') || process.env.NODE_ENV === 'development'
+export function isDev(): boolean {
+  return (
+    window?.logseq?.api?.get_state_from_store?.('ui/developer-mode?') ||
+    process.env.NODE_ENV === 'development'
+  )
 }
 }
 
 
-/**
- * Migrated from frontend.util/safari?
- */
+/** Migrated from frontend.util/safari? */
 export function isSafari(): boolean {
 export function isSafari(): boolean {
   const ua = window.navigator.userAgent.toLowerCase()
   const ua = window.navigator.userAgent.toLowerCase()
   return ua.includes('webkit') && !ua.includes('chrome')
   return ua.includes('webkit') && !ua.includes('chrome')
@@ -102,7 +102,7 @@ export function modKey(e: any): boolean {
   return isDarwin() ? e.metaKey : e.ctrlKey
   return isDarwin() ? e.metaKey : e.ctrlKey
 }
 }
 
 
-export const MOD_KEY = isDarwin() ? '⌘' : 'Ctrl'
+export const MOD_KEY = isDarwin() ? '⌘' : 'ctrl'
 
 
 export function isNonNullable<TValue>(value: TValue): value is NonNullable<TValue> {
 export function isNonNullable<TValue>(value: TValue): value is NonNullable<TValue> {
   return Boolean(value)
   return Boolean(value)

+ 0 - 1
tldraw/packages/react/src/hooks/useAppSetup.ts

@@ -11,7 +11,6 @@ export function useAppSetup<S extends TLReactShape, R extends TLReactApp<S> = TL
   )
   )
 
 
   React.useLayoutEffect(() => {
   React.useLayoutEffect(() => {
-    app.initKeyboardShortcuts()
     return () => {
     return () => {
       app.dispose()
       app.dispose()
     }
     }

+ 1 - 5
tldraw/packages/react/src/hooks/useSetup.ts

@@ -10,8 +10,6 @@ export function useSetup<
 >(app: R, props: TLAppPropsWithApp<S, R> | TLAppPropsWithoutApp<S, R>) {
 >(app: R, props: TLAppPropsWithApp<S, R> | TLAppPropsWithoutApp<S, R>) {
   const {
   const {
     onPersist,
     onPersist,
-    onSave,
-    onSaveAs,
     onError,
     onError,
     onMount,
     onMount,
     onCreateAssets,
     onCreateAssets,
@@ -43,8 +41,6 @@ export function useSetup<
   React.useLayoutEffect(() => {
   React.useLayoutEffect(() => {
     const unsubs: (() => void)[] = []
     const unsubs: (() => void)[] = []
     if (onPersist) unsubs.push(app.subscribe('persist', onPersist))
     if (onPersist) unsubs.push(app.subscribe('persist', onPersist))
-    if (onSave) unsubs.push(app.subscribe('save', onSave))
-    if (onSaveAs) unsubs.push(app.subscribe('saveAs', onSaveAs))
     if (onError) unsubs.push(app.subscribe('error', onError))
     if (onError) unsubs.push(app.subscribe('error', onError))
     if (onCreateShapes) unsubs.push(app.subscribe('create-shapes', onCreateShapes))
     if (onCreateShapes) unsubs.push(app.subscribe('create-shapes', onCreateShapes))
     if (onCreateAssets) unsubs.push(app.subscribe('create-assets', onCreateAssets))
     if (onCreateAssets) unsubs.push(app.subscribe('create-assets', onCreateAssets))
@@ -56,5 +52,5 @@ export function useSetup<
     if (onCanvasDBClick) unsubs.push(app.subscribe('canvas-dbclick', onCanvasDBClick))
     if (onCanvasDBClick) unsubs.push(app.subscribe('canvas-dbclick', onCanvasDBClick))
     // Kind of unusual, is this the right pattern?
     // Kind of unusual, is this the right pattern?
     return () => unsubs.forEach(unsub => unsub())
     return () => unsubs.forEach(unsub => unsub())
-  }, [app, onPersist, onSave, onSaveAs, onError])
+  }, [app, onPersist, onError])
 }
 }

+ 0 - 2
tldraw/packages/react/src/types/TLReactSubscriptions.tsx

@@ -31,8 +31,6 @@ export interface TLReactCallbacks<
 > {
 > {
   onMount: TLReactCallback<S, R, 'mount'>
   onMount: TLReactCallback<S, R, 'mount'>
   onPersist: TLReactCallback<S, R, 'persist'>
   onPersist: TLReactCallback<S, R, 'persist'>
-  onSave: TLReactCallback<S, R, 'save'>
-  onSaveAs: TLReactCallback<S, R, 'saveAs'>
   onError: TLReactCallback<S, R, 'error'>
   onError: TLReactCallback<S, R, 'error'>
   onCreateShapes: TLReactCallback<S, R, 'create-shapes'>
   onCreateShapes: TLReactCallback<S, R, 'create-shapes'>
   onCreateAssets: TLReactCallback<S, R, 'create-assets'>
   onCreateAssets: TLReactCallback<S, R, 'create-assets'>

+ 80 - 43
tldraw/yarn.lock

@@ -1115,11 +1115,12 @@
   dependencies:
   dependencies:
     "@types/ms" "*"
     "@types/ms" "*"
 
 
-"@types/fs-extra@^9.0.13":
-  version "9.0.13"
-  resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45"
-  integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==
+"@types/fs-extra@^11.0.1":
+  version "11.0.1"
+  resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.1.tgz#f542ec47810532a8a252127e6e105f487e0a6ea5"
+  integrity sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==
   dependencies:
   dependencies:
+    "@types/jsonfile" "*"
     "@types/node" "*"
     "@types/node" "*"
 
 
 "@types/glob@^7.1.1":
 "@types/glob@^7.1.1":
@@ -1142,6 +1143,13 @@
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
   integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
   integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
 
 
+"@types/jsonfile@*":
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.1.tgz#ac84e9aefa74a2425a0fb3012bdea44f58970f1b"
+  integrity sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==
+  dependencies:
+    "@types/node" "*"
+
 "@types/mdast@^3.0.0":
 "@types/mdast@^3.0.0":
   version "3.0.10"
   version "3.0.10"
   resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
   resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
@@ -1179,10 +1187,10 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850"
   integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==
   integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==
 
 
-"@types/node@^18.7.20":
-  version "18.8.3"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.3.tgz#ce750ab4017effa51aed6a7230651778d54e327c"
-  integrity sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==
+"@types/node@^18.16.3":
+  version "18.16.3"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.3.tgz#6bda7819aae6ea0b386ebc5b24bdf602f1b42b01"
+  integrity sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==
 
 
 "@types/offscreencanvas@^2019.6.4":
 "@types/offscreencanvas@^2019.6.4":
   version "2019.7.0"
   version "2019.7.0"
@@ -1240,10 +1248,10 @@
   resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
   resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
   integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
   integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
 
 
-"@types/which@^2.0.1":
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.1.tgz#27ecd67f915b7c3d6ba552135bb1eecd66e63501"
-  integrity sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==
+"@types/which@^3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/which/-/which-3.0.0.tgz#849afdd9fdcb0b67339b9cfc80fa6ea4e0253fc5"
+  integrity sha512-ASCxdbsrwNfSMXALlC3Decif9rwDMu+80KGp5zI2RLRotfMsTv7fHL8W8VDp24wymzDyIFudhUeSCugrgRFfHQ==
 
 
 "@typescript-eslint/eslint-plugin@^5.36.1":
 "@typescript-eslint/eslint-plugin@^5.36.1":
   version "5.36.1"
   version "5.36.1"
@@ -1821,10 +1829,10 @@ chalk@^4.0.0, chalk@^4.1.0:
     ansi-styles "^4.1.0"
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
     supports-color "^7.1.0"
 
 
-chalk@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6"
-  integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==
+chalk@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3"
+  integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==
 
 
 character-entities@^2.0.0:
 character-entities@^2.0.0:
   version "2.0.2"
   version "2.0.2"
@@ -2950,10 +2958,10 @@ from@~0:
   resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
   resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
   integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==
   integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==
 
 
-fs-extra@^10.1.0:
-  version "10.1.0"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
-  integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
+fs-extra@^11.1.1:
+  version "11.1.1"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"
+  integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==
   dependencies:
   dependencies:
     graceful-fs "^4.2.0"
     graceful-fs "^4.2.0"
     jsonfile "^6.0.1"
     jsonfile "^6.0.1"
@@ -2979,6 +2987,11 @@ functional-red-black-tree@^1.0.1:
   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
   integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==
   integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==
 
 
+fx@*:
+  version "27.0.0"
+  resolved "https://registry.yarnpkg.com/fx/-/fx-27.0.0.tgz#8322e81c7b66db169663765abe6497d527673de3"
+  integrity sha512-am6jTZW1vTfdc42QH63qqtN5QoNb7JiD+DH40SokzVKSofKCcqSAq1V6ASCR/d3R2YyLFD68h6nWaSVt/BZqDA==
+
 gensync@^1.0.0-beta.2:
 gensync@^1.0.0-beta.2:
   version "1.0.0-beta.2"
   version "1.0.0-beta.2"
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@@ -3082,10 +3095,10 @@ globby@^11.0.3, globby@^11.1.0:
     merge2 "^1.4.1"
     merge2 "^1.4.1"
     slash "^3.0.0"
     slash "^3.0.0"
 
 
-globby@^13.1.2:
-  version "13.1.2"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515"
-  integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==
+globby@^13.1.4:
+  version "13.1.4"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.4.tgz#2f91c116066bcec152465ba36e5caa4a13c01317"
+  integrity sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==
   dependencies:
   dependencies:
     dir-glob "^3.0.1"
     dir-glob "^3.0.1"
     fast-glob "^3.2.11"
     fast-glob "^3.2.11"
@@ -3767,6 +3780,11 @@ minimist@^1.2.6:
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
   integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
   integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
 
 
+minimist@^1.2.8:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+  integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
 mobx-react-lite@^3.4.0:
 mobx-react-lite@^3.4.0:
   version "3.4.0"
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.4.0.tgz#d59156a96889cdadad751e5e4dab95f28926dfff"
   resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.4.0.tgz#d59156a96889cdadad751e5e4dab95f28926dfff"
@@ -3832,10 +3850,10 @@ node-domexception@^1.0.0:
   resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
   resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
   integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
   integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
 
 
-node-fetch@3.2.10:
-  version "3.2.10"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8"
-  integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==
+node-fetch@3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.1.tgz#b3eea7b54b3a48020e46f4f88b9c5a7430d20b2e"
+  integrity sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==
   dependencies:
   dependencies:
     data-uri-to-buffer "^4.0.0"
     data-uri-to-buffer "^4.0.0"
     fetch-blob "^3.1.4"
     fetch-blob "^3.1.4"
@@ -5134,6 +5152,11 @@ webpack-virtual-modules@^0.4.3:
   resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.6.tgz#3e4008230731f1db078d9cb6f68baf8571182b45"
   resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.6.tgz#3e4008230731f1db078d9cb6f68baf8571182b45"
   integrity sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==
   integrity sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==
 
 
+webpod@^0:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/webpod/-/webpod-0.0.2.tgz#b577c93604fd23596488735887168b3236e3adae"
+  integrity sha512-cSwwQIeg8v4i3p4ajHhwgR7N6VyxAf+KYSSsY6Pd3aETE+xEU4vbitz7qQkB0I321xnhDdgtxuiSfk5r/FVtjg==
+
 whatwg-url@^7.0.0:
 whatwg-url@^7.0.0:
   version "7.1.0"
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
@@ -5150,13 +5173,20 @@ which@^1.3.1:
   dependencies:
   dependencies:
     isexe "^2.0.0"
     isexe "^2.0.0"
 
 
-which@^2.0.1, which@^2.0.2:
+which@^2.0.1:
   version "2.0.2"
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
   dependencies:
   dependencies:
     isexe "^2.0.0"
     isexe "^2.0.0"
 
 
+which@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-3.0.1.tgz#89f1cd0c23f629a8105ffe69b8172791c87b4be1"
+  integrity sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==
+  dependencies:
+    isexe "^2.0.0"
+
 word-wrap@^1.2.3:
 word-wrap@^1.2.3:
   version "1.2.3"
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
@@ -5215,6 +5245,11 @@ yaml@^2.1.1:
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.1.tgz#1e06fb4ca46e60d9da07e4f786ea370ed3c3cfec"
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.1.tgz#1e06fb4ca46e60d9da07e4f786ea370ed3c3cfec"
   integrity sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==
   integrity sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==
 
 
+yaml@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073"
+  integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==
+
 yargs-parser@^21.0.0:
 yargs-parser@^21.0.0:
   version "21.1.1"
   version "21.1.1"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
@@ -5238,21 +5273,23 @@ yocto-queue@^0.1.0:
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
 
 
-zx@^7.1.1:
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/zx/-/zx-7.1.1.tgz#8dc709fb8acd90ae5f39476145e5a2f3883dca1e"
-  integrity sha512-5YlTO2AJ+Ku2YuZKSSSqnUKuagcM/f/j4LmHs15O84Ch80Z9gzR09ZK3gR7GV+rc8IFpz2H/XNFtFVmj31yrZA==
+zx@^7.2.2:
+  version "7.2.2"
+  resolved "https://registry.yarnpkg.com/zx/-/zx-7.2.2.tgz#fe1e9ba84a3806f6ea9ece39e0aea1cd7e5a4855"
+  integrity sha512-50Gjicd6ijTt7Zcz5fNX+rHrmE0uVqC+X6lYKhf2Cu8wIxDpNIzXwTmzchNdW+JY3LFsRcU43B1lHE4HBMmKgQ==
   dependencies:
   dependencies:
-    "@types/fs-extra" "^9.0.13"
+    "@types/fs-extra" "^11.0.1"
     "@types/minimist" "^1.2.2"
     "@types/minimist" "^1.2.2"
-    "@types/node" "^18.7.20"
+    "@types/node" "^18.16.3"
     "@types/ps-tree" "^1.1.2"
     "@types/ps-tree" "^1.1.2"
-    "@types/which" "^2.0.1"
-    chalk "^5.0.1"
-    fs-extra "^10.1.0"
-    globby "^13.1.2"
-    minimist "^1.2.6"
-    node-fetch "3.2.10"
+    "@types/which" "^3.0.0"
+    chalk "^5.2.0"
+    fs-extra "^11.1.1"
+    fx "*"
+    globby "^13.1.4"
+    minimist "^1.2.8"
+    node-fetch "3.3.1"
     ps-tree "^1.2.0"
     ps-tree "^1.2.0"
-    which "^2.0.2"
-    yaml "^2.1.1"
+    webpod "^0"
+    which "^3.0.0"
+    yaml "^2.2.2"