浏览代码

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

Tienson Qin 7 月之前
父节点
当前提交
b87c43e697
共有 38 个文件被更改,包括 857 次插入605 次删除
  1. 21 21
      .github/workflows/build-desktop-release.yml
  2. 1 1
      .github/workflows/e2e.yml
  3. 3 3
      deps/db/script/create_graph.cljs
  4. 5 5
      deps/db/script/dump_datoms.cljs
  5. 9 9
      deps/db/script/query.cljs
  6. 1 1
      deps/db/src/logseq/db/sqlite/build.cljs
  7. 2 2
      deps/db/src/logseq/db/sqlite/export.cljs
  8. 7 7
      deps/outliner/script/transact.cljs
  9. 19 14
      deps/outliner/src/logseq/outliner/property.cljs
  10. 4 4
      deps/publishing/script/publishing.cljs
  11. 34 33
      deps/shui/src/logseq/shui/popup/core.cljs
  12. 9 8
      deps/shui/src/logseq/shui/ui.cljs
  13. 53 0
      packages/ui/@/components/ui/button-group.tsx
  14. 2 1
      packages/ui/src/ui.ts
  15. 4 4
      scripts/src/logseq/tasks/db_graph/create_graph_with_large_sizes.cljs
  16. 2 1
      src/main/frontend/components/block.cljs
  17. 16 8
      src/main/frontend/components/container.cljs
  18. 1 1
      src/main/frontend/components/content.cljs
  19. 49 49
      src/main/frontend/components/editor.cljs
  20. 1 0
      src/main/frontend/components/icon.cljs
  21. 5 2
      src/main/frontend/components/objects.cljs
  22. 1 1
      src/main/frontend/components/page.cljs
  23. 80 58
      src/main/frontend/components/property.cljs
  24. 79 83
      src/main/frontend/components/property/config.cljs
  25. 13 4
      src/main/frontend/components/property/dialog.cljs
  26. 190 186
      src/main/frontend/components/property/value.cljs
  27. 4 0
      src/main/frontend/components/property/value.css
  28. 10 8
      src/main/frontend/components/select.cljs
  29. 87 0
      src/main/frontend/components/selection.cljs
  30. 7 12
      src/main/frontend/components/views.cljs
  31. 3 1
      src/main/frontend/handler.cljs
  32. 5 5
      src/main/frontend/handler/common/page.cljs
  33. 56 34
      src/main/frontend/handler/editor.cljs
  34. 25 2
      src/main/frontend/handler/events.cljs
  35. 11 5
      src/main/frontend/handler/export/common.cljs
  36. 3 2
      src/main/frontend/handler/export/text.cljs
  37. 18 14
      src/main/frontend/state.cljs
  38. 17 16
      src/main/frontend/ui.cljs

+ 21 - 21
.github/workflows/build-desktop-release.yml

@@ -183,7 +183,7 @@ jobs:
         uses: actions/checkout@v3
 
       - name: Download The Static Asset
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: static
           path: static
@@ -227,7 +227,7 @@ jobs:
     needs: [ compile-cljs ]
     steps:
       - name: Download The Static Asset
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: static
           path: static
@@ -273,7 +273,7 @@ jobs:
     needs: [ compile-cljs ]
     steps:
       - name: Download The Static Asset
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: static
           path: static
@@ -327,7 +327,7 @@ jobs:
     needs: [ compile-cljs ]
     steps:
       - name: Download The Static Asset
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: static
           path: static
@@ -394,7 +394,7 @@ jobs:
 
     steps:
       - name: Download The Static Asset
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: static
           path: static
@@ -470,7 +470,7 @@ jobs:
 
     steps:
       - name: Download The Static Asset
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: static
           path: static
@@ -560,7 +560,7 @@ jobs:
     runs-on: [self-hosted, macos, token]
     steps:
       - name: Download Windows Artifact
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-win64-unsigned-builds
           path: ./builds
@@ -584,43 +584,43 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: Download MacOS x64 Artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-darwin-x64-builds
           path: ./
 
       - name: Download MacOS arm64 Artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-darwin-arm64-builds
           path: ./
 
       - name: Download The Linux x64 Artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-linux-x64-builds
           path: ./
 
       - name: Download The Linux arm64 Artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-linux-arm64-builds
           path: ./
 
       - name: Download The Windows Artifact (Signed)
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-win64-signed-builds
           path: ./
 
       - name: Download The Windows Artifact
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-win64-builds
           path: ./
 
       - name: Download Android Artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-android-builds
           path: ./
@@ -664,43 +664,43 @@ jobs:
     runs-on: ubuntu-22.04
     steps:
       - name: Download MacOS x64 Artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-darwin-x64-builds
           path: ./
 
       - name: Download MacOS arm64 Artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-darwin-arm64-builds
           path: ./
 
       - name: Download The Linux x64 Artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-linux-x64-builds
           path: ./
 
       - name: Download The Linux arm64 Artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-linux-arm64-builds
           path: ./
 
       - name: Download The Windows Artifact (Signed)
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-win64-signed-builds
           path: ./
 
       - name: Download The Windows Artifact
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-win64-builds
           path: ./
 
       - name: Download Android Artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         if: ${{ github.event_name == 'schedule' || github.event.inputs.build-android == 'true' }}
         with:
           name: logseq-android-builds

+ 1 - 1
.github/workflows/e2e.yml

@@ -112,7 +112,7 @@ jobs:
         uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c  # v3.3.0
 
       - name: Download test build artifact
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: logseq-e2e-artifact
 

+ 3 - 3
deps/db/script/create_graph.cljs

@@ -3,15 +3,15 @@
   (:require ["fs" :as fs]
             ["os" :as os]
             ["path" :as node-path]
+            #_:clj-kondo/ignore
             [babashka.cli :as cli]
             [clojure.edn :as edn]
             [clojure.string :as string]
             [datascript.core :as d]
-            #_:clj-kondo/ignore
             [logseq.outliner.cli :as outliner-cli]
-            [validate-db]
             [nbb.classpath :as cp]
-            [nbb.core :as nbb]))
+            [nbb.core :as nbb]
+            [validate-db]))
 
 (defn- resolve-path
   "If relative path, resolve with $ORIGINAL_PWD"

+ 5 - 5
deps/db/script/dump_datoms.cljs

@@ -2,13 +2,13 @@
     "A script that dumps all eavt datoms to a specified edn file
 
      $ yarn -s nbb-logseq script/dump_datoms.cljs db-name datoms.edn"
-    (:require [datascript.core :as d]
+    (:require ["fs" :as fs]
+              ["os" :as os]
+              ["path" :as path]
               [clojure.pprint :as pprint]
+              [datascript.core :as d]
               [logseq.db.sqlite.cli :as sqlite-cli]
-              [nbb.core :as nbb]
-              ["path" :as path]
-              ["os" :as os]
-              ["fs" :as fs]))
+              [nbb.core :as nbb]))
 
 (defn read-graph
   "The db graph bare version of gp-cli/parse-graph"

+ 9 - 9
deps/db/script/query.cljs

@@ -2,17 +2,17 @@
   "A script that queries any db graph from the commandline e.g.
 
   $ yarn -s nbb-logseq script/query.cljs db-name '[:find (pull ?b [:block/name :block/title]) :where [?b :block/created-at]]'"
-  (:require [datascript.core :as d]
+  (:require ["child_process" :as child-process]
+            ["os" :as os]
+            ["path" :as node-path]
+            [babashka.cli :as cli]
             [clojure.edn :as edn]
-            [logseq.db.sqlite.cli :as sqlite-cli]
-            [logseq.db.frontend.rules :as rules]
-            [nbb.core :as nbb]
-            [clojure.string :as string]
             [clojure.pprint :as pprint]
-            [babashka.cli :as cli]
-            ["child_process" :as child-process]
-            ["path" :as node-path]
-            ["os" :as os]))
+            [clojure.string :as string]
+            [datascript.core :as d]
+            [logseq.db.frontend.rules :as rules]
+            [logseq.db.sqlite.cli :as sqlite-cli]
+            [nbb.core :as nbb]))
 
 (defn- sh
   "Run shell cmd synchronously and print to inherited streams by default. Aims

+ 1 - 1
deps/db/src/logseq/db/sqlite/build.cljs

@@ -344,7 +344,7 @@
                                              (mapcat (fn [m]
                                                        (if-let [pvalue-pages
                                                                 (->> (vals (:build/properties m))
-                                                                     (mapcat #(if (set? %) % [%]) )
+                                                                     (mapcat #(if (set? %) % [%]))
                                                                      (filter page-prop-value?)
                                                                      (map second)
                                                                      seq)]

+ 2 - 2
deps/db/src/logseq/db/sqlite/export.cljs

@@ -10,9 +10,9 @@
             [logseq.db.frontend.content :as db-content]
             [logseq.db.frontend.entity-plus :as entity-plus]
             [logseq.db.frontend.entity-util :as entity-util]
+            [logseq.db.frontend.malli-schema :as db-malli-schema]
             [logseq.db.frontend.property :as db-property]
-            [logseq.db.sqlite.build :as sqlite-build]
-            [logseq.db.frontend.malli-schema :as db-malli-schema]))
+            [logseq.db.sqlite.build :as sqlite-build]))
 
 ;; Export fns
 ;; ==========

+ 7 - 7
deps/outliner/script/transact.cljs

@@ -1,14 +1,14 @@
 (ns transact
   "This script generically runs transactions against the queried blocks"
-  (:require [logseq.outliner.db-pipeline :as db-pipeline]
-            [logseq.db.sqlite.cli :as sqlite-cli]
-            [logseq.db.frontend.rules :as rules]
-            [datascript.core :as d]
+  (:require ["os" :as os]
+            ["path" :as node-path]
             [clojure.edn :as edn]
             [clojure.string :as string]
-            [nbb.core :as nbb]
-            ["path" :as node-path]
-            ["os" :as os]))
+            [datascript.core :as d]
+            [logseq.db.frontend.rules :as rules]
+            [logseq.db.sqlite.cli :as sqlite-cli]
+            [logseq.outliner.db-pipeline :as db-pipeline]
+            [nbb.core :as nbb]))
 
 (defn -main [args]
   (when (< (count args) 3)

+ 19 - 14
deps/outliner/src/logseq/outliner/property.cljs

@@ -250,12 +250,12 @@
   [conn property-id v]
   (let [property (d/entity @conn property-id)
         closed-values? (seq (:property/closed-values property))
-        default-type? (= :default (:logseq.property/type property))]
+        default-or-url? (contains? #{:default :url} (:logseq.property/type property))]
     (cond
       closed-values?
       (get-property-value-eid @conn property-id v)
 
-      (and default-type?
+      (and default-or-url?
            ;; FIXME: remove this when :logseq.property/order-list-type updated to closed values
            (not= property-id :logseq.property/order-list-type))
       (let [v-uuid (create-property-text-block! conn nil property-id v {})]
@@ -339,20 +339,25 @@
         property-type (get property :logseq.property/type :default)
         _ (assert (some? v) "Can't set a nil property value must be not nil")
         ref? (db-property-type/value-ref-property-types property-type)
+        default-url-not-closed? (and (contains? #{:default :url} property-type)
+                                     (not (seq (:property/closed-values property))))
         v' (if ref?
              (convert-ref-property-value conn property-id v property-type)
-             v)]
-    (doseq [eid block-eids]
-      (let [block (d/entity @conn eid)]
-        (throw-error-if-self-value block v' ref?)))
-    (let [txs (mapcat
-               (fn [eid]
-                 (if-let [block (d/entity @conn eid)]
-                   (build-property-value-tx-data conn block property-id v')
-                   (js/console.error "Skipping setting a block's property because the block id could not be found:" eid)))
-               block-eids)]
-      (when (seq txs)
-        (ldb/transact! conn txs {:outliner-op :save-block})))))
+             v)
+        txs (doall
+             (mapcat
+              (fn [eid]
+                (if-let [block (d/entity @conn eid)]
+                  (let [v' (if default-url-not-closed?
+                             (let [v (if (number? v) (:block/title (d/entity @conn v)) v)]
+                               (convert-ref-property-value conn property-id v property-type))
+                             v')]
+                    (throw-error-if-self-value block v' ref?)
+                    (build-property-value-tx-data conn block property-id v'))
+                  (js/console.error "Skipping setting a block's property because the block id could not be found:" eid)))
+              block-eids))]
+    (when (seq txs)
+      (ldb/transact! conn txs {:outliner-op :save-block}))))
 
 (defn batch-remove-property!
   [conn block-ids property-id]

+ 4 - 4
deps/publishing/script/publishing.cljs

@@ -1,13 +1,13 @@
 (ns publishing
   "Basic script for publishing from CLI"
-  (:require [logseq.graph-parser.cli :as gp-cli]
-            [logseq.publishing :as publishing]
-            [logseq.db.sqlite.cli :as sqlite-cli]
-            ["fs" :as fs]
+  (:require ["fs" :as fs]
             ["path" :as node-path]
             [clojure.edn :as edn]
             [datascript.core :as d]
+            [logseq.db.sqlite.cli :as sqlite-cli]
             [logseq.db.sqlite.util :as sqlite-util]
+            [logseq.graph-parser.cli :as gp-cli]
+            [logseq.publishing :as publishing]
             [nbb.core :as nbb]))
 
 (defn- get-db [graph-dir]

+ 34 - 33
deps/shui/src/logseq/shui/popup/core.cljs

@@ -1,9 +1,9 @@
 (ns logseq.shui.popup.core
-  (:require [rum.core :as rum]
+  (:require [dommy.core :as d]
             [logseq.shui.util :as util]
-            [medley.core :as medley]
             [logseq.shui.util :refer [use-atom]]
-            [dommy.core :as d]))
+            [medley.core :as medley]
+            [rum.core :as rum]))
 
 ;; ui
 (def button (util/lsui-wrap "Button"))
@@ -39,7 +39,7 @@
   [id]
   (when id
     (some->> (medley/indexed @*popups)
-      (filter #(= id (:id (second %)))) (first))))
+             (filter #(= id (:id (second %)))) (first))))
 
 (defn get-popups [] @*popups)
 (defn get-last-popup [] (last @*popups))
@@ -97,13 +97,14 @@
                                 :start 0
                                 :end width
                                 (/ width 2)))
-                      (- bottom height) width height])
+                      (- bottom height)
+                      width height])
                    :else [0 0])]
     (reset! *last-show-target @*target)
     (js/setTimeout #(reset! *last-show-target nil) 64)
     (some-> @*target
-      (d/set-attr! "data-popup-active"
-        (if (keyword? id) (name id) (str id))))
+            (d/set-attr! "data-popup-active"
+                         (if (keyword? id) (name id) (str id))))
     (upsert-popup!
      (merge opts
             {:id id :target (deref *target)
@@ -166,32 +167,32 @@
                                     (when-not (false? (some-> content-props (:onPointerDownOutside) (apply [e])))
                                       (hide! id 1)))]
       (popup-root
-        (merge root-props {:open open?})
-        (popup-trigger
-          {:as-child true}
-          (button {:class "overflow-hidden fixed p-0 opacity-0"
-                   :style {:height (if (and (number? height)
-                                         (> height 0))
-                                     height 1)
-                           :width 1
-                           :top y
-                           :left x}} ""))
-        (let [content-props (cond-> (merge content-props {:onEscapeKeyDown handle-key-escape!
-                                                          :disableOutsideScroll false
-                                                          :onPointerDownOutside handle-pointer-outside!})
-                              (and (not force-popover?)
-                                (not as-dropdown?))
-                              (assoc :on-key-down (fn [^js e]
-                                                    (some-> content-props :on-key-down (apply [e]))
-                                                    (set! (. e -defaultPrevented) true))
-                                :on-pointer-move #(set! (. % -defaultPrevented) true)))
-              content (if (fn? content)
-                        (content (cond-> {:id id}
-                                   as-content?
-                                   (assoc :content-props content-props))) content)]
-          (if as-content?
-            content
-            (popup-content content-props content)))))))
+       (merge root-props {:open open?})
+       (popup-trigger
+        {:as-child true}
+        (button {:class "overflow-hidden fixed p-0 opacity-0"
+                 :style {:height (if (and (number? height)
+                                          (> height 0))
+                                   height 1)
+                         :width 1
+                         :top y
+                         :left x}} ""))
+       (let [content-props (cond-> (merge content-props {:onEscapeKeyDown handle-key-escape!
+                                                         :disableOutsideScroll false
+                                                         :onPointerDownOutside handle-pointer-outside!})
+                             (and (not force-popover?)
+                                  (not as-dropdown?))
+                             (assoc :on-key-down (fn [^js e]
+                                                   (some-> content-props :on-key-down (apply [e]))
+                                                   (set! (. e -defaultPrevented) true))
+                                    :on-pointer-move #(set! (. % -defaultPrevented) true)))
+             content (if (fn? content)
+                       (content (cond-> {:id id}
+                                  as-content?
+                                  (assoc :content-props content-props))) content)]
+         (if as-content?
+           content
+           (popup-content content-props content)))))))
 
 (rum/defc install-popups
   < rum/static

+ 9 - 8
deps/shui/src/logseq/shui/ui.cljs

@@ -1,21 +1,22 @@
 (ns logseq.shui.ui
-  (:require [logseq.shui.util :as util]
+  (:require [logseq.shui.base.core :as base-core]
+            [logseq.shui.dialog.core :as dialog-core]
+            [logseq.shui.form.core :as form-core]
             [logseq.shui.icon.v2 :as icon-v2]
-            [logseq.shui.shortcut.v1 :as shui.shortcut.v1]
-            [logseq.shui.toaster.core :as toaster-core]
+            [logseq.shui.popup.core :as popup-core]
             [logseq.shui.select.core :as select-core]
             [logseq.shui.select.multi :as select-multi]
-            [logseq.shui.dialog.core :as dialog-core]
-            [logseq.shui.popup.core :as popup-core]
-            [logseq.shui.base.core :as base-core]
-            [logseq.shui.form.core :as form-core]
-            [logseq.shui.table.core :as table-core]))
+            [logseq.shui.shortcut.v1 :as shui.shortcut.v1]
+            [logseq.shui.table.core :as table-core]
+            [logseq.shui.toaster.core :as toaster-core]
+            [logseq.shui.util :as util]))
 
 (def button base-core/button)
 (def button-icon base-core/button-icon)
 (def button-ghost-icon base-core/button-ghost-icon)
 (def button-outline-icon base-core/button-outline-icon)
 (def button-secondary-icon base-core/button-secondary-icon)
+(def button-group (util/lsui-wrap "ButtonGroup"))
 (def link base-core/link)
 (def trigger-as base-core/trigger-as)
 (def trigger-child-wrap base-core/trigger-child-wrap)

+ 53 - 0
packages/ui/@/components/ui/button-group.tsx

@@ -0,0 +1,53 @@
+import { Children, ReactElement, cloneElement } from 'react';
+
+import { ButtonProps } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+
+interface ButtonGroupProps {
+  className?: string;
+  orientation?: 'horizontal' | 'vertical';
+  children: ReactElement<ButtonProps>[];
+}
+
+export const ButtonGroup = ({
+  className,
+  orientation = 'horizontal',
+  children,
+}: ButtonGroupProps) => {
+  const totalButtons = Children.count(children);
+  const isHorizontal = orientation === 'horizontal';
+  const isVertical = orientation === 'vertical';
+
+  return (
+    <div
+      className={cn(
+        'flex',
+        {
+          'flex-col': isVertical,
+          'w-fit': isVertical,
+        },
+        className
+      )}
+    >
+      {Children.map(children, (child, index) => {
+        const isFirst = index === 0;
+        const isLast = index === totalButtons - 1;
+
+        return cloneElement(child, {
+          className: cn(
+            {
+              'rounded-l-none': isHorizontal && !isFirst,
+              'rounded-r-none': isHorizontal && !isLast,
+              'border-l-0': isHorizontal && !isFirst,
+
+              'rounded-t-none': isVertical && !isFirst,
+              'rounded-b-none': isVertical && !isLast,
+              'border-t-0': isVertical && !isFirst,
+            },
+            child.props.className
+          ),
+        });
+      })}
+    </div>
+  );
+};

+ 2 - 1
packages/ui/src/ui.ts

@@ -1,4 +1,5 @@
 import { Button } from '@/components/ui/button'
+import { ButtonGroup } from '@/components/ui/button-group'
 import { Slider, SliderTrack, SliderRange, SliderThumb } from '@/components/ui/slider'
 import {
   DropdownMenu,
@@ -99,7 +100,7 @@ declare global {
 }
 
 const shadui = {
-  Link, Button,
+  Link, Button, ButtonGroup,
   Slider, SliderTrack, SliderRange, SliderThumb,
   DropdownMenu,
   DropdownMenuContent,

+ 4 - 4
scripts/src/logseq/tasks/db_graph/create_graph_with_large_sizes.cljs

@@ -1,11 +1,11 @@
 (ns logseq.tasks.db-graph.create-graph-with-large-sizes
   "Script that generates graphs at large sizes"
-  (:require [logseq.outliner.cli :as outliner-cli]
+  (:require ["os" :as os]
+            ["path" :as node-path]
+            [babashka.cli :as cli]
             [clojure.string :as string]
             [datascript.core :as d]
-            [babashka.cli :as cli]
-            ["path" :as node-path]
-            ["os" :as os]
+            [logseq.outliner.cli :as outliner-cli]
             [nbb.classpath :as cp]
             [nbb.core :as nbb]))
 

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

@@ -2169,7 +2169,8 @@
                  (not edit?)
                  (not (:block.temp/top? block))
                  (not (:block.temp/bottom? block))
-                 (not (util/react *control-show?)))
+                 (not (util/react *control-show?))
+                 (not (:logseq.property/created-from-property  block)))
             (and doc-mode?
                  (not collapsed?)
                  (not (util/react *control-show?))))

+ 16 - 8
src/main/frontend/components/container.cljs

@@ -878,16 +878,18 @@
                             {:keys [page page-entity]} (state/sub :page-title/context)]
 
                         (let [show!
-                              (fn [content]
+                              (fn [content & {:as option}]
                                 (shui/popup-show! e
                                                   (fn [{:keys [id]}]
                                                     [:div {:on-click #(shui/popup-hide! id)
                                                            :data-keep-selection true}
                                                      content])
-                                                  {:on-before-hide state/dom-clear-selection!
-                                                   :on-after-hide state/state-clear-selection!
-                                                   :content-props {:class "w-[280px] ls-context-menu-content"}
-                                                   :as-dropdown? true}))
+                                                  (merge
+                                                   {:on-before-hide state/dom-clear-selection!
+                                                    :on-after-hide state/state-clear-selection!
+                                                    :content-props {:class "w-[280px] ls-context-menu-content"}
+                                                    :as-dropdown? true}
+                                                   option)))
 
                               handled
                               (cond
@@ -901,11 +903,12 @@
                                   (show! (cp-content/block-ref-custom-context-menu-content block block-ref))
                                   (state/set-state! :block-ref/context nil))
 
-                      ;; block selection
+                                ;; block selection
                                 (and (state/selection?) (not (d/has-class? target "bullet")))
-                                (show! (cp-content/custom-context-menu-content))
+                                (show! (cp-content/custom-context-menu-content)
+                                       {:id :blocks-selection-context-menu})
 
-                      ;; block bullet
+                                ;; block bullet
                                 (and block-id (parse-uuid block-id))
                                 (let [block (.closest target ".ls-block")
                                       property-default-value? (when block
@@ -922,6 +925,10 @@
   []
   nil)
 
+(defn- on-mouse-up
+  [_e]
+  (editor-handler/show-action-bar!))
+
 (rum/defcs ^:large-vars/cleanup-todo root-container < rum/reactive
   (mixins/event-mixin
    (fn [state]
@@ -1014,6 +1021,7 @@
                        (ui/focus-element (ui/main-node))))}
        (t :accessibility/skip-to-main-content)]
       [:div.#app-container
+       {:on-mouse-up on-mouse-up}
        [:div#left-container
         {:class (if (state/sub :ui/sidebar-open?) "overflow-hidden" "w-full")}
         (header/header {:light? light?

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

@@ -71,7 +71,7 @@
 
      (shui/dropdown-menu-item
       {:key "copy"
-       :on-click editor-handler/copy-selection-blocks}
+       :on-click #(editor-handler/copy-selection-blocks true)}
       (t :editor/copy)
       (shui/dropdown-menu-shortcut (ui/keyboard-shortcut-from-config :editor/copy)))
 

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

@@ -661,63 +661,63 @@
 (rum/defc shui-editor-popups
   [id format action _data]
   (hooks/use-effect!
-    (fn []
-      (let [pid (case action
-                  :commands
-                  (open-editor-popup! :commands
-                    (commands id format)
-                    {:content-props {:withoutAnimation false}})
-
-                  (:block-search :page-search :page-search-hashtag)
-                  (open-editor-popup! action
-                    (if (= :block-search action)
-                      (block-search id format)
-                      (page-search id format))
-                    {:root-props {:onOpenChange
-                                  #(when-not %
-                                     (when (contains?
-                                             #{:block-search :page-search :page-search-hashtag}
-                                             (state/get-editor-action))
-                                       (state/clear-editor-action!)))}})
-
-                  :datepicker
-                  (open-editor-popup! :datepicker
-                    (datetime-comp/date-picker id format nil) {})
-
-                  :input
-                  (open-editor-popup! :input
-                    (editor-input id
+   (fn []
+     (let [pid (case action
+                 :commands
+                 (open-editor-popup! :commands
+                                     (commands id format)
+                                     {:content-props {:withoutAnimation false}})
+
+                 (:block-search :page-search :page-search-hashtag)
+                 (open-editor-popup! action
+                                     (if (= :block-search action)
+                                       (block-search id format)
+                                       (page-search id format))
+                                     {:root-props {:onOpenChange
+                                                   #(when-not %
+                                                      (when (contains?
+                                                             #{:block-search :page-search :page-search-hashtag}
+                                                             (state/get-editor-action))
+                                                        (state/clear-editor-action!)))}})
+
+                 :datepicker
+                 (open-editor-popup! :datepicker
+                                     (datetime-comp/date-picker id format nil) {})
+
+                 :input
+                 (open-editor-popup! :input
+                                     (editor-input id
                       ;; on-submit
-                      (fn [command m]
-                        (editor-handler/handle-command-input command id format m))
+                                                   (fn [command m]
+                                                     (editor-handler/handle-command-input command id format m))
                       ;; on-cancel
-                      (fn []
-                        (editor-handler/handle-command-input-close id)))
-                    {:content-props {:onOpenAutoFocus #()}})
+                                                   (fn []
+                                                     (editor-handler/handle-command-input-close id)))
+                                     {:content-props {:onOpenAutoFocus #()}})
 
-                  :select-code-block-mode
-                  (open-editor-popup! :code-block-mode-picker
-                    (code-block-mode-picker id format) {})
+                 :select-code-block-mode
+                 (open-editor-popup! :code-block-mode-picker
+                                     (code-block-mode-picker id format) {})
 
-                  :template-search
-                  (open-editor-popup! :template-search
-                    (template-search id format) {})
+                 :template-search
+                 (open-editor-popup! :template-search
+                                     (template-search id format) {})
 
-                  (:property-search :property-value-search)
-                  (open-editor-popup! action
-                    (if (= :property-search action)
-                      (property-search id) (property-value-search id))
-                    {})
+                 (:property-search :property-value-search)
+                 (open-editor-popup! action
+                                     (if (= :property-search action)
+                                       (property-search id) (property-value-search id))
+                                     {})
 
-                  :zotero
-                  (open-editor-popup! :zotero
-                    (zotero/zotero-search id) {})
+                 :zotero
+                 (open-editor-popup! :zotero
+                                     (zotero/zotero-search id) {})
 
                   ;; TODO: try remove local model state
-                  false)]
-        #(when pid
-           (shui/popup-hide! pid))))
-    [action])
+                 false)]
+       #(when pid
+          (shui/popup-hide! pid))))
+   [action])
   [:<>])
 
 (rum/defc command-popups <

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

@@ -357,6 +357,7 @@
                          (.focus input))
                        (util/scroll-to (rum/deref *result-ref) 0 false))))]
     [:div.cp__emoji-icon-picker
+     {:data-keep-selection true}
      ;; header
      [:div.hd.bg-popover
       (tab-observer @*tab {:reset-q! reset-q!})

+ 5 - 2
src/main/frontend/components/objects.cljs

@@ -119,10 +119,13 @@
 
 (defn- add-new-property-object!
   [property set-data! properties]
-  (p/let [block (editor-handler/api-insert-new-block! ""
+  (p/let [default-value (if (= :checkbox (:logseq.property/type property))
+                          false
+                          (:db/id (db/entity :logseq.property/empty-placeholder)))
+          block (editor-handler/api-insert-new-block! ""
                                                       {:page (:block/uuid property)
                                                        :properties (merge
-                                                                    {(:db/ident property) (:db/id (db/entity :logseq.property/empty-placeholder))}
+                                                                    {(:db/ident property) default-value}
                                                                     properties)
                                                        :edit-block? false})
           _ (set-data! (get-property-related-objects (state/get-current-repo) property))]

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

@@ -244,7 +244,7 @@
                                            block' (if last-child-id (db/entity last-child-id) (last blocks))
                                            link (:block/link block')]
                                        (string/blank? (:block/title (or link block'))))))]
-            [:div
+            [:div.relative
              {:class (when add-button? "show-add-button")}
              (page-blocks-inner page-e blocks config sidebar? whiteboard? block-id)
              (let [args (if block-id

+ 80 - 58
src/main/frontend/components/property.cljs

@@ -14,8 +14,8 @@
             [frontend.db.async :as db-async]
             [frontend.db.model :as db-model]
             [frontend.handler.db-based.property :as db-property-handler]
-            [frontend.handler.editor :as editor-handler]
             [frontend.handler.notification :as notification]
+            [frontend.handler.property :as property-handler]
             [frontend.handler.property.util :as pu]
             [frontend.handler.route :as route-handler]
             [frontend.hooks :as hooks]
@@ -116,6 +116,8 @@
                    (do
                      (shui/popup-hide!)
                      (shui/dialog-close!))
+                   (pv/batch-operation?)
+                   nil
                    (and block (= type :checkbox))
                    (p/do!
                     (ui/hide-popups-until-preview-popup!)
@@ -176,6 +178,7 @@
                          :value (:block/uuid x)
                          :group "Tags"}) classes))]
       [:div.ls-property-add.flex.flex-row.items-center.property-key
+       {:data-keep-selection true}
        [:div.ls-property-key
         (select/select (merge
                         {:items items
@@ -212,44 +215,55 @@
                    :size 15})))
 
 (defn- property-input-on-chosen
-  [block *property *property-key *show-new-property-config? {:keys [class-schema?]}]
+  [block *property *property-key *show-new-property-config? {:keys [class-schema? remove-property?]}]
   (fn [{:keys [value label]}]
     (reset! *property-key (if (uuid? value) label value))
-    (let [property (when (uuid? value) (db/entity [:block/uuid value]))]
-      (when (and *show-new-property-config? (not (ldb/property? property)))
-        (reset! *show-new-property-config? true))
-      (reset! *property property)
-      (when property
-        (let [add-class-property? (and (ldb/class? block) class-schema?)
-              type (:logseq.property/type property)]
-          (cond
-            add-class-property?
-            (p/do!
-             (pv/<add-property! block (:db/ident property) "" {:class-schema? class-schema?})
-             (shui/popup-hide!)
-             (shui/dialog-close!))
-
-            (= :checkbox type)
-            (p/do!
-             (ui/hide-popups-until-preview-popup!)
-             (shui/popup-hide!)
-             (shui/dialog-close!)
-             (let [value (if-some [value (:logseq.property/scalar-default-value property)]
-                           value
-                           false)]
-               (pv/<add-property! block (:db/ident property) value {:exit-edit? true})))
-
-            (and (contains? #{:default :url} type)
-                 (not (seq (:property/closed-values property))))
-            (pv/<create-new-block! block property "")
+    (let [property (when (uuid? value) (db/entity [:block/uuid value]))
+          batch? (pv/batch-operation?)
+          repo (state/get-current-repo)]
+      (if (and property remove-property?)
+        (let [block-ids (map :block/uuid (pv/get-operating-blocks block))]
+          (property-handler/batch-remove-block-property! repo block-ids (:db/ident property))
+          (shui/popup-hide!))
+        (do
+          (when (and *show-new-property-config? (not (ldb/property? property)))
+            (reset! *show-new-property-config? true))
+          (reset! *property property)
+          (when property
+            (let [add-class-property? (and (ldb/class? block) class-schema?)
+                  type (:logseq.property/type property)
+                  default-or-url? (and (contains? #{:default :url} type)
+                                       (not (seq (:property/closed-values property))))]
+              (cond
+                add-class-property?
+                (p/do!
+                 (pv/<add-property! block (:db/ident property) "" {:class-schema? class-schema?})
+                 (shui/popup-hide!)
+                 (shui/dialog-close!))
+
+                (and batch? (or (= :checkbox type) (and batch? default-or-url?)))
+                nil
+
+                (= :checkbox type)
+                (p/do!
+                 (ui/hide-popups-until-preview-popup!)
+                 (shui/popup-hide!)
+                 (shui/dialog-close!)
+                 (let [value (if-some [value (:logseq.property/scalar-default-value property)]
+                               value
+                               false)]
+                   (pv/<add-property! block (:db/ident property) value {:exit-edit? true})))
+
+                default-or-url?
+                (pv/<create-new-block! block property "" {:batch-op? true})
 
             ;; using class as property
-            (and property (ldb/class? property))
-            (pv/<set-class-as-property! (state/get-current-repo) property)
+                (and property (ldb/class? property))
+                (pv/<set-class-as-property! (state/get-current-repo) property)
 
-            (or (not= :default type)
-                (and (= :default type) (seq (:property/closed-values property))))
-            (reset! *show-new-property-config? false)))))))
+                (or (not= :default type)
+                    (and (= :default type) (seq (:property/closed-values property))))
+                (reset! *show-new-property-config? false)))))))))
 
 (rum/defc property-key-title
   [block property class-schema?]
@@ -263,21 +277,20 @@
                          (route-handler/redirect-to-page! (:block/uuid property))
                          (.preventDefault e)))
     :on-click (fn [^js/MouseEvent e]
-                (if (state/editing?)
-                  (editor-handler/escape-editing {:select? true})
-                  (shui/popup-show! (.-target e)
-                                    (fn []
-                                      (property-config/dropdown-editor property block {:debug? (.-altKey e)
-                                                                                       :class-schema? class-schema?}))
-                                    {:content-props
-                                     {:class "ls-property-dropdown-editor as-root"
-                                      :onEscapeKeyDown (fn [e]
-                                                         (util/stop e)
-                                                         (shui/popup-hide!)
-                                                         (when-let [input (state/get-input)]
-                                                           (.focus input)))}
-                                     :align "start"
-                                     :as-dropdown? true})))}
+                (shui/popup-show! (.-target e)
+                                  (fn []
+                                    (property-config/dropdown-editor property block {:debug? (.-altKey e)
+                                                                                     :class-schema? class-schema?}))
+                                  {:content-props
+                                   {:class "ls-property-dropdown-editor as-root"
+                                    :onEscapeKeyDown (fn [e]
+                                                       (util/stop e)
+                                                       (shui/popup-hide!)
+                                                       (when-let [input (state/get-input)]
+                                                         (.focus input)))}
+                                   :align "start"
+                                   :as-dropdown? true}))}
+
    (:block/title property)))
 
 (rum/defc property-key-cp < rum/static
@@ -318,7 +331,7 @@
         (:block/title property)]
        (property-key-title block property class-schema?))]))
 
-(rum/defcs property-input < rum/reactive
+(rum/defcs ^:large-vars/cleanup-todo property-input < rum/reactive
   (rum/local nil ::ref)
   (rum/local false ::show-new-property-config?)
   (rum/local false ::show-class-select?)
@@ -371,16 +384,24 @@
                                    (and (not= view-context :all) (not (contains? block-types view-context)))
                                    (and (ldb/built-in? block) (contains? #{:logseq.property/parent} (:db/ident m))))))
         property (rum/react *property)
-        property-key (rum/react *property-key)]
+        property-key (rum/react *property-key)
+        batch? (pv/batch-operation?)
+        hide-property-key? (or (contains? #{:date :datetime} (:logseq.property/type property))
+                               (pv/select-type? block property)
+                               (and
+                                batch?
+                                (contains? #{:default :url} (:logseq.property/type property))
+                                (not (seq (:property/closed-values property)))))]
     [:div.ls-property-input.flex.flex-1.flex-row.items-center.flex-wrap.gap-1
      {:ref #(reset! *ref %)}
      (if property-key
        [:div.ls-property-add.gap-1.flex.flex-1.flex-row.items-center
-        [:div.flex.flex-row.items-center.property-key.gap-1
-         (when-not (:db/id property) (property-icon property (:logseq.property/type @*property-schema)))
-         (if (:db/id property)                              ; property exists already
-           (property-key-cp block property opts)
-           [:div property-key])]
+        (when-not hide-property-key?
+          [:div.flex.flex-row.items-center.property-key.gap-1
+           (when-not (:db/id property) (property-icon property (:logseq.property/type @*property-schema)))
+           (if (:db/id property)                              ; property exists already
+             (property-key-cp block property opts)
+             [:div property-key])])
         [:div.flex.flex-row {:on-pointer-down (fn [e] (util/stop-propagation e))}
          (when (not= @*show-new-property-config? :adding-property)
            (cond
@@ -413,8 +434,9 @@
                                       (= "" (.-value (.-target e))))
                              (util/stop e)
                              (shui/popup-hide!)))}]
-         (property-select exclude-properties {:on-chosen on-chosen
-                                              :input-opts input-opts})))]))
+         (property-select exclude-properties
+                          (merge (:select-opts opts) {:on-chosen on-chosen
+                                                      :input-opts input-opts}))))]))
 
 (rum/defcs new-property < rum/reactive
   [state block opts]

+ 79 - 83
src/main/frontend/components/property/config.cljs

@@ -83,58 +83,54 @@
 (rum/defc class-select
   [property {:keys [multiple-choices? disabled? default-open? no-class? on-hide]
              :or {multiple-choices? true}}]
-  (let [*ref (rum/use-ref nil)]
-    (hooks/use-effect!
-     (fn []
-       (when default-open?
-         (some-> (rum/deref *ref)
-                 (.click))))
-     [default-open?])
-    (let [schema-classes (:logseq.property/classes property)]
-      [:div.flex.flex-1.col-span-3
-       (let [content-fn
-             (fn [{:keys [id]}]
-               (let [toggle-fn #(do
-                                  (when (fn? on-hide) (on-hide))
-                                  (shui/popup-hide! id))
-                     classes (model/get-all-readable-classes (state/get-current-repo) {:except-root-class? true})
-                     options (map (fn [class]
-                                    {:label (:block/title class)
-                                     :value (:block/uuid class)})
-                                  classes)
-                     options (if no-class?
-                               (cons {:label "Skip choosing tag"
-                                      :value :no-tag}
-                                     options)
-                               options)
-                     opts {:items options
-                           :input-default-placeholder (if multiple-choices? "Choose tags" "Choose tag")
-                           :dropdown? false
-                           :close-modal? false
-                           :multiple-choices? multiple-choices?
-                           :selected-choices (map :block/uuid schema-classes)
-                           :extract-fn :label
-                           :extract-chosen-fn :value
-                           :show-new-when-not-exact-match? true
-                           :input-opts {:on-key-down
-                                        (fn [e]
-                                          (case (util/ekey e)
-                                            "Escape"
-                                            (do
-                                              (util/stop e)
-                                              (toggle-fn))
-                                            nil))}
-                           :on-chosen (fn [value select?]
-                                        (if (= value :no-tag)
-                                          (toggle-fn)
-                                          (p/let [result (<create-class-if-not-exists! value)
-                                                  value' (or result value)
-                                                  tx-data [[(if select? :db/add :db/retract) (:db/id property) :logseq.property/classes [:block/uuid value']]]
-                                                  _ (db/transact! (state/get-current-repo) tx-data {:outliner-op :update-property})]
-                                            (when-not multiple-choices? (toggle-fn)))))}]
-
-                 (select/select opts)))]
-
+  (let [*ref (rum/use-ref nil)
+        schema-classes (:logseq.property/classes property)]
+    [:div.flex.flex-1.col-span-3
+     (let [content-fn
+           (fn [{:keys [id]}]
+             (let [toggle-fn #(do
+                                (when (fn? on-hide) (on-hide))
+                                (shui/popup-hide! id))
+                   classes (model/get-all-readable-classes (state/get-current-repo) {:except-root-class? true})
+                   options (map (fn [class]
+                                  {:label (:block/title class)
+                                   :value (:block/uuid class)})
+                                classes)
+                   options (if no-class?
+                             (cons {:label "Skip choosing tag"
+                                    :value :no-tag}
+                                   options)
+                             options)
+                   opts {:items options
+                         :input-default-placeholder (if multiple-choices? "Choose tags" "Choose tag")
+                         :dropdown? false
+                         :close-modal? false
+                         :multiple-choices? multiple-choices?
+                         :selected-choices (map :block/uuid schema-classes)
+                         :extract-fn :label
+                         :extract-chosen-fn :value
+                         :show-new-when-not-exact-match? true
+                         :input-opts {:on-key-down
+                                      (fn [e]
+                                        (case (util/ekey e)
+                                          "Escape"
+                                          (do
+                                            (util/stop e)
+                                            (toggle-fn))
+                                          nil))}
+                         :on-chosen (fn [value select?]
+                                      (if (= value :no-tag)
+                                        (toggle-fn)
+                                        (p/let [result (<create-class-if-not-exists! value)
+                                                value' (or result value)
+                                                tx-data [[(if select? :db/add :db/retract) (:db/id property) :logseq.property/classes [:block/uuid value']]]
+                                                _ (db/transact! (state/get-current-repo) tx-data {:outliner-op :update-property})]
+                                          (when-not multiple-choices? (toggle-fn)))))}]
+
+               (select/select opts)))]
+
+       (if default-open?
+         (content-fn nil)
          [:div.flex.flex-1.cursor-pointer
           {:ref *ref
            :on-click (if disabled?
@@ -147,7 +143,7 @@
                [:a.text-sm (str "#" (:block/title class))])
              [:span.opacity-60.pl-1.top-1.relative.hover:opacity-80.active:opacity-60
               (shui/tabler-icon "edit")]]
-            (pv/property-empty-btn-value property))])])))
+            (pv/property-empty-btn-value property))]))]))
 
 (rum/defc name-edit-pane
   [property {:keys [set-sub-open! disabled?]}]
@@ -326,12 +322,12 @@
                                        :button-opts {:title "Set Icon"}})
      [:strong {:on-click (fn [^js e]
                            (shui/popup-show! (.-target e)
-                             (fn [] (choice-base-edit-form property block))
-                             {:id :ls-base-edit-form
-                              :align "start"}))}
+                                             (fn [] (choice-base-edit-form property block))
+                                             {:id :ls-base-edit-form
+                                              :align "start"}))}
       value]
      (shui/dropdown-menu
-       (shui/dropdown-menu-trigger
+      (shui/dropdown-menu-trigger
        {:as-child true
         :disabled disabled?}
        (shui/button
@@ -409,24 +405,24 @@
        [:<>
         [:ul.choices-list
          (dnd/items choice-items
-           {:sort-by-inner-element? false
-            :on-drag-end (fn [_ {:keys [active-id over-id direction]}]
-                           (let [move-down? (= direction :down)
-                                 over (db/entity [:block/uuid (uuid over-id)])
-                                 active (db/entity [:block/uuid (uuid active-id)])
-                                 over-order (:block/order over)
-                                 new-order (if move-down?
-                                             (let [next-order (db-order/get-next-order (db/get-db) property (:db/id over))]
-                                               (db-order/gen-key over-order next-order))
-                                             (let [prev-order (db-order/get-prev-order (db/get-db) property (:db/id over))]
-                                               (db-order/gen-key prev-order over-order)))]
-
-                             (db/transact! (state/get-current-repo)
-                               [{:db/id (:db/id active)
-                                 :block/order new-order}
-                                (outliner-core/block-with-updated-at
-                                  {:db/id (:db/id property)})]
-                               {:outliner-op :save-block})))})]
+                    {:sort-by-inner-element? false
+                     :on-drag-end (fn [_ {:keys [active-id over-id direction]}]
+                                    (let [move-down? (= direction :down)
+                                          over (db/entity [:block/uuid (uuid over-id)])
+                                          active (db/entity [:block/uuid (uuid active-id)])
+                                          over-order (:block/order over)
+                                          new-order (if move-down?
+                                                      (let [next-order (db-order/get-next-order (db/get-db) property (:db/id over))]
+                                                        (db-order/gen-key over-order next-order))
+                                                      (let [prev-order (db-order/get-prev-order (db/get-db) property (:db/id over))]
+                                                        (db-order/gen-key prev-order over-order)))]
+
+                                      (db/transact! (state/get-current-repo)
+                                                    [{:db/id (:db/id active)
+                                                      :block/order new-order}
+                                                     (outliner-core/block-with-updated-at
+                                                      {:db/id (:db/id property)})]
+                                                    {:outliner-op :save-block})))})]
         (shui/dropdown-menu-separator)])
 
      ;; add choice
@@ -463,14 +459,14 @@
   [choices]
   (let [select-cp (fn [opts]
                     (shui/select
-                      opts
-                      (shui/select-trigger
-                        {:class "h-8"}
-                        (shui/select-value {:placeholder "Select a choice"}))
-                      (shui/select-content
-                        (map (fn [choice]
-                               (shui/select-item {:key (str (:db/id choice))
-                                                  :value (:db/id choice)} (:block/title choice))) choices))))
+                     opts
+                     (shui/select-trigger
+                      {:class "h-8"}
+                      (shui/select-value {:placeholder "Select a choice"}))
+                     (shui/select-content
+                      (map (fn [choice]
+                             (shui/select-item {:key (str (:db/id choice))
+                                                :value (:db/id choice)} (:block/title choice))) choices))))
         checked-choice (some (fn [choice] (when (true? (:logseq.property/choice-checkbox-state choice)) choice)) choices)
         unchecked-choice (some (fn [choice] (when (false? (:logseq.property/choice-checkbox-state choice)) choice)) choices)]
     [:div.flex.flex-col.gap-4.text-sm.p-2

+ 13 - 4
src/main/frontend/components/property/dialog.cljs

@@ -1,18 +1,27 @@
 (ns frontend.components.property.dialog
   "Property && value choose"
   (:require [frontend.components.property :as property-component]
-            [rum.core :as rum]
+            [frontend.db :as db]
             [frontend.modules.shortcut.core :as shortcut]
-            [frontend.db :as db]))
+            [frontend.state :as state]
+            [rum.core :as rum]))
 
 (rum/defcs dialog <
   shortcut/disable-all-shortcuts
   (rum/local nil ::property-value)
   {:init (fn [state]
-           (let [k (:property-key (last (:rum/args state)))]
+           (let [opts (last (:rum/args state))
+                 k (:property-key opts)]
+             (when-let [view-selected-blocks (:selected-blocks opts)]
+               (state/set-state! :view/selected-blocks view-selected-blocks))
              (assoc state
                     ::property-key (atom k)
-                    ::property (atom (when k (db/get-case-page k))))))}
+                    ::property (atom (when k (db/get-case-page k))))))
+   :will-unmount (fn [state]
+                   (when-let [close-fn (:on-dialog-close (last (:rum/args state)))]
+                     (close-fn))
+                   (state/set-state! :view/selected-blocks nil)
+                   state)}
   [state blocks opts]
   (when (seq blocks)
     (let [*property-key (::property-key state)

+ 190 - 186
src/main/frontend/components/property/value.cljs

@@ -18,6 +18,7 @@
             [frontend.handler.db-based.page :as db-page-handler]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.notification :as notification]
             [frontend.handler.page :as page-handler]
             [frontend.handler.property :as property-handler]
             [frontend.handler.property.util :as pu]
@@ -64,6 +65,29 @@
        (ui/icon "line-dashed"))
      "Empty")])
 
+(defn- get-selected-blocks
+  []
+  (some->> (state/get-selection-block-ids)
+           (map (fn [id] (db/entity [:block/uuid id])))
+           (seq)
+           block-handler/get-top-level-blocks
+           (remove ldb/property?)))
+
+(defn get-operating-blocks
+  [block]
+  (let [selected-blocks (get-selected-blocks)
+        view-selected-blocks (:view/selected-blocks @state/state)]
+    (or (seq selected-blocks)
+        (seq view-selected-blocks)
+        [block])))
+
+(defn batch-operation?
+  []
+  (let [selected-blocks (get-selected-blocks)
+        view-selected-blocks (:view/selected-blocks @state/state)]
+    (or (> (count selected-blocks) 1)
+        (seq view-selected-blocks))))
+
 (rum/defc icon-row
   [block editing?]
   (let [icon-value (:logseq.property/icon block)
@@ -71,14 +95,13 @@
                          (shui/dialog-close!)
                          (shui/popup-hide-all!))
         on-chosen! (fn [_e icon]
-                     (if icon
-                       (db-property-handler/set-block-property!
-                        (:db/id block)
+                     (let [repo (state/get-current-repo)
+                           blocks (get-operating-blocks block)]
+                       (property-handler/batch-set-block-property!
+                        repo
+                        (map :db/id blocks)
                         :logseq.property/icon
-                        (select-keys icon [:type :id :color]))
-                       (db-property-handler/remove-block-property!
-                        (:db/id block)
-                        :logseq.property/icon))
+                        (when icon (select-keys icon [:type :id :color]))))
                      (clear-overlay!)
                      (when editing?
                        (editor-handler/restore-last-saved-cursor!)))]
@@ -96,14 +119,14 @@
                                             (.querySelector ".block-main-container"))]
                 (state/set-editor-action! :property-icon-picker)
                 (shui/popup-show! target
-                  #(icon-component/icon-search
-                     {:on-chosen on-chosen!
-                      :icon-value icon
-                      :del-btn? (some? icon)})
-                  {:id :ls-icon-picker
-                   :on-after-hide #(state/set-editor-action! nil)
-                   :content-props {:onEscapeKeyDown #(when editing? (editor-handler/restore-last-saved-cursor!))}
-                   :align :start})))))))
+                                  #(icon-component/icon-search
+                                    {:on-chosen on-chosen!
+                                     :icon-value icon
+                                     :del-btn? (some? icon)})
+                                  {:id :ls-icon-picker
+                                   :on-after-hide #(state/set-editor-action! nil)
+                                   :content-props {:onEscapeKeyDown #(when editing? (editor-handler/restore-last-saved-cursor!))}
+                                   :align :start})))))))
      [editing?])
 
     [:div.col-span-3.flex.flex-row.items-center.gap-2
@@ -112,22 +135,14 @@
                                   :del-btn? (some? icon-value)
                                   :on-chosen on-chosen!})]))
 
-(defn- select-type?
-  [block property type]
-  (or (contains? #{:node :number :date :page :class :property} type)
-      ;; closed values
-      (seq (:property/closed-values property))
-      (and (= (:db/ident property) :logseq.property/default-value)
-           (= (:logseq.property/type block) :number))))
-
-(defn- get-operating-blocks
-  [block]
-  (let [selected-blocks (some->> (state/get-selection-block-ids)
-                                 (map (fn [id] (db/entity [:block/uuid id])))
-                                 (seq)
-                                 block-handler/get-top-level-blocks
-                                 (remove ldb/property?))]
-    (or (seq selected-blocks) [block])))
+(defn select-type?
+  [block property]
+  (let [type (:logseq.property/type property)]
+    (or (contains? #{:node :number :date :page :class :property} type)
+       ;; closed values
+        (seq (:property/closed-values property))
+        (and (= (:db/ident property) :logseq.property/default-value)
+             (= (:logseq.property/type block) :number)))))
 
 (defn <create-new-block!
   [block property value & {:keys [edit-block? batch-op?]
@@ -139,22 +154,17 @@
   (let [<create-block (fn [block]
                         (if (and (contains? #{:default :url} (:logseq.property/type property))
                                  (not (db-property/many? property)))
-                          (p/let [existing-value (get block (:db/ident property))
-                                  default-value (:logseq.property/default-value property)
-                                  existing-value? (and (some? existing-value)
-                                                       (not= (:db/ident existing-value) :logseq.property/empty-placeholder)
-                                                       (not= (:db/id existing-value) (:db/id default-value)))
-                                  new-block-id (when-not existing-value? (db/new-block-id))
-                                  _ (when-not existing-value?
-                                      (let [value' (if (and default-value (string? value) (string/blank? value))
-                                                     (db-property/property-value-content default-value)
-                                                     value)]
-                                        (db-property-handler/create-property-text-block!
-                                         (:db/id block)
-                                         (:db/id property)
-                                         value'
-                                         {:new-block-id new-block-id})))]
-                            (if existing-value? existing-value (db/entity [:block/uuid new-block-id])))
+                          (p/let [default-value (:logseq.property/default-value property)
+                                  new-block-id (db/new-block-id)
+                                  _ (let [value' (if (and default-value (string? value) (string/blank? value))
+                                                   (db-property/property-value-content default-value)
+                                                   value)]
+                                      (db-property-handler/create-property-text-block!
+                                       (:db/id block)
+                                       (:db/id property)
+                                       value'
+                                       {:new-block-id new-block-id}))]
+                            (db/entity [:block/uuid new-block-id]))
                           (p/let [new-block-id (db/new-block-id)
                                   _ (db-property-handler/create-property-text-block!
                                      (:db/id block)
@@ -187,7 +197,7 @@
 (defn <add-property!
   "If a class and in a class schema context, add the property to its schema.
   Otherwise, add a block's property and its value"
-  ([block property-key property-value] (<add-property! block property-key property-value {}))
+  ([block property-id property-value] (<add-property! block property-id property-value {}))
   ([block property-id property-value {:keys [selected? exit-edit? class-schema?]
                                       :or {exit-edit? true}}]
    (let [repo (state/get-current-repo)
@@ -204,20 +214,17 @@
            (<set-class-as-property! repo property))
          (db-property-handler/class-add-property! (:db/id block) property-id))
         (let [block-ids (map :block/uuid blocks)]
-          (if (and (db-property-type/all-ref-property-types (:logseq.property/type property))
-                   (string? property-value))
-            (p/let [new-block (<create-new-block! block (db/entity property-id) property-value {:edit-block? false})]
-              (when (seq (remove #{(:db/id block)} (map :db/id block)))
-                (property-handler/batch-set-block-property! repo block-ids property-id (:db/id new-block)))
-              new-block)
-            (property-handler/batch-set-block-property! repo block-ids property-id property-value))))
-      (cond
-        exit-edit?
-        (do
-          (ui/hide-popups-until-preview-popup!)
-          (shui/dialog-close!))
-        selected?
-        (shui/popup-hide!))
+          (property-handler/batch-set-block-property! repo block-ids property-id property-value)))
+      (when (seq (:view/selected-blocks @state/state))
+        (notification/show! "Property updated!" :success))
+      (when-not many?
+        (cond
+          exit-edit?
+          (do
+            (ui/hide-popups-until-preview-popup!)
+            (shui/dialog-close!))
+          selected?
+          (shui/popup-hide!)))
       (when-not (or many? checkbox?)
         (when-let [input (state/get-input)]
           (.focus input)))
@@ -439,8 +446,7 @@
 
 (rum/defc date-picker
   [value {:keys [block property datetime? on-change on-delete del-btn? editing? multiple-values? other-position?]}]
-  (let [*trigger-ref (rum/use-ref nil)
-        content-fn (fn [{:keys [id]}] (calendar-inner id
+  (let [content-fn (fn [{:keys [id]}] (calendar-inner id
                                                       {:block block
                                                        :property property
                                                        :on-change on-change
@@ -456,52 +462,44 @@
                           (shui/popup-show! (.-target e) content-fn
                                             {:align "start" :auto-focus? true}))))
         repeated-task? (:logseq.task/repeated? block)]
-    (hooks/use-effect!
-     (fn []
-       (when editing?
-         (js/setTimeout
-          #(some-> (rum/deref *trigger-ref)
-                   (.click)) 32)))
-     [editing?])
+    (if editing?
+      (content-fn {:id :date-picker})
+      (if multiple-values?
+        (shui/button
+         {:class "jtrigger h-6 empty-btn"
+          :variant :text
+          :size :sm
+          :on-click open-popup!}
+         (ui/icon "calendar-plus" {:size 16}))
+        (shui/trigger-as
+         :div.flex.flex-1.flex-row.gap-1.items-center.flex-wrap
+         {:tabIndex 0
+          :class "jtrigger min-h-[24px]"                     ; FIXME: min-h-6 not works
+          :on-click open-popup!}
+         [:div.flex.flex-row.gap-1.items-center
+          (when repeated-task?
+            (ui/icon "repeat" {:size 14 :class "opacity-40"}))
+          (cond
+            (map? value)
+            (let [date (tc/to-date-time (date/journal-day->utc-ms (:block/journal-day value)))
+                  compare-value (some-> date
+                                        (t/plus (t/days 1))
+                                        (t/minus (t/seconds 1)))
+                  content (when-let [page-cp (state/get-component :block/page-cp)]
+                            (rum/with-key
+                              (page-cp {:disable-preview? true
+                                        :meta-click? other-position?
+                                        :label (human-date-label (t/to-default-time-zone date))} value)
+                              (:db/id value)))]
+              (if (or repeated-task? (contains? #{:logseq.task/deadline :logseq.task/scheduled} (:db/id property)))
+                (overdue compare-value content)
+                content))
+
+            (number? value)
+            (datetime-value value (:db/ident property) repeated-task?)
 
-    (if multiple-values?
-      (shui/button
-       {:class "jtrigger h-6 empty-btn"
-        :ref *trigger-ref
-        :variant :text
-        :size :sm
-        :on-click open-popup!}
-       (ui/icon "calendar-plus" {:size 16}))
-      (shui/trigger-as
-       :div.flex.flex-1.flex-row.gap-1.items-center.flex-wrap
-       {:tabIndex 0
-        :class "jtrigger min-h-[24px]"                     ; FIXME: min-h-6 not works
-        :ref *trigger-ref
-        :on-click open-popup!}
-       [:div.flex.flex-row.gap-1.items-center
-        (when repeated-task?
-          (ui/icon "repeat" {:size 14 :class "opacity-40"}))
-        (cond
-          (map? value)
-          (let [date (tc/to-date-time (date/journal-day->utc-ms (:block/journal-day value)))
-                compare-value (some-> date
-                                      (t/plus (t/days 1))
-                                      (t/minus (t/seconds 1)))
-                content (when-let [page-cp (state/get-component :block/page-cp)]
-                          (rum/with-key
-                            (page-cp {:disable-preview? true
-                                      :meta-click? other-position?
-                                      :label (human-date-label (t/to-default-time-zone date))} value)
-                            (:db/id value)))]
-            (if (or repeated-task? (contains? #{:logseq.task/deadline :logseq.task/scheduled} (:db/id property)))
-              (overdue compare-value content)
-              content))
-
-          (number? value)
-          (datetime-value value (:db/ident property) repeated-task?)
-
-          :else
-          (property-empty-btn-value nil))]))))
+            :else
+            (property-empty-btn-value nil))])))))
 
 (rum/defc property-value-date-picker
   [block property value opts]
@@ -578,8 +576,8 @@
                               (remove nil?)
                               (remove #(= :logseq.property/empty-placeholder %)))
         clear-value (str "No " (:block/title property))
-        clear-value-label [:div.flex.flex-row.items-center.gap-2
-                           (ui/icon "x")
+        clear-value-label [:div.flex.flex-row.items-center.gap-1.text-sm
+                           (ui/icon "x" {:size 14})
                            [:div clear-value]]
         items' (->>
                 (if (and (seq selected-choices)
@@ -685,10 +683,17 @@
                            (and alias? (= (or (:db/id (:block/page block))
                                               (:db/id block))
                                           (:db/id node)))
-                           (when (and property-type (not= property-type :node))
+                           (cond
+                             (= property-type :class)
+                             (ldb/private-tags (:db/ident node))
+
+                             (and property-type (not= property-type :node))
                              (if (= property-type :page)
                                (not (db/page? node))
-                               (not (contains? (ldb/get-entity-types node) property-type))))))
+                               (not (contains? (ldb/get-entity-types node) property-type)))
+
+                             :else
+                             false)))
                      result))))
 
         options (map (fn [node]
@@ -731,13 +736,11 @@
                                               "Set tags"
                                               alias?
                                               "Set alias"
-                                              multiple-choices?
-                                              "Choose nodes"
                                               :else
-                                              "Choose node")
+                                              (str "Set " (:block/title property)))
                  :show-new-when-not-exact-match? (if (or (and parent-property? (contains? (set children-pages) (:db/id block)))
                                                          ;; Don't allow creating private tags
-                                                         (seq (set/intersection (set (map :db/ident classes))
+                                                         (seq (set/intersection (set (map :db/ident classes'))
                                                                                 ldb/private-tags)))
                                                    false
                                                    true)
@@ -801,7 +804,10 @@
                                      (set-result! result)))))
         repo (state/get-current-repo)
         classes (:logseq.property/classes property)
-        non-root-classes (remove (fn [c] (= (:db/ident c) :logseq.class/Root)) classes)
+        class? (= :class (:logseq.property/type property))
+        non-root-classes (cond-> (remove (fn [c] (= (:db/ident c) :logseq.class/Root)) classes)
+                           class?
+                           (conj (frontend.db/entity :logseq.class/Tag)))
         parent-property? (= (:db/ident property) :logseq.property/parent)]
     (when (and (not parent-property?) (seq non-root-classes))
       ;; effect runs once
@@ -891,7 +897,7 @@
                      :selected-choices selected-choices
                      :dropdown? dropdown?
                      :show-new-when-not-exact-match? (not (or closed-values? (= :date type)))
-                     :input-default-placeholder "Select"
+                     :input-default-placeholder (str "Set " (:block/title property))
                      :extract-chosen-fn :value
                      :extract-fn (fn [x] (or (:label-value x) (:label x)))
                      :content-props content-props
@@ -1044,49 +1050,46 @@
        (inline-text-cp (str value)))]))
 
 (rum/defc single-value-select
-  [block property value value-f select-opts opts]
-  (let [*el (rum/use-ref nil)]
-    ;; Open popover initially when editing a property
-    (hooks/use-effect!
-     (fn []
-       (when (:editing? opts)
-         (.click (rum/deref *el))))
-     [(:editing? opts)])
-    (let [type (:logseq.property/type property)
-          select-opts' (assoc select-opts :multiple-choices? false)
-          popup-content (fn content-fn [_]
-                          [:div.property-select
-                           (case type
-                             (:entity :number :default :url)
-                             (select block property select-opts' opts)
-
-                             (:node :class :property :page :date)
-                             (property-value-select-node block property select-opts' opts))])
-          trigger-id (str "trigger-" (:container-id opts) "-" (:db/id block) "-" (:db/id property))
-          show! (fn [e]
-                  (let [target (.-target e)]
-                    (when-not (or config/publishing?
-                                  (util/shift-key? e)
-                                  (util/meta-key? e)
-                                  (util/link? target)
-                                  (when-let [node (.closest target "a")]
-                                    (not (or (d/has-class? node "page-ref")
-                                             (d/has-class? node "tag")))))
-
-                      (shui/popup-show! target popup-content
-                                        {:align "start"
-                                         :as-dropdown? true
-                                         :auto-focus? true
-                                         :trigger-id trigger-id}))))]
-      (shui/trigger-as
-       (if (:other-position? opts) :div.jtrigger :div.jtrigger.flex.flex-1.w-full)
-       {:ref *el
-        :id trigger-id
-        :tabIndex 0
-        :on-click show!}
-       (if (string/blank? value)
-         (property-empty-text-value property opts)
-         (value-f))))))
+  [block property value select-opts {:keys [value-render] :as opts}]
+  (let [*el (rum/use-ref nil)
+        editing? (:editing? opts)
+        type (:logseq.property/type property)
+        select-opts' (assoc select-opts :multiple-choices? false)
+        popup-content (fn content-fn [_]
+                        [:div.property-select
+                         (case type
+                           (:entity :number :default :url)
+                           (select block property select-opts' opts)
+
+                           (:node :class :property :page :date)
+                           (property-value-select-node block property select-opts' opts))])]
+    (if editing?
+      (popup-content nil)
+      (let [trigger-id (str "trigger-" (:container-id opts) "-" (:db/id block) "-" (:db/id property))
+            show! (fn [e]
+                    (let [target (.-target e)]
+                      (when-not (or config/publishing?
+                                    (util/shift-key? e)
+                                    (util/meta-key? e)
+                                    (util/link? target)
+                                    (when-let [node (.closest target "a")]
+                                      (not (or (d/has-class? node "page-ref")
+                                               (d/has-class? node "tag")))))
+
+                        (shui/popup-show! target popup-content
+                                          {:align "start"
+                                           :as-dropdown? true
+                                           :auto-focus? true
+                                           :trigger-id trigger-id}))))]
+        (shui/trigger-as
+         (if (:other-position? opts) :div.jtrigger :div.jtrigger.flex.flex-1.w-full)
+         {:ref *el
+          :id trigger-id
+          :tabIndex 0
+          :on-click show!}
+         (if (string/blank? value)
+           (property-empty-text-value property opts)
+           (value-render)))))))
 
 (defn- property-value-inner
   [block property value {:keys [inline-text page-cp
@@ -1125,8 +1128,10 @@
         editing? (or editing?
                      (and (state/sub-editing? [container-id (:block/uuid block)])
                           (= (:db/id property) (:db/id (:property (state/get-editor-action-data))))))
-        select-type?' (select-type? block property type)
+        batch? (batch-operation?)
         closed-values? (seq (:property/closed-values property))
+        select-type?' (or (select-type? block property)
+                          (and editing? batch? (contains? #{:default :url} type) (not closed-values?)))
         select-opts {:on-chosen on-chosen}
         value (if (and (de/entity? value*) (= (:db/ident value*) :logseq.property/empty-placeholder))
                 nil
@@ -1154,9 +1159,10 @@
                                                      (when choice
                                                        (db-property-handler/set-block-property! (:db/id block) (:db/ident property) (:db/id choice)))))}))
             (single-value-select block property value
-                                 (fn [] (select-item property type value opts))
                                  select-opts
-                                 (assoc opts :editing? editing?))))
+                                 (assoc opts
+                                        :editing? editing?
+                                        :value-render (fn [] (select-item property type value opts))))))
         (case type
           (:date :datetime)
           (property-value-date-picker block property value (merge opts {:editing? editing?}))
@@ -1187,24 +1193,22 @@
         *el (rum/use-ref nil)
         items (cond->> (if (de/entity? v) #{v} v)
                 (= (:db/ident property) :block/tags)
-                (remove (fn [v] (contains? ldb/hidden-tags (:db/ident v)))))]
-    (hooks/use-effect!
-     (fn []
-       (when editing?
-         (.click (rum/deref *el))))
-     [editing?])
-    (let [select-cp (fn [select-opts]
-                      (let [select-opts (merge {:multiple-choices? true
-                                                :on-chosen (fn []
-                                                             (when on-chosen (on-chosen)))}
-                                               select-opts
-                                               {:dropdown? false})]
-                        [:div.property-select
-                         (if (contains? #{:node :page :class :property} type)
-                           (property-value-select-node block property
-                                                       select-opts
-                                                       opts)
-                           (select block property select-opts opts))]))]
+                (remove (fn [v] (contains? ldb/hidden-tags (:db/ident v)))))
+        select-cp (fn [select-opts]
+                    (let [select-opts (merge {:multiple-choices? true
+                                              :on-chosen (fn []
+                                                           (when on-chosen (on-chosen)))}
+                                             select-opts
+                                             (when-not editing?
+                                               {:dropdown? false}))]
+                      [:div.property-select
+                       (if (contains? #{:node :page :class :property} type)
+                         (property-value-select-node block property
+                                                     select-opts
+                                                     opts)
+                         (select block property select-opts opts))]))]
+    (if editing?
+      (select-cp {})
       (let [toggle-fn shui/popup-hide!
             content-fn (fn [{:keys [_id content-props]}]
                          (select-cp {:content-props content-props}))]
@@ -1223,7 +1227,7 @@
                            (do (some-> (rum/deref *el) (.click))
                                (util/stop e))
                            :dune))
-          :class "flex flex-1 flex-row items-center flex-wrap gap-x-2 gap-y-2"}
+          :class "flex flex-1 flex-row items-center flex-wrap gap-1"}
          (let [not-empty-value? (not= (map :db/ident items) [:logseq.property/empty-placeholder])]
            (if (and (seq items) not-empty-value?)
              (concat
@@ -1245,7 +1249,7 @@
     (multiple-values-inner block property value' opts)))
 
 (rum/defcs property-value < rum/reactive db-mixins/query
-  [state block property {:keys [show-tooltip? p-block p-property]
+  [state block property {:keys [show-tooltip? p-block p-property editing?]
                          :as opts}]
   (ui/catch-error
    (ui/block-error "Something wrong" {})
@@ -1303,7 +1307,7 @@
                          (properties-cp {} block {:selected? false
                                                   :class-schema? true})
 
-                         (and multiple-values? (contains? #{:default :url} type) (not closed-values?))
+                         (and multiple-values? (contains? #{:default :url} type) (not closed-values?) (not editing?))
                          (property-normal-block-value block property v)
 
                          multiple-values?

+ 4 - 0
src/main/frontend/components/property/value.css

@@ -27,3 +27,7 @@
     @apply text-yellow-rx-08 dark:text-yellow-rx-10;
   }
 }
+
+.ls-property-key .cp__select-input, .property-select .cp__select-input {
+    @apply text-sm;
+}

+ 10 - 8
src/main/frontend/components/select.cljs

@@ -3,20 +3,20 @@
   select-config to add a new use or select-type for this component. To use the
   new select-type, create an event that calls `select/dialog-select!` with the
   select-type. See the :graph/open command for a full example."
-  (:require [frontend.modules.shortcut.core :as shortcut]
+  (:require [clojure.string :as string]
+            [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
+            [frontend.handler.common.developer :as dev-common-handler]
+            [frontend.handler.repo :as repo-handler]
+            [frontend.modules.shortcut.core :as shortcut]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.ui :as ui]
-            [logseq.shui.ui :as shui]
             [frontend.util :as util]
             [frontend.util.text :as text-util]
-            [rum.core :as rum]
-            [frontend.config :as config]
-            [frontend.handler.repo :as repo-handler]
-            [frontend.handler.common.developer :as dev-common-handler]
+            [logseq.shui.ui :as shui]
             [reitit.frontend.easy :as rfe]
-            [clojure.string :as string]))
+            [rum.core :as rum]))
 
 (rum/defc render-item < rum/reactive
   [result chosen? multiple-choices? *selected-choices]
@@ -24,7 +24,9 @@
                                     (:value result)) result)
         header (:header result)
         selected-choices (rum/react *selected-choices)
-        row [:div.flex.flex-row.justify-between.w-full {:class (when chosen? "chosen")}
+        row [:div.flex.flex-row.justify-between.w-full
+             {:class (when chosen? "chosen")
+              :on-pointer-down util/stop-propagation}
              [:div.flex.flex-row.items-center.gap-1
               (when multiple-choices?
                 (ui/checkbox {:checked (boolean (selected-choices (:value result)))

+ 87 - 0
src/main/frontend/components/selection.cljs

@@ -0,0 +1,87 @@
+(ns frontend.components.selection
+  "Block selection"
+  (:require [frontend.config :as config]
+            [frontend.handler.editor :as editor-handler]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [logseq.shui.ui :as shui]
+            [rum.core :as rum]))
+
+(rum/defc action-bar
+  [& {:keys [on-cut on-copy selected-blocks hide-dots? button-border?]
+      :or {on-cut #(editor-handler/cut-selection-blocks true)}}]
+  (let [on-copy (if (and selected-blocks (nil? on-copy))
+                  #(editor-handler/copy-selection-blocks true {:selected-blocks selected-blocks
+                                                               :page-title-only? true})
+                  (or on-copy #(editor-handler/copy-selection-blocks true)))
+        button-opts {:variant :outline
+                     :size :sm
+                     :class (str "p-2 text-xs h-8"
+                                 (when-not button-border?
+                                   " !border-b-0"))}
+        db-graph? (config/db-based-graph?)]
+    [:div.selection-action-bar
+     (shui/button-group
+      ;; set tag
+      (when db-graph?
+        (shui/button
+         (assoc button-opts
+                :on-pointer-down (fn [e]
+                                   (util/stop e)
+                                   (state/pub-event! [:editor/new-property {:target (.-target e)
+                                                                            :selected-blocks selected-blocks
+                                                                            :property-key "Tags"
+                                                                            :on-dialog-close #(state/pub-event! [:editor/hide-action-bar])}])))
+         (ui/tooltip (ui/icon "hash" {:size 13}) "Set tag"
+                     {:trigger-props {:class "flex"}})))
+      (shui/button
+       (assoc button-opts
+              :on-pointer-down (fn [e]
+                                 (util/stop e)
+                                 (on-copy)
+                                 (state/pub-event! [:editor/hide-action-bar])))
+       "Copy")
+      (when db-graph?
+        (shui/button
+         (assoc button-opts
+                :on-pointer-down (fn [e]
+                                   (util/stop e)
+                                   (state/pub-event! [:editor/new-property {:target (.-target e)
+                                                                            :selected-blocks selected-blocks
+                                                                            :on-dialog-close #(state/pub-event! [:editor/hide-action-bar])}])))
+         "Set property"))
+      (when db-graph?
+        (shui/button
+         (assoc button-opts
+                :on-pointer-down (fn [e]
+                                   (util/stop e)
+                                   (state/pub-event! [:editor/new-property {:target (.-target e)
+                                                                            :selected-blocks selected-blocks
+                                                                            :remove-property? true
+                                                                            :select-opts {:show-new-when-not-exact-match? false}
+                                                                            :on-dialog-close #(state/pub-event! [:editor/hide-action-bar])}])))
+         "Unset property"))
+      (shui/button
+       (assoc button-opts
+              :on-pointer-down (fn [e]
+                                 (util/stop e)
+                                 (on-cut)
+                                 (state/pub-event! [:editor/hide-action-bar])))
+       (ui/icon "trash" {:size 13}))
+      (when-not hide-dots?
+        (shui/button
+         (assoc button-opts
+                :on-pointer-down (fn [e]
+                                   (util/stop e)
+                                   (shui/popup-hide!)
+                                   (shui/popup-show! e
+                                                     (fn [{:keys [id]}]
+                                                       [:div {:on-click #(shui/popup-hide! id)
+                                                              :data-keep-selection true}
+                                                        ((state/get-component :selection/context-menu))])
+                                                     {:on-before-hide state/dom-clear-selection!
+                                                      :on-after-hide state/state-clear-selection!
+                                                      :content-props {:class "w-[280px] ls-context-menu-content"}
+                                                      :as-dropdown? true})))
+         (ui/icon "dots" {:size 13}))))]))

+ 7 - 12
src/main/frontend/components/views.cljs

@@ -13,6 +13,7 @@
             [frontend.components.property.config :as property-config]
             [frontend.components.property.value :as pv]
             [frontend.components.select :as select]
+            [frontend.components.selection :as selection]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
@@ -362,18 +363,12 @@
   [table selected-rows {:keys [on-delete-rows]}]
   (shui/table-actions
    {}
-   (shui/button
-    {:variant "ghost"
-     :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
-     :disabled true}
-    (str (count selected-rows) " selected"))
-   (when (fn? on-delete-rows)
-     (shui/button
-      {:variant "ghost"
-       :class "h-8 !pl-0 !px-2 !py-0 text-muted-foreground hover:text-foreground w-full justify-start"
-       :on-click (fn []
-                   (on-delete-rows table selected-rows))}
-      (ui/icon "trash")))))
+   [:div (str (count selected-rows) " selected")]
+   (selection/action-bar
+    {:on-cut #(on-delete-rows table selected-rows)
+     :selected-blocks selected-rows
+     :hide-dots? true
+     :button-border? true})))
 
 (rum/defc column-resizer
   [_column on-sized!]

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

@@ -5,6 +5,7 @@
             [electron.ipc :as ipc]
             [electron.listener :as el]
             [frontend.components.block :as block]
+            [frontend.components.content :as cp-content]
             [frontend.components.editor :as editor]
             [frontend.components.page :as page]
             [frontend.components.reference :as reference]
@@ -16,8 +17,8 @@
             [frontend.error :as error]
             [frontend.handler.command-palette :as command-palette]
             [frontend.handler.events :as events]
-            [frontend.handler.file-based.file :as file-handler]
             [frontend.handler.file-based.events]
+            [frontend.handler.file-based.file :as file-handler]
             [frontend.handler.global-config :as global-config-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.page :as page-handler]
@@ -127,6 +128,7 @@
   (state/set-component! :block/inline-text block/inline-text)
   (state/set-component! :block/asset-cp block/asset-cp)
   (state/set-component! :editor/box editor/box)
+  (state/set-component! :selection/context-menu cp-content/custom-context-menu-content)
   (command-palette/register-global-shortcut-commands))
 
 (defn- get-system-info

+ 5 - 5
src/main/frontend/handler/common/page.cljs

@@ -71,13 +71,13 @@
                    result (ui-outliner-tx/transact!
                            {:outliner-op :create-page}
                            (outliner-op/create-page! title' options'))
-                   [_page-name page-uuid] (ldb/read-transit-str result)]
+                   [_page-name page-uuid] (ldb/read-transit-str result)
+                   page (db/get-page (or page-uuid title'))]
              (when redirect?
                (route-handler/redirect-to-page! page-uuid)
-               (let [page (db/get-page (or page-uuid title'))]
-                 (when-let [first-block (ldb/get-first-child @conn (:db/id page))]
-                   (block-handler/edit-block! first-block :max {:container-id :unknown-container}))
-                 page)))))))))
+               (when-let [first-block (ldb/get-first-child @conn (:db/id page))]
+                 (block-handler/edit-block! first-block :max {:container-id :unknown-container})))
+             page)))))))
 
 ;; favorite fns
 ;; ============

+ 56 - 34
src/main/frontend/handler/editor.cljs

@@ -924,14 +924,16 @@
   (block-handler/select-block! block-uuid))
 
 (defn- compose-copied-blocks-contents
-  [repo block-ids]
+  [repo block-ids & {:as opts}]
   (let [blocks (db-utils/pull-many repo '[*] (mapv (fn [id] [:block/uuid id]) block-ids))
         top-level-block-uuids (->> (block-handler/get-top-level-blocks blocks)
                                    (map :block/uuid))
         content (export-text/export-blocks-as-markdown
                  repo top-level-block-uuids
-                 {:indent-style (state/get-export-block-text-indent-style)
-                  :remove-options (set (state/get-export-block-text-remove-options))})]
+                 (merge
+                  opts
+                  {:indent-style (state/get-export-block-text-indent-style)
+                   :remove-options (set (state/get-export-block-text-remove-options))}))]
     [top-level-block-uuids content]))
 
 (defn- get-all-blocks-by-ids
@@ -947,35 +949,37 @@
       result)))
 
 (defn copy-selection-blocks
-  [html?]
-  (when-let [blocks (seq (state/get-selection-blocks))]
-    (let [repo (state/get-current-repo)
-          ids (distinct (keep #(when-let [id (dom/attr % "blockid")]
+  [html? & {:keys [selected-blocks] :as opts}]
+  (let [repo (state/get-current-repo)
+        blocks (seq (state/get-selection-blocks))
+        ids (if blocks
+              (distinct (keep #(when-let [id (dom/attr % "blockid")]
                                  (uuid id)) blocks))
-          [top-level-block-uuids content] (compose-copied-blocks-contents repo ids)
-          block (db/entity [:block/uuid (first ids)])
-          db-based? (config/db-based-graph? repo)]
-      (when block
-        (let [html (export-html/export-blocks-as-html repo top-level-block-uuids nil)
-              copied-blocks (cond->> (get-all-blocks-by-ids repo top-level-block-uuids)
-                              db-based?
-                              (map (fn [block]
-                                     (let [b (db/entity (:db/id block))]
-                                       (->
-                                        (->> (map (fn [[k v]]
-                                                    (let [v' (cond
-                                                               (and (map? v) (:db/id v))
-                                                               [:block/uuid (:block/uuid (db/entity (:db/id v)))]
-                                                               (and (coll? v) (every? #(and (map? %) (:db/id %)) v))
-                                                               (set (map (fn [i] [:block/uuid (:block/uuid (db/entity (:db/id i)))]) v))
-                                                               :else
-                                                               v)]
-                                                      [k v'])) b)
-                                             (into {}))
-                                        (assoc :db/id (:db/id b)))))))]
-          (common-handler/copy-to-clipboard-without-id-property! repo (get block :block/format :markdown) content (when html? html) copied-blocks))
-        (state/set-block-op-type! :copy)
-        (notification/show! "Copied!" :success)))))
+              (map :block/uuid selected-blocks))
+        [top-level-block-uuids content] (compose-copied-blocks-contents repo ids opts)
+        block (db/entity [:block/uuid (first ids)])
+        db-based? (config/db-based-graph? repo)]
+    (when block
+      (let [html (export-html/export-blocks-as-html repo top-level-block-uuids nil)
+            copied-blocks (cond->> (get-all-blocks-by-ids repo top-level-block-uuids)
+                            db-based?
+                            (map (fn [block]
+                                   (let [b (db/entity (:db/id block))]
+                                     (->
+                                      (->> (map (fn [[k v]]
+                                                  (let [v' (cond
+                                                             (and (map? v) (:db/id v))
+                                                             [:block/uuid (:block/uuid (db/entity (:db/id v)))]
+                                                             (and (coll? v) (every? #(and (map? %) (:db/id %)) v))
+                                                             (set (map (fn [i] [:block/uuid (:block/uuid (db/entity (:db/id i)))]) v))
+                                                             :else
+                                                             v)]
+                                                    [k v'])) b)
+                                           (into {}))
+                                      (assoc :db/id (:db/id b)))))))]
+        (common-handler/copy-to-clipboard-without-id-property! repo (get block :block/format :markdown) content (when html? html) copied-blocks))
+      (state/set-block-op-type! :copy)
+      (notification/show! "Copied!" :success))))
 
 (defn copy-block-refs
   []
@@ -1236,6 +1240,18 @@
                     (state/conj-selection-block! blocks direction)))
               (state/exit-editing-and-set-selected-blocks! blocks direction))))))))
 
+(defonce *action-bar-timeout (atom nil))
+
+(defn show-action-bar!
+  [& {:keys [delay]
+      :or {delay 200}}]
+  (when (config/db-based-graph?)
+    (when-let [timeout @*action-bar-timeout]
+      (js/clearTimeout timeout))
+    (state/pub-event! [:editor/hide-action-bar])
+    (let [timeout (js/setTimeout #(state/pub-event! [:editor/show-action-bar]) delay)]
+      (reset! *action-bar-timeout timeout))))
+
 (defn- select-block-up-down
   [direction]
   (cond
@@ -1273,6 +1289,7 @@
       (when element
         (util/scroll-to-block element)
         (state/drop-last-selection-block!))))
+  (show-action-bar! {:delay 500})
   nil)
 
 (defn on-select-block
@@ -2643,7 +2660,7 @@
           (keydown-new-line))))))
 
 (defn- select-first-last
-  "Select first or last block in viewpoint"
+  "Select first or last block in viewport"
   [direction]
   (let [f (case direction :up last :down first)
         container (if (some-> js/document.activeElement
@@ -3380,6 +3397,7 @@
 
 (defn shortcut-up-down [direction]
   (fn [e]
+    (state/pub-event! [:editor/hide-action-bar])
     (when (and (not (auto-complete?))
                (or (in-page-preview?)
                    (not (in-shui-popup?)))
@@ -3419,10 +3437,14 @@
           (cursor/select-up-down input direction anchor cursor-rect)))
       (select-block-up-down direction))))
 
+(defn popup-exists?
+  [id]
+  (some->> (shui-popup/get-popups)
+           (some #(some-> % (:id) (str) (string/includes? (str id))))))
+
 (defn editor-commands-popup-exists?
   []
-  (some->> (shui-popup/get-popups)
-           (some #(some-> % (:id) (str) (string/starts-with? ":editor.commands")))))
+  (popup-exists? "editor.commands"))
 
 (defn open-selected-block!
   [direction e]

+ 25 - 2
src/main/frontend/handler/events.cljs

@@ -20,6 +20,7 @@
             [frontend.components.property.dialog :as property-dialog]
             [frontend.components.repo :as repo]
             [frontend.components.select :as select]
+            [frontend.components.selection :as selection]
             [frontend.components.settings :as settings]
             [frontend.components.shell :as shell]
             [frontend.components.user.login :as login]
@@ -927,11 +928,15 @@
   (when-let [blocks (and block (db-model/get-block-immediate-children (state/get-current-repo) (:block/uuid block)))]
     (editor-handler/toggle-blocks-as-own-order-list! blocks)))
 
-(defn- editor-new-property [block target opts]
+(defn- editor-new-property [block target {:keys [selected-blocks] :as opts}]
   (let [editing-block (state/get-edit-block)
         pos (state/get-edit-pos)
-        edit-block-or-selected (if editing-block
+        edit-block-or-selected (cond
+                                 editing-block
                                  [editing-block]
+                                 (seq selected-blocks)
+                                 selected-blocks
+                                 :else
                                  (seq (keep #(db/entity [:block/uuid %]) (state/get-selection-block-ids))))
         current-block (when-let [s (state/get-current-page)]
                         (when (util/uuid-string? s)
@@ -1069,6 +1074,24 @@
 (defmethod handle :editor/run-query-command [_]
   (editor-handler/run-query-command!))
 
+(defmethod handle :editor/show-action-bar []
+  (let [selection (state/get-selection-blocks)
+        first-visible-block (some #(when (util/el-visible-in-viewport? % true) %) selection)]
+    (when first-visible-block
+      (shui/popup-hide! :selection-action-bar)
+      (shui/popup-show!
+       first-visible-block
+       (fn []
+         (selection/action-bar))
+       {:id :selection-action-bar
+        :content-props {:side "top"
+                        :class "!py-0 !px-0 !border-none"}
+        :auto-side? false
+        :align :start}))))
+
+(defmethod handle :editor/hide-action-bar []
+  (shui/popup-hide! :selection-action-bar))
+
 (defn run!
   []
   (let [chan (state/get-events-chan)]

+ 11 - 5
src/main/frontend/handler/export/common.cljs

@@ -5,6 +5,7 @@
   (:refer-clojure :exclude [map filter mapcat concat remove])
   (:require [cljs.core.match :refer [match]]
             [clojure.string :as string]
+            [frontend.common.file.core :as common-file]
             [frontend.db :as db]
             [frontend.format.mldoc :as mldoc]
             [frontend.modules.file.core :as outliner-file]
@@ -12,11 +13,10 @@
             [frontend.persist-db.browser :as db-browser]
             [frontend.state :as state]
             [frontend.util :as util :refer [concatv mapcatv removev]]
-            [frontend.common.file.core :as common-file]
+            [logseq.db :as ldb]
             [malli.core :as m]
             [malli.util :as mu]
-            [promesa.core :as p]
-            [logseq.db :as ldb]))
+            [promesa.core :as p]))
 
 ;;; TODO: split frontend.handler.export.text related states
 (def ^:dynamic *state*
@@ -64,8 +64,14 @@
       (outliner-file/tree->file-content {:init-level init-level})))
 
 (defn root-block-uuids->content
-  [repo root-block-uuids]
-  (let [contents (mapv #(get-blocks-contents repo %) root-block-uuids)]
+  [repo root-block-uuids & {:keys [page-title-only?]}]
+  (let [contents (mapv (fn [id]
+                         (if-let [page (and page-title-only?
+                                            (let [e (db/entity [:block/uuid id])]
+                                              (when (:block/name e)
+                                                e)))]
+                           (:block/title page)
+                           (get-blocks-contents repo id))) root-block-uuids)]
     (string/join "\n" (mapv string/trim-newline contents))))
 
 (declare remove-block-ast-pos Properties-block-ast?)

+ 3 - 2
src/main/frontend/handler/export/text.cljs

@@ -506,7 +506,8 @@
   "options:
   :indent-style \"dashes\" | \"spaces\" | \"no-indent\"
   :remove-options [:emphasis :page-ref :tag :property]
-  :other-options {:keep-only-level<=N int :newline-after-block bool}"
+  :other-options {:keep-only-level<=N int :newline-after-block bool}
+  :page-title-only? boolean"
   [repo root-block-uuids-or-page-uuid options]
   {:pre [(or (coll? root-block-uuids-or-page-uuid)
              (uuid? root-block-uuids-or-page-uuid))]}
@@ -517,7 +518,7 @@
            (if (uuid? root-block-uuids-or-page-uuid)
              ;; page
              (common/get-page-content root-block-uuids-or-page-uuid)
-             (common/root-block-uuids->content repo root-block-uuids-or-page-uuid))
+             (common/root-block-uuids->content repo root-block-uuids-or-page-uuid options))
            first-block (and (coll? root-block-uuids-or-page-uuid)
                             (db/entity [:block/uuid (first root-block-uuids-or-page-uuid)]))
            format (get first-block :block/format :markdown)]

+ 18 - 14
src/main/frontend/state.cljs

@@ -258,6 +258,7 @@
       :command-palette/commands              (atom [])
 
       :view/components                       {}
+      :view/selected-blocks                  nil
 
       :srs/mode?                             false
 
@@ -1185,8 +1186,21 @@ Similar to re-frame subscriptions"
   (doseq [node nodes]
     (dom/add-class! node "selected")))
 
+(defn get-events-chan
+  []
+  (:system/events @state))
+
+(defn pub-event!
+  {:malli/schema [:=> [:cat vector?] :any]}
+  [payload]
+  (let [d (p/deferred)
+        chan (get-events-chan)]
+    (async/put! chan [payload d])
+    d))
+
 (defn- set-selection-blocks-aux!
   [blocks]
+  (set-state! :view/selected-blocks nil)
   (let [selected-ids (set (get-selected-block-ids @(:selection/blocks @state)))
         _ (set-state! :selection/blocks blocks)
         new-ids (set (get-selection-block-ids))
@@ -1210,7 +1224,8 @@ Similar to re-frame subscriptions"
   (set-state! :selection/blocks nil)
   (set-state! :selection/direction nil)
   (set-state! :selection/start-block nil)
-  (set-state! :selection/selected-all? false))
+  (set-state! :selection/selected-all? false)
+  (pub-event! [:editor/hide-action-bar]))
 
 (defn clear-selection!
   []
@@ -1366,7 +1381,8 @@ Similar to re-frame subscriptions"
   (set-state! :editor/content {})
   (set-state! :ui/select-query-cache {})
   (set-state! :editor/block-refs #{})
-  (set-state! :editor/action-data nil))
+  (set-state! :editor/action-data nil)
+  (set-state! :view/selected-blocks nil))
 
 (defn into-code-editor-mode!
   []
@@ -1829,18 +1845,6 @@ Similar to re-frame subscriptions"
   ([] (open-settings! true))
   ([active-tab] (set-state! :ui/settings-open? active-tab)))
 
-(defn get-events-chan
-  []
-  (:system/events @state))
-
-(defn pub-event!
-  {:malli/schema [:=> [:cat vector?] :any]}
-  [payload]
-  (let [d (p/deferred)
-        chan (get-events-chan)]
-    (async/put! chan [payload d])
-    d))
-
 (defn sidebar-add-block!
   [repo db-id block-type]
   (when (not (util/sm-breakpoint?))

+ 17 - 16
src/main/frontend/ui.cljs

@@ -517,20 +517,20 @@
   (let [*current-idx (get state ::current-idx)
         *groups (atom #{})
         render-f (fn [matched]
-                   (for [[idx item] (medley/indexed matched)]
+                   (for [[idx item] matched]
                      (let [react-key (str idx)
                            item-cp
                            [:div.menu-link-wrap
                             {:key react-key
-                   ;; mouse-move event to indicate that cursor moved by user
+                             ;; mouse-move event to indicate that cursor moved by user
                              :on-mouse-move  #(reset! *current-idx idx)}
                             (let [chosen? (= @*current-idx idx)]
                               (menu-link
                                {:id (str "ac-" react-key)
                                 :tab-index "0"
                                 :class (when chosen? "chosen")
-                       ;; TODO: should have more tests on touch devices
-                       ;:on-pointer-down #(util/stop %)
+                                ;; TODO: should have more tests on touch devices
+                                        ;:on-pointer-down #(util/stop %)
                                 :on-click (fn [e]
                                             (util/stop e)
                                             (when-not (:disabled? item)
@@ -552,13 +552,16 @@
        [:div#ui__ac-inner.hide-scrollbar
         (when header header)
         (if grouped?
-          (for [[group matched] (group-by :group matched)]
-            (if group
-              [:div
-               [:div.ui__ac-group-name group]
-               (render-f matched)]
-              (render-f matched)))
-          (render-f matched))]
+          (let [*idx (atom -1)
+                inc-idx #(swap! *idx inc)]
+            (for [[group matched] (group-by :group matched)]
+              (let [matched' (doall (map (fn [item] [(inc-idx) item]) matched))]
+                (if group
+                  [:div
+                   [:div.ui__ac-group-name group]
+                   (render-f matched')]
+                  (render-f matched')))))
+          (render-f (medley/indexed matched)))]
        (when empty-placeholder
          empty-placeholder))]))
 
@@ -1016,13 +1019,11 @@
       :small? true)]]))
 
 (rum/defc tooltip
-  [trigger tooltip-content]
+  [trigger tooltip-content & {:keys [trigger-props]}]
   (shui/tooltip-provider
    (shui/tooltip
-    (shui/tooltip-trigger
-     trigger)
-    (shui/tooltip-content
-     tooltip-content))))
+    (shui/tooltip-trigger trigger-props trigger)
+    (shui/tooltip-content tooltip-content))))
 
 (rum/defc DelDateButton
   [on-delete]