Просмотр исходного кода

Merge pull request #11540 from logseq/refactor/fsrs

refactor: flashcards uses FSRS
Tienson Qin 1 год назад
Родитель
Сommit
75f540bebc
35 измененных файлов с 1829 добавлено и 1271 удалено
  1. 8 1
      deps.edn
  2. 14 7
      deps/common/src/logseq/common/util.cljs
  3. 20 3
      deps/db/src/logseq/db.cljs
  4. 21 7
      deps/db/src/logseq/db/frontend/class.cljs
  5. 20 6
      deps/db/src/logseq/db/frontend/property.cljs
  6. 2 3
      deps/db/src/logseq/db/frontend/schema.cljs
  7. 1 1
      deps/db/src/logseq/db/sqlite/create_graph.cljs
  8. 5 12
      deps/outliner/src/logseq/outliner/property.cljs
  9. 108 109
      deps/shui/src/logseq/shui/dialog/core.cljs
  10. 3 0
      package.json
  11. 321 300
      src/main/frontend/components/block.cljs
  12. 57 60
      src/main/frontend/components/container.cljs
  13. 8 3
      src/main/frontend/components/content.cljs
  14. 13 18
      src/main/frontend/components/property.cljs
  15. 126 95
      src/main/frontend/components/property/value.cljs
  16. 1 1
      src/main/frontend/components/query.cljs
  17. 64 52
      src/main/frontend/components/query/builder.cljs
  18. 1 1
      src/main/frontend/components/query/result.cljs
  19. 1 1
      src/main/frontend/components/settings.cljs
  20. 102 104
      src/main/frontend/components/views.cljs
  21. 20 15
      src/main/frontend/db/query_dsl.cljs
  22. 391 0
      src/main/frontend/extensions/fsrs.cljs
  23. 22 44
      src/main/frontend/extensions/srs.cljs
  24. 17 1
      src/main/frontend/extensions/srs/handler.cljs
  25. 4 4
      src/main/frontend/handler/db_based/property.cljs
  26. 115 115
      src/main/frontend/handler/editor.cljs
  27. 175 175
      src/main/frontend/handler/events.cljs
  28. 18 15
      src/main/frontend/mobile/action_bar.cljs
  29. 12 0
      src/main/frontend/modules/shortcut/config.cljs
  30. 33 10
      src/main/frontend/worker/db/migrate.cljs
  31. 2 2
      src/main/frontend/worker/rtc/db_listener.cljs
  32. 104 105
      src/main/logseq/api.cljs
  33. 4 0
      src/resources/dicts/en.edn
  34. 1 1
      src/test/frontend/db/db_based_model_test.cljs
  35. 15 0
      yarn.lock

+ 8 - 1
deps.edn

@@ -40,7 +40,14 @@
   metosin/malli                         {:mvn/version "0.16.1"}
   com.cognitect/transit-cljs            {:mvn/version "0.8.280"}
   missionary/missionary                 {:mvn/version "b.39"}
-  meander/epsilon                       {:mvn/version "0.0.650"}}
+  meander/epsilon                       {:mvn/version "0.0.650"}
+
+  io.github.open-spaced-repetition/cljc-fsrs {:git/sha "0e70e96a73cf63c85dcc2df4d022edf12806b239"
+                                              ;; TODO: use https://github.com/open-spaced-repetition/cljc-fsrs
+                                              ;; when PR merged
+                                              ;; https://github.com/open-spaced-repetition/cljc-fsrs/pull/5
+                                              :git/url "https://github.com/rcmerci/cljc-fsrs"}
+  tick/tick                             {:mvn/version "0.7.5"}}
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
                   :extra-deps  {org.clojure/clojurescript        {:mvn/version "1.11.132"}

+ 14 - 7
deps/common/src/logseq/common/util.cljs

@@ -82,9 +82,18 @@
   [v]
   (string/trim (subs v 1 (dec (count v)))))
 
+(defn wrapped-by
+  [v start end]
+  (and (string? v) (>= (count v) 2)
+       (= start (first v)) (= end (last v))))
+
 (defn wrapped-by-quotes?
   [v]
-  (and (string? v) (>= (count v) 2) (= "\"" (first v) (last v))))
+  (wrapped-by v "\"" "\""))
+
+(defn wrapped-by-parens?
+  [v]
+  (wrapped-by v "(" ")"))
 
 (defn url?
   "Test if it is a `protocol://`-style URL.
@@ -161,7 +170,6 @@
            (map string/capitalize)
            (string/join " ")))
 
-
 (defn distinct-by
   "Copy from medley"
   [f coll]
@@ -177,9 +185,9 @@
     (step (seq coll) #{})))
 
 (defn distinct-by-last-wins
-     [f col]
-     {:pre [(sequential? col)]}
-     (reverse (distinct-by f (reverse col))))
+  [f col]
+  {:pre [(sequential? col)]}
+  (reverse (distinct-by f (reverse col))))
 
 (defn normalize-format
   [format]
@@ -315,7 +323,6 @@
   [s old-value new-value]
   (string/replace-first s (re-pattern (str "(?i)" (escape-regex-chars old-value))) new-value))
 
-
 (defn sort-coll-by-dependency
   "Sort the elements in the collection based on dependencies.
 coll:  [{:id 1 :depend-on 2} {:id 2 :depend-on 3} {:id 3}]
@@ -347,4 +354,4 @@ return: [{:id 3} {:id 2 :depend-on 3} {:id 1 :depend-on 2}]"
               (let [rest-ids* (disj rest-ids id)]
                 (vreset! seen-ids #{})
                 (recur (conj r id) rest-ids* (first rest-ids*))))))]
-    (mapv id->elem sorted-ids)))
+    (mapv id->elem sorted-ids)))

+ 20 - 3
deps/db/src/logseq/db.cljs

@@ -1,7 +1,8 @@
 (ns logseq.db
   "Main namespace for public db fns. For DB and file graphs.
    For shared file graph only fns, use logseq.graph-parser.db"
-  (:require [clojure.string :as string]
+  (:require [clojure.set :as set]
+            [clojure.string :as string]
             [datascript.core :as d]
             [datascript.impl.entity :as de]
             [logseq.common.config :as common-config]
@@ -189,7 +190,6 @@
   (assert (or (de/entity? block) (nil? block)))
   (first (sort-by-order (:block/_parent block))))
 
-
 (defn get-pages
   [db]
   (->> (d/q
@@ -450,7 +450,6 @@
     (when (seq refs)
       (d/pull-many db '[*] (map :db/id refs)))))
 
-
 (defn get-block-refs-count
   [db id]
   (some-> (d/entity db id)
@@ -620,6 +619,24 @@
           (recur (:logseq.property/parent current-parent)))))
     @*classes))
 
+(defn get-classes-parents
+  [db tags]
+  (let [tags' (filter class? tags)
+        result (map
+                (fn [id] (d/entity db id))
+                (mapcat get-class-parents tags'))]
+    (set result)))
+
+(defn class-instance?
+  "Whether `object` is an instance of `class`"
+  [class object]
+  (let [tags (:block/tags object)
+        tags-ids (set (map :db/id tags))]
+    (or
+     (contains? tags-ids (:db/id class))
+     (let [class-parents (get-classes-parents (.-db object) tags)]
+       (contains? (set/union class-parents tags-ids) (:db/id class))))))
+
 (defn get-all-pages-views
   [db]
   (when (db-based-graph? db)

+ 21 - 7
deps/db/src/logseq/db/frontend/class.cljs

@@ -7,18 +7,32 @@
   "Map of built-in classes for db graphs with their :db/ident as keys"
   {:logseq.class/Root {:title "Root Tag"}
 
-   :logseq.class/Query
-   {:title "Query"
-    :properties {:logseq.property/icon {:type :tabler-icon :id "search"}}}
-
    :logseq.class/Task
    {:title "Task"
     :schema {:properties [:logseq.task/status :logseq.task/priority :logseq.task/deadline]}}
 
-   :logseq.class/Card {:title "Card"}
+   :logseq.class/Journal
+   {:title "Journal"
+    :properties {:logseq.property.journal/title-format "MMM do, yyyy"}}
 
-   :logseq.class/Journal {:title "Journal"
-                          :properties {:logseq.property.journal/title-format "MMM do, yyyy"}}
+   :logseq.class/Query
+   {:title "Query"
+    :properties {:logseq.property/icon {:type :tabler-icon :id "search"}}
+    :schema {:properties [:logseq.property/query]}}
+
+   :logseq.class/Advanced-query
+   {:title "Advanced query"
+    :properties {:logseq.property/icon {:type :tabler-icon :id "search"}
+                 :logseq.property/parent :logseq.class/Query}}
+
+   :logseq.class/Card
+   {:title "Card"
+    :schema {:properties [:logseq.property.fsrs/state :logseq.property.fsrs/due]}}
+
+   :logseq.class/Cards
+   {:title "Cards"
+    :properties {:logseq.property/icon {:type :tabler-icon :id "search"}
+                 :logseq.property/parent :logseq.class/Query}}
 
    ;; TODO: Add more classes such as :book, :paper, :movie, :music, :project
    })

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

@@ -51,6 +51,10 @@
                                           :schema {:type :checkbox
                                                    :public? true
                                                    :view-context :class}}
+   :logseq.property/query {:title "Query"
+                           :schema {:type :default
+                                    :public? true
+                                    :view-context :block}}
    :logseq.property/page-tags {:title "Page Tags"
                                :schema {:type :page
                                         :public? true
@@ -210,10 +214,10 @@
                                             :hide? true
                                             :public? false}}
 
-    :logseq.property.table/sized-columns {:schema
-                                          {:type :map
-                                           :hide? true
-                                           :public? false}}
+   :logseq.property.table/sized-columns {:schema
+                                         {:type :map
+                                          :hide? true
+                                          :public? false}}
 
    :logseq.property/view-for {:schema
                               {:type :node
@@ -222,7 +226,17 @@
    :logseq.property.asset/remote-metadata {:schema
                                            {:type :map
                                             :hide? true
-                                            :public? false}}))
+                                            :public? false}}
+   :logseq.property.fsrs/due {:title "Due"
+                              :schema
+                              {:type :datetime
+                               :hide? false
+                               :public? false}}
+   :logseq.property.fsrs/state {:title "State"
+                                :schema
+                                {:type :map
+                                 :hide? false ; TODO: show for debug now, hide it later
+                                 :public? false}}))
 
 (def built-in-properties
   (->> built-in-properties*
@@ -249,7 +263,7 @@
         "All db attribute properties are configured in built-in-properties")
 
 (def logseq-property-namespaces
-  #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.task"
+  #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.property.fsrs" "logseq.task"
     "logseq.property.linked-references" "logseq.property.asset" "logseq.property.table"
     "logseq.property.journal" "logseq.property.class" "logseq.property.view"})
 

+ 2 - 3
deps/db/src/logseq/db/frontend/schema.cljs

@@ -2,7 +2,7 @@
   "Main datascript schemas for the Logseq app"
   (:require [clojure.set :as set]))
 
-(def version 21)
+(def version 25)
 ;; A page is a special block, a page can corresponds to multiple files with the same ":block/name".
 (def ^:large-vars/data-var schema
   {:db/ident        {:db/unique :db.unique/identity}
@@ -103,8 +103,7 @@
    :file/content {}
    :file/created-at {}
    :file/last-modified-at {}
-   :file/size {}
-   })
+   :file/size {}})
 
 (def schema-for-db-based-graph
   (merge

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

@@ -82,7 +82,7 @@
          (let [schema-properties (mapv
                                   (fn [db-ident]
                                     (let [property (get db-ident->properties db-ident)]
-                                      (assert property (str "Built-in property " db-ident "is not defined yet"))
+                                      (assert property (str "Built-in property " db-ident " is not defined yet"))
                                       db-ident))
                                   (:properties schema))]
            (cond->

+ 5 - 12
deps/outliner/src/logseq/outliner/property.cljs

@@ -43,7 +43,6 @@
           [:db/retract (:db/id block) property-id :logseq.property/empty-placeholder])
         block-tx-data]))))
 
-
 (defn- get-property-value-schema
   "Gets a malli schema to validate the property value for the given property type and builds
    it with additional args like datascript db"
@@ -267,15 +266,13 @@
 
 (defn batch-set-property!
   "Sets properties for multiple blocks. Automatically handles property value refs.
-   Does no validation of property values.
-   NOTE: This fn only works for properties with cardinality equal to `one`."
+   Does no validation of property values."
   [conn block-ids property-id v]
   (assert property-id "property-id is nil")
   (throw-error-if-read-only-property property-id)
   (let [block-eids (map ->eid block-ids)
         property (d/entity @conn property-id)
         _ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
-        _ (assert (not (db-property/many? property)) "Property must be cardinality :one in batch-set-property!")
         property-type (get-in property [:block/schema :type] :default)
         _ (assert v "Can't set a nil property value must be not nil")
         v' (if (db-property-type/value-ref-property-types property-type)
@@ -356,17 +353,13 @@
                            [[:db/retract (:db/id block) property-id property-value]]
                            {:outliner-op :save-block})))))))
 
-(defn ^:api get-class-parents
+(defn ^:api get-classes-parents
   [db tags]
-  (let [tags' (filter ldb/class? tags)
-        result (map
-                (fn [id] (d/entity db id))
-                (mapcat ldb/get-class-parents tags'))]
-    (set result)))
+  (ldb/get-classes-parents db tags))
 
 (defn ^:api get-class-properties
   [db class]
-  (let [class-parents (get-class-parents db [class])]
+  (let [class-parents (get-classes-parents db [class])]
     (->> (mapcat (fn [class]
                    (:logseq.property.class/properties class)) (concat [class] class-parents))
          (common-util/distinct-by :db/id)
@@ -378,7 +371,7 @@
         classes (->> (:block/tags block)
                      (sort-by :block/name)
                      (filter ldb/class?))
-        class-parents (get-class-parents db classes)
+        class-parents (get-classes-parents db classes)
         all-classes (->> (concat classes class-parents)
                          (filter (fn [class]
                                    (seq (:logseq.property.class/properties class)))))

+ 108 - 109
deps/shui/src/logseq/shui/dialog/core.cljs

@@ -38,7 +38,7 @@
             (let [v (get config k)
                   v (if (fn? v) (apply v args) v)]
               (if (vector? v) (assoc config k (interpret v)) config)))
-    config ks))
+          config ks))
 
 ;; {:id :title :description :content :footer :open? :on-close ...}
 (def ^:private *modals (atom []))
@@ -49,7 +49,7 @@
   [id]
   (when id
     (some->> (medley/indexed @*modals)
-      (filter #(= id (:id (second %)))) (first))))
+             (filter #(= id (:id (second %)))) (first))))
 
 (defn update-modal!
   [id ks val]
@@ -90,13 +90,14 @@
         config (cond-> config
                  (fn? content)
                  (assoc :content (content config)))]
-    (upsert-modal! config)))
+    (upsert-modal! (assoc-in config [:content-props :onOpenAutoFocus]
+                             #(.preventDefault %)))))
 
 (defn alert!
   [content-or-config & config']
   (let [deferred (p/deferred)]
     (open! content-or-config
-      (merge {:alert? :default :deferred deferred} (first config')))
+           (merge {:alert? :default :deferred deferred} (first config')))
     (p/promise deferred)))
 
 (defn confirm!
@@ -121,45 +122,45 @@
   (let [{:keys [id title description content footer on-open-change align open?
                 auto-width? close-btn? root-props content-props]} config
         props (dissoc config
-                :id :title :description :content :footer :auto-width? :close-btn?
-                :align :on-open-change :open? :root-props :content-props)
+                      :id :title :description :content :footer :auto-width? :close-btn?
+                      :align :on-open-change :open? :root-props :content-props)
         props (assoc-in props [:overlay-props :data-align] (name (or align :center)))]
 
     (rum/use-effect!
-      (fn []
-        (when (false? open?)
-          (js/setTimeout #(detach-modal! id) 128)))
-      [open?])
+     (fn []
+       (when (false? open?)
+         (js/setTimeout #(detach-modal! id) 128)))
+     [open?])
 
     (dialog
-      (merge root-props
-        {:key (str "modal-" id)
-         :open open?
-         :on-open-change (fn [v]
-                           (let [set-open! #(update-modal! id :open? %)]
-                             (if (fn? on-open-change)
-                               (on-open-change {:value v :set-open! set-open!})
-                               (set-open! v))))})
-      (let [onPointerDownOutside (:onPointerDownOutside content-props)
-            content-props (assoc content-props
-                            :onPointerDownOutside
-                            (fn [^js e]
-                              (when (fn? onPointerDownOutside)
-                                (onPointerDownOutside e))
-                              (when-not (some-> (.-target e) (.closest ".ui__dialog-overlay"))
-                                (.preventDefault e))))]
-        (dialog-content
-          (cond-> (merge props content-props)
-            auto-width? (assoc :data-auto-width true)
-            (false? close-btn?) (assoc :data-close-btn false))
-          (when title
-            (dialog-header
-              (when title (dialog-title title))
-              (when description (dialog-description description))))
-          (when content
-            [:div.ui__dialog-main-content content])
-          (when footer
-            (dialog-footer footer)))))))
+     (merge root-props
+            {:key (str "modal-" id)
+             :open open?
+             :on-open-change (fn [v]
+                               (let [set-open! #(update-modal! id :open? %)]
+                                 (if (fn? on-open-change)
+                                   (on-open-change {:value v :set-open! set-open!})
+                                   (set-open! v))))})
+     (let [onPointerDownOutside (:onPointerDownOutside content-props)
+           content-props (assoc content-props
+                                :onPointerDownOutside
+                                (fn [^js e]
+                                  (when (fn? onPointerDownOutside)
+                                    (onPointerDownOutside e))
+                                  (when-not (some-> (.-target e) (.closest ".ui__dialog-overlay"))
+                                    (.preventDefault e))))]
+       (dialog-content
+        (cond-> (merge props content-props)
+          auto-width? (assoc :data-auto-width true)
+          (false? close-btn?) (assoc :data-close-btn false))
+        (when title
+          (dialog-header
+           (when title (dialog-title title))
+           (when description (dialog-description description))))
+        (when content
+          [:div.ui__dialog-main-content content])
+        (when footer
+          (dialog-footer footer)))))))
 
 (rum/defc alert-inner
   [config]
@@ -167,35 +168,34 @@
         props (dissoc config :id :title :description :content :footer :deferred :open? :alert?)]
 
     (rum/use-effect!
-      (fn []
-        (when (false? open?)
-          (js/setTimeout #(detach-modal! id) 128)))
-      [open?])
+     (fn []
+       (when (false? open?)
+         (js/setTimeout #(detach-modal! id) 128)))
+     [open?])
 
     (alert-dialog
-      {:key (str "alert-" id)
-       :open open?
-       :on-open-change #(update-modal! id :open? %)}
-      (alert-dialog-content props
-        (when (or title description)
-          (alert-dialog-header
-            {:class "ui__alert-dialog-header"}
-            (when title (alert-dialog-title title))
-            (when description (alert-dialog-description description))))
+     {:key (str "alert-" id)
+      :open open?
+      :on-open-change #(update-modal! id :open? %)}
+     (alert-dialog-content props
+                           (when (or title description)
+                             (alert-dialog-header
+                              {:class "ui__alert-dialog-header"}
+                              (when title (alert-dialog-title title))
+                              (when description (alert-dialog-description description))))
 
-        (when content
-          [:div.ui__alert-dialog-main-content content])
+                           (when content
+                             [:div.ui__alert-dialog-main-content content])
 
-        (alert-dialog-footer
-          {:class "ui__alert-dialog-footer"}
-          (if footer
-            footer
-            [:<>
-             (base/button
-               {:key "ok"
-                :on-click #(do (close!) (p/resolve! deferred true))
-                :size :sm
-                } "OK")]))))))
+                           (alert-dialog-footer
+                            {:class "ui__alert-dialog-footer"}
+                            (if footer
+                              footer
+                              [:<>
+                               (base/button
+                                {:key "ok"
+                                 :on-click #(do (close!) (p/resolve! deferred true))
+                                 :size :sm} "OK")]))))))
 
 (rum/defc confirm-inner
   [config]
@@ -206,56 +206,55 @@
         *reminder-ref (rum/use-ref nil)]
 
     (rum/use-effect!
-      (fn []
-        (when ready?
-          (js/setTimeout
-            #(some-> (rum/deref *ok-ref) (.focus)) 128)))
-      [ready?])
+     (fn []
+       (when ready?
+         (js/setTimeout
+          #(some-> (rum/deref *ok-ref) (.focus)) 128)))
+     [ready?])
 
     (rum/use-effect!
-      (fn []
-        (try
-          (if-let [reminder-v (and reminder? (js/localStorage.getItem (str id)))]
-            (if (< (- (js/Date.now) reminder-v) (* 1000 60 10))
-              (do (detach-modal! id) (p/resolve! deferred true))
-              (set-ready! true))
-            (set-ready! true))
-          (catch js/Error _e
-            (set-ready! true))))
-      [])
+     (fn []
+       (try
+         (if-let [reminder-v (and reminder? (js/localStorage.getItem (str id)))]
+           (if (< (- (js/Date.now) reminder-v) (* 1000 60 10))
+             (do (detach-modal! id) (p/resolve! deferred true))
+             (set-ready! true))
+           (set-ready! true))
+         (catch js/Error _e
+           (set-ready! true))))
+     [])
 
     (when ready?
       (alert-inner
-        (assoc config
-          :data-mode :confirm
-          :overlay-props
-          {:on-click #(when outside-cancel? (close!) (p/reject! deferred nil))}
+       (assoc config
+              :data-mode :confirm
+              :overlay-props
+              {:on-click #(when outside-cancel? (close!) (p/reject! deferred nil))}
 
-          :footer
-          [:<>
-           [:span.flex.items-center.pt-1
-            (when (and id data-reminder)
-              [:label.flex.items-center.gap-1.text-sm
-               (form/checkbox {:ref *reminder-ref})
-               [:span.opacity-50 "Don't remind me again"]])]
-           [:span.flex.gap-2
-            (base/button
-              {:key "cancel"
-               :on-click #(do (close!) (p/reject! deferred false))
-               :variant :outline
-               :size :sm}
-              "Cancel")
-            (base/button
-              {:key "ok"
-               :ref *ok-ref
-               :on-click (fn []
-                           (when-let [^js reminder (and id data-reminder (rum/deref *reminder-ref))]
-                             (when (= "checked" (.-state (.-dataset reminder)))
-                               (js/localStorage.setItem (str id) (js/Date.now))))
-                           (close!)
-                           (p/resolve! deferred true))
-               :size :sm
-               } "OK")]])))))
+              :footer
+              [:<>
+               [:span.flex.items-center.pt-1
+                (when (and id data-reminder)
+                  [:label.flex.items-center.gap-1.text-sm
+                   (form/checkbox {:ref *reminder-ref})
+                   [:span.opacity-50 "Don't remind me again"]])]
+               [:span.flex.gap-2
+                (base/button
+                 {:key "cancel"
+                  :on-click #(do (close!) (p/reject! deferred false))
+                  :variant :outline
+                  :size :sm}
+                 "Cancel")
+                (base/button
+                 {:key "ok"
+                  :ref *ok-ref
+                  :on-click (fn []
+                              (when-let [^js reminder (and id data-reminder (rum/deref *reminder-ref))]
+                                (when (= "checked" (.-state (.-dataset reminder)))
+                                  (js/localStorage.setItem (str id) (js/Date.now))))
+                              (close!)
+                              (p/resolve! deferred true))
+                  :size :sm} "OK")]])))))
 
 (rum/defc install-modals
   < rum/static
@@ -266,8 +265,8 @@
       (let [id (:id config)
             alert? (:alert? config)
             config (interpret-vals config
-                     [:title :description :content :footer]
-                     {:id id})]
+                                   [:title :description :content :footer]
+                                   {:id id})]
         (case alert?
           :default
           (alert-inner config)

+ 3 - 0
package.json

@@ -106,6 +106,9 @@
         "@glidejs/glide": "^3.6.0",
         "@highlightjs/cdn-assets": "10.4.1",
         "@isomorphic-git/lightning-fs": "^4.6.0",
+        "@js-joda/core": "3.2.0",
+        "@js-joda/locale_en-us": "3.1.1",
+        "@js-joda/timezone": "2.5.0",
         "@logseq/capacitor-file-sync": "5.0.2",
         "@logseq/diff-merge": "0.2.2",
         "@logseq/react-tweet-embed": "1.3.1-1",

+ 321 - 300
src/main/frontend/components/block.cljs

@@ -280,106 +280,104 @@
   (let [size (get state ::size)
         breadcrumb? (:breadcrumb? config)]
     (ui/resize-provider
-      (ui/resize-consumer
-        (if (and (not (mobile-util/native-platform?))
-              (not breadcrumb?))
-          (cond->
-            {:className "resize image-resize"
-             :onSizeChanged (fn [value]
-                              (when (and (not @*resizing-image?)
+     (ui/resize-consumer
+      (if (and (not (mobile-util/native-platform?))
+               (not breadcrumb?))
+        (cond->
+         {:className "resize image-resize"
+          :onSizeChanged (fn [value]
+                           (when (and (not @*resizing-image?)
                                       (some? @size)
                                       (not= value @size))
-                                (reset! *resizing-image? true))
-                              (reset! size value))
-             :onPointerUp (fn []
-                            (when (and @size @*resizing-image?)
-                              (when-let [block-id (:block/uuid config)]
-                                (let [size (bean/->clj @size)]
-                                  (editor-handler/resize-image! block-id metadata full-text size))))
-                            (when @*resizing-image?
+                             (reset! *resizing-image? true))
+                           (reset! size value))
+          :onPointerUp (fn []
+                         (when (and @size @*resizing-image?)
+                           (when-let [block-id (:block/uuid config)]
+                             (let [size (bean/->clj @size)]
+                               (editor-handler/resize-image! block-id metadata full-text size))))
+                         (when @*resizing-image?
                               ;; TODO: need a better way to prevent the clicking to edit current block
-                              (js/setTimeout #(reset! *resizing-image? false) 200)))
-             :onClick (fn [e]
-                        (when @*resizing-image? (util/stop e)))}
-            (and (:width metadata) (not (util/mobile?)))
-            (assoc :style {:width (:width metadata)}))
-          {})
-        [:div.asset-container {:key "resize-asset-container"}
-         [:img.rounded-sm.relative
-          (merge
-            {:loading "lazy"
-             :referrerPolicy "no-referrer"
-             :src src
-             :title title}
-            metadata)]
-         (when-not breadcrumb?
-           [:<>
-            (let [image-src (fs/asset-path-normalize src)]
-              [:.asset-action-bar {:aria-hidden "true"}
-               [:.flex
-                (when-not config/publishing?
-                  [:button.asset-action-btn
-                   {:title (t :asset/delete)
-                    :tabIndex "-1"
-                    :on-pointer-down util/stop
-                    :on-click
-                    (fn [e]
-                      (util/stop e)
-                      (when-let [block-id (:block/uuid config)]
-                        (let [*local-selected? (atom local?)]
-                          (-> (shui/dialog-confirm!
-                                [:div.text-xs.opacity-60.-my-2
-                                 (when local?
-                                   [:label.flex.gap-1.items-center
-                                    (shui/checkbox
-                                      {:default-checked @*local-selected?
-                                       :on-checked-change #(reset! *local-selected? %)})
-                                    (t :asset/physical-delete)])]
-                                {:title (t :asset/confirm-delete (.toLocaleLowerCase (t :text/image)))
-                                 :outside-cancel? true})
+                           (js/setTimeout #(reset! *resizing-image? false) 200)))
+          :onClick (fn [e]
+                     (when @*resizing-image? (util/stop e)))}
+          (and (:width metadata) (not (util/mobile?)))
+          (assoc :style {:width (:width metadata)}))
+        {})
+      [:div.asset-container {:key "resize-asset-container"}
+       [:img.rounded-sm.relative
+        (merge
+         {:loading "lazy"
+          :referrerPolicy "no-referrer"
+          :src src
+          :title title}
+         metadata)]
+       (when-not breadcrumb?
+         [:<>
+          (let [image-src (fs/asset-path-normalize src)]
+            [:.asset-action-bar {:aria-hidden "true"}
+             [:.flex
+              (when-not config/publishing?
+                [:button.asset-action-btn
+                 {:title (t :asset/delete)
+                  :tabIndex "-1"
+                  :on-pointer-down util/stop
+                  :on-click
+                  (fn [e]
+                    (util/stop e)
+                    (when-let [block-id (:block/uuid config)]
+                      (let [*local-selected? (atom local?)]
+                        (-> (shui/dialog-confirm!
+                             [:div.text-xs.opacity-60.-my-2
+                              (when local?
+                                [:label.flex.gap-1.items-center
+                                 (shui/checkbox
+                                  {:default-checked @*local-selected?
+                                   :on-checked-change #(reset! *local-selected? %)})
+                                 (t :asset/physical-delete)])]
+                             {:title (t :asset/confirm-delete (.toLocaleLowerCase (t :text/image)))
+                              :outside-cancel? true})
                             (p/then (fn []
                                       (shui/dialog-close!)
                                       (editor-handler/delete-asset-of-block!
-                                        {:block-id block-id
-                                         :local? local?
-                                         :delete-local? @*local-selected?
-                                         :repo (state/get-current-repo)
-                                         :href src
-                                         :title title
-                                         :full-text full-text})))))
-                        ))}
-                   (ui/icon "trash")])
+                                       {:block-id block-id
+                                        :local? local?
+                                        :delete-local? @*local-selected?
+                                        :repo (state/get-current-repo)
+                                        :href src
+                                        :title title
+                                        :full-text full-text})))))))}
+                 (ui/icon "trash")])
+
+              [:button.asset-action-btn
+               {:title (t :asset/copy)
+                :tabIndex "-1"
+                :on-pointer-down util/stop
+                :on-click (fn [e]
+                            (util/stop e)
+                            (-> (util/copy-image-to-clipboard image-src)
+                                (p/then #(notification/show! "Copied!" :success))))}
+               (ui/icon "copy")]
+
+              [:button.asset-action-btn
+               {:title (t :asset/maximize)
+                :tabIndex "-1"
+                :on-pointer-down util/stop
+                :on-click open-lightbox}
 
+               (ui/icon "maximize")]
+
+              (when (util/electron?)
                 [:button.asset-action-btn
-                 {:title (t :asset/copy)
+                 {:title (t (if local? :asset/show-in-folder :asset/open-in-browser))
                   :tabIndex "-1"
                   :on-pointer-down util/stop
                   :on-click (fn [e]
                               (util/stop e)
-                              (-> (util/copy-image-to-clipboard image-src)
-                                (p/then #(notification/show! "Copied!" :success))))}
-                 (ui/icon "copy")]
-
-                [:button.asset-action-btn
-                 {:title (t :asset/maximize)
-                  :tabIndex "-1"
-                  :on-pointer-down util/stop
-                  :on-click open-lightbox}
-
-                 (ui/icon "maximize")]
-
-                (when (util/electron?)
-                  [:button.asset-action-btn
-                   {:title (t (if local? :asset/show-in-folder :asset/open-in-browser))
-                    :tabIndex "-1"
-                    :on-pointer-down util/stop
-                    :on-click (fn [e]
-                                (util/stop e)
-                                (if local?
-                                  (ipc/ipc "openFileInFolder" image-src)
-                                  (js/window.apis.openExternal image-src)))}
-                   (shui/tabler-icon "folder-pin")])
-                ]])])]))))
+                              (if local?
+                                (ipc/ipc "openFileInFolder" image-src)
+                                (js/window.apis.openExternal image-src)))}
+                 (shui/tabler-icon "folder-pin")])]])])]))))
 
 (rum/defc audio-cp [src]
   ;; Change protocol to allow media fragment uris to play
@@ -388,16 +386,16 @@
            :on-touch-start #(util/stop %)}])
 
 (rum/defcs asset-link < rum/reactive
-                        (rum/local nil ::src)
+  (rum/local nil ::src)
   [state config title href metadata full_text]
   (let [src (::src state)
         granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
         href (config/get-local-asset-absolute-path href)]
     (when (and (or granted?
-                 (util/electron?)
-                 (mobile-util/native-platform?)
-                 (config/db-based-graph? (state/get-current-repo)))
-            (nil? @src))
+                   (util/electron?)
+                   (mobile-util/native-platform?)
+                   (config/db-based-graph? (state/get-current-repo)))
+               (nil? @src))
       (p/then (assets-handler/make-asset-url href) #(reset! src %)))
 
     (when @src
@@ -539,7 +537,7 @@
 
         :else
         (-> (or (:on-redirect-to-page config) route-handler/redirect-to-page!)
-            (apply [(:block/uuid page)])))))
+            (apply [(or (:block/uuid page) (:block/name page))])))))
   (when (and contents-page?
              (util/mobile?)
              (state/get-left-sidebar-open?))
@@ -673,31 +671,31 @@
   [children {:keys [*timer *timer1 visible? set-visible! render *el-popup]}]
   (let [*el-trigger (rum/use-ref nil)]
     (rum/use-effect!
-      (fn []
-        (when (true? visible?)
-          (shui/popup-show!
-            (rum/deref *el-trigger) render
-            {:root-props {:onOpenChange (fn [v] (set-visible! v))
-                          :modal false}
-             :content-props {:class "ls-preview-popup"
-                             :onInteractOutside (fn [^js e] (.preventDefault e))
-                             :onEscapeKeyDown (fn [^js e]
-                                                (when (state/editing?)
-                                                  (.preventDefault e)
-                                                  (some-> (rum/deref *el-popup) (.focus))))}
-             :as-dropdown? false}))
-
-        (when (false? visible?)
-          (shui/popup-hide!)
-          (when (state/get-edit-block)
-            (state/clear-edit!)))
-        (rum/set-ref! *timer nil)
-        (rum/set-ref! *timer1 nil)
+     (fn []
+       (when (true? visible?)
+         (shui/popup-show!
+          (rum/deref *el-trigger) render
+          {:root-props {:onOpenChange (fn [v] (set-visible! v))
+                        :modal false}
+           :content-props {:class "ls-preview-popup"
+                           :onInteractOutside (fn [^js e] (.preventDefault e))
+                           :onEscapeKeyDown (fn [^js e]
+                                              (when (state/editing?)
+                                                (.preventDefault e)
+                                                (some-> (rum/deref *el-popup) (.focus))))}
+           :as-dropdown? false}))
+
+       (when (false? visible?)
+         (shui/popup-hide!)
+         (when (state/get-edit-block)
+           (state/clear-edit!)))
+       (rum/set-ref! *timer nil)
+       (rum/set-ref! *timer1 nil)
         ;; teardown
-        (fn []
-          (when visible?
-            (shui/popup-hide!))))
-      [visible?])
+       (fn []
+         (when visible?
+           (shui/popup-hide!))))
+     [visible?])
 
     [:span.preview-ref-link
      {:ref *el-trigger
@@ -706,7 +704,7 @@
                               timer1 (rum/deref *timer1)]
                           (when-not timer
                             (rum/set-ref! *timer
-                              (js/setTimeout #(set-visible! true) 1000)))
+                                          (js/setTimeout #(set-visible! true) 1000)))
                           (when timer1
                             (js/clearTimeout timer1)
                             (rum/set-ref! *timer1 nil))))
@@ -718,7 +716,7 @@
                             (rum/set-ref! *timer nil))
                           (when-not timer1
                             (rum/set-ref! *timer1
-                              (js/setTimeout #(set-visible! false) 300)))))}
+                                          (js/setTimeout #(set-visible! false) 300)))))}
      children]))
 
 (rum/defc page-preview-trigger
@@ -738,27 +736,27 @@
                                   [])
 
                                  (when-let [source (or (db/get-alias-source-page (state/get-current-repo) (:db/id page-entity))
-                                                  page-entity)]
+                                                       page-entity)]
                                    [:div.tippy-wrapper.as-page
-                                      {:ref *el-popup
-                                       :tab-index -1
-                                       :style {:width 600
-                                               :text-align "left"
-                                               :font-weight 500
-                                               :padding-bottom 64}
-                                       :on-mouse-enter (fn []
-                                                         (when-let [timer1 (rum/deref *timer1)]
-                                                           (js/clearTimeout timer1)))
-                                       :on-mouse-leave (fn []
+                                    {:ref *el-popup
+                                     :tab-index -1
+                                     :style {:width 600
+                                             :text-align "left"
+                                             :font-weight 500
+                                             :padding-bottom 64}
+                                     :on-mouse-enter (fn []
+                                                       (when-let [timer1 (rum/deref *timer1)]
+                                                         (js/clearTimeout timer1)))
+                                     :on-mouse-leave (fn []
                                                          ;; check the top popup whether is the preview popup
-                                                         (when (ui/last-shui-preview-popup?)
-                                                           (rum/set-ref! *timer1
-                                                                         (js/setTimeout #(set-visible! false) 500))))}
-                                      (let [page-cp (state/get-page-blocks-cp)]
-                                        (page-cp {:repo (state/get-current-repo)
-                                                  :page-name (str (:block/uuid source))
-                                                  :sidebar? sidebar?
-                                                  :preview? true}))]))]
+                                                       (when (ui/last-shui-preview-popup?)
+                                                         (rum/set-ref! *timer1
+                                                                       (js/setTimeout #(set-visible! false) 500))))}
+                                    (let [page-cp (state/get-page-blocks-cp)]
+                                      (page-cp {:repo (state/get-current-repo)
+                                                :page-name (str (:block/uuid source))
+                                                :sidebar? sidebar?
+                                                :preview? true}))]))]
 
     (if (and (not (:preview? config))
              (or (not manual?) open?))
@@ -776,13 +774,13 @@
   (let [db-based? (config/db-based-graph? (state/get-current-repo))
         ->ref (if db-based? page-ref/->page-ref block-ref/->block-ref)]
     [:span.warning.mr-1 {:title "Node ref invalid"}
-   (->ref id)]))
+     (->ref id)]))
 
 (rum/defcs page-cp-inner < db-mixins/query rum/reactive
   "Component for a page. `page` argument contains :block/name which can be (un)sanitized page name.
    Keys for `config`:
    - `:preview?`: Is this component under preview mode? (If true, `page-preview-trigger` won't be registered to this `page-cp`)"
-  [state {:keys [label children preview? disable-preview?] :as config} page]
+  [state {:keys [label children preview? disable-preview? show-non-exists-page?] :as config} page]
   (let [entity (if (e/entity? page)
                  page
                  ;; Use uuid when available to uniquely identify case sensitive contexts
@@ -817,6 +815,10 @@
         (and (:block/name page) (util/uuid-string? (:block/name page)))
         (invalid-node-ref (:block/name page))
 
+        (and (:block/name page) show-non-exists-page?)
+        (page-inner config {:block/title (:block/name page)
+                            :block/name (:block/name page)} children label)
+
         :else
         nil))))
 
@@ -1020,15 +1022,15 @@
 
                                   :on-mouse-leave (fn []
                                                     (rum/set-ref! *timer1
-                                                      (js/setTimeout #(set-visible! false) 500)))}
+                                                                  (js/setTimeout #(set-visible! false) 500)))}
                                  [(breadcrumb config repo id {:indent? true})
                                   (blocks-container
-                                    (assoc config :id (str id) :preview? true)
-                                    [(db/entity [:block/uuid id])])]])]
+                                   (assoc config :id (str id) :preview? true)
+                                   [(db/entity [:block/uuid id])])]])]
     (popup-preview-impl children
-      {:visible? visible? :set-visible! set-visible!
-       :*timer *timer :*timer1 *timer1
-       :render render})))
+                        {:visible? visible? :set-visible! set-visible!
+                         :*timer *timer :*timer1 *timer1
+                         :render render})))
 
 (rum/defc block-reference < rum/reactive db-mixins/query
   {:init (fn [state]
@@ -1225,24 +1227,24 @@
                                         (state/set-current-pdf! current)
                                         (util/stop e))))]
                         (-> (load$)
-                          (p/catch
-                            (fn [^js _e]
+                            (p/catch
+                             (fn [^js _e]
                               ;; load pdf asset to indexed db
-                              (p/let [[handle] (js/window.showOpenFilePicker
+                               (p/let [[handle] (js/window.showOpenFilePicker
                                                  (bean/->js {:multiple false :startIn "documents" :types [{:accept {"application/pdf" [".pdf"]}}]}))
-                                      file (.getFile handle)
-                                      buffer (.arrayBuffer file)]
-                                (when-let [content (some-> buffer (js/Uint8Array.))]
-                                  (let [repo (state/get-current-repo)
-                                        file-rpath (string/replace s #"^[.\/\\]*assets[\/\\]+" "assets/")
-                                        dir (config/get-repo-dir repo)]
-                                    (-> (fs/write-file! repo dir file-rpath content nil)
-                                      (p/then load$)))))
-                              (js/console.error _e)))))))
+                                       file (.getFile handle)
+                                       buffer (.arrayBuffer file)]
+                                 (when-let [content (some-> buffer (js/Uint8Array.))]
+                                   (let [repo (state/get-current-repo)
+                                         file-rpath (string/replace s #"^[.\/\\]*assets[\/\\]+" "assets/")
+                                         dir (config/get-repo-dir repo)]
+                                     (-> (fs/write-file! repo dir file-rpath content nil)
+                                         (p/then load$)))))
+                               (js/console.error _e)))))))
         :draggable true
         :on-drag-start #(.setData (gobj/get % "dataTransfer") "file" s)}
        (or label-text
-         (->elem :span (map-inline config label)))]
+           (->elem :span (map-inline config label)))]
 
       (contains? config/doc-formats ext)
       (asset-link config label-text s metadata full_text)
@@ -1264,7 +1266,7 @@
 
     ;; FIXME: same headline, see more https://orgmode.org/manual/Internal-Links.html
     (and (= \* (first s))
-      (not= \* (last s)))
+         (not= \* (last s)))
     (->elem :a {:on-click #(route-handler/jump-to-anchor! (mldoc/anchorLink (subs s 1)))} (subs s 1))
 
     (block-ref/block-ref? s)
@@ -1278,7 +1280,7 @@
     (->elem :a {:href s
                 :data-href s
                 :target "_blank"}
-      (map-inline config label))
+            (map-inline config label))
 
     (show-link? config metadata s full_text)
     (media-link config url s label metadata full_text)
@@ -1294,9 +1296,9 @@
                  :else
                  (relative-assets-path->absolute-path s))]
       (->elem
-        :a
-        (cond->
-          {:href (path/path-join "file://" path)
+       :a
+       (cond->
+        {:href (path/path-join "file://" path)
          :data-href path
          :target    "_blank"}
          title
@@ -1880,7 +1882,7 @@
     :else
     (when uuid
       (-> (or on-redirect-to-page route-handler/redirect-to-page!)
-        (apply [(str uuid)])))))
+          (apply [(str uuid)])))))
 
 (declare block-list)
 (rum/defc block-children < rum/reactive
@@ -1975,7 +1977,8 @@
                           (and (some? icon)
                                (or (db/page? block)
                                    (:logseq.property/icon block)
-                                   link?))
+                                   link?
+                                   (some :logseq.property/icon (:block/tags block))))
                           icon
 
                           :else
@@ -2035,9 +2038,9 @@
 (declare block-content)
 
 (defn build-block-title
-  [config {:block/keys [marker pre-block? properties] :as t}]
-  (let [block-title (:block.temp/ast-title t)
-        config (assoc config :block t)
+  [config {:block/keys [marker pre-block? properties] :as block}]
+  (let [block-title (:block.temp/ast-title block)
+        config (assoc config :block block)
         level (:level config)
         slide? (boolean (:slide? config))
         block-ref? (:block-ref? config)
@@ -2045,7 +2048,7 @@
         html-export? (:html-export? config)
         bg-color (pu/lookup properties :logseq.property/background-color)
         ;; `heading-level` is for backward compatibility, will remove it in later releases
-        heading-level (:block/heading-level t)
+        heading-level (:block/heading-level block)
         heading (or
                  (and heading-level
                       (<= heading-level 6)
@@ -2085,7 +2088,7 @@
                              (if (and area? (.contains (.-classList target) "blank"))
                                :actions
                                (do
-                                 (pdf-assets/open-block-ref! t)
+                                 (pdf-assets/open-block-ref! block)
                                  (util/stop e)))
 
                              :dune)))}
@@ -2098,20 +2101,20 @@
 
                       (when (and area?
                                  (pu/lookup properties :logseq.property.pdf/hl-stamp))
-                        (pdf-assets/area-display t))])]
+                        (pdf-assets/area-display block))])]
        (remove-nils
         (concat
          (when (config/local-file-based-graph? (state/get-current-repo))
-          [(when (and (not pre-block?)
-                      (not html-export?)
-                      (not slide?))
-             (file-block/block-checkbox t (str "mr-1 cursor")))
-           (when (and (not pre-block?)
-                      (not html-export?)
-                      (not slide?))
-             (file-block/marker-switch t))
-           (file-block/marker-cp t)
-           (file-block/priority-cp t)])
+           [(when (and (not pre-block?)
+                       (not html-export?)
+                       (not slide?))
+              (file-block/block-checkbox block (str "mr-1 cursor")))
+            (when (and (not pre-block?)
+                       (not html-export?)
+                       (not slide?))
+              (file-block/marker-switch block))
+            (file-block/marker-cp block)
+            (file-block/priority-cp block)])
 
          ;; highlight ref block (inline)
          (when-not area? [(hl-ref)])
@@ -2121,7 +2124,16 @@
           (when (= block-type :whiteboard-shape) [:span.mr-1 (ui/icon "whiteboard-element" {:extension? true})]))
 
          ;; highlight ref block (area)
-         (when area? [(hl-ref)])))))))
+         (when area? [(hl-ref)])
+
+         (when (and (seq block-title) (ldb/class-instance? (db/entity :logseq.class/Cards) block))
+           [(shui/button
+             {:variant :ghost
+              :size :sm
+              :class "ml-2 !px-1 !h-5 text-xs text-muted-foreground"
+              :on-click (fn [_]
+                          (state/pub-event! [:modal/show-cards (:db/id block)]))}
+             "Practice")])))))))
 
 (rum/defc span-comma
   []
@@ -2231,17 +2243,17 @@
 
   (let [*trigger-ref (rum/use-ref nil)]
     (rum/use-effect!
-      (fn []
-        (let [pid (shui/popup-show!
-                    (.closest (rum/deref *trigger-ref) "a")
-                    (datetime-comp/date-picker nil nil (repeated/timestamp->map ast))
-                    {:id :timestamp-editor
-                     :align :start
-                     :root-props {:onOpenChange #(reset! *show-datapicker? %)}
-                     :content-props {:onEscapeKeyDown #(reset! *show-datapicker? false)}})]
-          #(do (shui/popup-hide! pid)
-               (reset! *show-datapicker? false))))
-      [])
+     (fn []
+       (let [pid (shui/popup-show!
+                  (.closest (rum/deref *trigger-ref) "a")
+                  (datetime-comp/date-picker nil nil (repeated/timestamp->map ast))
+                  {:id :timestamp-editor
+                   :align :start
+                   :root-props {:onOpenChange #(reset! *show-datapicker? %)}
+                   :content-props {:onEscapeKeyDown #(reset! *show-datapicker? false)}})]
+         #(do (shui/popup-hide! pid)
+              (reset! *show-datapicker? false))))
+     [])
     [:i {:ref *trigger-ref}]))
 
 (rum/defcs timestamp-cp
@@ -2385,19 +2397,19 @@
       (-> block (dissoc :block/children :block/page)))]
 
     (let [title-collapse-enabled? (:outliner/block-title-collapse-enabled? (state/get-config))]
-        (when (and (not block-ref-with-title?)
-                   (seq body)
-                   (or (not title-collapse-enabled?)
-                       (and title-collapse-enabled?
-                            (or (not collapsed?)
-                                (some? (mldoc/extract-first-query-from-ast body))))))
-          [:div.block-body
-           (let [body (block/trim-break-lines! (:block.temp/ast-body block))
-                 uuid (:block/uuid block)]
-             (for [[idx child] (medley/indexed body)]
-               (when-let [block (markup-element-cp config child)]
-                 (rum/with-key (block-child block)
-                   (str uuid "-" idx)))))]))))
+      (when (and (not block-ref-with-title?)
+                 (seq body)
+                 (or (not title-collapse-enabled?)
+                     (and title-collapse-enabled?
+                          (or (not collapsed?)
+                              (some? (mldoc/extract-first-query-from-ast body))))))
+        [:div.block-body
+         (let [body (block/trim-break-lines! (:block.temp/ast-body block))
+               uuid (:block/uuid block)]
+           (for [[idx child] (medley/indexed body)]
+             (when-let [block (markup-element-cp config child)]
+               (rum/with-key (block-child block)
+                 (str uuid "-" idx)))))]))))
 
 (rum/defcs block-tag <
   (rum/local false ::hover?)
@@ -2500,21 +2512,21 @@
                      :other-position? true})]
     (when (seq properties)
       (case position
-         :block-below
-         [:div.positioned-properties.block-below.flex.flex-row.gap-1.item-center.flex-wrap.text-sm.overflow-x-hidden.max-h-6
-          (for [pid properties]
-            (let [property (db/entity pid)
-                  v (get block pid)]
-              [:div.flex.flex-row.items-center.gap-2.hover:bg-secondary.rounded
-               [:div.flex.flex-row.opacity-50.hover:opacity-100
-                (property-component/property-key-cp block property opts)
-                [:div.select-none ":"]]
-               (pv/property-value block property v opts)]))]
-         [:div.positioned-properties.flex.flex-row.gap-1.select-none.h-6
-          {:class (name position)}
-          (for [pid properties]
-            (when-let [property (db/entity pid)]
-              (pv/property-value block property (get block pid) (assoc opts :show-tooltip? true))))]))))
+        :block-below
+        [:div.positioned-properties.block-below.flex.flex-row.gap-1.item-center.flex-wrap.text-sm.overflow-x-hidden.max-h-6
+         (for [pid properties]
+           (let [property (db/entity pid)
+                 v (get block pid)]
+             [:div.flex.flex-row.items-center.gap-2.hover:bg-secondary.rounded
+              [:div.flex.flex-row.opacity-50.hover:opacity-100
+               (property-component/property-key-cp block property opts)
+               [:div.select-none ":"]]
+              (pv/property-value block property v opts)]))]
+        [:div.positioned-properties.flex.flex-row.gap-1.select-none.h-6
+         {:class (name position)}
+         (for [pid properties]
+           (when-let [property (db/entity pid)]
+             (pv/property-value block property (get block pid) (assoc opts :show-tooltip? true))))]))))
 
 (rum/defc ^:large-vars/cleanup-todo block-content < rum/reactive
   [config {:block/keys [uuid properties scheduled deadline format pre-block?] :as block} edit-input-id block-id slide?]
@@ -2641,7 +2653,7 @@
                                  (:db/id block)
                                  :block-ref)
                                 (swap! *hide-block-refs? not)))}
-      [:span.text-sm block-refs-count'])))
+                 [:span.text-sm block-refs-count'])))
 
 (rum/defc block-left-menu < rum/reactive
   [_config {:block/keys [uuid] :as _block}]
@@ -2811,7 +2823,7 @@
            :else
            (when-let [uuid (:block/uuid block)]
              (-> (or (:on-redirect-to-page config) route-handler/redirect-to-page!)
-               (apply [(str uuid)])))))}
+                 (apply [(str uuid)])))))}
    label])
 
 (rum/defc breadcrumb-separator
@@ -2861,10 +2873,10 @@
                                (if name
                                  [block (page-cp {} block)]
                                  (let [result (block/parse-title-and-body
-                                                                   uuid
-                                                                   (:block/format block)
-                                                                   (:block/pre-block? block)
-                                                                   title)
+                                               uuid
+                                               (:block/format block)
+                                               (:block/pre-block? block)
+                                               title)
                                        ast-body (:block.temp/ast-body result)
                                        ast-title (:block.temp/ast-title result)
                                        config (assoc config :block/uuid uuid)]
@@ -2874,14 +2886,14 @@
                                         (->elem :span.inline-wrap (map-inline config ast-title))
                                         (->elem :div (markup-elements-cp config ast-body))))]))))
               breadcrumbs (->> (into [] parents-props)
-                              (concat [page-name-props] (when more? [:more]))
-                              (filterv identity)
-                              (map (fn [x]
-                                     (if (and (vector? x) (second x))
-                                       (let [[block label] x]
-                                         (rum/with-key (breadcrumb-fragment config block label opts) (:block/uuid block)))
-                                       [:span.opacity-70 "⋯"])))
-                              (interpose (breadcrumb-separator)))]
+                               (concat [page-name-props] (when more? [:more]))
+                               (filterv identity)
+                               (map (fn [x]
+                                      (if (and (vector? x) (second x))
+                                        (let [[block label] x]
+                                          (rum/with-key (breadcrumb-fragment config block label opts) (:block/uuid block)))
+                                        [:span.opacity-70 "⋯"])))
+                               (interpose (breadcrumb-separator)))]
           (when (seq breadcrumbs)
             [:div.breadcrumb.block-parents
              {:class (when (seq breadcrumbs)
@@ -3269,6 +3281,15 @@
        [:div (when-not (:page-title? config) {:style {:padding-left 45}})
         (db-properties-cp config block {:in-block-container? true})])
 
+     (when (and db-based? (not collapsed?) (not (or table? property?)) (not (string/blank? (:block/title (:logseq.property/query block)))))
+       (let [query (:block/title (:logseq.property/query block))]
+         [:div.dsl-query {:style {:padding-left 42}}
+          (query/custom-query (wrap-query-components (assoc config
+                                                            :dsl-query? true
+                                                            :cards? (ldb/class-instance? (db/entity :logseq.class/Cards) block)))
+                              {:builder nil
+                               :query (query-builder-component/sanitize-q query)})]))
+
      (when-not (or (:hide-children? config) in-whiteboard? (or table? property?))
        (let [config' (-> (update config :level inc)
                          (dissoc :original-block :data))]
@@ -3321,14 +3342,14 @@
                   config)]
     (when (:block/uuid block)
       (ui/catch-error
-        (fn [^js error]
-          [:div.flex.flex-col.pl-6.my-1
-           [:code (str "#uuid\"" (:block/uuid block) "\"")]
-           [:code.flex.p-1.text-red-rx-09 "Block render error: " (.-message error)]])
-        (rum/with-key
-          (block-container-inner state repo config' block
-            {:navigating-block navigating-block :navigated? navigated?})
-          (str "block-inner" (:block/uuid block)))))))
+       (fn [^js error]
+         [:div.flex.flex-col.pl-6.my-1
+          [:code (str "#uuid\"" (:block/uuid block) "\"")]
+          [:code.flex.p-1.text-red-rx-09 "Block render error: " (.-message error)]])
+       (rum/with-key
+         (block-container-inner state repo config' block
+                                {:navigating-block navigating-block :navigated? navigated?})
+         (str "block-inner" (:block/uuid block)))))))
 
 (defn divide-lists
   [[f & l]]
@@ -3416,42 +3437,42 @@
 
   (let [tr (fn [elm cols]
              (->elem
-               :tr
-               (mapv (fn [col]
-                       (->elem
-                         elm
-                         {:scope "col"
-                          :class "org-left"}
-                         (map-inline config col)))
-                 cols)))
+              :tr
+              (mapv (fn [col]
+                      (->elem
+                       elm
+                       {:scope "col"
+                        :class "org-left"}
+                       (map-inline config col)))
+                    cols)))
         tb-col-groups (try
                         (mapv (fn [number]
                                 (let [col-elem [:col {:class "org-left"}]]
                                   (->elem
-                                    :colgroup
-                                    (repeat number col-elem))))
-                          col_groups)
+                                   :colgroup
+                                   (repeat number col-elem))))
+                              col_groups)
                         (catch :default _e
                           []))
         head (when header
                [:thead (tr :th header)])
         groups (mapv (fn [group]
                        (->elem
-                         :tbody
-                         (mapv #(tr :td %) group)))
-                 groups)]
+                        :tbody
+                        (mapv #(tr :td %) group)))
+                     groups)]
     [:div.table-wrapper.classic-table.force-visible-scrollbar.markdown-table
      (->elem
-       :table
-       {:class "table-auto"
-        :border 2
-        :cell-spacing 0
-        :cell-padding 6
-        :rules "groups"
-        :frame "hsides"}
-       (vec-cat
-         tb-col-groups
-         (cons head groups)))]))
+      :table
+      {:class "table-auto"
+       :border 2
+       :cell-spacing 0
+       :cell-padding 6
+       :rules "groups"
+       :frame "hsides"}
+      (vec-cat
+       tb-col-groups
+       (cons head groups)))]))
 
 (defn logbook-cp
   [log]
@@ -3763,26 +3784,26 @@
         *wrap-ref (rum/use-ref nil)]
 
     (rum/use-effect!
-      (fn []
+     (fn []
         ;; Try to fix virtuoso scrollable container blink for the block insertion at bottom
-        (when virtualized?
-          (let [^js *ob (volatile! nil)]
-            (js/setTimeout
-              (fn []
-                (when-let [_inst (rum/deref *virtualized-ref)]
-                  (when-let [^js target (.-firstElementChild (rum/deref *wrap-ref))]
-                    (let [set-wrap-h! #(when-let [ref (rum/deref *wrap-ref)] (set! (.-height (.-style ref)) %))
-                          set-wrap-h! (debounce set-wrap-h! 16)
-                          ob (js/ResizeObserver.
-                               (fn []
-                                 (when-let [h (and (rum/deref *wrap-ref)
+       (when virtualized?
+         (let [^js *ob (volatile! nil)]
+           (js/setTimeout
+            (fn []
+              (when-let [_inst (rum/deref *virtualized-ref)]
+                (when-let [^js target (.-firstElementChild (rum/deref *wrap-ref))]
+                  (let [set-wrap-h! #(when-let [ref (rum/deref *wrap-ref)] (set! (.-height (.-style ref)) %))
+                        set-wrap-h! (debounce set-wrap-h! 16)
+                        ob (js/ResizeObserver.
+                            (fn []
+                              (when-let [h (and (rum/deref *wrap-ref)
                                                 (.-height (.-style target)))]
                                    ;(prn "==>> debug: " h)
-                                   (set-wrap-h! h))))]
-                      (.observe ob target)
-                      (vreset! *ob ob))))))
-            #(some-> @*ob (.disconnect)))))
-      [])
+                                (set-wrap-h! h))))]
+                    (.observe ob target)
+                    (vreset! *ob ob))))))
+           #(some-> @*ob (.disconnect)))))
+     [])
 
     [:div.blocks-list-wrap
      {:data-level (or (:level config) 0)
@@ -3842,22 +3863,22 @@
         parent-blocks (group-by :block/parent page-blocks)]
     [:div.my-2.references-blocks-item {:key (str "page-" (:db/id page))}
      (let [items (for [[parent blocks] parent-blocks]
-                     (let [blocks' (map (fn [b]
-                                          (if (e/entity? b)
-                                            b
-                                            (update b :block/children
-                                              (fn [col]
-                                                (tree/non-consecutive-blocks->vec-tree col))))) blocks)]
-                       (rum/with-key
-                         (breadcrumb-with-container blocks' config)
-                         (:db/id parent))))]
+                   (let [blocks' (map (fn [b]
+                                        (if (e/entity? b)
+                                          b
+                                          (update b :block/children
+                                                  (fn [col]
+                                                    (tree/non-consecutive-blocks->vec-tree col))))) blocks)]
+                     (rum/with-key
+                       (breadcrumb-with-container blocks' config)
+                       (:db/id parent))))]
        (if page
          (ui/foldable
-           [:div.with-foldable-page
-            (page-cp config page)
-            (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
-           items
-           {:debug-id page})
+          [:div.with-foldable-page
+           (page-cp config page)
+           (when alias? [:span.text-sm.font-medium.opacity-50 " Alias"])]
+          items
+          {:debug-id page})
          [:div.only-page-blocks items]))]))
 
 ;; headers to hiccup

+ 57 - 60
src/main/frontend/components/container.cljs

@@ -25,7 +25,6 @@
             [frontend.db.model :as db-model]
             [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.storage :as storage]
-            [frontend.extensions.srs :as srs]
             [frontend.handler.common :as common-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.page :as page-handler]
@@ -58,6 +57,7 @@
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
             [logseq.db :as ldb]
+            [frontend.extensions.fsrs :as fsrs]
             [logseq.common.util.namespace :as ns-util]))
 
 (rum/defc nav-content-item < rum/reactive
@@ -213,16 +213,13 @@
           :data-ref name}
          (page-name page (icon/get-node-icon-cp page {}) true)])])))
 
-(rum/defcs flashcards < db-mixins/query rum/reactive
-  {:did-mount (fn [state]
-                (srs/update-cards-due-count!)
-                state)}
-  [_state srs-open?]
+(rum/defc flashcards < db-mixins/query rum/reactive
+  [srs-open?]
   (let [num (state/sub :srs/cards-due-count)]
     [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
      {:class (util/classnames [{:active srs-open?}])
       :on-click #(do
-                   (srs/update-cards-due-count!)
+                   (fsrs/update-due-cards-count)
                    (state/pub-event! [:modal/show-cards]))}
      (ui/icon "infinity")
      [:span.flex-1 (t :right-side-bar/flashcards)]
@@ -335,59 +332,59 @@
         (repo/repos-dropdown)
 
         [:div.nav-header.flex.flex-col.mt-1
-           (let [page (:page default-home)]
-             (if (and page (not (state/enable-journals? (state/get-current-repo))))
-               (sidebar-item
-                {:class "home-nav"
-                 :title page
-                 :on-click-handler route-handler/redirect-to-home!
-                 :active (and (not srs-open?)
-                              (= route-name :page)
-                              (= page (get-in route-match [:path-params :name])))
-                 :icon "home"
-                 :shortcut :go/home})
-               (sidebar-item
-                {:class "journals-nav"
-                 :active (and (not srs-open?)
-                              (or (= route-name :all-journals) (= route-name :home)))
-                 :title (t :left-side-bar/journals)
-                 :on-click-handler (fn [e]
-                                     (if (gobj/get e "shiftKey")
-                                       (route-handler/sidebar-journals!)
-                                       (route-handler/go-to-journals!)))
-                 :icon "calendar"
-                 :shortcut :go/journals})))
-
-           (when enable-whiteboards?
-             (when (or config/dev? (not db-based?))
-               (sidebar-item
-                {:class "whiteboard"
-                 :title (t :right-side-bar/whiteboards)
-                 :href (rfe/href :whiteboards)
-                 :on-click-handler (fn [_e] (whiteboard-handler/onboarding-show))
-                 :active (and (not srs-open?) (#{:whiteboard :whiteboards} route-name))
-                 :icon "whiteboard"
-                 :icon-extension? true
-                 :shortcut :go/whiteboards})))
-
-           (when (and (state/enable-flashcards? (state/get-current-repo)) (not db-based?))
-             [:div.flashcards-nav
-              (flashcards srs-open?)])
-
-           (sidebar-item
-            {:class "graph-view-nav"
-             :title (t :right-side-bar/graph-view)
-             :href (rfe/href :graph)
-             :active (and (not srs-open?) (= route-name :graph))
-             :icon "hierarchy"
-             :shortcut :go/graph-view})
-
-           (sidebar-item
-            {:class "all-pages-nav"
-             :title (t :right-side-bar/all-pages)
-             :href (rfe/href :all-pages)
-             :active (and (not srs-open?) (= route-name :all-pages))
-             :icon "files"})]]
+         (let [page (:page default-home)]
+           (if (and page (not (state/enable-journals? (state/get-current-repo))))
+             (sidebar-item
+              {:class "home-nav"
+               :title page
+               :on-click-handler route-handler/redirect-to-home!
+               :active (and (not srs-open?)
+                            (= route-name :page)
+                            (= page (get-in route-match [:path-params :name])))
+               :icon "home"
+               :shortcut :go/home})
+             (sidebar-item
+              {:class "journals-nav"
+               :active (and (not srs-open?)
+                            (or (= route-name :all-journals) (= route-name :home)))
+               :title (t :left-side-bar/journals)
+               :on-click-handler (fn [e]
+                                   (if (gobj/get e "shiftKey")
+                                     (route-handler/sidebar-journals!)
+                                     (route-handler/go-to-journals!)))
+               :icon "calendar"
+               :shortcut :go/journals})))
+
+         (when enable-whiteboards?
+           (when (or config/dev? (not db-based?))
+             (sidebar-item
+              {:class "whiteboard"
+               :title (t :right-side-bar/whiteboards)
+               :href (rfe/href :whiteboards)
+               :on-click-handler (fn [_e] (whiteboard-handler/onboarding-show))
+               :active (and (not srs-open?) (#{:whiteboard :whiteboards} route-name))
+               :icon "whiteboard"
+               :icon-extension? true
+               :shortcut :go/whiteboards})))
+
+         (when (state/enable-flashcards? (state/get-current-repo))
+           [:div.flashcards-nav
+            (flashcards srs-open?)])
+
+         (sidebar-item
+          {:class "graph-view-nav"
+           :title (t :right-side-bar/graph-view)
+           :href (rfe/href :graph)
+           :active (and (not srs-open?) (= route-name :graph))
+           :icon "hierarchy"
+           :shortcut :go/graph-view})
+
+         (sidebar-item
+          {:class "all-pages-nav"
+           :title (t :right-side-bar/all-pages)
+           :href (rfe/href :all-pages)
+           :active (and (not srs-open?) (= route-name :all-pages))
+           :icon "files"})]]
 
        [:div.nav-contents-container.flex.flex-col.gap-1.pt-1
         {:on-scroll on-contents-scroll}

+ 8 - 3
src/main/frontend/components/content.cljs

@@ -31,7 +31,8 @@
             [logseq.common.util.page-ref :as page-ref]
             [promesa.core :as p]
             [rum.core :as rum]
-            [logseq.db :as ldb]))
+            [logseq.db :as ldb]
+            [frontend.extensions.fsrs :as fsrs]))
 
 ;; TODO i18n support
 
@@ -100,7 +101,9 @@
      (when (state/enable-flashcards?)
        (shui/dropdown-menu-item
         {:key "Make a Card"
-         :on-click #(srs/batch-make-cards!)}
+         :on-click #(if (config/db-based-graph? (state/get-current-repo))
+                      (fsrs/batch-make-cards!)
+                      (srs/batch-make-cards!))}
         (t :context-menu/make-a-flashcard)))
 
      (shui/dropdown-menu-item
@@ -289,7 +292,9 @@
            (state/enable-flashcards?)
            (shui/dropdown-menu-item
             {:key      "Make a Card"
-             :on-click #(srs/make-block-a-card! block-id)}
+             :on-click #(if (config/db-based-graph? (state/get-current-repo))
+                          (fsrs/batch-make-cards! [block-id])
+                          (srs/batch-make-cards! [block-id]))}
             (t :context-menu/make-a-flashcard))
            :else
            nil)

+ 13 - 18
src/main/frontend/components/property.cljs

@@ -33,7 +33,6 @@
             [promesa.core :as p]
             [rum.core :as rum]))
 
-
 (defn- property-type-label
   [property-type]
   (case property-type
@@ -265,17 +264,17 @@
      (when-not other-position?
        (let [content-fn (fn [{:keys [id]}]
                           (icon-component/icon-search
-                            {:on-chosen
-                             (fn [_e icon]
-                               (if icon
-                                 (db-property-handler/upsert-property! (:db/ident property)
-                                   (:block/schema property)
-                                   {:properties {:logseq.property/icon icon}})
-                                 (db-property-handler/remove-block-property! (:db/id property)
-                                   (pu/get-pid :logseq.property/icon)))
-                               (shui/popup-hide! id))
-                             :icon-value icon
-                             :del-btn? (boolean icon)}))]
+                           {:on-chosen
+                            (fn [_e icon]
+                              (if icon
+                                (db-property-handler/upsert-property! (:db/ident property)
+                                                                      (:block/schema property)
+                                                                      {:properties {:logseq.property/icon icon}})
+                                (db-property-handler/remove-block-property! (:db/id property)
+                                                                            (pu/get-pid :logseq.property/icon)))
+                              (shui/popup-hide! id))
+                            :icon-value icon
+                            :del-btn? (boolean icon)}))]
 
          (shui/trigger-as
           :button.property-m
@@ -333,10 +332,6 @@
         *show-new-property-config? (::show-new-property-config? state)
         *show-class-select? (::show-class-select? state)
         *property-schema (::property-schema state)
-        existing-tag-alias (->> db-property/db-attribute-properties
-                                (map db-property/built-in-properties)
-                                (keep #(when (get block (:attribute %)) (:title %)))
-                                set)
         block-type (keyword (get block :block/type :block))
         page? (ldb/page? block)
         exclude-properties (fn [m]
@@ -344,7 +339,7 @@
                                    block-types (if (and page? (not= block-type :page))
                                                  #{:page block-type}
                                                  #{block-type})]
-                               (or (and (not page?) (contains? existing-tag-alias (:block/title m)))
+                               (or (and (not page?) (contains? #{:block/alias} (:block/title m)))
                                    ;; Filters out properties from being in wrong :view-context and :never view-contexts
                                    (and (not= view-context :all) (not (contains? block-types view-context))))))
         property (rum/react *property)
@@ -534,7 +529,7 @@
   [block]
   (let [repo (state/get-current-repo)
         db (db/get-db repo)
-        classes (concat (:block/tags block) (outliner-property/get-class-parents db (:block/tags block)))]
+        classes (concat (:block/tags block) (outliner-property/get-classes-parents db (:block/tags block)))]
     (doseq [class classes]
       (db-async/<get-block repo (:db/id class) :children? false))
     (when (ldb/class? block)

+ 126 - 95
src/main/frontend/components/property/value.cljs

@@ -9,6 +9,7 @@
             [frontend.db.model :as model]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.page :as page-handler]
+            [frontend.handler.block :as block-handler]
             [frontend.handler.property :as property-handler]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.db-based.page :as db-page-handler]
@@ -32,7 +33,9 @@
             [goog.functions :refer [debounce]]
             [frontend.handler.route :as route-handler]
             [frontend.components.title :as title]
-            [cljs-time.coerce :as tc]))
+            [cljs-time.coerce :as tc]
+            [frontend.modules.outliner.ui :as ui-outliner-tx]
+            [frontend.components.query.builder :as query-builder-component]))
 
 (rum/defc property-empty-btn-value
   [property & opts]
@@ -61,45 +64,45 @@
         on-chosen! (fn [_e icon]
                      (if icon
                        (db-property-handler/set-block-property!
-                         (:db/id block)
-                         :logseq.property/icon
-                         (select-keys icon [:type :id :color]))
+                        (:db/id block)
+                        :logseq.property/icon
+                        (select-keys icon [:type :id :color]))
                        (db-property-handler/remove-block-property!
-                         (:db/id block)
-                         :logseq.property/icon))
+                        (:db/id block)
+                        :logseq.property/icon))
                      (clear-overlay!))]
 
     (rum/use-effect!
-      (fn []
-        (when editing?
-          (clear-overlay!)
-          (let [^js container (or (some-> js/document.activeElement (.closest ".page"))
-                                (gdom/getElement "main-content-container"))
-                icon (get block (pu/get-pid :logseq.property/icon))]
-            (util/schedule
-              (fn []
-                (when-let [^js target (some-> (.querySelector container (str "#ls-block-" (str (:block/uuid block))))
-                                        (.querySelector ".block-main-container"))]
-                  (shui/popup-show! target
-                    #(icon-component/icon-search
-                       {:on-chosen on-chosen!
-                        :icon-value icon
-                        :del-btn? (some? icon)})
-                    {:id :ls-icon-picker
-                     :align :start})))))))
-      [editing?])
+     (fn []
+       (when editing?
+         (clear-overlay!)
+         (let [^js container (or (some-> js/document.activeElement (.closest ".page"))
+                                 (gdom/getElement "main-content-container"))
+               icon (get block (pu/get-pid :logseq.property/icon))]
+           (util/schedule
+            (fn []
+              (when-let [^js target (some-> (.querySelector container (str "#ls-block-" (str (:block/uuid block))))
+                                            (.querySelector ".block-main-container"))]
+                (shui/popup-show! target
+                                  #(icon-component/icon-search
+                                    {:on-chosen on-chosen!
+                                     :icon-value icon
+                                     :del-btn? (some? icon)})
+                                  {:id :ls-icon-picker
+                                   :align :start})))))))
+     [editing?])
 
     [:div.col-span-3.flex.flex-row.items-center.gap-2
      (icon-component/icon-picker icon-value
-       {:disabled? config/publishing?
-        :del-btn? (some? icon-value)
-        :on-chosen on-chosen!})]))
+                                 {:disabled? config/publishing?
+                                  :del-btn? (some? icon-value)
+                                  :on-chosen on-chosen!})]))
 
 (defn- select-type?
   [property type]
   (or (contains? #{:node :number :url :date :page :class :property} type)
     ;; closed values
-    (seq (:property/closed-values property))))
+      (seq (:property/closed-values property))))
 
 (defn <create-new-block!
   [block property value & {:keys [edit-block?]
@@ -131,6 +134,15 @@
       (editor-handler/edit-block! block :max {:container-id :unknown-container}))
     block))
 
+(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 <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"
@@ -141,15 +153,20 @@
          class? (ldb/class? block)
          property (db/entity property-id)
          many? (db-property/many? property)
-         checkbox? (= :checkbox (get-in property [:block/schema :type]))]
+         checkbox? (= :checkbox (get-in property [:block/schema :type]))
+         blocks (get-operating-blocks block)]
      (assert (qualified-keyword? property-id) "property to add must be a keyword")
      (p/do!
       (if (and class? class-schema?)
         (db-property-handler/class-add-property! (:db/id block) property-id)
-        (if (and (db-property-type/user-ref-property-types (get-in property [:block/schema :type]))
-                 (string? property-value'))
-          (<create-new-block! block (db/entity property-id) property-value' {:edit-block? false})
-          (property-handler/set-block-property! repo (:block/uuid block) property-id property-value')))
+        (let [block-ids (map :block/uuid blocks)]
+          (if (and (db-property-type/user-ref-property-types (get-in property [:block/schema :type]))
+                   (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'))))
       (when exit-edit?
         (ui/hide-popups-until-preview-popup!)
         (shui/dialog-close!))
@@ -162,17 +179,21 @@
 
 (defn- add-or-remove-property-value
   [block property value selected? {:keys [refresh-result-f]}]
-  (let [many? (db-property/many? property)]
+  (let [many? (db-property/many? property)
+        blocks (get-operating-blocks block)]
     (p/do!
-      (if selected?
-        (<add-property! block (:db/ident property) value {:exit-edit? (not many?)})
-        (p/do!
-          (db-property-handler/delete-property-value! (:db/id block) (:db/ident property) value)
-          (when (or (not many?)
+     (if selected?
+       (<add-property! block (:db/ident property) value {:exit-edit? (not many?)})
+       (p/do!
+        (ui-outliner-tx/transact!
+         {:outliner-op :save-block}
+         (doseq [block blocks]
+           (db-property-handler/delete-property-value! (:db/id block) (:db/ident property) value)))
+        (when (or (not many?)
                   ;; values will be cleared
                   (and many? (<= (count (get block (:db/ident property))) 1)))
-            (shui/popup-hide!))))
-      (when (fn? refresh-result-f) (refresh-result-f)))))
+          (shui/popup-hide!))))
+     (when (fn? refresh-result-f) (refresh-result-f)))))
 
 (rum/defcs calendar-inner <
   (rum/local (str "calendar-inner-" (js/Date.now)) ::identity)
@@ -285,7 +306,8 @@
            [:div.flex.flex-row.gap-1.items-center
             (when-let [page-cp (state/get-component :block/page-cp)]
               (let [page-title (date/journal-name (date/js-date->goog-date date))]
-                (page-cp {:disable-preview? true}
+                (page-cp {:disable-preview? true
+                          :show-non-exists-page? true}
                          {:block/name page-title})))
             [:span.opacity-50
              (str (util/zero-pad (.getHours date))
@@ -382,8 +404,12 @@
              (if (or (and (not multiple-choices?) (= chosen clear-value))
                      (and multiple-choices? (= chosen [clear-value])))
                (p/do!
-                (property-handler/remove-block-property! (state/get-current-repo) (:block/uuid block)
-                                                         (:db/ident property))
+                (let [blocks (get-operating-blocks block)
+                      block-ids (map :block/uuid blocks)]
+                  (property-handler/batch-remove-block-property!
+                   (state/get-current-repo)
+                   block-ids
+                   (:db/ident property)))
                 (shui/popup-hide!))
                (f chosen selected?)))]
     (select/select (assoc opts
@@ -570,16 +596,16 @@
     (select-node property opts' *result)))
 
 (rum/defcs select < rum/reactive db-mixins/query
-                    {:init (fn [state]
-                             (let [*values (atom :loading)
-                                   refresh-result-f (fn []
-                                                      (p/let [result (db-async/<get-block-property-values (state/get-current-repo)
-                                                                       (:db/ident (nth (:rum/args state) 1)))]
-                                                        (reset! *values result)))]
-                               (refresh-result-f)
-                               (assoc state
-                                 ::values *values
-                                 ::refresh-result-f refresh-result-f)))}
+  {:init (fn [state]
+           (let [*values (atom :loading)
+                 refresh-result-f (fn []
+                                    (p/let [result (db-async/<get-block-property-values (state/get-current-repo)
+                                                                                        (:db/ident (nth (:rum/args state) 1)))]
+                                      (reset! *values result)))]
+             (refresh-result-f)
+             (assoc state
+                    ::values *values
+                    ::refresh-result-f refresh-result-f)))}
   [state block property
    {:keys [multiple-choices? dropdown? content-props] :as select-opts}
    {:keys [*show-new-property-config?]}]
@@ -604,55 +630,55 @@
                                :value (:db/id block)
                                :label-value value})) (:property/closed-values property))
                     (->> values
-                      (mapcat (fn [value]
-                                (if (coll? value)
-                                  (map (fn [v] {:value v}) value)
-                                  [{:value value}])))
-                      (map (fn [{:keys [value]}]
-                             (if (and ref-type? (number? value))
-                               (when-let [e (db/entity value)]
-                                 {:label (db-property/property-value-content e)
-                                  :value value})
-                               {:label value
-                                :value value})))
-                      (distinct)))
+                         (mapcat (fn [value]
+                                   (if (coll? value)
+                                     (map (fn [v] {:value v}) value)
+                                     [{:value value}])))
+                         (map (fn [{:keys [value]}]
+                                (if (and ref-type? (number? value))
+                                  (when-let [e (db/entity value)]
+                                    {:label (db-property/property-value-content e)
+                                     :value value})
+                                  {:label value
+                                   :value value})))
+                         (distinct)))
             items (->> (if (= :date type)
                          (map (fn [m] (let [label (:block/title (db/entity (:value m)))]
                                         (when label
                                           (assoc m :label label)))) items)
                          items)
-                    (remove nil?))
+                       (remove nil?))
             on-chosen (fn [chosen selected?]
                         (let [value (if (map? chosen) (:value chosen) chosen)]
                           (add-or-remove-property-value block property value selected?
-                            {:refresh-result-f refresh-result-f})))
+                                                        {:refresh-result-f refresh-result-f})))
             selected-choices' (get block (:db/ident property))
             selected-choices (if (every? de/entity? selected-choices')
                                (map :db/id selected-choices')
                                [selected-choices'])]
         (select-aux block property
-          {:multiple-choices? multiple-choices?
-           :items items
-           :selected-choices selected-choices
-           :dropdown? dropdown?
-           :show-new-when-not-exact-match? (not (or closed-values? (= :date type)))
-           :input-default-placeholder "Select"
-           :extract-chosen-fn :value
-           :extract-fn (fn [x] (or (:label-value x) (:label x)))
-           :content-props content-props
-           :on-chosen on-chosen
-           :input-opts (fn [_]
-                         {:on-blur (fn []
-                                     (when-let [f (:on-chosen select-opts)] (f)))
-                          :on-click (fn []
-                                      (when *show-new-property-config?
-                                        (reset! *show-new-property-config? false)))
-                          :on-key-down
-                          (fn [e]
-                            (case (util/ekey e)
-                              "Escape"
-                              (when-let [f (:on-chosen select-opts)] (f))
-                              nil))})})))))
+                    {:multiple-choices? multiple-choices?
+                     :items items
+                     :selected-choices selected-choices
+                     :dropdown? dropdown?
+                     :show-new-when-not-exact-match? (not (or closed-values? (= :date type)))
+                     :input-default-placeholder "Select"
+                     :extract-chosen-fn :value
+                     :extract-fn (fn [x] (or (:label-value x) (:label x)))
+                     :content-props content-props
+                     :on-chosen on-chosen
+                     :input-opts (fn [_]
+                                   {:on-blur (fn []
+                                               (when-let [f (:on-chosen select-opts)] (f)))
+                                    :on-click (fn []
+                                                (when *show-new-property-config?
+                                                  (reset! *show-new-property-config? false)))
+                                    :on-key-down
+                                    (fn [e]
+                                      (case (util/ekey e)
+                                        "Escape"
+                                        (when-let [f (:on-chosen select-opts)] (f))
+                                        nil))})})))))
 
 (rum/defcs property-normal-block-value <
   {:init (fn [state]
@@ -699,6 +725,11 @@
                                "Invalid block value, please delete the current property."]]
           (when v-block
             (cond
+              (= (:db/ident property) :logseq.property/query)
+              (let [query (:block/title v-block)]
+                (query-builder-component/builder query {:property property
+                                                        :block v-block}))
+
               (:block/page v-block)
               (property-normal-block-value block property v-block)
 
@@ -964,7 +995,7 @@
   (let [block (db/sub-block (:db/id block))
         value (get block (:db/ident property))
         value' (if (coll? value) value
-                 (when (some? value) #{value}))]
+                   (when (some? value) #{value}))]
     (multiple-values-inner block property value' opts schema)))
 
 (rum/defcs property-value < rum/reactive
@@ -1014,10 +1045,10 @@
                      :else
                      (let [parent? (= (:db/ident property) :logseq.property/parent)
                            value-cp (property-scalar-value block property v
-                                      (merge
-                                        opts
-                                        {:editor-id editor-id
-                                         :dom-id dom-id}))
+                                                           (merge
+                                                            opts
+                                                            {:editor-id editor-id
+                                                             :dom-id dom-id}))
                            page-ancestors (when parent?
                                             (let [ancestor-pages (loop [parents [block]]
                                                                    (if-let [parent (:logseq.property/parent (last parents))]

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

@@ -185,7 +185,7 @@
                                             :result result
                                             :collapsed? collapsed?'}))
 
-         (when dsl-query? builder)
+         (when (and dsl-query? builder) builder)
 
          (if built-in?
            [:div {:style {:margin-left 2}}

+ 64 - 52
src/main/frontend/components/query/builder.cljs

@@ -33,10 +33,10 @@
                {:label "Pages"
                 :value "page"
                 :selected (= @*find :page)}]
-     (fn [e v]
+              (fn [e v]
        ;; Prevent opening the current block's editor
-       (util/stop e)
-       (reset! *find (keyword v))))])
+                (util/stop e)
+                (reset! *find (keyword v))))])
 
 (defn- select
   ([items on-chosen]
@@ -83,10 +83,10 @@
 
 (defonce *between-dates (atom {}))
 (rum/defcs datepicker < rum/reactive
-                        (rum/local nil ::input-value)
-                        {:will-unmount (fn [state]
-                                         (swap! *between-dates dissoc (first (:rum/args state)))
-                                         state)}
+  (rum/local nil ::input-value)
+  {:will-unmount (fn [state]
+                   (swap! *between-dates dissoc (first (:rum/args state)))
+                   state)}
   [state id placeholder {:keys [auto-focus]}]
   (let [*input-value (::input-value state)]
     [:div.ml-4
@@ -97,24 +97,24 @@
        :value (some-> @*input-value (first))
        :on-focus (fn [^js e]
                    (js/setTimeout
-                     #(shui/popup-show! (.-target e)
-                        (let [select-handle! (fn [^js d]
-                                               (let [gd (date/js-date->goog-date d)
-                                                     journal-date (date/js-date->journal-title gd)]
-                                                 (reset! *input-value [journal-date d])
-                                                 (swap! *between-dates assoc id journal-date))
-                                               (shui/popup-hide!))]
-                          (shui/calendar
-                            {:mode "single"
-                             :initial-focus true
-                             :selected (some-> @*input-value (second))
-                             :on-select select-handle!
-                             :on-day-key-down (fn [^js d _ ^js e]
-                                                (when (= "Enter" (.-key e))
-                                                  (select-handle! d)
-                                                  (util/stop e)))}))
-                        {:id :query-datepicker
-                         :align :start}) 16))}]]))
+                    #(shui/popup-show! (.-target e)
+                                       (let [select-handle! (fn [^js d]
+                                                              (let [gd (date/js-date->goog-date d)
+                                                                    journal-date (date/js-date->journal-title gd)]
+                                                                (reset! *input-value [journal-date d])
+                                                                (swap! *between-dates assoc id journal-date))
+                                                              (shui/popup-hide!))]
+                                         (shui/calendar
+                                          {:mode "single"
+                                           :initial-focus true
+                                           :selected (some-> @*input-value (second))
+                                           :on-select select-handle!
+                                           :on-day-key-down (fn [^js d _ ^js e]
+                                                              (when (= "Enter" (.-key e))
+                                                                (select-handle! d)
+                                                                (util/stop e)))}))
+                                       {:id :query-datepicker
+                                        :align :start}) 16))}]]))
 
 (rum/defcs between <
   (rum/local nil ::start)
@@ -127,12 +127,12 @@
     (datepicker :end "End date" opts)]
    [:p.pt-2
     (ui/button "Submit"
-      :on-click (fn []
-                  (let [{:keys [start end]} @*between-dates]
-                    (when (and start end)
-                      (let [clause [:between [:page-ref start] [:page-ref end]]]
-                        (append-tree! tree opts loc clause)
-                        (reset! *between-dates {}))))))]])
+               :on-click (fn []
+                           (let [{:keys [start end]} @*between-dates]
+                             (when (and start end)
+                               (let [clause [:between [:page-ref start] [:page-ref end]]]
+                                 (append-tree! tree opts loc clause)
+                                 (reset! *between-dates {}))))))]])
 
 (rum/defc property-select
   [*mode *property]
@@ -276,7 +276,7 @@
 
        "full text search"
        (search (fn [v] (append-tree! *tree opts loc v))
-         (:toggle-fn opts))
+               (:toggle-fn opts))
 
        "between"
        (between (merge opts
@@ -330,7 +330,7 @@
 (rum/defc add-filter
   [*find *tree loc clause]
   (shui/button
-   {:class "!px-1 h-6 add-filter text-muted-foreground"
+   {:class "jtrigger !px-1 h-6 add-filter text-muted-foreground"
     :size :sm
     :variant :ghost
     :title "Add clause"
@@ -439,13 +439,13 @@
       [:div.flex.flex-row.gap-2
        (for [op query-builder/operators]
          (ui/button (string/upper-case (name op))
-           :intent "logseq"
-           :small? true
-           :on-click (fn []
-                       (swap! *tree (fn [q]
-                                      (let [loc' (if operator? (vec (butlast loc)) loc)]
-                                        (query-builder/wrap-operator q loc' op))))
-                       (toggle-fn))))]
+                    :intent "logseq"
+                    :small? true
+                    :on-click (fn []
+                                (swap! *tree (fn [q]
+                                               (let [loc' (if operator? (vec (butlast loc)) loc)]
+                                                 (query-builder/wrap-operator q loc' op))))
+                                (toggle-fn))))]
 
       (when operator?
         [:div
@@ -453,12 +453,12 @@
          [:div.flex.flex-row.gap-2
           (for [op (remove #{(keyword (string/lower-case clause))} query-builder/operators)]
             (ui/button (string/upper-case (name op))
-              :intent "logseq"
-              :small? true
-              :on-click (fn []
-                          (swap! *tree (fn [q]
-                                         (query-builder/replace-element q loc op)))
-                          (toggle-fn))))]])])
+                       :intent "logseq"
+                       :small? true
+                       :on-click (fn []
+                                   (swap! *tree (fn [q]
+                                                  (query-builder/replace-element q loc op)))
+                                   (toggle-fn))))]])])
    {:modal-class (util/hiccup->class
                   "origin-top-right.absolute.left-0.mt-2.ml-2.rounded-md.shadow-lg.w-64")}))
 
@@ -505,10 +505,20 @@
                           [:and [@tree]])]
     (clauses-group *tree *find [0] kind' clauses)))
 
+(defn sanitize-q
+  [q-str]
+  (if (string/blank? q-str)
+    ""
+    (if (or (common-util/wrapped-by-parens? q-str)
+            (common-util/wrapped-by-quotes? q-str)
+            (page-ref/page-ref? q-str))
+      q-str
+      (str "\"" q-str "\""))))
+
 (rum/defcs builder <
   (rum/local nil ::find)
   {:init (fn [state]
-           (let [q-str (first (:rum/args state))
+           (let [q-str (sanitize-q (first (:rum/args state)))
                  query (common-util/safe-read-string
                         query-dsl/custom-readers
                         (query-dsl/pre-transform-query q-str))
@@ -535,13 +545,15 @@
                                                  repo (state/get-current-repo)
                                                  block (db/pull [:block/uuid (:block/uuid block)])]
                                              (when block
-                                               (let [content (string/replace (:block/title block)
-                                                                             #"\{\{query[^}]+\}\}"
-                                                                             (util/format "{{query %s}}" q))]
-                                                 (editor-handler/save-block! repo (:block/uuid block) content)))))))
+                                               (if (:property config)
+                                                 (editor-handler/save-block! repo (:block/uuid block) q)
+                                                 (let [content (string/replace (:block/title block)
+                                                                               #"\{\{query[^}]+\}\}"
+                                                                               (util/format "{{query %s}}" q))]
+                                                   (editor-handler/save-block! repo (:block/uuid block) content))))))))
              (assoc state ::tree *tree)))
    :will-mount (fn [state]
-                 (let [q-str (first (:rum/args state))
+                 (let [q-str (sanitize-q (first (:rum/args state)))
                        blocks-query? (:blocks? (query-dsl/parse-query q-str))
                        find-mode (cond
                                    blocks-query?

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

@@ -47,7 +47,7 @@
                              *fulltext-query-result)
 
                            :else
-                           (query-dsl/query (state/get-current-repo) q)))
+                           (query-dsl/query (state/get-current-repo) q {:cards? (:cards? config)})))
 
                        :else
                        (query-custom/custom-query query {:current-block-uuid current-block-uuid}))

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

@@ -1120,7 +1120,7 @@
        (plugin-system-switcher-row))
      (when (util/electron?)
        (http-server-switcher-row))
-     (when-not db-based? (flashcards-switcher-row enable-flashcards?))
+     (flashcards-switcher-row enable-flashcards?)
      (when-not db-based? (zotero-settings-row))
      (when (and config/dev? (config/db-based-graph? current-repo))
        ;; FIXME: Wire this up again to RTC init calls

+ 102 - 104
src/main/frontend/components/views.cljs

@@ -132,7 +132,8 @@
         (keep
          (fn [property]
            (let [ident (or (:db/ident property) (:id property))]
-             (when-not (contains? #{:logseq.property/built-in?} ident)
+             (when-not (or (contains? #{:logseq.property/built-in?} ident)
+                           (contains? #{:map :entity} (get-in property [:block/schema :type])))
                (let [property (if (de/entity? property)
                                 property
                                 (or (db/entity ident) property))
@@ -268,76 +269,74 @@
         remove-resizing-class #(dom/remove-class! js/document.documentElement "is-resizing-buf")]
 
     (rum/use-effect!
-      (fn []
-        (when (number? dx)
-          (some-> (rum/deref *el)
-            (dom/set-style! :transform (str "translate3D(" dx "px , 0, 0)")))))
-      [dx])
+     (fn []
+       (when (number? dx)
+         (some-> (rum/deref *el)
+                 (dom/set-style! :transform (str "translate3D(" dx "px , 0, 0)")))))
+     [dx])
 
     (rum/use-effect!
-      (fn []
-        (when-let [el (and (fn? js/window.interact) (rum/deref *el))]
-          (let [*field-rect (atom nil)
-                min-width 120
-                max-width 500]
-            (-> (js/interact el)
-              (.draggable
+     (fn []
+       (when-let [el (and (fn? js/window.interact) (rum/deref *el))]
+         (let [*field-rect (atom nil)
+               min-width 120
+               max-width 500]
+           (-> (js/interact el)
+               (.draggable
                 (bean/->js
-                  {:listeners
-                   {:start (fn []
-                             (let [{:keys [width right] :as rect} (bean/->clj (.toJSON (.getBoundingClientRect (.closest el ".ls-table-header-cell"))))
-                                   left-dx (if (>= width min-width) (- min-width width) 0)
-                                   right-dx (if (<= width max-width) (- max-width width) 0)]
-                               (reset! *field-rect rect)
-                               (swap! *field-rect assoc
+                 {:listeners
+                  {:start (fn []
+                            (let [{:keys [width right] :as rect} (bean/->clj (.toJSON (.getBoundingClientRect (.closest el ".ls-table-header-cell"))))
+                                  left-dx (if (>= width min-width) (- min-width width) 0)
+                                  right-dx (if (<= width max-width) (- max-width width) 0)]
+                              (reset! *field-rect rect)
+                              (swap! *field-rect assoc
                                  ;; calculate left/right boundary
-                                 :left-dx left-dx
-                                 :right-dx right-dx
-                                 :left-b (inc (+ left-dx right))
-                                 :right-b (inc (+ right-dx right)))
-                               (dom/add-class! el "is-active")))
-                    :move (fn [^js e]
-                            (let [dx (.-dx e)
-                                  pointer-x (js/Math.floor (.-clientX e))
-                                  {:keys [left-b right-b]} @*field-rect
-                                  left-b (js/Math.floor left-b)
-                                  right-b (js/Math.floor right-b)]
-                              (when (and (> pointer-x left-b)
-                                      (< pointer-x right-b))
-                                (set-dx! (fn [dx']
-                                           (if (contains? #{min-width max-width} (abs dx'))
-                                             dx'
-                                             (let [to-dx (+ (or dx' 0) dx)
-                                                   {:keys [left-dx right-dx]} @*field-rect]
-                                               (cond
+                                     :left-dx left-dx
+                                     :right-dx right-dx
+                                     :left-b (inc (+ left-dx right))
+                                     :right-b (inc (+ right-dx right)))
+                              (dom/add-class! el "is-active")))
+                   :move (fn [^js e]
+                           (let [dx (.-dx e)
+                                 pointer-x (js/Math.floor (.-clientX e))
+                                 {:keys [left-b right-b]} @*field-rect
+                                 left-b (js/Math.floor left-b)
+                                 right-b (js/Math.floor right-b)]
+                             (when (and (> pointer-x left-b)
+                                        (< pointer-x right-b))
+                               (set-dx! (fn [dx']
+                                          (if (contains? #{min-width max-width} (abs dx'))
+                                            dx'
+                                            (let [to-dx (+ (or dx' 0) dx)
+                                                  {:keys [left-dx right-dx]} @*field-rect]
+                                              (cond
                                                  ;; left
-                                                 (neg? to-dx) (if (> (abs left-dx) (abs to-dx)) to-dx left-dx)
+                                                (neg? to-dx) (if (> (abs left-dx) (abs to-dx)) to-dx left-dx)
                                                  ;; right
-                                                 (pos? to-dx) (if (> right-dx to-dx) to-dx right-dx)
-                                                 ))))))))
-                    :end (fn []
-                           (set-dx!
-                             (fn [dx]
-                               (let [w (js/Math.round (+ dx (:width @*field-rect)))]
-                                 (set-width! (cond
-                                               (< w min-width) min-width
-                                               (> w max-width) max-width
-                                               :else w)))
-                               (reset! *field-rect nil)
-                               (dom/remove-class! el "is-active")
-                               0)))}}))
-              (.styleCursor false)
-              (.on "dragstart" add-resizing-class)
-              (.on "dragend" remove-resizing-class)
-              (.on "mousedown" util/stop-propagation)
-              ))))
-      [])
+                                                (pos? to-dx) (if (> right-dx to-dx) to-dx right-dx)))))))))
+                   :end (fn []
+                          (set-dx!
+                           (fn [dx]
+                             (let [w (js/Math.round (+ dx (:width @*field-rect)))]
+                               (set-width! (cond
+                                             (< w min-width) min-width
+                                             (> w max-width) max-width
+                                             :else w)))
+                             (reset! *field-rect nil)
+                             (dom/remove-class! el "is-active")
+                             0)))}}))
+               (.styleCursor false)
+               (.on "dragstart" add-resizing-class)
+               (.on "dragend" remove-resizing-class)
+               (.on "mousedown" util/stop-propagation)))))
+     [])
 
     (rum/use-effect!
-      (fn []
-        (when (number? width)
-          (on-sized! width)))
-      [width])
+     (fn []
+       (when (number? width)
+         (on-sized! width)))
+     [width])
 
     [:a.ls-table-resize-handle
      {:data-no-dnd true
@@ -364,27 +363,27 @@
                                    ;; resize handle
                                    (when-not (false? (:resizable? column))
                                      (column-resizer column
-                                       (fn [size]
-                                         (set-sized-columns! (assoc sized-columns (:id column) size)))))])
+                                                     (fn [size]
+                                                       (set-sized-columns! (assoc sized-columns (:id column) size)))))])
                        :disabled? (= (:id column) :select)}) columns)
         items (if show-add-property?
                 (conj items
-                  {:id "add property"
-                   :prop {:style {:width "-webkit-fill-available"
-                                  :min-width 160}
-                          :on-click (fn [] (when (fn? add-property!) (add-property!)))}
-                   :value :add-new-property
-                   :content (add-property-button)
-                   :disabled? true})
+                      {:id "add property"
+                       :prop {:style {:width "-webkit-fill-available"
+                                      :min-width 160}
+                              :on-click (fn [] (when (fn? add-property!) (add-property!)))}
+                       :value :add-new-property
+                       :content (add-property-button)
+                       :disabled? true})
                 items)
         selection-rows-count (count selected-rows)]
     (shui/table-header
-      (dnd/items items {:vertical? false
-                        :on-drag-end (fn [ordered-columns _m]
-                                       (set-ordered-columns! ordered-columns))})
-      (when (pos? selection-rows-count)
-        [:div.absolute.top-0.left-8
-         (action-bar table selected-rows option)]))))
+     (dnd/items items {:vertical? false
+                       :on-drag-end (fn [ordered-columns _m]
+                                      (set-ordered-columns! ordered-columns))})
+     (when (pos? selection-rows-count)
+       [:div.absolute.top-0.left-8
+        (action-bar table selected-rows option)]))))
 
 (rum/defc table-row < rum/reactive
   [{:keys [row-selected?] :as table} row columns props {:keys [show-add-property?]}]
@@ -452,10 +451,10 @@
 
 (comment
   (defn- property-ref-type?
-   [property]
-   (let [schema (:block/schema property)
-         type (:type schema)]
-     (db-property-type/all-ref-property-types type))))
+    [property]
+    (let [schema (:block/schema property)
+          type (:type schema)]
+      (db-property-type/all-ref-property-types type))))
 
 (defn- get-property-values
   [rows property]
@@ -807,7 +806,6 @@
                         (set-filters! new-filters))))
         :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})
 
-
       (filter-value-select table property value operator idx))))
 
 (rum/defc filters-row < rum/static
@@ -1008,27 +1006,27 @@
   [table option row-selection add-new-object! ready?]
   (let [selected-rows (shui/table-get-selection-rows row-selection (:rows table))]
     (shui/table
-    (let [columns' (:columns table)
-          rows (:rows table)]
-      [:div.ls-table-rows.content.overflow-x-auto.force-visible-scrollbar
-       {:class (when (not ready?) "invisible")}
-       [:div.relative
-        (table-header table columns' option selected-rows)
-
-        (ui/virtualized-list
-         {:custom-scroll-parent (gdom/getElement "main-content-container")
-          :increase-viewport-by 128
-          :overscan 128
-          :compute-item-key (fn [idx]
-                              (let [block (nth rows idx)]
-                                (str "table-row-" (:db/id block))))
-          :total-count (count rows)
-          :item-content (fn [idx]
-                          (let [row (nth rows idx)]
-                            (table-row table row columns' {} option)))})
-
-        (when add-new-object!
-          (shui/table-footer (add-new-row table)))]]))))
+     (let [columns' (:columns table)
+           rows (:rows table)]
+       [:div.ls-table-rows.content.overflow-x-auto.force-visible-scrollbar
+        {:class (when (not ready?) "invisible")}
+        [:div.relative
+         (table-header table columns' option selected-rows)
+
+         (ui/virtualized-list
+          {:custom-scroll-parent (gdom/getElement "main-content-container")
+           :increase-viewport-by 128
+           :overscan 128
+           :compute-item-key (fn [idx]
+                               (let [block (nth rows idx)]
+                                 (str "table-row-" (:db/id block))))
+           :total-count (count rows)
+           :item-content (fn [idx]
+                           (let [row (nth rows idx)]
+                             (table-row table row columns' {} option)))})
+
+         (when add-new-object!
+           (shui/table-footer (add-new-row table)))]]))))
 
 (rum/defc list-view < rum/static
   [view-entity result config]

+ 20 - 15
src/main/frontend/db/query_dsl.cljs

@@ -21,7 +21,6 @@
             [frontend.config :as config]
             [frontend.state :as state]))
 
-
 ;; Query fields:
 
 ;; Operators:
@@ -241,20 +240,19 @@
     (= 4 (count e))
     (build-between-three-arg e)))
 
-
 (defn ->file-property-value
   "Parses property values for file graphs and handles non-string values or any page-ref like values"
   [v*]
   (if (some? v*)
-   (let [v (str v*)
-         result (if-some [res (text/parse-non-string-property-value v)]
-                  res
-                  (if (string/starts-with? v "#")
-                    (subs v 1)
-                    (or (page-ref/get-page-name v) v)))]
-     (if (string? result)
-       (or (parse-double result) (string/trim result))
-       result))
+    (let [v (str v*)
+          result (if-some [res (text/parse-non-string-property-value v)]
+                   res
+                   (if (string/starts-with? v "#")
+                     (subs v 1)
+                     (or (page-ref/get-page-name v) v)))]
+      (if (string? result)
+        (or (parse-double result) (string/trim result))
+        result))
     v*))
 
 (defn ->db-property-value
@@ -323,7 +321,7 @@
       (if db-graph?
         (let [markers' (set (map (comp string/capitalize name) markers))]
           {:query (list 'task '?b (set markers'))
-          :rules [:task]})
+           :rules [:task]})
         (let [markers (set (map (comp string/upper-case name) markers))]
           {:query (list 'task '?b markers)
            :rules [:task]})))))
@@ -571,7 +569,7 @@ Some bindings in this fn:
 
 (def custom-readers {:readers {'tag (fn [x] (page-ref/->page-ref x))}})
 (defn parse
-  [s {:keys [db-graph?] :as opts}]
+  [s {:keys [db-graph? cards?] :as opts}]
   (when (and (string? s)
              (not (string/blank? s)))
     (let [s (if (= \# (first s)) (page-ref/->page-ref (subs s 1)) s)
@@ -586,7 +584,8 @@ Some bindings in this fn:
           (when form (build-query form {:sort-by sort-by
                                         :blocks? blocks?
                                         :db-graph? db-graph?
-                                        :sample sample}))
+                                        :sample sample
+                                        :cards? cards?}))
           result' (when (seq result)
                     (let [key (if (coll? (first result))
                                 ;; Only queries for this branch are not's like:
@@ -652,7 +651,13 @@ Some bindings in this fn:
    (query repo query-string {}))
   ([repo query-string query-opts]
    (when (and (string? query-string) (not= "\"\"" query-string))
-     (let [{query* :query :keys [rules sort-by blocks? sample]} (parse-query query-string)]
+     (let [{query* :query :keys [rules sort-by blocks? sample]} (parse-query query-string {:cards? (:cards? query-opts)})
+           query* (if (:cards? query-opts)
+                    (let [card-id (:db/id (db-utils/entity :logseq.class/Card))]
+                      (util/concat-without-nil
+                       [['?b :block/tags card-id]]
+                       (if (coll? (first query*)) query* [query*])))
+                    query*)]
        (when-let [query' (some-> query* (query-wrapper {:blocks? blocks?
                                                         :block-attrs (when (config/db-based-graph? repo)
                                                                        db-block-attrs)}))]

+ 391 - 0
src/main/frontend/extensions/fsrs.cljs

@@ -0,0 +1,391 @@
+(ns frontend.extensions.fsrs
+  "Flashcards functions based on FSRS, only works in db-based graphs"
+  (:require [clojure.string :as string]
+            [frontend.common.missionary-util :as c.m]
+            [frontend.components.block :as component-block]
+            [frontend.config :as config]
+            [frontend.context.i18n :refer [t]]
+            [frontend.db :as db]
+            [frontend.db-mixins :as db-mixins]
+            [frontend.db.async :as db-async]
+            [frontend.db.model :as db-model]
+            [frontend.db.query-dsl :as query-dsl]
+            [frontend.extensions.srs :as srs]
+            [frontend.handler.block :as block-handler]
+            [frontend.handler.property :as property-handler]
+            [frontend.modules.shortcut.core :as shortcut]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [logseq.db :as ldb]
+            [logseq.shui.ui :as shui]
+            [missionary.core :as m]
+            [open-spaced-repetition.cljc-fsrs.core :as fsrs.core]
+            [promesa.core :as p]
+            [rum.core :as rum]
+            [tick.core :as tick]))
+
+(def ^:private instant->inst-ms (comp inst-ms tick/inst))
+(defn- inst-ms->instant [ms] (tick/instant (js/Date. ms)))
+
+(defn- fsrs-card-map->property-fsrs-state
+  "Convert card-map to value stored in property"
+  [fsrs-card-map]
+  (-> fsrs-card-map
+      (update :last-repeat instant->inst-ms)
+      (update :due instant->inst-ms)))
+
+(defn- property-fsrs-state->fsrs-card-map
+  "opposite version of `fsrs-card->property-fsrs-state`"
+  [prop-fsrs-state]
+  (-> prop-fsrs-state
+      (update :last-repeat inst-ms->instant)
+      (update :due inst-ms->instant)))
+
+(defn- get-card-map
+  "Return nil if block is not #card.
+  Return default card-map if `:logseq.property.fsrs/state` or `:logseq.property.fsrs/due` is nil"
+  [block-entity]
+  (when (some (fn [tag]
+                (assert (some? (:db/ident tag)) tag)
+                (= :logseq.class/Card (:db/ident tag))) ;block should contains #Card
+              (:block/tags block-entity))
+    (let [fsrs-state (:logseq.property.fsrs/state block-entity)
+          fsrs-due (:logseq.property.fsrs/due block-entity)
+          return-default-card-map? (not (and fsrs-state fsrs-due))]
+      (if return-default-card-map?
+        (if-let [block-created-at (some-> (:block/created-at block-entity) (js/Date.) tick/instant)]
+          (assoc (fsrs.core/new-card!)
+                 :last-repeat block-created-at
+                 :due block-created-at)
+          (fsrs.core/new-card!))
+        (property-fsrs-state->fsrs-card-map (assoc fsrs-state :due fsrs-due))))))
+
+(defn- repeat-card!
+  [repo block-id rating]
+  (let [eid (if (uuid? block-id) [:block/uuid block-id] block-id)
+        block-entity (db/entity repo eid)]
+    (when-let [card-map (get-card-map block-entity)]
+      (let [next-card-map (fsrs.core/repeat-card! card-map rating)
+            prop-card-map (fsrs-card-map->property-fsrs-state next-card-map)
+            prop-fsrs-state (-> prop-card-map
+                                (dissoc :due)
+                                (assoc :logseq/last-rating rating))
+            prop-fsrs-due (:due prop-card-map)]
+        (property-handler/set-block-properties!
+         repo (:block/uuid block-entity)
+         {:logseq.property.fsrs/state prop-fsrs-state
+          :logseq.property.fsrs/due prop-fsrs-due})))))
+
+(defn- <get-due-card-block-ids
+  [repo cards-id]
+  (let [now-inst-ms (inst-ms (js/Date.))
+        cards (when (and cards-id (not= (keyword cards-id) :global)) (db/entity cards-id))
+        query (:block/title (:logseq.property/query cards))
+        result (query-dsl/parse query {:db-graph? true})
+        q '[:find [?b ...]
+            :in $ ?now-inst-ms %
+            :where
+            [?b :block/tags :logseq.class/Card]
+            (or-join [?b ?now-inst-ms]
+                     (and
+                      [?b :logseq.property.fsrs/due ?due]
+                      [(>= ?now-inst-ms ?due)])
+                     [(missing? $ ?b :logseq.property.fsrs/due)])
+            [?b :block/uuid]]
+        q' (if query
+             (let [query* (:query result)]
+               (util/concat-without-nil
+                q
+                (if (coll? (first query*)) query* [query*])))
+             q)]
+    (db-async/<q repo {:transact-db? false} q' now-inst-ms (:rules result))))
+
+(defn- btn-with-shortcut [{:keys [shortcut id btn-text background on-click class]}]
+  (shui/button
+   {:variant :outline
+    :auto-focus false
+    :size :sm
+    :id id
+    :class (str id " " class " !px-2 !py-1")
+    :background background
+    :on-pointer-down (fn [e] (util/stop-propagation e))
+    :on-click (fn [_e]
+                (js/setTimeout #(on-click) 10))}
+   [:div.flex.flex-row.items-center.gap-1
+    [:span btn-text]
+    (when-not (util/sm-breakpoint?)
+      (shui/button
+       {:variant :outline
+        :tab-index -1
+        :auto-focus false
+        :class "text-muted-foreground !px-1 !py-0 !h-4"
+        :size :sm}
+       [:span.text-sm shortcut]))]))
+
+(defn- has-cloze?
+  [block]
+  (string/includes? (:block/title block) "{{cloze "))
+
+(defn- phase->next-phase
+  [block phase]
+  (let [cloze? (has-cloze? block)]
+    (case phase
+      :init
+      (if cloze? :show-cloze :show-answer)
+      :show-cloze
+      (if cloze? :show-answer :init)
+      :show-answer
+      :init)))
+
+(def ^:private rating->shortcut
+  {:again "1"
+   :hard  "2"
+   :good  "3"
+   :easy  "4"})
+
+(defn- rating-btns
+  [repo block-id *card-index *phase]
+  [:div.flex.flex-row.items-center.gap-2.flex-wrap
+   (mapv
+    (fn [rating]
+      (btn-with-shortcut {:btn-text (string/capitalize (name rating))
+                          :shortcut (rating->shortcut rating)
+                          :id (str "card-" (name rating))
+                          :on-click #(do (repeat-card! repo block-id rating)
+                                         (swap! *card-index inc)
+                                         (reset! *phase :init))}))
+    (keys rating->shortcut))
+   (shui/button
+    {:variant :ghost
+     :size :sm
+     :class "!px-0 text-muted-foreground !h-4"
+     :on-click (fn [e]
+                 (shui/popup-show! (.-target e)
+                                   (fn []
+                                     [:div.p-4.max-w-lg
+                                      [:dl
+                                       [:dt "Again"]
+                                       [:dd "We got the answer wrong. Automatically means that we have forgotten the card. This is a lapse in memory."]]
+                                      [:dl
+                                       [:dt "Hard"]
+                                       [:dd "The answer was only partially correct and/or we took too long to recall it."]]
+                                      [:dl
+                                       [:dt "Good"]
+                                       [:dd "The answer was correct but we were not confident about it."]]
+                                      [:dl
+                                       [:dt "Easy"]
+                                       [:dd "The answer was correct and we were confident and quick in our recall."]]])
+                                   {:align "start"}))}
+    (ui/icon "info-circle"))])
+
+(rum/defcs ^:private card-view < rum/reactive db-mixins/query
+  {:will-mount (fn [state]
+                 (when-let [[repo block-id _] (:rum/args state)]
+                   (db-async/<get-block repo block-id))
+                 state)}
+  [state repo block-id *card-index *phase]
+  (when-let [block-entity (db/sub-block block-id)]
+    (let [phase (rum/react *phase)
+          next-phase (phase->next-phase block-entity phase)]
+      [:div.ls-card.content
+       [:div (component-block/breadcrumb {} repo (:block/uuid block-entity) {})]
+       (let [option (case phase
+                      :init
+                      {:hide-children? true}
+                      :show-cloze
+                      {:show-cloze? true
+                       :hide-children? true}
+                      {:show-cloze? true})]
+         (component-block/blocks-container option [block-entity]))
+       [:div.mt-8
+        (if (contains? #{:show-cloze :show-answer} next-phase)
+          (btn-with-shortcut {:btn-text (t
+                                         (case next-phase
+                                           :show-answer
+                                           :flashcards/modal-btn-show-answers
+                                           :show-cloze
+                                           :flashcards/modal-btn-show-clozes
+                                           :init
+                                           :flashcards/modal-btn-hide-answers))
+                              :shortcut "s"
+                              :id (str "card-answers")
+                              :on-click #(swap! *phase
+                                                (fn [phase]
+                                                  (phase->next-phase block-entity phase)))})
+          (rating-btns repo (:db/id block-entity) *card-index *phase))]])))
+
+(declare update-due-cards-count)
+(rum/defcs cards-view < rum/reactive
+  (rum/local 0 ::card-index)
+  (shortcut/mixin :shortcut.handler/cards false)
+  {:init (fn [state]
+           (let [*block-ids (atom nil)
+                 *loading? (atom nil)
+                 cards-id (last (:rum/args state))]
+             (reset! *loading? true)
+             (p/let [result (<get-due-card-block-ids (state/get-current-repo) cards-id)]
+               (reset! *block-ids result)
+               (reset! *loading? false))
+             (assoc state
+                    ::block-ids *block-ids
+                    ::cards-id (atom (or cards-id :global))
+                    ::loading? *loading?)))
+   :will-unmount (fn [state]
+                   (update-due-cards-count)
+                   state)}
+  [state _cards-id]
+  (let [repo (state/get-current-repo)
+        *cards-id (::cards-id state)
+        cards-id (rum/react *cards-id)
+        all-cards (concat
+                   [{:db/id :global
+                     :block/title "All cards"}]
+                   (db-model/get-class-objects repo (:db/id (db/entity :logseq.class/Cards))))
+        *block-ids (::block-ids state)
+        block-ids (rum/react *block-ids)
+        loading? (rum/react (::loading? state))
+        *card-index (::card-index state)
+        *phase (atom :init)]
+    (when (false? loading?)
+      [:div#cards-modal.p-1.flex.flex-col.gap-8
+       [:div.flex.flex-row.items-center.gap-2.flex-wrap
+        (shui/select
+         {:on-value-change (fn [v]
+                             (reset! *cards-id v)
+                             (p/let [result (<get-due-card-block-ids repo (if (= :global v) nil v))]
+                               (reset! *card-index 0)
+                               (reset! *block-ids result)))
+          :default-value cards-id}
+         (shui/select-trigger
+          {:class "!px-2 !py-0 !h-8 w-64"}
+          (shui/select-value
+           {:placeholder "Select cards"})
+          (shui/select-content
+           (shui/select-group
+            (for [card-entity all-cards]
+              (shui/select-item {:value (:db/id card-entity)}
+                                (:block/title card-entity)))))))
+
+        [:span.text-sm.opacity-50 (str (min (inc @*card-index) (count @*block-ids)) "/" (count @*block-ids))]]
+       (if-let [block-id (nth block-ids @*card-index nil)]
+         [:div.flex.flex-col
+          (card-view repo block-id *card-index *phase)]
+         [:p (t :flashcards/modal-finished)])])))
+
+(defonce ^:private *last-update-due-cards-count-canceler (atom nil))
+(def ^:private new-task--update-due-cards-count
+  "Return a task that update `:srs/cards-due-count` periodically."
+  (m/sp
+   (let [repo (state/get-current-repo)]
+     (if (config/db-based-graph? repo)
+       (m/?
+        (m/reduce
+         (fn [_ _]
+           (p/let [due-cards (<get-due-card-block-ids repo nil)]
+             (state/set-state! :srs/cards-due-count (count due-cards))))
+         (c.m/clock (* 3600 1000))))
+       (srs/update-cards-due-count!)))))
+
+(defn update-due-cards-count
+  []
+  (when-let [canceler @*last-update-due-cards-count-canceler]
+    (canceler)
+    (reset! *last-update-due-cards-count-canceler nil))
+  (let [canceler (c.m/run-task new-task--update-due-cards-count :update-due-cards-count)]
+    (reset! *last-update-due-cards-count-canceler canceler)
+    nil))
+
+(defn- get-operating-blocks
+  [block-ids]
+  (some->> block-ids
+           (map (fn [id] (db/entity [:block/uuid id])))
+           (seq)
+           block-handler/get-top-level-blocks
+           (remove ldb/property?)))
+
+(defn batch-make-cards!
+  ([] (batch-make-cards! (state/get-selection-block-ids)))
+  ([block-ids]
+   (let [repo (state/get-current-repo)
+         blocks (get-operating-blocks block-ids)]
+     (when-let [block-ids (not-empty (map :block/uuid blocks))]
+       (property-handler/batch-set-block-property!
+        repo
+        block-ids
+        :block/tags
+        (:db/id (db/entity :logseq.class/Card)))))))
+
+(defn- cards-in-time-range
+  [cards start-instant end-instant]
+  (assert (and (tick/instant? start-instant)
+               (tick/instant? end-instant))
+          [start-instant end-instant])
+  (->> cards
+       (filter (fn [card] (tick/<= start-instant (:last-repeat card) end-instant)))))
+
+(defn- cards-today
+  [cards]
+  (let [date-today (tick/new-date)
+        start-instant (tick/instant (tick/at date-today (tick/new-time 0 0)))
+        end-instant (tick/instant)]
+    (cards-in-time-range cards start-instant end-instant)))
+
+(defn- cards-recent-7-days
+  [cards]
+  (let [now-instant (tick/instant)
+        date-7-days-ago (tick/date (tick/<< now-instant (tick/new-duration 7 :days)))
+        start-instant (tick/instant (tick/at date-7-days-ago (tick/new-time 0 0)))
+        end-instant now-instant]
+    (cards-in-time-range cards start-instant end-instant)))
+
+(defn- cards-recent-30-days
+  [cards]
+  (let [now-instant (tick/instant)
+        date-30-days-ago (tick/date (tick/<< now-instant (tick/new-duration 30 :days)))
+        start-instant (tick/instant (tick/at date-30-days-ago (tick/new-time 0 0)))
+        end-instant now-instant]
+    (cards-in-time-range cards start-instant end-instant)))
+
+(defn- cards-stat
+  [cards]
+  (let [state-grouped-cards (group-by :state cards)
+        state-cards-count (update-vals state-grouped-cards count)
+        {new-count :new
+         learning-count :learning
+         review-count :review
+         relearning-count :relearning} state-cards-count
+        passed-repeat-count (count (filter #(contains? #{:good :easy} (:logseq/last-rating %)) cards))
+        lapsed-repeat-count (count (filter #(contains? #{:again :hard} (:logseq/last-rating %)) cards))
+        true-retention-percent (when (seq cards) (/ review-count (count cards)))]
+    {:true-retention true-retention-percent
+     :passed-repeats passed-repeat-count
+     :lapsed-repeats lapsed-repeat-count
+     :new-state-cards (or new-count 0)
+     :learning-state-cards (or learning-count 0)
+     :review-state-cards (or review-count 0)
+     :relearning-state-cards (or relearning-count 0)}))
+
+(comment
+  (defn <cards-stat
+    "Some explanations on return value:
+  :true-retention, cards in review-state / all-cards-count
+  :passed-repeats, last rating is :good or :easy
+  :lapsed-repeats, last rating is :again or :hard
+  :XXX-state-cards, cards' state is XXX"
+    []
+    (p/let [repo (state/get-current-repo)
+            all-card-blocks
+            (db-async/<q repo {:transact-db? false}
+                         '[:find [(pull ?b [* {:block/tags [:db/ident]}]) ...]
+                           :where
+                           [?b :block/tags :logseq.class/Card]
+                           [?b :block/uuid]])
+            all-cards (map get-card-map all-card-blocks)
+            [today-stat
+             recent-7-days-stat
+             recent-30-days-stat]
+            (map cards-stat ((juxt cards-today cards-recent-7-days cards-recent-30-days) all-cards))]
+      {:today-stat today-stat
+       :recent-7-days-stat recent-7-days-stat
+       :recent-30-days-stat recent-30-days-stat})))

+ 22 - 44
src/main/frontend/extensions/srs.cljs

@@ -1,21 +1,24 @@
 (ns frontend.extensions.srs
+  "SRS fns, will be deprecated in db-based version.
+  see also `frontend.extensions.fsrs`"
   (:require [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
             [cljs-time.local :as tl]
             [clojure.string :as string]
-            [frontend.config :as config]
             [frontend.commands :as commands]
             [frontend.components.block :as component-block]
             [frontend.components.editor :as editor]
             [frontend.components.macro :as component-macro]
             [frontend.components.select :as component-select]
             [frontend.components.svg :as svg]
+            [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.query-dsl :as query-dsl]
             [frontend.db.query-react :as query-react]
+            [frontend.format.mldoc :as mldoc]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.property :as property-handler]
             [frontend.handler.property.file :as property-file]
@@ -24,11 +27,9 @@
             [frontend.template :as template]
             [frontend.ui :as ui]
             [frontend.util :as util]
-            [frontend.format.mldoc :as mldoc]
             [frontend.util.file-based.drawer :as drawer]
             [frontend.util.persist-var :as persist-var]
             [logseq.graph-parser.property :as gp-property]
-            [logseq.common.util.page-ref :as page-ref]
             [logseq.shui.ui :as shui]
             [medley.core :as medley]
             [rum.core :as rum]))
@@ -124,10 +125,8 @@
                                       card-next-schedule-property "nil"
                                       card-last-score-property "nil"}))
 
-
 ;;; used by other ns
 
-
 (defn card-block?
   [block]
   (let [card-entity (db/get-page card-hash-tag)
@@ -190,11 +189,9 @@
       [-1 1 ef next-of-matrix]
       [(fix-2f next-interval) (+ 1 repeats) (fix-2f next-ef) next-of-matrix])))
 
-
 ;;; ================================================================
 ;;; card protocol
 
-
 (defprotocol ICard
   (get-root-block [this]))
 
@@ -204,7 +201,6 @@
 
   (show-cycle-config [this phase]))
 
-
 (defn- has-cloze?
   [blocks]
   (->> (map :block/title blocks)
@@ -267,16 +263,8 @@
      (let [result (if (string/blank? query-string)
                     (:block/_refs (db/get-page card-hash-tag))
                     (let [query-string (template/resolve-dynamic-template! query-string)
-                          query-string (if-not (or (string/blank? query-string)
-                                                   (string/starts-with? query-string "(")
-                                                   (string/starts-with? query-string "["))
-                                         (page-ref/->page-ref (string/trim query-string))
-                                         query-string)
-                          {query* :query :keys [sort-by rules]} (query-dsl/parse query-string {:db-graph? (config/db-based-graph? repo)})
-                          query** (util/concat-without-nil
-                                  [['?b :block/refs '?br] ['?br :block/name card-hash-tag]]
-                                  (if (coll? (first query*)) query* [query*]))]
-                      (when-let [query' (query-dsl/query-wrapper query**
+                          {query* :query :keys [sort-by rules]} (query-dsl/parse query-string {:db-graph? (config/db-based-graph? repo)})]
+                      (when-let [query' (query-dsl/query-wrapper query*
                                                                  {:blocks? true
                                                                   :block-attrs [:db/id :block/properties]})]
                         (let [result (query-react/react-query repo
@@ -309,11 +297,9 @@
     {:total (count blocks)
      :result sort-by-next-schedule}))
 
-
 ;;; ================================================================
 ;;; operations
 
-
 (defn- get-next-interval
   [card score]
   {:pre [(and (<= score 5) (>= score 0))
@@ -626,8 +612,8 @@
             (fn [{:keys [toggle-fn]}]
               [:div.ml-1.text-sm.font-medium.cursor
                {:on-pointer-down (fn [e]
-                                 (util/stop e)
-                                 (toggle-fn))}
+                                   (util/stop e)
+                                   (toggle-fn))}
                [:span.flex (if (string/blank? query-string) (t :flashcards/modal-select-all) query-string)
                 [:span {:style {:margin-top 2}}
                  (svg/caret-down)]]])
@@ -669,8 +655,8 @@
               {:icon "letter-a"
                :intent "link"
                :on-click (fn [e]
-                          (util/stop e)
-                          (swap! *preview-mode? not)
+                           (util/stop e)
+                           (swap! *preview-mode? not)
                            (reset! *card-index 0))
                :button-props {:id "preview-all-cards"}
                :small? true}
@@ -696,8 +682,8 @@
           (when (and (not modal?) (not @*preview-mode?))
             {:on-click (fn []
                          (shui/dialog-open!
-                           #(cards (assoc config :modal? true) {:query-string query-string})
-                           {:id :srs}))})
+                          #(cards (assoc config :modal? true) {:query-string query-string})
+                          {:id :srs}))})
           (let [view-fn (if modal? view-modal view)
                 blocks (if @*preview-mode? query-result review-cards)
                 blocks (if @*random-mode? (shuffle blocks) blocks)]
@@ -771,34 +757,26 @@
 (commands/register-slash-command ["Cards"
                                   [[:editor/input "{{cards }}" {:backward-pos 2}]]
                                   "Create a cards query"
-                                  {:db-graph? false
-                                   :icon :icon/cards}])
+                                  {:icon :icon/cards
+                                   :db-graph? false}])
 
 (commands/register-slash-command ["Cloze"
                                   [[:editor/input "{{cloze }}" {:backward-pos 2}]]
                                   "Create a cloze"
-                                  {:db-graph? false
-                                   :icon :icon/eye-question}])
+                                  {:icon :icon/eye-question}])
 
 ;; handlers
 (defn add-card-tag-to-block
   "given a block struct, adds the #card to title and returns
    a seq of [original-block new-content-string]"
   [block]
-    (when-let [content (:block/title block)]
-      (let [format (:block/format block)
-            content (-> (property-file/remove-built-in-properties-when-file-based
-                         (state/get-current-repo) (:block/format block) content)
-                        (drawer/remove-logbook))
-            [title body] (mldoc/get-title&body content format)]
-        [block (str title " #" card-hash-tag "\n" body)])))
-
-(defn make-block-a-card!
-  [block-id]
-  (when-let [block (db/entity [:block/uuid block-id])]
-    (let [block-content (add-card-tag-to-block block)
-          new-content (get block-content 1)]
-      (editor-handler/save-block! (state/get-current-repo) block-id new-content))))
+  (when-let [content (:block/title block)]
+    (let [format (:block/format block)
+          content (-> (property-file/remove-built-in-properties-when-file-based
+                       (state/get-current-repo) (:block/format block) content)
+                      (drawer/remove-logbook))
+          [title body] (mldoc/get-title&body content format)]
+      [block (str title " #" card-hash-tag "\n" body)])))
 
 (defn batch-make-cards!
   ([] (batch-make-cards! (state/get-selection-block-ids)))

+ 17 - 1
src/main/frontend/extensions/srs/handler.cljs

@@ -5,11 +5,14 @@
   [id]
   (let [nodes (sel [:#cards-modal (str "." id)])]
     (doseq [node nodes]
-      (.click node))))
+      (.focus node)
+      (.click node)
+      (js/setTimeout #(.blur node) 100))))
 
 (defn toggle-answers []
   (click "card-answers"))
 
+;; file-based
 (defn next-card []
   (click "card-next"))
 
@@ -21,3 +24,16 @@
 
 (defn recall []
   (click "card-recall"))
+
+;; db-based
+(defn card-again []
+  (click "card-again"))
+
+(defn card-hard []
+  (click "card-hard"))
+
+(defn card-good []
+  (click "card-good"))
+
+(defn card-easy []
+  (click "card-easy"))

+ 4 - 4
src/main/frontend/handler/db_based/property.cljs

@@ -49,16 +49,16 @@
     (outliner-op/create-property-text-block! block-id property-id value opts)))
 
 (defn batch-set-property!
-  [block-id property-id value]
+  [block-ids property-id value]
   (ui-outliner-tx/transact!
    {:outliner-op :batch-set-property}
-    (outliner-op/batch-set-property! block-id property-id value)))
+    (outliner-op/batch-set-property! block-ids property-id value)))
 
 (defn batch-remove-property!
-  [block-id property-id]
+  [block-ids property-id]
   (ui-outliner-tx/transact!
    {:outliner-op :batch-remove-property}
-    (outliner-op/batch-remove-property! block-id property-id)))
+    (outliner-op/batch-remove-property! block-ids property-id)))
 
 (defn class-add-property!
   [class-id property-id]

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

@@ -344,7 +344,6 @@
                                                             :ordered-list? ordered-list?
                                                             :replace-empty-target? replace-empty-target?}))))
 
-
 (defn- block-self-alone-when-insert?
   [config uuid]
   (let [current-page (state/get-current-page)
@@ -393,7 +392,7 @@
                        (wrap-parse-block))
         sibling? (or (:block/collapsed? (:block/link block)) (when block-self? false))
         result (outliner-insert-block! config current-block next-block {:sibling? sibling?
-                                                                          :keep-uuid? true})]
+                                                                        :keep-uuid? true})]
     [result sibling? next-block]))
 
 (defn clear-when-saved!
@@ -583,20 +582,20 @@
                                    nil)]
           (when block-m
             (p/do!
-              (ui-outliner-tx/transact!
-                {:outliner-op :insert-blocks}
-                (outliner-insert-block! {} block-m new-block {:sibling? sibling?
-                                                              :keep-uuid? true
-                                                              :ordered-list? ordered-list?
-                                                              :replace-empty-target? replace-empty-target?})
-                (when (and db-base? (seq properties))
-                  (property-handler/set-block-properties! repo (:block/uuid new-block) properties)))
-              (when edit-block?
-                (if (and replace-empty-target?
-                      (string/blank? (:block/title last-block)))
-                  (edit-block! last-block :max)
-                  (edit-block! new-block :max)))
-              new-block)))))))
+             (ui-outliner-tx/transact!
+              {:outliner-op :insert-blocks}
+              (outliner-insert-block! {} block-m new-block {:sibling? sibling?
+                                                            :keep-uuid? true
+                                                            :ordered-list? ordered-list?
+                                                            :replace-empty-target? replace-empty-target?})
+              (when (and db-base? (seq properties))
+                (property-handler/set-block-properties! repo (:block/uuid new-block) properties)))
+             (when edit-block?
+               (if (and replace-empty-target?
+                        (string/blank? (:block/title last-block)))
+                 (edit-block! last-block :max)
+                 (edit-block! new-block :max)))
+             new-block)))))))
 
 (defn insert-first-page-block-if-not-exists!
   [page-uuid-or-title]
@@ -605,12 +604,12 @@
       (when-let [page (db/get-page page-title)]
         (let [class-or-property? (or (ldb/class? page) (ldb/property? page))]
           (when (or class-or-property? (db/page-empty? (state/get-current-repo) (:db/id page)))
-           (let [format (or (:block/format page) (state/get-preferred-format))
-                 new-block {:block/title ""
-                            :block/format format}]
-             (ui-outliner-tx/transact!
-              {:outliner-op :insert-blocks}
-              (outliner-op/insert-blocks! [new-block] page {:sibling? false})))))))))
+            (let [format (or (:block/format page) (state/get-preferred-format))
+                  new-block {:block/title ""
+                             :block/format format}]
+              (ui-outliner-tx/transact!
+               {:outliner-op :insert-blocks}
+               (outliner-op/insert-blocks! [new-block] page {:sibling? false})))))))))
 
 (defn update-timestamps-content!
   [{:block/keys [repeated? marker format] :as block} content]
@@ -749,9 +748,9 @@
   [{:block/keys [priority title] :as block} new-priority]
   (when-not (config/db-based-graph? (state/get-current-repo))
     (let [new-content (string/replace-first title
-                                           (util/format "[#%s]" priority)
-                                           (util/format "[#%s]" new-priority))]
-     (save-block-if-changed! block new-content))))
+                                            (util/format "[#%s]" priority)
+                                            (util/format "[#%s]" new-priority))]
+      (save-block-if-changed! block new-content))))
 
 (defn delete-block-aux!
   [{:block/keys [uuid] :as _block}]
@@ -858,18 +857,18 @@
                           (save-block! repo block new-content {}))
                          (edit-block! (assoc block :block/title new-content) (count (:block/title prev-block))))
                         (p/do!
-                          (ui-outliner-tx/transact!
-                           transact-opts
-                           (when (seq children)
-                             (outliner-op/move-blocks! children prev-block false))
-                           (delete-block-aux! block)
-                           (save-block! repo prev-block new-content {}))
-                          (when edit-block-f (edit-block-f)))))
+                         (ui-outliner-tx/transact!
+                          transact-opts
+                          (when (seq children)
+                            (outliner-op/move-blocks! children prev-block false))
+                          (delete-block-aux! block)
+                          (save-block! repo prev-block new-content {}))
+                         (when edit-block-f (edit-block-f)))))
 
                     :else
                     (p/do!
-                      (delete-block-aux! block)
-                      (when edit-block-f (edit-block-f)))))))))))))
+                     (delete-block-aux! block)
+                     (when edit-block-f (edit-block-f)))))))))))))
 
 (defn delete-block!
   [repo]
@@ -1028,10 +1027,10 @@
                                    (if (config/db-based-graph? (state/get-current-repo))
                                      (str (string/join (repeat (dec level) "\t")) "- " (page-ref/->page-ref id))
                                      (condp = (:block/format block)
-                                      :org
-                                      (str (string/join (repeat level "*")) " " (block-ref/->block-ref id))
-                                      :markdown
-                                      (str (string/join (repeat (dec level) "\t")) "- " (block-ref/->block-ref id))))))
+                                       :org
+                                       (str (string/join (repeat level "*")) " " (block-ref/->block-ref id))
+                                       :markdown
+                                       (str (string/join (repeat (dec level) "\t")) "- " (block-ref/->block-ref id))))))
                             (string/join "\n\n"))]
       (set-blocks-id! (map :id blocks))
       (util/copy-to-clipboard! copy-str))))
@@ -1704,11 +1703,11 @@
 (defn in-shui-popup?
   []
   (or (some-> js/document.activeElement
-        (.closest "[data-radix-menu-content]")
-        (nil?)
-        (not))
-    (.querySelector js/document.body
-      "div[data-radix-popper-content-wrapper]")))
+              (.closest "[data-radix-menu-content]")
+              (nil?)
+              (not))
+      (.querySelector js/document.body
+                      "div[data-radix-popper-content-wrapper]")))
 
 (defn get-current-input-char
   [input]
@@ -1919,9 +1918,9 @@
       ;; Open "Search page or New page" auto-complete
       (and (= last-input-char commands/hashtag)
              ;; Only trigger at beginning of a line, before whitespace or after a reference
-             (or (re-find #"(?m)^#" (str (.-value input)))
-                 (start-of-new-word? input pos)
-                 (and db-based? (= page-ref/right-brackets (common-util/safe-subs (str (.-value input)) (- pos 3) (dec pos))))))
+           (or (re-find #"(?m)^#" (str (.-value input)))
+               (start-of-new-word? input pos)
+               (and db-based? (= page-ref/right-brackets (common-util/safe-subs (str (.-value input)) (- pos 3) (dec pos))))))
       (do
         (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
         (state/set-editor-last-pos! pos)
@@ -1982,10 +1981,10 @@
              :block/title new-content}
              (not db-based?)
              (assoc :block/properties (apply dissoc (:block/properties block)
-                                        (concat
-                                         (when-not keep-uuid? [:id])
-                                         [:custom_id :custom-id]
-                                         exclude-properties)))
+                                             (concat
+                                              (when-not keep-uuid? [:id])
+                                              [:custom_id :custom-id]
+                                              exclude-properties)))
              (not db-based?)
              (assoc :block/properties-text-values (apply dissoc (:block/properties-text-values block)
                                                          (concat
@@ -2020,24 +2019,24 @@
            :or {exclude-properties []}}]
   (let [editing-block (when-let [editing-block (state/get-edit-block)]
                         (some-> (db/entity [:block/uuid (:block/uuid editing-block)])
-                          (assoc :block/title (state/get-edit-content))))
+                                (assoc :block/title (state/get-edit-content))))
         has-unsaved-edits (and editing-block
-                            (not= (:block/title (db/entity (:db/id editing-block)))
-                              (state/get-edit-content)))
+                               (not= (:block/title (db/entity (:db/id editing-block)))
+                                     (state/get-edit-content)))
         target-block (or target-block editing-block)
         block (db/entity (:db/id target-block))
         page (if (:block/name block) block
-               (when target-block (:block/page (db/entity (:db/id target-block)))))
+                 (when target-block (:block/page (db/entity (:db/id target-block)))))
         empty-target? (if (true? skip-empty-target?) false
-                        (string/blank? (:block/title target-block)))
+                          (string/blank? (:block/title target-block)))
         paste-nested-blocks? (nested-blocks blocks)
         target-block-has-children? (db/has-children? (:block/uuid target-block))
         replace-empty-target? (and empty-target?
-                                (or (not target-block-has-children?)
-                                  (and target-block-has-children? (= (count blocks) 1))))
+                                   (or (not target-block-has-children?)
+                                       (and target-block-has-children? (= (count blocks) 1))))
         target-block' (if (and empty-target? target-block-has-children? paste-nested-blocks?)
                         (or (ldb/get-left-sibling target-block)
-                          (:block/parent (db/entity (:db/id target-block))))
+                            (:block/parent (db/entity (:db/id target-block))))
                         target-block)
         sibling? (cond
                    (and paste-nested-blocks? empty-target?)
@@ -2052,24 +2051,24 @@
                    :else
                    true)
         transact-blocks! #(ui-outliner-tx/transact!
-                            {:outliner-op :insert-blocks
-                             :additional-tx revert-cut-txs}
-                            (when target-block'
-                              (let [format (or (:block/format target-block') (state/get-preferred-format))
-                                    repo (state/get-current-repo)
-                                    blocks' (map (fn [block]
-                                                   (paste-block-cleanup repo block page exclude-properties format content-update-fn keep-uuid?))
-                                              blocks)]
-                                (outliner-op/insert-blocks! blocks' target-block' {:sibling? sibling?
-                                                                                   :outliner-op :paste
-                                                                                   :replace-empty-target? replace-empty-target?
-                                                                                   :keep-uuid? keep-uuid?}))))]
+                           {:outliner-op :insert-blocks
+                            :additional-tx revert-cut-txs}
+                           (when target-block'
+                             (let [format (or (:block/format target-block') (state/get-preferred-format))
+                                   repo (state/get-current-repo)
+                                   blocks' (map (fn [block]
+                                                  (paste-block-cleanup repo block page exclude-properties format content-update-fn keep-uuid?))
+                                                blocks)]
+                               (outliner-op/insert-blocks! blocks' target-block' {:sibling? sibling?
+                                                                                  :outliner-op :paste
+                                                                                  :replace-empty-target? replace-empty-target?
+                                                                                  :keep-uuid? keep-uuid?}))))]
     (if ops-only?
       (transact-blocks!)
       (p/let [_ (when has-unsaved-edits
                   (ui-outliner-tx/transact!
-                    {:outliner-op :save-block}
-                    (outliner-save-block! editing-block)))
+                   {:outliner-op :save-block}
+                   (outliner-save-block! editing-block)))
               result (transact-blocks!)]
         (state/set-block-op-type! nil)
         (when-let [result (some-> result (ldb/read-transit-str))]
@@ -2534,7 +2533,7 @@
   [direction]
   (let [f (case direction :up last :down first)
         container (if (some-> js/document.activeElement
-                        (.querySelector ".blocks-container"))
+                              (.querySelector ".blocks-container"))
                     js/document.activeElement js/document.body)
         block (->> (util/get-blocks-noncollapse container)
                    (f))]
@@ -2729,14 +2728,14 @@
             current-pos (cursor/pos input)
             value (gobj/get input "value")
             deleted (and (> current-pos 0)
-                      (util/nth-safe value (dec current-pos)))
+                         (util/nth-safe value (dec current-pos)))
             selected-start (util/get-selection-start input)
             selected-end (util/get-selection-end input)
             block (state/get-edit-block)
             block (db/entity (:db/id block))
             repo (state/get-current-repo)
             top-block? (= (:db/id (or (ldb/get-left-sibling block) (:block/parent block)))
-                         (:db/id (:block/page block)))
+                          (:db/id (:block/page block)))
             single-block? (inside-of-single-block (.-target e))
             root-block? (= (:block.temp/container block) (str (:block/uuid block)))]
         (block-handler/mark-last-input-time! repo)
@@ -2753,18 +2752,18 @@
                 custom-query? (get-in editor-state [:config :custom-query?])]
             (util/stop e)
             (when (and (not (and top-block? (not (string/blank? value))))
-                    (not root-block?)
-                    (not single-block?)
-                    (not custom-query?))
+                       (not root-block?)
+                       (not single-block?)
+                       (not custom-query?))
               (if (own-order-number-list? block)
                 (p/do!
-                  (save-current-block!)
-                  (remove-block-own-order-list-type! block))
+                 (save-current-block!)
+                 (remove-block-own-order-list-type! block))
                 (delete-block! repo))))
 
           (and (> current-pos 0)
-            (contains? #{commands/command-trigger commands/command-ask}
-              (util/nth-safe value (dec current-pos))))
+               (contains? #{commands/command-trigger commands/command-ask}
+                          (util/nth-safe value (dec current-pos))))
           (do
             (util/stop e)
             (commands/restore-state)
@@ -2772,12 +2771,12 @@
 
           ;; pair
           (and
-            deleted
-            (contains?
-              (set (keys delete-map))
-              deleted)
-            (>= (count value) (inc current-pos))
-            (= (util/nth-safe value current-pos)
+           deleted
+           (contains?
+            (set (keys delete-map))
+            deleted)
+           (>= (count value) (inc current-pos))
+           (= (util/nth-safe value current-pos)
               (get delete-map deleted)))
 
           (do
@@ -2804,7 +2803,7 @@
           (when (and input (not (mobile-util/native-ios?)))
             (util/stop e)
             (delete-and-update
-              input (util/safe-dec-current-pos-from-end (.-value input) current-pos) current-pos))))
+             input (util/safe-dec-current-pos-from-end (.-value input) current-pos) current-pos))))
       false)))
 
 (defn indent-outdent
@@ -3230,14 +3229,14 @@
 (defn- in-page-preview?
   []
   (some-> js/document.activeElement
-    (.closest ".ls-preview-popup")
-    (nil?) (not)))
+          (.closest ".ls-preview-popup")
+          (nil?) (not)))
 
 (defn shortcut-up-down [direction]
   (fn [e]
     (when (and (not (auto-complete?))
                (or (in-page-preview?)
-                 (not (in-shui-popup?)))
+                   (not (in-shui-popup?)))
                (not (slide-focused?))
                (not (state/get-timestamp-block)))
       (util/stop e)
@@ -3396,6 +3395,7 @@
            (or (if ignore-children? false (db-model/has-children? block-id))
                (valid-dsl-query-block? block repo)
                (valid-custom-query-block? block)
+               (and db-based? (ldb/class-instance? (db/entity :logseq.class/Query) block))
                (and db-based?
                     (seq property-keys)
                     (not (db-pu/all-hidden-properties? property-keys)))
@@ -3592,39 +3592,39 @@
 (defn toggle-collapse!
   ([e] (toggle-collapse! e false))
   ([e clear-selection?]
-    (when e (util/stop e))
-    (cond
-      (state/editing?)
-      (when-let [block (state/get-edit-block)]
+   (when e (util/stop e))
+   (cond
+     (state/editing?)
+     (when-let [block (state/get-edit-block)]
         ;; get-edit-block doesn't track the latest collapsed state, so we need to reload from db.
-        (let [block-id (:block/uuid block)
-              block (db/entity [:block/uuid block-id])]
-          (if (:block/collapsed? block)
-            (expand! e clear-selection?)
-            (collapse! e clear-selection?))))
+       (let [block-id (:block/uuid block)
+             block (db/entity [:block/uuid block-id])]
+         (if (:block/collapsed? block)
+           (expand! e clear-selection?)
+           (collapse! e clear-selection?))))
 
-      (state/selection?)
-      (do
-        (let [block-ids (map #(-> % (dom/attr "blockid") uuid) (get-selected-blocks))
-              first-block-id (first block-ids)]
-          (when first-block-id
+     (state/selection?)
+     (do
+       (let [block-ids (map #(-> % (dom/attr "blockid") uuid) (get-selected-blocks))
+             first-block-id (first block-ids)]
+         (when first-block-id
             ;; If multiple blocks are selected, they may not have all the same collapsed state.
             ;; For simplicity, use the first block's state to decide whether to collapse/expand all.
-            (let [first-block (db/entity [:block/uuid first-block-id])]
-              (if (:block/collapsed? first-block)
-                (doseq [block-id block-ids] (expand-block! block-id))
-                (doseq [block-id block-ids] (collapse-block! block-id))))))
-        (and clear-selection? (clear-selection!)))
+           (let [first-block (db/entity [:block/uuid first-block-id])]
+             (if (:block/collapsed? first-block)
+               (doseq [block-id block-ids] (expand-block! block-id))
+               (doseq [block-id block-ids] (collapse-block! block-id))))))
+       (and clear-selection? (clear-selection!)))
 
-      (whiteboard?)
+     (whiteboard?)
       ;; TODO: Looks like detecting the whiteboard selection's collapse state will take more work.
       ;; Leaving unimplemented for now.
-      nil
+     nil
 
-      :else
+     :else
       ;; If no block is being edited or selected, the "toggle" action doesn't make sense,
       ;; so we no-op here, unlike in the expand! & collapse! functions.
-      nil)))
+     nil)))
 
 (defn collapse-all!
   ([]

+ 175 - 175
src/main/frontend/handler/events.cljs

@@ -79,7 +79,8 @@
             [frontend.date :as date]
             [logseq.db :as ldb]
             [frontend.persist-db :as persist-db]
-            [frontend.handler.export :as export]))
+            [frontend.handler.export :as export]
+            [frontend.extensions.fsrs :as fsrs]))
 
 ;; TODO: should we move all events here?
 
@@ -87,12 +88,12 @@
 
 (defn file-sync-restart! []
   (async/go (async/<! (p->c (persist-var/load-vars)))
-    (async/<! (sync/<sync-stop))
-    (some-> (sync/<sync-start) async/<!)))
+            (async/<! (sync/<sync-stop))
+            (some-> (sync/<sync-start) async/<!)))
 
 (defn- file-sync-stop! []
   (async/go (async/<! (p->c (persist-var/load-vars)))
-    (async/<! (sync/<sync-stop))))
+            (async/<! (sync/<sync-stop))))
 
 (defn- enable-beta-features!
   []
@@ -170,7 +171,6 @@
          (repo-config-handler/restore-repo-config! graph)
          (when-not (= :draw (state/get-current-route))
            (route-handler/redirect-to-home!))
-         (srs/update-cards-due-count!)
          (state/pub-event! [:graph/ready graph])
          (if db-based?
            (rtc-handler/<rtc-start! graph)
@@ -225,32 +225,32 @@
           (when-let [root (state/get-local-container-root-url)]
             (let [graph-path (graph-picker/validate-graph-dirname root graph-name)]
               (->
-                (p/let [exists? (fs/dir-exists? graph-path)]
-                  (let [overwrite? (if exists?
-                                     (js/confirm (str "There's already a directory with the name \"" graph-name "\", do you want to overwrite it? Make sure to backup it first if you're not sure about it."))
-                                     true)]
-                    (if overwrite?
-                      (p/let [_ (fs/mkdir-if-not-exists graph-path)]
-                        (nfs-handler/ls-dir-files-with-path!
-                          graph-path
-                          {:ok-handler (fn []
-                                         (file-sync-handler/init-remote-graph graph-path graph)
-                                         (js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))}))
-                      (let [graph-name (-> (js/prompt "Please specify a new directory name to download the graph:")
-                                         str
-                                         string/trim)]
-                        (when-not (string/blank? graph-name)
-                          (state/pub-event! [:graph/pull-down-remote-graph graph graph-name]))))))
-                (p/catch (fn [^js e]
-                           (notification/show! (str e) :error)
-                           (js/console.error e)))))))))
+               (p/let [exists? (fs/dir-exists? graph-path)]
+                 (let [overwrite? (if exists?
+                                    (js/confirm (str "There's already a directory with the name \"" graph-name "\", do you want to overwrite it? Make sure to backup it first if you're not sure about it."))
+                                    true)]
+                   (if overwrite?
+                     (p/let [_ (fs/mkdir-if-not-exists graph-path)]
+                       (nfs-handler/ls-dir-files-with-path!
+                        graph-path
+                        {:ok-handler (fn []
+                                       (file-sync-handler/init-remote-graph graph-path graph)
+                                       (js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))}))
+                     (let [graph-name (-> (js/prompt "Please specify a new directory name to download the graph:")
+                                          str
+                                          string/trim)]
+                       (when-not (string/blank? graph-name)
+                         (state/pub-event! [:graph/pull-down-remote-graph graph graph-name]))))))
+               (p/catch (fn [^js e]
+                          (notification/show! (str e) :error)
+                          (js/console.error e)))))))))
     (shui/dialog-open!
-      (file-sync/pick-dest-to-sync-panel graph))))
+     (file-sync/pick-dest-to-sync-panel graph))))
 
 (defmethod handle :graph/pick-page-histories [[_ graph-uuid page-name]]
   (shui/dialog-open!
-    (file-sync/pick-page-histories-panel graph-uuid page-name)
-    {:id :page-histories :label "modal-page-histories"}))
+   (file-sync/pick-page-histories-panel graph-uuid page-name)
+   {:id :page-histories :label "modal-page-histories"}))
 
 (defmethod handle :graph/open-new-window [[_ev target-repo]]
   (p/let [current-repo (state/get-current-repo)]
@@ -269,7 +269,7 @@
   [repo]
   (when
    (and (not (util/electron?))
-     (not (mobile-util/native-platform?)))
+        (not (mobile-util/native-platform?)))
     (fn [{:keys [close]}]
       [:div
        ;; TODO: fn translation with args
@@ -277,28 +277,29 @@
         "Grant native filesystem permission for directory: "
         [:b (config/get-local-dir repo)]]
        (ui/button
-         (t :settings-permission/start-granting)
-         :class "ui__modal-enter"
-         :on-click (fn []
-                     (nfs/check-directory-permission! repo)
-                     (close)))])))
+        (t :settings-permission/start-granting)
+        :class "ui__modal-enter"
+        :on-click (fn []
+                    (nfs/check-directory-permission! repo)
+                    (close)))])))
 
 (defmethod handle :modal/nfs-ask-permission []
   (when-let [repo (get-local-repo)]
     (some-> (ask-permission repo)
-      (shui/dialog-open! {:align :top}))))
+            (shui/dialog-open! {:align :top}))))
 
-(defmethod handle :modal/show-cards [_]
-  (shui/dialog-open!
-    srs/global-cards
-    {:id :srs
-     :label "flashcards__cp"}))
+(defmethod handle :modal/show-cards [[_ cards-id]]
+  (let [db-based? (config/db-based-graph? (state/get-current-repo))]
+    (shui/dialog-open!
+     (if db-based? (fn [] (fsrs/cards-view cards-id)) srs/global-cards)
+     {:id :srs
+      :label "flashcards__cp"})))
 
 (defmethod handle :modal/show-instruction [_]
   (shui/dialog-open!
-    capacitor-fs/instruction
-    {:id :instruction
-     :label "instruction__cp"}))
+   capacitor-fs/instruction
+   {:id :instruction
+    :label "instruction__cp"}))
 
 (defmethod handle :modal/show-themes-modal [[_ classic?]]
   (if classic?
@@ -310,10 +311,10 @@
     (if (shui/dialog-get label)
       (shui/dialog-close! label)
       (shui/dialog-open!
-        #(settings/modal-appearance-inner)
-        {:id      label
-         :overlay-props {:label label}
-         :label   label}))))
+       #(settings/modal-appearance-inner)
+       {:id      label
+        :overlay-props {:label label}
+        :label   label}))))
 
 (defmethod handle :modal/set-git-username-and-email [[_ _content]]
   (shui/dialog-open! git-component/set-git-username-and-email))
@@ -336,13 +337,12 @@
 (defmethod handle :file/not-matched-from-disk [[_ path disk-content db-content]]
   (when-let [repo (state/get-current-repo)]
     (shui/dialog-open!
-      #(diff/local-file repo path disk-content db-content)
-      {:label "diff__cp"})))
-
+     #(diff/local-file repo path disk-content db-content)
+     {:label "diff__cp"})))
 
 (defmethod handle :modal/display-file-version-selector  [[_ versions path  get-content]]
   (shui/dialog-open!
-    #(git-component/file-version-selector versions path get-content)))
+   #(git-component/file-version-selector versions path get-content)))
 
 (defmethod handle :graph/sync-context []
   (let [context {:dev? config/dev?
@@ -392,10 +392,10 @@
 
 (defmethod handle :go/search [_]
   (state/set-modal! cmdk/cmdk-modal
-    {:fullscreen? true
-     :close-btn?  false
-     :panel?      false
-     :label "ls-modal-search"}))
+                    {:fullscreen? true
+                     :close-btn?  false
+                     :panel?      false
+                     :label "ls-modal-search"}))
 
 (defmethod handle :go/plugins [_]
   (plugin/open-plugins-modal!))
@@ -414,9 +414,8 @@
 
 (defmethod handle :go/proxy-settings [[_ agent-opts]]
   (shui/dialog-open!
-    (plugin/user-proxy-settings-panel agent-opts)
-    {:id :https-proxy-panel :center? true :class "lg:max-w-2xl"}))
-
+   (plugin/user-proxy-settings-panel agent-opts)
+   {:id :https-proxy-panel :center? true :class "lg:max-w-2xl"}))
 
 (defmethod handle :redirect-to-home [_]
   (page-handler/create-today-journal!))
@@ -429,12 +428,12 @@
 (defmethod handle :capture-error [[_ {:keys [error payload]}]]
   (let [[user-uuid graph-uuid tx-id] @sync/graphs-txid
         payload (assoc payload
-                  :user-id user-uuid
-                  :graph-id graph-uuid
-                  :tx-id tx-id
-                  :db-based (config/db-based-graph? (state/get-current-repo)))]
+                       :user-id user-uuid
+                       :graph-id graph-uuid
+                       :tx-id tx-id
+                       :db-based (config/db-based-graph? (state/get-current-repo)))]
     (Sentry/captureException error
-      (bean/->js {:tags payload}))))
+                             (bean/->js {:tags payload}))))
 
 (defmethod handle :exec-plugin-cmd [[_ {:keys [pid cmd action]}]]
   (commands/exec-plugin-simple-command! pid cmd action))
@@ -468,7 +467,7 @@
       (js/setTimeout (fn []
                        (when-let [toolbar (.querySelector main-node "#mobile-editor-toolbar")]
                          (set! (.. toolbar -style -bottom) (str keyboard-height "px"))))
-        100))))
+                     100))))
 
 (defmethod handle :mobile/keyboard-will-hide [[_]]
   (let [main-node (util/app-scroll-container-node)]
@@ -496,8 +495,8 @@
   [repo-url]
   (when repo-url
     (let [app-id (-> (first (string/split repo-url "/Documents"))
-                   (string/split "/")
-                   last)]
+                     (string/split "/")
+                     last)]
       app-id)))
 
 (defmethod handle :validate-appId [[_ graph-switch-f graph]]
@@ -512,13 +511,13 @@
               current-document-url (.getUri Filesystem #js {:path ""
                                                             :directory (.-Documents Directory)})
               current-app-id (-> (js->clj current-document-url :keywordize-keys true)
-                               get-ios-app-id)]
+                                 get-ios-app-id)]
         (if (= deprecated-app-id current-app-id)
           (when graph-switch-f (graph-switch-f graph true))
           (do
             (notification/show! [:div "Migrating from previous App installation..."]
-              :warning
-              true)
+                                :warning
+                                true)
             (prn ::migrate-app-id :from deprecated-app-id :to current-app-id)
             (file-sync-stop!)
             (.unwatch mobile-util/fs-watcher)
@@ -527,11 +526,11 @@
               (try
                 ;; replace app-id part of repo url
                 (reset! conn/conns
-                  (update-keys @conn/conns
-                    (fn [key]
-                      (if (string/includes? key deprecated-app-id)
-                        (string/replace key deprecated-app-id current-app-id)
-                        key))))
+                        (update-keys @conn/conns
+                                     (fn [key]
+                                       (if (string/includes? key deprecated-app-id)
+                                         (string/replace key deprecated-app-id current-app-id)
+                                         key))))
                 (db-persist/rename-graph! deprecated-repo current-repo)
                 (search/remove-db! deprecated-repo)
                 (state/add-repo! {:url current-repo :nfs? true})
@@ -549,7 +548,7 @@
   (let [downloading?   (:plugin/updates-downloading? @state/state)
         auto-checking? (plugin-handler/get-auto-checking?)]
     (when-let [coming (and (not downloading?)
-                        (get-in @state/state [:plugin/updates-coming id]))]
+                           (get-in @state/state [:plugin/updates-coming id]))]
       (let [error-code (:error-code coming)
             error-code (if (= error-code (str :no-new-version)) nil error-code)
             title      (:title coming)]
@@ -557,14 +556,14 @@
           (if-not error-code
             (plugin/set-updates-sub-content! (str title "...") 0)
             (notification/show!
-              (str "[Checked]<" title "> " error-code) :error)))))
+             (str "[Checked]<" title "> " error-code) :error)))))
 
     (if (and updated? downloading?)
       ;; try to start consume downloading item
       (if-let [next-coming (state/get-next-selected-coming-update)]
         (plugin-handler/check-or-update-marketplace-plugin!
-          (assoc next-coming :only-check false :error-code nil)
-          (fn [^js e] (js/console.error "[Download Err]" next-coming e)))
+         (assoc next-coming :only-check false :error-code nil)
+         (fn [^js e] (js/console.error "[Download Err]" next-coming e)))
         (plugin-handler/close-updates-downloading))
 
       ;; try to start consume pending item
@@ -572,29 +571,29 @@
         (do
           (println "Updates: take next pending - " (:id next-pending))
           (js/setTimeout
-            #(plugin-handler/check-or-update-marketplace-plugin!
-               (assoc next-pending :only-check true :auto-check auto-checking? :error-code nil)
-               (fn [^js e]
-                 (notification/show! (.toString e) :error)
-                 (js/console.error "[Check Err]" next-pending e))) 500))
+           #(plugin-handler/check-or-update-marketplace-plugin!
+             (assoc next-pending :only-check true :auto-check auto-checking? :error-code nil)
+             (fn [^js e]
+               (notification/show! (.toString e) :error)
+               (js/console.error "[Check Err]" next-pending e))) 500))
 
         ;; try to open waiting updates list
         (do (when (and prev-pending? (not auto-checking?)
-                    (seq (state/all-available-coming-updates)))
+                       (seq (state/all-available-coming-updates)))
               (plugin/open-waiting-updates-modal!))
             (plugin-handler/set-auto-checking! false))))))
 
 (defmethod handle :plugin/hook-db-tx [[_ {:keys [blocks tx-data] :as payload}]]
   (when-let [payload (and (seq blocks)
-                       (merge payload {:tx-data (map #(into [] %) tx-data)}))]
+                          (merge payload {:tx-data (map #(into [] %) tx-data)}))]
     (plugin-handler/hook-plugin-db :changed payload)
     (plugin-handler/hook-plugin-block-changes payload)))
 
 (defmethod handle :plugin/loader-perf-tip [[_ {:keys [^js o _s _e]}]]
   (when-let [opts (.-options o)]
     (notification/show!
-      (plugin/perf-tip-content (.-id o) (.-name opts) (.-url opts))
-      :warning false (.-id o))))
+     (plugin/perf-tip-content (.-id o) (.-name opts) (.-url opts))
+     :warning false (.-id o))))
 
 (defmethod handle :mobile-file-watcher/changed [[_ ^js event]]
   (let [type (.-event event)
@@ -615,16 +614,16 @@
 
 (defmethod handle :graph/ask-for-re-fresh [_]
   (shui/dialog-open!
-    [:div {:style {:max-width 700}}
-     [:p (t :sync-from-local-changes-detected)]
-     [:div.flex.justify-end
-      (ui/button
-        (t :yes)
-        :autoFocus "on"
-        :class "ui__modal-enter"
-        :on-click (fn []
-                    (shui/dialog-close!)
-                    (nfs-handler/refresh! (state/get-current-repo) refresh-cb)))]]))
+   [:div {:style {:max-width 700}}
+    [:p (t :sync-from-local-changes-detected)]
+    [:div.flex.justify-end
+     (ui/button
+      (t :yes)
+      :autoFocus "on"
+      :class "ui__modal-enter"
+      :on-click (fn []
+                  (shui/dialog-close!)
+                  (nfs-handler/refresh! (state/get-current-repo) refresh-cb)))]]))
 
 (defmethod handle :sync/create-remote-graph [[_ current-repo]]
   (let [graph-name (js/decodeURI (util/node-path.basename current-repo))]
@@ -638,21 +637,21 @@
         (state/set-repos! (map (fn [r]
                                  (if (= (:url r) current-repo)
                                    (assoc r
-                                     :GraphUUID GraphUUID
-                                     :GraphName graph-name
-                                     :remote? true)
+                                          :GraphUUID GraphUUID
+                                          :GraphName graph-name
+                                          :remote? true)
                                    r))
-                            (state/get-repos)))))))
+                               (state/get-repos)))))))
 
 (defmethod handle :modal/remote-encryption-input-pw-dialog [[_ repo-url remote-graph-info type opts]]
   (shui/dialog-open!
-    (encryption/input-password
-      repo-url nil (merge
-                     (assoc remote-graph-info
-                       :type (or type :create-pwd-remote)
-                       :repo repo-url)
-                     opts))
-    {:center? true :close-btn? false :close-backdrop? false}))
+   (encryption/input-password
+    repo-url nil (merge
+                  (assoc remote-graph-info
+                         :type (or type :create-pwd-remote)
+                         :repo repo-url)
+                  opts))
+   {:center? true :close-btn? false :close-backdrop? false}))
 
 (defmethod handle :journal/insert-template [[_ page-name]]
   (let [page-name (util/page-name-sanity-lc page-name)]
@@ -673,32 +672,32 @@
     (let [file (:block/file page-entity)]
       (when-let [path (:file/path file)]
         (when (and (not= content (:file/content file))
-                (:file/content file))
+                   (:file/content file))
           (sync/add-new-version-file graph path (:file/content file)))
         (p/let [_ (file-handler/alter-file graph
-                    path
-                    content
-                    {:re-render-root? true
-                     :skip-compare? true})]
+                                           path
+                                           content
+                                           {:re-render-root? true
+                                            :skip-compare? true})]
           (state/close-modal!)
           (route-handler/redirect! {:to :page
                                     :path-params {:name (:block/name page-entity)}}))))))
 
 (defmethod handle :whiteboard/onboarding [[_ opts]]
   (shui/dialog-open!
-    (fn [{:keys [close]}] (whiteboard/onboarding-welcome close))
-    (merge {:close-btn?      false
-            :center?         true
-            :close-backdrop? false} opts)))
+   (fn [{:keys [close]}] (whiteboard/onboarding-welcome close))
+   (merge {:close-btn?      false
+           :center?         true
+           :close-backdrop? false} opts)))
 
 (defmethod handle :file-sync/onboarding-tip [[_ type opts]]
   (let [type (keyword type)]
     (when-not (config/db-based-graph? (state/get-current-repo))
       (shui/dialog-open!
-        (file-sync/make-onboarding-panel type)
-        (merge {:close-btn? false
-                :center? true
-                :close-backdrop? (not= type :welcome)} opts)))))
+       (file-sync/make-onboarding-panel type)
+       (merge {:close-btn? false
+               :center? true
+               :close-backdrop? (not= type :welcome)} opts)))))
 
 (defmethod handle :file-sync/maybe-onboarding-show [[_ type]]
   (file-sync/maybe-onboarding-show type))
@@ -714,6 +713,7 @@
 (defmethod handle :graph/restored [[_ graph]]
   (mobile/init!)
   (rtc-handler/<rtc-start! graph)
+  (fsrs/update-due-cards-count)
   (when-not (mobile-util/native-ios?)
     (state/pub-event! [:graph/ready graph])))
 
@@ -745,24 +745,24 @@
 
 (defmethod handle :ui/notify-skipped-downloading-files [[_ paths]]
   (notification/show!
-    [:div
-     [:div.mb-4
-      [:div.font-semibold.mb-4.text-xl "It seems that some of your filenames are in the outdated format."]
-      [:p
-       "The files below that have reserved characters can't be saved on this device."]
-      [:div.overflow-y-auto.max-h-96
-       [:ol.my-2
-        (for [path paths]
-          [:li path])]]
+   [:div
+    [:div.mb-4
+     [:div.font-semibold.mb-4.text-xl "It seems that some of your filenames are in the outdated format."]
+     [:p
+      "The files below that have reserved characters can't be saved on this device."]
+     [:div.overflow-y-auto.max-h-96
+      [:ol.my-2
+       (for [path paths]
+         [:li path])]]
 
-      [:div
-       [:p
-        "Check " [:a {:href "https://docs.logseq.com/#/page/logseq%20file%20and%20folder%20naming%20rules"
-                      :target "_blank"}
-                  "Logseq file and folder naming rules"]
-        " for more details."]]]]
-    :warning
-    false))
+     [:div
+      [:p
+       "Check " [:a {:href "https://docs.logseq.com/#/page/logseq%20file%20and%20folder%20naming%20rules"
+                     :target "_blank"}
+                 "Logseq file and folder naming rules"]
+       " for more details."]]]]
+   :warning
+   false))
 
 (defmethod handle :graph/setup-a-repo [[_ opts]]
   (let [opts' (merge {:picked-root-fn #(state/close-modal!)
@@ -770,16 +770,16 @@
                       :logged?        (user-handler/logged-in?)} opts)]
     (if (mobile-util/native-ios?)
       (shui/dialog-open!
-        #(graph-picker/graph-picker-cp opts')
-        {:label "graph-setup"})
+       #(graph-picker/graph-picker-cp opts')
+       {:label "graph-setup"})
       (page-handler/ls-dir-files! st/refresh! opts'))))
 
 (defmethod handle :graph/new-db-graph [[_ _opts]]
   (shui/dialog-open!
-    repo/new-db-graph
-    {:id :new-db-graph
-     :title [:h2 "Create a new graph"]
-     :style {:max-width "500px"}}))
+   repo/new-db-graph
+   {:id :new-db-graph
+    :title [:h2 "Create a new graph"]
+    :style {:max-width "500px"}}))
 
 (defmethod handle :graph/save-db-to-disk [[_ _opts]]
   (persist-db/export-current-graph! {:succ-notification? true}))
@@ -812,21 +812,21 @@
         [:div
          [:p
           (str "It seems that another whiteboard file already has the ID \"" id
-            "\". You can fix it by changing the ID in this file with another UUID.")]
+               "\". You can fix it by changing the ID in this file with another UUID.")]
          [:p
           "Or, let me"
           (ui/button "Fix"
-            :on-click (fn []
-                        (let [dir (config/get-repo-dir repo)]
-                          (p/let [content (fs/read-file dir file)]
-                            (let [new-content (string/replace content (str id) (str (random-uuid)))]
-                              (p/let [_ (fs/write-file! repo
-                                          dir
-                                          file
-                                          new-content
-                                          {})]
-                                (reset! resolved? true))))))
-            :class "inline mx-1")
+                     :on-click (fn []
+                                 (let [dir (config/get-repo-dir repo)]
+                                   (p/let [content (fs/read-file dir file)]
+                                     (let [new-content (string/replace content (str id) (str (random-uuid)))]
+                                       (p/let [_ (fs/write-file! repo
+                                                                 dir
+                                                                 file
+                                                                 new-content
+                                                                 {})]
+                                         (reset! resolved? true))))))
+                     :class "inline mx-1")
           "it."]])]]))
 
 (defmethod handle :file/parse-and-load-error [[_ repo parse-errors]]
@@ -839,8 +839,8 @@
                           (let [data (ex-data error)]
                             (cond
                               (and (common-config/whiteboard? file)
-                                (= :transact/upsert (:error data))
-                                (uuid? (last (:assertion data))))
+                                   (= :transact/upsert (:error data))
+                                   (uuid? (last (:assertion data))))
                               (rum/with-key (file-id-conflict-item repo file data) file)
 
                               :else
@@ -933,8 +933,9 @@
                                                                 (assoc :custom-content content'))))))))))]
      (when (seq blocks)
        (let [target' (or target
-                       (some-> (state/get-edit-input-id)
-                            (gdom/getElement)))]
+                         (some-> (state/get-edit-input-id)
+                                 (gdom/getElement))
+                         (first (state/get-selection-blocks)))]
          (if target'
            (shui/popup-show! target'
                              #(property-dialog/dialog blocks opts')
@@ -942,8 +943,7 @@
                               :auto-focus? true})
            (shui/dialog-open! #(property-dialog/dialog blocks opts')
                               {:id :property-dialog
-                               :align "start"
-                               :content-props {:onOpenAutoFocus #(.preventDefault %)}})))))))
+                               :align "start"})))))))
 
 (rum/defc multi-tabs-dialog
   []
@@ -951,7 +951,7 @@
     [:div.flex.p-4.flex-col.gap-4.h-64
      [:span.warning.text-lg
       (util/format "Logseq doesn't support multiple %ss access to the same graph yet, please close this %s or switch to another graph."
-        word word)]
+                   word word)]
      [:div.text-lg
       [:p "Switch to another repo: "]
       [:div.border.rounded.bg-gray-01.overflow-hidden.w-60
@@ -973,7 +973,7 @@
 (defmethod handle :rtc/download-remote-graph [[_ graph-name graph-uuid]]
   (->
    (p/do!
-     (rtc-handler/<rtc-download-graph! graph-name graph-uuid 60000))
+    (rtc-handler/<rtc-download-graph! graph-name graph-uuid 60000))
    (p/catch (fn [e]
               (println "RTC download graph failed, error:")
               (js/console.error e)))))
@@ -993,18 +993,18 @@
     (async/go-loop []
       (let [[payload d] (async/<! chan)]
         (->
-          (try
-            (p/resolved (handle payload))
-            (catch :default error
-              (p/rejected error)))
-          (p/then (fn [result]
-                    (p/resolve! d result)))
-          (p/catch (fn [error]
-                     (let [type :handle-system-events/failed]
-                       (state/pub-event! [:capture-error {:error error
-                                                          :payload {:type type
-                                                                    :payload payload}}])
-                       (p/reject! d error))))))
+         (try
+           (p/resolved (handle payload))
+           (catch :default error
+             (p/rejected error)))
+         (p/then (fn [result]
+                   (p/resolve! d result)))
+         (p/catch (fn [error]
+                    (let [type :handle-system-events/failed]
+                      (state/pub-event! [:capture-error {:error error
+                                                         :payload {:type type
+                                                                   :payload payload}}])
+                      (p/reject! d error))))))
       (recur))
     chan))
 

+ 18 - 15
src/main/frontend/mobile/action_bar.cljs

@@ -1,19 +1,20 @@
 (ns frontend.mobile.action-bar
   "Block Action bar, activated when swipe on a block"
-  (:require
-   [frontend.db :as db]
-   [frontend.extensions.srs :as srs]
-   [frontend.handler.editor :as editor-handler]
-   [frontend.mixins :as mixins]
-   [frontend.state :as state]
-   [frontend.ui :as ui]
-   [frontend.util :as util]
-   [frontend.util.url :as url-util]
-   [goog.dom :as gdom]
-   [goog.object :as gobj]
-   [rum.core :as rum]
-   [logseq.common.util.block-ref :as block-ref]
-   [frontend.mobile.util :as mobile-util]))
+  (:require [frontend.config :as config]
+            [frontend.db :as db]
+            [frontend.extensions.fsrs :as fsrs]
+            [frontend.extensions.srs :as srs]
+            [frontend.handler.editor :as editor-handler]
+            [frontend.mixins :as mixins]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.state :as state]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [frontend.util.url :as url-util]
+            [goog.dom :as gdom]
+            [goog.object :as gobj]
+            [logseq.common.util.block-ref :as block-ref]
+            [rum.core :as rum]))
 
 (defn- action-command
   [icon description command-handler]
@@ -56,7 +57,9 @@
           (.scrollBy (util/app-scroll-container-node) #js {:top (- 10 delta)})))
       [:div.action-bar
        [:div.action-bar-commands
-        (action-command "infinity" "Card" #(srs/make-block-a-card! (:block/uuid block)))
+        (action-command "infinity" "Card" #(if (config/db-based-graph? (state/get-current-repo))
+                                             (fsrs/batch-make-cards! [(:block/uuid block)])
+                                             (srs/batch-make-cards! [(:block/uuid block)])))
         (action-command "copy" "Copy" #(editor-handler/copy-selection-blocks false))
         (action-command "cut" "Cut" #(editor-handler/cut-selection-blocks true))
         (action-command "trash" "Delete" #(editor-handler/delete-block-aux! block))

+ 12 - 0
src/main/frontend/modules/shortcut/config.cljs

@@ -180,6 +180,18 @@
    :cards/recall                            {:binding "t"
                                              :fn      srs/recall}
 
+   :cards/again                             {:binding "1"
+                                             :fn      srs/card-again}
+
+   :cards/hard                              {:binding "2"
+                                             :fn      srs/card-hard}
+
+   :cards/good                              {:binding "3"
+                                             :fn      srs/card-good}
+
+   :cards/easy                              {:binding "4"
+                                             :fn      srs/card-easy}
+
    :editor/escape-editing                   {:binding []
                                              :fn      (fn [_ _]
                                                         (editor-handler/escape-editing))}

+ 33 - 10
src/main/frontend/worker/db/migrate.cljs

@@ -123,7 +123,7 @@
                                          types)]
                               (when type
                                 [[:db/retract id :block/type]
-                                [:db/add id :block/type type]])))))
+                                 [:db/add id :block/type type]])))))
         schema (:schema db)]
     (ldb/transact! conn new-type-tx {:db-migrate? true})
     (d/reset-schema! conn (update schema :block/type #(assoc % :db/cardinality :db.cardinality/one)))
@@ -134,12 +134,12 @@
   (let [db @conn]
     (when (ldb/db-based-graph? db)
       (let [datoms (d/datoms db :avet :class/parent)]
-       (->> (set (map :e datoms))
-            (mapcat
-             (fn [id]
-               (let [value (:db/id (:class/parent (d/entity db id)))]
-                 [[:db/retract id :class/parent]
-                  [:db/add id :logseq.property/parent value]]))))))))
+        (->> (set (map :e datoms))
+             (mapcat
+              (fn [id]
+                (let [value (:db/id (:class/parent (d/entity db id)))]
+                  [[:db/retract id :class/parent]
+                   [:db/add id :logseq.property/parent value]]))))))))
 
 (defn- deprecate-class-schema-properties
   [conn _search-db]
@@ -184,13 +184,30 @@
             fix-data (keep
                       (fn [d]
                         (if-let [id (if (= :all-pages (:v d))
-                                   (:db/id (ldb/get-case-page db common-config/views-page-name))
-                                   (:db/id (d/entity db (:v d))))]
+                                      (:db/id (ldb/get-case-page db common-config/views-page-name))
+                                      (:db/id (d/entity db (:v d))))]
                           [:db/add (:e d) :logseq.property/view-for id]
                           [:db/retract (:e d) :logseq.property/view-for (:v d)]))
                       datoms)]
         (cons fix-schema fix-data)))))
 
+(defn- add-card-properties
+  [conn _search-db]
+  (let [db @conn]
+    (when (ldb/db-based-graph? db)
+      (let [card (d/entity db :logseq.class/Card)
+            card-id (:db/id card)]
+        [[:db/add card-id :logseq.property.class/properties :logseq.property.fsrs/due]
+         [:db/add card-id :logseq.property.class/properties :logseq.property.fsrs/state]]))))
+
+(defn- add-query-property-to-query-tag
+  [conn _search-db]
+  (let [db @conn]
+    (when (ldb/db-based-graph? db)
+      (let [query (d/entity db :logseq.class/Query)
+            query-id (:db/id query)]
+        [[:db/add query-id :logseq.property.class/properties :logseq.property/query]]))))
+
 (defn- add-addresses-in-kvs-table
   [^Object sqlite-db]
   (let [columns (->> (.exec sqlite-db #js {:sql "SELECT NAME FROM PRAGMA_TABLE_INFO('kvs')"
@@ -252,7 +269,13 @@
    [18 {:properties [:logseq.property.view/type]}]
    [19 {:classes [:logseq.class/Query]}]
    [20 {:fix fix-view-for}]
-   [21 {:properties [:logseq.property.table/sized-columns]}]])
+   [21 {:properties [:logseq.property.table/sized-columns]}]
+   [22 {:properties [:logseq.property.fsrs/state :logseq.property.fsrs/due]}]
+   [23 {:fix add-card-properties}]
+   [24 {:classes [:logseq.class/Cards]}]
+   [25 {:classes [:logseq.class/Advanced-query]
+        :properties [:logseq.property/query]
+        :fix add-query-property-to-query-tag}]])
 
 (let [max-schema-version (apply max (map first schema-version->updates))]
   (assert (<= db-schema/version max-schema-version))

+ 2 - 2
src/main/frontend/worker/rtc/db_listener.cljs

@@ -26,8 +26,8 @@
     :db/index :db/valueType :db/cardinality})
 
 (def ^:private watched-attr-ns
-  #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.task"
-    "logseq.property.linked-references"
+  #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.property.fsrs"
+    "logseq.property.linked-references" "logseq.task"
     "logseq.class" "logseq.kv"})
 
 (defn- watched-attr?

+ 104 - 105
src/main/logseq/api.cljs

@@ -54,8 +54,7 @@
             [frontend.handler.search :as search-handler]
             [logseq.api.block :as api-block]
             [logseq.db :as ldb]
-            [logseq.db.frontend.property.util :as db-property-util]
-            [logseq.db.frontend.property :as db-property]))
+            [logseq.db.frontend.property.util :as db-property-util]))
 
 ;; Alert: this namespace shouldn't invoke any reactive queries
 
@@ -74,7 +73,7 @@
 (defn- db-graph?
   []
   (some-> (state/get-current-repo)
-    (config/db-based-graph?)))
+          (config/db-based-graph?)))
 
 (defn get-caller-plugin-id
   [] (gobj/get js/window "$$callerPluginID"))
@@ -83,16 +82,16 @@
   [k]
   (if (string? k)
     (-> k (string/trim)
-      (string/replace #"^\:+" "")
-      (string/lower-case))
+        (string/replace #"^\:+" "")
+        (string/lower-case))
     k))
 
 (defn- encode-user-property-name
   [k]
-  (if (string? k)
+  (when (string? k)
     (-> k (string/trim)
-      (string/replace "/" "")
-      (string/replace " " ""))))
+        (string/replace "/" "")
+        (string/replace " " ""))))
 
 ;; helpers
 (defn ^:export install-plugin-hook
@@ -134,25 +133,25 @@
   ;; get app base info
   []
   (bean/->js
-    (sdk-utils/normalize-keyword-for-json
-      {:version fv/version})))
+   (sdk-utils/normalize-keyword-for-json
+    {:version fv/version})))
 
 (def ^:export get_user_configs
   (fn []
     (bean/->js
-      (sdk-utils/normalize-keyword-for-json
-        {:preferred-language      (:preferred-language @state/state)
-         :preferred-theme-mode    (:ui/theme @state/state)
-         :preferred-format        (state/get-preferred-format)
-         :preferred-workflow      (state/get-preferred-workflow)
-         :preferred-todo          (state/get-preferred-todo)
-         :preferred-date-format   (state/get-date-formatter)
-         :preferred-start-of-week (state/get-start-of-week)
-         :current-graph           (state/get-current-repo)
-         :show-brackets           (state/show-brackets?)
-         :enabled-journals        (state/enable-journals?)
-         :enabled-flashcards      (state/enable-flashcards?)
-         :me                      (state/get-me)}))))
+     (sdk-utils/normalize-keyword-for-json
+      {:preferred-language      (:preferred-language @state/state)
+       :preferred-theme-mode    (:ui/theme @state/state)
+       :preferred-format        (state/get-preferred-format)
+       :preferred-workflow      (state/get-preferred-workflow)
+       :preferred-todo          (state/get-preferred-todo)
+       :preferred-date-format   (state/get-date-formatter)
+       :preferred-start-of-week (state/get-start-of-week)
+       :current-graph           (state/get-current-repo)
+       :show-brackets           (state/show-brackets?)
+       :enabled-journals        (state/enable-journals?)
+       :enabled-flashcards      (state/enable-flashcards?)
+       :me                      (state/get-me)}))))
 
 (def ^:export get_current_graph_configs
   (fn [& keys]
@@ -171,19 +170,19 @@
   (fn []
     (if (db-graph?)
       (-> (page-handler/get-favorites)
-        (p/then #(-> % (sdk-utils/normalize-keyword-for-json) (bean/->js))))
+          (p/then #(-> % (sdk-utils/normalize-keyword-for-json) (bean/->js))))
       (some->> (:favorites (state/get-config))
-        (remove string/blank?)
-        (filter string?)
-        (bean/->js)))))
+               (remove string/blank?)
+               (filter string?)
+               (bean/->js)))))
 
 (def ^:export get_current_graph_recent
   (fn []
     (some->> (recent-handler/get-recent-pages)
-      (map #(db-utils/entity (:db/id %)))
-      (remove nil?)
-      (sdk-utils/normalize-keyword-for-json)
-      (bean/->js))))
+             (map #(db-utils/entity (:db/id %)))
+             (remove nil?)
+             (sdk-utils/normalize-keyword-for-json)
+             (bean/->js))))
 
 (def ^:export get_current_graph_templates
   (fn []
@@ -335,8 +334,8 @@
                         (plugin-handler/get-ls-dotdir-root))
             plugin-id (util/node-path.basename plugin-id)
             exist?    (fs/file-exists?
-                        (util/node-path.join root "storages" plugin-id)
-                        file)]
+                       (util/node-path.join root "storages" plugin-id)
+                       file)]
       exist?)))
 
 (def ^:export clear_plugin_storage_files
@@ -357,8 +356,8 @@
             ^js files  (ipc/ipc :listdir files-path)]
       (when (js-iterable? files)
         (bean/->js
-          (map #(some-> (string/replace-first % files-path "")
-                        (string/replace #"^/+" "")) files))))))
+         (map #(some-> (string/replace-first % files-path "")
+                       (string/replace #"^/+" "")) files))))))
 
 (def ^:export load_user_preferences
   (fn []
@@ -394,8 +393,8 @@
   (fn [pid ^js cmd-actions]
     (when-let [[cmd actions] (bean/->clj cmd-actions)]
       (plugin-handler/register-plugin-slash-command
-        pid [cmd (mapv #(into [(keyword (first %))]
-                              (rest %)) actions)]))))
+       pid [cmd (mapv #(into [(keyword (first %))]
+                             (rest %)) actions)]))))
 
 (def ^:export register_plugin_simple_command
   (fn [pid ^js cmd-action palette?]
@@ -460,7 +459,7 @@
   (fn [pid type ^js opts]
     (when-let [opts (bean/->clj opts)]
       (plugin-handler/register-plugin-ui-item
-        pid (assoc opts :type type)))))
+       pid (assoc opts :type type)))))
 
 ;; app
 (def ^:export relaunch
@@ -581,9 +580,9 @@
     (when-let [blocks (state/selection?)]
       (let [blocks (->> blocks
                         (map (fn [^js el] (some->
-                                            (.getAttribute el "blockid")
-                                            (db-model/query-block-by-uuid)
-                                            (api-block/into-properties)))))]
+                                           (.getAttribute el "blockid")
+                                           (db-model/query-block-by-uuid)
+                                           (api-block/into-properties)))))]
         (bean/->js (sdk-utils/normalize-keyword-for-json blocks))))))
 
 (def ^:export clear_selected_blocks
@@ -595,7 +594,7 @@
     (when-let [page (state/get-current-page)]
       (p/let [page (<pull-block page)]
         (when-let [page (and (:block/name page)
-                          (some->> page (api-block/into-properties (state/get-current-repo))))]
+                             (some->> page (api-block/into-properties (state/get-current-repo))))]
           (bean/->js (sdk-utils/normalize-keyword-for-json page)))))))
 
 (def ^:export get_page
@@ -609,21 +608,21 @@
                                    :else
                                    [:block/name (util/page-name-sanity-lc id-or-page-name)]))]
       (when-let [page (and (:block/name page)
-                        (some->> page (api-block/into-properties (state/get-current-repo))))]
+                           (some->> page (api-block/into-properties (state/get-current-repo))))]
         (bean/->js (sdk-utils/normalize-keyword-for-json page))))))
 
 (def ^:export get_all_pages
   (fn []
     (let [db (conn/get-db (state/get-current-repo))]
       (some->
-        (->>
-          (d/datoms db :avet :block/name)
-          (map #(db-utils/pull (:e %)))
-          (remove ldb/hidden?)
-          (remove (fn [page]
-                    (common-util/uuid-string? (:block/name page)))))
-        (sdk-utils/normalize-keyword-for-json)
-        (bean/->js)))))
+       (->>
+        (d/datoms db :avet :block/name)
+        (map #(db-utils/pull (:e %)))
+        (remove ldb/hidden?)
+        (remove (fn [page]
+                  (common-util/uuid-string? (:block/name page)))))
+       (sdk-utils/normalize-keyword-for-json)
+       (bean/->js)))))
 
 (def ^:export create_page
   (fn [name ^js properties ^js opts]
@@ -633,15 +632,15 @@
       (p/let [page (<pull-block name)
               new-page (when-not page
                          (page-handler/<create!
-                           name
-                           (cond->
-                             {:redirect? (if (boolean? redirect) redirect true)
-                              :journal? journal
-                              :create-first-block? (if (boolean? createFirstBlock) createFirstBlock true)
-                              :format format}
-
-                             (not db-base?)
-                             (assoc :properties properties))))
+                          name
+                          (cond->
+                           {:redirect? (if (boolean? redirect) redirect true)
+                            :journal? journal
+                            :create-first-block? (if (boolean? createFirstBlock) createFirstBlock true)
+                            :format format}
+
+                            (not db-base?)
+                            (assoc :properties properties))))
               _ (when (and db-base? (seq properties))
                   (api-block/save-db-based-block-properties! new-page properties))]
         (some-> (or page new-page)
@@ -660,9 +659,9 @@
 (defn ^:export open_in_right_sidebar
   [block-id-or-uuid]
   (editor-handler/open-block-in-sidebar!
-    (if (number? block-id-or-uuid)
-      block-id-or-uuid
-      (sdk-utils/uuid-or-throw-error block-id-or-uuid))))
+   (if (number? block-id-or-uuid)
+     block-id-or-uuid
+     (sdk-utils/uuid-or-throw-error block-id-or-uuid))))
 
 (defn ^:export new_block_uuid []
   (str (db/new-block-id)))
@@ -757,10 +756,10 @@
                 block (if before
                         (db/pull (:db/id (ldb/get-left-sibling (db/entity (:db/id block))))) block)]
             (some-> (editor-handler/insert-block-tree-after-target
-                      (:db/id block) sibling bb (:block/format block) keep-uuid?)
-              (p/then (fn [results]
-                        (some-> results (ldb/read-transit-str)
-                          :blocks (sdk-utils/normalize-keyword-for-json) (bean/->js)))))))))))
+                     (:db/id block) sibling bb (:block/format block) keep-uuid?)
+                    (p/then (fn [results]
+                              (some-> results (ldb/read-transit-str)
+                                      :blocks (sdk-utils/normalize-keyword-for-json) (bean/->js)))))))))))
 
 (def ^:export remove_block
   (fn [block-uuid ^js _opts]
@@ -777,29 +776,29 @@
             opts (bean/->clj opts)]
       (when block
         (p/do!
-          (when (and db-base? (some? (:properties opts)))
-            (api-block/save-db-based-block-properties! block (:properties opts)))
-          (editor-handler/save-block! repo
-            (sdk-utils/uuid-or-throw-error block-uuid) content
-            (if db-base? (dissoc opts :properties) opts)))))))
+         (when (and db-base? (some? (:properties opts)))
+           (api-block/save-db-based-block-properties! block (:properties opts)))
+         (editor-handler/save-block! repo
+                                     (sdk-utils/uuid-or-throw-error block-uuid) content
+                                     (if db-base? (dissoc opts :properties) opts)))))))
 
 (def ^:export move_block
   (fn [src-block-uuid target-block-uuid ^js opts]
     (p/let [_ (<pull-block src-block-uuid)
             _ (<pull-block target-block-uuid)]
       (let [{:keys [before children]} (bean/->clj opts)
-           move-to      (cond
-                          (boolean before)
-                          :top
+            move-to      (cond
+                           (boolean before)
+                           :top
 
-                          (boolean children)
-                          :nested
+                           (boolean children)
+                           :nested
 
-                          :else
-                          nil)
-           src-block    (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error src-block-uuid))
-           target-block (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error target-block-uuid))]
-       (editor-dnd-handler/move-blocks nil [src-block] target-block nil move-to)))))
+                           :else
+                           nil)
+            src-block    (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error src-block-uuid))
+            target-block (db-model/query-block-by-uuid (sdk-utils/uuid-or-throw-error target-block-uuid))]
+        (editor-dnd-handler/move-blocks nil [src-block] target-block nil move-to)))))
 
 (def ^:export get_block
   (fn [id ^js opts]
@@ -854,7 +853,7 @@
 (defn convert?to-built-in-property-name
   [property-name]
   (if (and (not (qualified-keyword? property-name))
-        (contains? #{:background-color} property-name))
+           (contains? #{:background-color} property-name))
     (keyword :logseq.property property-name)
     property-name))
 
@@ -893,19 +892,19 @@
     (p/let [opts (or (some-> opts (bean/->clj)) {})
             name (or (:name opts) (some-> (str k) (string/trim)))
             k (if (qualified-keyword? k') k'
-                (get-db-ident-for-property-name k))
+                  (get-db-ident-for-property-name k))
             schema (or (some-> schema (bean/->clj)
-                         (update-keys #(if (contains? #{:hide :public} %)
-                                         (keyword (str (name %) "?")) %))) {})
+                               (update-keys #(if (contains? #{:hide :public} %)
+                                               (keyword (str (name %) "?")) %))) {})
             schema (cond-> schema
                      (string? (:cardinality schema))
                      (update :cardinality keyword)
                      (string? (:type schema))
                      (update :type keyword))
             p (db-property-handler/upsert-property! k schema
-                (cond-> opts
-                  name
-                  (assoc :property-name name)))]
+                                                    (cond-> opts
+                                                      name
+                                                      (assoc :property-name name)))]
       (bean/->js (sdk-utils/normalize-keyword-for-json p)))))
 
 ;; block properties
@@ -931,8 +930,8 @@
             key (if key-ns? key (-> (if (keyword? key) (name key) key) (util/safe-lower-case)))
             key (if (and db? (not key-ns?)) (get-db-ident-for-property-name key) key)]
       (property-handler/remove-block-property!
-      (state/get-current-repo)
-      block-uuid key))))
+       (state/get-current-repo)
+       block-uuid key))))
 
 (def ^:export get_block_property
   (fn [block-uuid key]
@@ -943,8 +942,8 @@
           (let [key (sanitize-user-property-name key)
                 property-name (-> (if (keyword? key) (name key) key) (util/safe-lower-case))
                 property-value (or (get properties key)
-                                 (get properties (keyword property-name))
-                                 (get properties (get-db-ident-for-property-name property-name)))
+                                   (get properties (keyword property-name))
+                                   (get properties (get-db-ident-for-property-name property-name)))
                 property-value (if-let [property-id (:db/id property-value)]
                                  (db/pull property-id) property-value)
                 ret (sdk-utils/normalize-keyword-for-json property-value)]
@@ -1020,12 +1019,12 @@
                                                                        :format              (state/get-preferred-format)}))]
     (when-let [block (db-model/get-page uuid-or-page-name)]
       (-> (api-block/sync-children-blocks! block)
-        (p/then (fn []
-                  (let [block' (first-child-of-block block)
-                        opts (bean/->clj opts)
-                        [block opts] (if block' [block' (assoc opts :before true :sibling true)] [block opts])
-                        target (str (:block/uuid block))]
-                    (insert_block target content (bean/->js opts)))))))))
+          (p/then (fn []
+                    (let [block' (first-child-of-block block)
+                          opts (bean/->clj opts)
+                          [block opts] (if block' [block' (assoc opts :before true :sibling true)] [block opts])
+                          target (str (:block/uuid block))]
+                      (insert_block target content (bean/->js opts)))))))))
 
 (defn ^:export append_block_in_page
   [uuid-or-page-name content ^js opts]
@@ -1076,7 +1075,7 @@
                                       :else %)
                                    inputs)
               result          (apply db-async/<q repo {:transact-db? false}
-                                (cons query resolved-inputs))]
+                                     (cons query resolved-inputs))]
         (bean/->js (sdk-utils/normalize-keyword-for-json result false))))))
 
 (defn ^:export custom_query
@@ -1121,10 +1120,10 @@
                 (plugin-handler/register-plugin-resources pid :scripts {:key s :src s})
                 (upt-status :pending)
                 (-> (loader/load s nil {:attributes {:data-ref (name pid)}})
-                  (p/then (fn [] (upt-status :done)))
-                  (p/catch (fn [] (upt-status :error))))))
-      (vec)
-      (p/all))))
+                    (p/then (fn [] (upt-status :done)))
+                    (p/catch (fn [] (upt-status :error))))))
+            (vec)
+            (p/all))))
 
 ;; http request
 (defonce *request-k (volatile! 0))
@@ -1156,7 +1155,7 @@
   (p/let [exists? (page-handler/<template-exists? template-name)]
     (when exists?
       (when-let [target (db-model/get-block-by-uuid target-uuid)]
-       (editor-handler/insert-template! nil template-name {:target target}) nil))))
+        (editor-handler/insert-template! nil template-name {:target target}) nil))))
 
 (defn ^:export exist_template
   [name]

+ 4 - 0
src/resources/dicts/en.edn

@@ -638,6 +638,10 @@
   :cards/forgotten              "Cards: forgotten"
   :cards/remembered             "Cards: remembered"
   :cards/recall                 "Cards: take a while to recall"
+  :cards/again                  "Cards: again"
+  :cards/hard                   "Cards: hard"
+  :cards/good                   "Cards: good"
+  :cards/easy                   "Cards: easy"
   :editor/escape-editing        "Escape editing"
   :editor/backspace             "Backspace / Delete backwards"
   :editor/delete                "Delete / Delete forwards"

+ 1 - 1
src/test/frontend/db/db_based_model_test.cljs

@@ -24,7 +24,7 @@
   (let [opts {:redirect? false :create-first-block? false :class? true}
         _ (test-helper/create-page! "class1" opts)
         _ (test-helper/create-page! "class2" opts)]
-    (is (= ["Card" "Journal" "Query" "Root Tag" "Task" "class1" "class2"] (sort (map :block/title (model/get-all-classes repo)))))))
+    (is (= ["Advanced query" "Card" "Cards" "Journal" "Query" "Root Tag" "Task" "class1" "class2"] (sort (map :block/title (model/get-all-classes repo)))))))
 
 (deftest ^:fix-me get-class-objects-test
   (let [opts {:redirect? false :create-first-block? false :class? true}

+ 15 - 0
yarn.lock

@@ -552,6 +552,21 @@
     "@jridgewell/resolve-uri" "^3.1.0"
     "@jridgewell/sourcemap-codec" "^1.4.14"
 
+"@js-joda/[email protected]":
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
+  integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==
+
+"@js-joda/[email protected]":
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/@js-joda/locale_en-us/-/locale_en-us-3.1.1.tgz#c2eab2561aa048f5366046edd313ce94108263c6"
+  integrity sha512-EYrs4h0Um/9LqcEwDb0kGTHGaGkJgEO2cj78KKICPz7hsdvJHPOADIkDtjesYInZ1YkNrtE3HopnfETLDBvnWg==
+
+"@js-joda/[email protected]":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@js-joda/timezone/-/timezone-2.5.0.tgz#b422ff400c25ae311384239c62724eecee2e442b"
+  integrity sha512-HHFVhGUKIOtITiT+sbQRdYuO5Q+a8FDj/vQSGUSxe6+5w2in5JsavfRsAN2tU/NCdBeFx/6q8evHMtOrXfdn2g==
+
 "@logseq/[email protected]":
   version "5.0.2"
   resolved "https://registry.yarnpkg.com/@logseq/capacitor-file-sync/-/capacitor-file-sync-5.0.2.tgz#10c56e35b41b1a0afd293c9b045fbcfe150c3477"