From 8a1c54173b40e6ce9b40e25d1397206077f5fc19 Mon Sep 17 00:00:00 2001 From: rcmerci Date: Tue, 20 Jun 2023 18:07:41 +0800 Subject: [PATCH] [wip] property edit --- deps/db/src/logseq/db/schema.cljs | 14 +- .../src/logseq/graph_parser/block.cljs | 4 +- deps/publishing/src/logseq/publishing/db.cljs | 4 +- src/electron/electron/db.cljs | 2 +- src/main/frontend/components/block.cljs | 25 +- src/main/frontend/components/property.cljs | 257 ++++++++++++++++++ src/main/frontend/db/restore.cljs | 4 +- src/main/frontend/handler/property.cljs | 152 +++++++++++ src/main/frontend/publishing.cljs | 2 + src/main/frontend/util/property_edit.cljs | 26 +- 10 files changed, 468 insertions(+), 22 deletions(-) create mode 100644 src/main/frontend/components/property.cljs create mode 100644 src/main/frontend/handler/property.cljs diff --git a/deps/db/src/logseq/db/schema.cljs b/deps/db/src/logseq/db/schema.cljs index 581173aec2..15e49cc791 100644 --- a/deps/db/src/logseq/db/schema.cljs +++ b/deps/db/src/logseq/db/schema.cljs @@ -1,5 +1,6 @@ (ns logseq.db.schema - "Main db schema for the Logseq app") + "Main db schema for the Logseq app" + (:require [frontend.config :as config])) (defonce version 2) (defonce ast-version 1) @@ -15,7 +16,8 @@ ;; :block/type is a string type of the current block ;; "whiteboard" for whiteboards ;; "macros" for macro - :block/type {} + ;; "property" for property blocks + :block/type {:db/index true} :block/uuid {:db/unique :db.unique/identity} :block/parent {:db/valueType :db.type/ref :db/index true} @@ -106,7 +108,13 @@ (merge schema {:property/schema {} - :property/name {}})) + :property/name {:db/unique :db.unique/identity}})) + +(defn get-schema + [repo] + (if (config/db-based-graph? repo) + schema-for-db-based-graph + schema)) (def retract-attributes #{ diff --git a/deps/graph-parser/src/logseq/graph_parser/block.cljs b/deps/graph-parser/src/logseq/graph_parser/block.cljs index 7ee6fe5f79..db9cbc601b 100644 --- a/deps/graph-parser/src/logseq/graph_parser/block.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/block.cljs @@ -34,7 +34,7 @@ ""))) (string/join)))) -(defn- get-page-reference +(defn get-page-reference [block format] (let [page (cond (and (vector? block) (= "Link" (first block))) @@ -83,7 +83,7 @@ nil)] (when page (or (block-ref/get-block-ref-id page) page)))) -(defn- get-block-reference +(defn get-block-reference [block] (when-let [block-id (cond (and (vector? block) diff --git a/deps/publishing/src/logseq/publishing/db.cljs b/deps/publishing/src/logseq/publishing/db.cljs index 935763d802..c17df0e3db 100644 --- a/deps/publishing/src/logseq/publishing/db.cljs +++ b/deps/publishing/src/logseq/publishing/db.cljs @@ -107,7 +107,7 @@ (not (contains? non-public-datom-ids (:e datom))))))) datoms (d/datoms filtered-db :eavt) assets (get-assets db datoms)] - [@(d/conn-from-datoms datoms db-schema/schema) assets])) + [@(d/conn-from-datoms datoms (:schema db)) assets])) (defn filter-only-public-pages-and-blocks "Prepares a database assuming all pages are private unless a page has a 'public:: true'" @@ -129,4 +129,4 @@ (contains? public-pages (:db/id (:block/page (d/entity db (:e datom)))))))))))) datoms (d/datoms filtered-db :eavt) assets (get-assets db datoms)] - [@(d/conn-from-datoms datoms db-schema/schema) assets]))) + [@(d/conn-from-datoms datoms (:schema db)) assets]))) diff --git a/src/electron/electron/db.cljs b/src/electron/electron/db.cljs index bc37c1c79d..cb8966c703 100644 --- a/src/electron/electron/db.cljs +++ b/src/electron/electron/db.cljs @@ -140,7 +140,7 @@ :uuid) latest-journal-blocks (when recent-journal (query repo db (str "select * from blocks where type = 1 and page_uuid = '" recent-journal "'"))) - init-data (query repo db "select * from blocks where type in (3, 4, 5)")] + init-data (query repo db "select * from blocks where type in (3, 4, 5, 6)")] {:all-pages all-pages :all-blocks all-block-ids :journal-blocks latest-journal-blocks diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 45bde412ff..c382ceb4d3 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -20,6 +20,7 @@ [frontend.components.query.builder :as query-builder-component] [frontend.components.svg :as svg] [frontend.components.query :as query] + [frontend.components.property :as property-component] [frontend.config :as config] [frontend.context.i18n :refer [t]] [frontend.date :as date] @@ -2813,17 +2814,17 @@ (state/sub-block-selected? blocks-container-id uuid))] [:div.ls-block (cond-> - {:id block-id - :data-refs data-refs - :data-refs-self data-refs-self - :data-collapsed (and collapsed? has-child?) - :class (str uuid - (when pre-block? " pre-block") - (when (and card? (not review-cards?)) " shadow-md") - (when selected? " selected noselect") - (when (string/blank? content) " is-blank")) - :blockid (str uuid) - :haschild (str (boolean has-child?))} + {:id block-id + :data-refs data-refs + :data-refs-self data-refs-self + :data-collapsed (and collapsed? has-child?) + :class (str uuid + (when pre-block? " pre-block") + (when (and card? (not review-cards?)) " shadow-md") + (when selected? " selected noselect") + (when (string/blank? content) " is-blank")) + :blockid (str uuid) + :haschild (str (boolean has-child?))} level (assoc :level level) @@ -2878,6 +2879,8 @@ (when @*show-right-menu? (block-right-menu config block edit?))] + (when (config/db-based-graph? repo) + (property-component/properties-area block (:block/properties block))) (block-children config block children collapsed?) diff --git a/src/main/frontend/components/property.cljs b/src/main/frontend/components/property.cljs new file mode 100644 index 0000000000..8addd1d78e --- /dev/null +++ b/src/main/frontend/components/property.cljs @@ -0,0 +1,257 @@ +(ns frontend.components.property + "Block properties management." + (:require [frontend.ui :as ui] + [frontend.util :as util] + [clojure.string :as string] + [frontend.handler.property :as property-handler] + [frontend.db :as db] + [rum.core :as rum] + [frontend.state :as state] + [goog.dom :as gdom] + [frontend.search :as search] + ;; [frontend.components.search.highlight :as highlight] + [frontend.components.svg :as svg] + [frontend.modules.shortcut.core :as shortcut] + [medley.core :as medley])) + +;; (defn- add-property +;; [entity k *new-property?] +;; (when-not (string/blank? k) +;; (property-handler/add-property! (:db/id entity) k) +;; (reset! *new-property? false))) + +;; (rum/defc search-item-render +;; [search-q content] +;; [:div.font-medium "TODO-search-item-render" +;; ;; (highlight/highlight-exact-query content search-q) +;; ]) + +;; (rum/defcs property-input < +;; (shortcut/disable-all-shortcuts) +;; (rum/local nil ::q) +;; [state entity *new-property?] +;; (let [*q (::q state) +;; result (when-not (string/blank? @*q) +;; (search/property-search @*q))] +;; [:div +;; [:div.ls-property-add.grid.grid-cols-4.flex.flex-row.items-center +;; [:input#add-property.form-input.simple-input.block.col-span-1.focus:outline-none +;; {:placeholder "Property key" +;; :auto-focus true +;; :on-change (fn [e] +;; (reset! *q (util/evalue e))) +;; :on-blur (fn [_e] +;; (add-property entity @*q *new-property?)) +;; :on-key-up (fn [e] +;; (case (util/ekey e) +;; "Enter" +;; (add-property entity @*q *new-property?) + +;; "Escape" +;; (reset! *new-property? false) + +;; nil))}] +;; [:a.close {:on-mouse-down #(do +;; (reset! *q nil) +;; (reset! *new-property? false))} +;; svg/close]] +;; (ui/auto-complete +;; result +;; {:class "search-results" +;; :on-chosen #(add-property entity % *new-property?) +;; :item-render #(search-item-render @*q %)})])) + +(rum/defcs property-key < (rum/local false ::show-close?) + [state entity k page-cp property-id ref-property?] + (let [*show-close? (::show-close? state)] + [:div.relative + {:on-mouse-over (fn [_] (reset! *show-close? true)) + :on-mouse-out (fn [_] (reset! *show-close? false))} + (page-cp {} {:block/name k}) + (when (and @*show-close? (not ref-property?)) + [:div.absolute.top-0.right-0 + [:a.fade-link.fade-in.py-2.px-1 + {:title "Remove this property" + :on-click (fn [_e] + (property-handler/delete-property! entity property-id))} + (ui/icon "x")]])])) + +(rum/defcs multiple-value-item < (rum/local false ::show-close?) + [state entity property item dom-id' editor-id' {:keys [edit-fn page-cp inline-text]}] + (let [*show-close? (::show-close? state) + object? (= "object" (:type (:block/property-schema property)))] + [:div.flex.flex-1.flex-row {:on-mouse-over #(reset! *show-close? true) + :on-mouse-out #(reset! *show-close? false)} + [:div.flex.flex-1.property-value-content + {:id dom-id' + :on-click (fn [] (edit-fn editor-id' dom-id' item))} + (if object? + (page-cp {} {:block/name (util/page-name-sanity-lc item)}) + (inline-text {} :markdown (str item)))] + (when @*show-close? + [:a.close.fade-in + {:title "Delete this value" + :on-mouse-down + (fn [] + (property-handler/delete-property-value! entity (:block/uuid property) item))} + svg/close])])) + +(rum/defcs property-value < rum/reactive + [state entity property k v k' {:keys [inline-text editor-box page-cp]}] + (let [block (assoc entity :editing-property property) + dom-id (str "ls-property-" k) + editor-id (str "property-" (:db/id entity) "-" k') + editing? (state/sub [:editor/editing? editor-id]) + schema (:block/property-schema property) + edit-fn (fn [editor-id id v] + (let [v (str v) + cursor-range (util/caret-range (gdom/getElement (or id dom-id)))] + (state/set-editing! editor-id v block cursor-range) + (js/setTimeout + (fn [] + (state/set-editor-action-data! {:property (:block/original-name property) + :entity entity + :pos 0}) + (state/set-editor-action! :property-value-search)) + 50))) + multiple-values? (:multiple-values? schema) + type (:type schema)] + (cond + multiple-values? + (let [v' (if (coll? v) v (when v [v])) + v' (if (seq v') v' [""]) + editor-id' (str editor-id (count v')) + new-editing? (state/sub [:editor/editing? editor-id'])] + [:div.flex.flex-1.flex-col + [:div.flex.flex-1.flex-col + (for [[idx item] (medley/indexed v')] + (let [dom-id' (str dom-id "-" idx) + editor-id' (str editor-id idx) + editing? (state/sub [:editor/editing? editor-id'])] + (if editing? + (editor-box {:format :markdown + :block block} editor-id' {}) + (multiple-value-item entity property item dom-id' editor-id' {:page-cp page-cp + :edit-fn edit-fn + :inline-text inline-text})))) + + (let [fv (first v')] + (when (and (not new-editing?) + fv + (or (and (string? fv) (not (string/blank? fv))) + (and (not (string? fv)) (some? fv)))) + [:div.rounded-sm.ml-1 + {:on-click (fn [] + (edit-fn (str editor-id (count v')) nil ""))} + [:div.flex.flex-row + [:div.block {:style {:height 20 + :width 20}} + [:a.add-button-link.block {:title "Add another value" + :style {:margin-left -4}} + (ui/icon "circle-plus")]]]]))] + (when new-editing? + (editor-box {:format :markdown + :block block} editor-id' {}))]) + + editing? + (editor-box {:format :markdown + :block block} editor-id {}) + + :else + [:div.flex.flex-1.property-value-content + {:id dom-id + :on-click (fn [] + (edit-fn editor-id nil v))} + (cond + (and (= type "date") (string/blank? v)) + [:div "TBD (date icon)"] + + :else + (when-not (string/blank? (str v)) + (inline-text {} :markdown (str v))))]))) + +;; (rum/defcs properties-area < +;; (rum/local false ::new-property?) +;; rum/reactive +;; [state entity properties refs-properties {:keys [page-cp inline-text]}] +;; (let [*new-property? (::new-property? state) +;; editor-box (state/get-component :editor/box) +;; ref-keys (set (keys refs-properties)) +;; page? (:block/name entity)] +;; [:div.ls-properties-area +;; (when (seq properties) +;; [:div +;; (for [[k v] properties] +;; (when-let [property (db/pull [:block/uuid k])] +;; (when-let [k' (:block/original-name property)] +;; (let [ref-property? (contains? ref-keys k)] +;; [:div.grid.grid-cols-4.gap-1 +;; [:div.property-key.col-span-1 +;; (property-key entity k' page-cp k ref-property?)] + +;; [:div.col-span-3 +;; (property-value entity property k v k' {:page-cp page-cp +;; :inline-text inline-text +;; :editor-box editor-box})]]))))]) + +;; (when page? +;; (if @*new-property? +;; (property-input entity *new-property?) +;; [:div.flex-1.flex-col.rounded-sm +;; {:on-click (fn [] +;; (reset! *new-property? true))} +;; [:div.flex.flex-row +;; [:div.block {:style {:height 20 +;; :width 20}} +;; [:a.add-button-link.block {:title "Add another property" +;; :style {:margin-left -4}} +;; (ui/icon "circle-plus")]]]]))])) + +;; (rum/defc composed-properties < rum/reactive +;; [entity refs block-components-m] +;; (let [namespaces (map :block/namespace (distinct refs)) +;; refs-properties (map +;; (fn [ref] +;; (:block/properties +;; (db/pull (:db/id ref)))) +;; (concat namespaces refs)) +;; property-maps (concat refs-properties +;; [(:block/properties entity)]) +;; properties (apply merge property-maps) +;; refs-properties' (apply merge refs-properties)] +;; (properties-area entity properties refs-properties' block-components-m))) + +(rum/defcs properties-area < + (rum/local nil ::new-property) + rum/reactive + [state block properties] + (let [*new-property (::new-property state) + repo (state/get-current-repo)] + [:div.ls-properties-area.pl-6 + (when (seq properties) + (prn :properties properties) + [:div + (for [[prop-uuid-or-built-in-prop v] properties] + (if (uuid? prop-uuid-or-built-in-prop) + (when-let [property-class (db/pull [:block/uuid prop-uuid-or-built-in-prop])] + [:div + [:a.mr-2 (:property/name property-class)] + [:input {:value v}]]) + ;; builtin + [:div + [:a.mr-2 (str prop-uuid-or-built-in-prop)] + [:input {:value v}]]))]) + (if (nil? @*new-property) + [:a {:title "Add another value" + :on-click (fn [] (reset! *new-property {}))} + (ui/icon "circle-plus")] + + [:div + [:input.block-properties {:on-change #(swap! *new-property assoc :k (util/evalue %))}] + [:input.block-properties {:on-change #(swap! *new-property assoc :v (util/evalue %))}] + [:a {:on-click (fn [] + (when (and (:k @*new-property) (:k @*new-property)) + (prn :*new-property @*new-property) + (property-handler/add-property! repo block (:k @*new-property) (:v @*new-property)) + (reset! *new-property nil)))} + "Save"]])])) diff --git a/src/main/frontend/db/restore.cljs b/src/main/frontend/db/restore.cljs index 18c66444be..9f8ba9fa3b 100644 --- a/src/main/frontend/db/restore.cljs +++ b/src/main/frontend/db/restore.cljs @@ -101,7 +101,7 @@ (let [all-datoms (persistent! datoms) new-db (util/profile (str "DB init! " (count all-datoms) " datoms") - (d/init-db all-datoms db-schema/schema))] + (d/init-db all-datoms db-schema/schema-for-db-based-graph))] (reset! conn new-db) @@ -174,7 +174,7 @@ all-eav-coll) db-name (db-conn/datascript-db repo) db-conn (util/profile :restore-graph-from-sqlite!-init-db - (d/conn-from-datoms datoms db-schema/schema)) + (d/conn-from-datoms datoms db-schema/schema-for-db-based-graph)) _ (swap! db-conn/conns assoc db-name db-conn) end-time (t/now)] (println :restore-graph-from-sqlite!-prepare (t/in-millis (t/interval start-time end-time)) "ms" diff --git a/src/main/frontend/handler/property.cljs b/src/main/frontend/handler/property.cljs new file mode 100644 index 0000000000..4428c5dfc2 --- /dev/null +++ b/src/main/frontend/handler/property.cljs @@ -0,0 +1,152 @@ +(ns frontend.handler.property + "Block properties handler." + (:require [frontend.state :as state] + [frontend.db :as db] + [frontend.util :as util] + [frontend.format.block :as block] + [clojure.string :as string] + [logseq.graph-parser.mldoc :as gp-mldoc] + [logseq.graph-parser.block :as gp-block] + [logseq.graph-parser.util :as gp-util] + [logseq.graph-parser.util.page-ref :as page-ref] + [frontend.modules.outliner.core :as outliner-core] + [frontend.modules.outliner.transaction :as outliner-tx] + [frontend.util.property-edit :as property-edit])) + +(defn add-property! + [repo block k v] + (let [tx-data (property-edit/insert-property-when-db-based repo block k v)] + (db/transact! repo tx-data))) + +(defn remove-property! + [repo block k] + (let [tx-data (property-edit/remove-property-when-db-based block k)] + (db/transact! repo tx-data))) + + +;; (defn add-property! +;; [block-db-id key] +;; (let [block (db/pull block-db-id) +;; key (string/trim key) +;; key-name (util/page-name-sanity-lc key) +;; property (db/entity [:block/name key-name])] +;; (when-not (or +;; (= (:block/name block) key-name) +;; (and property +;; (or +;; (= (:block/type property) "tag") +;; (= (:db/id property) (:db/id block))))) +;; (let [property-uuid (db/new-block-id)] +;; (db/transact! (state/get-current-repo) +;; [ +;; ;; property +;; {:block/uuid property-uuid +;; :block/type "property" +;; :block/property-schema {:type "any"} +;; :block/original-name key +;; :block/name key-name} + +;; {:block/uuid (:block/uuid block) +;; :block/properties (assoc (:block/properties block) +;; property-uuid "")}]))))) + +(defn- extract-refs + [entity properties] + (let [property-values (->> + properties + (map (fn [[k v]] + (let [schema (:block/property-schema (db/pull [:block/uuid k])) + object? (= (:type schema) "object") + f (if object? page-ref/->page-ref identity)] + (->> (if (coll? v) + v + [v]) + (map f))))) + (apply concat) + (filter string?)) + block-text (string/join " " + (cons + (:block/content entity) + property-values)) + ast-refs (gp-mldoc/get-references block-text (gp-mldoc/default-config :markdown)) + refs (map #(or (gp-block/get-page-reference % #{}) + (gp-block/get-block-reference %)) ast-refs) + refs' (->> refs + (remove string/blank?) + distinct)] + (map #(if (util/uuid-string? %) + [:block/uuid (uuid %)] + (block/page-name->map % true)) refs'))) + +(defn delete-property! + [entity property-id] + (when (and entity (uuid? property-id)) + (let [properties' (dissoc (:block/properties entity) property-id) + refs (extract-refs entity properties')] + (outliner-tx/transact! + {:outliner-op :save-block} + (outliner-core/save-block! + {:block/uuid (:block/uuid entity) + :block/properties properties' + :block/refs refs}))))) + +(defn validate + "Check whether the `value` validate against the `schema`." + [schema value] + (if (string/blank? value) + [true value] + (case (:type schema) + "any" [true value] + "number" (if-let [n (parse-double value)] + (let [[min-n max-n] [(:min schema) (:max schema)] + min-result (if min-n (>= n min-n) true) + max-result (if max-n (<= n max-n) true)] + (cond + (and min-result max-result) + [true n] + + (false? min-result) + [false (str "the min value is " min-n)] + + (false? max-result) + [false (str "the max value is " max-n)] + + :else + n)) + [false "invalid number"]) + "date" (if-let [result (js/Date. value)] + (if (not= (str result) "Invalid Date") + [true value] + [false "invalid date"]) + [false "invalid date"]) + "url" (if (gp-util/url? value) + [true value] + [false "invalid URL"]) + "object" (let [page-name (or + (try + (page-ref/get-page-name value) + (catch :default _)) + value)] + [true page-name])))) + +(defn delete-property-value! + "Delete value if a property has multiple values" + [entity property-id property-value] + (when (and entity (uuid? property-id)) + (when (not= property-id (:block/uuid entity)) + (when-let [property (db/pull [:block/uuid property-id])] + (let [schema (:block/property-schema property) + [success? property-value-or-error] (validate schema property-value) + multiple-values? (:multiple-values? schema)] + (when (and multiple-values? success?) + (let [properties (:block/properties entity) + properties' (update properties property-id disj property-value-or-error) + refs (extract-refs entity properties')] + (outliner-tx/transact! + {:outliner-op :save-block} + (outliner-core/save-block! + {:block/uuid (:block/uuid entity) + :block/properties properties' + :block/refs refs})))) + (state/clear-editor-action!) + (state/clear-edit!)))))) diff --git a/src/main/frontend/publishing.cljs b/src/main/frontend/publishing.cljs index 22f22ae0d9..799ec92dfc 100644 --- a/src/main/frontend/publishing.cljs +++ b/src/main/frontend/publishing.cljs @@ -52,6 +52,8 @@ (state/set-current-repo! "local") (when-let [data js/window.logseq_db] (let [data (unescape-html data) + ;; FIXME: how to decide which schema to use here? + ;; db-schema/schema or db-schema/schema-for-db-based-graph? db-conn (d/create-conn db-schema/schema) _ (swap! db/conns assoc "logseq-db/local" db-conn) db (db/string->db data)] diff --git a/src/main/frontend/util/property_edit.cljs b/src/main/frontend/util/property_edit.cljs index d722d67fe7..b0e6e359df 100644 --- a/src/main/frontend/util/property_edit.cljs +++ b/src/main/frontend/util/property_edit.cljs @@ -1,7 +1,8 @@ (ns frontend.util.property-edit "Property related fns, both file-based and db-based version need to be considered." (:require [frontend.util.property :as property] - [frontend.config :as config])) + [frontend.config :as config] + [frontend.db :as db])) ;; Why need these XXX-when-file-based fns? @@ -64,3 +65,26 @@ (def property-key-exist?-when-file-based property/property-key-exist?) (def goto-properties-end-when-file-based property/goto-properties-end) (def front-matter?-when-file-based property/front-matter?) + + +(defn insert-property-when-db-based + "return tx-data" + [repo block k-name v] + (let [property-class (db/pull repo '[*] [:property/name k-name]) + property-class-uuid (or (:block/uuid property-class) (random-uuid))] + (cond-> [] + (nil? property-class) (conj {:property/schema {} + :property/name k-name + :block/uuid property-class-uuid + :block/type "property"}) + true (conj {:block/uuid (:block/uuid block) + :block/properties (assoc (:block/properties block) (str property-class-uuid) v)})))) + +(defn remove-property-when-db-based + "return tx-data" + [block k-uuid-or-builtin-k-name] + {:pre (string? k-uuid-or-builtin-k-name)} + (let [origin-properties (:block/properties block)] + (assert (contains? (set (keys origin-properties)) k-uuid-or-builtin-k-name)) + [{:block/uuid (:block/uuid block) + :block/properties (dissoc origin-properties k-uuid-or-builtin-k-name)}]))