Explorar o código

Merge branch 'master' into feat/tweet-shape

Konstantinos Kaloutas %!s(int64=2) %!d(string=hai) anos
pai
achega
7f471e865f
Modificáronse 46 ficheiros con 650 adicións e 187 borrados
  1. 3 1
      .clj-kondo/config.edn
  2. 6 1
      README.md
  3. 2 2
      android/app/build.gradle
  4. 1 1
      deps.edn
  5. 4 0
      deps/graph-parser/.carve/ignore
  6. 1 1
      deps/graph-parser/src/logseq/graph_parser.cljs
  7. 1 1
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  8. 1 1
      deps/graph-parser/src/logseq/graph_parser/extract.cljc
  9. 8 2
      deps/graph-parser/src/logseq/graph_parser/text.cljs
  10. 12 1
      deps/graph-parser/src/logseq/graph_parser/util.cljs
  11. 26 8
      docs/dev-practices.md
  12. 87 0
      e2e-tests/editor.spec.ts
  13. 1 1
      e2e-tests/whiteboards.spec.ts
  14. 4 4
      ios/App/App.xcodeproj/project.pbxproj
  15. 1 1
      libs/src/LSPlugin.ts
  16. 1 1
      resources/package.json
  17. 57 32
      src/electron/electron/handler.cljs
  18. 4 2
      src/electron/electron/search.cljs
  19. 12 3
      src/main/frontend/components/block.cljs
  20. 118 0
      src/main/frontend/components/bug_report.cljs
  21. 7 0
      src/main/frontend/components/bug_report.css
  22. 2 3
      src/main/frontend/components/header.cljs
  23. 3 1
      src/main/frontend/components/query_table.cljs
  24. 2 0
      src/main/frontend/components/search.cljs
  25. 5 1
      src/main/frontend/components/sidebar.css
  26. 9 3
      src/main/frontend/db/query_custom.cljs
  27. 1 2
      src/main/frontend/db/utils.cljs
  28. 33 33
      src/main/frontend/extensions/pdf/assets.cljs
  29. 62 44
      src/main/frontend/extensions/pdf/highlights.cljs
  30. 1 0
      src/main/frontend/extensions/pdf/utils.cljs
  31. 3 2
      src/main/frontend/fs/sync.cljs
  32. 4 4
      src/main/frontend/handler/editor.cljs
  33. 5 1
      src/main/frontend/handler/editor/keyboards.cljs
  34. 16 17
      src/main/frontend/handler/file_sync.cljs
  35. 33 1
      src/main/frontend/handler/page.cljs
  36. 1 2
      src/main/frontend/handler/repo.cljs
  37. 1 0
      src/main/frontend/handler/search.cljs
  38. 1 1
      src/main/frontend/handler/user.cljs
  39. 3 0
      src/main/frontend/publishing.cljs
  40. 75 0
      src/main/frontend/pubsub.cljc
  41. 11 2
      src/main/frontend/routes.cljs
  42. 3 0
      src/main/frontend/search.cljs
  43. 4 5
      src/main/frontend/util.cljc
  44. 1 1
      src/main/frontend/version.cljs
  45. 12 0
      src/test/frontend/db/query_custom_test.cljs
  46. 2 1
      src/test/frontend/extensions/pdf/assets_test.cljs

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

@@ -22,7 +22,9 @@
                              frontend.util/node-path.dirname
                              frontend.util/node-path.join
                              frontend.util/node-path.extname
-                             frontend.util/node-path.name]}
+                             frontend.util/node-path.name
+                             ;; frontend.pubsub/def-mult-or-pub generate vars clj-kondo cannot resolve
+                             frontend.pubsub]}
 
   :consistent-alias
   {:aliases {cljs.reader reader

+ 6 - 1
README.md

@@ -90,7 +90,12 @@ There are more guides in [docs/](docs/), e.g. the [Guide for contributing to tra
 ## How to contribute with a PR
 If you would like to contribute by solving an open issue, please fork this repository and then create a branch for the fix.
 
-Once you push your code to your fork, you'll be able to open a PR into Logseq repository. For more info you can follow this guide from [GitHub docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
+Once you push your code to your fork, you'll be able to open a PR into Logseq repository. For more info you can follow this guide from [GitHub docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork). 
+
+Enabling "allow edits from maintainers" for PR is highly appreciated!
+
+There's a nice [project board](https://github.com/orgs/logseq/projects/5/views/1?pane=info
+) listing items that easy for contributors to catch-up
 
 And here a list of some [good first issues](https://github.com/logseq/logseq/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)!
 

+ 2 - 2
android/app/build.gradle

@@ -6,8 +6,8 @@ android {
         applicationId "com.logseq.app"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 49
-        versionName "0.8.15"
+        versionCode 50
+        versionName "0.8.16"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

+ 1 - 1
deps.edn

@@ -33,7 +33,7 @@
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
                   :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.54"}
                                 org.clojure/tools.namespace      {:mvn/version "0.2.11"}
-                                cider/cider-nrepl                {:mvn/version "0.28.4"}
+                                cider/cider-nrepl                {:mvn/version "0.29.0"}
                                 org.clojars.knubie/cljs-run-test {:mvn/version "1.0.1"}}
                   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
 

+ 4 - 0
deps/graph-parser/.carve/ignore

@@ -38,3 +38,7 @@ logseq.graph-parser/get-blocks-to-delete
 logseq.graph-parser.property/colons-org
 ;; API
 logseq.graph-parser.util.db/resolve-input
+;; TODO: use fast-remove-nils instead
+logseq.graph-parser.util/remove-nils
+;; API
+logseq.graph-parser.text/get-file-basename

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

@@ -122,7 +122,7 @@ Options available:
                          new?
                          ;; TODO: use file system timestamp?
                          (assoc :file/created-at (date-time-util/time-ms)))])
-        tx' (gp-util/remove-nils tx)
+        tx' (gp-util/fast-remove-nils tx)
         result (if skip-db-transact?
                  tx'
                  (d/transact! conn tx' (select-keys options [:new-graph? :from-disk?])))]

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

@@ -376,7 +376,7 @@
   [blocks]
   (map (fn [block]
          (if (map? block)
-           (block-keywordize (gp-util/remove-nils block))
+           (block-keywordize (gp-util/remove-nils-non-nested block))
            block))
        blocks))
 

+ 1 - 1
deps/graph-parser/src/logseq/graph_parser/extract.cljc

@@ -109,7 +109,7 @@
         invalid-properties (set (->> (map (comp name first) *invalid-properties)
                                      (concat invalid-properties)))
         page-m (->
-                (gp-util/remove-nils
+                (gp-util/remove-nils-non-nested
                  (assoc
                   (gp-block/page-name->map page false db true date-formatter
                                            :from-page from-page)

+ 8 - 2
deps/graph-parser/src/logseq/graph_parser/text.cljs

@@ -10,9 +10,15 @@
             [logseq.graph-parser.util.page-ref :as page-ref]))
 
 (defn get-file-basename
+  "Returns the basename of a file path. e.g. /a/b/c.md -> c.md"
+  [path]
+  (when-not (string/blank? path)
+    (.-base (path/parse (string/replace path "+" "/")))))
+
+(defn get-file-rootname
+  "Returns the rootname of a file path. e.g. /a/b/c.md -> c"
   [path]
   (when-not (string/blank? path)
-    ;; Same as util/node-path.name
     (.-name (path/parse (string/replace path "+" "/")))))
 
 (def page-ref-re-0 #"\[\[(.*)\]\]")
@@ -28,7 +34,7 @@
        (or (when-let [[_ label _path] (re-matches markdown-page-ref-re s)]
              (string/trim label))
            (when-let [[_ path _label] (re-matches org-page-ref-re s)]
-             (some-> (get-file-basename path)
+             (some-> (get-file-rootname path)
                      (string/replace "." "/")))
            (-> (re-matches page-ref-re-0 s)
                second))))

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

@@ -28,7 +28,8 @@
   (.normalize s "NFC"))
 
 (defn remove-nils
-  "remove pairs of key-value that has nil value from a (possibly nested) map."
+  "remove pairs of key-value that has nil value from a (possibly nested) map or
+  coll of maps."
   [nm]
   (walk/postwalk
    (fn [el]
@@ -37,6 +38,16 @@
        el))
    nm))
 
+(defn remove-nils-non-nested
+  "remove pairs of key-value that has nil value from a map (nested not supported)."
+  [nm]
+  (into {} (remove (comp nil? second)) nm))
+
+(defn fast-remove-nils
+  "remove pairs of key-value that has nil value from a coll of maps."
+  [nm]
+  (keep (fn [m] (if (map? m) (remove-nils-non-nested m) m)) nm))
+
 (defn split-first [pattern s]
   (when-let [first-index (string/index-of s pattern)]
     [(subs s 0 first-index)

+ 26 - 8
docs/dev-practices.md

@@ -11,8 +11,8 @@ this section, run `bb dev:lint`.
 ### Clojure code
 
 To lint:
-```
-clojure -M:clj-kondo --lint src
+```sh
+clojure -M:clj-kondo --parallel --lint src --cache false
 ```
 
 We lint our Clojure(Script) code with https://github.com/clj-kondo/clj-kondo/. If you need to configure specific linters, see [this documentation](https://github.com/clj-kondo/clj-kondo/blob/master/doc/linters.md). Where possible, a global linting configuration is used and namespace specific configuration is avoided.
@@ -27,7 +27,7 @@ There are outstanding linting items that are currently ignored to allow linting
 We use https://github.com/borkdude/carve to detect unused vars in our codebase.
 
 To run this linter:
-```
+```sh
 bb lint:carve
 ```
 
@@ -35,7 +35,7 @@ By default, the script runs in CI mode which prints unused vars if they are
 found. The script can be run in an interactive mode which prompts for keeping
 (ignoring) an unused var or removing it. Run this mode with:
 
-```
+```sh
 bb lint:carve '{:interactive true}'
 ```
 
@@ -46,7 +46,7 @@ why a var is ignored to help others understand why it's unused.
 
 Large vars have a lot of complexity and make it hard for the team to maintain
 and understand them. To run this linter:
-```
+```sh
 bb lint:large-vars
 ```
 
@@ -55,7 +55,7 @@ To configure the linter, see the `[:tasks/config :large-vars]` path of bb.edn.
 ### Document namespaces
 
 Documentation helps teams share their knowledge and enables more individuals to contribute to the codebase. Documenting our namespaces is a good first step to improving our documentation. To run this linter:
-```
+```sh
 bb lint:ns-docstrings
 ```
 
@@ -83,7 +83,7 @@ We have unit and end to end tests.
 
 To run end to end tests
 
-``` bash
+```sh
 yarn electron-watch
 # in another shell
 yarn e2e-test # or npx playwright test
@@ -91,8 +91,9 @@ yarn e2e-test # or npx playwright test
 
 If e2e failed after first running:
 - `rm -rdf ~/.logseq`
+- `rm -rdf ~/.config/Logseq`
 - `rm -rdf <repo dir>/tmp/`  
-- `rm -rdf <appData dir>/Electron`  (Reference: https://www.electronjs.org/de/docs/latest/api/app#appgetpathname)
+- Windows: `rmdir /s %APPDATA%/Electron`  (Reference: https://www.electronjs.org/de/docs/latest/api/app#appgetpathname)
 
 If e2e tests fail, they can be debugged by examining a trace dump with [the
 playwright trace
@@ -212,3 +213,20 @@ Currently the codebase is not formatted/indented consistently. We loosely follow
 There are some babashka tasks under `nbb:` which are useful for inspecting
 database changes in realtime. See [these
 docs](https://github.com/logseq/bb-tasks#logseqbb-tasksnbbwatch) for more info.
+
+## FAQ
+
+If dev app launch failed after electron upgrade:
+```sh
+yarn
+yarn watch
+```
+In another window:
+```sh
+cd static
+yarn
+cd ..
+yarn dev-electron-app
+``` 
+and kill all electron process
+Then a normal start happens via `yarn dev-electron-app`

+ 87 - 0
e2e-tests/editor.spec.ts

@@ -596,3 +596,90 @@ test('should not erase typed text when expanding block quickly after typing #389
     ''
   )
 })
+
+test('should keep correct undo and redo seq after indenting or outdenting the block #7615',async({page,block}) => {
+  await createRandomPage(page)
+
+  await block.mustFill("foo")
+  
+  await page.keyboard.press("Enter")
+  await expect(page.locator('textarea >> nth=0')).toHaveText("")
+  await block.indent()
+  await block.mustFill("bar")
+  await expect(page.locator('textarea >> nth=0')).toHaveText("bar")
+
+  if (IsMac) {
+    await page.keyboard.press('Meta+z')
+  } else {
+    await page.keyboard.press('Control+z')
+  }
+  // should undo "bar" input
+  await expect(page.locator('textarea >> nth=0')).toHaveText("")
+  if (IsMac) {
+    await page.keyboard.press('Shift+Meta+z')
+  } else {
+    await page.keyboard.press('Shift+Control+z')
+  }
+  // should redo "bar" input
+  await expect(page.locator('textarea >> nth=0')).toHaveText("bar")
+  await page.keyboard.press("Shift+Tab")
+  
+  await page.keyboard.press("Enter")
+  await expect(page.locator('textarea >> nth=0')).toHaveText("")
+  // swap input seq
+  await block.mustFill("baz")
+  await block.indent()
+
+  if (IsMac) {
+    await page.keyboard.press('Meta+z')
+  } else {
+    await page.keyboard.press('Control+z')
+  }
+  // should undo indention
+  await expect(page.locator('textarea >> nth=0')).toHaveText("baz")
+  await page.keyboard.press("Shift+Tab")
+
+  await page.keyboard.press("Enter")
+  await expect(page.locator('textarea >> nth=0')).toHaveText("")
+  // #7615
+  await page.keyboard.type("aaa")
+  await block.indent()
+  await page.keyboard.type(" bbb")
+  await expect(page.locator('textarea >> nth=0')).toHaveText("aaa bbb")
+  if (IsMac) {
+    await page.keyboard.press('Meta+z')
+  } else {
+    await page.keyboard.press('Control+z')
+  }
+  await expect(page.locator('textarea >> nth=0')).toHaveText("aaa")
+  if (IsMac) {
+    await page.keyboard.press('Meta+z')
+  } else {
+    await page.keyboard.press('Control+z')
+  }
+  await expect(page.locator('textarea >> nth=0')).toHaveText("aaa")
+  if (IsMac) {
+    await page.keyboard.press('Meta+z')
+  } else {
+    await page.keyboard.press('Control+z')
+  }
+  await expect(page.locator('textarea >> nth=0')).toHaveText("")
+  if (IsMac) {
+    await page.keyboard.press('Shift+Meta+z')
+  } else {
+    await page.keyboard.press('Shift+Control+z')
+  }
+  await expect(page.locator('textarea >> nth=0')).toHaveText("aaa")
+  if (IsMac) {
+    await page.keyboard.press('Shift+Meta+z')
+  } else {
+    await page.keyboard.press('Shift+Control+z')
+  }
+  await expect(page.locator('textarea >> nth=0')).toHaveText("aaa")
+  if (IsMac) {
+    await page.keyboard.press('Shift+Meta+z')
+  } else {
+    await page.keyboard.press('Shift+Control+z')
+  }
+  await expect(page.locator('textarea >> nth=0')).toHaveText("aaa bbb")
+})

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

@@ -105,7 +105,7 @@ test('zoom out', async ({ page }) => {
   await page.keyboard.press('Shift+0')
   await page.waitForTimeout(1500)
   await page.click('#tl-zoom-out')
-  await expect(page.locator('#tl-zoom')).toContainText('100%')
+  await expect(page.locator('#tl-zoom')).toContainText('80%')
 })
 
 test('open context menu', async ({ page }) => {

+ 4 - 4
ios/App/App.xcodeproj/project.pbxproj

@@ -515,7 +515,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.8.15;
+				MARKETING_VERSION = 0.8.16;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
@@ -542,7 +542,7 @@
 				INFOPLIST_FILE = App/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
-				MARKETING_VERSION = 0.8.15;
+				MARKETING_VERSION = 0.8.16;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -567,7 +567,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.8.15;
+				MARKETING_VERSION = 0.8.16;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
@@ -594,7 +594,7 @@
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
-				MARKETING_VERSION = 0.8.15;
+				MARKETING_VERSION = 0.8.16;
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
 				PRODUCT_NAME = "$(TARGET_NAME)";

+ 1 - 1
libs/src/LSPlugin.ts

@@ -317,7 +317,7 @@ export interface IPluginSearchServiceHooks {
 
   onIndiceInit: (graph: string) => Promise<SearchIndiceInitStatus>
   onIndiceReset: (graph: string) => Promise<void>
-  onBlocksChanged: (graph: string, changes: { added: Array<SearchBlockItem>, removed: Array<BlockEntity> }) => Promise<void>
+  onBlocksChanged: (graph: string, changes: { added: Array<SearchBlockItem>, removed: Array<EntityID> }) => Promise<void>
   onGraphRemoved: (graph: string, opts?: {}) => Promise<any>
 }
 

+ 1 - 1
resources/package.json

@@ -1,7 +1,7 @@
 {
   "name": "Logseq",
   "productName": "Logseq",
-  "version": "0.8.15",
+  "version": "0.8.16",
   "main": "electron.js",
   "author": "Logseq",
   "license": "AGPL-3.0",

+ 57 - 32
src/electron/electron/handler.cljs

@@ -1,34 +1,34 @@
 (ns electron.handler
   "This ns starts the event handling for the electron main process and defines
   all the application-specific event types"
-  (:require ["electron" :refer [ipcMain dialog app autoUpdater shell]]
-            [cljs-bean.core :as bean]
-            ["fs" :as fs]
+  (:require ["/electron/utils" :as js-utils]
+            ["abort-controller" :as AbortController]
             ["buffer" :as buffer]
+            ["diff-match-patch" :as google-diff]
+            ["electron" :refer [app autoUpdater dialog ipcMain shell]]
+            ["fs" :as fs]
             ["fs-extra" :as fs-extra]
-            ["path" :as path]
             ["os" :as os]
-            ["diff-match-patch" :as google-diff]
-            ["/electron/utils" :as js-utils]
-            ["abort-controller" :as AbortController]
-            [electron.shell :as shell]
-            [electron.fs-watcher :as watcher]
-            [electron.configs :as cfgs]
-            [promesa.core :as p]
-            [clojure.string :as string]
-            [electron.utils :as utils]
-            [electron.logger :as logger]
-            [electron.state :as state]
+            ["path" :as path]
+            [cljs-bean.core :as bean]
+            [cljs.reader :as reader]
             [clojure.core.async :as async]
-            [electron.search :as search]
+            [clojure.string :as string]
+            [electron.backup-file :as backup-file]
+            [electron.configs :as cfgs]
+            [electron.file-sync-rsapi :as rsapi]
+            [electron.find-in-page :as find]
+            [electron.fs-watcher :as watcher]
             [electron.git :as git]
+            [electron.logger :as logger]
             [electron.plugin :as plugin]
-            [electron.window :as win]
-            [electron.file-sync-rsapi :as rsapi]
-            [electron.backup-file :as backup-file]
-            [cljs.reader :as reader]
+            [electron.search :as search]
             [electron.server :as server]
-            [electron.find-in-page :as find]))
+            [electron.shell :as shell]
+            [electron.state :as state]
+            [electron.utils :as utils]
+            [electron.window :as win]
+            [promesa.core :as p]))
 
 (defmulti handle (fn [_window args] (keyword (first args))))
 
@@ -38,21 +38,24 @@
 (defmethod handle :mkdir-recur [_window [_ dir]]
   (fs/mkdirSync dir #js {:recursive true}))
 
-;; {encoding: 'utf8', withFileTypes: true}
 (defn- readdir
-  [dir]
+  "Read directory recursively, return all filenames"
+  [root-dir]
   (->> (tree-seq
-        (fn [^js fpath]
-          (.isDirectory (fs/statSync fpath)))
-        (fn [dir]
-          (let [files (fs/readdirSync dir (clj->js {:withFileTypes true}))]
+        (fn [[is-dir _fpath]]
+          is-dir)
+        (fn [[_is-dir dir]]
+          (let [files (fs/readdirSync dir #js {:withFileTypes true})]
             (->> files
                  (remove #(.isSymbolicLink ^js %))
                  (remove #(string/starts-with? (.-name ^js %) "."))
-                 (map #(.join path dir (.-name %))))))
-        dir)
+                 (map #(do
+                         [(.isDirectory %)
+                          (.join path dir (.-name %))])))))
+        [true root-dir])
+       (filter (complement first))
+       (map second)
        (map utils/fix-win-path!)
-       (doall)
        (vec)))
 
 (defmethod handle :readdir [_window [_ dir]]
@@ -174,12 +177,34 @@
           result (get (js->clj result) "filePaths")]
     (p/resolved (first result))))
 
-(defmethod handle :openDir [^js _window _messages]
+(defn- pretty-print-js-error
+  "Converts file related JS Error messages to a human readable format.
+   Ex.:
+   Error: EACCES: permission denied, scandir '/tmp/test'
+   Permission denied for path: '/tmp/test' (Code: EACCES)"
+  [e]
+  (some->>
+   e
+   str
+   ;; Message parsed as "Error: $ERROR_CODE$: $REASON$, function $PATH$"
+   (re-matches #"(?:Error\: )(.+)(?:\: )(.+)(?:, \w+ )('.+')")
+   rest
+   (#(str (string/capitalize (second %)) " for path: " (nth % 2) " (Code: " (first %) ")"))))
+
+(defmethod handle :openDir [^js window _messages]
   (logger/info ::open-dir "open folder selection dialog")
   (p/let [path (open-dir-dialog)]
     (logger/debug ::open-dir {:path path})
     (if path
-      (p/resolved (bean/->js (get-files path)))
+      (try
+        (p/resolved (bean/->js (get-files path)))
+        (catch js/Error e 
+          (do
+            (utils/send-to-renderer window "notification" {:type "error"
+                                                           :payload (str "Opening the specified directory failed.\n"
+                                                                         (or (pretty-print-js-error e) (str "Unexpected error: " e)))})
+            (p/rejected e))))
+
       (p/rejected (js/Error "path empty")))))
 
 (defmethod handle :getFiles [_window [_ path]]

+ 4 - 2
src/electron/electron/search.cljs

@@ -169,7 +169,8 @@
   [repo pages]
   (if-let [db (get-db repo)]
     ;; TODO: what if a CONFLICT on uuid
-    (let [insert (prepare db "INSERT INTO pages (id, uuid, content) VALUES (@id, @uuid, @content) ON CONFLICT (id) DO UPDATE SET content = @content")
+    ;; Should update all values on id conflict
+    (let [insert (prepare db "INSERT INTO pages (id, uuid, content) VALUES (@id, @uuid, @content) ON CONFLICT (id) DO UPDATE SET (uuid, content) = (@uuid, @content)")
           insert-many (.transaction ^object db
                                     (fn [pages]
                                       (doseq [page pages]
@@ -190,7 +191,8 @@
   [repo blocks]
   (if-let [db (get-db repo)]
     ;; TODO: what if a CONFLICT on uuid
-    (let [insert (prepare db "INSERT INTO blocks (id, uuid, content, page) VALUES (@id, @uuid, @content, @page) ON CONFLICT (id) DO UPDATE SET content = @content")
+    ;; Should update all values on id conflict
+    (let [insert (prepare db "INSERT INTO blocks (id, uuid, content, page) VALUES (@id, @uuid, @content, @page) ON CONFLICT (id) DO UPDATE SET (uuid, content, page) = (@uuid, @content, @page)")
           insert-many (.transaction ^object db
                                     (fn [blocks]
                                       (doseq [block blocks]

+ 12 - 3
src/main/frontend/components/block.cljs

@@ -2611,14 +2611,23 @@
    (editor-handler/unhighlight-blocks!)))
 
 (defn- block-drop
-  [event uuid target-block *move-to]
+  [^js event uuid target-block *move-to]
   (util/stop event)
   (when-not (dnd-same-block? uuid)
     (let [block-uuids (state/get-selection-block-ids)
           lookup-refs (map (fn [id] [:block/uuid id]) block-uuids)
           selected (db/pull-many (state/get-current-repo) '[*] lookup-refs)
-          blocks (if (seq selected) selected [@*dragging-block])]
-      (dnd/move-blocks event blocks target-block @*move-to)))
+          blocks (if (seq selected) selected [@*dragging-block])
+          blocks (remove-nils blocks)]
+      (if-not (seq blocks)
+        (when-let [text (.getData (.-dataTransfer event) "text/plain")]
+          (editor-handler/api-insert-new-block!
+           text
+           {:block-uuid  uuid
+            :edit-block? false
+            :sibling?    (= @*move-to :sibling)
+            :before?     (= @*move-to :top)}))
+        (dnd/move-blocks event blocks target-block @*move-to))))
   (block-drag-end event *move-to))
 
 (defn- block-mouse-over

+ 118 - 0
src/main/frontend/components/bug_report.cljs

@@ -0,0 +1,118 @@
+(ns frontend.components.bug-report
+  (:require [rum.core :as rum]
+            [frontend.ui :as ui]
+            [frontend.components.header :as header]
+            [frontend.util :as util]
+            [reitit.frontend.easy :as rfe]
+            [clojure.string :as string]
+            [frontend.handler.notification :as notification]))
+
+(defn parse-clipboard-data-transfer
+  "parse dataTransfer
+
+   input: dataTransfer
+
+   output: {:types {:type :data} :items {:kind :type} :files {:name :size :type}}"
+  [data]
+  (let [items (.-items data)
+        types (.-types data)
+        files (.-files data)]
+    (conj
+     {:items (->> items
+                  (map (fn [item] {:kind (.-kind item) :type (.-type item)}))
+                  (conj))}
+     {:types (->> types
+                  (map (fn [type] {:type type :data (.getData data type)}))
+                  (conj))}
+     {:files (->> files
+                  (map (fn [file] {:name (.-name file) :type (.-type file) :size (.-size file)}))
+                  (conj))})))
+
+(rum/defc clipboard-data-inspector
+  "bug report tool for clipboard"
+  []
+  (let [[result set-result!] (rum/use-state {})
+        [step set-step!] (rum/use-state 0)
+        paste-handler! (fn [e]
+                         (let [clipboard-data (.-clipboardData e)
+                               result (parse-clipboard-data-transfer clipboard-data)
+                               result (into {} result)]
+                           (set-result! result)
+                           (set-step! 1)))
+
+        copy-result-to-clipboard! (fn [result]
+                                    (util/copy-to-clipboard! result)
+                                    (notification/show! "Copied to clipboard!"))
+
+        reset-step! (fn []
+                      (set-step! 0)
+                      (set-result! {}))]
+
+    (rum/use-effect!
+     (fn []
+       (cond (= step 0) (js/addEventListener "paste" paste-handler!))
+       (fn [] (cond (= step 0) (js/removeEventListener "paste" paste-handler!))))
+     [step]) ;; when step === 0
+
+    [:div.flex.flex-col
+     (when (= step 0)
+       (list [:div.mx-auto "Press Ctrl+V / ⌘+V to inspect your clipboard data"]
+             [:div.mx-auto "or click here to paste if you are using the mobile version"]
+             ;; for mobile
+             [:input.form-input.is-large.transition.duration-150.ease-in-out {:type "text" :placeholder "Long press here to paste if you are on mobile"}]
+             [:div.flex.justify-between.items-center.mt-2
+              [:div "Something wrong? No problem, click to go back to the previous step."]
+              (ui/button "Go back" :on-click #(util/open-url (rfe/href :bug-report)))]))
+
+     (when (= step 1)
+       (list
+        [:div "Here is the data read from clipboard."]
+        [:div.flex.justify-between.items-center.mt-2
+         [:div "If this is okay to share, click the copy button."]
+         (ui/button "Copy the result" :on-click #(copy-result-to-clipboard! (js/JSON.stringify (clj->js result) nil 2)))]
+        [:div.flex.justify-between.items-center.mt-2
+         [:div "Now you can report the result pasted to your clipboard. Please paste the result in the 'Additional Context' section and state where you copied the original content from. Thanks!"]
+         (ui/button "Create an issue" :href header/bug-report-url)]
+        [:div.flex.justify-between.items-center.mt-2
+         [:div "Something wrong? No problem, click to go back to the previous step."]
+         (ui/button "Go back" :on-click reset-step!)]
+
+        [:pre.whitespace-pre-wrap [:code (js/JSON.stringify (clj->js result) nil 2)]]))]))
+
+(rum/defc bug-report-tool-route
+  [route-match]
+  (let [name (get-in route-match [:parameters :path :tool])]
+    [:div.flex.flex-col ;; container
+     [:h1.text-2xl.mx-auto.mb-4 (ui/icon "clipboard") " " (-> name (string/replace #"-" " ") (string/capitalize))]
+     (cond ;; TODO any fallback?
+       (= name "clipboard-data-inspector")
+       (clipboard-data-inspector))]))
+
+(rum/defc report-item-button
+  [title description icon-name {:keys [on-click]}]
+   [:a.cp__bug-report-item-button.flex.items-center.px-4.py-2.my-2.rounded-lg {:on-click on-click}
+    [(ui/icon icon-name)
+     [:div.flex.flex-col.ml-2
+      [:div title]
+      [:div.opacity-60 description]]]])
+
+(rum/defc bug-report
+  []
+  [:div.flex.flex-col
+   [:div.flex.flex-col.items-center
+    [:div.flex.items-center.mb-2
+     (ui/icon "bug")
+     [:h1.text-3xl.ml-2 "Bug report"]]
+    [:div.opacity-60 "Can you help us out by submitting a bug report? We'll get it sorted out as soon as we can."]]
+   [:div.cp__bug-report-reporter.rounded-lg.p-8.mt-8
+    [:h1.text-2xl "Is the bug you encountered related to these features?"]
+    [:div.opacity-60 "You can use these handy tools to give us additional information."]
+    (report-item-button "Clipboard helper"
+                 "Inspect and collect clipboard data"
+                 "clipboard"
+                 {:on-click #(util/open-url (rfe/href :bug-report-tools {:tool "clipboard-data-inspector"}))})
+    [:div.py-2] ;; divider
+    [:div.flex.flex-col
+     [:h1.text-2xl "Or..."]
+     [:div.opacity-60 "If there are no tools available for you to gather additional information, please report the bug directly."]
+     (report-item-button "Submit a bug report" "Help Make Logseq Better!" "message-report" {:on-click #(util/open-url header/bug-report-url)})]]])

+ 7 - 0
src/main/frontend/components/bug_report.css

@@ -0,0 +1,7 @@
+.cp__bug-report-reporter {
+  background-color: var(--ls-tertiary-background-color);
+}
+
+.cp__bug-report-item-button {
+  background-color: var(--ls-quaternary-background-color);
+}

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

@@ -70,6 +70,7 @@
     (str "https://github.com/logseq/logseq/issues/new?"
          "title=&"
          "template=bug_report.yaml&"
+         "labels=from:in-app&"
          "platform="
          (js/encodeURIComponent platform))))
 
@@ -120,9 +121,7 @@
 
        {:title [:div.flex-row.flex.justify-between.items-center
                 [:span (t :help/bug)]]
-        :options {:href bug-report-url
-                  :title "Fire a bug report on Github"
-                  :target "_blank"}
+        :options {:href (rfe/href :bug-report)}
         :icon (ui/icon "bug")}
 
        (when (and (state/sub :auth/id-token) (user-handler/logged-in?))

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

@@ -170,7 +170,9 @@
                               [:string (when-let [updated-at (:block/updated-at item)]
                                          (date/int->local-time-2 updated-at))]
 
-                              [:string (get-in item [:block/properties column])])]
+                              [:string (or (get-in item [:block/properties-text-values column])
+                                           ;; Fallback to property relationships for page blocks
+                                           (get-in item [:block/properties column]))])]
                   [:td.whitespace-nowrap {:on-mouse-down (fn [] (reset! select? false))
                                           :on-mouse-move (fn [] (reset! select? true))
                                           :on-mouse-up (fn []

+ 2 - 0
src/main/frontend/components/search.cljs

@@ -331,6 +331,8 @@
        nil)]))
 
 (rum/defc search-auto-complete
+  "has-more? - if the result is truncated
+   all? - if true, in show-more mode"
   [{:keys [engine pages files pages-content blocks has-more?] :as result} search-q all?]
   (let [pages (when-not all? (map (fn [page]
                                     (let [alias (model/get-redirect-page-name page)]

+ 5 - 1
src/main/frontend/components/sidebar.css

@@ -223,9 +223,13 @@
         padding: 0;
         margin: 0;
 
+        li {
+          margin: 0;
+        }
+        
         a {
           width: 100%;
-          padding: 2px 24px;
+          padding: 4px 24px;
           transition: background-color .3s;
 
           .page-title {

+ 9 - 3
src/main/frontend/db/query_custom.cljs

@@ -28,7 +28,7 @@
   (let [{:keys [where in]} (datalog-util/query-vec->map query)
         rules-found (datalog-util/find-rules-in-where where (-> rules/query-dsl-rules keys set))]
     (if (seq rules-found)
-      (if (= '% (last in))
+      (if (and (= '% (last in)) (vector? (last (:inputs query-m))))
         ;; Add to existing :inputs rules
         (update query-m
                 :inputs
@@ -46,9 +46,15 @@
             (update :query
                     (fn [q]
                       (if (contains? (set q) :in)
-                        (datalog-util/add-to-end-of-query-section q :in ['%])
+                        ;; only add '% if not already present
+                        (if (not (contains? (set q) '%))
+                          (datalog-util/add-to-end-of-query-section q :in ['%])
+                          q)
                         (into q [:in '$ '%]))))
-            (assoc :rules (mapv rules/query-dsl-rules rules-found))))
+            (update :rules
+                    (fn [rules]
+                      (into (or rules [])
+                            (mapv rules/query-dsl-rules rules-found))))))
       query-m)))
 
 (defn custom-query

+ 1 - 2
src/main/frontend/db/utils.cljs

@@ -93,8 +93,7 @@
    (transact! repo-url tx-data nil))
   ([repo-url tx-data tx-meta]
    (when-not config/publishing?
-     (let [tx-data (->> (gp-util/remove-nils tx-data)
-                        (remove nil?))]
+     (let [tx-data (gp-util/fast-remove-nils tx-data)]
        (when (seq tx-data)
          (when-let [conn (conn/get-db repo-url false)]
            (if tx-meta

+ 33 - 33
src/main/frontend/extensions/pdf/assets.cljs

@@ -29,21 +29,20 @@
 
 (defn inflate-asset
   [original-path]
-  (let [filename (util/node-path.basename original-path)
+  (let [filename  (util/node-path.basename original-path)
         web-link? (string/starts-with? original-path "http")
-        ext-name (util/get-file-ext filename)
-        url (assets-handler/normalize-asset-resource-url original-path)]
-    (when-let [key
-               (if web-link?
-                 (str (hash url))
-                 (and
-                   (= ext-name "pdf")
-                   (subs filename 0 (- (count filename) 4))))]
-      {:key      key
-       :identity (subs key (- (count key) 15))
-       :filename filename
-       :url      url
-       :hls-file (str "assets/" key ".edn")
+        ext-name  (util/get-file-ext filename)
+        url       (assets-handler/normalize-asset-resource-url original-path)
+        filekey   (util/safe-sanitize-file-name (subs filename 0 (- (count filename) (inc (count ext-name)))))]
+    (when-let [key (and (not (string/blank? filekey))
+                        (if web-link?
+                          (str filekey "__" (hash url)) filekey))]
+
+      {:key           key
+       :identity      (subs key (- (count key) 15))
+       :filename      filename
+       :url           url
+       :hls-file      (str "assets/" key ".edn")
        :original-path original-path})))
 
 (defn resolve-area-image-file
@@ -184,25 +183,26 @@
   ([pdf hl] (ensure-ref-block! pdf hl nil))
   ([pdf-current {:keys [id content page properties]} insert-opts]
    (when-let [ref-page (and pdf-current (resolve-ref-page pdf-current))]
-     (if-let [ref-block (db-model/query-block-by-uuid id)]
-       (do
-         (println "[existed ref block]" ref-block)
-         ref-block)
-       (let [text       (:text content)
-             wrap-props #(if-let [stamp (:image content)]
-                           (assoc % :hl-type "area" :hl-stamp stamp) %)]
-
-         (when (string? text)
-           (editor-handler/api-insert-new-block!
-            text (merge {:page        (:block/name ref-page)
-                         :custom-uuid id
-                         :properties  (wrap-props
-                                       {:ls-type  "annotation"
-                                        :hl-page  page
-                                        :hl-color (:color properties)
-                                        ;; force custom uuid
-                                        :id       (str id)})}
-                        insert-opts))))))))
+     (let [ref-block (db-model/query-block-by-uuid id)]
+       (if-not (nil? (:block/content ref-block))
+         (do
+           (println "[existed ref block]" ref-block)
+           ref-block)
+         (let [text       (:text content)
+               wrap-props #(if-let [stamp (:image content)]
+                             (assoc % :hl-type "area" :hl-stamp stamp) %)]
+
+           (when (string? text)
+             (editor-handler/api-insert-new-block!
+              text (merge {:page        (:block/name ref-page)
+                           :custom-uuid id
+                           :properties  (wrap-props
+                                         {:ls-type  "annotation"
+                                          :hl-page  page
+                                          :hl-color (:color properties)
+                                          ;; force custom uuid
+                                          :id       (str id)})}
+                          insert-opts)))))))))
 
 (defn del-ref-block!
   [{:keys [id]}]

+ 62 - 44
src/main/frontend/extensions/pdf/highlights.cljs

@@ -2,6 +2,7 @@
   (:require [cljs-bean.core :as bean]
             [clojure.string :as string]
             [frontend.components.svg :as svg]
+            [frontend.components.block :as block]
             [frontend.context.i18n :refer [t]]
             [frontend.extensions.pdf.assets :as pdf-assets]
             [frontend.extensions.pdf.utils :as pdf-utils]
@@ -93,11 +94,11 @@
   "The contextual menu which appears over a text selection and allows e.g. creating a highlight."
   [^js viewer
    {:keys [highlight point ^js selection]}
-   {:keys [clear-ctx-tip! add-hl! upd-hl! del-hl!]}]
+   {:keys [clear-ctx-menu! add-hl! upd-hl! del-hl!]}]
 
   (rum/use-effect!
    (fn []
-     (let [cb #(clear-ctx-tip!)]
+     (let [cb #(clear-ctx-menu!)]
        (js/setTimeout #(js/document.addEventListener "click" cb))
        #(js/document.removeEventListener "click" cb)))
    [])
@@ -158,7 +159,7 @@
 
                               (reset! *highlight-last-color (keyword action)))))
 
-                        (and clear? (js/setTimeout #(clear-ctx-tip!) 68))))]
+                        (and clear? (js/setTimeout #(clear-ctx-menu!) 68))))]
 
     (rum/use-effect!
      (fn []
@@ -208,43 +209,58 @@
      ]))
 
 (rum/defc pdf-highlights-text-region
-  [^js viewer vw-hl hl
-   {:keys [show-ctx-tip!]}]
+  [^js viewer vw-hl hl {:keys [show-ctx-menu!]}]
 
-  (let [{:keys [rects]} (:position vw-hl)
+  (let [{:keys [id]} hl
+        {:keys [rects]} (:position vw-hl)
         {:keys [color]} (:properties hl)
-        open-tip! (fn [^js/MouseEvent e]
-                    (.preventDefault e)
-                    (let [x (.-clientX e)
-                          y (.-clientY e)]
 
-                      (show-ctx-tip! viewer hl {:x x :y y})))]
+        open-ctx-menu!
+        (fn [^js/MouseEvent e]
+          (.preventDefault e)
+          (let [x (.-clientX e)
+                y (.-clientY e)]
+
+            (show-ctx-menu! viewer hl {:x x :y y})))
+
+        dragstart-handle!
+        (fn [^js e]
+          (when-let [^js dt (and id (.-dataTransfer e))]
+            (reset! block/*dragging? true)
+            (pdf-assets/ensure-ref-block! (state/get-current-pdf) hl)
+            (.setData dt "text/plain" (str "((" id "))"))))]
 
     [:div.extensions__pdf-hls-text-region
-     {:on-click        open-tip!
-      :on-context-menu open-tip!}
+     {:on-click        open-ctx-menu!
+      :on-context-menu open-ctx-menu!}
 
      (map-indexed
       (fn [idx rect]
         [:div.hls-text-region-item
-         {:key        idx
-          :style      rect
-          :data-color color}])
+         {:key           idx
+          :style         rect
+          :draggable     "true"
+          :on-drag-start dragstart-handle!
+          :data-color    color}])
       rects)]))
 
 (rum/defc ^:large-vars/cleanup-todo pdf-highlight-area-region
-  [^js viewer vw-hl hl
-   {:keys [show-ctx-tip! upd-hl!]}]
+  [^js viewer vw-hl hl {:keys [show-ctx-menu! upd-hl!]}]
+
+  (let [{:keys [id]} hl
+        *el    (rum/use-ref nil)
+        *dirty (rum/use-ref nil)
+        open-ctx-menu! (fn [^js/MouseEvent e]
+                         (.preventDefault e)
+                         (when-not (rum/deref *dirty)
+                           (let [x (.-clientX e)
+                                 y (.-clientY e)]
 
-  (let [*el       (rum/use-ref nil)
-        *dirty    (rum/use-ref nil)
-        open-tip! (fn [^js/MouseEvent e]
-                    (.preventDefault e)
-                    (when-not (rum/deref *dirty)
-                      (let [x (.-clientX e)
-                            y (.-clientY e)]
+                             (show-ctx-menu! viewer hl {:x x :y y}))))
 
-                        (show-ctx-tip! viewer hl {:x x :y y}))))]
+        dragstart-handle! (fn [^js e]
+                            (when-let [^js dt (and id (.-dataTransfer e))]
+                              (.setData dt "text/plain" (str "((" id "))"))))]
 
     ;; resizable
     (rum/use-effect!
@@ -331,8 +347,10 @@
          {:ref             *el
           :style           vw-bounding
           :data-color      color
-          :on-click        open-tip!
-          :on-context-menu open-tip!}]))))
+          :draggable       "true"
+          :on-drag-start   dragstart-handle!
+          :on-click        open-ctx-menu!
+          :on-context-menu open-ctx-menu!}]))))
 
 (rum/defc pdf-highlights-region-container
   "Displays the highlights over a pdf document."
@@ -349,7 +367,7 @@
        ))])
 
 (rum/defc ^:large-vars/cleanup-todo pdf-highlight-area-selection
-  [^js viewer {:keys [show-ctx-tip!]}]
+  [^js viewer {:keys [show-ctx-menu!]}]
 
   (let [^js viewer-clt          (.. viewer -viewer -classList)
         ^js cnt-el              (.-container viewer)
@@ -439,7 +457,7 @@
                                                      :properties {}}]
 
                                     ;; ctx tips
-                                    (show-ctx-tip! viewer hl point {:reset-fn #(reset-coords)})
+                                    (show-ctx-menu! viewer hl point {:reset-fn #(reset-coords)})
 
                                     ;; export area highlight
                                     ;;(dd "[selection end] :start"
@@ -481,17 +499,17 @@
         *mounted       (rum/use-ref false)
         [sel-state, set-sel-state!] (rum/use-state {:selection nil :range nil :collapsed nil :point nil})
         [highlights, set-highlights!] (rum/use-state initial-hls)
-        [tip-state, set-tip-state!] (rum/use-state {:highlight nil :vw-pos nil :selection nil :point nil :reset-fn nil})
+        [ctx-menu-state, set-ctx-menu-state!] (rum/use-state {:highlight nil :vw-pos nil :selection nil :point nil :reset-fn nil})
 
-        clear-ctx-tip! (rum/use-callback
-                        #(let [reset-fn (:reset-fn tip-state)]
-                           (set-tip-state! {})
+        clear-ctx-menu! (rum/use-callback
+                        #(let [reset-fn (:reset-fn ctx-menu-state)]
+                           (set-ctx-menu-state! {})
                            (and (fn? reset-fn) (reset-fn)))
-                        [tip-state])
+                        [ctx-menu-state])
 
-        show-ctx-tip!  (fn [^js viewer hl point & ops]
+        show-ctx-menu!  (fn [^js viewer hl point & ops]
                          (let [vw-pos (pdf-utils/scaled-to-vw-pos viewer (:position hl))]
-                           (set-tip-state! (apply merge (list* {:highlight hl :vw-pos vw-pos :point point} ops)))))
+                           (set-ctx-menu-state! (apply merge (list* {:highlight hl :vw-pos vw-pos :point point} ops)))))
 
         add-hl!        (fn [hl] (when (:id hl)
                                   ;; fix js object
@@ -603,7 +621,7 @@
 
            ;; show ctx menu
            (js/setTimeout (fn []
-                            (set-tip-state! {:highlight hl-fn
+                            (set-ctx-menu-state! {:highlight hl-fn
                                              :selection selection
                                              :point     point})))) 0))
 
@@ -621,7 +639,7 @@
 
                (rum/mount
                 (pdf-highlights-region-container
-                 viewer page-hls {:show-ctx-tip! show-ctx-tip!
+                 viewer page-hls {:show-ctx-menu! show-ctx-menu!
                                   :upd-hl!       upd-hl!})
 
                 hls-layer)))))
@@ -633,10 +651,10 @@
     [:div.extensions__pdf-highlights-cnt
 
      ;; hl context tip menu
-     (when-let [_hl (:highlight tip-state)]
+     (when-let [_hl (:highlight ctx-menu-state)]
        (js/ReactDOM.createPortal
-        (pdf-highlights-ctx-menu viewer tip-state
-                                 {:clear-ctx-tip! clear-ctx-tip!
+        (pdf-highlights-ctx-menu viewer ctx-menu-state
+                                 {:clear-ctx-menu! clear-ctx-menu!
                                   :add-hl!        add-hl!
                                   :del-hl!        del-hl!
                                   :upd-hl!        upd-hl!})
@@ -660,8 +678,8 @@
      ;; area selection container
      (pdf-highlight-area-selection
       viewer
-      {:clear-ctx-tip! clear-ctx-tip!
-       :show-ctx-tip!  show-ctx-tip!
+      {:clear-ctx-menu! clear-ctx-menu!
+       :show-ctx-menu!  show-ctx-menu!
        :add-hl!        add-hl!
        })]))
 

+ 1 - 0
src/main/frontend/extensions/pdf/utils.cljs

@@ -183,6 +183,7 @@
         (-> filename
             (subs 0 (if local-asset? (- len 15) len))
             (string/replace #"^hls__" "")
+            (string/replace #"__[-\d]+$" "")
             (string/replace "_" " ")
             (string/trimr))
         filename))))

+ 3 - 2
src/main/frontend/fs/sync.cljs

@@ -27,6 +27,7 @@
             [frontend.db :as db]
             [frontend.fs :as fs]
             [frontend.encrypt :as encrypt]
+            [frontend.pubsub :as pubsub]
             [logseq.graph-parser.util :as gp-util]
             [medley.core :refer [dedupe-by]]
             [rum.core :as rum]
@@ -2766,7 +2767,7 @@
     (async/tap remote->local-sync-mult private-remote->local-sync-chan)
     (async/tap remote->local-full-sync-mult private-remote->local-full-sync-chan)
     (async/tap pause-resume-mult private-pause-resume-chan)
-    (async/tap util/app-wake-up-from-sleep-mult app-awake-from-sleep-chan)
+    (async/tap pubsub/app-wake-up-from-sleep-mult app-awake-from-sleep-chan)
     (go-loop []
       (let [{:keys [remote->local remote->local-full-sync local->remote-full-sync local->remote resume pause stop]}
             (async/alt!
@@ -3074,7 +3075,7 @@
         (async/untap remote->local-sync-mult private-remote->local-sync-chan)
         (async/untap remote->local-full-sync-mult private-remote->local-full-sync-chan)
         (async/untap pause-resume-mult private-pause-resume-chan)
-        (async/untap util/app-wake-up-from-sleep-mult app-awake-from-sleep-chan)
+        (async/untap pubsub/app-wake-up-from-sleep-mult app-awake-from-sleep-chan)
         (when ops-chan (async/close! ops-chan))
         (stop-local->remote! local->remote-syncer)
         (stop-remote->local! remote->local-syncer)

+ 4 - 4
src/main/frontend/handler/editor.cljs

@@ -1660,11 +1660,11 @@
   [up?]
   (fn [event]
     (util/stop event)
+    (save-current-block!)
     (let [edit-block-id (:block/uuid (state/get-edit-block))
           move-nodes (fn [blocks]
                        (outliner-tx/transact!
                         {:outliner-op :move-blocks}
-                        (save-current-block!)
                         (outliner-core/move-blocks-up-down! blocks up?))
                        (when-let [block-node (util/get-first-block-by-id (:block/uuid (first blocks)))]
                          (.scrollIntoView block-node #js {:behavior "smooth" :block "nearest"})))]
@@ -2139,10 +2139,10 @@
   [node]
   (when-not (parent-is-page? node)
     (let [parent-node (tree/-get-parent node)]
+      (save-current-block!)
       (outliner-tx/transact!
        {:outliner-op :move-blocks
         :real-outliner-op :indent-outdent}
-       (save-current-block!)
        (outliner-core/move-blocks! [(:data node)] (:data parent-node) true)))))
 
 (defn- last-top-level-child?
@@ -2666,6 +2666,7 @@
 
 (defn indent-outdent
   [indent?]
+  (save-current-block!)
   (state/set-editor-op! :indent-outdent)
   (let [pos (some-> (state/get-input) cursor/pos)
         {:keys [block]} (get-state)]
@@ -2674,8 +2675,7 @@
       (outliner-tx/transact!
        {:outliner-op :move-blocks
         :real-outliner-op :indent-outdent}
-       (save-current-block!)
-       (outliner-core/indent-outdent-blocks! [block] indent?)))
+        (outliner-core/indent-outdent-blocks! [block] indent?)))
     (state/set-editor-op! :nil)))
 
 (defn keydown-tab-handler

+ 5 - 1
src/main/frontend/handler/editor/keyboards.cljs

@@ -11,7 +11,7 @@
     (mixins/hide-when-esc-or-outside
      state
      :on-hide
-     (fn [_state _e event]
+     (fn [_state e event]
        (cond
          (contains?
           #{:commands :block-commands
@@ -25,6 +25,10 @@
          (= :input (state/get-editor-action))
          nil
 
+         (some-> (.-target e)
+                 (.closest ".ls-keep-editing-when-outside-click"))
+         nil
+
          :else
          (let [{:keys [on-hide value]} (editor-handler/get-state)]
            (when on-hide

+ 16 - 17
src/main/frontend/handler/file_sync.cljs

@@ -145,23 +145,22 @@
                                      (apply path/join base-path))
             version-file-paths (<! (p->c (fs/readdir version-files-dir :path-only? true)))]
         (when-not (instance? ExceptionInfo version-file-paths)
-          (let [version-file-paths (remove #{version-files-dir} version-file-paths)]
-            (when (seq version-file-paths)
-              (->>
-               (mapv
-                (fn [path]
-                  (try
-                    (let [create-time
-                          (-> (path/parse path)
-                              (js->clj :keywordize-keys true)
-                              :name
-                              (#(tf/parse (tf/formatter "yyyy-MM-dd'T'HH_mm_ss.SSSZZ") %)))]
-                      {:create-time create-time :path path :relative-path (string/replace-first path base-path "")})
-                    (catch :default e
-                      (log/error :page-history/parse-format-error e)
-                      nil)))
-                version-file-paths)
-               (remove nil?)))))))))
+          (when (seq version-file-paths)
+            (->>
+             (mapv
+              (fn [path]
+                (try
+                  (let [create-time
+                        (-> (path/parse path)
+                            (js->clj :keywordize-keys true)
+                            :name
+                            (#(tf/parse (tf/formatter "yyyy-MM-dd'T'HH_mm_ss.SSSZZ") %)))]
+                    {:create-time create-time :path path :relative-path (string/replace-first path base-path "")})
+                  (catch :default e
+                    (log/error :page-history/parse-format-error e)
+                    nil)))
+              version-file-paths)
+             (remove nil?))))))))
 
 (defn fetch-page-file-versions [graph-uuid page]
   []

+ 33 - 1
src/main/frontend/handler/page.cljs

@@ -427,6 +427,31 @@
     (doseq [page-id page-ids]
       (outliner-file/sync-to-file page-id))))
 
+(defn- rename-update-namespace!
+  "update :block/namespace of the renamed block"
+  [page old-original-name new-name]
+  (let [old-namespace? (text/namespace-page? old-original-name)
+        new-namespace? (text/namespace-page? new-name)
+        update-namespace! (fn [] (let [namespace (first (gp-util/split-last "/" new-name))]
+                                   (when namespace
+                                     (create! namespace {:redirect? false}) ;; create parent page if not exist, creation of namespace ref is handled in `create!`
+                                     (let [namespace-block (db/pull [:block/name (gp-util/page-name-sanity-lc namespace)])
+                                           repo                (state/get-current-repo)
+                                           page-txs [{:db/id (:db/id page)
+                                                      :block/namespace (:db/id namespace-block)}]]
+                                       (d/transact! (db/get-db repo false) page-txs)))))
+        remove-namespace! (fn []
+                            (db/transact! [[:db/retract (:db/id page) :block/namespace]]))]
+
+    (when old-namespace?
+      (if new-namespace?
+        (update-namespace!)
+        (remove-namespace!)))
+
+    (when-not old-namespace?
+      (when new-namespace?
+        (update-namespace!)))))
+
 (defn- rename-page-aux
   "Only accepts unsanitized page names"
   [old-name new-name redirect?]
@@ -474,6 +499,8 @@
 
         (rename-update-refs! page old-original-name new-name)
 
+        (rename-update-namespace! page old-original-name new-name)
+
         (outliner-file/sync-to-file page))
 
       ;; Redirect to the newly renamed page
@@ -580,7 +607,12 @@
 
       (rename-update-refs! from-page
                            (util/get-page-original-name from-page)
-                           (util/get-page-original-name to-page)))
+                           (util/get-page-original-name to-page))
+
+      (rename-update-namespace! from-page
+                                (util/get-page-original-name from-page)
+                                (util/get-page-original-name to-page)))
+
 
     (delete! from-page-name nil)
 

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

@@ -24,7 +24,6 @@
             [promesa.core :as p]
             [shadow.resource :as rc]
             [frontend.db.persist :as db-persist]
-            [logseq.graph-parser.util :as gp-util]
             [logseq.graph-parser :as graph-parser]
             [logseq.graph-parser.config :as gp-config]
             [electron.ipc :as ipc]
@@ -302,7 +301,7 @@
                              [])
               add-or-modify-files (some->>
                                    (concat modify-files add-files)
-                                   (gp-util/remove-nils))
+                                   (remove nil?))
               options {:delete-files (concat delete-files delete-pages)
                        :delete-blocks delete-blocks
                        :re-render? true}]

+ 1 - 0
src/main/frontend/handler/search.cljs

@@ -27,6 +27,7 @@
   (text/remove-level-spaces content format (config/get-block-pattern format)))
 
 (defn search
+  "The aggretation of search results"
   ([q]
    (search (state/get-current-repo) q))
   ([repo q]

+ 1 - 1
src/main/frontend/handler/user.cljs

@@ -146,7 +146,7 @@
         ;; refresh remote graph list by pub login event
         (when (user-uuid) (state/pub-event! [:user/fetch-info-and-graphs]))))))
 
-(defn login-callback [code]
+(defn ^:export login-callback [code]
   (state/set-state! [:ui/loading? :login] true)
   (go
     (let [resp (<! (http/get (str "https://" config/API-DOMAIN "/auth_callback?code=" code)

+ 3 - 0
src/main/frontend/publishing.cljs

@@ -9,6 +9,7 @@
             [frontend.page :as page]
             [frontend.util :as util]
             [frontend.routes :as routes]
+            [frontend.context.i18n :as i18n]
             [reitit.frontend :as rf]
             [reitit.frontend.easy :as rfe]
             [cljs.reader :as reader]
@@ -79,6 +80,8 @@
   ;; this is called in the index.html and must be exported
   ;; so it is available even in :advanced release builds
   (register-components-fns!)
+  ;; Set :preferred-lang as some components depend on it
+  (i18n/start)
   (restore-from-transit-str!)
   (restore-state!)
   (shortcut/refresh!)

+ 75 - 0
src/main/frontend/pubsub.cljc

@@ -0,0 +1,75 @@
+(ns frontend.pubsub
+  "All mults and pubs are collected to this ns.
+  vars with suffix '-mult' is a/Mult, use a/tap and a/untap on them. used by event subscribers
+  vars with suffix '-pub' is a/Pub, use a/sub and a/unsub on them. used by event subscribers
+  vars with suffix '-ch' is chan used by event publishers."
+  {:clj-kondo/config {:linters {:unresolved-symbol {:level :off}}}}
+  #?(:cljs (:require-macros [frontend.pubsub :refer [def-mult-or-pub chan-of]]))
+  (:require [clojure.core.async :as a :refer [chan mult pub]]
+            [clojure.core.async.impl.protocols :as ap]
+            [malli.core :as m]
+            [malli.dev.pretty :as mdp]
+            [clojure.pprint :as pp]))
+
+;;; helper macro
+(defmacro chan-of [malli-schema malli-schema-validator & chan-args]
+  `(let [ch# (chan ~@chan-args)]
+     (reify
+       ap/ReadPort
+       (~'take! [~'_ fn1-handler#]
+        (ap/take! ch# fn1-handler#))
+       ap/WritePort
+       (~'put! [~'_ val# fn1-handler#]
+        (if (~malli-schema-validator val#)
+          (ap/put! ch# val# fn1-handler#)
+          (do (mdp/explain ~malli-schema val#)
+              (throw (ex-info "validate chan value failed" {:val val#}))))))))
+
+(defmacro def-mult-or-pub
+  "define following vars:
+  - `symbol-name`-ch for event publisher.
+  - `symbol-name`-mult or `symbol-name`-pub for event subscribers.
+  - `symbol-name`-validator is malli schema validator
+  def -pub var when `:topic-fn` exists otherwise -mult var"
+  [symbol-name doc-string malli-schema & {:keys [ch-buffer topic-fn]
+                                          :or   {ch-buffer 1}}]
+  (let [schema-validator-name (symbol (str symbol-name "-validator"))
+        schema-name           (symbol (str symbol-name "-schema"))
+        ch-name               (symbol (str symbol-name "-ch"))
+        mult-or-pub-name      (if topic-fn
+                                (symbol (str symbol-name "-pub"))
+                                (symbol (str symbol-name "-mult")))
+        doc-string*           (str doc-string "\nMalli-schema:\n" (with-out-str (pp/pprint malli-schema)))]
+    `(do
+       (def ~schema-name ~malli-schema)
+       (def ~schema-validator-name (m/validator ~malli-schema))
+       (def ~ch-name ~doc-string* (chan-of ~malli-schema ~schema-validator-name ~ch-buffer))
+       ~(if topic-fn
+          `(def ~mult-or-pub-name ~doc-string* (pub ~ch-name ~topic-fn))
+          `(def ~mult-or-pub-name ~doc-string* (mult ~ch-name))))))
+
+;;; all chan, mult, pub defined here...
+
+(def-mult-or-pub app-wake-up-from-sleep
+  "app wake up from sleep event"
+  [:map
+   [:last-activated-at :int]
+   [:now :int]])
+
+(def-mult-or-pub sync-events
+  "file-sync events"
+  [:map
+   [:event [:enum
+            :created-local-version-file
+            :finished-local->remote
+            :finished-remote->local
+            :start
+            :pause
+            :resume
+            :exception-decrypt-failed
+            :remote->local-full-sync-failed
+            :local->remote-full-sync-failed
+            :get-remote-graph-failed
+            :get-deletion-logs-failed]]
+   [:data :map]]
+  :topic-fn :event)

+ 11 - 2
src/main/frontend/routes.cljs

@@ -10,8 +10,9 @@
             [frontend.components.search :as search]
             [frontend.components.settings :as settings]
             [frontend.components.shortcut :as shortcut]
-            [frontend.components.whiteboard :as whiteboard]
-            [frontend.extensions.zotero :as zotero]))
+            [frontend.components.whiteboard :as whiteboard] 
+            [frontend.extensions.zotero :as zotero]
+            [frontend.components.bug-report :as bug-report]))
 
 ;; http://localhost:3000/#?anchor=fn.1
 (def routes
@@ -78,6 +79,14 @@
    ["/import"
     {:name :import
      :view setups/importer}]
+   
+   ["/bug-report"
+    {:name :bug-report
+     :view bug-report/bug-report}]
+   
+    ["/bug-report-tool/:tool"
+     {:name :bug-report-tools
+      :view bug-report/bug-report-tool-route}]
 
    ["/all-journals"
     {:name :all-journals

+ 3 - 0
src/main/frontend/search.cljs

@@ -111,6 +111,9 @@
     (protocol/transact-blocks! engine data)))
 
 (defn- transact-pages!
+  "Transact pages to search engine
+   :pages-to-remove-set the set of pages to remove (not include those to update)
+   :pages-to-add        the page entities to add"
   [repo data]
   (when-let [engine (get-engine repo)]
     (protocol/transact-pages! engine data)))

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

@@ -28,7 +28,8 @@
             [rum.core :as rum]
             [clojure.core.async :as async]
             [cljs.core.async.impl.channels :refer [ManyToManyChannel]]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.pubsub :as pubsub]))
   (:require
    [clojure.pprint]
    [clojure.string :as string]
@@ -1448,11 +1449,9 @@
 
 #?(:cljs
    (do
-     (def ^:private app-wake-up-from-sleep-chan (async/chan 1))
-     (def app-wake-up-from-sleep-mult (async/mult app-wake-up-from-sleep-chan))
      (defn <app-wake-up-from-sleep-loop
        "start a async/go-loop to check the app awake from sleep.
-Use (async/tap `app-wake-up-from-sleep-mult`) to receive messages.
+Use (async/tap `pubsub/app-wake-up-from-sleep-mult`) to receive messages.
 Arg *stop: atom, reset to true to stop the loop"
        [*stop]
        (let [*last-activated-at (volatile! (tc/to-epoch (t/now)))]
@@ -1461,7 +1460,7 @@ Arg *stop: atom, reset to true to stop the loop"
              (println :<app-wake-up-from-sleep-loop :stop)
              (let [now-epoch (tc/to-epoch (t/now))]
                (when (< @*last-activated-at (- now-epoch 10))
-                 (async/>! app-wake-up-from-sleep-chan {:last-activated-at @*last-activated-at :now now-epoch}))
+                 (async/>! pubsub/app-wake-up-from-sleep-ch {:last-activated-at @*last-activated-at :now now-epoch}))
                (vreset! *last-activated-at now-epoch)
                (async/<! (async/timeout 5000))
                (recur))))))))

+ 1 - 1
src/main/frontend/version.cljs

@@ -1,3 +1,3 @@
 (ns ^:no-doc frontend.version)
 
-(defonce version "0.8.15")
+(defonce version "0.8.16")

+ 12 - 0
src/test/frontend/db/query_custom_test.cljs

@@ -51,6 +51,18 @@
                                            (not [?b :block/marker _])]]]})))
         "advanced query that uses rule from logseq and rule from :inputs")
 
+    (is (= ["LATER b3"]
+           (map :block/content
+                (custom-query {:query '[:find (pull ?b [*])
+                                        :in $ %
+                                        :where
+                                        (starts-with ?b "LA")
+                                        (task ?b #{"LATER"})]
+                               :rules '[[(starts-with ?b ?substr)
+                                         [?b :block/content ?content]
+                                         [(clojure.string/starts-with? ?content ?substr)]]]})))
+        "advanced query that uses :rules and rules from logseq")
+
     (is (= #{"page1"}
            (set
             (map #(get-in % [:block/page :block/name])

+ 2 - 1
src/test/frontend/extensions/pdf/assets_test.cljs

@@ -7,7 +7,8 @@
     (are [x y] (= y (pdf-utils/fix-local-asset-pagename x))
       "2015_Book_Intertwingled_1659920114630_0" "2015 Book Intertwingled"
       "hls__2015_Book_Intertwingled_1659920114630_0" "2015 Book Intertwingled"
-      "hls/2015_Book_Intertwingled_1659920114630_0" "hls/2015 Book Intertwingled"))
+      "hls/2015_Book_Intertwingled_1659920114630_0" "hls/2015 Book Intertwingled"
+      "hls__sicp__-1234567" "sicp"))
   (testing "non matched filenames"
     (are [x y] (= y (pdf-utils/fix-local-asset-pagename x))
       "foo" "foo"