Browse Source

feat: time tracking (#11666)

* Schema update for property history

* feat: record property history

* Initial support for time tracking

* enhance: status history view

* fix: reactive time tracking

* fix: tests

* enhance: set max height for status history

* enhance: disable some options for property enable-history?

* address suggestions from zhiyuan

* fix: spent-time for doing tasks

* enhance: status history support reverse order

* chore: merge feat/db
Tienson Qin 10 months ago
parent
commit
b24ad28bac

+ 13 - 5
deps/db/src/logseq/db/frontend/malli_schema.cljs

@@ -2,14 +2,14 @@
   "Malli schemas and fns for logseq.db.frontend.*"
   (:require [clojure.set :as set]
             [clojure.string :as string]
-            [logseq.db.frontend.schema :as db-schema]
-            [logseq.db.frontend.property.type :as db-property-type]
             [datascript.core :as d]
-            [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.class :as db-class]
             [logseq.db.frontend.entity-plus :as entity-plus]
             [logseq.db.frontend.entity-util :as entity-util]
-            [logseq.db.frontend.order :as db-order]))
+            [logseq.db.frontend.order :as db-order]
+            [logseq.db.frontend.property :as db-property]
+            [logseq.db.frontend.property.type :as db-property-type]
+            [logseq.db.frontend.schema :as db-schema]))
 
 ;; :db/ident malli schemas
 ;; =======================
@@ -389,6 +389,13 @@
     (remove #(#{:block/title :logseq.property/created-from-property} (first %)) block-attrs)
     page-or-block-attrs)))
 
+(def property-history-block
+  [:map
+   [:block/uuid :uuid]
+   [:block/created-at :int]
+   [:block/properties block-properties]
+   [:block/tx-id {:optional true} :int]])
+
 (def closed-value-block*
   (vec
    (concat
@@ -424,7 +431,8 @@
    normal-block
    closed-value-block
    whiteboard-block
-   property-value-block])
+   property-value-block
+   property-history-block])
 
 (def asset-block
   "A block tagged with #Asset"

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

@@ -1,14 +1,14 @@
 (ns logseq.db.frontend.property
   "Property related fns for DB graphs and frontend/datascript usage"
-  (:require [clojure.string :as string]
+  (:require [clojure.set :as set]
+            [clojure.string :as string]
             [datascript.core :as d]
             [flatland.ordered.map :refer [ordered-map]]
+            [logseq.common.util :as common-util]
             [logseq.common.uuid :as common-uuid]
             [logseq.db.frontend.db-ident :as db-ident]
-            [clojure.set :as set]
             [logseq.db.frontend.order :as db-order]
-            [logseq.db.frontend.property.type :as db-property-type]
-            [logseq.common.util :as common-util]))
+            [logseq.db.frontend.property.type :as db-property-type]))
 
 (defn build-property-value-block
   "Builds a property value entity given a block map/entity, a property entity or
@@ -313,7 +313,8 @@
            [:logseq.task/priority.medium "Medium" "priorityLvlMedium"]
            [:logseq.task/priority.high "High" "priorityLvlHigh"]
            [:logseq.task/priority.urgent "Urgent" "priorityLvlUrgent"]])
-    :properties {:logseq.property/hide-empty-value true}}
+    :properties {:logseq.property/hide-empty-value true
+                 :logseq.property/enable-history? true}}
    :logseq.task/status
    {:title "Status"
     :schema
@@ -335,7 +336,8 @@
            [:logseq.task/status.done "Done" "Done" true]
            [:logseq.task/status.canceled "Canceled" "Cancelled"]])
     :properties {:logseq.property/hide-empty-value true
-                 :logseq.property/default-value :logseq.task/status.todo}
+                 :logseq.property/default-value :logseq.task/status.todo
+                 :logseq.property/enable-history? true}
     :queryable? true}
    :logseq.task/deadline
    {:title "Deadline"
@@ -520,7 +522,23 @@
                                  :schema
                                  {:type :string
                                   :hide? false
-                                  :public? true}}))
+                                  :public? true}}
+   :logseq.property/enable-history? {:title "Enable property history"
+                                     :schema {:type :checkbox
+                                              :public? true
+                                              :view-context :property}}
+   :logseq.property.history/block {:title "History block"
+                                   :schema {:type :entity
+                                            :hide? true}}
+   :logseq.property.history/property {:title "History property"
+                                      :schema {:type :property
+                                               :hide? true}}
+   :logseq.property.history/ref-value {:title "History value"
+                                       :schema {:type :entity
+                                                :hide? true}}
+   :logseq.property.history/scalar-value {:title "History scalar value"
+                                          :schema {:type :any
+                                                   :hide? true}}))
 
 (def built-in-properties
   (->> built-in-properties*
@@ -568,7 +586,7 @@
     ;; attribute ns is for db attributes that don't start with :block
     "logseq.property.attribute"
     "logseq.property.journal" "logseq.property.class" "logseq.property.view"
-    "logseq.property.user"})
+    "logseq.property.user" "logseq.property.history"})
 
 (defn logseq-property?
   "Determines if keyword is a logseq property"

+ 1 - 1
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 55)
+(def version 56)
 
 ;; A page is a special block, a page can corresponds to multiple files with the same ":block/name".
 (def ^:large-vars/data-var schema

+ 9 - 8
deps/outliner/src/logseq/outliner/pipeline.cljs

@@ -1,16 +1,16 @@
 (ns logseq.outliner.pipeline
   "Core fns for use with frontend worker and node"
-  (:require [datascript.core :as d]
-            [datascript.impl.entity :as de]
+  (:require [cljs-time.coerce :as tc]
+            [cljs-time.core :as t]
+            [cljs-time.format :as tf]
             [clojure.set :as set]
+            [datascript.core :as d]
+            [datascript.impl.entity :as de]
             [logseq.db :as ldb]
             [logseq.db.frontend.content :as db-content]
-            [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.entity-plus :as entity-plus]
-            [logseq.outliner.datascript-report :as ds-report]
-            [cljs-time.core :as t]
-            [cljs-time.coerce :as tc]
-            [cljs-time.format :as tf]))
+            [logseq.db.frontend.property :as db-property]
+            [logseq.outliner.datascript-report :as ds-report]))
 
 (defn filter-deleted-blocks
   [datoms]
@@ -161,7 +161,8 @@
                     (->> (entity-plus/lookup-kv-then-entity (d/entity db (:db/id block)) :block/properties)
                          (into {}))
                     ;; both page and parent shouldn't be counted as refs
-                    (dissoc :block/parent :block/page))
+                    (dissoc :block/parent :block/page
+                            :logseq.property.history/block :logseq.property.history/property :logseq.property.history/ref-value))
         property-key-refs (->> (keys properties)
                                (remove private-built-in-props))
         page-or-object? (fn [block]

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

@@ -64,6 +64,7 @@
             [frontend.template :as template]
             [frontend.ui :as ui]
             [frontend.util :as util]
+            [frontend.util.file-based.clock :as clock]
             [frontend.util.file-based.drawer :as drawer]
             [frontend.util.text :as text-util]
             [goog.dom :as gdom]
@@ -2719,11 +2720,55 @@
            (when-let [property (db/entity pid)]
              (pv/property-value block property (assoc opts :show-tooltip? true))))]))))
 
+(rum/defc status-history-cp
+  [status-history]
+  (let [[sort-desc? set-sort-desc!] (rum/use-state true)]
+    [:div.p-2.text-muted-foreground.text-sm.max-h-96
+     [:div.font-medium.mb-2.flex.flex-row.gap-2.items-center
+      [:div "Status history"]
+      (shui/button-ghost-icon (if sort-desc? :arrow-down :arrow-up)
+                              {:title "Sort order"
+                               :class "text-muted-foreground !h-4 !w-4"
+                               :icon-props {:size 14}
+                               :on-click #(set-sort-desc! (not sort-desc?))})]
+     [:div.flex.flex-col.gap-1
+      (for [item (if sort-desc? (reverse status-history) status-history)]
+        (let [status (:logseq.property.history/ref-value item)]
+          [:div.flex.flex-row.gap-1.items-center.text-sm.justify-between
+           [:div.flex.flex-row.gap-1.items-center
+            (icon-component/get-node-icon-cp status {:size 14 :color? true})
+            [:div (:block/title status)]]
+           [:div (date/int->local-time-2 (:block/created-at item))]]))]]))
+
+(rum/defc task-spent-time-cp
+  [block]
+  (when (and (state/enable-timetracking?) (ldb/class-instance? (db/entity :logseq.class/Task) block))
+    (let [[result set-result!] (rum/use-state nil)
+          repo (state/get-current-repo)
+          [status-history time-spent] result]
+      (rum/use-effect!
+       (fn []
+         (p/let [result (db-async/<task-spent-time repo (:db/id block))]
+           (set-result! result)))
+       [(:logseq.task/status block)])
+      (when (and time-spent (> time-spent 0))
+        [:div.text-sm.time-spent.ml-1
+         (shui/button
+          {:variant :ghost
+           :size :sm
+           :class "text-muted-foreground !py-0 !px-1 h-6"
+           :on-click (fn [e]
+                       (shui/popup-show! (.-target e)
+                                         (fn [] (status-history-cp status-history))
+                                         {:align :end}))}
+          (clock/seconds->days:hours:minutes:seconds time-spent))]))))
+
 (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?]
   (let [collapsed? (:collapsed? config)
         repo (state/get-current-repo)
-        content (if (config/db-based-graph? (state/get-current-repo))
+        db-based? (config/db-based-graph? (state/get-current-repo))
+        content (if db-based?
                   (:block/raw-title block)
                   (property-util/remove-built-in-properties format (:block/raw-title block)))
         block (merge block (block/parse-title-and-body uuid format pre-block? content))
@@ -2795,7 +2840,9 @@
          [:div.block-head-wrap
           (block-title config block)])
 
-       (file-block/clock-summary-cp block ast-body)]
+       (if db-based?
+         (task-spent-time-cp block)
+         (file-block/clock-summary-cp block ast-body))]
 
       (when deadline
         (when-let [deadline-ast (block-handler/get-deadline-ast block)]

+ 41 - 39
src/main/frontend/components/property/config.cljs

@@ -2,32 +2,32 @@
   (:require [clojure.string :as string]
             [frontend.components.dnd :as dnd]
             [frontend.components.icon :as icon-component]
+            [frontend.components.property.default-value :as pdv]
+            [frontend.components.property.value :as pv]
+            [frontend.components.select :as select]
             [frontend.config :as config]
-            [frontend.handler.common.developer :as dev-common-handler]
-            [frontend.handler.db-based.property :as db-property-handler]
             [frontend.db :as db]
+            [frontend.db-mixins :as db-mixins]
             [frontend.db.async :as db-async]
+            [frontend.db.model :as model]
+            [frontend.handler.common.developer :as dev-common-handler]
+            [frontend.handler.db-based.page :as db-page-handler]
+            [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.property :as property-handler]
             [frontend.handler.route :as route-handler]
             [frontend.state :as state]
+            [frontend.ui :as ui]
             [frontend.util :as util]
+            [goog.dom :as gdom]
             [logseq.db :as ldb]
             [logseq.db.frontend.order :as db-order]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.outliner.core :as outliner-core]
-            [logseq.shui.ui :as shui]
             [logseq.shui.popup.core :as shui-popup]
+            [logseq.shui.ui :as shui]
             [promesa.core :as p]
-            [goog.dom :as gdom]
-            [rum.core :as rum]
-            [frontend.db-mixins :as db-mixins]
-            [frontend.components.property.value :as pv]
-            [frontend.components.property.default-value :as pdv]
-            [frontend.components.select :as select]
-            [frontend.db.model :as model]
-            [frontend.handler.db-based.page :as db-page-handler]
-            [frontend.ui :as ui]))
+            [rum.core :as rum]))
 
 (defn- re-init-commands!
   "Update commands after task status and priority's closed values has been changed"
@@ -625,7 +625,8 @@
      (when (and (contains? db-property-type/default-value-ref-property-types property-type)
                 (not (db-property/many? property))
                 (not (and enable-closed-values?
-                          (seq (:property/closed-values property)))))
+                          (seq (:property/closed-values property))))
+                (not= :logseq.property/enable-history? (:db/ident property)))
        (default-value-subitem property))
 
      (when enable-closed-values?
@@ -670,33 +671,34 @@
                                               (p/then update-cardinality-fn))
                                           (update-cardinality-fn))))})))
 
-     (let [property-type (get-in property [:block/schema :type])
-           group' (->> [(when (and (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
-                                   (contains? #{:default :number :date :checkbox :node} property-type)
-                                   (not
-                                    (and (= :default property-type)
-                                         (empty? (:property/closed-values property))
-                                         (contains? #{nil :properties} (:position property-schema)))))
-                          (let [position (:position property-schema)]
-                            (dropdown-editor-menuitem {:icon :float-left :title "UI position" :desc (some->> position (get position-labels) (:title))
-                                                       :item-props {:class "ui__position-trigger-item"}
+     (when (not= :logseq.property/enable-history? (:db/ident property))
+       (let [property-type (get-in property [:block/schema :type])
+             group' (->> [(when (and (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
+                                     (contains? #{:default :number :date :checkbox :node} property-type)
+                                     (not
+                                      (and (= :default property-type)
+                                           (empty? (:property/closed-values property))
+                                           (contains? #{nil :properties} (:position property-schema)))))
+                            (let [position (:position property-schema)]
+                              (dropdown-editor-menuitem {:icon :float-left :title "UI position" :desc (some->> position (get position-labels) (:title))
+                                                         :item-props {:class "ui__position-trigger-item"}
+                                                         :disabled? config/publishing?
+                                                         :submenu-content (fn [ops] (ui-position-sub-pane property (assoc ops :position position)))})))
+
+                          (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
+                            (dropdown-editor-menuitem {:icon :eye-off :title "Hide by default" :toggle-checked? (boolean (:hide? property-schema))
+                                                       :disabled? config/publishing?
+                                                       :on-toggle-checked-change #(db-property-handler/upsert-property! (:db/ident property)
+                                                                                                                        (assoc property-schema :hide? %) {})}))
+                          (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
+                            (dropdown-editor-menuitem {:icon :eye-off :title "Hide empty value" :toggle-checked? (boolean (:logseq.property/hide-empty-value property))
                                                        :disabled? config/publishing?
-                                                       :submenu-content (fn [ops] (ui-position-sub-pane property (assoc ops :position position)))})))
-
-                        (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
-                          (dropdown-editor-menuitem {:icon :eye-off :title "Hide by default" :toggle-checked? (boolean (:hide? property-schema))
-                                                     :disabled? config/publishing?
-                                                     :on-toggle-checked-change #(db-property-handler/upsert-property! (:db/ident property)
-                                                                                                                      (assoc property-schema :hide? %) {})}))
-                        (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
-                          (dropdown-editor-menuitem {:icon :eye-off :title "Hide empty value" :toggle-checked? (boolean (:logseq.property/hide-empty-value property))
-                                                     :disabled? config/publishing?
-                                                     :on-toggle-checked-change #(db-property-handler/set-block-property! (:db/id property)
-                                                                                                                         :logseq.property/hide-empty-value
-                                                                                                                         (not (:logseq.property/hide-empty-value property)))}))]
-                       (remove nil?))]
-       (when (> (count group') 0)
-         (cons (shui/dropdown-menu-separator) group')))
+                                                       :on-toggle-checked-change #(db-property-handler/set-block-property! (:db/id property)
+                                                                                                                           :logseq.property/hide-empty-value
+                                                                                                                           (not (:logseq.property/hide-empty-value property)))}))]
+                         (remove nil?))]
+         (when (> (count group') 0)
+           (cons (shui/dropdown-menu-separator) group'))))
 
      (when owner-block
        [:<>

+ 54 - 14
src/main/frontend/db/async.cljs

@@ -1,25 +1,25 @@
 (ns frontend.db.async
   "Async queries"
-  (:require [promesa.core :as p]
-            [frontend.state :as state]
+  (:require [cljs-time.coerce :as tc]
+            [cljs-time.core :as t]
+            [cljs-time.format :as tf]
+            [datascript.core :as d]
             [frontend.config :as config]
-            [frontend.db.utils :as db-utils]
+            [frontend.date :as date]
+            [frontend.db :as db]
             [frontend.db.async.util :as db-async-util]
             [frontend.db.file-based.async :as file-async]
-            [frontend.db :as db]
             [frontend.db.model :as db-model]
-            [logseq.db.frontend.rules :as rules]
-            [frontend.persist-db.browser :as db-browser]
-            [datascript.core :as d]
             [frontend.db.react :as react]
-            [frontend.date :as date]
-            [cljs-time.core :as t]
-            [cljs-time.coerce :as tc]
-            [cljs-time.format :as tf]
-            [logseq.db :as ldb]
-            [frontend.util :as util]
+            [frontend.db.utils :as db-utils]
             [frontend.handler.file-based.property.util :as property-util]
-            [logseq.db.frontend.property :as db-property]))
+            [frontend.persist-db.browser :as db-browser]
+            [frontend.state :as state]
+            [frontend.util :as util]
+            [logseq.db :as ldb]
+            [logseq.db.frontend.property :as db-property]
+            [logseq.db.frontend.rules :as rules]
+            [promesa.core :as p]))
 
 (def <q db-async-util/<q)
 (def <pull db-async-util/<pull)
@@ -340,6 +340,46 @@
                      pdf-id)]
     result))
 
+(defn <get-block-properties-history
+  [graph block-id]
+  (p/let [result (<q graph {:transact-db? true}
+                     '[:find [(pull ?b [*]) ...]
+                       :in $ ?block-id
+                       :where
+                       [?b :logseq.property.history/block ?block-id]]
+                     block-id)]
+    (->> (sort-by :block/created-at result)
+         (map (fn [b] (db/entity (:db/id b)))))))
+
+(defn <task-spent-time
+  [graph block-id]
+  (p/let [history (<get-block-properties-history graph block-id)
+          status-history (filter
+                          (fn [b] (= :logseq.task/status (:db/ident (:logseq.property.history/property b))))
+                          history)]
+    (when (seq status-history)
+      (let [time (loop [[last-item item & others] status-history
+                        time 0]
+                   (if item
+                     (let [last-status (:db/ident (:logseq.property.history/ref-value last-item))
+                           this-status (:db/ident (:logseq.property.history/ref-value item))]
+                       (if (and (= this-status :logseq.task/status.doing)
+                                (empty? others))
+                         (-> (+ time (- (tc/to-long (t/now)) (:block/created-at item)))
+                             (quot 1000))
+                         (let [time' (if (or
+                                          (= last-status :logseq.task/status.doing)
+                                          (and
+                                           (not (contains? #{:logseq.task/status.canceled
+                                                             :logseq.task/status.backlog
+                                                             :logseq.task/status.done} last-status))
+                                           (= this-status :logseq.task/status.done)))
+                                       (+ time (- (:block/created-at item) (:block/created-at last-item)))
+                                       time)]
+                           (recur (cons item others) time'))))
+                     (quot time 1000)))]
+        [status-history time]))))
+
 (comment
   (defn <fetch-all-pages
     [graph]

+ 20 - 11
src/main/frontend/util/file_based/clock.cljs

@@ -1,12 +1,12 @@
 (ns frontend.util.file-based.clock
   "Provides clock related functionality used by tasks"
-  (:require [frontend.state :as state]
-            [frontend.util.file-based.drawer :as drawer]
-            [frontend.util :as util]
-            [cljs-time.core :as t]
+  (:require [cljs-time.core :as t]
             [cljs-time.format :as tf]
+            [clojure.string :as string]
             [frontend.date :as date]
-            [clojure.string :as string]))
+            [frontend.state :as state]
+            [frontend.util :as util]
+            [frontend.util.file-based.drawer :as drawer]))
 
 (defn minutes->hours:minutes
   [minutes]
@@ -22,7 +22,7 @@
     (util/format "%02d:%02d:%02d" hours minutes seconds)))
 
 (defn s->dhms-util
-  "A function that returns the values for easier testing. 
+  "A function that returns the values for easier testing.
    Always in the order [days, hours, minutes, seconds]"
   [seconds]
   (let [days (quot (quot seconds 3600) 24)
@@ -36,11 +36,20 @@
 (defn seconds->days:hours:minutes:seconds
   [seconds]
   (let [[days hours minutes seconds] (s->dhms-util seconds)]
-    (util/format "%s%s%s%s"
-                 (if (zero? days) "" (str days "d"))
-                 (if (zero? hours) "" (str hours "h"))
-                 (if (zero? minutes) "" (str minutes "m"))
-                 (if (zero? seconds) "" (str seconds "s")))))
+    (cond
+      (> days 0)
+      (util/format "%s%s"
+                   (if (zero? days) "" (str days "d"))
+                   (if (zero? hours) "" (str hours "h")))
+
+      (> minutes 0)
+      (util/format "%s%s"
+                   (if (zero? hours) "" (str hours "h"))
+                   (if (zero? minutes) "" (str minutes "m")))
+      :else
+      (if (> seconds 1)
+        (str seconds "s")
+        ""))))
 
 (def support-seconds?
   (get-in (state/get-config)

+ 65 - 24
src/main/frontend/worker/commands.cljs

@@ -1,13 +1,14 @@
 (ns frontend.worker.commands
   "Invoke commands based on user settings"
-  (:require [datascript.core :as d]
-            [logseq.db.frontend.property.type :as db-property-type]
+  (:require [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
-            [cljs-time.coerce :as tc]
-            [logseq.db.frontend.property :as db-property]
-            [logseq.outliner.pipeline :as outliner-pipeline]
+            [datascript.core :as d]
             [frontend.worker.handler.page.db-based.page :as worker-db-page]
-            [logseq.common.util.date-time :as date-time-util]))
+            [logseq.common.util.date-time :as date-time-util]
+            [logseq.db :as ldb]
+            [logseq.db.frontend.property :as db-property]
+            [logseq.db.frontend.property.type :as db-property-type]
+            [logseq.outliner.pipeline :as outliner-pipeline]))
 
 ;; TODO: allow users to add command or configure it through #Command (which parent should be #Code)
 (def *commands
@@ -19,7 +20,13 @@
       :tx-conditions [{:property :status
                        :value :done}]
       :actions [[:reschedule]
-                [:set-property :status :todo]]}]]))
+                [:set-property :status :todo]]}]
+    [:property-history
+     {:title "Record property history"
+      :tx-conditions [{:kind :datom-attribute-check?
+                       :property :logseq.property/enable-history?
+                       :value true}]
+      :actions [[:record-property-history]]}]]))
 
 (defn- get-property
   [entity property]
@@ -59,9 +66,9 @@
 
 (defn satisfy-condition?
   "Whether entity or updated datoms satisfy the `condition`"
-  [db entity {:keys [property value]} datoms]
+  [db entity {:keys [kind property value]} datoms]
   (let [property' (get-property entity property)
-        value (get-value entity property value)]
+        value' (get-value entity property value)]
     (when-let [property-entity (d/entity db property')]
       (let [value-matches? (fn [datom-value]
                              (let [ref? (contains? db-property-type/all-ref-property-types (:type (:block/schema property-entity)))
@@ -75,20 +82,26 @@
                                               :else
                                               datom-value)]
                                (cond
-                                 (qualified-keyword? value)
-                                 (and (map? db-value) (= value (:db/ident db-value)))
+                                 (qualified-keyword? value')
+                                 (and (map? db-value) (= value' (:db/ident db-value)))
 
                                  ref?
                                  (or
-                                  (and (uuid? value) (= (:block/uuid db-value) value))
-                                  (= value (db-property/property-value-content db-value))
-                                  (= value (:db/id db-value)))
+                                  (and (uuid? value') (= (:block/uuid db-value) value'))
+                                  (= value' (db-property/property-value-content db-value))
+                                  (= value' (:db/id db-value)))
 
                                  :else
-                                 (= db-value value))))]
+                                 (= db-value value'))))]
         (if (seq datoms)
-          (some (fn [d] (and (value-matches? (:v d)) (:added d)))
-                (filter (fn [d] (= property' (:a d))) datoms))
+          (case kind
+            :datom-attribute-check?
+            (some (fn [d]
+                    (= value' (get (d/entity db (:a d)) property)))
+                  datoms)
+
+            (some (fn [d] (and (value-matches? (:v d)) (:added d)))
+                  (filter (fn [d] (= property' (:a d))) datoms)))
           (value-matches? nil))))))
 
 (defmulti handle-command (fn [action-id & _others] action-id))
@@ -136,7 +149,7 @@
                         (t/plus (t/now) delta))]
         (tc/to-long next-time)))))
 
-(defmethod handle-command :reschedule [_ db entity]
+(defmethod handle-command :reschedule [_ db entity _datoms]
   (let [property-ident (or (:db/ident (:logseq.task/scheduled-on-property entity))
                            :logseq.task/scheduled)
         frequency (db-property/property-value-content (:logseq.task/recur-frequency entity))
@@ -155,16 +168,41 @@
            (when value
              [[:db/add (:db/id entity) property-ident value]])))))))
 
-(defmethod handle-command :set-property [_ _db entity property value]
+(defmethod handle-command :set-property [_ _db entity _datoms property value]
   (let [property' (get-property entity property)
         value' (get-value entity property value)]
     [[:db/add (:db/id entity) property' value']]))
 
+(defmethod handle-command :record-property-history [_ db entity datoms]
+  (let [changes (keep (fn [d]
+                        (let [property (d/entity db (:a d))]
+                          (when (and (true? (get property :logseq.property/enable-history?))
+                                     (:added d))
+                            {:property property
+                             :value (:v d)}))) datoms)
+        created-at (tc/to-long (t/now))]
+    (map
+     (fn [{:keys [property value]}]
+       (let [ref? (= :db.type/ref (:db/valueType property))
+             value-key (if ref? :logseq.property.history/ref-value :logseq.property.history/scalar-value)]
+         {:block/uuid (ldb/new-block-id)
+          value-key value
+          :logseq.property.history/block (:db/id entity)
+          :logseq.property.history/property (:db/id property)
+          :block/created-at created-at}))
+     changes)))
+
+(defmethod handle-command :default [command _db entity datoms]
+  (throw (ex-info "Unhandled command"
+                  {:command command
+                   :entity entity
+                   :datoms datoms})))
+
 (defn execute-command
   "Build tx-data"
-  [db entity [_command {:keys [actions]}]]
+  [db entity datoms [_command {:keys [actions]}]]
   (mapcat (fn [action]
-            (apply handle-command (first action) db entity (rest action))) actions))
+            (apply handle-command (first action) db entity datoms (rest action))) actions))
 
 (defn run-commands
   [{:keys [tx-data db-after]}]
@@ -172,10 +210,13 @@
     (mapcat (fn [[e datoms]]
               (let [entity (d/entity db e)
                     commands (filter (fn [[_command {:keys [entity-conditions tx-conditions]}]]
-                                       (and (every? #(satisfy-condition? db entity % nil) entity-conditions)
-                                            (every? #(satisfy-condition? db entity % datoms) tx-conditions))) @*commands)]
+                                       (and
+                                        (if (seq entity-conditions)
+                                          (every? #(satisfy-condition? db entity % nil) entity-conditions)
+                                          true)
+                                        (every? #(satisfy-condition? db entity % datoms) tx-conditions))) @*commands)]
                 (mapcat
                  (fn [command]
-                   (execute-command db entity command))
+                   (execute-command db entity datoms command))
                  commands)))
             (group-by :e tx-data))))

+ 4 - 1
src/main/frontend/worker/db/migrate.cljs

@@ -579,7 +579,10 @@
                      :logseq.task/scheduled-on-property :logseq.task/recur-status-property]
         :fix add-scheduled-to-task}]
    [54 {:properties [:logseq.property/choice-checkbox-state :logseq.property/checkbox-display-properties]}]
-   [55 {:fix update-deadline-to-datetime}]])
+   [55 {:fix update-deadline-to-datetime}]
+   [56 {:properties [:logseq.property/enable-history?
+                     :logseq.property.history/block :logseq.property.history/property
+                     :logseq.property.history/ref-value :logseq.property.history/scalar-value]}]])
 
 (let [max-schema-version (apply max (map first schema-version->updates))]
   (assert (<= db-schema/version max-schema-version))