mirror of
https://github.com/logseq/logseq.git
synced 2026-05-28 06:34:34 +00:00
wip: property UX
This commit is contained in:
@@ -3,42 +3,66 @@
|
||||
(:require [frontend.ui :as ui]
|
||||
[frontend.util :as util]
|
||||
[frontend.handler.property :as property-handler]
|
||||
[frontend.handler.ui :as ui-handler]
|
||||
[frontend.db :as db]
|
||||
[rum.core :as rum]
|
||||
[frontend.state :as state]
|
||||
[frontend.mixins :as mixins]
|
||||
[clojure.edn :as edn]))
|
||||
[clojure.edn :as edn]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(rum/defcs property-class-config <
|
||||
(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)]
|
||||
(reset! (::property-name state) (:block/name (db/pull repo '[*] [:block/uuid property-uuid])))
|
||||
(reset! (::property-schema state) (:block/schema (db/pull repo '[*] [:block/uuid property-uuid])))
|
||||
(let [[repo property-uuid] (:rum/args state)
|
||||
property (db/pull repo '[*] [:block/uuid property-uuid])]
|
||||
(reset! (::property-name state) (:block/name property))
|
||||
(reset! (::property-schema state) (:block/schema property))
|
||||
state))}
|
||||
[state repo property-uuid]
|
||||
(let [*property-name (::property-name state)
|
||||
*property-schema (::property-schema state)]
|
||||
[:div
|
||||
[:div
|
||||
[:span.mr-8 "property name:"]
|
||||
[:input
|
||||
[:div.property-configure
|
||||
[:h1.title "Configure property"]
|
||||
|
||||
[: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}]]
|
||||
|
||||
[: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
|
||||
[:span.mr-8 "property schema:"]
|
||||
[:input
|
||||
{:on-change #(reset! *property-schema (util/evalue %))
|
||||
:value (str @*property-schema)}]]
|
||||
[:a
|
||||
{:on-click (fn []
|
||||
(property-handler/update-property-class!
|
||||
repo property-uuid
|
||||
{:property-name @*property-name
|
||||
:property-schema (edn/read-string @*property-schema)}))}
|
||||
"Save"]]))
|
||||
(ui/button
|
||||
"Save"
|
||||
:on-click (fn []
|
||||
(property-handler/update-property!
|
||||
repo property-uuid
|
||||
{:property-name @*property-name
|
||||
:property-schema @*property-schema})
|
||||
(state/close-modal!)))]]]))
|
||||
|
||||
(rum/defcs new-property < rum/reactive
|
||||
(rum/local nil ::property-key)
|
||||
@@ -83,11 +107,11 @@
|
||||
[: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])]
|
||||
(when-let [property (db/pull [:block/uuid prop-uuid-or-built-in-prop])]
|
||||
[:div
|
||||
[:a.mr-2
|
||||
{:on-click (fn [] (state/set-modal! #(property-class-config repo prop-uuid-or-built-in-prop)))}
|
||||
(:block/name property-class)]
|
||||
{: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 []
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"Block properties handler."
|
||||
(:require [clojure.edn :as edn]
|
||||
[clojure.string :as string]
|
||||
[clojure.set :as set]
|
||||
[frontend.db :as db]
|
||||
[frontend.format.block :as block]
|
||||
[frontend.handler.notification :as notification]
|
||||
@@ -14,11 +15,27 @@
|
||||
[logseq.graph-parser.util :as gp-util]
|
||||
[logseq.graph-parser.util.page-ref :as page-ref]
|
||||
[malli.util :as mu]
|
||||
[malli.core :as m]))
|
||||
[malli.core :as m]
|
||||
[malli.error :as me]))
|
||||
|
||||
(defn- date-str?
|
||||
[value]
|
||||
(when-let [d (js/Date. value)]
|
||||
(not= (str d) "Invalid Date")))
|
||||
|
||||
(def builtin-schema-types
|
||||
{:string-contains-refs :string ;default
|
||||
:refs [:sequential :string]})
|
||||
{:default string? ; default, might be mixed with refs, tags
|
||||
:number number?
|
||||
:date inst?
|
||||
:boolean boolean?
|
||||
:url uri?
|
||||
:object uuid?}) ; TODO: make sure block exists
|
||||
|
||||
;; schema -> type, cardinality, object's class
|
||||
;; min, max -> string length, number range, cardinality size limit
|
||||
|
||||
(def builtin-schema->type
|
||||
(set/map-invert builtin-schema-types))
|
||||
|
||||
(def ^:private gp-mldoc-config (gp-mldoc/default-config :markdown))
|
||||
|
||||
@@ -31,79 +48,77 @@
|
||||
distinct)]
|
||||
refs'))
|
||||
|
||||
(defn- is-type-x?
|
||||
[schema-ast x]
|
||||
(or (= x (:type schema-ast))
|
||||
(and (= :and (:type schema-ast))
|
||||
(some #(= x (:type %)) (:children schema-ast)))))
|
||||
|
||||
(defn- schema-base-type
|
||||
[schema]
|
||||
(when-let [ast (try (m/ast schema) (catch :default _))]
|
||||
(cond
|
||||
(is-type-x? ast :int)
|
||||
:int
|
||||
|
||||
(is-type-x? ast :float)
|
||||
:float
|
||||
|
||||
(is-type-x? ast :string)
|
||||
:string
|
||||
|
||||
:else
|
||||
nil)))
|
||||
|
||||
(defn- infer-schema-from-input-string
|
||||
[v-str]
|
||||
(cond
|
||||
(parse-long v-str) :int
|
||||
(parse-double v-str) :float
|
||||
:else nil))
|
||||
(parse-long v-str) :number
|
||||
(parse-double v-str) :number
|
||||
(util/uuid-string? v-str) :object
|
||||
(gp-util/url? v-str) :url
|
||||
(date-str? v-str) :date
|
||||
(contains? #{"true" "false"} (string/lower-case v-str)) :boolean
|
||||
:else :default))
|
||||
|
||||
(defn convert-property-input-string
|
||||
[schema v-str]
|
||||
(case (schema-base-type schema)
|
||||
:string
|
||||
[schema-type v-str]
|
||||
(case schema-type
|
||||
:default
|
||||
v-str
|
||||
|
||||
(:int :float nil)
|
||||
(edn/read-string v-str)))
|
||||
:number
|
||||
(edn/read-string v-str)
|
||||
|
||||
:boolean
|
||||
(edn/read-string (string/lower-case v-str))
|
||||
|
||||
:object
|
||||
(uuid v-str)
|
||||
|
||||
:date
|
||||
(js/Date. v-str)
|
||||
|
||||
:url
|
||||
(goog.Uri. v-str)))
|
||||
|
||||
(defn add-property!
|
||||
[repo block k-name v]
|
||||
(let [property-class (db/pull repo '[*] [:block/name k-name])
|
||||
property-class-uuid (or (:block/uuid property-class) (random-uuid))
|
||||
property-schema (:block/schema property-class)
|
||||
(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)
|
||||
infer-schema (infer-schema-from-input-string v)
|
||||
property-schema (or property-schema infer-schema :string-contains-refs)
|
||||
schema (get builtin-schema-types property-schema property-schema)]
|
||||
property-type (or property-type infer-schema :default)
|
||||
schema (get builtin-schema-types property-type)]
|
||||
(when-let [v* (try
|
||||
(convert-property-input-string schema v)
|
||||
(convert-property-input-string property-type v)
|
||||
(catch :default e
|
||||
(notification/show! (str e) :error false)
|
||||
nil))]
|
||||
(if-let [msg (malli.util/explain-data schema v*)]
|
||||
(notification/show! (str msg) :error false)
|
||||
(do (when (nil? property-class) ;if property-class not exists yet
|
||||
(db/transact! repo [{:block/schema property-schema
|
||||
:block/name k-name
|
||||
:block/uuid property-class-uuid
|
||||
:block/type "property"}]))
|
||||
(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-class-uuid
|
||||
(if (= property-schema :string-contains-refs)
|
||||
(set (extract-page-refs-from-prop-str-value v*))
|
||||
property-uuid
|
||||
(if (= property-type :default)
|
||||
(let [refs (extract-page-refs-from-prop-str-value v*)]
|
||||
(if (seq refs) (set refs) v*))
|
||||
v*))
|
||||
block-properties-text-values
|
||||
(if (= property-schema :string-contains-refs)
|
||||
(assoc (:block/properties-text-values block) property-class-uuid v*)
|
||||
(dissoc (:block/properties-text-values block) property-class-uuid))]
|
||||
(if (= 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)
|
||||
:block/properties block-properties
|
||||
:block/properties-text-values block-properties-text-values}))))))))
|
||||
{:outliner-op :save-block}
|
||||
(outliner-core/save-block!
|
||||
{:block/uuid (:block/uuid block)
|
||||
:block/properties block-properties
|
||||
:block/properties-text-values block-properties-text-values}))))))))
|
||||
|
||||
(defn remove-property!
|
||||
[repo block k-uuid-or-builtin-k-name]
|
||||
@@ -111,29 +126,27 @@
|
||||
(let [origin-properties (:block/properties block)]
|
||||
(assert (contains? (set (keys origin-properties)) k-uuid-or-builtin-k-name))
|
||||
(db/transact!
|
||||
repo
|
||||
[{:block/uuid (:block/uuid block)
|
||||
: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)}])))
|
||||
repo
|
||||
[{:block/uuid (:block/uuid block)
|
||||
: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 update-property-class!
|
||||
(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))]
|
||||
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/property-schema (db/pull [:block/uuid k]))
|
||||
object? (= (:type schema) "object")
|
||||
(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
|
||||
@@ -155,78 +168,28 @@
|
||||
[:block/uuid (uuid %)]
|
||||
(block/page-name->map % true)) 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!
|
||||
[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 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!))))))
|
||||
(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 set-editing-new-property!
|
||||
[value]
|
||||
|
||||
@@ -63,6 +63,11 @@
|
||||
(assoc :block/created-at updated-at))]
|
||||
block))
|
||||
|
||||
(defn block-with-updated-at
|
||||
[block]
|
||||
(let [updated-at (util/time-ms)]
|
||||
(assoc block :block/updated-at updated-at)))
|
||||
|
||||
(defn- remove-orphaned-page-refs!
|
||||
[db-id txs-state old-refs new-refs]
|
||||
(when (not= old-refs new-refs)
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
(defn properties-hidden?
|
||||
[properties]
|
||||
(and (seq properties)
|
||||
(let [ks (map (comp keyword string/lower-case name) (keys properties))
|
||||
(let [ks (if (config/db-based-graph? (state/get-current-repo))
|
||||
(map #(:block/name (db/entity [:block/uuid %])) (keys properties))
|
||||
(map (comp keyword string/lower-case name) (keys properties)))
|
||||
hidden-properties-set (hidden-properties)]
|
||||
(every? hidden-properties-set ks))))
|
||||
|
||||
@@ -462,4 +464,4 @@
|
||||
(if (seq properties-order)
|
||||
(keep (fn [k] (when (contains? properties k) [k (get properties k)]))
|
||||
(distinct properties-order))
|
||||
properties*)))
|
||||
properties*)))
|
||||
|
||||
Reference in New Issue
Block a user