From e0028a39fa7256ecea188251bc791ae0c2f17432 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Mon, 3 Jul 2023 12:02:19 +0800 Subject: [PATCH] wip: property UX --- src/main/frontend/components/block.cljs | 32 +- src/main/frontend/components/editor.cljs | 3 +- src/main/frontend/components/property.cljs | 338 ++++++++++++++---- src/main/frontend/components/property.css | 34 ++ src/main/frontend/components/search.cljs | 45 +-- .../frontend/components/search/highlight.cljs | 43 +++ src/main/frontend/db/model.cljs | 14 + src/main/frontend/handler/editor.cljs | 16 +- src/main/frontend/handler/property.cljs | 146 ++++---- src/main/frontend/state.cljs | 3 +- 10 files changed, 468 insertions(+), 206 deletions(-) create mode 100644 src/main/frontend/components/property.css create mode 100644 src/main/frontend/components/search/highlight.cljs diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 2908a2b179..67a6c6c25c 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -2269,7 +2269,8 @@ (rum/defc block-content < rum/reactive [config {:block/keys [uuid content children properties scheduled deadline format pre-block?] :as block} edit-input-id block-id slide? selected?] - (let [content (property/remove-built-in-properties format content) + (let [repo (state/get-current-repo) + content (property/remove-built-in-properties format content) {:block/keys [title body] :as block} (if (:block/title block) block (merge block (block/parse-title-and-body uuid format pre-block? content))) collapsed? (util/collapsed? block) @@ -2335,15 +2336,17 @@ (when-let [scheduled-ast (block-handler/get-scheduled-ast block)] (timestamp-cp block "SCHEDULED" scheduled-ast))) - (when-let [invalid-properties (:block/invalid-properties block)] - (invalid-properties-cp invalid-properties)) + (when-not (config/db-based-graph? repo) + (when-let [invalid-properties (:block/invalid-properties block)] + (invalid-properties-cp invalid-properties))) (when (and (seq properties) (let [hidden? (property-edit/properties-hidden? properties)] (not hidden?)) (not (and block-ref? (or (seq title) (seq body)))) (not (:slide? config)) - (not= block-type :whiteboard-shape)) + (not= block-type :whiteboard-shape) + (not (config/db-based-graph? repo))) (properties-cp config block)) (block-content-inner config block body plugin-slotted? collapsed? block-ref-with-title?) @@ -2843,20 +2846,21 @@ (if whiteboard-block? (block-reference {} (str uuid) nil) ;; Not embed self - (let [block (merge block (block/parse-title-and-body uuid (:block/format block) pre-block? content)) - hide-block-refs-count? (and (:embed? config) - (= (:block/uuid block) (:embed-id config)))] - (block-content-or-editor config block edit-input-id block-id edit? hide-block-refs-count? selected?))) + [:div.flex.flex-col.w-full + (let [block (merge block (block/parse-title-and-body uuid (:block/format block) pre-block? content)) + hide-block-refs-count? (and (:embed? config) + (= (:block/uuid block) (:embed-id config)))] + (block-content-or-editor config block edit-input-id block-id edit? hide-block-refs-count? selected?)) + (when (config/db-based-graph? repo) + (property-component/properties-area block + (:block/properties block) + (:block/properties-text-values block) + edit-input-id {:inline-text inline-text + :editor-box (get config :editor-box)}))]) (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/properties-text-values block) - edit-input-id)) - (when-not (:hide-children? config) (let [children (db/sort-by-left (:block/_parent block) block)] (block-children config block children collapsed?))) diff --git a/src/main/frontend/components/editor.cljs b/src/main/frontend/components/editor.cljs index 572fb2af5a..b1a4d19d86 100644 --- a/src/main/frontend/components/editor.cljs +++ b/src/main/frontend/components/editor.cljs @@ -5,6 +5,7 @@ [frontend.components.block :as block] [frontend.components.datetime :as datetime-comp] [frontend.components.search :as search] + [frontend.components.search.highlight :as highlight] [frontend.components.svg :as svg] [frontend.context.i18n :refer [t]] [frontend.db :as db] @@ -157,7 +158,7 @@ (when (db-model/whiteboard-page? page-name) [:span.mr-1 (ui/icon "whiteboard" {:extension? true})]) [:div.flex.space-x-1 [:div (when-not (db/page-exists? page-name) (t :new-page))] - (search/highlight-exact-query page-name q)]] + (highlight/highlight-exact-query page-name q)]] :open? chosen? :manual? true :fixed-position? true diff --git a/src/main/frontend/components/property.cljs b/src/main/frontend/components/property.cljs index e06b6007af..8f2fb9f1b4 100644 --- a/src/main/frontend/components/property.cljs +++ b/src/main/frontend/components/property.cljs @@ -5,23 +5,29 @@ [frontend.handler.property :as property-handler] [frontend.handler.ui :as ui-handler] [frontend.db :as db] + [frontend.config :as config] [rum.core :as rum] [frontend.state :as state] [frontend.mixins :as mixins] [clojure.edn :as edn] - [clojure.string :as string])) + [clojure.string :as string] + [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])) (rum/defcs property-config < rum/static (rum/local nil ::property-name) (rum/local nil ::property-schema) {:will-mount (fn [state] - (let [[repo property-uuid] (:rum/args state) - property (db/pull repo '[*] [:block/uuid property-uuid])] + (let [[repo property] (:rum/args state)] (reset! (::property-name state) (:block/name property)) (reset! (::property-schema state) (:block/schema property)) state))} - [state repo property-uuid] + [state repo property] (let [*property-name (::property-name state) *property-schema (::property-schema state)] [:div.property-configure @@ -29,40 +35,117 @@ [:div.grid.gap-2.p-1 [:div.grid.grid-cols-4.gap-1.items-center.leading-8 - [:label.cols-1 "Name:"] - [:input.form-input - {:on-change #(reset! *property-name (util/evalue %)) - :value @*property-name}]] + [:label.cols-1 "Name:"] + [:input.form-input + {:on-change #(reset! *property-name (util/evalue %)) + :value @*property-name}]] - [:div.grid.grid-cols-4.gap-1.leading-8 - [:label.cols-1 "Schema type:"] - (let [schema-types (->> (keys property-handler/builtin-schema-types) - (map (comp string/capitalize name)) - (map (fn [type] - {:label type - :value type - :selected (= (keyword (string/lower-case type)) - (:type @*property-schema))})))] - (ui/select schema-types - (fn [_e v] - (let [type (keyword (string/lower-case v))] - (swap! *property-schema assoc :type type)))))] + [:div.grid.grid-cols-4.gap-1.leading-8 + [:label.cols-1 "Schema type:"] + (let [schema-types (->> (keys property-handler/builtin-schema-types) + (map (comp string/capitalize name)) + (map (fn [type] + {:label type + :value type + :selected (= (keyword (string/lower-case type)) + (:type @*property-schema))})))] + (ui/select schema-types + (fn [_e v] + (let [type (keyword (string/lower-case v))] + (swap! *property-schema assoc :type type)))))] - [:div.grid.grid-cols-4.gap-1.items-center.leading-8 - [:label.cols-1 "Multiple values:"] - (ui/checkbox {:checked (= :many (:cardinality @*property-schema)) - :on-change (fn [v] - (swap! *property-schema assoc :cardinality (if (= "on" (util/evalue v)) :many :one)))})] + [:div.grid.grid-cols-4.gap-1.items-center.leading-8 + [:label.cols-1 "Multiple values:"] + (let [many? (boolean (= :many (:cardinality @*property-schema)))] + (ui/checkbox {:checked many? + :on-change (fn [] + (swap! *property-schema assoc :cardinality (if many? :one :many)))}))] - [:div - (ui/button - "Save" - :on-click (fn [] - (property-handler/update-property! - repo property-uuid - {:property-name @*property-name - :property-schema @*property-schema}) - (state/close-modal!)))]]])) + [:div + (ui/button + "Save" + :on-click (fn [] + (property-handler/update-property! + repo (:block/uuid property) + {:property-name @*property-name + :property-schema @*property-schema}) + (state/close-modal!)))] + + (when config/dev? + [:div {:style {:max-width 900}} + [:hr] + [:p "Debug data:"] + [:code + (str property)]])]])) + +(rum/defc search-item-render + [search-q content] + [:div.font-medium + (highlight/highlight-exact-query content search-q)]) + +(defn- exit-edit-property + [*property-key *property-value] + (reset! *property-key nil) + (reset! *property-value nil) + (property-handler/set-editing-new-property! nil)) + +(defn- add-property! + [block *property-key *property-value] + (let [repo (state/get-current-repo)] + (when (and @*property-key @*property-value) + (property-handler/add-property! repo block @*property-key @*property-value)) + (exit-edit-property *property-key *property-value))) + +(rum/defcs property-key-input < rum/reactive + (rum/local true ::search?) + shortcut/disable-all-shortcuts + [state entity *property-key *property-value] + (let [*search? (::search? state) + result (when-not (string/blank? @*property-key) + (search/property-search @*property-key))] + [:div + [:div.ls-property-add.grid.grid-cols-4.gap-1.flex.flex-row.items-center + [:input#add-property.form-input.simple-input.block.col-span-1.focus:outline-none + {:placeholder "Property key" + :value (rum/react *property-key) + :auto-focus true + :on-change (fn [e] + (reset! *property-key (util/evalue e)) + (reset! *search? true)) + :on-key-down (fn [e] + (case (util/ekey e) + "Escape" + (exit-edit-property *property-key *property-value) + + "Enter" + (do + (reset! *search? false) + (.focus (js/document.getElementById "add-property-value"))) + + nil))}] + + [:input#add-property-value.block-properties + {:on-change #(reset! *property-value (util/evalue %)) + :on-key-down (fn [e] + (case (util/ekey e) + "Enter" + (do + (add-property! entity *property-key *property-value) + (reset! *search? false)) + + nil))}] + + [:a.close {:on-mouse-down #(exit-edit-property *property-key *property-value)} + svg/close]] + (when @*search? + (ui/auto-complete + result + {:class "search-results" + :on-chosen (fn [chosen] + (reset! *property-key chosen) + (reset! *search? false) + (.focus (js/document.getElementById "add-property-value"))) + :item-render #(search-item-render @*property-key %)}))])) (rum/defcs new-property < rum/reactive (rum/local nil ::property-key) @@ -74,22 +157,13 @@ :on-hide (fn [] (property-handler/set-editing-new-property! nil)) :node (js/document.getElementById "edit-new-property")))) - [state repo block edit-input-id properties] - (let [new-property? (= edit-input-id (state/sub :ui/new-property-input-id)) - *property-key (::property-key state) + [state repo block edit-input-id properties new-property?] + (let [*property-key (::property-key state) *property-value (::property-value state)] (cond new-property? [:div#edit-new-property - [:input.block-properties {:on-change #(reset! *property-key (util/evalue %))}] - [:input.block-properties {:on-change #(reset! *property-value (util/evalue %))}] - [:a {:on-click (fn [] - (when (and @*property-key @*property-value) - (property-handler/add-property! repo block @*property-key @*property-value)) - (reset! *property-key nil) - (reset! *property-value nil) - (property-handler/set-editing-new-property! nil))} - "Save"]] + (property-key-input block *property-key *property-value)] (seq properties) [:a {:title "Add another value" @@ -99,26 +173,150 @@ (reset! *property-value nil))} (ui/icon "circle-plus")]))) -(rum/defc properties-area < rum/static - [block properties properties-text-values edit-input-id] - (let [repo (state/get-current-repo)] - [:div.ls-properties-area.pl-6 - (when (seq properties) - [:div - (for [[prop-uuid-or-built-in-prop v] properties] - (if (uuid? prop-uuid-or-built-in-prop) - (when-let [property (db/pull [:block/uuid prop-uuid-or-built-in-prop])] - [:div - [:a.mr-2 - {:on-click (fn [] (state/set-modal! #(property-config repo prop-uuid-or-built-in-prop)))} - (:block/name property)] - [:span (or (get properties-text-values prop-uuid-or-built-in-prop) (str v))] - [:a.ml-8 {:on-click - (fn [] - (property-handler/remove-property! repo block prop-uuid-or-built-in-prop))} - "DEL"]]) - ;; builtin - [:div - [:a.mr-2 (str prop-uuid-or-built-in-prop)] - [:span v]]))]) - (new-property repo block edit-input-id properties)])) +(rum/defcs property-key < (rum/local false ::show-close?) + [state block property] + (let [repo (state/get-current-repo) + *show-close? (::show-close? state)] + [:div.relative + {:on-mouse-over (fn [_] (reset! *show-close? true)) + :on-mouse-out (fn [_] (reset! *show-close? false))} + [:a.mr-2 + {:on-click (fn [] (state/set-modal! #(property-config repo property)))} + (:block/name property)] + (when @*show-close? + [: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/remove-property! repo block (:block/uuid property)))} + (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/schema property))) + block (when object? (db/pull [:block/uuid item]))] + [: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 block + ;; TODO: page/block + (str block) + (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! (state/get-current-repo) + entity + (:block/uuid property) + item))} + svg/close])])) + +;; (add-property! block *property-key *property-value) +(rum/defcs property-value < rum/reactive + [state block property {:keys [inline-text editor-box page-cp]}] + (let [k (:block/uuid property) + v (get (:block/properties-text-values block) + k + (get (:block/properties block) k)) + dom-id (str "ls-property-" k) + editor-id (str "property-" (:db/id block) "-" k) + editing? (state/sub [:editor/editing? editor-id]) + schema (:block/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! {:block block + :property property + :pos 0}) + (state/set-editor-action! :property-value-search) + (state/set-state! :ui/editing-property property)) + 50))) + multiple-values? (= :many (:cardinality 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 block 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/defc properties-area < rum/reactive + [block properties properties-text-values edit-input-id block-components-m] + (let [repo (state/get-current-repo) + new-property? (= edit-input-id (state/sub :ui/new-property-input-id))] + (when (or (seq properties) new-property?) + [:div.ls-properties-area + (when (seq properties) + [:div + (for [[prop-uuid-or-built-in-prop v] properties] + (if (uuid? prop-uuid-or-built-in-prop) + (when-let [property (db/pull [:block/uuid prop-uuid-or-built-in-prop])] + [:div.grid.grid-cols-4.gap-1 + [:div.property-key.col-span-1 + (property-key block property)] + [:div.property-value.col-span-3 + (property-value block property block-components-m)]]) + ;; TODO: built in properties should have UUID and corresponding schema + ;; builtin + [:div + [:a.mr-2 (str prop-uuid-or-built-in-prop)] + [:span v]]))]) + (new-property repo block edit-input-id properties new-property?)]))) diff --git a/src/main/frontend/components/property.css b/src/main/frontend/components/property.css new file mode 100644 index 0000000000..45cd7075fe --- /dev/null +++ b/src/main/frontend/components/property.css @@ -0,0 +1,34 @@ +.property-value-content { + @apply px-1 rounded-sm; + cursor: text; + min-height: 24px; +} + +.property-value-content:hover { + background: var(--ls-secondary-background-color); +} + +.ls-properties-area { + .add-button-link { + opacity: 0.5; + } + + tr:nth-child(even), tr:nth-child(odd) { + background: none; + } + + .editor-inner { + @apply px-1; + } +} + +input.simple-input { + @apply px-1; + border-radius: 0; + border: none; + border-bottom: 1px solid; +} + +input.simple-input:focus { + box-shadow: none; +} diff --git a/src/main/frontend/components/search.cljs b/src/main/frontend/components/search.cljs index 6a84718459..79e24ec5f4 100644 --- a/src/main/frontend/components/search.cljs +++ b/src/main/frontend/components/search.cljs @@ -4,6 +4,7 @@ [frontend.util :as util] [frontend.components.block :as block] [frontend.components.svg :as svg] + [frontend.components.search.highlight :as highlight] [frontend.handler.route :as route-handler] [frontend.handler.editor :as editor-handler] [frontend.handler.page :as page-handler] @@ -25,44 +26,6 @@ [frontend.modules.shortcut.core :as shortcut] [frontend.util.text :as text-util])) -(defn highlight-exact-query - [content q] - (if (or (string/blank? content) (string/blank? q)) - content - (when (and content q) - (let [q-words (string/split q #" ") - lc-content (util/search-normalize content (state/enable-search-remove-accents?)) - lc-q (util/search-normalize q (state/enable-search-remove-accents?))] - (if (and (string/includes? lc-content lc-q) - (not (util/safe-re-find #" " q))) - (let [i (string/index-of lc-content lc-q) - [before after] [(subs content 0 i) (subs content (+ i (count q)))]] - [:div - (when-not (string/blank? before) - [:span before]) - [:mark.p-0.rounded-none (subs content i (+ i (count q)))] - (when-not (string/blank? after) - [:span after])]) - (let [elements (loop [words q-words - content content - result []] - (if (and (seq words) content) - (let [word (first words) - lc-word (util/search-normalize word (state/enable-search-remove-accents?)) - lc-content (util/search-normalize content (state/enable-search-remove-accents?))] - (if-let [i (string/index-of lc-content lc-word)] - (recur (rest words) - (subs content (+ i (count word))) - (vec - (concat result - [[:span (subs content 0 i)] - [:mark.p-0.rounded-none (subs content i (+ i (count word)))]]))) - (recur nil - content - result))) - (conj result [:span content])))] - [:p {:class "m-0"} elements])))))) - (defn highlight-page-content-query "Return hiccup of highlighted page content FTS result" [content q] @@ -113,7 +76,7 @@ (clojure.core/uuid uuid) {:indent? false})]) [:div {:class "font-medium" :key "content"} - (highlight-exact-query content q)]])) + (highlight/highlight-exact-query content q)]])) (defonce search-timeout (atom nil)) @@ -282,12 +245,12 @@ (search-result-item {:name (if (model/whiteboard-page? data) "whiteboard" "page") :extension? true :title (t (if (model/whiteboard-page? data) :search-item/whiteboard :search-item/page))} - (highlight-exact-query data search-q))] + (highlight/highlight-exact-query data search-q))] :file (search-result-item {:name "file" :title (t :search-item/file)} - (highlight-exact-query data search-q)) + (highlight/highlight-exact-query data search-q)) :block (let [{:block/keys [page uuid content]} data ;; content here is normalized diff --git a/src/main/frontend/components/search/highlight.cljs b/src/main/frontend/components/search/highlight.cljs new file mode 100644 index 0000000000..c5cc5a5c6d --- /dev/null +++ b/src/main/frontend/components/search/highlight.cljs @@ -0,0 +1,43 @@ +(ns frontend.components.search.highlight + "Search highlight component" + (:require [frontend.util :as util] + [frontend.state :as state] + [clojure.string :as string])) + +(defn highlight-exact-query + [content q] + (if (or (string/blank? content) (string/blank? q)) + content + (when (and content q) + (let [q-words (string/split q #" ") + lc-content (util/search-normalize content (state/enable-search-remove-accents?)) + lc-q (util/search-normalize q (state/enable-search-remove-accents?))] + (if (and (string/includes? lc-content lc-q) + (not (util/safe-re-find #" " q))) + (let [i (string/index-of lc-content lc-q) + [before after] [(subs content 0 i) (subs content (+ i (count q)))]] + [:div + (when-not (string/blank? before) + [:span before]) + [:mark.p-0.rounded-none (subs content i (+ i (count q)))] + (when-not (string/blank? after) + [:span after])]) + (let [elements (loop [words q-words + content content + result []] + (if (and (seq words) content) + (let [word (first words) + lc-word (util/search-normalize word (state/enable-search-remove-accents?)) + lc-content (util/search-normalize content (state/enable-search-remove-accents?))] + (if-let [i (string/index-of lc-content lc-word)] + (recur (rest words) + (subs content (+ i (count word))) + (vec + (concat result + [[:span (subs content 0 i)] + [:mark.p-0.rounded-none (subs content i (+ i (count word)))]]))) + (recur nil + content + result))) + (conj result [:span content])))] + [:p {:class "m-0"} elements])))))) diff --git a/src/main/frontend/db/model.cljs b/src/main/frontend/db/model.cljs index b441d1cfa1..fd89ff0c6c 100644 --- a/src/main/frontend/db/model.cljs +++ b/src/main/frontend/db/model.cljs @@ -1213,6 +1213,20 @@ independent of format as format specific heading characters are stripped" (distinct) (sort)))) +(defn get-block-property-values + "Get blocks which have this property." + [property-uuid] + (->> + (d/q + '[:find ?b ?v + :in $ ?property-uuid + :where + [?b :block/properties ?p] + [(get ?p ?property-uuid) ?v] + [(some? ?v)]] + (conn/get-db) + property-uuid))) + (defn get-template-by-name [name] (when (string? name) diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index fd953803c5..082fa94495 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -27,6 +27,7 @@ [frontend.handler.route :as route-handler] [frontend.handler.db-based.editor :as db-editor-handler] [frontend.handler.file-based.editor :as file-editor-handler] + [frontend.handler.property :as property-handler] [frontend.mobile.util :as mobile-util] [frontend.modules.outliner.core :as outliner-core] [frontend.modules.outliner.transaction :as outliner-tx] @@ -1161,10 +1162,17 @@ (defn save-block-aux! [block value opts] - (let [value (string/trim value)] - ;; FIXME: somehow frontend.components.editor's will-unmount event will loop forever - ;; maybe we shouldn't save the block/file in "will-unmount" event? - (save-block-if-changed! block value opts))) + (let [entity (db/entity [:block/uuid (:block/uuid block)]) + editing-property (:ui/editing-property @state/state)] + (when (:db/id entity) + (let [value (string/trim value)] + (if editing-property + (property-handler/add-property! (state/get-current-repo) entity + (:block/name editing-property) + value) + ;; FIXME: somehow frontend.components.editor's will-unmount event will loop forever + ;; maybe we shouldn't save the block/file in "will-unmount" event? + (save-block-if-changed! block value opts)))))) (defn save-block! ([repo block-or-uuid content] diff --git a/src/main/frontend/handler/property.cljs b/src/main/frontend/handler/property.cljs index 8a58e982ee..fbf1bbd910 100644 --- a/src/main/frontend/handler/property.cljs +++ b/src/main/frontend/handler/property.cljs @@ -4,6 +4,7 @@ [clojure.string :as string] [clojure.set :as set] [frontend.db :as db] + [frontend.db.model :as model] [frontend.format.block :as block] [frontend.handler.notification :as notification] [frontend.modules.outliner.core :as outliner-core] @@ -46,7 +47,10 @@ refs' (->> refs (remove string/blank?) distinct)] - refs')) + (-> (map #(if (util/uuid-string? %) + {:block/uuid (uuid %)} + (block/page-name->map % true)) refs') + set))) (defn- infer-schema-from-input-string [v-str] @@ -84,41 +88,48 @@ [repo block k-name v] (let [property (db/pull repo '[*] [:block/name k-name]) property-uuid (or (:block/uuid property) (random-uuid)) - existing-schema (:block/schema property) - property-type (:type existing-schema) + {:keys [type cardinality]} (:block/schema property) + multiple-values? (= cardinality :many) infer-schema (infer-schema-from-input-string v) - property-type (or property-type infer-schema :default) - schema (get builtin-schema-types property-type)] + property-type (or type infer-schema :default) + schema (get builtin-schema-types property-type) + properties (:block/properties block) + value (get properties property-uuid)] (when-let [v* (try (convert-property-input-string property-type v) (catch :default e (notification/show! (str e) :error false) nil))] - (if-let [msg (me/humanize (mu/explain-data schema v*))] - (notification/show! msg :error false) - (do (when (nil? property) ;if property not exists yet + (when-not (contains? (if (set? value) value #{value}) v*) + (if-let [msg (me/humanize (mu/explain-data schema v*))] + (notification/show! msg :error false) + (do + (when (nil? property) ;if property not exists yet (db/transact! repo [(outliner-core/block-with-timestamps {:block/schema {:type property-type} :block/original-name k-name :block/name (util/page-name-sanity-lc k-name) :block/uuid property-uuid :block/type "property"})])) - (let [block-properties (assoc (:block/properties block) - property-uuid - (if (= property-type :default) - (let [refs (extract-page-refs-from-prop-str-value v*)] - (if (seq refs) (set refs) v*)) - v*)) + (let [refs (when (= property-type :default) (extract-page-refs-from-prop-str-value v*)) + refs' (when (seq refs) + (concat (:block/refs (db/pull [:block/uuid (:block/uuid block)])) + refs)) + v' (if (= property-type :default) + (if (seq refs) refs v*) + v*) + new-value (if multiple-values? (vec (distinct (conj value v'))) v') + block-properties (assoc properties property-uuid new-value) block-properties-text-values - (if (= property-type :default) + (if (and (not multiple-values?) (= property-type :default)) (assoc (:block/properties-text-values block) property-uuid v*) (dissoc (:block/properties-text-values block) property-uuid))] - (outliner-tx/transact! - {:outliner-op :save-block} - (outliner-core/save-block! - {:block/uuid (:block/uuid block) + ;; TODO: fix block/properties-order + (db/transact! repo + [{:block/uuid (:block/uuid block) :block/properties block-properties - :block/properties-text-values block-properties-text-values})))))))) + :block/properties-text-values block-properties-text-values + :block/refs refs'}])))))))) (defn remove-property! [repo block k-uuid-or-builtin-k-name] @@ -131,65 +142,50 @@ :block/properties (dissoc origin-properties k-uuid-or-builtin-k-name) :block/properties-text-values (dissoc (:block/properties-text-values block) k-uuid-or-builtin-k-name)}]))) +(defn- fix-cardinality-many-values! + [property-uuid] + (let [ev (->> (model/get-block-property-values property-uuid) + (remove (fn [[_ v]] (coll? v)))) + tx-data (map (fn [[e v]] + (let [entity (db/entity e) + properties (:block/properties entity)] + {:db/id e + :block/properties (assoc properties property-uuid [v])})) ev)] + (when (seq tx-data) + (db/transact! tx-data)))) + (defn update-property! [repo property-uuid {:keys [property-name property-schema]}] {:pre [(uuid? property-uuid)]} - (let [tx-data (cond-> {:block/uuid property-uuid} - property-name (assoc :block/name property-name) - property-schema (assoc :block/schema property-schema) - true outliner-core/block-with-updated-at)] - (db/transact! repo [tx-data]))) + (when-let [property (db/entity [:block/uuid property-uuid])] + (when (and (= :many (:cardinality property-schema)) + (not= :many (:cardinality (:block/schema property)))) + ;; cardinality changed from :one to :many + (fix-cardinality-many-values! property-uuid)) + (let [tx-data (cond-> {:block/uuid property-uuid} + property-name (assoc :block/name property-name) + property-schema (assoc :block/schema property-schema) + true outliner-core/block-with-updated-at)] + (db/transact! repo [tx-data])))) -(defn- extract-refs - [entity properties] - (let [property-values (->> - properties - (map (fn [[k v]] - (let [schema (:block/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'))) - -(comment - (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/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!))))))) +(defn delete-property-value! + "Delete value if a property has multiple values" + [repo block property-id property-value] + (when (and block (uuid? property-id)) + (when (not= property-id (:block/uuid block)) + (when-let [property (db/pull [:block/uuid property-id])] + (let [schema (:block/schema property)] + (when (= :many (:cardinality schema)) + (let [properties (:block/properties block) + properties' (update properties property-id + (fn [col] + (vec (remove #{property-value} col))))] + (outliner-tx/transact! + {:outliner-op :save-block} + (outliner-core/save-block! + {:block/uuid (:block/uuid block) + :block/properties properties'})))) + (state/clear-editor-action!)))))) (defn set-editing-new-property! [value] diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index f6c708da21..78271f1520 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -1872,7 +1872,8 @@ Similar to re-frame subscriptions" (assoc :editor/editing? {edit-input-id true} :editor/set-timestamp-block nil - :cursor-range cursor-range)))) + :cursor-range cursor-range + :ui/editing-property nil)))) (set-state! :editor/block block) (set-state! :editor/content content :path-in-sub-atom edit-input-id) (set-state! :editor/last-key-code nil)