Selaa lähdekoodia

Merge branch 'feat/db' into perf/app-start

Tienson Qin 6 kuukautta sitten
vanhempi
sitoutus
4169f20a28
84 muutettua tiedostoa jossa 1517 lisäystä ja 795 poistoa
  1. 3 3
      .github/workflows/build.yml
  2. 3 3
      .github/workflows/db.yml
  3. 3 3
      .github/workflows/graph-parser.yml
  4. 2 2
      android/app/capacitor.build.gradle
  5. 2 2
      android/build.gradle
  6. BIN
      android/gradle/wrapper/gradle-wrapper.jar
  7. 3 1
      android/gradle/wrapper/gradle-wrapper.properties
  8. 31 13
      android/gradlew
  9. 21 16
      android/gradlew.bat
  10. 11 11
      android/variables.gradle
  11. 2 1
      deps.edn
  12. 8 20
      deps/db/src/logseq/db/frontend/property.cljs
  13. 36 25
      deps/db/src/logseq/db/frontend/property/build.cljs
  14. 36 22
      deps/db/src/logseq/db/sqlite/build.cljs
  15. 127 64
      deps/db/src/logseq/db/sqlite/export.cljs
  16. 40 1
      deps/db/test/logseq/db/sqlite/build_test.cljs
  17. 68 9
      deps/db/test/logseq/db/sqlite/export_test.cljs
  18. 2 3
      deps/graph-parser/nbb.edn
  19. 2 1
      deps/graph-parser/package.json
  20. 44 21
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  21. 10 3
      deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
  22. 6 0
      deps/graph-parser/test/logseq/graph_parser/test_runner.cljs
  23. 3 1
      deps/shui/deps.edn
  24. 6 5
      deps/shui/src/logseq/shui/demo.cljs
  25. 235 236
      deps/shui/src/logseq/shui/demo2.cljs
  26. 12 8
      deps/shui/src/logseq/shui/dialog/core.cljs
  27. 13 7
      deps/shui/src/logseq/shui/hooks.cljs
  28. 114 113
      deps/shui/src/logseq/shui/select/multi.cljs
  29. 3 2
      deps/shui/src/logseq/shui/table/core.cljc
  30. 17 16
      deps/shui/src/logseq/shui/toaster/core.cljs
  31. 1 1
      gulpfile.js
  32. 1 1
      ios/App/Podfile
  33. 131 0
      ios/App/Podfile.lock
  34. 18 18
      package.json
  35. 1 1
      src/main/frontend/components/assets.cljs
  36. 1 1
      src/main/frontend/components/block.cljs
  37. 1 1
      src/main/frontend/components/bug_report.cljs
  38. 1 1
      src/main/frontend/components/cmdk/core.cljs
  39. 1 1
      src/main/frontend/components/cmdk/list_item.cljs
  40. 1 1
      src/main/frontend/components/container.cljs
  41. 4 2
      src/main/frontend/components/content.cljs
  42. 1 1
      src/main/frontend/components/dnd.cljs
  43. 1 1
      src/main/frontend/components/editor.cljs
  44. 26 4
      src/main/frontend/components/export.cljs
  45. 1 1
      src/main/frontend/components/file_based/block.cljs
  46. 5 5
      src/main/frontend/components/file_based/git.cljs
  47. 1 1
      src/main/frontend/components/file_sync.cljs
  48. 1 1
      src/main/frontend/components/handbooks.cljs
  49. 1 1
      src/main/frontend/components/header.cljs
  50. 1 1
      src/main/frontend/components/icon.cljs
  51. 1 1
      src/main/frontend/components/imports.cljs
  52. 1 0
      src/main/frontend/components/objects.cljs
  53. 3 2
      src/main/frontend/components/page.cljs
  54. 2 1
      src/main/frontend/components/page_menu.cljs
  55. 1 1
      src/main/frontend/components/plugins.cljs
  56. 1 1
      src/main/frontend/components/plugins_settings.cljs
  57. 1 1
      src/main/frontend/components/property.cljs
  58. 1 1
      src/main/frontend/components/property/config.cljs
  59. 1 1
      src/main/frontend/components/property/value.cljs
  60. 1 0
      src/main/frontend/components/query.cljs
  61. 1 1
      src/main/frontend/components/query/builder.cljs
  62. 1 1
      src/main/frontend/components/reference.cljs
  63. 1 1
      src/main/frontend/components/right_sidebar.cljs
  64. 1 1
      src/main/frontend/components/select.cljs
  65. 1 1
      src/main/frontend/components/server.cljs
  66. 1 1
      src/main/frontend/components/settings.cljs
  67. 1 1
      src/main/frontend/components/shortcut.cljs
  68. 1 1
      src/main/frontend/components/theme.cljs
  69. 1 1
      src/main/frontend/components/user/login.cljs
  70. 1 1
      src/main/frontend/components/views.cljs
  71. 1 1
      src/main/frontend/components/whiteboard.cljs
  72. 2 2
      src/main/frontend/db/model.cljs
  73. 1 1
      src/main/frontend/extensions/handbooks/core.cljs
  74. 1 1
      src/main/frontend/extensions/pdf/core.cljs
  75. 1 1
      src/main/frontend/extensions/pdf/toolbar.cljs
  76. 1 1
      src/main/frontend/extensions/tldraw.cljs
  77. 1 1
      src/main/frontend/extensions/zotero.cljs
  78. 25 26
      src/main/frontend/handler/db_based/export.cljs
  79. 1 1
      src/main/frontend/mobile/graph_picker.cljs
  80. 1 1
      src/main/frontend/rum.cljs
  81. 1 1
      src/main/frontend/state.cljs
  82. 1 1
      src/main/frontend/ui.cljs
  83. 2 1
      src/main/frontend/worker/db_worker.cljs
  84. 393 81
      yarn.lock

+ 3 - 3
.github/workflows/build.yml

@@ -112,13 +112,13 @@ jobs:
         run: clojure -M:clj-kondo --parallel --lint src
 
       - name: Carve lint for unused vars
-        run: bb lint:carve 2>/dev/null
+        run: bb lint:carve
 
       - name: Lint for vars that are too large
-        run: bb lint:large-vars 2>/dev/null
+        run: bb lint:large-vars
 
       - name: Lint for namespaces that aren't documented
-        run: bb lint:ns-docstrings 2>/dev/null
+        run: bb lint:ns-docstrings
 
       - name: Lint invalid translation entries
         run: bb lang:validate-translations

+ 3 - 3
.github/workflows/db.yml

@@ -86,13 +86,13 @@ jobs:
         run: clojure -M:clj-kondo --lint src test
 
       - name: Carve lint for unused vars
-        run: bb lint:carve 2>/dev/null
+        run: bb lint:carve
 
       - name: Lint for vars that are too large
-        run: bb lint:large-vars 2>/dev/null
+        run: bb lint:large-vars
 
       - name: Lint datalog rules
         run: bb lint:rules
 
       - name: Lint for namespaces that aren't documented
-        run: bb lint:ns-docstrings 2>/dev/null
+        run: bb lint:ns-docstrings

+ 3 - 3
.github/workflows/graph-parser.yml

@@ -108,10 +108,10 @@ jobs:
         run: clojure -M:clj-kondo --parallel --lint src test
 
       - name: Carve lint for unused vars
-        run: bb lint:carve 2>/dev/null
+        run: bb lint:carve
 
       - name: Lint for vars that are too large
-        run: bb lint:large-vars 2>/dev/null
+        run: bb lint:large-vars
 
       - name: Lint for namespaces that aren't documented
-        run: bb lint:ns-docstrings 2>/dev/null
+        run: bb lint:ns-docstrings

+ 2 - 2
android/app/capacitor.build.gradle

@@ -2,8 +2,8 @@
 
 android {
   compileOptions {
-      sourceCompatibility JavaVersion.VERSION_17
-      targetCompatibility JavaVersion.VERSION_17
+      sourceCompatibility JavaVersion.VERSION_21
+      targetCompatibility JavaVersion.VERSION_21
   }
 }
 

+ 2 - 2
android/build.gradle

@@ -8,8 +8,8 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:8.1.1'
-        classpath 'com.google.gms:google-services:4.3.15'
+        classpath 'com.android.tools.build:gradle:8.7.2'
+        classpath 'com.google.gms:google-services:4.4.2'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files

BIN
android/gradle/wrapper/gradle-wrapper.jar


+ 3 - 1
android/gradle/wrapper/gradle-wrapper.properties

@@ -1,5 +1,7 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
+networkTimeout=10000
+validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists

+ 31 - 13
android/gradlew

@@ -15,6 +15,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+# SPDX-License-Identifier: Apache-2.0
+#
 
 ##############################################################################
 #
@@ -55,7 +57,7 @@
 #       Darwin, MinGW, and NonStop.
 #
 #   (3) This script is generated from the Groovy template
-#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
 #       within the Gradle project.
 #
 #       You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +82,12 @@ do
     esac
 done
 
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-APP_NAME="Gradle"
+# This is normally unused
+# shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD=maximum
@@ -133,22 +134,29 @@ location of your Java installation."
     fi
 else
     JAVACMD=java
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
+    fi
 fi
 
 # Increase the maximum file descriptors if we can.
 if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
     case $MAX_FD in #(
       max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
         MAX_FD=$( ulimit -H -n ) ||
             warn "Could not query maximum file descriptor limit"
     esac
     case $MAX_FD in  #(
       '' | soft) :;; #(
       *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
         ulimit -n "$MAX_FD" ||
             warn "Could not set maximum file descriptor limit to $MAX_FD"
     esac
@@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then
     done
 fi
 
-# Collect all arguments for the java command;
-#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-#     shell script including quotes and variable substitutions, so put them in
-#     double quotes to make sure that they get re-expanded; and
-#   * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
 
 set -- \
         "-Dorg.gradle.appname=$APP_BASE_NAME" \
@@ -205,6 +217,12 @@ set -- \
         org.gradle.wrapper.GradleWrapperMain \
         "$@"
 
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
 # Use "xargs" to parse quoted args.
 #
 # With -n1 it outputs one arg per line, with the quotes and backslashes removed.

+ 21 - 16
android/gradlew.bat

@@ -13,8 +13,10 @@
 @rem See the License for the specific language governing permissions and
 @rem limitations under the License.
 @rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
 
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
 @rem ##########################################################################
 @rem
 @rem  Gradle startup script for Windows
@@ -25,7 +27,8 @@
 if "%OS%"=="Windows_NT" setlocal
 
 set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
 
 set JAVA_EXE=java.exe
 %JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
 
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
 
 goto fail
 
@@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
 
 if exist "%JAVA_EXE%" goto execute
 
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
 
 goto fail
 
@@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
 
 :end
 @rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
 
 :fail
 rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
 rem the _cmd.exe /c_ return code!
-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
 
 :mainEnd
 if "%OS%"=="Windows_NT" endlocal

+ 11 - 11
android/variables.gradle

@@ -1,16 +1,16 @@
 ext {
-    minSdkVersion = 22
-    compileSdkVersion = 33
-    targetSdkVersion = 33
-    androidxActivityVersion = '1.7.0'
-    androidxAppCompatVersion = '1.6.1'
+    minSdkVersion = 23
+    compileSdkVersion = 35
+    targetSdkVersion = 35
+    androidxActivityVersion = '1.9.2'
+    androidxAppCompatVersion = '1.7.0'
     androidxCoordinatorLayoutVersion = '1.2.0'
-    androidxCoreVersion = '1.10.0'
-    androidxFragmentVersion = '1.5.6'
+    androidxCoreVersion = '1.15.0'
+    androidxFragmentVersion = '1.8.4'
     junitVersion = '4.13.2'
-    androidxJunitVersion = '1.1.5'
-    androidxEspressoCoreVersion = '3.5.1'
+    androidxJunitVersion = '1.2.1'
+    androidxEspressoCoreVersion = '3.6.1'
     cordovaAndroidVersion = '10.1.1'
-    coreSplashScreenVersion = '1.0.0'
-    androidxWebkitVersion = '1.6.1'
+    coreSplashScreenVersion = '1.0.1'
+    androidxWebkitVersion = '1.12.1'
 }

+ 2 - 1
deps.edn

@@ -1,7 +1,8 @@
 {:paths ["src/main" "src/electron" "src/resources"]
  :deps
  {org.clojure/clojure                   {:mvn/version "1.11.1"}
-  rum/rum                               {:mvn/version "0.12.9"}
+  rum/rum                               {:git/url "https://github.com/logseq/rum" ;; fork
+                                         :sha     "5d672bf84ed944414b9f61eeb83808ead7be9127"}
 
   datascript/datascript                 {:git/url "https://github.com/logseq/datascript" ;; fork
                                          :sha     "1f84d10df4970f054489b0ee78799f64b8dd4ee2"}

+ 8 - 20
deps/db/src/logseq/db/frontend/property.cljs

@@ -430,6 +430,9 @@
                                      {:type :coll
                                       :hide? true
                                       :public? false}
+                                     ;; ignore this property when rtc,
+                                     ;; since users frequently click the sort button to view table content temporarily,
+                                     ;; but this action does not need to be synchronized with other clients.
                                      :rtc {:rtc/ignore-attr-when-init-upload true
                                            :rtc/ignore-attr-when-init-download true
                                            :rtc/ignore-attr-when-syncing true}}
@@ -438,47 +441,32 @@
                                      :schema
                                      {:type :map
                                       :hide? true
-                                      :public? false}
-                                     :rtc {:rtc/ignore-attr-when-init-upload true
-                                           :rtc/ignore-attr-when-init-download true
-                                           :rtc/ignore-attr-when-syncing true}}
+                                      :public? false}}
 
      :logseq.property.table/hidden-columns {:title "View hidden columns"
                                             :schema
                                             {:type :keyword
                                              :cardinality :many
                                              :hide? true
-                                             :public? false}
-                                            :rtc {:rtc/ignore-attr-when-init-upload true
-                                                  :rtc/ignore-attr-when-init-download true
-                                                  :rtc/ignore-attr-when-syncing true}}
+                                             :public? false}}
 
      :logseq.property.table/ordered-columns {:title "View ordered columns"
                                              :schema
                                              {:type :coll
                                               :hide? true
-                                              :public? false}
-                                             :rtc {:rtc/ignore-attr-when-init-upload true
-                                                   :rtc/ignore-attr-when-init-download true
-                                                   :rtc/ignore-attr-when-syncing true}}
+                                              :public? false}}
 
      :logseq.property.table/sized-columns {:title "View columns settings"
                                            :schema
                                            {:type :map
                                             :hide? true
-                                            :public? false}
-                                           :rtc {:rtc/ignore-attr-when-init-upload true
-                                                 :rtc/ignore-attr-when-init-download true
-                                                 :rtc/ignore-attr-when-syncing true}}
+                                            :public? false}}
      :logseq.property.table/pinned-columns {:title "Table view pinned columns"
                                             :schema
                                             {:type :property
                                              :cardinality :many
                                              :hide? true
-                                             :public? false}
-                                            :rtc {:rtc/ignore-attr-when-init-upload true
-                                                  :rtc/ignore-attr-when-init-download true
-                                                  :rtc/ignore-attr-when-syncing true}}
+                                             :public? false}}
      :logseq.property/view-for {:title "This view belongs to"
                                 :schema
                                 {:type :node

+ 36 - 25
deps/db/src/logseq/db/frontend/property/build.cljs

@@ -65,26 +65,32 @@
     (into [property-tx]
           (closed-values->blocks property))))
 
-(defn- build-property-value-block
+(defn build-property-value-block
   "Builds a property value entity given a block map/entity, a property entity or
-  ident and its property value"
-  [block property value & {:keys [block-uuid]}]
+  ident and its property value. Takes the following options:
+   * :block-uuid - :block/uuid for property value entity
+   * :properties - Additional properties and attributes to add to entity"
+  [block property value & {:keys [block-uuid properties]}]
   (let [block-id (or (:db/id block) (:db/ident block))]
-    (-> (merge
-         {:block/uuid (or block-uuid (common-uuid/gen-uuid))
-          :block/page (if (:block/page block)
-                        (:db/id (:block/page block))
+    (cond->
+     (merge
+      {:block/uuid (or block-uuid (common-uuid/gen-uuid))
+       :block/page (if (:block/page block)
+                     (:db/id (:block/page block))
                         ;; page block
-                        block-id)
-          :block/parent block-id
-          :logseq.property/created-from-property (if (= (:db/ident property) :logseq.property/default-value)
-                                                   block-id
-                                                   (or (:db/id property) {:db/ident (:db/ident property)}))
-          :block/order (db-order/gen-key)}
-         (if (db-property-type/property-value-content? (:logseq.property/type property) property)
-           {:logseq.property/value value}
-           {:block/title value}))
-        common-util/block-with-timestamps)))
+                     block-id)
+       :block/parent block-id
+       :logseq.property/created-from-property (if (= (:db/ident property) :logseq.property/default-value)
+                                                block-id
+                                                (or (:db/id property) {:db/ident (:db/ident property)}))
+       :block/order (db-order/gen-key)}
+      (if (db-property-type/property-value-content? (:logseq.property/type property) property)
+        {:logseq.property/value value}
+        {:block/title value}))
+      true
+      common-util/block-with-timestamps
+      properties
+      (merge properties))))
 
 (defn build-property-values-tx-m
   "Builds a map of property names to their property value blocks to be
@@ -99,7 +105,7 @@
   (let [block' (if (:db/id block) block (assoc block :db/id [:block/uuid (:block/uuid block)]))]
     (->> properties
          (map (fn [[k v]]
-                (let [property-map (if (map? k) k {:db/ident k})
+                (let [{:keys [property-value-properties] :as property-map} (if (map? k) k {:db/ident k})
                       gen-uuid-value-prefix (when pure?
                                               (or (:db/ident block) (:block/uuid block)))]
                   (assert (:db/ident property-map) "Key in map must have a :db/ident")
@@ -108,15 +114,20 @@
                    (if (set? v)
                      (set (map #(build-property-value-block
                                  block' property-map %
-                                 (when pure?
-                                   {:block-uuid
-                                    (common-uuid/gen-uuid :builtin-block-uuid (str gen-uuid-value-prefix "-" %))}))
+                                 (cond-> {}
+                                   property-value-properties
+                                   (assoc :properties property-value-properties)
+                                   pure?
+                                   (assoc :block-uuid
+                                          (common-uuid/gen-uuid :builtin-block-uuid (str gen-uuid-value-prefix "-" %)))))
                                v))
                      (build-property-value-block block' property-map v
-                                                 (when pure?
-                                                   {:block-uuid
-                                                    (common-uuid/gen-uuid
-                                                     :builtin-block-uuid (str gen-uuid-value-prefix "-" v))})))])))
+                                                 (cond-> {}
+                                                   property-value-properties
+                                                   (assoc :properties property-value-properties)
+                                                   pure?
+                                                   (assoc :block-uuid
+                                                          (common-uuid/gen-uuid :builtin-block-uuid (str gen-uuid-value-prefix "-" v))))))])))
          (into {}))))
 
 (defn build-properties-with-ref-values

+ 36 - 22
deps/db/src/logseq/db/sqlite/build.cljs

@@ -99,6 +99,30 @@
   "Provides the next temp :db/id to use in a create-graph transact!"
   #(swap! current-db-id dec))
 
+(defn- build-property-map-for-pvalue-tx
+  "Returns a property map if the given property pair should have a property value entity constructured
+   or nil if it should not. Property maps must at least contain the :db/ident and :logseq.property/type keys"
+  [k v new-block properties-config all-idents]
+  (if-let [built-in-type (get-in db-property/built-in-properties [k :schema :type])]
+    (if (and (db-property-type/value-ref-property-types built-in-type)
+             ;; closed values are referenced by their :db/ident so no need to create values
+             (not (get-in db-property/built-in-properties [k :closed-values])))
+      {:db/ident k
+       :logseq.property/type built-in-type}
+      (when-let [built-in-type' (get (or (:build/properties-ref-types new-block)
+                                         ;; Reasonable default for properties like logseq.property/default-value
+                                         {:entity :number})
+                                     built-in-type)]
+        {:db/ident k
+         :logseq.property/type built-in-type'}))
+    (when (and (db-property-type/value-ref-property-types (get-in properties-config [k :logseq.property/type]))
+               ;; Don't build property value entity if values are :block/uuid refs
+               (if (set? v) (not (vector? (first v))) (not (vector? v))))
+      (let [prop-type (get-in properties-config [k :logseq.property/type])]
+        {:db/ident (get-ident all-idents k)
+         :original-property-id k
+         :logseq.property/type prop-type}))))
+
 (defn- ->property-value-tx-m
   "Given a new block and its properties, creates a map of properties which have values of property value tx.
    This map is used for both creating the new property values and then adding them to a block.
@@ -106,28 +130,18 @@
   [new-block properties properties-config all-idents]
   (->> properties
        (keep (fn [[k v]]
-               (if-let [built-in-type (get-in db-property/built-in-properties [k :schema :type])]
-                 (if (and (db-property-type/value-ref-property-types built-in-type)
-                          ;; closed values are referenced by their :db/ident so no need to create values
-                          (not (get-in db-property/built-in-properties [k :closed-values])))
-                   (let [property-map {:db/ident k
-                                       :logseq.property/type built-in-type}]
-                     [property-map v])
-                   (when-let [built-in-type' (get (or (:build/properties-ref-types new-block)
-                                                      ;; Reasonable default for properties like logseq.property/default-value
-                                                      {:entity :number})
-                                                  built-in-type)]
-                     (let [property-map {:db/ident k
-                                         :logseq.property/type built-in-type'}]
-                       [property-map v])))
-                 (when (and (db-property-type/value-ref-property-types (get-in properties-config [k :logseq.property/type]))
-                            ;; TODO: Support translate-property-value without this hack
-                            (not (vector? v)))
-                   (let [prop-type (get-in properties-config [k :logseq.property/type])
-                         property-map {:db/ident (get-ident all-idents k)
-                                       :original-property-id k
-                                       :logseq.property/type prop-type}]
-                     [property-map v])))))
+               (when-let [property-map (build-property-map-for-pvalue-tx k v new-block properties-config all-idents)]
+                 [(let [pvalue-attrs (when (:build/property-value v)
+                                       (merge (:build/properties v)
+                                              {:block/tags (mapv #(hash-map :db/ident (get-ident all-idents %))
+                                                                 (:build/tags v))}
+                                              (select-keys v [:block/created-at :block/updated-at])))]
+                    (cond-> property-map
+                      (and (:build/property-value v) (seq pvalue-attrs))
+                      (assoc :property-value-properties pvalue-attrs)))
+                  (if (:build/property-value v)
+                    (or (:logseq.property/value v) (:block/title v))
+                    v)])))
        (db-property-build/build-property-values-tx-m new-block)))
 
 (defn- extract-basic-content-refs

+ 127 - 64
deps/db/src/logseq/db/sqlite/export.cljs

@@ -12,7 +12,8 @@
             [logseq.db.frontend.entity-plus :as entity-plus]
             [logseq.db.frontend.entity-util :as entity-util]
             [logseq.db.frontend.property :as db-property]
-            [logseq.db.sqlite.build :as sqlite-build]))
+            [logseq.db.sqlite.build :as sqlite-build]
+            [medley.core :as medley]))
 
 ;; Export fns
 ;; ==========
@@ -37,54 +38,84 @@
     {:build/journal (:block/journal-day page-entity)}
     {:block/title (block-title page-entity)}))
 
-(defn- buildable-property-value-entity
-  "Converts property value to a buildable version"
-  [property-ent pvalue {:keys [property-value-uuids?]}]
-  (cond (and (not property-value-uuids?) (ldb/internal-page? pvalue))
+(defn- build-pvalue-entity-for-build-page
+  [pvalue]
+  (cond (ldb/internal-page? pvalue)
         ;; Should page properties be pulled here?
         [:build/page (cond-> (shallow-copy-page pvalue)
                        (seq (:block/tags pvalue))
                        (assoc :build/tags (->build-tags (:block/tags pvalue))))]
-        (and (not property-value-uuids?) (entity-util/journal? pvalue))
-        [:build/page {:build/journal (:block/journal-day pvalue)}]
-        :else
-        (if (contains? #{:node :date} (:logseq.property/type property-ent))
-          ;; Idents take precedence over uuid because they are keep data graph-agnostic
-          (if (:db/ident pvalue)
-            (:db/ident pvalue)
-            ;; Use metadata distinguish from block references that don't exist like closed values
-            ^::existing-property-value? [:block/uuid (:block/uuid pvalue)])
-          (or (:db/ident pvalue)
-              ;; nbb-compatible version of db-property/property-value-content
-              (or (block-title pvalue)
-                  (:logseq.property/value pvalue))))))
+        (entity-util/journal? pvalue)
+        [:build/page {:build/journal (:block/journal-day pvalue)}]))
+
+(defn- build-pvalue-entity-default [ent-properties pvalue options]
+  (if (or (seq ent-properties) (seq (:block/tags pvalue)))
+    (cond-> {:build/property-value :block
+             :block/title (or (block-title pvalue)
+                              (:logseq.property/value pvalue))}
+      (seq (:block/tags pvalue))
+      (assoc :build/tags (->build-tags (:block/tags pvalue)))
+
+      (seq ent-properties)
+      (assoc :build/properties ent-properties)
+
+      (:include-timestamps? options)
+      (merge (select-keys pvalue [:block/created-at :block/updated-at])))
+    ;; nbb-compatible version of db-property/property-value-content
+    (or (block-title pvalue)
+        (:logseq.property/value pvalue))))
 
 (defn- buildable-properties
   "Originally copied from db-test/readable-properties. Modified so that property values are
    valid sqlite.build EDN"
   [db ent-properties properties-config options]
-  (->> ent-properties
-       (map (fn [[k v]]
-              [k
-               (if (and (:block/closed-value-property v) (not (db-property/logseq-property? k)))
-                 (if-let [closed-uuid (some #(when (= (:value %) (db-property/property-value-content v))
-                                               (:uuid %))
-                                            (get-in properties-config [k :build/closed-values]))]
-                   [:block/uuid closed-uuid]
-                   (throw (ex-info (str "No closed value found for content: " (pr-str (db-property/property-value-content v))) {:properties properties-config})))
-                 (cond
-                   (de/entity? v)
-                   (buildable-property-value-entity (d/entity db k) v options)
-                   (and (set? v) (every? de/entity? v))
-                   (let [property-ent (d/entity db k)]
-                     (set (map #(buildable-property-value-entity property-ent % options) v)))
-                   :else
-                   v))]))
-       (into {})))
+  (letfn [(build-pvalue-entity
+            [db' property-ent pvalue properties-config' {:keys [property-value-uuids?] :as options'}]
+            (if-let [build-page (and (not property-value-uuids?) (build-pvalue-entity-for-build-page pvalue))]
+              build-page
+              (if (contains? #{:node :date} (:logseq.property/type property-ent))
+                ;; Idents take precedence over uuid because they are keep data graph-agnostic
+                (if (:db/ident pvalue)
+                  (:db/ident pvalue)
+                  ;; Use metadata distinguish from block references that don't exist like closed values
+                  ^::existing-property-value? [:block/uuid (:block/uuid pvalue)])
+                (or (:db/ident pvalue)
+                    (let [ent-properties* (->> (apply dissoc (db-property/properties pvalue)
+                                                      :logseq.property/value :logseq.property/created-from-property
+                                                      db-property/public-db-attribute-properties)
+                                               ;; TODO: Allow user properties when sqlite.build supports it
+                                               (medley/filter-keys db-property/internal-property?))
+                          ent-properties (when (and (not (:block/closed-value-property pvalue)) (seq ent-properties*))
+                                           (buildable-properties db' ent-properties* properties-config' options'))]
+                      (build-pvalue-entity-default ent-properties pvalue options'))))))]
+    (->> ent-properties
+         (map (fn [[k v]]
+                [k
+                 ;; handle user closed value properties. built-ins have idents and shouldn't be handled here
+                 (if (and (not (db-property/logseq-property? k))
+                          (or (:block/closed-value-property v)
+                              (and (set? v) (:block/closed-value-property (first v)))))
+                   (let [find-closed-uuid (fn [val]
+                                            (or (some #(when (= (:value %) (db-property/property-value-content val))
+                                                         (:uuid %))
+                                                      (get-in properties-config [k :build/closed-values]))
+                                                (throw (ex-info (str "No closed value found for content: " (pr-str (db-property/property-value-content val))) {:properties properties-config}))))]
+                     (if (set? v)
+                       (set (map #(vector :block/uuid (find-closed-uuid %)) v))
+                       [:block/uuid (find-closed-uuid v)]))
+                   (cond
+                     (de/entity? v)
+                     (build-pvalue-entity db (d/entity db k) v properties-config options)
+                     (and (set? v) (every? de/entity? v))
+                     (let [property-ent (d/entity db k)]
+                       (set (map #(build-pvalue-entity db property-ent % properties-config options) v)))
+                     :else
+                     v))]))
+         (into {}))))
 
 (defn- build-export-properties
   "The caller of this fn is responsible for building :build/:property-classes unless shallow-copy?"
-  [db user-property-idents {:keys [include-properties? include-timestamps? include-uuid? shallow-copy?] :as options}]
+  [db user-property-idents {:keys [include-properties? include-timestamps? include-uuid? shallow-copy? include-alias?] :as options}]
   (let [properties-config-by-ent
         (->> user-property-idents
              (map (fn [ident]
@@ -98,6 +129,8 @@
                          (assoc :block/uuid (:block/uuid property) :build/keep-uuid? true)
                          include-timestamps?
                          (merge (select-keys property [:block/created-at :block/updated-at]))
+                         (and (not shallow-copy?) include-alias? (:block/alias property))
+                         (assoc :block/alias (set (map #(vector :block/uuid (:block/uuid %)) (:block/alias property))))
                          (and (not shallow-copy?) (:logseq.property/classes property))
                          (assoc :build/property-classes (mapv :db/ident (:logseq.property/classes property)))
                          (seq closed-values)
@@ -129,8 +162,7 @@
 (defn- build-export-class
   "The caller of this fn is responsible for building any classes or properties from this fn
    unless shallow-copy?"
-  [class-ent {:keys [include-parents? include-uuid? shallow-copy? include-timestamps?]
-              :or {include-parents? true}}]
+  [class-ent {:keys [include-uuid? shallow-copy? include-timestamps? include-alias?]}]
   (cond-> (select-keys class-ent [:block/title :block/collapsed?])
     include-uuid?
     (assoc :block/uuid (:block/uuid class-ent) :build/keep-uuid? true)
@@ -139,11 +171,10 @@
     (and (:logseq.property.class/properties class-ent) (not shallow-copy?))
     (assoc :build/class-properties
            (mapv :db/ident (:logseq.property.class/properties class-ent)))
-    (and (not shallow-copy?) (:block/alias class-ent))
+    (and (not shallow-copy?) include-alias? (:block/alias class-ent))
     (assoc :block/alias (set (map #(vector :block/uuid (:block/uuid %)) (:block/alias class-ent))))
     ;; It's caller's responsibility to ensure parent is included in final export
-    (and include-parents?
-         (not shallow-copy?)
+    (and (not shallow-copy?)
          (:logseq.property/parent class-ent)
          (not= :logseq.class/Root (:db/ident (:logseq.property/parent class-ent))))
     (assoc :build/class-parent
@@ -364,12 +395,26 @@
      :classes (apply merge (map :classes uuid-block-pages))
      :pages-and-blocks (mapv #(select-keys % [:page :blocks]) uuid-block-pages)}))
 
+(defn sort-pages-and-blocks
+  "Provide a reliable sort order since this tends to be large. Helps with diffing
+   and readability"
+  [pages-and-blocks]
+  (vec
+   (sort-by #(or (get-in % [:page :block/title])
+                 (some-> (get-in % [:page :build/journal]) str)
+                 (str (get-in % [:page :block/uuid])))
+            pages-and-blocks)))
+
 (defn- finalize-export-maps
-  "Given final export maps, merges them, adds any missing class parents and merges those in"
+  "Given final export maps, merges them, adds any missing class parents and merges those in.
+   If :pages-and-blocks exist, sorts them in order to have reliable sort order"
   [db & export-maps]
   (let [final-export* (apply merge-export-maps export-maps)
-        class-parents-export (some->> (:classes final-export*) (build-class-parents-export db))]
-    (merge-export-maps final-export* class-parents-export)))
+        class-parents-export (some->> (:classes final-export*) (build-class-parents-export db))
+        merged-map (merge-export-maps final-export* class-parents-export)]
+    (cond-> merged-map
+      (:pages-and-blocks merged-map)
+      (update :pages-and-blocks sort-pages-and-blocks))))
 
 (defn- build-block-export
   "Exports block for given block eid"
@@ -387,7 +432,7 @@
     (merge {::block (:node node-export)}
            block-export)))
 
-(defn- build-page-blocks-export [db page-entity {:keys [properties classes blocks ontology-page?] :as options}]
+(defn- build-page-blocks-export [db page-entity {:keys [properties classes blocks ontology-page? include-alias?] :as options}]
   (let [options' (cond-> (dissoc options :classes :blocks :graph-ontology)
                    (:exclude-ontology? options)
                    (assoc :properties (get-in options [:graph-ontology :properties])))
@@ -400,7 +445,7 @@
                (:node page-ent-export)
                (merge (dissoc (:node page-ent-export) :block/title)
                       (shallow-copy-page page-entity)
-                      (when (:block/alias page-entity)
+                      (when (and include-alias? (:block/alias page-entity))
                         {:block/alias (set (map #(vector :block/uuid (:block/uuid %)) (:block/alias page-entity)))})))
         page-blocks-export {:pages-and-blocks [{:page page :blocks blocks}]
                             :properties properties
@@ -439,7 +484,9 @@
         page-export (finalize-export-maps db page-export* uuid-block-export content-ref-export)]
     page-export))
 
-(defn build-view-nodes-export* [db nodes opts]
+(defn- build-nodes-export
+  "Export a mix of pages and blocks"
+  [db nodes opts]
   (let [node-pages (filter entity-util/page? nodes)
         pages-export
         (merge
@@ -452,9 +499,7 @@
         (->> node-blocks
              (group-by :block/page)
              (map (fn [[parent-page-ent blocks]]
-                    (merge (build-blocks-export db
-                                                (sort-by :block/order blocks)
-                                                (merge opts {:include-children? false}))
+                    (merge (build-blocks-export db (sort-by :block/order blocks) opts)
                            {:page (shallow-copy-page parent-page-ent)}))))
         pages-to-blocks-export
         {:properties (apply merge (map :properties pages-to-blocks))
@@ -474,7 +519,29 @@
         {:keys [content-ref-uuids content-ref-ents] :as content-ref-export}
         (build-content-ref-export db (into nodes property-value-ents))
         {:keys [pvalue-uuids] :as nodes-export}
-        (build-view-nodes-export* db nodes {:include-uuid-fn content-ref-uuids})
+        (build-nodes-export db nodes {:include-uuid-fn content-ref-uuids :include-children? false})
+        uuid-block-export (build-uuid-block-export db pvalue-uuids content-ref-ents {})
+        view-nodes-export (finalize-export-maps db nodes-export uuid-block-export content-ref-export)]
+    view-nodes-export))
+
+(defn- build-selected-nodes-export
+  "Exports given nodes selected by a user. Nodes can be a mix of blocks and pages"
+  [db eids]
+  (let [top-level-nodes (map #(d/entity db %) eids)
+        children-nodes (->> top-level-nodes
+                            ;; Remove pages b/c when selected their children are not highlighted
+                            (remove entity-util/page?)
+                            (mapcat #(rest (ldb/get-block-and-children db (:block/uuid %))))
+                            (remove :logseq.property/created-from-property))
+        nodes (concat top-level-nodes children-nodes)
+        property-value-ents (mapcat #(->> (apply dissoc (db-property/properties %) db-property/public-db-attribute-properties)
+                                          vals
+                                          (filter de/entity?))
+                                    nodes)
+        {:keys [content-ref-uuids content-ref-ents] :as content-ref-export}
+        (build-content-ref-export db (into nodes property-value-ents))
+        {:keys [pvalue-uuids] :as nodes-export}
+        (build-nodes-export db nodes {:include-uuid-fn content-ref-uuids :include-children? true})
         uuid-block-export (build-uuid-block-export db pvalue-uuids content-ref-ents {})
         view-nodes-export (finalize-export-maps db nodes-export uuid-block-export content-ref-export)]
     view-nodes-export))
@@ -607,16 +674,6 @@
                            :blocks (sqlite-build/update-each-block blocks remove-uuid-if-not-ref)})
                         pages-and-blocks))))))
 
-(defn sort-pages-and-blocks
-  "Provide a reliable sort order since this tends to be large. Helps with diffing
-   and readability"
-  [pages-and-blocks]
-  (vec
-   (sort-by #(or (get-in % [:page :block/title])
-                 (some-> (get-in % [:page :build/journal]) str)
-                 (str (get-in % [:page :block/uuid])))
-            pages-and-blocks)))
-
 (defn- add-ontology-for-include-namespaces
   "Adds :properties to export for given namespace parents. Current use case is for :exclude-namespaces
    so no need to add :classes yet"
@@ -644,7 +701,8 @@
    * :exclude-built-in-pages? - When set, built-in pages are excluded from export
    * :exclude-files? - When set, files are excluded from export"
   [db {:keys [exclude-files?] :as options*}]
-  (let [options (merge options* {:property-value-uuids? true})
+  (let [options (merge options* {:property-value-uuids? true
+                                 :include-alias? true})
         content-ref-uuids (get-graph-content-ref-uuids db options)
         ontology-options (merge options {:include-uuid? true})
         ontology-export (build-graph-ontology-export db ontology-options)
@@ -702,7 +760,10 @@
         ;; Only looks one-level deep in properties e.g. not inside :build/page
         ;; Doesn't find :block/link refs
         ref-uuids
-        (->> (concat (mapcat get-pvalue-uuids (vals classes))
+        (->> (concat (mapcat #(map second (:block/alias %)) (vals classes))
+                     (mapcat #(map second (:block/alias %)) (vals properties))
+                     (mapcat #(map second (:block/alias (:page %))) pages-and-blocks)
+                     (mapcat get-pvalue-uuids (vals classes))
                      (mapcat get-pvalue-uuids (vals properties))
                      (mapcat (comp get-pvalue-uuids :page) pages-and-blocks)
                      (mapcat #(sqlite-build/extract-from-blocks (:blocks %) get-pvalue-uuids) pages-and-blocks))
@@ -736,6 +797,8 @@
           (build-page-export db (:page-id options))
           :view-nodes
           (build-view-nodes-export db (:node-ids options))
+          :selected-nodes
+          (build-selected-nodes-export db (:node-ids options))
           :graph-ontology
           (build-graph-ontology-export db {})
           :graph

+ 40 - 1
deps/db/test/logseq/db/sqlite/build_test.cljs

@@ -205,4 +205,43 @@
            (->> (d/q '[:find [?b ...] :in $ ?page-id :where [?b :block/page ?page-id]]
                      @conn [:block/uuid property-uuid])
                 (map #(:block/title (d/entity @conn %)))))
-        "Property page has correct blocks")))
+        "Property page has correct blocks")))
+
+(deftest property-value-with-properties-and-tags
+  (let [conn (db-test/create-conn-with-blocks
+              {:properties {:p1 {:logseq.property/type :default}}
+               :classes {:C1 {}}
+               :pages-and-blocks
+               [{:page {:block/title "page1"}
+                 :blocks [{:block/title "block has pvalue with built-in tag"
+                           :build/properties
+                           {:p1 {:build/property-value :block
+                                 :block/title "t1"
+                                 :build/tags [:logseq.class/Task]}}}
+                          {:block/title "block has pvalue with user tag"
+                           :build/properties
+                           {:p1 {:build/property-value :block
+                                 :block/title "u1"
+                                 :build/tags [:C1]}}}
+                          {:block/title "Todo query",
+                           :build/tags [:logseq.class/Query],
+                           :build/properties
+                           {:logseq.property/query
+                            {:build/property-value :block
+                             :block/title "{:query (task Todo)}"
+                             :build/properties
+                             {:logseq.property.code/lang "clojure"
+                              :logseq.property.node/display-type :code}}}}]}]})]
+    (is (= {:logseq.property.node/display-type :code
+            :logseq.property.code/lang "clojure"}
+           (-> (db-test/find-block-by-content @conn "{:query (task Todo)}")
+               db-test/readable-properties
+               (dissoc :logseq.property/created-from-property))))
+    (is (= {:block/tags [:logseq.class/Task]}
+           (-> (db-test/find-block-by-content @conn "t1")
+               db-test/readable-properties
+               (dissoc :logseq.property/created-from-property))))
+    (is (= {:block/tags [:user.class/C1]}
+           (-> (db-test/find-block-by-content @conn "u1")
+               db-test/readable-properties
+               (dissoc :logseq.property/created-from-property))))))

+ 68 - 9
deps/db/test/logseq/db/sqlite/export_test.cljs

@@ -57,17 +57,21 @@
     (sqlite-export/build-export @import-conn {:export-type :page :page-id (:db/id page2)})))
 
 (defn- import-second-time-assertions [conn conn2 page-title original-data
-                                      & {:keys [transform-expected-blocks]
+                                      & {:keys [transform-expected-blocks build-journal]
                                          :or {transform-expected-blocks (fn [bs] (into bs bs))}}]
   (let [page (db-test/find-page-by-title @conn2 page-title)
         imported-page (export-page-and-import-to-another-graph conn conn2 page-title)
         updated-page (db-test/find-page-by-title @conn2 page-title)
         expected-page-and-blocks
-        (update-in (:pages-and-blocks original-data) [0 :blocks] transform-expected-blocks)]
+        (update-in (:pages-and-blocks original-data) [0 :blocks] transform-expected-blocks)
+        filter-imported-page (if build-journal
+                               #(= build-journal (get-in % [:page :build/journal]))
+                               #(= (get-in % [:page :block/title]) page-title))]
 
+    (assert (first expected-page-and-blocks))
     ;; Assume first page is one being imported for now
     (is (= (first expected-page-and-blocks)
-           (first (:pages-and-blocks imported-page)))
+           (first (filter filter-imported-page (:pages-and-blocks imported-page))))
         "Blocks are appended to existing page")
     (is (= (:block/created-at page) (:block/created-at updated-page))
         "Existing page didn't get re-created")
@@ -323,7 +327,8 @@
     (is (= (-> (:pages-and-blocks original-data)
                (medley/dissoc-in [1 :blocks 0 :build/properties])
                ;; shallow block means this page doesn't get included
-               butlast)
+               butlast
+               sort-pages-and-blocks)
            (:pages-and-blocks imported-page))
         "Page's blocks are imported")
 
@@ -412,7 +417,7 @@
     (is (= (:pages-and-blocks original-data) (:pages-and-blocks imported-page))
         "Page's blocks are imported")
 
-    (import-second-time-assertions conn conn2 journal-title original-data)))
+    (import-second-time-assertions conn conn2 journal-title original-data {:build-journal 20250210})))
 
 (deftest import-page-with-different-property-types
   (let [block-object-uuid (random-uuid)
@@ -458,7 +463,8 @@
         "Page's classes are imported")
     (is (= (-> (:pages-and-blocks original-data)
                ;; adjust shallow block
-               (medley/dissoc-in [1 :blocks 0 :build/tags]))
+               (medley/dissoc-in [1 :blocks 0 :build/tags])
+               sort-pages-and-blocks)
            (:pages-and-blocks imported-page))
         "Page's blocks are imported")
 
@@ -527,7 +533,44 @@
         imported-nodes (sqlite-export/build-export @conn2 {:export-type :view-nodes
                                                            :node-ids (get-node-ids @conn2)})]
 
-    (is (= (:pages-and-blocks original-data) (:pages-and-blocks imported-nodes)))
+    (is (= (sort-pages-and-blocks (:pages-and-blocks original-data)) (:pages-and-blocks imported-nodes)))
+    (is (= (expand-properties (:properties original-data)) (:properties imported-nodes)))
+    (is (= (expand-classes (:classes original-data)) (:classes imported-nodes)))))
+
+(deftest import-selected-nodes
+  (let [original-data
+        ;; Test a mix of pages and blocks
+        {:properties {:user.property/p1 {:logseq.property/type :default}}
+         :classes {:user.class/class1 {}}
+         :pages-and-blocks [{:page {:block/title "page1"}
+                             :blocks [{:block/title "b1"
+                                       :build/properties {:user.property/p1 "ok"}
+                                       :build/children [{:block/title "b2"}]}
+                                      {:block/title "b3"
+                                       :build/tags [:user.class/class1]
+                                       :build/children [{:block/title "b4"}]}]}
+                            {:page {:block/title "page2"}
+                             :blocks [{:block/title "dont export"}]}]}
+        conn (db-test/create-conn-with-blocks original-data)
+        get-node-ids (fn [db]
+                       (->> [(db-test/find-block-by-content db "b1")
+                             (db-test/find-page-by-title db "b3")
+                             (db-test/find-page-by-title db "page2")]
+                            (remove nil?)
+                            (mapv #(vector :block/uuid (:block/uuid %)))))
+        conn2 (db-test/create-conn)
+        {:keys [init-tx block-props-tx] :as _txs}
+        (-> (sqlite-export/build-export @conn {:export-type :selected-nodes :node-ids (get-node-ids @conn)})
+            (sqlite-export/build-import @conn2 {}))
+        ;; _ (cljs.pprint/pprint _txs)
+        _ (d/transact! conn2 init-tx)
+        _ (d/transact! conn2 block-props-tx)
+        _ (validate-db @conn2)
+        imported-nodes (sqlite-export/build-export @conn2 {:export-type :selected-nodes :node-ids (get-node-ids @conn2)})]
+
+    (is (= (->> (:pages-and-blocks original-data)
+                (map #(if (= (get-in % [:page :block/title]) "page2") (dissoc % :blocks) %)))
+           (:pages-and-blocks imported-nodes)))
     (is (= (expand-properties (:properties original-data)) (:properties imported-nodes)))
     (is (= (expand-classes (:classes original-data)) (:classes imported-nodes)))))
 
@@ -558,6 +601,7 @@
                                                    :logseq.property/default-value 42})}
           :user.property/default-closed
           {:logseq.property/type :default
+           :db/cardinality :db.cardinality/many
            :build/closed-values [{:value "joy" :uuid closed-value-uuid}
                                  {:value "sad" :uuid (random-uuid)}]}
           :user.property/checkbox {:logseq.property/type :checkbox}
@@ -587,10 +631,25 @@
                                      :user.property/node #{[:block/uuid page-pvalue-uuid]}}}
            :blocks [{:block/title "b1"
                      :build/properties {:user.property/num 1
-                                        :user.property/default-closed [:block/uuid closed-value-uuid]
+                                        :user.property/default-closed #{[:block/uuid closed-value-uuid]}
                                         :user.property/date [:block/uuid journal-uuid]}}
                     {:block/title "b2" :build/properties {:user.property/node #{[:block/uuid page-object-uuid]}}}
-                    {:block/title "b3" :build/properties {:user.property/node #{[:block/uuid page-object-uuid]}}}]}
+                    {:block/title "b3" :build/properties {:user.property/node #{[:block/uuid page-object-uuid]}}}
+                    {:block/title "Example advanced query",
+                     :build/tags [:logseq.class/Query],
+                     :build/properties
+                     {:logseq.property/query
+                      {:build/property-value :block
+                       :block/title "{:query (task Todo)}"
+                       :build/properties
+                       {:logseq.property.code/lang "clojure"
+                        :logseq.property.node/display-type :code}}}}
+                    {:block/title "block has property value with tags and properties"
+                     :build/properties
+                     {:user.property/url
+                      {:build/property-value :block
+                       :block/title "https://example.com"
+                       :build/tags [:user.class/MyClass]}}}]}
           {:page {:block/title "page object"
                   :block/uuid page-object-uuid
                   :build/keep-uuid? true}

+ 2 - 3
deps/graph-parser/nbb.edn

@@ -2,9 +2,8 @@
  :deps
  {logseq/common
   {:local/root "../common"}
-
   logseq/db
   {:local/root "../db"}
-
   io.github.nextjournal/nbb-test-runner
-  {:git/sha "60ed57aa04bca8d604f5ba6b28848bd887109347"}}}
+  {:git/sha "60ed57aa04bca8d604f5ba6b28848bd887109347"}
+  io.github.pez/baldr {:mvn/version "1.0.9"}}}

+ 2 - 1
deps/graph-parser/package.json

@@ -10,6 +10,7 @@
     "mldoc": "^1.5.9"
   },
   "scripts": {
-    "test": "nbb-logseq -cp test:../outliner/src -m nextjournal.test-runner"
+    "test": "nbb-logseq -cp test:../outliner/src -m nextjournal.test-runner",
+    "test-v": "nbb-logseq -cp test:../outliner/src -m logseq.graph-parser.test-runner"
   }
 }

+ 44 - 21
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -918,30 +918,41 @@
       (dissoc :block/whiteboard?)
       (update-page-tags db user-options per-file-state all-idents)))
 
+(defn- get-page-parents
+  "Like ldb/get-page-parents but using all-existing-page-uuids"
+  [node all-existing-page-uuids]
+  (let [get-parent (fn get-parent [n]
+                     (when (:block/uuid (:logseq.property/parent n))
+                       (or (get all-existing-page-uuids (:block/uuid (:logseq.property/parent n)))
+                           (throw (ex-info (str "No parent page found for " (pr-str (:block/uuid (:logseq.property/parent n))))
+                                           {:node n})))))]
+    (when-let [parent (get-parent node)]
+      (loop [current-parent parent
+             parents' []]
+        (if (and current-parent (not (contains? parents' current-parent)))
+          (recur (get-parent current-parent)
+                 (conj parents' current-parent))
+          (vec (reverse parents')))))))
+
 (defn- get-all-existing-page-uuids
   "Returns a map of unique page names mapped to their uuids. The page names
    are in a format that is compatible with extract/extract e.g. namespace pages have
    their full hierarchy in the name"
-  [db classes-from-property-parents]
-  (->> db
-       ;; don't fetch built-in as that would give the wrong entity if a user used
-       ;; a db-only built-in property name e.g. description
-       (d/q '[:find [?b ...]
-              :where [?b :block/name] [(missing? $ ?b :logseq.property/built-in?)]])
-       (map #(d/entity db %))
-       (map #(vector
-              (if-let [parents (and (or (ldb/internal-page? %) (ldb/class? %))
+  [classes-from-property-parents all-existing-page-uuids]
+  (->> all-existing-page-uuids
+       (map (fn [[_ p]]
+              (vector
+               (if-let [parents (and (or (contains? (:block/tags p) :logseq.class/Tag)
+                                         (contains? (:block/tags p) :logseq.class/Page))
                                     ;; These classes have parents now but don't in file graphs (and in extract)
-                                    (not (contains? classes-from-property-parents (:block/title %)))
-                                    (->> (ldb/get-page-parents %)
-                                         (remove (fn [e] (= :logseq.class/Root (:db/ident e))))
-                                         seq))]
+                                     (not (contains? classes-from-property-parents (:block/title p)))
+                                     (get-page-parents p all-existing-page-uuids))]
                 ;; Build a :block/name for namespace pages that matches data from extract/extract
-                (string/join ns-util/namespace-char (map :block/name (conj (vec parents) %)))
-                (:block/name %))
-              (or (:block/uuid %)
-                  (throw (ex-info (str "No uuid for existing page " (pr-str (:block/name %)))
-                                  (select-keys % [:block/name :block/tags]))))))
+                 (string/join ns-util/namespace-char (map :block/name (conj (vec parents) p)))
+                 (:block/name p))
+               (or (:block/uuid p)
+                   (throw (ex-info (str "No uuid for existing page " (pr-str (:block/name p)))
+                                   (select-keys p [:block/name :block/tags])))))))
        (into {})))
 
 (defn- build-existing-page
@@ -1008,8 +1019,9 @@
                                       (not (:block/file %))))
                         ;; remove file path relative
                         (map #(dissoc % :block/file)))
-        ;; Fetch all named ents once per import file to speed up named lookups
-        all-existing-page-uuids (get-all-existing-page-uuids @conn @(:classes-from-property-parents import-state))
+        ;; Build all named ents once per import file to speed up named lookups
+        all-existing-page-uuids (get-all-existing-page-uuids @(:classes-from-property-parents import-state)
+                                                             @(:all-existing-page-uuids import-state))
         all-pages (map #(modify-page-tx % all-existing-page-uuids) all-pages*)
         all-new-page-uuids (->> all-pages
                                 (remove #(all-existing-page-uuids (or (::original-name %) (:block/name %))))
@@ -1119,6 +1131,8 @@
    ;; Map of property names (keyword) and their current schemas (map of qualified properties).
    ;; Used for adding schemas to properties and detecting changes across a property's usage
    :property-schemas (atom {})
+   ;; Indexes all created pages by uuid. Index is used to fetch all parents of a page
+   :all-existing-page-uuids (atom {})
    ;; Map of property or class names (keyword) to db-ident keywords
    :all-idents (atom {})
    ;; Set of children pages turned into classes by :property-parent-classes option
@@ -1312,6 +1326,12 @@
                  classes-tx)
            retract-page-tag-from-existing-pages)}))
 
+(defn- save-from-tx
+  "Save importer state from given txs"
+  [txs {:keys [import-state]}]
+  (when-let [nodes (seq (filter :block/name txs))]
+    (swap! (:all-existing-page-uuids import-state) merge (into {} (map (juxt :block/uuid identity) nodes)))))
+
 (defn add-file-to-db-graph
   "Parse file and save parsed data to the given db graph. Options available:
 
@@ -1351,6 +1371,7 @@
         ;; _ (when (seq property-pages-tx) (cljs.pprint/pprint {:property-pages-tx property-pages-tx}))
         ;; Necessary to transact new property entities first so that block+page properties can be transacted next
         main-props-tx-report (d/transact! conn property-pages-tx {::new-graph? true ::path file})
+        _ (save-from-tx property-pages-tx options)
 
         classes-tx @(:classes-tx tx-options)
         {:keys [retract-page-tags-tx] pages-tx'' :pages-tx} (clean-extra-invalid-tags @conn pages-tx' classes-tx existing-pages)
@@ -1376,11 +1397,13 @@
         ;;                        [whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx' classes-tx blocks-index blocks-tx]))
         ;; _ (when (not (seq whiteboard-pages)) (cljs.pprint/pprint {#_:property-pages-tx #_property-pages-tx :pages-tx pages-tx :tx tx'}))
         main-tx-report (d/transact! conn tx' {::new-graph? true ::path file})
+        _ (save-from-tx tx' options)
 
         upstream-properties-tx
         (build-upstream-properties-tx @conn @(:upstream-properties tx-options) (:import-state options) log-fn)
         ;; _ (when (seq upstream-properties-tx) (cljs.pprint/pprint {:upstream-properties-tx upstream-properties-tx}))
-        upstream-tx-report (when (seq upstream-properties-tx) (d/transact! conn upstream-properties-tx {::new-graph? true ::path file}))]
+        upstream-tx-report (when (seq upstream-properties-tx) (d/transact! conn upstream-properties-tx {::new-graph? true ::path file}))
+        _ (save-from-tx upstream-properties-tx options)]
 
     ;; Return all tx-reports that occurred in this fn as UI needs to know what changed
     [main-props-tx-report main-tx-report upstream-tx-report]))

+ 10 - 3
deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs

@@ -10,6 +10,7 @@
             [logseq.common.util.date-time :as date-time-util]
             [logseq.db :as ldb]
             [logseq.db.frontend.content :as db-content]
+            [logseq.db.frontend.entity-plus :as entity-plus]
             [logseq.db.frontend.malli-schema :as db-malli-schema]
             [logseq.db.frontend.rules :as rules]
             [logseq.db.frontend.validate :as db-validate]
@@ -19,8 +20,7 @@
             [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper]
             [logseq.graph-parser.test.helper :as test-helper :include-macros true :refer [deftest-async]]
             [logseq.outliner.db-pipeline :as db-pipeline]
-            [promesa.core :as p]
-            [logseq.db.frontend.entity-plus :as entity-plus]))
+            [promesa.core :as p]))
 
 ;; Helpers
 ;; =======
@@ -130,11 +130,18 @@
 
 (deftest-async ^:integration export-docs-graph-with-convert-all-tags
   (p/let [file-graph-dir "test/resources/docs-0.10.9"
+          start-time (cljs.core/system-time)
           _ (docs-graph-helper/clone-docs-repo-if-not-exists file-graph-dir "v0.10.9")
           conn (db-test/create-conn)
           _ (db-pipeline/add-listener conn)
           {:keys [import-state]}
-          (import-file-graph-to-db file-graph-dir conn {:convert-all-tags? true})]
+          (import-file-graph-to-db file-graph-dir conn {:convert-all-tags? true})
+          end-time (cljs.core/system-time)]
+
+    ;; Add multiplicative factor for CI as it runs about twice as slow
+    (let [max-time (-> 15 (* (if js/process.env.CI 2 1)))]
+      (is (< (-> end-time (- start-time) (/ 1000)) max-time)
+          (str "Importing large graph takes less than " max-time "s")))
 
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
         "Created graph has no validation errors")

+ 6 - 0
deps/graph-parser/test/logseq/graph_parser/test_runner.cljs

@@ -0,0 +1,6 @@
+(ns logseq.graph-parser.test-runner
+  "Test runner which enables https://github.com/PEZ/baldr by default"
+  (:require [nextjournal.test-runner :as next-runner]
+            [pez.baldr]))
+
+(def -main next-runner/-main)

+ 3 - 1
deps/shui/deps.edn

@@ -3,6 +3,8 @@
  {org.clojure/clojure                   {:mvn/version "1.11.1"}
   org.clojure/clojurescript             {:mvn/version "1.11.132"}
   funcool/promesa                       {:mvn/version "11.0.678"}
-  rum/rum                               {:mvn/version "0.12.9"}
+  rum/rum                               {:git/url "https://github.com/logseq/rum" ;; fork
+                                         :sha     "5d672bf84ed944414b9f61eeb83808ead7be9127"}
+
   medley/medley                         {:mvn/version "1.4.0"}
   cljs-bean/cljs-bean                   {:mvn/version "1.5.0"}}}

+ 6 - 5
deps/shui/src/logseq/shui/demo.cljs

@@ -1,10 +1,11 @@
 (ns logseq.shui.demo
-  (:require [rum.core :as rum]
-            [logseq.shui.ui :as ui]
-            [dommy.core :refer-macros [sel1]]
+  (:require [dommy.core :refer-macros [sel1]]
+            [logseq.shui.dialog.core :as dialog-core]
             [logseq.shui.form.core :refer [yup yup-resolver] :as form-core]
+            [logseq.shui.hooks :as hooks]
+            [logseq.shui.ui :as ui]
             [promesa.core :as p]
-            [logseq.shui.dialog.core :as dialog-core]))
+            [rum.core :as rum]))
 
 (rum/defc section-item
   [title children]
@@ -511,7 +512,7 @@
   []
 
   (let [el-ref (rum/use-ref nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [^js container (get-main-scroll-container)
              ^js el (rum/deref el-ref)

+ 235 - 236
deps/shui/src/logseq/shui/demo2.cljs

@@ -1,23 +1,24 @@
 (ns logseq.shui.demo2
-  (:require [clojure.string :as string]
-            [rum.core :as rum]
-            [logseq.shui.ui :as ui]
-            [logseq.shui.popup.core :refer [install-popups update-popup! get-popup]]
-            [logseq.shui.select.multi :refer [x-select-content]]
+  (:require [cljs-bean.core :as bean]
+            [clojure.string :as string]
             [frontend.components.icon :refer [emojis-cp emojis icon-search]]
             [frontend.storage :as storage]
-            [cljs-bean.core :as bean]
-            [promesa.core :as p]))
+            [logseq.shui.hooks :as hooks]
+            [logseq.shui.popup.core :refer [install-popups update-popup! get-popup]]
+            [logseq.shui.select.multi :refer [x-select-content]]
+            [logseq.shui.ui :as ui]
+            [promesa.core :as p]
+            [rum.core :as rum]))
 
 (defn do-fetch!
   ([action] (do-fetch! action nil))
   ([action query-str]
    (-> (js/window.fetch
-         (str "https://movies-api14.p.rapidapi.com/" (name action) (when query-str (str "?" query-str)))
-         #js {:method "GET"
-              :headers #js {:X-RapidAPI-Key "808ffd08c0mshc67d496f6024b46p164350jsn7b35179966c9",
-                            :X-RapidAPI-Host "movies-api14.p.rapidapi.com"}})
-     (p/then #(.json %)))))
+        (str "https://movies-api14.p.rapidapi.com/" (name action) (when query-str (str "?" query-str)))
+        #js {:method "GET"
+             :headers #js {:X-RapidAPI-Key "808ffd08c0mshc67d496f6024b46p164350jsn7b35179966c9",
+                           :X-RapidAPI-Host "movies-api14.p.rapidapi.com"}})
+       (p/then #(.json %)))))
 
 (rum/defc multi-select-demo
   []
@@ -35,96 +36,96 @@
 
          rm-item! (fn [item-or-id]
                     (set-selected-items!
-                      (remove #(or (= item-or-id %)
-                                 (= item-or-id (str (:id %))))
-                        selected-items)))
+                     (remove #(or (= item-or-id %)
+                                  (= item-or-id (str (:id %))))
+                             selected-items)))
          add-item! (fn [item] (set-selected-items! (conj selected-items item)))
 
          [open? set-open!] (rum/use-state false)]
 
-     (rum/use-effect!
-       (fn []
-         (storage/set :ls-demo-multi-selected-items selected-items))
-       [selected-items])
+     (hooks/use-effect!
+      (fn []
+        (storage/set :ls-demo-multi-selected-items selected-items))
+      [selected-items])
 
      (ui/card
-       (ui/card-header
-         (ui/card-title "Search Movies")
-         (ui/card-description "x multiselect for the remote items"))
-       (ui/card-content
+      (ui/card-header
+       (ui/card-title "Search Movies")
+       (ui/card-description "x multiselect for the remote items"))
+      (ui/card-content
 
          ;; Basic
-         (ui/dropdown-menu
-           {:open open?}
+       (ui/dropdown-menu
+        {:open open?}
            ;; trigger
-           (ui/dropdown-menu-trigger
-             [:div.border.p-2.rounded.w-full.cursor-pointer.flex.items-center.gap-1.flex-wrap
-              {:on-click (fn [^js e]
-                           (let [^js target (.-target e)]
-                             (if-let [^js c (some-> target (.closest ".close"))]
-                               (some-> (.-dataset c) (.-k) (rm-item!))
-                               (set-open! true))))}
-              (for [{:keys [id original_title class poster_path]} selected-items]
-                (ui/badge {:variant :secondary :class (str class " group relative")}
-                  [:span.flex.items-center.gap-1.flex-nowrap
-                   [:img {:src poster_path :class "w-[16px] scale-75"}]
-                   [:b original_title]]
-                  (ui/button
-                    {:variant :destructive
-                     :size :icon
-                     :data-k id
-                     :class "!rounded-full !h-4 !w-4 absolute top-[-7px] right-[-3px] group-hover:visible invisible close"}
-                    (ui/tabler-icon "x" {:size 12}))))
-              (ui/button {:variant :link :size :sm} "+")])
+        (ui/dropdown-menu-trigger
+         [:div.border.p-2.rounded.w-full.cursor-pointer.flex.items-center.gap-1.flex-wrap
+          {:on-click (fn [^js e]
+                       (let [^js target (.-target e)]
+                         (if-let [^js c (some-> target (.closest ".close"))]
+                           (some-> (.-dataset c) (.-k) (rm-item!))
+                           (set-open! true))))}
+          (for [{:keys [id original_title class poster_path]} selected-items]
+            (ui/badge {:variant :secondary :class (str class " group relative")}
+                      [:span.flex.items-center.gap-1.flex-nowrap
+                       [:img {:src poster_path :class "w-[16px] scale-75"}]
+                       [:b original_title]]
+                      (ui/button
+                       {:variant :destructive
+                        :size :icon
+                        :data-k id
+                        :class "!rounded-full !h-4 !w-4 absolute top-[-7px] right-[-3px] group-hover:visible invisible close"}
+                       (ui/tabler-icon "x" {:size 12}))))
+          (ui/button {:variant :link :size :sm} "+")])
            ;; content
-           (x-select-content items selected-items
-             {;; test item render
-              :open? open?
-              :close! #(set-open! false)
-              :search-enabled? true
-              :search-key q
-              :search-fn (fn [items]
-                           (when (not fetching?) items))
-              :on-search-key-change (fn [v]
-                                      (set-q! v)
-                                      (if (string/blank? v)
-                                        (set-items! [])
-                                        (when (not fetching?)
-                                          (set-fetching? true)
-                                          (-> (do-fetch! :search (str "query=" v))
-                                            (p/then #(when-let [ret (bean/->clj %)]
-                                                       (when-let [items (:contents ret)]
-                                                         (set-items! (map (fn [item] (assoc item :id (:_id item))) (take 12 items))))))
-                                            (p/finally #(set-fetching? false))))))
-
-              :item-render (fn [item {:keys [selected?]}]
-                             (if item
-                               (ui/dropdown-menu-checkbox-item
-                                 {:checked selected?
-                                  :on-click (fn []
-                                              (if selected?
-                                                (rm-item! item)
-                                                (add-item! item))
+        (x-select-content items selected-items
+                          {;; test item render
+                           :open? open?
+                           :close! #(set-open! false)
+                           :search-enabled? true
+                           :search-key q
+                           :search-fn (fn [items]
+                                        (when (not fetching?) items))
+                           :on-search-key-change (fn [v]
+                                                   (set-q! v)
+                                                   (if (string/blank? v)
+                                                     (set-items! [])
+                                                     (when (not fetching?)
+                                                       (set-fetching? true)
+                                                       (-> (do-fetch! :search (str "query=" v))
+                                                           (p/then #(when-let [ret (bean/->clj %)]
+                                                                      (when-let [items (:contents ret)]
+                                                                        (set-items! (map (fn [item] (assoc item :id (:_id item))) (take 12 items))))))
+                                                           (p/finally #(set-fetching? false))))))
+
+                           :item-render (fn [item {:keys [selected?]}]
+                                          (if item
+                                            (ui/dropdown-menu-checkbox-item
+                                             {:checked selected?
+                                              :on-click (fn []
+                                                          (if selected?
+                                                            (rm-item! item)
+                                                            (add-item! item))
                                               ;(set-open! false)
-                                              )}
-                                 [:div.flex.items-center.gap-2
-                                  [:span [:img {:src (:poster_path item)
-                                                :class "w-[20px]"}]]
-                                  [:span.flex.flex-col
-                                   [:b (:original_title item)]
-                                   [:small.opacity-50
-                                    {:class "text-[10px]"}
-                                    (:release_date item)]]])
-                               (ui/dropdown-menu-separator)))
-
-              :head-render (fn [] (when (and fetching? (not (string/blank? q)))
-                                    [:b.flex.items-center.justify-center.py-4
-                                     (ui/tabler-icon "loader" {:class "animate-spin"})]))
+                                                          )}
+                                             [:div.flex.items-center.gap-2
+                                              [:span [:img {:src (:poster_path item)
+                                                            :class "w-[20px]"}]]
+                                              [:span.flex.flex-col
+                                               [:b (:original_title item)]
+                                               [:small.opacity-50
+                                                {:class "text-[10px]"}
+                                                (:release_date item)]]])
+                                            (ui/dropdown-menu-separator)))
+
+                           :head-render (fn [] (when (and fetching? (not (string/blank? q)))
+                                                 [:b.flex.items-center.justify-center.py-4
+                                                  (ui/tabler-icon "loader" {:class "animate-spin"})]))
               ;:foot-render (fn [] [:b "footer"])
 
-              :content-props
-              {:align "start"
-               :class "w-80"}})))))
+                           :content-props
+                           {:align "start"
+                            :class "w-80"}})))))
 
    [:hr]
 
@@ -146,50 +147,50 @@
          [open? set-open!] (rum/use-state false)]
 
      (ui/card
-       (ui/card-header
-         (ui/card-title "Basic")
-         (ui/card-description "x multiselect for shui"))
-       (ui/card-content
-         [:label.block.flex.items-center.pb-3.cursor-pointer
-          (ui/checkbox {:checked search?
-                        :on-click #(set-search? (not search?))})
-          [:small.pl-2 "Enable basic search input"]]
+      (ui/card-header
+       (ui/card-title "Basic")
+       (ui/card-description "x multiselect for shui"))
+      (ui/card-content
+       [:label.block.flex.items-center.pb-3.cursor-pointer
+        (ui/checkbox {:checked search?
+                      :on-click #(set-search? (not search?))})
+        [:small.pl-2 "Enable basic search input"]]
          ;; Basic
-         (ui/dropdown-menu
-           {:open open?}
+       (ui/dropdown-menu
+        {:open open?}
            ;; trigger
-           (ui/dropdown-menu-trigger
-             [:p.border.p-2.rounded.w-full.cursor-pointer
-              {:on-click #(set-open! true)}
-              (for [{:keys [key value class]} selected-items]
-                (ui/badge {:variant :secondary :class class} (str "#" key " " value)))
-              (ui/button {:variant :link :size :sm} "+")])
+        (ui/dropdown-menu-trigger
+         [:p.border.p-2.rounded.w-full.cursor-pointer
+          {:on-click #(set-open! true)}
+          (for [{:keys [key value class]} selected-items]
+            (ui/badge {:variant :secondary :class class} (str "#" key " " value)))
+          (ui/button {:variant :link :size :sm} "+")])
            ;; content
-           (x-select-content items selected-items
-             {:close! #(set-open! false)
-              :search-enabled? search?
-              :search-key-render (fn [q {:keys [items]}]
-                                   (when (and (not (string/blank? q))
-                                           (not (seq items)))
-                                     [:b.flex.items-center.justify-center.py-4.gap-2.font-normal.opacity-80
-                                      (ui/tabler-icon "lemon") [:small "No fruits!"]]))
-              :on-chosen on-chosen
-              :value-render (fn [v {:keys [selected?]}]
-                              (if selected?
-                                [:b.text-red-800 v]
-                                [:b.text-green-800 v]))
-              :content-props
-              {:class "w-48"}})))))
+        (x-select-content items selected-items
+                          {:close! #(set-open! false)
+                           :search-enabled? search?
+                           :search-key-render (fn [q {:keys [items]}]
+                                                (when (and (not (string/blank? q))
+                                                           (not (seq items)))
+                                                  [:b.flex.items-center.justify-center.py-4.gap-2.font-normal.opacity-80
+                                                   (ui/tabler-icon "lemon") [:small "No fruits!"]]))
+                           :on-chosen on-chosen
+                           :value-render (fn [v {:keys [selected?]}]
+                                           (if selected?
+                                             [:b.text-red-800 v]
+                                             [:b.text-green-800 v]))
+                           :content-props
+                           {:class "w-48"}})))))
 
    [:hr]
 
    (let [[items set-items!]
          (rum/use-state
-           [{:key 1 :value "Apple" :class "bg-gray-800 text-gray-50"}
-            {:key 2 :value "Orange" :class "bg-orange-700 text-gray-50"}
-            nil
-            {:key 3 :value "Pear"}
-            {:key 4 :value "Banana" :class "bg-yellow-700 text-gray-700"}])
+          [{:key 1 :value "Apple" :class "bg-gray-800 text-gray-50"}
+           {:key 2 :value "Orange" :class "bg-orange-700 text-gray-50"}
+           nil
+           {:key 3 :value "Pear"}
+           {:key 4 :value "Banana" :class "bg-yellow-700 text-gray-700"}])
 
          [selected-items set-selected-items!]
          (rum/use-state [(last items) (first items)])
@@ -202,55 +203,54 @@
          [open? set-open!] (rum/use-state false)]
 
      (ui/card
-       (ui/card-header
-         (ui/card-title "Search & Custom")
-         (ui/card-description "x multiselect for shui"))
-       (ui/card-content
+      (ui/card-header
+       (ui/card-title "Search & Custom")
+       (ui/card-description "x multiselect for shui"))
+      (ui/card-content
 
          ;; Basic
-         (ui/dropdown-menu
-           {:open open?}
+       (ui/dropdown-menu
+        {:open open?}
            ;; trigger
-           (ui/dropdown-menu-trigger
-             [:p.border.p-2.rounded.w-full.cursor-pointer
-              {:on-click #(set-open! true)}
-              (for [{:keys [key value class]} selected-items]
-                (ui/badge {:variant :secondary :class class} (str "#" key " " value)))
-              (ui/button {:variant :link :size :sm} "+")])
+        (ui/dropdown-menu-trigger
+         [:p.border.p-2.rounded.w-full.cursor-pointer
+          {:on-click #(set-open! true)}
+          (for [{:keys [key value class]} selected-items]
+            (ui/badge {:variant :secondary :class class} (str "#" key " " value)))
+          (ui/button {:variant :link :size :sm} "+")])
            ;; content
-           (x-select-content items selected-items
-             {;; test item render
-              :open? open?
-              :close! #(set-open! false)
-              :search-enabled? true
-              :item-render (fn [item {:keys [selected?]}]
-                             (if item
-                               (ui/dropdown-menu-checkbox-item
-                                 {:checked selected?
-                                  :on-click (fn []
-                                              (if selected?
-                                                (rm-item! item)
-                                                (add-item! item)))}
-                                 (:value item))
-                               (ui/dropdown-menu-separator)))
-
-              :search-key-render
-              (fn [k {:keys [items x-item exist-fn]}]
-                (when (and
-                        (not (string/blank? k))
-                        (not (exist-fn)))
-                  (x-item
-                    {:on-click (fn []
-                                 (ui/toast! (str "Create: " k) :warning)
-                                 (set-open! false))}
-                    (str "+ create: " k))))
+        (x-select-content items selected-items
+                          {;; test item render
+                           :open? open?
+                           :close! #(set-open! false)
+                           :search-enabled? true
+                           :item-render (fn [item {:keys [selected?]}]
+                                          (if item
+                                            (ui/dropdown-menu-checkbox-item
+                                             {:checked selected?
+                                              :on-click (fn []
+                                                          (if selected?
+                                                            (rm-item! item)
+                                                            (add-item! item)))}
+                                             (:value item))
+                                            (ui/dropdown-menu-separator)))
+
+                           :search-key-render
+                           (fn [k {:keys [items x-item exist-fn]}]
+                             (when (and
+                                    (not (string/blank? k))
+                                    (not (exist-fn)))
+                               (x-item
+                                {:on-click (fn []
+                                             (ui/toast! (str "Create: " k) :warning)
+                                             (set-open! false))}
+                                (str "+ create: " k))))
 
               ;:head-render (fn [] [:b "header"])
               ;:foot-render (fn [] [:b "footer"])
-              :content-props
-              {:align "start"
-               :class "w-48"}})))))
-   ])
+                           :content-props
+                           {:align "start"
+                            :class "w-48"}})))))])
 
 (rum/defc icon-picker-demo
   []
@@ -282,27 +282,27 @@
             [:a.underline
              {:on-click
               #(ui/popup-show! %
-                 (fn [_config]
-                   [:div.max-h-72.overflow-auto.p-1
-                    (emojis-cp (take 80 emojis)
-                      {:on-chosen
-                       (fn [_ t]
-                         (set-emoji! t)
-                         (ui/popup-hide-all!))})])
-                 {:content-props {:class "w-72 p-0"}
-                  :as-dropdown? true})}
+                               (fn [_config]
+                                 [:div.max-h-72.overflow-auto.p-1
+                                  (emojis-cp (take 80 emojis)
+                                             {:on-chosen
+                                              (fn [_ t]
+                                                (set-emoji! t)
+                                                (ui/popup-hide-all!))})])
+                               {:content-props {:class "w-72 p-0"}
+                                :as-dropdown? true})}
              (if emoji [:strong.px-1.text-6xl [:em-emoji emoji]] "emoji :O")] "."])]
      [:<>
       (emoji-picker nil)
 
       [:p.py-4
        (ui/button
-         {:variant :secondary
-          :on-click #(ui/popup-show! %
-                       (fn []
-                         [:p.p-4
-                          (emoji-picker true)]))}
-         "Play a nested x popup.")]
+        {:variant :secondary
+         :on-click #(ui/popup-show! %
+                                    (fn []
+                                      [:p.p-4
+                                       (emoji-picker true)]))}
+        "Play a nested x popup.")]
 
       [:p.py-4
        (let [gen-content
@@ -312,60 +312,60 @@
                 (emoji-picker true)
                 [:strong.px-1.text-6xl q]])]
          (ui/input
-           {:placeholder "Select a fruit."
-            :ref *q-ref
-            :value q
-            :on-change (fn [^js e]
-                         (let [val (.-value (.-target e))]
-                           (set-q! val)
-                           (update-popup! :select-a-fruit-input [:content] (gen-content val))))
-            :class "w-1/5"
-            :on-focus (fn [^js e]
-                        (let [id :select-a-fruit-input
-                              [_ popup] (get-popup id)]
-                          (if (not popup)
-                            (ui/popup-show! (.-target e)
-                              (gen-content q)
-                              {:id id
-                               :align "start"
-                               :content-props
-                               {:class "x-input-popup-content"
-                                :onPointerDownOutside
-                                (fn [^js e]
-                                  (js/console.log "===>> onPointerDownOutside:" e (rum/deref *q-ref))
-                                  (when-let [q-ref (rum/deref *q-ref)]
-                                    (let [^js target (or (.-relatedTarget e)
-                                                       (.-target e))]
-                                      (js/console.log "t:" target)
-                                      (when (and
-                                              (not (.contains q-ref target))
-                                              (not (.closest target ".x-input-popup-content")))
-                                        (ui/popup-hide! id)))))
-                                :onOpenAutoFocus #(.preventDefault %)}})
+          {:placeholder "Select a fruit."
+           :ref *q-ref
+           :value q
+           :on-change (fn [^js e]
+                        (let [val (.-value (.-target e))]
+                          (set-q! val)
+                          (update-popup! :select-a-fruit-input [:content] (gen-content val))))
+           :class "w-1/5"
+           :on-focus (fn [^js e]
+                       (let [id :select-a-fruit-input
+                             [_ popup] (get-popup id)]
+                         (if (not popup)
+                           (ui/popup-show! (.-target e)
+                                           (gen-content q)
+                                           {:id id
+                                            :align "start"
+                                            :content-props
+                                            {:class "x-input-popup-content"
+                                             :onPointerDownOutside
+                                             (fn [^js e]
+                                               (js/console.log "===>> onPointerDownOutside:" e (rum/deref *q-ref))
+                                               (when-let [q-ref (rum/deref *q-ref)]
+                                                 (let [^js target (or (.-relatedTarget e)
+                                                                      (.-target e))]
+                                                   (js/console.log "t:" target)
+                                                   (when (and
+                                                          (not (.contains q-ref target))
+                                                          (not (.closest target ".x-input-popup-content")))
+                                                     (ui/popup-hide! id)))))
+                                             :onOpenAutoFocus #(.preventDefault %)}})
 
                             ;; update content
-                            (update-popup! id [:content]
-                              (gen-content q)))))
+                           (update-popup! id [:content]
+                                          (gen-content q)))))
             ;:on-blur     (fn [^js e]
             ;               (let [^js target (.-relatedTarget e)]
             ;                 (js/console.log "==>>>" target)
             ;                 (when-not (.closest target ".x-input-popup-content")
             ;                   (hide-x-popup! :select-a-fruit-input))))
-            }))]
+           }))]
 
       [:div.w-full.p-4.border.rounded.dotted.h-48.mt-8.bg-gray-02
        {:on-click #(ui/popup-show! %
-                     (->> (range 8)
-                       (map (fn [it]
-                              (ui/dropdown-menu-item
-                                {:on-select (fn []
-                                              (ui/toast! it)
-                                              (ui/popup-hide-all!))}
-                                [:strong it]))))
-                     {:as-dropdown? true
-                      :content-props {:class "w-48"}})
+                                   (->> (range 8)
+                                        (map (fn [it]
+                                               (ui/dropdown-menu-item
+                                                {:on-select (fn []
+                                                              (ui/toast! it)
+                                                              (ui/popup-hide-all!))}
+                                                [:strong it]))))
+                                   {:as-dropdown? true
+                                    :content-props {:class "w-48"}})
         :on-context-menu #(ui/popup-show! %
-                            [:h1.text-3xl.font-bold "hi x popup for custom context menu!"])}]])])
+                                          [:h1.text-3xl.font-bold "hi x popup for custom context menu!"])}]])])
 
 (rum/defc custom-trigger-content
   []
@@ -381,17 +381,16 @@
    [:h1.text-3xl.font-bold.border-b.pb-4 "Sample dropdown/menu trigger"]
    [:div.py-4
     (ui/dropdown-menu
-      (ui/dropdown-menu-trigger
-        {:as-child true}
-        (ui/trigger-child-wrap
-          {:class "border p-6 border"}
-          (custom-trigger-content)))
-      (ui/dropdown-menu-content
-        (ui/dropdown-menu-item "A item")
-        (ui/dropdown-menu-item "B item")
-        (ui/dropdown-menu-item "C item")))]
-   ])
+     (ui/dropdown-menu-trigger
+      {:as-child true}
+      (ui/trigger-child-wrap
+       {:class "border p-6 border"}
+       (custom-trigger-content)))
+     (ui/dropdown-menu-content
+      (ui/dropdown-menu-item "A item")
+      (ui/dropdown-menu-item "B item")
+      (ui/dropdown-menu-item "C item")))]])
 
 (rum/defc page
   []
-  (sample-dropdown-trigger))
+  (sample-dropdown-trigger))

+ 12 - 8
deps/shui/src/logseq/shui/dialog/core.cljs

@@ -2,6 +2,7 @@
   (:require [daiquiri.interpreter :refer [interpret]]
             [logseq.shui.base.core :as base]
             [logseq.shui.form.core :as form]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.util :as util]
             [medley.core :as medley]
             [promesa.core :as p]
@@ -130,10 +131,11 @@
                       :close :align :on-open-change :open? :root-props :content-props)
         props (assoc-in props [:overlay-props :data-align] (name (or align :center)))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when (false? open?)
-         (js/setTimeout #(detach-modal! id) 128)))
+         (let [timeout (js/setTimeout #(detach-modal! id) 128)]
+           #(js/clearTimeout timeout))))
      [open?])
 
     (dialog
@@ -171,10 +173,11 @@
   (let [{:keys [id title description content footer deferred open?]} config
         props (dissoc config :id :title :description :content :footer :deferred :open? :alert?)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when (false? open?)
-         (js/setTimeout #(detach-modal! id) 128)))
+         (let [timeout (js/setTimeout #(detach-modal! id) 128)]
+           #(js/clearTimeout timeout))))
      [open?])
 
     (alert-dialog
@@ -209,14 +212,15 @@
         *ok-ref (rum/use-ref nil)
         *reminder-ref (rum/use-ref nil)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when ready?
-         (js/setTimeout
-          #(some-> (rum/deref *ok-ref) (.focus)) 128)))
+         (let [timeout (js/setTimeout
+                        #(some-> (rum/deref *ok-ref) (.focus)) 128)]
+           #(js/clearTimeout timeout))))
      [ready?])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (try
          (if-let [reminder-v (and reminder? (js/localStorage.getItem (str id)))]

+ 13 - 7
src/main/frontend/hooks.cljs → deps/shui/src/logseq/shui/hooks.cljs

@@ -1,4 +1,4 @@
-(ns frontend.hooks
+(ns logseq.shui.hooks
   "React custom hooks."
   (:refer-clojure :exclude [ref deref])
   (:require [frontend.common.missionary :as c.m]
@@ -26,17 +26,23 @@
   "setup-fn will be invoked every render of component when no deps arg provided"
   ([setup-fn] (rum/use-effect! setup-fn))
   ([setup-fn deps & {:keys [equal-fn]}]
-   (rum/use-effect! setup-fn (if (empty? deps)
-                               deps
-                               #js[(memo-deps equal-fn deps)]))))
+   (rum/use-effect! (fn [& deps]
+                      (let [result (apply setup-fn deps)]
+                        (when (fn? result) result)))
+                    (if (empty? deps)
+                      deps
+                      #js[(memo-deps equal-fn deps)]))))
 
 #_{:clj-kondo/ignore [:discouraged-var]}
 (defn use-layout-effect!
   ([setup-fn] (rum/use-layout-effect! setup-fn))
   ([setup-fn deps & {:keys [equal-fn]}]
-   (rum/use-layout-effect! setup-fn (if (empty? deps)
-                                      deps
-                                      #js[(memo-deps equal-fn deps)]))))
+   (rum/use-layout-effect! (fn [& deps]
+                             (let [result (apply setup-fn deps)]
+                               (when (fn? result) result)))
+                           (if (empty? deps)
+                             deps
+                             #js[(memo-deps equal-fn deps)]))))
 
 #_{:clj-kondo/ignore [:discouraged-var]}
 (defn use-callback

+ 114 - 113
deps/shui/src/logseq/shui/select/multi.cljs

@@ -1,14 +1,15 @@
 (ns logseq.shui.select.multi
   (:require [clojure.string :as string]
-            [rum.core :as rum]
+            [logseq.shui.form.core :as form]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.popup.core :as popup]
-            [logseq.shui.form.core :as form]))
+            [rum.core :as rum]))
 
 (defn- get-k [item]
   (if (map? item)
     (some->> ((juxt :id :key :label) item)
-      (remove nil?)
-      (first))
+             (remove nil?)
+             (first))
     item))
 
 (defn- get-v
@@ -21,27 +22,27 @@
   (let [*el (rum/use-ref nil)
         [down set-down!] (rum/use-state 0)]
 
-    (rum/use-effect!
-      (fn []
-        (when-let [^js item (and (> down 0)
-                              (some-> (rum/deref *el)
-                                (.closest ".head")
-                                (.-nextSibling)))]
-          (some-> (if valid-search-key? (.-nextSibling item) item)
-            (.focus))))
-      [down])
+    (hooks/use-effect!
+     (fn []
+       (when-let [^js item (and (> down 0)
+                                (some-> (rum/deref *el)
+                                        (.closest ".head")
+                                        (.-nextSibling)))]
+         (some-> (if valid-search-key? (.-nextSibling item) item)
+                 (.focus))))
+     [down])
 
     [:div.search-input
      {:ref *el}
      (form/input
-       (merge {:placeholder "search"
-               :on-key-up #(case (.-key %)
-                             "ArrowDown" (set-down! (inc down))
-                             "ArrowUp" nil
-                             "Enter" (when (fn? on-enter) (on-enter))
-                             :dune)
-               :auto-focus true}
-         input-props))]))
+      (merge {:placeholder "search"
+              :on-key-up #(case (.-key %)
+                            "ArrowDown" (set-down! (inc down))
+                            "ArrowUp" nil
+                            "Enter" (when (fn? on-enter) (on-enter))
+                            :dune)
+              :auto-focus true}
+             input-props))]))
 
 (defn- simple-search-fn
   [items q]
@@ -49,8 +50,8 @@
     (if (string/blank? q)
       items
       (filter #(some-> (get-v %)
-                 (string/lower-case)
-                 (string/includes? q)) items))))
+                       (string/lower-case)
+                       (string/includes? q)) items))))
 
 (rum/defc x-select-content
   [items selected-items & {:keys [on-chosen item-render value-render
@@ -70,8 +71,8 @@
                               (when (and search-enabled? (not= "INPUT" (.-nodeName target)))
                                 ;; focus search input
                                 (some-> (get-content-el target)
-                                  (.querySelector "input")
-                                  (.focus))))
+                                        (.querySelector "input")
+                                        (.focus))))
         items (if search-enabled?
                 (if (fn? search-fn)
                   (search-fn items search-key1)
@@ -79,100 +80,100 @@
                 items)
         close1! #(when (fn? close!) (close!))]
 
-    (rum/use-effect!
-      (fn []
-        (when (fn? on-search-key-change)
-          (on-search-key-change search-key1')))
-      [search-key1'])
+    (hooks/use-effect!
+     (fn []
+       (when (fn? on-search-key-change)
+         (on-search-key-change search-key1')))
+     [search-key1'])
 
-    (rum/use-effect!
-      (fn []
-        (when-let [t (when (and search-enabled? (false? open?))
-                       (js/setTimeout #(set-search-key! "") 500))]
-          #(js/clearTimeout t)))
-      [open?])
+    (hooks/use-effect!
+     (fn []
+       (when-let [t (when (and search-enabled? (false? open?))
+                      (js/setTimeout #(set-search-key! "") 500))]
+         #(js/clearTimeout t)))
+     [open?])
 
     (x-content
-      (merge
-        {:onInteractOutside close1!
-         :onEscapeKeyDown close1!
-         :on-key-down (fn [^js e]
-                        (when-let [^js target (.-target e)]
-                          (case (.-key e)
-                            "ArrowUp"
-                            (when (= (some-> (get-item-nodes target) (first))
-                                    js/document.activeElement)
-                              (focus-search-input! target))
-                            "l" (when (or (.-metaKey e) (.-ctrlKey e))
-                                  (focus-search-input! target))
-                            :dune)))
-         :class (str (:class content-props)
-                  " ui__multi-select-content"
-                  (when valid-search-key? " has-search-key"))}
-        (dissoc content-props :class))
+     (merge
+      {:onInteractOutside close1!
+       :onEscapeKeyDown close1!
+       :on-key-down (fn [^js e]
+                      (when-let [^js target (.-target e)]
+                        (case (.-key e)
+                          "ArrowUp"
+                          (when (= (some-> (get-item-nodes target) (first))
+                                   js/document.activeElement)
+                            (focus-search-input! target))
+                          "l" (when (or (.-metaKey e) (.-ctrlKey e))
+                                (focus-search-input! target))
+                          :dune)))
+       :class (str (:class content-props)
+                   " ui__multi-select-content"
+                   (when valid-search-key? " has-search-key"))}
+      (dissoc content-props :class))
       ;; header
-      (when (or search-enabled? (fn? head-render))
-        [:div.head
-         {:ref *head-ref}
-         (when search-enabled?
-           (search-input
-             {:value search-key1
-              :on-key-down (fn [^js e]
-                             (.stopPropagation e)
-                             (case (.-key e)
-                               "Escape" (if (string/blank? search-key1)
-                                          (some-> (.-target e) (.closest "[data-radix-menu-content]") (.focus))
-                                          (set-search-key! ""))
-                               :dune))
-              :on-change #(set-search-key! (.-value (.-target %)))}
+     (when (or search-enabled? (fn? head-render))
+       [:div.head
+        {:ref *head-ref}
+        (when search-enabled?
+          (search-input
+           {:value search-key1
+            :on-key-down (fn [^js e]
+                           (.stopPropagation e)
+                           (case (.-key e)
+                             "Escape" (if (string/blank? search-key1)
+                                        (some-> (.-target e) (.closest "[data-radix-menu-content]") (.focus))
+                                        (set-search-key! ""))
+                             :dune))
+            :on-change #(set-search-key! (.-value (.-target %)))}
 
-             {:on-enter (fn []
-                          (when-let [head-el (and (not (string/blank? search-key1'))
-                                               (rum/deref *head-ref))]
-                            (when-let [^js item (.-nextSibling head-el)]
-                              (when (contains? #{"menuitemcheckbox" "menuitem"}
-                                      (.getAttribute item "role"))
-                                (.click item)))))
-              :valid-search-key? valid-search-key?}))
-         (when head-render (head-render))])
+           {:on-enter (fn []
+                        (when-let [head-el (and (not (string/blank? search-key1'))
+                                                (rum/deref *head-ref))]
+                          (when-let [^js item (.-nextSibling head-el)]
+                            (when (contains? #{"menuitemcheckbox" "menuitem"}
+                                             (.getAttribute item "role"))
+                              (.click item)))))
+            :valid-search-key? valid-search-key?}))
+        (when head-render (head-render))])
       ;; items
-      (for [item items
-            :let [selected? (some #(let [k (get-k item)
-                                         k' (get-k %)]
-                                     (or (= item %)
-                                       (and (not (nil? k))
-                                         (not (nil? k'))
-                                         (= k k'))))
-                              selected-items)]]
-        (if (fn? item-render)
-          (item-render item {:x-item x-item :selected? selected?})
-          (let [k (get-k item)
-                v (get-v item)]
-            (when k
-              (let [opts {:selected? selected?}
-                    on-click' (:on-click item-props)
-                    on-click (fn [e]
+     (for [item items
+           :let [selected? (some #(let [k (get-k item)
+                                        k' (get-k %)]
+                                    (or (= item %)
+                                        (and (not (nil? k))
+                                             (not (nil? k'))
+                                             (= k k'))))
+                                 selected-items)]]
+       (if (fn? item-render)
+         (item-render item {:x-item x-item :selected? selected?})
+         (let [k (get-k item)
+               v (get-v item)]
+           (when k
+             (let [opts {:selected? selected?}
+                   on-click' (:on-click item-props)
+                   on-click (fn [e]
                                ;; TODO: return value
-                               (when (fn? on-click') (on-click' e))
-                               (when (fn? on-chosen)
-                                 (on-chosen item opts)))]
-                (x-item (merge {:data-k k :on-click on-click} item-props)
-                  [:span.flex.items-center.gap-2.w-full
-                   (form/checkbox {:checked selected?})
-                   (let [v' (if (fn? v) (v item opts) v)]
-                     (if (fn? value-render)
-                       (value-render v' (assoc opts :item item)) v'))]))))))
+                              (when (fn? on-click') (on-click' e))
+                              (when (fn? on-chosen)
+                                (on-chosen item opts)))]
+               (x-item (merge {:data-k k :on-click on-click} item-props)
+                       [:span.flex.items-center.gap-2.w-full
+                        (form/checkbox {:checked selected?})
+                        (let [v' (if (fn? v) (v item opts) v)]
+                          (if (fn? value-render)
+                            (value-render v' (assoc opts :item item)) v'))]))))))
 
-      (when (and search-enabled?
-              (fn? search-key-render))
-        (let [exist-fn (fn []
-                         (and (not (string/blank? search-key1))
-                           (seq items)
-                           (contains? (into #{} (map #(some-> (get-v %) (string/lower-case)) items))
-                             (string/lower-case search-key1))))]
-          (search-key-render search-key1
-            {:items items :x-item x-item :exist-fn exist-fn})))
+     (when (and search-enabled?
+                (fn? search-key-render))
+       (let [exist-fn (fn []
+                        (and (not (string/blank? search-key1))
+                             (seq items)
+                             (contains? (into #{} (map #(some-> (get-v %) (string/lower-case)) items))
+                                        (string/lower-case search-key1))))]
+         (search-key-render search-key1
+                            {:items items :x-item x-item :exist-fn exist-fn})))
       ;; footer
-      (when (fn? foot-render)
-        [:div.foot
-         (foot-render)]))))
+     (when (fn? foot-render)
+       [:div.foot
+        (foot-render)]))))

+ 3 - 2
deps/shui/src/logseq/shui/table/core.cljc

@@ -2,6 +2,7 @@
   "Table"
   (:require [clojure.set :as set]
             [dommy.core :refer-macros [sel1]]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.table.impl :as impl]
             [rum.core :as rum]))
 
@@ -147,7 +148,7 @@
 ;; FIXME: ux
 (defn- use-sticky-element!
   [^js/HTMLElement container target-ref]
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (let [^js el (rum/deref target-ref)
            ^js cls (.-classList el)
@@ -189,7 +190,7 @@
 ;; FIXME: another solution for the sticky header
 (defn- use-sticky-element2!
   [^js/HTMLDivElement target-ref]
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (let [^js target (rum/deref target-ref)
            ^js container (or (.closest target ".sidebar-item-list") (get-main-scroll-container))

+ 17 - 16
deps/shui/src/logseq/shui/toaster/core.cljs

@@ -1,8 +1,9 @@
 (ns logseq.shui.toaster.core
-  (:require [rum.core :as rum]
+  (:require [cljs-bean.core :as bean]
             [daiquiri.interpreter :refer [interpret]]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.util :as util]
-            [cljs-bean.core :as bean]))
+            [rum.core :as rum]))
 
 (defonce ^:private Toaster (util/lsui-wrap "Toaster"))
 (defonce ^:private *toast (atom nil))
@@ -23,22 +24,22 @@
   < rum/static
   []
   (let [^js js-toast (js/window.LSUI.useToast)]
-    (rum/use-effect!
-      (fn []
-        (reset! *toast {:toast   (.-toast js-toast)
-                        :dismiss (.-dismiss js-toast)
-                        :update  (.-update js-toast)})
-        #())
-      [])
+    (hooks/use-effect!
+     (fn []
+       (reset! *toast {:toast   (.-toast js-toast)
+                       :dismiss (.-dismiss js-toast)
+                       :update  (.-update js-toast)})
+       #())
+     [])
     [:<> (Toaster)]))
 
 (defn update-html-props
   [v]
   (update-keys v
-    #(case %
-       :class :className
-       :for :htmlFor
-       %)))
+               #(case %
+                  :class :className
+                  :for :htmlFor
+                  %)))
 
 (defn interpret-vals
   [config ks & args]
@@ -46,7 +47,7 @@
             (let [v (get config k)
                   v (if (fn? v) (apply v args) v)]
               (if (vector? v) (assoc config k (interpret v)) config)))
-    config ks))
+          config ks))
 
 (defn toast!
   ([content-or-config] (toast! content-or-config :default nil))
@@ -56,12 +57,12 @@
      (let [config (if (map? content-or-config)
                     content-or-config
                     (-> {:description content-or-config}
-                      (merge (if (map? status) status {:variant status}))))
+                        (merge (if (map? status) status {:variant status}))))
            config (update-html-props (merge config opts))
            id (or (:id config) (gen-id))
            config (assoc config :id id)
            config (interpret-vals config [:title :description :action :icon]
-                    {:id id :dismiss! #(dismiss id) :update! #(toast! (assoc %1 :id id))})]
+                                  {:id id :dismiss! #(dismiss id) :update! #(toast! (assoc %1 :id id))})]
        (js->clj (toast (clj->js config))))
      :exception)))
 

+ 1 - 1
gulpfile.js

@@ -161,7 +161,7 @@ const common = {
       stdio: 'inherit',
     })
 
-    cp.execSync(`npx cap run ${mode} --external`, {
+    cp.execSync(`npx cap run ${mode}`, {
       stdio: 'inherit',
       env: Object.assign(process.env, {
         LOGSEQ_APP_SERVER_URL,

+ 1 - 1
ios/App/Podfile

@@ -1,6 +1,6 @@
 require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
 
-platform :ios, '13.0'
+platform :ios, '14.0'
 use_frameworks!
 
 # workaround to avoid Xcode caching of Pods that requires

+ 131 - 0
ios/App/Podfile.lock

@@ -0,0 +1,131 @@
+PODS:
+  - AgeEncryption (1.0.6)
+  - Alamofire (5.6.2)
+  - AWSCore (2.27.10)
+  - AWSS3 (2.27.10):
+    - AWSCore (= 2.27.10)
+  - Capacitor (7.2.0):
+    - CapacitorCordova
+  - CapacitorActionSheet (7.0.1):
+    - Capacitor
+  - CapacitorApp (7.0.1):
+    - Capacitor
+  - CapacitorCamera (7.0.1):
+    - Capacitor
+  - CapacitorClipboard (7.0.1):
+    - Capacitor
+  - CapacitorCordova (7.2.0)
+  - CapacitorFilesystem (7.0.1):
+    - Capacitor
+  - CapacitorHaptics (7.0.1):
+    - Capacitor
+  - CapacitorKeyboard (7.0.1):
+    - Capacitor
+  - CapacitorShare (7.0.1):
+    - Capacitor
+  - CapacitorSplashScreen (7.0.1):
+    - Capacitor
+  - CapacitorStatusBar (7.0.1):
+    - Capacitor
+  - CapacitorVoiceRecorder (5.0.0):
+    - Capacitor
+  - CapawesomeCapacitorBackgroundTask (7.0.1):
+    - Capacitor
+  - CapgoCapacitorNavigationBar (7.1.2):
+    - Capacitor
+  - LogseqCapacitorFileSync (5.0.2):
+    - AgeEncryption (~> 1.0.6)
+    - Alamofire
+    - AWSS3
+    - Capacitor
+  - SendIntent (0.0.1):
+    - Capacitor
+
+DEPENDENCIES:
+  - "Capacitor (from `../../node_modules/@capacitor/ios`)"
+  - "CapacitorActionSheet (from `../../node_modules/@capacitor/action-sheet`)"
+  - "CapacitorApp (from `../../node_modules/@capacitor/app`)"
+  - "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
+  - "CapacitorClipboard (from `../../node_modules/@capacitor/clipboard`)"
+  - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
+  - "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
+  - "CapacitorHaptics (from `../../node_modules/@capacitor/haptics`)"
+  - "CapacitorKeyboard (from `../../node_modules/@capacitor/keyboard`)"
+  - "CapacitorShare (from `../../node_modules/@capacitor/share`)"
+  - "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)"
+  - "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
+  - CapacitorVoiceRecorder (from `../../node_modules/capacitor-voice-recorder`)
+  - "CapawesomeCapacitorBackgroundTask (from `../../node_modules/@capawesome/capacitor-background-task`)"
+  - "CapgoCapacitorNavigationBar (from `../../node_modules/@capgo/capacitor-navigation-bar`)"
+  - "LogseqCapacitorFileSync (from `../../node_modules/@logseq/capacitor-file-sync`)"
+  - SendIntent (from `../../node_modules/send-intent`)
+
+SPEC REPOS:
+  trunk:
+    - AgeEncryption
+    - Alamofire
+    - AWSCore
+    - AWSS3
+
+EXTERNAL SOURCES:
+  Capacitor:
+    :path: "../../node_modules/@capacitor/ios"
+  CapacitorActionSheet:
+    :path: "../../node_modules/@capacitor/action-sheet"
+  CapacitorApp:
+    :path: "../../node_modules/@capacitor/app"
+  CapacitorCamera:
+    :path: "../../node_modules/@capacitor/camera"
+  CapacitorClipboard:
+    :path: "../../node_modules/@capacitor/clipboard"
+  CapacitorCordova:
+    :path: "../../node_modules/@capacitor/ios"
+  CapacitorFilesystem:
+    :path: "../../node_modules/@capacitor/filesystem"
+  CapacitorHaptics:
+    :path: "../../node_modules/@capacitor/haptics"
+  CapacitorKeyboard:
+    :path: "../../node_modules/@capacitor/keyboard"
+  CapacitorShare:
+    :path: "../../node_modules/@capacitor/share"
+  CapacitorSplashScreen:
+    :path: "../../node_modules/@capacitor/splash-screen"
+  CapacitorStatusBar:
+    :path: "../../node_modules/@capacitor/status-bar"
+  CapacitorVoiceRecorder:
+    :path: "../../node_modules/capacitor-voice-recorder"
+  CapawesomeCapacitorBackgroundTask:
+    :path: "../../node_modules/@capawesome/capacitor-background-task"
+  CapgoCapacitorNavigationBar:
+    :path: "../../node_modules/@capgo/capacitor-navigation-bar"
+  LogseqCapacitorFileSync:
+    :path: "../../node_modules/@logseq/capacitor-file-sync"
+  SendIntent:
+    :path: "../../node_modules/send-intent"
+
+SPEC CHECKSUMS:
+  AgeEncryption: 23c203675d5e4883a18b133ab1d5e61ff29e0c18
+  Alamofire: d368e1ff8a298e6dde360e35a3e68e6c610e7204
+  AWSCore: dbad318b7ff0fd86fb8387d3ad1f30049c05bc58
+  AWSS3: 0cf2cedb263368f624ca721e5c56a8cb59fc44bc
+  Capacitor: 106e7a4205f4618d582b886a975657c61179138d
+  CapacitorActionSheet: 24609588961cc27c87e8b033be92b5eee65b5d4c
+  CapacitorApp: d63334c052278caf5d81585d80b21905c6f93f39
+  CapacitorCamera: eb8687d8687fed853598ec9460d94bcd5e16babe
+  CapacitorClipboard: b98aead5dc7ec595547fc2c5d75bacd2ae3338bc
+  CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f
+  CapacitorFilesystem: 307f97c27a265edf8396a1c9c235592fd8572fe3
+  CapacitorHaptics: 70e47470fa1a6bd6338cd102552e3846b7f9a1b3
+  CapacitorKeyboard: 969647d0ca2e5c737d7300088e2517aa832434e2
+  CapacitorShare: 58d6c2da63b093e8693287b2d36db92435538435
+  CapacitorSplashScreen: 19cd3573e57507e02d6f34597a8c421e00931487
+  CapacitorStatusBar: 275cbf2f4dfc00388f519ef80c7ec22edda342c9
+  CapacitorVoiceRecorder: 872ea857b497ce2c71afe3e4eb5de0a74290c0db
+  CapawesomeCapacitorBackgroundTask: 834d797abc9933fac4354490d1a2f3c0e389b98d
+  CapgoCapacitorNavigationBar: 3a0e93a40b7da3d3cb74c0410bb761a1525406be
+  LogseqCapacitorFileSync: 541dcd5492b8aeb2257cce3d18bb9ed5800cfcad
+  SendIntent: 0a17b6984c4f27e9dfa56513267ba2c044a5a6c8
+
+PODFILE CHECKSUM: d9e4df9f9578f817a4148a5e29ee928f271e4cc8
+
+COCOAPODS: 1.11.2

+ 18 - 18
package.json

@@ -5,7 +5,7 @@
     "main": "static/electron.js",
     "devDependencies": {
         "@axe-core/playwright": "=4.4.4",
-        "@capacitor/cli": "^5.0.0",
+        "@capacitor/cli": "7.2.0",
         "@playwright/test": "=1.51.0",
         "@tailwindcss/aspect-ratio": "0.4.2",
         "@tailwindcss/forms": "0.5.3",
@@ -85,21 +85,21 @@
         "postinstall": "yarn tldraw:build && yarn amplify:build && yarn ui:build"
     },
     "dependencies": {
-        "@capacitor/action-sheet": "^5.0.7",
-        "@capacitor/android": "^5.0.0",
-        "@capacitor/app": "^5.0.0",
-        "@capacitor/camera": "^5.0.0",
-        "@capacitor/clipboard": "^5.0.0",
-        "@capacitor/core": "^5.0.0",
-        "@capacitor/filesystem": "^5.0.0",
-        "@capacitor/haptics": "^5.0.0",
-        "@capacitor/ios": "^5.0.0",
-        "@capacitor/keyboard": "^5.0.0",
-        "@capacitor/share": "^5.0.0",
-        "@capacitor/splash-screen": "^5.0.0",
-        "@capacitor/status-bar": "^5.0.0",
-        "@capawesome/capacitor-background-task": "^5.0.0",
-        "@capgo/capacitor-navigation-bar": "^6.0.0",
+        "@capacitor/action-sheet": "7.0.1",
+        "@capacitor/android": "7.2.0",
+        "@capacitor/app": "7.0.1",
+        "@capacitor/camera": "7.0.1",
+        "@capacitor/clipboard": "7.0.1",
+        "@capacitor/core": "7.2.0",
+        "@capacitor/filesystem": "7.0.1",
+        "@capacitor/haptics": "7.0.1",
+        "@capacitor/ios": "7.2.0",
+        "@capacitor/keyboard": "7.0.1",
+        "@capacitor/share": "7.0.1",
+        "@capacitor/splash-screen": "7.0.1",
+        "@capacitor/status-bar": "7.0.1",
+        "@capawesome/capacitor-background-task": "7.0.1",
+        "@capgo/capacitor-navigation-bar": "7.1.2",
         "@dnd-kit/core": "^6.0.8",
         "@dnd-kit/sortable": "^7.0.2",
         "@emoji-mart/data": "^1.1.2",
@@ -155,8 +155,8 @@
         "pixi.js": "6.2.0",
         "posthog-js": "1.10.2",
         "prop-types": "^15.7.2",
-        "react": "17.0.2",
-        "react-dom": "17.0.2",
+        "react": "18.3.1",
+        "react-dom": "18.3.1",
         "react-grid-layout": "0.16.6",
         "react-intersection-observer": "^9.3.5",
         "react-resize-context": "3.0.0",

+ 1 - 1
src/main/frontend/components/assets.cljs

@@ -8,10 +8,10 @@
    [frontend.context.i18n :refer [t]]
    [frontend.handler.assets :as assets-handler]
    [frontend.handler.notification :as notification]
-   [frontend.hooks :as hooks]
    [frontend.state :as state]
    [frontend.ui :as ui]
    [frontend.util :as util]
+   [logseq.shui.hooks :as hooks]
    [logseq.shui.ui :as shui]
    [medley.core :as medley]
    [promesa.core :as p]

+ 1 - 1
src/main/frontend/components/block.cljs

@@ -55,7 +55,6 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
-            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.mobile.intent :as mobile-intent]
             [frontend.mobile.util :as mobile-util]
@@ -87,6 +86,7 @@
             [logseq.graph-parser.text :as text]
             [logseq.outliner.property :as outliner-property]
             [logseq.shui.dialog.core :as shui-dialog]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [medley.core :as medley]
             [promesa.core :as p]

+ 1 - 1
src/main/frontend/components/bug_report.cljs

@@ -3,9 +3,9 @@
             [frontend.components.header :as header]
             [frontend.context.i18n :refer [t]]
             [frontend.handler.notification :as notification]
-            [frontend.hooks :as hooks]
             [frontend.ui :as ui]
             [frontend.util :as util]
+            [logseq.shui.hooks :as hooks]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))
 

+ 1 - 1
src/main/frontend/components/cmdk/core.cljs

@@ -18,7 +18,6 @@
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
-            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.modules.shortcut.utils :as shortcut-utils]
@@ -36,6 +35,7 @@
             [logseq.common.util.block-ref :as block-ref]
             [logseq.db :as ldb]
             [logseq.graph-parser.text :as text]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]))

+ 1 - 1
src/main/frontend/components/cmdk/list_item.cljs

@@ -2,8 +2,8 @@
   (:require
    ["remove-accents" :as remove-accents]
    [clojure.string :as string]
-   [frontend.hooks :as hooks]
    [goog.string :as gstring]
+   [logseq.shui.hooks :as hooks]
    [logseq.shui.ui :as shui]
    [rum.core :as rum]))
 

+ 1 - 1
src/main/frontend/components/container.cljs

@@ -32,7 +32,6 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
-            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.mobile.action-bar :as action-bar]
             [frontend.mobile.footer :as footer]
@@ -53,6 +52,7 @@
             [logseq.common.util.namespace :as ns-util]
             [logseq.db :as ldb]
             [logseq.shui.dialog.core :as shui-dialog]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.popup.core :as shui-popup]
             [logseq.shui.toaster.core :as shui-toaster]
             [logseq.shui.ui :as shui]

+ 4 - 2
src/main/frontend/components/content.cljs

@@ -81,7 +81,8 @@
                           (let [block-uuids (state/get-selection-block-ids)]
                             (shui/popup-hide!)
                             (shui/dialog-open!
-                             #(export/export-blocks block-uuids {:whiteboard? false}))))}
+                             #(export/export-blocks block-uuids {:whiteboard? false
+                                                                 :export-type :selected-nodes}))))}
       (t :content/copy-export-as))
 
      (shui/dropdown-menu-item
@@ -260,7 +261,8 @@
           {:key      "Copy as"
            :on-click (fn [_]
                        (shui/dialog-open!
-                        #(export/export-blocks [block-id] {:whiteboard? false})))}
+                        #(export/export-blocks [block-id] {:whiteboard? false
+                                                           :export-type :block})))}
           (t :content/copy-export-as))
 
          (when-not property-default-value?

+ 1 - 1
src/main/frontend/components/dnd.cljs

@@ -3,9 +3,9 @@
             ["@dnd-kit/sortable" :refer [useSortable arrayMove SortableContext verticalListSortingStrategy horizontalListSortingStrategy] :as sortable]
             ["@dnd-kit/utilities" :refer [CSS]]
             [cljs-bean.core :as bean]
-            [frontend.hooks :as hooks]
             [frontend.rum :as r]
             [frontend.state :as state]
+            [logseq.shui.hooks :as hooks]
             [rum.core :as rum]))
 
 (def dnd-context (r/adapt-class DndContext))

+ 1 - 1
src/main/frontend/components/editor.cljs

@@ -18,7 +18,6 @@
             [frontend.handler.paste :as paste-handler]
             [frontend.handler.property.util :as pu]
             [frontend.handler.search :as search-handler]
-            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.search :refer [fuzzy-search]]
             [frontend.state :as state]
@@ -33,6 +32,7 @@
             [logseq.db :as ldb]
             [logseq.db.frontend.class :as db-class]
             [logseq.graph-parser.property :as gp-property]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [react-draggable]

+ 26 - 4
src/main/frontend/components/export.cljs

@@ -1,6 +1,7 @@
 (ns frontend.components.export
   (:require ["/frontend/utils" :as utils]
             [cljs-time.core :as t]
+            [cljs.pprint :as pprint]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
@@ -160,6 +161,20 @@
              current-repo top-level-ids {:remove-options text-remove-options :other-options text-other-options})
       "")))
 
+(defn- <export-edn-helper
+  [root-block-uuids-or-page-uuid export-type]
+  (let [export-args (case export-type
+                      :page
+                      {:page-id [:block/uuid root-block-uuids-or-page-uuid]}
+                      :block
+                      {:block-id [:block/uuid (first root-block-uuids-or-page-uuid)]}
+                      :selected-nodes
+                      {:node-ids (mapv #(vector :block/uuid %) root-block-uuids-or-page-uuid)}
+                      {})]
+    (state/<invoke-db-worker :thread-api/export-edn
+                             (state/get-current-repo)
+                             (merge {:export-type export-type} export-args))))
+
 (defn- get-zoom-level
   [page-uuid]
   (let [uuid (:block/uuid (db/get-page page-uuid))
@@ -225,7 +240,7 @@
                    (reset! (::text-indent-style state) (state/get-export-block-text-indent-style))
                    (reset! (::text-other-options state) (state/get-export-block-text-other-options))
                    (assoc state ::top-level-uuids top-level-uuids)))}
-  [state _selection-ids {:keys [whiteboard?] :as options}]
+  [state _selection-ids {:keys [whiteboard? export-type] :as options}]
   (let [top-level-uuids (::top-level-uuids state)
         tp @*export-block-type
         *text-other-options (::text-other-options state)
@@ -252,11 +267,18 @@
                                    (reset! *content (export-helper top-level-uuids))))
          (when-not (seq? top-level-uuids)
            (ui/button "PNG"
-                      :class "w-20"
+                      :class "mr-4 w-20"
                       :on-click #(do (reset! *export-block-type :png)
                                      (reset! *content nil)
-                                     (get-image-blob top-level-uuids (merge options {:transparent-bg? false}) (fn [blob] (reset! *content blob))))))])
-
+                                     (get-image-blob top-level-uuids (merge options {:transparent-bg? false}) (fn [blob] (reset! *content blob))))))
+         (when (config/db-based-graph?)
+           (ui/button "EDN"
+                      :class "w-20"
+                      :on-click #(do (reset! *export-block-type :edn)
+                                     (p/let [result (<export-edn-helper top-level-uuids export-type)
+                                             pull-data (with-out-str (pprint/pprint result))]
+                                       (when-not (= :export-edn-error result)
+                                         (reset! *content pull-data))))))])
       (if (= :png tp)
         [:div.flex.items-center.justify-center.relative
          (when (not @*content) [:div.absolute (ui/loading "")])

+ 1 - 1
src/main/frontend/components/file_based/block.cljs

@@ -4,12 +4,12 @@
             [frontend.components.file-based.datetime :as datetime-comp]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.file-based.repeated :as repeated]
-            [frontend.hooks :as hooks]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.file-based.clock :as clock]
             [frontend.util.file-based.drawer :as drawer]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))

+ 5 - 5
src/main/frontend/components/file_based/git.cljs

@@ -2,10 +2,10 @@
   (:require [clojure.string :as string]
             [frontend.handler.file-based.file :as file-handler]
             [frontend.handler.shell :as shell]
-            [frontend.hooks :as hooks]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
+            [logseq.shui.hooks :as hooks]
             [promesa.core :as p]
             [rum.core :as rum]))
 
@@ -73,7 +73,7 @@
        (ui/button "Revert"
                   :on-click (fn []
                               (file-handler/alter-file (state/get-current-repo)
-                                               path
-                                               content
-                                               {:re-render-root? true
-                                                :skip-compare? true})))]]]))
+                                                       path
+                                                       content
+                                                       {:re-render-root? true
+                                                        :skip-compare? true})))]]]))

+ 1 - 1
src/main/frontend/components/file_sync.cljs

@@ -19,7 +19,6 @@
             [frontend.handler.page :as page-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.user :as user-handler]
-            [frontend.hooks :as hooks]
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
             [frontend.storage :as storage]
@@ -29,6 +28,7 @@
             [frontend.util.persist-var :as persist-var]
             [goog.functions :refer [debounce]]
             [logseq.common.util :as common-util]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]

+ 1 - 1
src/main/frontend/components/handbooks.cljs

@@ -1,9 +1,9 @@
 (ns frontend.components.handbooks
   (:require ;[shadow.lazy :as lazy]
    [frontend.extensions.handbooks.core :as handbooks]
-   [frontend.hooks :as hooks]
    [frontend.modules.layout.core :as layout]
    [frontend.state :as state]
+   [logseq.shui.hooks :as hooks]
    [rum.core :as rum]))
 
 #_:clj-kondo/ignore

+ 1 - 1
src/main/frontend/components/header.cljs

@@ -23,7 +23,6 @@
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
-            [frontend.hooks :as hooks]
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
             [frontend.storage :as storage]
@@ -31,6 +30,7 @@
             [frontend.util :as util]
             [frontend.version :refer [version]]
             [logseq.db :as ldb]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [logseq.shui.util :as shui-util]
             [missionary.core :as m]

+ 1 - 1
src/main/frontend/components/icon.cljs

@@ -6,7 +6,6 @@
             [clojure.string :as string]
             [frontend.config :as config]
             [frontend.handler.property.util :as pu]
-            [frontend.hooks :as hooks]
             [frontend.search :as search]
             [frontend.storage :as storage]
             [frontend.ui :as ui]
@@ -14,6 +13,7 @@
             [goog.functions :refer [debounce]]
             [goog.object :as gobj]
             [logseq.db :as ldb]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [medley.core :as medley]
             [promesa.core :as p]

+ 1 - 1
src/main/frontend/components/imports.cljs

@@ -18,7 +18,6 @@
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
-            [frontend.hooks :as hooks]
             [frontend.persist-db.browser :as db-browser]
             [frontend.state :as state]
             [frontend.ui :as ui]
@@ -32,6 +31,7 @@
             [logseq.graph-parser.exporter :as gp-exporter]
             [logseq.shui.dialog.core :as shui-dialog]
             [logseq.shui.form.core :as form-core]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]))

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

@@ -9,6 +9,7 @@
             [frontend.state :as state]
             [logseq.db.frontend.property :as db-property]
             [logseq.outliner.property :as outliner-property]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]))

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

@@ -33,7 +33,6 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
-            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.mobile.util :as mobile-util]
             [frontend.rum :as frontend-rum]
@@ -46,6 +45,7 @@
             [logseq.common.util.page-ref :as page-ref]
             [logseq.db :as ldb]
             [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
@@ -404,7 +404,8 @@
                                        :preview? preview?})
               [:span.title.block
                {:on-click (fn []
-                            (when (and (state/home?) (not preview?))
+                            (when (and (not preview?)
+                                       (contains? #{:home :all-journals} (get-in (state/get-route-match) [:data :name])))
                               (route-handler/redirect-to-page! (:block/uuid page))))
                 :data-value @*input-value
                 :data-ref   (:block/title page)

+ 2 - 1
src/main/frontend/components/page_menu.cljs

@@ -132,7 +132,8 @@
             {:title   (t :export-page)
              :options {:on-click #(shui/dialog-open!
                                    (fn []
-                                     (export/export-blocks [(:block/uuid page)] {:whiteboard? whiteboard?}))
+                                     (export/export-blocks [(:block/uuid page)] {:whiteboard? whiteboard?
+                                                                                 :export-type :page}))
                                    {:class "w-auto md:max-w-4xl max-h-[80vh] overflow-y-auto"})}})
 
           (when (util/electron?)

+ 1 - 1
src/main/frontend/components/plugins.cljs

@@ -12,7 +12,6 @@
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.plugin-config :as plugin-config-handler]
             [frontend.handler.ui :as ui-handler]
-            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.rum :as rum-utils]
             [frontend.search :as search]
@@ -20,6 +19,7 @@
             [frontend.storage :as storage]
             [frontend.ui :as ui]
             [frontend.util :as util]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]))

+ 1 - 1
src/main/frontend/components/plugins_settings.cljs

@@ -3,10 +3,10 @@
             [frontend.components.lazy-editor :as lazy-editor]
             [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
-            [frontend.hooks :as hooks]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [goog.functions :refer [debounce]]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [rum.core :as rum]))
 

+ 1 - 1
src/main/frontend/components/property.cljs

@@ -18,7 +18,6 @@
             [frontend.handler.property :as property-handler]
             [frontend.handler.property.util :as pu]
             [frontend.handler.route :as route-handler]
-            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.state :as state]
@@ -31,6 +30,7 @@
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.outliner.core :as outliner-core]
             [logseq.outliner.property :as outliner-property]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]))

+ 1 - 1
src/main/frontend/components/property/config.cljs

@@ -15,7 +15,6 @@
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.property :as property-handler]
             [frontend.handler.route :as route-handler]
-            [frontend.hooks :as hooks]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
@@ -25,6 +24,7 @@
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.outliner.core :as outliner-core]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.popup.core :as shui-popup]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]

+ 1 - 1
src/main/frontend/components/property/value.cljs

@@ -22,7 +22,6 @@
             [frontend.handler.property :as property-handler]
             [frontend.handler.property.util :as pu]
             [frontend.handler.route :as route-handler]
-            [frontend.hooks :as hooks]
             [frontend.modules.outliner.ui :as ui-outliner-tx]
             [frontend.search :as search]
             [frontend.state :as state]
@@ -37,6 +36,7 @@
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.outliner.property :as outliner-property]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]))

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

@@ -15,6 +15,7 @@
             [frontend.util :as util]
             [lambdaisland.glogi :as log]
             [logseq.db :as ldb]
+            [logseq.shui.hooks :as hooks]
             [rum.core :as rum]))
 
 (defn- built-in-custom-query?

+ 1 - 1
src/main/frontend/components/query/builder.cljs

@@ -11,7 +11,6 @@
             [frontend.db.query-dsl :as query-dsl]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.query.builder :as query-builder]
-            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.state :as state]
             [frontend.ui :as ui]
@@ -22,6 +21,7 @@
             [logseq.db.frontend.property :as db-property]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.graph-parser.db :as gp-db]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]))

+ 1 - 1
src/main/frontend/components/reference.cljs

@@ -10,10 +10,10 @@
             [frontend.db-mixins :as db-mixins]
             [frontend.db.async :as db-async]
             [frontend.db.utils :as db-utils]
-            [frontend.hooks :as hooks]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [logseq.db.common.view :as db-view]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [missionary.core :as m]
             [promesa.core :as p]

+ 1 - 1
src/main/frontend/components/right_sidebar.cljs

@@ -17,11 +17,11 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
-            [frontend.hooks :as hooks]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [logseq.db :as ldb]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [medley.core :as medley]
             [promesa.core :as p]

+ 1 - 1
src/main/frontend/components/select.cljs

@@ -8,13 +8,13 @@
             [frontend.context.i18n :refer [t]]
             [frontend.handler.common.developer :as dev-common-handler]
             [frontend.handler.repo :as repo-handler]
-            [frontend.hooks :as hooks]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.text :as text-util]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))

+ 1 - 1
src/main/frontend/components/server.cljs

@@ -3,10 +3,10 @@
    [clojure.string :as string]
    [electron.ipc :as ipc]
    [frontend.handler.notification :as notification]
-   [frontend.hooks :as hooks]
    [frontend.state :as state]
    [frontend.ui :as ui]
    [frontend.util :as util]
+   [logseq.shui.hooks :as hooks]
    [logseq.shui.ui :as shui]
    [medley.core :as medley]
    [promesa.core :as p]

+ 1 - 1
src/main/frontend/components/settings.cljs

@@ -21,7 +21,6 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
-            [frontend.hooks :as hooks]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.core :as instrument]
             [frontend.modules.shortcut.data-helper :as shortcut-helper]
@@ -34,6 +33,7 @@
             [goog.object :as gobj]
             [goog.string :as gstring]
             [logseq.db :as ldb]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]

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

@@ -3,7 +3,6 @@
             [clojure.string :as string]
             [frontend.context.i18n :refer [t]]
             [frontend.handler.notification :as notification]
-            [frontend.hooks :as hooks]
             [frontend.modules.shortcut.config :as shortcut-config]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.modules.shortcut.data-helper :as dh]
@@ -14,6 +13,7 @@
             [frontend.util :as util]
             [goog.events :as events]
             [logseq.shui.dialog.core :as shui-dialog]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum])

+ 1 - 1
src/main/frontend/components/theme.cljs

@@ -8,12 +8,12 @@
             [frontend.handler.plugin-config :as plugin-config-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
-            [frontend.hooks :as hooks]
             [frontend.rum :refer [use-mounted]]
             [frontend.state :as state]
             [frontend.storage :as storage]
             [frontend.ui :as ui]
             [frontend.util :as util]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [rum.core :as rum]))
 

+ 1 - 1
src/main/frontend/components/user/login.cljs

@@ -6,10 +6,10 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user]
-            [frontend.hooks :as hooks]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.rum :refer [adapt-class]]
             [frontend.state :as state]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [rum.core :as rum]))
 

+ 1 - 1
src/main/frontend/components/views.cljs

@@ -27,7 +27,6 @@
             [frontend.handler.property :as property-handler]
             [frontend.handler.property.util :as pu]
             [frontend.handler.ui :as ui-handler]
-            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.modules.outliner.op :as outliner-op]
             [frontend.modules.outliner.ui :as ui-outliner-tx]
@@ -40,6 +39,7 @@
             [logseq.db :as ldb]
             [logseq.db.common.view :as db-view]
             [logseq.db.frontend.property :as db-property]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [medley.core :as medley]
             [missionary.core :as m]

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

@@ -12,13 +12,13 @@
             [frontend.db.model :as model]
             [frontend.handler.route :as route-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
-            [frontend.hooks :as hooks]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.rum :refer [use-bounding-client-rect use-breakpoint]]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [logseq.common.util :as common-util]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]

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

@@ -12,7 +12,6 @@
             [frontend.db.conn :as conn]
             [frontend.db.react :as react]
             [frontend.db.utils :as db-utils]
-            [frontend.hooks :as hooks]
             [frontend.state :as state]
             [frontend.util :as util :refer [react]]
             [logseq.common.util :as common-util]
@@ -22,7 +21,8 @@
             [logseq.db.frontend.class :as db-class]
             [logseq.db.frontend.content :as db-content]
             [logseq.db.frontend.rules :as rules]
-            [logseq.graph-parser.db :as gp-db]))
+            [logseq.graph-parser.db :as gp-db]
+            [logseq.shui.hooks :as hooks]))
 
 ;; TODO: extract to specific models and move data transform logic to the
 ;; corresponding handlers.

+ 1 - 1
src/main/frontend/extensions/handbooks/core.cljs

@@ -9,7 +9,6 @@
             [frontend.extensions.lightbox :as lightbox]
             [frontend.extensions.video.youtube :as youtube]
             [frontend.handler.notification :as notification]
-            [frontend.hooks :as hooks]
             [frontend.modules.shortcut.config :as shortcut-config]
             [frontend.rum :as r]
             [frontend.search :as search]
@@ -17,6 +16,7 @@
             [frontend.storage :as storage]
             [frontend.ui :as ui]
             [frontend.util :as util]
+            [logseq.shui.hooks :as hooks]
             [medley.core :as medley]
             [promesa.core :as p]
             [rum.core :as rum]))

+ 1 - 1
src/main/frontend/extensions/pdf/core.cljs

@@ -14,13 +14,13 @@
             [frontend.extensions.pdf.windows :as pdf-windows]
             [frontend.handler.notification :as notification]
             [frontend.handler.property :as property-handler]
-            [frontend.hooks :as hooks]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.rum :refer [use-atom]]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [goog.functions :refer [debounce]]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [medley.core :as medley]
             [promesa.core :as p]

+ 1 - 1
src/main/frontend/extensions/pdf/toolbar.cljs

@@ -13,13 +13,13 @@
             [frontend.extensions.pdf.windows :refer [resolve-own-container] :as pdf-windows]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.notification :as notification]
-            [frontend.hooks :as hooks]
             [frontend.rum :refer [use-atom]]
             [frontend.state :as state]
             [frontend.storage :as storage]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [logseq.publishing.db :as publish-db]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]))

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

@@ -19,7 +19,6 @@
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
-            [frontend.hooks :as hooks]
             [frontend.rum :as r]
             [frontend.search :as search]
             [frontend.state :as state]
@@ -28,6 +27,7 @@
             [frontend.util.text :as text-util]
             [goog.object :as gobj]
             [logseq.common.util :as common-util]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]))

+ 1 - 1
src/main/frontend/extensions/zotero.cljs

@@ -9,11 +9,11 @@
             [frontend.extensions.zotero.setting :as setting]
             [frontend.handler.notification :as notification]
             [frontend.handler.route :as route-handler]
-            [frontend.hooks :as hooks]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [goog.dom :as gdom]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]))

+ 25 - 26
src/main/frontend/handler/db_based/export.cljs

@@ -17,10 +17,10 @@
                                             (state/get-current-repo)
                                             {:export-type :block :block-id [:block/uuid block-uuid]})
             pull-data (with-out-str (pprint/pprint result))]
-      (.writeText js/navigator.clipboard pull-data)
-      (println pull-data)
-      (notification/show! "Copied block's data!" :success))
-
+      (when-not (= :export-edn-error result)
+        (.writeText js/navigator.clipboard pull-data)
+        (println pull-data)
+        (notification/show! "Copied block's data!" :success)))
     (notification/show! "No block found" :warning)))
 
 (defn export-view-nodes-data [nodes]
@@ -29,9 +29,10 @@
                                             (state/get-current-repo)
                                             {:export-type :view-nodes :node-ids block-uuids})
             pull-data (with-out-str (pprint/pprint result))]
-      (.writeText js/navigator.clipboard pull-data)
-      (println pull-data)
-      (notification/show! "Copied block's data!" :success))))
+      (when-not (= :export-edn-error result)
+        (.writeText js/navigator.clipboard pull-data)
+        (println pull-data)
+        (notification/show! "Copied view nodes' data!" :success)))))
 
 (defn ^:export export-page-data []
   (if-let [page-id (page-util/get-current-page-id)]
@@ -39,9 +40,10 @@
                                             (state/get-current-repo)
                                             {:export-type :page :page-id page-id})
             pull-data (with-out-str (pprint/pprint result))]
-      (.writeText js/navigator.clipboard pull-data)
-      (println pull-data)
-      (notification/show! "Copied page's data!" :success))
+      (when-not (= :export-edn-error result)
+        (.writeText js/navigator.clipboard pull-data)
+        (println pull-data)
+        (notification/show! "Copied page's data!" :success)))
     (notification/show! "No page found" :warning)))
 
 (defn ^:export export-graph-ontology-data []
@@ -49,19 +51,12 @@
                                           (state/get-current-repo)
                                           {:export-type :graph-ontology})
           pull-data (with-out-str (pprint/pprint result))]
-    (.writeText js/navigator.clipboard pull-data)
-    (println pull-data)
-    (js/console.log (str "Exported " (count (:classes result)) " classes and "
-                         (count (:properties result)) " properties"))
-    (notification/show! "Copied graphs's ontology data!" :success)))
-
-(defn- export-graph-edn-data []
-  (p/let [result (state/<invoke-db-worker :thread-api/export-edn
-                                          (state/get-current-repo)
-                                          {:export-type :graph
-                                           :graph-options {:include-timestamps? true}})
-          pull-data (with-out-str (pprint/pprint result))]
-    pull-data))
+    (when-not (= :export-edn-error result)
+      (.writeText js/navigator.clipboard pull-data)
+      (println pull-data)
+      (js/console.log (str "Exported " (count (:classes result)) " classes and "
+                           (count (:properties result)) " properties"))
+      (notification/show! "Copied graphs's ontology data!" :success))))
 
 ;; Copied from handler.export
 (defn- file-name [repo extension]
@@ -72,9 +67,13 @@
 
 (defn export-repo-as-db-edn!
   [repo]
-  (p/let [edn-str (export-graph-edn-data)]
-    (when edn-str
-      (let [data-str (some->> edn-str
+  (p/let [result (state/<invoke-db-worker :thread-api/export-edn
+                                          (state/get-current-repo)
+                                          {:export-type :graph
+                                           :graph-options {:include-timestamps? true}})
+          pull-data (with-out-str (pprint/pprint result))]
+    (when-not (= :export-edn-error result)
+      (let [data-str (some->> pull-data
                               js/encodeURIComponent
                               (str "data:text/edn;charset=utf-8,"))
             filename (file-name repo :edn)]

+ 1 - 1
src/main/frontend/mobile/graph_picker.cljs

@@ -6,13 +6,13 @@
    [frontend.handler.file-based.nfs :as nfs-handler]
    [frontend.handler.notification :as notification]
    [frontend.handler.page :as page-handler]
-   [frontend.hooks :as hooks]
    [frontend.mobile.util :as mobile-util]
    [frontend.modules.shortcut.core :as shortcut]
    [frontend.state :as state]
    [frontend.ui :as ui]
    [frontend.util :as util]
    [logseq.common.path :as path]
+   [logseq.shui.hooks :as hooks]
    [logseq.shui.ui :as shui]
    [promesa.core :as p]
    [rum.core :as rum]))

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

@@ -5,7 +5,7 @@
             [clojure.string :as string]
             [clojure.walk :as w]
             [daiquiri.interpreter :as interpreter]
-            [frontend.hooks :as hooks]
+            [logseq.shui.hooks :as hooks]
             [rum.core :refer [use-state] :as rum]))
 
 ;; copy from https://github.com/priornix/antizer/blob/35ba264cf48b84e6597743e28b3570d8aa473e74/src/antizer/core.cljs

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

@@ -12,7 +12,6 @@
             [frontend.db.conn-state :as db-conn-state]
             [frontend.db.transact :as db-transact]
             [frontend.flows :as flows]
-            [frontend.hooks :as hooks]
             [frontend.mobile.util :as mobile-util]
             [frontend.spec.storage :as storage-spec]
             [frontend.storage :as storage]
@@ -25,6 +24,7 @@
             [logseq.db.frontend.entity-plus :as entity-plus]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.shui.dialog.core :as shui-dialog]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.ui :as shui]
             [missionary.core :as m]
             [promesa.core :as p]

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

@@ -18,7 +18,6 @@
             [frontend.db-mixins :as db-mixins]
             [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
-            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.shortcut.config :as shortcut-config]
@@ -32,6 +31,7 @@
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
+            [logseq.shui.hooks :as hooks]
             [logseq.shui.icon.v2 :as shui.icon.v2]
             [logseq.shui.popup.core :as shui-popup]
             [logseq.shui.ui :as shui]

+ 2 - 1
src/main/frontend/worker/db_worker.cljs

@@ -743,7 +743,8 @@
         (js/console.error "export-edn error: " e)
         (worker-util/post-message :notification
                                   ["An unexpected error occurred during export. See the javascript console for details."
-                                   :error])))))
+                                   :error])
+        :export-edn-error))))
 
 (def-thread-api :thread-api/get-view-data
   [repo view-id option]

+ 393 - 81
yarn.lock

@@ -240,25 +240,48 @@
     "@babel/helper-validator-identifier" "^7.22.20"
     to-fast-properties "^2.0.0"
 
-"@capacitor/action-sheet@^5.0.7":
-  version "5.0.7"
-  resolved "https://registry.yarnpkg.com/@capacitor/action-sheet/-/action-sheet-5.0.7.tgz#4808af8c483a85f58c7fffdea76f8d374883e3a0"
-  integrity sha512-8q8fYYy9dwLi4eiDiYq+ys/uOm9C2YpjfAnlG7fYdha8QNpaz2314jR+dxsJO423zYB2Wag9NXlupHpXeGdGOA==
+"@capacitor/action-sheet@7.0.1":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/action-sheet/-/action-sheet-7.0.1.tgz#8b6fd171375d1e4d9392ee9968ef4152309dcaa7"
+  integrity sha512-0ROMy6S62B/Ci9nZ7CRxhkK2LS0JBmhN+sxOXQOtntd2WHKW7+8J+VAtu8vk0LgvTjj7SjzJnicYC75KUjwLWw==
 
-"@capacitor/android@^5.0.0":
-  version "5.4.1"
-  resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-5.4.1.tgz#5b0445202ca5e48fcb79d0c88e4403acc32504bc"
-  integrity sha512-25k/GyIly/8xKQo0EO6eIvJStpVWsBPyYh9Eiv+Or9DCz9iuVlGZY3vui+zjFhfHQ5f9Uwywa8B+thOGWU8f6g==
+"@capacitor/android@7.2.0":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-7.2.0.tgz#f30e313315ab92c500bd57d9f0c4716a46b42f40"
+  integrity sha512-zdhEy3jZPG5Toe/pGzKtDgIiBGywjaoEuQWnGVjBYPlSAEUtAhpZ2At7V0SCb26yluAuzrAUV0Ue+LQeEtHwFQ==
 
-"@capacitor/app@^5.0.0":
-  version "5.0.6"
-  resolved "https://registry.yarnpkg.com/@capacitor/app/-/app-5.0.6.tgz#2ee02551115fd2e92dc7e81bc30a6c6fa78efa66"
-  integrity sha512-6ZXVdnNmaYILasC/RjQw+yfTmq2ZO7Q3v5lFcDVfq3PFGnybyYQh+RstBrYri+376OmXOXxBD7E6UxBhrMzXGA==
+"@capacitor/[email protected]":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/app/-/app-7.0.1.tgz#0d0709fb4dde5046c24853f2d6b77a7ea411f748"
+  integrity sha512-ArlVZAAla4MwQoKh26x2AaTDOBh5Vhp1VhMKR3RwqZSsZnazKTFGNrPbr9Ez5r1knnEDfApyjwp1uZnXK1WTYQ==
+
+"@capacitor/[email protected]":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/camera/-/camera-7.0.1.tgz#bcbd1c05cce648fa54b844bf7a6745854cae6ba4"
+  integrity sha512-gDUFsYlhMra5VVOa4iJV6+MQRhp3VXpTLQY4JDATj7UvoZ8Hv4DG8qplPL9ufUFNoR3QbDDnf8+gbQOsKdkDjg==
 
-"@capacitor/camera@^5.0.0":
-  version "5.0.7"
-  resolved "https://registry.yarnpkg.com/@capacitor/camera/-/camera-5.0.7.tgz#42f60168d731fc521df797aacc7cda703b7ac2b1"
-  integrity sha512-1Wk3Dk0UhhNHdBB07UrPvUOSL7Wi5gFZRyLY1LZL2awt34iqy2cnajtfJplFmEZHk8lD0i7NAl3HbkWm4td4OQ==
+"@capacitor/[email protected]":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-7.2.0.tgz#ea279f14e575b059edb7fe8dc64a8e3379088616"
+  integrity sha512-RNW9vtYYYSDmOdguYBSW0VpRnG/d6lGydlc9DLrJ7qbSPxFrotTz9IjkM48O+SruUma61DyuSqJttdbay2xSxg==
+  dependencies:
+    "@ionic/cli-framework-output" "^2.2.8"
+    "@ionic/utils-subprocess" "^3.0.1"
+    "@ionic/utils-terminal" "^2.3.5"
+    commander "^12.1.0"
+    debug "^4.4.0"
+    env-paths "^2.2.0"
+    fs-extra "^11.2.0"
+    kleur "^4.1.5"
+    native-run "^2.0.1"
+    open "^8.4.0"
+    plist "^3.1.0"
+    prompts "^2.4.2"
+    rimraf "^6.0.1"
+    semver "^7.6.3"
+    tar "^6.1.11"
+    tslib "^2.8.1"
+    xml2js "^0.6.2"
 
 "@capacitor/cli@^5.0.0":
   version "5.4.1"
@@ -283,10 +306,17 @@
     tslib "^2.4.0"
     xml2js "^0.5.0"
 
-"@capacitor/clipboard@^5.0.0":
-  version "5.0.6"
-  resolved "https://registry.yarnpkg.com/@capacitor/clipboard/-/clipboard-5.0.6.tgz#abc0101fb1b2780e829542b2d31f9eb1480b8ee8"
-  integrity sha512-VsokRAn+0HVWj6riSRdspczEfqFoHbrhS/XRhGoEPsj0uvYPSufy0Kb2dpnSqkeeElhh2Jvn8jmVAzII2XeR9w==
+"@capacitor/[email protected]":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/clipboard/-/clipboard-7.0.1.tgz#159fefa56b7d23632e9d08c9efd9cbd75a868b21"
+  integrity sha512-n4XEHma7apLOYvyeaR9S5u3uGzDYG7WeQxmtZlwP01HneIzMnusVgw4Im6I+pMBcoUN9TfVdf6eqKph97B1bAw==
+
+"@capacitor/[email protected]":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@capacitor/core/-/core-7.2.0.tgz#fdd1c5d2fbfa257887ce6065f66151a51f0e6d1b"
+  integrity sha512-2zCnA6RJeZ9ec4470o8QMZEQTWpekw9FNoqm5TLc10jeCrhvHVI8MPgxdZVc3mOdFlyieYu4AS1fNxSqbS57Pw==
+  dependencies:
+    tslib "^2.1.0"
 
 "@capacitor/core@^5.0.0":
   version "5.4.1"
@@ -295,50 +325,50 @@
   dependencies:
     tslib "^2.1.0"
 
-"@capacitor/filesystem@^5.0.0":
-  version "5.1.4"
-  resolved "https://registry.yarnpkg.com/@capacitor/filesystem/-/filesystem-5.1.4.tgz#6e3278f4c47c72416d586367889736b576113ef5"
-  integrity sha512-10EM1KvFMs+pTzxkcflspzxBWcX9sOnS9nTP5Afjr5hn4OxLrwTFySw2Z12Uv6jdN4OnhY3jXtDKXPljXvXILg==
+"@capacitor/filesystem@7.0.1":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/filesystem/-/filesystem-7.0.1.tgz#b0518d781f7640e936f529b80a59724e221d0471"
+  integrity sha512-dxuKNLFoUejm7tBKkQPs1j7+BLpDh5JKPSVu7nT8jgCbA/KXt5FoCLiepfkjWkYfq60X0JNFzGnIquc5FPOLzQ==
 
-"@capacitor/haptics@^5.0.0":
-  version "5.0.6"
-  resolved "https://registry.yarnpkg.com/@capacitor/haptics/-/haptics-5.0.6.tgz#c22fd6acbc62cbdff39279ded687c418f2c89a5a"
-  integrity sha512-UrMcR7p2X10ql4VLlowUuH/VckTeu0lj+RQpekxox14uxDmu5AGIFDK/iDTi8W6QZkxTJRZK6sbCjgwYgNJ7Pw==
+"@capacitor/haptics@7.0.1":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/haptics/-/haptics-7.0.1.tgz#8e992837e87c0b65a4a38430944762d1e2fce6dc"
+  integrity sha512-ewZmspE5krgDUj5ZvUDcfNZvgerAIr+3bDSk6DLzyvBZ/dYmr/tMLu5H6WtYaaKYZJ32aZAudGpIal5epDyBYA==
 
-"@capacitor/ios@^5.0.0":
-  version "5.4.1"
-  resolved "https://registry.yarnpkg.com/@capacitor/ios/-/ios-5.4.1.tgz#79c1620ac7b57dc1ea85653ad1e5a32e8ec7dbea"
-  integrity sha512-s5djQEX1HFM6u4gQlWz7DUWG7ma0d3AwwnjbSbCh25aYEt40Dbei0TjzlKrfU6b3nUbyVVAZJBJgMuk7Bq59qA==
-
-"@capacitor/keyboard@^5.0.0":
-  version "5.0.6"
-  resolved "https://registry.yarnpkg.com/@capacitor/keyboard/-/keyboard-5.0.6.tgz#04400e71b677abf9f1fc1ceaffd1211e7d864319"
-  integrity sha512-9GewAa/y2Hwkdw/Be8MTdiAjrFZ7TPDKpR44M0Y/0QMnK+mBbgzcoZ/UkuumWv6e2F1IAI+VY5eYVQHDeZcRoA==
-
-"@capacitor/share@^5.0.0":
-  version "5.0.6"
-  resolved "https://registry.yarnpkg.com/@capacitor/share/-/share-5.0.6.tgz#76d2f6f13d1596999c49a0d99d21e7a63b8ecfef"
-  integrity sha512-/dVOW7kcuuD7hWB9Z1drArIpEk+5JCoMnMrAs2c7CLp3q5PeaXNJjTkGr6RJ1OtMhsHyXc6DAFwQ4frFkmZsgw==
-
-"@capacitor/splash-screen@^5.0.0":
-  version "5.0.6"
-  resolved "https://registry.yarnpkg.com/@capacitor/splash-screen/-/splash-screen-5.0.6.tgz#d29320de49cc68add59e17fecb64aa2f1a08447f"
-  integrity sha512-9B8wSm89D+LlshFw8B+mjPU8pJNf1WOx2mkMjMvcH0/EqxNaE+ZaO8lPCX+9WvWSEZs3O3l11qiSnOFHeK0t9A==
-
-"@capacitor/status-bar@^5.0.0":
-  version "5.0.6"
-  resolved "https://registry.yarnpkg.com/@capacitor/status-bar/-/status-bar-5.0.6.tgz#281568a7f7aeacf80777702cb9947c29dc32685c"
-  integrity sha512-7od8CxsBnot1XMK3IeOkproFL4hgoKoWAc3pwUvmDOkQsXoxwQm4SR9mLwQavv1XfxtHbFV9Ukd7FwMxOPSViw==
-
-"@capawesome/capacitor-background-task@^5.0.0":
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/@capawesome/capacitor-background-task/-/capacitor-background-task-5.0.0.tgz#eb985131065df59f95ffa1e9ef3abf0f3573a35f"
-  integrity sha512-puZ/MaNNlIMmwojuMX5Z5rqWJFHjvpdgMB+xGe/QXG+IMrcY5eMtR3l/NLGtbH8izwid1dAsxzBACX5RGHdVzA==
+"@capacitor/[email protected]":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@capacitor/ios/-/ios-7.2.0.tgz#c3d9435582e5267b57085229e5678d1d53b15b5a"
+  integrity sha512-MQgRZcXZpbpjN83bjkGrzQd7s3XeHBZplmWf38/msF/siMGJKLrXNmNzmmPIWA5Xpi/aH6UoJFk1wXuU2U+zMg==
 
-"@capgo/capacitor-navigation-bar@^6.0.0":
-  version "6.0.6"
-  resolved "https://registry.yarnpkg.com/@capgo/capacitor-navigation-bar/-/capacitor-navigation-bar-6.0.6.tgz#e2c8d346298abf470d5a4583059fca34d4d4ff94"
-  integrity sha512-X0w0BFB07vyb2BipWaok8itPgn6inR5zuZIJgZGhsJXYmmyVAwsTbSRZZluIrDbMzd3LHD4bfzbzFzU02tfG3g==
+"@capacitor/[email protected]":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/keyboard/-/keyboard-7.0.1.tgz#edf212528cd1587494ceb1d260868c1a9540aed1"
+  integrity sha512-Gi064vOARMac+x9/DmEFeywN9oAETMf3OYsMuYm9gA8SvdsDJ3QJqMoFnSEIORYXe21Jzt2SIEdLlpT65P/b2g==
+
+"@capacitor/[email protected]":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/share/-/share-7.0.1.tgz#1d1e6d312504703c1f34fa0de94bed14af4bcbf8"
+  integrity sha512-7GAtWrb2inEWohC8E7mx38qAX6D9yqPDDnUtJaZ8JRpo15jjFRS40Cx388M8h4NlBWjV5NU3qf1sHXnyOBSJ5g==
+
+"@capacitor/[email protected]":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/splash-screen/-/splash-screen-7.0.1.tgz#ea70d39c4346db4558f749eeb2d3ac2f5c34c550"
+  integrity sha512-Nbqw9bEIe7uHj/HOT81mf4jT6uK1YykozpQw/uIKQDueMg6RJYaJK2/TMajIOohLk8fJF4TYIc1i9nGjNLnfGg==
+
+"@capacitor/[email protected]":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@capacitor/status-bar/-/status-bar-7.0.1.tgz#6bd3769ef35158c961ff2a6b571c03e9bce55809"
+  integrity sha512-iDv3mXYo9CdxYRVwt3/pRyuk25p7Sn4GfaS/zMZyVIqTzsvKLCIIH3GdKK+ta+nsNcAVpCw/t5jFEBt1D18ctA==
+
+"@capawesome/[email protected]":
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/@capawesome/capacitor-background-task/-/capacitor-background-task-7.0.1.tgz#5531717de4cea255156c7f83fd4bf0f1e472c534"
+  integrity sha512-ILkJ0bCOLperUc+fezzhpiH3Bfnr/318TI9XSrPU/vwvBXjMH7p7xYxKtjDA4VpJfbVh1cHmWLtRSWIk2wUglg==
+
+"@capgo/[email protected]":
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/@capgo/capacitor-navigation-bar/-/capacitor-navigation-bar-7.1.2.tgz#d017f22007e6e848c6a94aa38d70546b08d95473"
+  integrity sha512-lganepu29pay05+clCE41yEICE34xDzB61dmvtwWxZlWccvlu+XWbS8WnMSncvIotqBUmU1owfivG+usfrp4CA==
 
 "@colors/[email protected]":
   version "1.5.0"
@@ -430,6 +460,15 @@
     debug "^4.0.0"
     tslib "^2.0.1"
 
+"@ionic/cli-framework-output@^2.2.8":
+  version "2.2.8"
+  resolved "https://registry.yarnpkg.com/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz#29d541acc7773a6aaceec5f3b079937fbcef5402"
+  integrity sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==
+  dependencies:
+    "@ionic/utils-terminal" "2.3.5"
+    debug "^4.0.0"
+    tslib "^2.0.1"
+
 "@ionic/[email protected]":
   version "2.1.6"
   resolved "https://registry.yarnpkg.com/@ionic/utils-array/-/utils-array-2.1.6.tgz#eee863be945ee1a28b9a10ff16fdea776fa18c22"
@@ -438,7 +477,7 @@
     debug "^4.0.0"
     tslib "^2.0.1"
 
-"@ionic/[email protected]", "@ionic/utils-fs@^3.1.6":
+"@ionic/[email protected]", "@ionic/utils-fs@^3.1.6", "@ionic/utils-fs@^3.1.7":
   version "3.1.7"
   resolved "https://registry.yarnpkg.com/@ionic/utils-fs/-/utils-fs-3.1.7.tgz#e0d41225272c346846867e88a0b84b1a4ee9d9c9"
   integrity sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==
@@ -468,6 +507,18 @@
     tree-kill "^1.2.2"
     tslib "^2.0.1"
 
+"@ionic/[email protected]":
+  version "2.1.12"
+  resolved "https://registry.yarnpkg.com/@ionic/utils-process/-/utils-process-2.1.12.tgz#17b05d66201859fe11f53b47be22b85aa90b9556"
+  integrity sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==
+  dependencies:
+    "@ionic/utils-object" "2.1.6"
+    "@ionic/utils-terminal" "2.3.5"
+    debug "^4.0.0"
+    signal-exit "^3.0.3"
+    tree-kill "^1.2.2"
+    tslib "^2.0.1"
+
 "@ionic/[email protected]":
   version "3.1.6"
   resolved "https://registry.yarnpkg.com/@ionic/utils-stream/-/utils-stream-3.1.6.tgz#7c2fdcf4d9e621e8b2260e2fee2471825a4e214f"
@@ -476,6 +527,14 @@
     debug "^4.0.0"
     tslib "^2.0.1"
 
+"@ionic/[email protected]":
+  version "3.1.7"
+  resolved "https://registry.yarnpkg.com/@ionic/utils-stream/-/utils-stream-3.1.7.tgz#224f8c99012aa54e7dbf59950de903b6a61cd811"
+  integrity sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==
+  dependencies:
+    debug "^4.0.0"
+    tslib "^2.0.1"
+
 "@ionic/utils-subprocess@^2.1.11":
   version "2.1.12"
   resolved "https://registry.yarnpkg.com/@ionic/utils-subprocess/-/utils-subprocess-2.1.12.tgz#08ef08a13928aa97b9156b153e39fe5ff9c1e661"
@@ -490,6 +549,20 @@
     debug "^4.0.0"
     tslib "^2.0.1"
 
+"@ionic/utils-subprocess@^3.0.1":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz#561608fecf432c28fd80f94c1563dc0d092581b7"
+  integrity sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==
+  dependencies:
+    "@ionic/utils-array" "2.1.6"
+    "@ionic/utils-fs" "3.1.7"
+    "@ionic/utils-process" "2.1.12"
+    "@ionic/utils-stream" "3.1.7"
+    "@ionic/utils-terminal" "2.3.5"
+    cross-spawn "^7.0.3"
+    debug "^4.0.0"
+    tslib "^2.0.1"
+
 "@ionic/[email protected]", "@ionic/utils-terminal@^2.3.3":
   version "2.3.4"
   resolved "https://registry.yarnpkg.com/@ionic/utils-terminal/-/utils-terminal-2.3.4.tgz#e40c44b676265ed6a07a68407bda6e135870f879"
@@ -505,6 +578,33 @@
     untildify "^4.0.0"
     wrap-ansi "^7.0.0"
 
+"@ionic/[email protected]", "@ionic/utils-terminal@^2.3.4", "@ionic/utils-terminal@^2.3.5":
+  version "2.3.5"
+  resolved "https://registry.yarnpkg.com/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz#a48465f40496ee8f29c6d92e4506d5f19762ac3c"
+  integrity sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==
+  dependencies:
+    "@types/slice-ansi" "^4.0.0"
+    debug "^4.0.0"
+    signal-exit "^3.0.3"
+    slice-ansi "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+    tslib "^2.0.1"
+    untildify "^4.0.0"
+    wrap-ansi "^7.0.0"
+
+"@isaacs/cliui@^8.0.2":
+  version "8.0.2"
+  resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
+  integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
+  dependencies:
+    string-width "^5.1.2"
+    string-width-cjs "npm:string-width@^4.2.0"
+    strip-ansi "^7.0.1"
+    strip-ansi-cjs "npm:strip-ansi@^6.0.1"
+    wrap-ansi "^8.1.0"
+    wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
+
 "@isomorphic-git/[email protected]":
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/@isomorphic-git/idb-keyval/-/idb-keyval-3.3.2.tgz#c0509a6c5987d8a62efb3e47f2815bcc5eda2489"
@@ -1246,6 +1346,11 @@ ansi-regex@^5.0.1:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
   integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
 
+ansi-regex@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654"
+  integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==
+
 ansi-styles@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -1260,6 +1365,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   dependencies:
     color-convert "^2.0.1"
 
+ansi-styles@^6.1.0:
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
+  integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
+
 [email protected], ansi-wrap@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
@@ -2210,6 +2320,11 @@ comlink@^4.4.1:
   resolved "https://registry.yarnpkg.com/comlink/-/comlink-4.4.1.tgz#e568b8e86410b809e8600eb2cf40c189371ef981"
   integrity sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==
 
+commander@^12.1.0:
+  version "12.1.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
+  integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==
+
 commander@^4.0.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
@@ -2390,6 +2505,15 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+cross-spawn@^7.0.6:
+  version "7.0.6"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+  integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
 crypto-browserify@^3.11.0:
   version "3.12.0"
   resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
@@ -2570,6 +2694,13 @@ debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, d
   dependencies:
     ms "2.1.2"
 
+debug@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
+  integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
+  dependencies:
+    ms "^2.1.3"
+
 debug@~4.3.1, debug@~4.3.2, debug@~4.3.4:
   version "4.3.6"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b"
@@ -2878,6 +3009,11 @@ earcut@^2.2.2:
   resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a"
   integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==
 
+eastasianwidth@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
+  integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
+
 [email protected]:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -2943,6 +3079,11 @@ emoji-regex@^8.0.0:
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
   integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
 
+emoji-regex@^9.2.2:
+  version "9.2.2"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
+  integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
+
 encodeurl@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@@ -3519,6 +3660,14 @@ for-own@^1.0.0:
   dependencies:
     for-in "^1.0.1"
 
+foreground-child@^3.1.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
+  integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
+  dependencies:
+    cross-spawn "^7.0.6"
+    signal-exit "^4.0.1"
+
 fraction.js@^4.3.6:
   version "4.3.6"
   resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.6.tgz#e9e3acec6c9a28cf7bc36cbe35eea4ceb2c5c92d"
@@ -3550,6 +3699,15 @@ fs-extra@^10.0.0:
     jsonfile "^6.0.1"
     universalify "^2.0.0"
 
+fs-extra@^11.2.0:
+  version "11.3.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d"
+  integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==
+  dependencies:
+    graceful-fs "^4.2.0"
+    jsonfile "^6.0.1"
+    universalify "^2.0.0"
+
 fs-extra@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
@@ -3784,6 +3942,18 @@ [email protected]:
     minipass "^4.2.4"
     path-scurry "^1.5.0"
 
+glob@^11.0.0:
+  version "11.0.1"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.1.tgz#1c3aef9a59d680e611b53dcd24bb8639cef064d9"
+  integrity sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==
+  dependencies:
+    foreground-child "^3.1.0"
+    jackspeak "^4.0.1"
+    minimatch "^10.0.0"
+    minipass "^7.1.2"
+    package-json-from-dist "^1.0.0"
+    path-scurry "^2.0.0"
+
 glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.7:
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@@ -4300,6 +4470,11 @@ ini@^3.0.1:
   resolved "https://registry.yarnpkg.com/ini/-/ini-3.0.1.tgz#c76ec81007875bc44d544ff7a11a55d12294102d"
   integrity sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==
 
+ini@^4.1.1:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795"
+  integrity sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==
+
 inter-ui@^3.19.3:
   version "3.19.3"
   resolved "https://registry.yarnpkg.com/inter-ui/-/inter-ui-3.19.3.tgz#cf4b4b6d30de8d5463e2462588654b325206488c"
@@ -4765,6 +4940,13 @@ istextorbinary@^3.0.0:
     binaryextensions "^2.2.0"
     textextensions "^3.2.0"
 
+jackspeak@^4.0.1:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.0.tgz#c489c079f2b636dc4cbe9b0312a13ff1282e561b"
+  integrity sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==
+  dependencies:
+    "@isaacs/cliui" "^8.0.2"
+
 jiti@^1.19.1:
   version "1.21.0"
   resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
@@ -4941,7 +5123,7 @@ kleur@^3.0.3:
   resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
   integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
 
-kleur@^4.1.4:
+kleur@^4.1.4, kleur@^4.1.5:
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
   integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
@@ -5140,6 +5322,11 @@ lru-cache@^10.2.0:
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
   integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
 
+lru-cache@^11.0.0:
+  version "11.1.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.1.0.tgz#afafb060607108132dbc1cf8ae661afb69486117"
+  integrity sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==
+
 lru-cache@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -5413,6 +5600,13 @@ minimalistic-crypto-utils@^1.0.1:
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
   integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==
 
+minimatch@^10.0.0:
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b"
+  integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==
+  dependencies:
+    brace-expansion "^2.0.1"
+
 minimatch@^3.0.4, minimatch@^3.1.1:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -5470,6 +5664,11 @@ minipass@^5.0.0:
   resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.3.tgz#05ea638da44e475037ed94d1c7efcc76a25e1974"
   integrity sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==
 
+minipass@^7.1.2:
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
+  integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
+
 minizlib@^2.1.1:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
@@ -5515,6 +5714,11 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
+ms@^2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
 mute-stdout@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz#acb0300eb4de23a7ddeec014e3e96044b3472331"
@@ -5578,6 +5782,23 @@ native-run@^1.7.3:
     tslib "^2.4.0"
     yauzl "^2.10.0"
 
+native-run@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/native-run/-/native-run-2.0.1.tgz#a9b213c32824b007cbdd0279e0edd3c24bcc2f7a"
+  integrity sha512-XfG1FBZLM50J10xH9361whJRC9SHZ0Bub4iNRhhI61C8Jv0e1ud19muex6sNKB51ibQNUJNuYn25MuYET/rE6w==
+  dependencies:
+    "@ionic/utils-fs" "^3.1.7"
+    "@ionic/utils-terminal" "^2.3.4"
+    bplist-parser "^0.3.2"
+    debug "^4.3.4"
+    elementtree "^0.1.7"
+    ini "^4.1.1"
+    plist "^3.1.0"
+    split2 "^4.2.0"
+    through2 "^4.0.2"
+    tslib "^2.6.2"
+    yauzl "^2.10.0"
+
 [email protected]:
   version "0.6.3"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
@@ -5944,6 +6165,11 @@ p-try@^2.0.0:
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
+package-json-from-dist@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
+  integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
+
 pako@~1.0.2, pako@~1.0.5:
   version "1.0.11"
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
@@ -6118,6 +6344,14 @@ path-scurry@^1.6.1:
     lru-cache "^9.1.1 || ^10.0.0"
     minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
 
+path-scurry@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580"
+  integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==
+  dependencies:
+    lru-cache "^11.0.0"
+    minipass "^7.1.2"
+
 path-type@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -6334,7 +6568,7 @@ [email protected], playwright@=1.51.0:
   optionalDependencies:
     fsevents "2.3.2"
 
-plist@^3.0.5, plist@^3.0.6:
+plist@^3.0.5, plist@^3.0.6, plist@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9"
   integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==
@@ -6943,14 +7177,13 @@ [email protected]:
     iconv-lite "0.4.24"
     unpipe "1.0.0"
 
-react-dom@17.0.2:
-  version "17.0.2"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
-  integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
+react-dom@18.3.1:
+  version "18.3.1"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
+  integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
   dependencies:
     loose-envify "^1.1.0"
-    object-assign "^4.1.1"
-    scheduler "^0.20.2"
+    scheduler "^0.23.2"
 
 [email protected]:
   version "3.3.2"
@@ -7038,13 +7271,12 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.12.5.tgz#cf92efc2527e56d6df1d4d63c6e4dd3fac5a4030"
   integrity sha512-YeCbRRsC9CLf0buD0Rct7WsDbzf+yBU1wGbo05/XjbcN2nJuhgh040m3y3+6HVogTZxEqVm45ac9Fpae4/MxRQ==
 
-react@17.0.2:
-  version "17.0.2"
-  resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
-  integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
+react@18.3.1:
+  version "18.3.1"
+  resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
+  integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
   dependencies:
     loose-envify "^1.1.0"
-    object-assign "^4.1.1"
 
 read-cache@^1.0.0:
   version "1.0.0"
@@ -7387,6 +7619,14 @@ rimraf@^4.4.1:
   dependencies:
     glob "^9.2.0"
 
+rimraf@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.1.tgz#ffb8ad8844dd60332ab15f52bc104bc3ed71ea4e"
+  integrity sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==
+  dependencies:
+    glob "^11.0.0"
+    package-json-from-dist "^1.0.0"
+
 ripemd160@^2.0.0, ripemd160@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@@ -7472,13 +7712,12 @@ sax@>=0.6.0:
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
   integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
 
-scheduler@^0.20.2:
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
-  integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
+scheduler@^0.23.2:
+  version "0.23.2"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
+  integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
   dependencies:
     loose-envify "^1.1.0"
-    object-assign "^4.1.1"
 
 semver-compare@^1.0.0:
   version "1.0.0"
@@ -7516,6 +7755,11 @@ semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7:
   dependencies:
     lru-cache "^6.0.0"
 
+semver@^7.6.3:
+  version "7.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
+  integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
+
 send-intent@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/send-intent/-/send-intent-5.0.0.tgz#95d00455a7db4d95d9f79f8203698e96760c7408"
@@ -7652,6 +7896,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
   integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
 
+signal-exit@^4.0.1:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
+  integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
+
 simple-concat@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
@@ -7854,7 +8103,7 @@ split-string@^3.0.1, split-string@^3.0.2:
   dependencies:
     extend-shallow "^3.0.0"
 
-split2@^4.1.0:
+split2@^4.1.0, split2@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
   integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
@@ -7935,6 +8184,15 @@ strictdom@^1.0.1:
   resolved "https://registry.yarnpkg.com/strictdom/-/strictdom-1.0.1.tgz#189de91649f73d44d59b8432efa68ef9d2659460"
   integrity sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==
 
+"string-width-cjs@npm:string-width@^4.2.0":
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.1"
+
 string-width@^1.0.1, string-width@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -7961,6 +8219,15 @@ string-width@^2.0.0, string-width@^2.1.1:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^4.0.0"
 
+string-width@^5.0.1, string-width@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
+  integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
+  dependencies:
+    eastasianwidth "^0.2.0"
+    emoji-regex "^9.2.2"
+    strip-ansi "^7.0.1"
+
 string.prototype.padend@^3.0.0:
   version "3.1.5"
   resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz#311ef3a4e3c557dd999cdf88fbdde223f2ac0f95"
@@ -8011,6 +8278,13 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
 strip-ansi@^3.0.0, strip-ansi@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@@ -8032,6 +8306,13 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
   dependencies:
     ansi-regex "^5.0.1"
 
+strip-ansi@^7.0.1:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
+  integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
+  dependencies:
+    ansi-regex "^6.0.1"
+
 strip-bom@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
@@ -8474,6 +8755,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0:
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
   integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
 
+tslib@^2.6.2, tslib@^2.8.1:
+  version "2.8.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+  integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
 [email protected]:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@@ -8974,6 +9260,15 @@ wide-align@^1.1.2:
   dependencies:
     string-width "^1.0.2 || 2 || 3 || 4"
 
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
 wrap-ansi@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
@@ -8991,6 +9286,15 @@ wrap-ansi@^7.0.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
+wrap-ansi@^8.1.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
+  integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
+  dependencies:
+    ansi-styles "^6.1.0"
+    string-width "^5.0.1"
+    strip-ansi "^7.0.1"
+
 wrappy@1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -9024,6 +9328,14 @@ xml2js@^0.5.0:
     sax ">=0.6.0"
     xmlbuilder "~11.0.0"
 
+xml2js@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499"
+  integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==
+  dependencies:
+    sax ">=0.6.0"
+    xmlbuilder "~11.0.0"
+
 xmlbuilder@^15.1.1:
   version "15.1.1"
   resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5"