mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 22:25:01 +00:00
Merge pull request #12296 from logseq/feat/bi-directional-properties
feat: bi-directional property
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
(ns user
|
||||
"fns used on repl"
|
||||
(:require [clojure.test :refer [run-tests run-test]]
|
||||
[logseq.e2e.bidirectional-properties-test]
|
||||
[logseq.e2e.block :as b]
|
||||
[logseq.e2e.commands-basic-test]
|
||||
[logseq.e2e.config :as config]
|
||||
@@ -57,6 +58,11 @@
|
||||
(->> (future (run-tests 'logseq.e2e.property-scoped-choices-test))
|
||||
(swap! *futures assoc :property-scoped-choices-test)))
|
||||
|
||||
(defn run-bidirectional-properties-test
|
||||
[]
|
||||
(->> (future (run-tests 'logseq.e2e.bidirectional-properties-test))
|
||||
(swap! *futures assoc :bidirectional-properties-test)))
|
||||
|
||||
(defn run-outliner-test
|
||||
[]
|
||||
(->> (future (run-tests 'logseq.e2e.outliner-basic-test))
|
||||
|
||||
51
clj-e2e/test/logseq/e2e/bidirectional_properties_test.clj
Normal file
51
clj-e2e/test/logseq/e2e/bidirectional_properties_test.clj
Normal file
@@ -0,0 +1,51 @@
|
||||
(ns logseq.e2e.bidirectional-properties-test
|
||||
(:require [clojure.test :refer [deftest is testing use-fixtures]]
|
||||
[logseq.e2e.api :refer [ls-api-call!]]
|
||||
[logseq.e2e.assert :as assert]
|
||||
[logseq.e2e.fixtures :as fixtures]
|
||||
[logseq.e2e.page :as page]
|
||||
[wally.main :as w]))
|
||||
|
||||
(use-fixtures :once fixtures/open-page)
|
||||
|
||||
(use-fixtures :each
|
||||
fixtures/new-logseq-page
|
||||
fixtures/validate-graph)
|
||||
|
||||
(deftest bidirectional-properties-test
|
||||
(testing "shows reverse property references when a class enables bidirectional properties"
|
||||
(let [friend-prop "friend"
|
||||
person-tag "Person"
|
||||
project-tag "Project"
|
||||
target "Bob"
|
||||
container-page "Bidirectional Props"]
|
||||
(ls-api-call! :editor.createTag person-tag
|
||||
{:tagProperties [{:name friend-prop
|
||||
:schema {:type "node"}}]})
|
||||
(ls-api-call! :editor.createTag project-tag)
|
||||
(let [person (ls-api-call! :editor.getTag person-tag)
|
||||
person-uuid (get person "uuid")
|
||||
friend (ls-api-call! :editor.getPage friend-prop)]
|
||||
(ls-api-call! :editor.upsertBlockProperty (get friend "id")
|
||||
"logseq.property/classes"
|
||||
(get person "id"))
|
||||
(is (string? person-uuid))
|
||||
(ls-api-call! :editor.upsertBlockProperty person-uuid
|
||||
"logseq.property.class/bidirectional-property-title"
|
||||
"People")
|
||||
(ls-api-call! :editor.upsertBlockProperty person-uuid
|
||||
"logseq.property.class/enable-bidirectional?"
|
||||
true))
|
||||
(ls-api-call! :editor.createPage target)
|
||||
(ls-api-call! :editor.createPage container-page)
|
||||
(let [bob (ls-api-call! :editor.getPage target)
|
||||
bob-id (get bob "id")]
|
||||
(ls-api-call! :editor.insertBlock container-page (str "Alice #" person-tag)
|
||||
{:properties {friend-prop bob-id}})
|
||||
(ls-api-call! :editor.insertBlock container-page (str "Charlie #" project-tag)
|
||||
{:properties {friend-prop bob-id}}))
|
||||
|
||||
(page/goto-page target)
|
||||
(w/wait-for ".property-k:text('People')")
|
||||
(assert/assert-is-visible ".property-value .block-title-wrap:text('Alice')")
|
||||
(assert/assert-have-count ".property-k:text('Projects')" 0))))
|
||||
7
deps/common/.carve/ignore
vendored
7
deps/common/.carve/ignore
vendored
@@ -6,4 +6,9 @@ logseq.common.graph/read-directories
|
||||
;; Profile utils
|
||||
logseq.common.profile/profile-fn!
|
||||
logseq.common.profile/*key->call-count
|
||||
logseq.common.profile/*key->time-sum
|
||||
logseq.common.profile/*key->time-sum
|
||||
|
||||
;; API fn
|
||||
logseq.common.plural/is-plural?
|
||||
logseq.common.plural/is-singular?
|
||||
logseq.common.plural/pluralize
|
||||
|
||||
3
deps/common/bb.edn
vendored
3
deps/common/bb.edn
vendored
@@ -23,4 +23,5 @@
|
||||
|
||||
:tasks/config
|
||||
{:large-vars
|
||||
{:max-lines-count 45}}}
|
||||
{:metadata-exceptions #{:large-vars/cleanup-todo}
|
||||
:max-lines-count 45}}}
|
||||
|
||||
333
deps/common/src/logseq/common/plural.cljs
vendored
Normal file
333
deps/common/src/logseq/common/plural.cljs
vendored
Normal file
@@ -0,0 +1,333 @@
|
||||
(ns logseq.common.plural
|
||||
"ClojureScript port of pluralize.js core (rules + API).
|
||||
|
||||
Usage:
|
||||
(pluralize \"duck\" 2 true) ;; => \"2 ducks\"
|
||||
(plural \"person\") ;; => \"people\"
|
||||
(singular \"people\") ;; => \"person\"
|
||||
(is-plural? \"ducks\") ;; => true
|
||||
(is-singular? \"duck\") ;; => true
|
||||
|
||||
You can add rules at runtime:
|
||||
(add-plural-rule! #\"(ox)$\" \"$1en\")
|
||||
(add-uncountable-rule! \"metadata\")"
|
||||
(:require [clojure.string :as string]))
|
||||
|
||||
;; -----------------------------------------------------------------------------
|
||||
;; Rule storage (mirrors original semantics)
|
||||
;; pluralize and singularize must run rules sequentially.
|
||||
;; -----------------------------------------------------------------------------
|
||||
|
||||
(defonce ^:private plural-rules (atom [])) ;; vector of [js/RegExp replacement]
|
||||
(defonce ^:private singular-rules (atom [])) ;; vector of [js/RegExp replacement]
|
||||
(defonce ^:private uncountables (atom {})) ;; token -> true
|
||||
(defonce ^:private irregular-plurals (atom {})) ;; plural -> singular
|
||||
(defonce ^:private irregular-singles (atom {})) ;; singular -> plural
|
||||
|
||||
;; -----------------------------------------------------------------------------
|
||||
;; Helpers
|
||||
;; -----------------------------------------------------------------------------
|
||||
|
||||
(defn- sanitize-rule
|
||||
"If rule is a string, compile to case-insensitive regexp that matches the whole string.
|
||||
Else keep it (assumed to be js/RegExp)."
|
||||
[rule]
|
||||
(if (string? rule)
|
||||
(js/RegExp. (str "^" rule "$") "i")
|
||||
rule))
|
||||
|
||||
(defn- restore-case
|
||||
"Replicate casing of `word` onto `token`."
|
||||
[word token]
|
||||
(cond
|
||||
(= word token)
|
||||
token
|
||||
|
||||
(= word (string/lower-case word))
|
||||
(string/lower-case token)
|
||||
|
||||
(= word (string/upper-case word))
|
||||
(string/upper-case token)
|
||||
|
||||
(and (seq word)
|
||||
(= (subs word 0 1) (string/upper-case (subs word 0 1))))
|
||||
(str (string/upper-case (subs token 0 1))
|
||||
(string/lower-case (subs token 1)))
|
||||
|
||||
:else
|
||||
(string/lower-case token)))
|
||||
|
||||
(defn- interpolate
|
||||
"Replace $1..$12 etc in `s` using JS replace args (match, g1, g2 ...)."
|
||||
[s js-args]
|
||||
(.replace s (js/RegExp. "\\$(\\d{1,2})" "g")
|
||||
(fn [_ idx]
|
||||
(let [i (js/parseInt idx 10)
|
||||
v (aget js-args i)]
|
||||
(or v "")))))
|
||||
|
||||
(defn- replace-with-rule
|
||||
"Apply a [re repl] rule to word with casing restoration (matches JS behavior)."
|
||||
[word [re repl]]
|
||||
(.replace word re
|
||||
(fn [& args]
|
||||
;; args: [match g1 g2 ... offset string]
|
||||
(let [match (nth args 0)
|
||||
;; In JS replace callback, second-to-last is offset
|
||||
offset (nth args (- (count args) 2))
|
||||
;; interpolate expects JS-ish indexed args;
|
||||
;; easiest is to turn args into a JS array.
|
||||
js-args (to-array args)
|
||||
result (interpolate repl js-args)]
|
||||
(if (= match "")
|
||||
;; match empty => restore based on char before match
|
||||
(restore-case (subs word (dec offset) offset) result)
|
||||
(restore-case match result))))))
|
||||
|
||||
(defn- sanitize-word
|
||||
"Return sanitized `word` based on `token` and `rules`."
|
||||
[token word rules]
|
||||
(cond
|
||||
(or (zero? (count token))
|
||||
(contains? @uncountables token))
|
||||
word
|
||||
|
||||
:else
|
||||
(let [rs rules
|
||||
;; JS iterates from end to start
|
||||
n (count rs)]
|
||||
(loop [i (dec n)]
|
||||
(if (neg? i)
|
||||
word
|
||||
(let [[re _ :as rule] (nth rs i)]
|
||||
(if (.test re word)
|
||||
(replace-with-rule word rule)
|
||||
(recur (dec i)))))))))
|
||||
|
||||
(defn- replace-word-fn
|
||||
"Build a word transformer (plural or singular)."
|
||||
[replace-map-atom keep-map-atom rules-atom]
|
||||
(fn [word]
|
||||
(let [token (string/lower-case word)
|
||||
keep-map @keep-map-atom
|
||||
replace-map @replace-map-atom
|
||||
rules @rules-atom]
|
||||
(cond
|
||||
(contains? keep-map token)
|
||||
(restore-case word token)
|
||||
|
||||
(contains? replace-map token)
|
||||
(restore-case word (get replace-map token))
|
||||
|
||||
:else
|
||||
(sanitize-word token word rules)))))
|
||||
|
||||
(defn- check-word-fn
|
||||
"Build a predicate for whether word is plural/singular (mirrors JS `checkWord`)."
|
||||
[replace-map-atom keep-map-atom rules-atom]
|
||||
(fn [word]
|
||||
(let [token (string/lower-case word)
|
||||
keep-map @keep-map-atom
|
||||
replace-map @replace-map-atom
|
||||
rules @rules-atom]
|
||||
(cond
|
||||
(contains? keep-map token) true
|
||||
(contains? replace-map token) false
|
||||
:else (= (sanitize-word token token rules) token)))))
|
||||
|
||||
;; -----------------------------------------------------------------------------
|
||||
;; Public API (matches original surface)
|
||||
;; -----------------------------------------------------------------------------
|
||||
|
||||
(def plural (replace-word-fn irregular-singles irregular-plurals plural-rules))
|
||||
(def singular (replace-word-fn irregular-plurals irregular-singles singular-rules))
|
||||
|
||||
(def is-plural? (check-word-fn irregular-singles irregular-plurals plural-rules))
|
||||
(def is-singular? (check-word-fn irregular-plurals irregular-singles singular-rules))
|
||||
|
||||
(defn pluralize
|
||||
"Pluralize or singularize based on count. If inclusive, prefix with count."
|
||||
([word item-count] (pluralize word item-count false))
|
||||
([word item-count inclusive]
|
||||
(let [pluralized (if (= item-count 1) (singular word) (plural word))]
|
||||
(str (when inclusive (str item-count " "))
|
||||
pluralized))))
|
||||
|
||||
(defn add-plural-rule!
|
||||
[rule replacement]
|
||||
(swap! plural-rules conj [(sanitize-rule rule) replacement]))
|
||||
|
||||
(defn add-singular-rule!
|
||||
[rule replacement]
|
||||
(swap! singular-rules conj [(sanitize-rule rule) replacement]))
|
||||
|
||||
(defn add-uncountable-rule!
|
||||
"If word is string => mark as uncountable.
|
||||
If regexp => add plural+singular passthrough rules ($0)."
|
||||
[word]
|
||||
(if (string? word)
|
||||
(swap! uncountables assoc (string/lower-case word) true)
|
||||
(do
|
||||
(add-plural-rule! word "$0")
|
||||
(add-singular-rule! word "$0"))))
|
||||
|
||||
(defn add-irregular-rule!
|
||||
[single plural-word]
|
||||
(let [p (string/lower-case plural-word)
|
||||
s (string/lower-case single)]
|
||||
(swap! irregular-singles assoc s p)
|
||||
(swap! irregular-plurals assoc p s)))
|
||||
|
||||
;; -----------------------------------------------------------------------------
|
||||
;; Data initialization (same as original JS)
|
||||
;; -----------------------------------------------------------------------------
|
||||
|
||||
(defn- ^:large-vars/cleanup-todo init-irregulars! []
|
||||
(doseq [[s p]
|
||||
;; Pronouns + irregulars
|
||||
[["I" "we"]
|
||||
["me" "us"]
|
||||
["he" "they"]
|
||||
["she" "they"]
|
||||
["them" "them"]
|
||||
["myself" "ourselves"]
|
||||
["yourself" "yourselves"]
|
||||
["itself" "themselves"]
|
||||
["herself" "themselves"]
|
||||
["himself" "themselves"]
|
||||
["themself" "themselves"]
|
||||
["is" "are"]
|
||||
["was" "were"]
|
||||
["has" "have"]
|
||||
["this" "these"]
|
||||
["that" "those"]
|
||||
["my" "our"]
|
||||
["its" "their"]
|
||||
["his" "their"]
|
||||
["her" "their"]
|
||||
;; Words ending with consonant + o
|
||||
["echo" "echoes"]
|
||||
["dingo" "dingoes"]
|
||||
["volcano" "volcanoes"]
|
||||
["tornado" "tornadoes"]
|
||||
["torpedo" "torpedoes"]
|
||||
;; Ends with us
|
||||
["genus" "genera"]
|
||||
["viscus" "viscera"]
|
||||
;; Ends with ma
|
||||
["stigma" "stigmata"]
|
||||
["stoma" "stomata"]
|
||||
["dogma" "dogmata"]
|
||||
["lemma" "lemmata"]
|
||||
["schema" "schemata"]
|
||||
["anathema" "anathemata"]
|
||||
;; Other irregular
|
||||
["ox" "oxen"]
|
||||
["axe" "axes"]
|
||||
["die" "dice"]
|
||||
["yes" "yeses"]
|
||||
["foot" "feet"]
|
||||
["eave" "eaves"]
|
||||
["goose" "geese"]
|
||||
["tooth" "teeth"]
|
||||
["quiz" "quizzes"]
|
||||
["human" "humans"]
|
||||
["proof" "proofs"]
|
||||
["carve" "carves"]
|
||||
["valve" "valves"]
|
||||
["looey" "looies"]
|
||||
["thief" "thieves"]
|
||||
["groove" "grooves"]
|
||||
["pickaxe" "pickaxes"]
|
||||
["passerby" "passersby"]
|
||||
["canvas" "canvases"]]]
|
||||
(add-irregular-rule! s p)))
|
||||
|
||||
(defn- init-plural-rules! []
|
||||
(doseq [[rule repl]
|
||||
[[(js/RegExp. "s?$" "i") "s"]
|
||||
[(js/RegExp. "[^\\u0000-\\u007F]$" "i") "$0"]
|
||||
[(js/RegExp. "([^aeiou]ese)$" "i") "$1"]
|
||||
[(js/RegExp. "(ax|test)is$" "i") "$1es"]
|
||||
[(js/RegExp. "(alias|[^aou]us|t[lm]as|gas|ris)$" "i") "$1es"]
|
||||
[(js/RegExp. "(e[mn]u)s?$" "i") "$1s"]
|
||||
[(js/RegExp. "([^l]ias|[aeiou]las|[ejzr]as|[iu]am)$" "i") "$1"]
|
||||
[(js/RegExp. "(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$" "i") "$1i"]
|
||||
[(js/RegExp. "(alumn|alg|vertebr)(?:a|ae)$" "i") "$1ae"]
|
||||
[(js/RegExp. "(seraph|cherub)(?:im)?$" "i") "$1im"]
|
||||
[(js/RegExp. "(her|at|gr)o$" "i") "$1oes"]
|
||||
[(js/RegExp. "(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$" "i") "$1a"]
|
||||
[(js/RegExp. "(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$" "i") "$1a"]
|
||||
[(js/RegExp. "sis$" "i") "ses"]
|
||||
[(js/RegExp. "(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$" "i") "$1$2ves"]
|
||||
[(js/RegExp. "([^aeiouy]|qu)y$" "i") "$1ies"]
|
||||
[(js/RegExp. "([^ch][ieo][ln])ey$" "i") "$1ies"]
|
||||
[(js/RegExp. "(x|ch|ss|sh|zz)$" "i") "$1es"]
|
||||
[(js/RegExp. "(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$" "i") "$1ices"]
|
||||
[(js/RegExp. "\\b((?:tit)?m|l)(?:ice|ouse)$" "i") "$1ice"]
|
||||
[(js/RegExp. "(pe)(?:rson|ople)$" "i") "$1ople"]
|
||||
[(js/RegExp. "(child)(?:ren)?$" "i") "$1ren"]
|
||||
[(js/RegExp. "eaux$" "i") "$0"]
|
||||
[(js/RegExp. "m[ae]n$" "i") "men"]
|
||||
["thou" "you"]]]
|
||||
(add-plural-rule! rule repl)))
|
||||
|
||||
(defn- init-singular-rules! []
|
||||
(doseq [[rule repl]
|
||||
[[(js/RegExp. "s$" "i") ""]
|
||||
[(js/RegExp. "(ss)$" "i") "$1"]
|
||||
[(js/RegExp. "(wi|kni|(?:after|half|high|low|mid|non|night|[^\\w]|^)li)ves$" "i") "$1fe"]
|
||||
[(js/RegExp. "(ar|(?:wo|[ae])l|[eo][ao])ves$" "i") "$1f"]
|
||||
[(js/RegExp. "ies$" "i") "y"]
|
||||
[(js/RegExp. "(dg|ss|ois|lk|ok|wn|mb|th|ch|ec|oal|is|ck|ix|sser|ts|wb)ies$" "i") "$1ie"]
|
||||
[(js/RegExp. "\\b(l|(?:neck|cross|hog|aun)?t|coll|faer|food|gen|goon|group|hipp|junk|vegg|(?:pork)?p|charl|calor|cut)ies$" "i") "$1ie"]
|
||||
[(js/RegExp. "\\b(mon|smil)ies$" "i") "$1ey"]
|
||||
[(js/RegExp. "\\b((?:tit)?m|l)ice$" "i") "$1ouse"]
|
||||
[(js/RegExp. "(seraph|cherub)im$" "i") "$1"]
|
||||
[(js/RegExp. "(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$" "i") "$1"]
|
||||
[(js/RegExp. "(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$" "i") "$1sis"]
|
||||
[(js/RegExp. "(movie|twelve|abuse|e[mn]u)s$" "i") "$1"]
|
||||
[(js/RegExp. "(test)(?:is|es)$" "i") "$1is"]
|
||||
[(js/RegExp. "(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$" "i") "$1us"]
|
||||
[(js/RegExp. "(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$" "i") "$1um"]
|
||||
[(js/RegExp. "(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$" "i") "$1on"]
|
||||
[(js/RegExp. "(alumn|alg|vertebr)ae$" "i") "$1a"]
|
||||
[(js/RegExp. "(cod|mur|sil|vert|ind)ices$" "i") "$1ex"]
|
||||
[(js/RegExp. "(matr|append)ices$" "i") "$1ix"]
|
||||
[(js/RegExp. "(pe)(rson|ople)$" "i") "$1rson"]
|
||||
[(js/RegExp. "(child)ren$" "i") "$1"]
|
||||
[(js/RegExp. "(eau)x?$" "i") "$1"]
|
||||
[(js/RegExp. "men$" "i") "man"]]]
|
||||
(add-singular-rule! rule repl)))
|
||||
|
||||
(defn- init-uncountables! []
|
||||
(doseq [w
|
||||
["adulthood" "advice" "agenda" "aid" "aircraft" "alcohol" "ammo"
|
||||
"analytics" "anime" "athletics" "audio" "bison" "blood" "bream"
|
||||
"buffalo" "butter" "carp" "cash" "chassis" "chess" "clothing" "cod"
|
||||
"commerce" "cooperation" "corps" "debris" "diabetes" "digestion" "elk"
|
||||
"energy" "equipment" "excretion" "expertise" "firmware" "flounder"
|
||||
"fun" "gallows" "garbage" "graffiti" "hardware" "headquarters" "health"
|
||||
"herpes" "highjinks" "homework" "housework" "information" "jeans"
|
||||
"justice" "kudos" "labour" "literature" "machinery" "mackerel" "mail"
|
||||
"media" "mews" "moose" "music" "mud" "manga" "news" "only" "personnel"
|
||||
"pike" "plankton" "pliers" "police" "pollution" "premises" "rain"
|
||||
"research" "rice" "salmon" "scissors" "series" "sewage" "shambles"
|
||||
"shrimp" "software" "staff" "swine" "tennis" "traffic"
|
||||
"transportation" "trout" "tuna" "wealth" "welfare" "whiting"
|
||||
"wildebeest" "wildlife" "you"]]
|
||||
(add-uncountable-rule! w))
|
||||
(doseq [re [(js/RegExp. "pok[eé]mon$" "i")
|
||||
(js/RegExp. "[^aeiou]ese$" "i")
|
||||
(js/RegExp. "deer$" "i")
|
||||
(js/RegExp. "fish$" "i")
|
||||
(js/RegExp. "measles$" "i")
|
||||
(js/RegExp. "o[iu]s$" "i")
|
||||
(js/RegExp. "pox$" "i")
|
||||
(js/RegExp. "sheep$" "i")]]
|
||||
(add-uncountable-rule! re)))
|
||||
|
||||
(init-irregulars!)
|
||||
(init-plural-rules!)
|
||||
(init-singular-rules!)
|
||||
(init-uncountables!)
|
||||
67
deps/db/src/logseq/db.cljs
vendored
67
deps/db/src/logseq/db.cljs
vendored
@@ -9,6 +9,7 @@
|
||||
[datascript.core :as d]
|
||||
[datascript.impl.entity :as de]
|
||||
[logseq.common.config :as common-config]
|
||||
[logseq.common.plural :as common-plural]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.common.uuid :as common-uuid]
|
||||
[logseq.db.common.delete-blocks :as delete-blocks] ;; Load entity extensions
|
||||
@@ -677,3 +678,69 @@
|
||||
(recur (:block/parent parent)))))))
|
||||
|
||||
(def get-class-title-with-extends db-db/get-class-title-with-extends)
|
||||
|
||||
(defn- bidirectional-property-attr?
|
||||
[db attr]
|
||||
(when (qualified-keyword? attr)
|
||||
(let [attr-ns (namespace attr)]
|
||||
(and (or (db-property/user-property-namespace? attr-ns)
|
||||
(db-property/plugin-property? attr))
|
||||
(when-let [property (d/entity db attr)]
|
||||
(= :db.type/ref (:db/valueType property)))))))
|
||||
|
||||
(defn- get-ea-by-v
|
||||
[db v]
|
||||
(d/q '[:find ?e ?a
|
||||
:in $ ?v
|
||||
:where
|
||||
[?e ?a ?v]
|
||||
[?ea :db/ident ?a]
|
||||
[?ea :logseq.property/classes]]
|
||||
db
|
||||
v))
|
||||
|
||||
(defn get-bidirectional-properties
|
||||
"Given a target entity id, returns a seq of maps with:
|
||||
* :class - class entity
|
||||
* :title - pluralized class title
|
||||
* :entities - node entities that reference the target via ref properties"
|
||||
[db target-id]
|
||||
(when (and db target-id (d/entity db target-id))
|
||||
(let [add-entity
|
||||
(fn [acc class-id entity]
|
||||
(if class-id
|
||||
(update acc class-id (fnil conj #{}) entity)
|
||||
acc))]
|
||||
(->> (get-ea-by-v db target-id)
|
||||
(keep (fn [[e a]]
|
||||
(when (bidirectional-property-attr? db a)
|
||||
(when-let [entity (d/entity db e)]
|
||||
(when (and (not= (:db/id entity) target-id)
|
||||
(not (entity-util/class? entity))
|
||||
(not (entity-util/property? entity)))
|
||||
(let [classes (filter entity-util/class? (:block/tags entity))]
|
||||
(when (seq classes)
|
||||
(keep (fn [class-ent]
|
||||
(when-not (built-in? class-ent)
|
||||
[(:db/id class-ent) entity]))
|
||||
classes))))))))
|
||||
(mapcat identity)
|
||||
(reduce (fn [acc [class-ent entity]]
|
||||
(add-entity acc class-ent entity))
|
||||
{})
|
||||
(keep (fn [[class-id entities]]
|
||||
(let [class (d/entity db class-id)]
|
||||
(when (true? (:logseq.property.class/enable-bidirectional? class))
|
||||
(let [custom-title (when-let [custom (:logseq.property.class/bidirectional-property-title class)]
|
||||
(if (string? custom)
|
||||
custom
|
||||
(db-property/property-value-content custom)))
|
||||
title (if (string/blank? custom-title)
|
||||
(common-plural/plural (:block/title class))
|
||||
custom-title)]
|
||||
{:title title
|
||||
:class (-> (into {} class)
|
||||
(assoc :db/id (:db/id class)))
|
||||
:entities (->> entities
|
||||
(sort-by :block/created-at))})))))
|
||||
(sort-by (comp :block/created-at :class))))))
|
||||
|
||||
10
deps/db/src/logseq/db/frontend/property.cljs
vendored
10
deps/db/src/logseq/db/frontend/property.cljs
vendored
@@ -182,6 +182,16 @@
|
||||
:cardinality :many
|
||||
:public? true
|
||||
:view-context :never}}
|
||||
:logseq.property.class/bidirectional-property-title {:title "Bidirectional property title"
|
||||
:schema {:type :string
|
||||
:public? true
|
||||
:view-context :class}}
|
||||
:logseq.property.class/enable-bidirectional? {:title "Enable bidirectional properties"
|
||||
:schema {:type :checkbox
|
||||
:public? true
|
||||
:view-context :class}
|
||||
:properties
|
||||
{:logseq.property/description "When enabled, this tag will show reverse nodes that link to the current node via properties."}}
|
||||
:logseq.property/hide-empty-value {:title "Hide empty value"
|
||||
:schema {:type :checkbox
|
||||
:public? true
|
||||
|
||||
2
deps/db/src/logseq/db/frontend/schema.cljs
vendored
2
deps/db/src/logseq/db/frontend/schema.cljs
vendored
@@ -37,7 +37,7 @@
|
||||
(map (juxt :major :minor)
|
||||
[(parse-schema-version x) (parse-schema-version y)])))
|
||||
|
||||
(def version (parse-schema-version "65.19"))
|
||||
(def version (parse-schema-version "65.20"))
|
||||
|
||||
(defn major-version
|
||||
"Return a number.
|
||||
|
||||
41
deps/db/test/logseq/db_test.cljs
vendored
41
deps/db/test/logseq/db_test.cljs
vendored
@@ -108,4 +108,43 @@
|
||||
(fn [temp-conn]
|
||||
(ldb/transact! temp-conn [{:db/ident :logseq.class/Task
|
||||
:block/tags :logseq.class/Property}])
|
||||
(ldb/transact! temp-conn [[:db/retract :logseq.class/Task :block/tags :logseq.class/Property]]))))))
|
||||
(ldb/transact! temp-conn [[:db/retract :logseq.class/Task :block/tags :logseq.class/Property]]))))))
|
||||
|
||||
(deftest get-bidirectional-properties
|
||||
(testing "disabled by default"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:friend {:logseq.property/type :node
|
||||
:build/property-classes [:Person]}}
|
||||
:classes {:Person {}
|
||||
:Project {}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "Alice"
|
||||
:build/tags [:Person]
|
||||
:build/properties {:friend [:build/page {:block/title "Bob"}]}}}
|
||||
{:page {:block/title "Bob"}}
|
||||
{:page {:block/title "Charlie"
|
||||
:build/tags [:Project]
|
||||
:build/properties {:friend [:build/page {:block/title "Bob"}]}}}]})
|
||||
target (db-test/find-page-by-title @conn "Bob")]
|
||||
(is (empty? (ldb/get-bidirectional-properties @conn (:db/id target))))))
|
||||
|
||||
(testing "enabled per class"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:properties {:friend {:logseq.property/type :node
|
||||
:build/property-classes [:Person]}}
|
||||
:classes {:Person {:build/properties {:logseq.property.class/enable-bidirectional? true}}
|
||||
:Project {}}
|
||||
:pages-and-blocks
|
||||
[{:page {:block/title "Alice"
|
||||
:build/tags [:Person]
|
||||
:build/properties {:friend [:build/page {:block/title "Bob"}]}}}
|
||||
{:page {:block/title "Bob"}}
|
||||
{:page {:block/title "Charlie"
|
||||
:build/tags [:Project]
|
||||
:build/properties {:friend [:build/page {:block/title "Bob"}]}}}]})
|
||||
target (db-test/find-page-by-title @conn "Bob")
|
||||
results (ldb/get-bidirectional-properties @conn (:db/id target))]
|
||||
(is (= 1 (count results)))
|
||||
(is (= "People" (:title (first results))))
|
||||
(is (= ["Alice"]
|
||||
(map :block/title (:entities (first results))))))))
|
||||
|
||||
@@ -584,7 +584,6 @@
|
||||
(= existing-value v'))]
|
||||
(throw-error-if-self-value block v' ref?)
|
||||
|
||||
(prn :debug :value-matches? value-matches?)
|
||||
(when-not value-matches?
|
||||
(raw-set-block-property! conn block property v'))))))))
|
||||
|
||||
|
||||
@@ -1744,6 +1744,7 @@
|
||||
doc-mode? (state/sub :document/mode?)
|
||||
control-show? (util/react *control-show?)
|
||||
ref? (:ref? config)
|
||||
container-id (:container-id config)
|
||||
empty-content? (block-content-empty? block)
|
||||
fold-button-right? (state/enable-fold-button-right?)
|
||||
own-number-list? (:own-order-number-list? config)
|
||||
@@ -1774,9 +1775,10 @@
|
||||
:on-click (fn [event]
|
||||
(util/stop event)
|
||||
(state/clear-edit!)
|
||||
(state/set-state! :editor/container-id container-id)
|
||||
(p/do!
|
||||
(if ref?
|
||||
(state/toggle-collapsed-block! uuid)
|
||||
(state/toggle-collapsed-block! uuid container-id)
|
||||
(if collapsed?
|
||||
(editor-handler/expand-block! uuid)
|
||||
(editor-handler/collapse-block! uuid)))
|
||||
@@ -2984,7 +2986,7 @@
|
||||
(:view? config)
|
||||
(root-block? config block)
|
||||
(and (or (ldb/class? block) (ldb/property? block)) (:page-title? config)))
|
||||
(state/sub-block-collapsed uuid)
|
||||
(state/sub-block-collapsed uuid container-id)
|
||||
|
||||
:else
|
||||
db-collapsed?)
|
||||
@@ -3244,10 +3246,12 @@
|
||||
(boolean result)))
|
||||
|
||||
(defn- set-collapsed-block!
|
||||
[block-id v]
|
||||
[block-id v container-id]
|
||||
(if (false? v)
|
||||
(editor-handler/expand-block! block-id {:skip-db-collpsing? true})
|
||||
(state/set-collapsed-block! block-id v)))
|
||||
(do
|
||||
(editor-handler/expand-block! block-id {:skip-db-collpsing? true})
|
||||
(state/set-collapsed-block! block-id v container-id))
|
||||
(state/set-collapsed-block! block-id v container-id)))
|
||||
|
||||
(rum/defcs loaded-block-container < rum/reactive db-mixins/query
|
||||
(rum/local false ::show-block-left-menu?)
|
||||
@@ -3257,19 +3261,23 @@
|
||||
(let [[config block] (:rum/args state)
|
||||
block-id (:block/uuid block)
|
||||
linked-block? (or (:block/link block)
|
||||
(:original-block config))]
|
||||
(:original-block config))
|
||||
container-id (if (or linked-block? (nil? (:container-id config)))
|
||||
(state/get-next-container-id)
|
||||
(:container-id config))]
|
||||
(when-not (:property-block? config)
|
||||
(cond
|
||||
(and (:page-title? config) (or (ldb/class? block) (ldb/property? block)) (not config/publishing?))
|
||||
(let [collapsed? (state/get-block-collapsed block-id)]
|
||||
(set-collapsed-block! block-id (if (some? collapsed?) collapsed? true)))
|
||||
(let [collapsed? (state/get-block-collapsed block-id container-id)]
|
||||
(set-collapsed-block! block-id (if (some? collapsed?) collapsed? true) container-id))
|
||||
|
||||
(root-block? config block)
|
||||
(set-collapsed-block! block-id false)
|
||||
(set-collapsed-block! block-id false container-id)
|
||||
|
||||
(or (:view? config) (:ref? config) (:custom-query? config))
|
||||
(set-collapsed-block! block-id
|
||||
(boolean (editor-handler/block-default-collapsed? block config)))
|
||||
(boolean (editor-handler/block-default-collapsed? block config))
|
||||
container-id)
|
||||
|
||||
:else
|
||||
nil))
|
||||
@@ -3277,14 +3285,15 @@
|
||||
(assoc state
|
||||
::control-show? (atom false)
|
||||
::navigating-block (atom (:block/uuid block)))
|
||||
(or linked-block? (nil? (:container-id config)))
|
||||
(assoc ::container-id (state/get-next-container-id)))))
|
||||
(and container-id (or linked-block? (nil? (:container-id config))))
|
||||
(assoc ::container-id container-id))))
|
||||
:will-unmount (fn [state]
|
||||
;; restore root block's collapsed state
|
||||
(let [[config block] (:rum/args state)
|
||||
block-id (:block/uuid block)]
|
||||
block-id (:block/uuid block)
|
||||
container-id (or (:container-id config) (::container-id state))]
|
||||
(when (root-block? config block)
|
||||
(set-collapsed-block! block-id nil)))
|
||||
(set-collapsed-block! block-id nil container-id)))
|
||||
state)}
|
||||
[state config block & {:as opts}]
|
||||
(let [repo (state/get-current-repo)
|
||||
@@ -3318,7 +3327,8 @@
|
||||
(p/let [block (db-async/<get-block (state/get-current-repo)
|
||||
id
|
||||
{:children? (not
|
||||
(if-some [result (state/get-block-collapsed (:block/uuid block))]
|
||||
(if-some [result (state/get-block-collapsed (:block/uuid block)
|
||||
(:container-id config))]
|
||||
result
|
||||
(:block/collapsed? block)))
|
||||
:skip-refresh? false})]
|
||||
|
||||
@@ -1131,6 +1131,10 @@ html.is-mac {
|
||||
.block-tags {
|
||||
margin-top: 17px;
|
||||
}
|
||||
|
||||
.ls-properties-area .block-tags {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ls-page-title .ls-properties-area {
|
||||
|
||||
@@ -341,6 +341,48 @@
|
||||
(:block/title property)]
|
||||
(property-key-title block property class-schema?))]))
|
||||
|
||||
(defn- bidirectional-property-icon-cp
|
||||
[property]
|
||||
(if-let [icon (:logseq.property/icon property)]
|
||||
(icon-component/icon icon {:size 15 :color? true})
|
||||
(ui/icon "letter-b" {:class "opacity-50" :size 15})))
|
||||
|
||||
(rum/defcs bidirectional-values-cp < rum/static
|
||||
{:init (fn [state]
|
||||
(assoc state ::container-id (state/get-next-container-id)))}
|
||||
[state entities]
|
||||
(let [blocks-container (state/get-component :block/blocks-container)
|
||||
container-id (::container-id state)
|
||||
config {:id (str "bidirectional-" container-id)
|
||||
:container-id container-id
|
||||
:editor-box (state/get-component :editor/box)
|
||||
:default-collapsed? true
|
||||
:ref? true}]
|
||||
(if (and blocks-container (seq entities))
|
||||
[:div.property-block-container.content.w-full
|
||||
(blocks-container config entities)]
|
||||
[:span.opacity-60 "Empty"])))
|
||||
|
||||
(rum/defc bidirectional-properties-section < rum/static
|
||||
[bidirectional-properties]
|
||||
(when (seq bidirectional-properties)
|
||||
(for [{:keys [class title entities]} bidirectional-properties]
|
||||
[:div.property-pair.items-start {:key (str "bidirectional-" title)}
|
||||
[:div.property-key
|
||||
[:div.property-key-inner
|
||||
[:div.property-icon
|
||||
(bidirectional-property-icon-cp class)]
|
||||
(if class
|
||||
[:a.property-k.flex.select-none.w-full.jtrigger
|
||||
{:on-click (fn [e]
|
||||
(util/stop e)
|
||||
(route-handler/redirect-to-page! (:block/uuid class)))}
|
||||
title]
|
||||
[:div.property-k.flex.select-none.w-full title])]]
|
||||
[:div.ls-block.property-value-container.flex.flex-row.gap-1.items-start
|
||||
[:div.property-value.flex.flex-1
|
||||
(bidirectional-values-cp entities)]]])))
|
||||
|
||||
(rum/defcs ^:large-vars/cleanup-todo property-input < rum/reactive
|
||||
(rum/local false ::show-new-property-config?)
|
||||
(rum/local false ::show-class-select?)
|
||||
@@ -584,7 +626,18 @@
|
||||
[:div.mt-1
|
||||
(properties-section block hidden-properties opts)]]))
|
||||
|
||||
(rum/defc load-bidirectional-properties < rum/static
|
||||
[block root-block? set-bidirectional-properties!]
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(when (and root-block? (:db/id block))
|
||||
(p/let [result (db-async/<get-bidirectional-properties (:db/id block))]
|
||||
(set-bidirectional-properties! result)))
|
||||
(fn []))
|
||||
[root-block? (:db/id block)]))
|
||||
|
||||
(rum/defcs ^:large-vars/cleanup-todo properties-area < rum/reactive db-mixins/query
|
||||
(rum/local nil ::bidirectional-properties)
|
||||
{:init (fn [state]
|
||||
(let [target-block (first (:rum/args state))
|
||||
block (resolve-linked-block-if-exists target-block)]
|
||||
@@ -592,7 +645,9 @@
|
||||
::id (str (random-uuid))
|
||||
::block block)))}
|
||||
[state _target-block {:keys [page-title? journal-page? sidebar-properties? tag-dialog?] :as opts}]
|
||||
(let [id (::id state)
|
||||
(let [*bidirectional-properties (::bidirectional-properties state)
|
||||
bidirectional-properties @*bidirectional-properties
|
||||
id (::id state)
|
||||
db-id (:db/id (::block state))
|
||||
block (db/sub-block db-id)
|
||||
show-properties? (or sidebar-properties? tag-dialog?)
|
||||
@@ -600,7 +655,11 @@
|
||||
(and show?
|
||||
(or (= mode :global)
|
||||
(and (set? ids) (contains? ids (:block/uuid block))))))
|
||||
properties (:block/properties block)
|
||||
properties (cond-> (:block/properties block)
|
||||
(and (ldb/class? block)
|
||||
(not (ldb/built-in? block)))
|
||||
(assoc :logseq.property.class/enable-bidirectional?
|
||||
(:logseq.property.class/enable-bidirectional? block)))
|
||||
remove-built-in-or-other-position-properties
|
||||
(fn [properties show-in-hidden-properties?]
|
||||
(remove (fn [property]
|
||||
@@ -682,48 +741,53 @@
|
||||
(state/get-current-page))
|
||||
(and (= (str (:block/uuid block)) (:id opts))
|
||||
(not (entity-util/page? block))))]
|
||||
(cond
|
||||
(and (empty? full-properties) (seq hidden-properties) (not root-block?) (not sidebar-properties?))
|
||||
nil
|
||||
[:<>
|
||||
(load-bidirectional-properties block root-block? #(reset! *bidirectional-properties %))
|
||||
(let [has-bidirectional-properties? (seq bidirectional-properties)]
|
||||
(cond
|
||||
(and (empty? full-properties) (seq hidden-properties) (not root-block?) (not sidebar-properties?)
|
||||
(not has-bidirectional-properties?))
|
||||
nil
|
||||
|
||||
(and (empty? full-properties) (empty? hidden-properties))
|
||||
(when show-properties?
|
||||
(rum/with-key (new-property block opts) (str id "-add-property")))
|
||||
(and (empty? full-properties) (empty? hidden-properties) (not has-bidirectional-properties?))
|
||||
(when show-properties?
|
||||
(rum/with-key (new-property block opts) (str id "-add-property")))
|
||||
|
||||
:else
|
||||
(let [remove-properties #{:logseq.property/icon :logseq.property/query}
|
||||
properties' (->> (remove (fn [[k _v]] (contains? remove-properties k))
|
||||
full-properties)
|
||||
(remove (fn [[k _v]] (= k :logseq.property.class/properties))))
|
||||
page? (entity-util/page? block)
|
||||
class? (entity-util/class? block)]
|
||||
[:div.ls-properties-area
|
||||
{:id id
|
||||
:class (util/classnames [{:ls-page-properties page?}])
|
||||
:tab-index 0}
|
||||
[:<>
|
||||
(properties-section block properties' opts)
|
||||
:else
|
||||
(let [remove-properties #{:logseq.property/icon :logseq.property/query}
|
||||
properties' (->> (remove (fn [[k _v]] (contains? remove-properties k))
|
||||
full-properties)
|
||||
(remove (fn [[k _v]] (= k :logseq.property.class/properties))))
|
||||
page? (entity-util/page? block)
|
||||
class? (entity-util/class? block)]
|
||||
[:div.ls-properties-area
|
||||
{:id id
|
||||
:class (util/classnames [{:ls-page-properties page?}])
|
||||
:tab-index 0}
|
||||
[:<>
|
||||
(properties-section block properties' opts)
|
||||
(bidirectional-properties-section bidirectional-properties)
|
||||
|
||||
(when-not class?
|
||||
(hidden-properties-cp block hidden-properties
|
||||
(assoc opts :root-block? root-block?)))
|
||||
(when-not class?
|
||||
(hidden-properties-cp block hidden-properties
|
||||
(assoc opts :root-block? root-block?)))
|
||||
|
||||
(when (and page? (not class?))
|
||||
(rum/with-key (new-property block opts) (str id "-add-property")))
|
||||
(when (and page? (not class?))
|
||||
(rum/with-key (new-property block opts) (str id "-add-property")))
|
||||
|
||||
(when class?
|
||||
(let [properties (->> (:logseq.property.class/properties block)
|
||||
(map (fn [e] [(:db/ident e)])))
|
||||
opts' (assoc opts :class-schema? true)]
|
||||
[:div.flex.flex-col.gap-1
|
||||
[:div {:style {:font-size 15}}
|
||||
[:div.property-pair
|
||||
[:div.property-key.text-sm
|
||||
(property-key-cp block (db/entity :logseq.property.class/properties) {})]]
|
||||
[:div.text-muted-foreground {:style {:margin-left 26}}
|
||||
"Tag properties are inherited by all nodes using the tag. For example, each #Task node inherits 'Status' and 'Priority'."]]
|
||||
[:div.ml-4
|
||||
(properties-section block properties opts')
|
||||
(hidden-properties-cp block hidden-properties
|
||||
(assoc opts :root-block? root-block?))
|
||||
(rum/with-key (new-property block opts') (str id "-class-add-property"))]]))]]))))
|
||||
(when class?
|
||||
(let [properties (->> (:logseq.property.class/properties block)
|
||||
(map (fn [e] [(:db/ident e)])))
|
||||
opts' (assoc opts :class-schema? true)]
|
||||
[:div.flex.flex-col.gap-1
|
||||
[:div {:style {:font-size 15}}
|
||||
[:div.property-pair
|
||||
[:div.property-key.text-sm
|
||||
(property-key-cp block (db/entity :logseq.property.class/properties) {})]]
|
||||
[:div.text-muted-foreground {:style {:margin-left 26}}
|
||||
"Tag properties are inherited by all nodes using the tag. For example, each #Task node inherits 'Status' and 'Priority'."]]
|
||||
[:div.ml-4
|
||||
(properties-section block properties opts')
|
||||
(hidden-properties-cp block hidden-properties
|
||||
(assoc opts :root-block? root-block?))
|
||||
(rum/with-key (new-property block opts') (str id "-class-add-property"))]]))]])))]))
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
[promesa.core :as p]
|
||||
[rum.core :as rum]))
|
||||
|
||||
;; TODO: support :string editing
|
||||
(defonce string-value-on-click
|
||||
{:logseq.property.asset/external-url
|
||||
(fn [block property]
|
||||
@@ -1074,6 +1073,69 @@
|
||||
:else
|
||||
(property-normal-block-value block property v-block opts))))
|
||||
|
||||
(rum/defc single-string-input
|
||||
[block property value table-view?]
|
||||
(let [[editing? set-editing!] (hooks/use-state false)
|
||||
*ref (hooks/use-ref nil)
|
||||
string-value (cond
|
||||
(string? value) value
|
||||
(some? value) (str (db-property/property-value-content value))
|
||||
:else "")
|
||||
[value set-value!] (hooks/use-state string-value)
|
||||
set-property-value! (fn [value & {:keys [exit-editing?]
|
||||
:or {exit-editing? true}}]
|
||||
(let [next-value (or value "")
|
||||
blank? (string/blank? next-value)]
|
||||
(p/do!
|
||||
(if blank?
|
||||
(when (get block (:db/ident property))
|
||||
(db-property-handler/remove-block-property! (:db/id block) (:db/ident property)))
|
||||
(when (not= string-value next-value)
|
||||
(db-property-handler/set-block-property! (:db/id block)
|
||||
(:db/ident property)
|
||||
next-value)))
|
||||
(set-value! (or (get (db/entity (:db/id block)) (:db/ident property)) ""))
|
||||
(when exit-editing?
|
||||
(set-editing! false)))))]
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(set-value! string-value)
|
||||
#())
|
||||
[string-value])
|
||||
|
||||
[:div.ls-string.flex.flex-1.jtrigger
|
||||
{:ref *ref
|
||||
:on-click #(do
|
||||
(state/clear-selection!)
|
||||
(set-editing! true))}
|
||||
(if editing?
|
||||
(shui/input
|
||||
{:auto-focus true
|
||||
:class (str "ls-string-input h-6 px-0 py-0 border-none bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 text-base"
|
||||
(when table-view? " text-sm"))
|
||||
:value value
|
||||
:on-change (fn [e]
|
||||
(set-value! (util/evalue e)))
|
||||
:on-blur (fn [_e]
|
||||
(p/do!
|
||||
(set-property-value! value)))
|
||||
:on-key-down (fn [e]
|
||||
(case (util/ekey e)
|
||||
"Enter"
|
||||
(do
|
||||
(util/stop e)
|
||||
(set-property-value! value))
|
||||
"Escape"
|
||||
(do
|
||||
(util/stop e)
|
||||
(set-value! string-value)
|
||||
(set-editing! false)
|
||||
(some-> (rum/deref *ref) (.focus)))
|
||||
nil))})
|
||||
(if (string/blank? string-value)
|
||||
(property-empty-text-value property {:table-view? table-view?})
|
||||
string-value))]))
|
||||
|
||||
(rum/defc closed-value-item < rum/reactive db-mixins/query
|
||||
[value {:keys [inline-text icon?]}]
|
||||
(when value
|
||||
@@ -1372,6 +1434,9 @@
|
||||
(and (= type :number) (not editing?) (not closed-values?))
|
||||
(single-number-input block property value (:table-view? opts))
|
||||
|
||||
(= type :string)
|
||||
(single-string-input block property value (:table-view? opts))
|
||||
|
||||
:else
|
||||
(if (and select-type?'
|
||||
(not (and (not closed-values?) (= type :date))))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.property-value-inner:not([data-type="default"]):not([data-type="url"]):not([data-type="number"]):not([data-type="date"]):not([data-type="datetime"]) {
|
||||
.property-value-inner:not([data-type="default"]):not([data-type="url"]):not([data-type="number"]):not([data-type="string"]):not([data-type="date"]):not([data-type="datetime"]) {
|
||||
@apply cursor-pointer;
|
||||
&:hover, .as-scalar-value-wrap:hover {
|
||||
@apply bg-gray-02 rounded transition-[background-color] duration-300;
|
||||
@@ -41,6 +41,11 @@
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.ls-string {
|
||||
@apply cursor-text;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.ls-repeat-task-frequency .property-value-inner {
|
||||
@apply border rounded pl-2;
|
||||
min-width: 3em;
|
||||
|
||||
@@ -111,8 +111,8 @@
|
||||
(str result-count (if (> result-count 1) " results" " result"))])]))
|
||||
|
||||
(defn- calculate-collapsed?
|
||||
[current-block current-block-uuid {:keys [collapsed?]}]
|
||||
(let [temp-collapsed? (state/sub-block-collapsed current-block-uuid)
|
||||
[current-block current-block-uuid {:keys [collapsed? container-id]}]
|
||||
(let [temp-collapsed? (state/sub-block-collapsed current-block-uuid container-id)
|
||||
collapsed?' (if (some? temp-collapsed?)
|
||||
temp-collapsed?
|
||||
(or collapsed?
|
||||
@@ -185,7 +185,9 @@
|
||||
(:block/uuid config))
|
||||
current-block (db/entity [:block/uuid current-block-uuid])
|
||||
;; Get query result
|
||||
collapsed?' (calculate-collapsed? current-block current-block-uuid {:collapsed? false})
|
||||
collapsed?' (calculate-collapsed? current-block current-block-uuid
|
||||
{:collapsed? false
|
||||
:container-id (:container-id config)})
|
||||
built-in-collapsed? (and collapsed? built-in-query?)
|
||||
config' (assoc config
|
||||
:current-block current-block
|
||||
|
||||
@@ -1649,9 +1649,10 @@
|
||||
(when (and (get-in table [:data-fns :add-new-object!]) (or (empty? rows) items-rendered?))
|
||||
(shui/table-footer (add-new-row (:view-entity option) table)))]]))))
|
||||
|
||||
(rum/defc list-view < rum/static
|
||||
[{:keys [config ref-matched-children-ids disable-virtualized?] :as option} view-entity {:keys [rows]} *scroller-ref]
|
||||
(let [lazy-item-render (fn [rows idx]
|
||||
(rum/defcs list-view < rum/static mixins/container-id
|
||||
[state {:keys [config ref-matched-children-ids disable-virtualized?] :as option} view-entity {:keys [rows]} *scroller-ref]
|
||||
(let [config (assoc config :container-id (:container-id state))
|
||||
lazy-item-render (fn [rows idx]
|
||||
(lazy-item rows idx (assoc option :list-view? true)
|
||||
(fn [block]
|
||||
(let [config' (cond->
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
(state/<invoke-db-worker :thread-api/get-property-values (state/get-current-repo)
|
||||
(assoc opts :property-ident property-id))))
|
||||
|
||||
(defn <get-bidirectional-properties
|
||||
[target-id]
|
||||
(when target-id
|
||||
(state/<invoke-db-worker :thread-api/get-bidirectional-properties (state/get-current-repo)
|
||||
{:target-id target-id})))
|
||||
|
||||
(defn <get-block
|
||||
[graph id-uuid-or-name & {:keys [children? include-collapsed-children? skip-transact? skip-refresh? properties]
|
||||
:or {children? true}
|
||||
|
||||
@@ -3452,6 +3452,7 @@
|
||||
(or (:block/_parent block) (:block.temp/has-children? block))
|
||||
(integer? (:block-level config))
|
||||
(>= (:block-level config) (state/get-ref-open-blocks-level)))
|
||||
(:default-collapsed? config)
|
||||
(and (or (:view? config) (:popup? config))
|
||||
(or (ldb/page? block)
|
||||
(:table-block-title? config))))))
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
:logseq.property/exclude-from-graph-view :logseq.property/template-applied-to
|
||||
:logseq.property/hide-empty-value :logseq.property.class/hide-from-node
|
||||
:logseq.property/page-tags :logseq.property.class/extends
|
||||
:logseq.property.class/bidirectional-property-title
|
||||
:logseq.property.class/enable-bidirectional?
|
||||
:logseq.property/publishing-public? :logseq.property.user/avatar
|
||||
:logseq.property.user/email :logseq.property.user/name})
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
;; 2. zoom-in view
|
||||
;; 3. queries
|
||||
;; 4. references
|
||||
;; graph => {:block-id bool}
|
||||
;; graph => {container-id {:block-id bool}}
|
||||
:ui/collapsed-blocks {}
|
||||
:ui/sidebar-collapsed-blocks {}
|
||||
:ui/root-component nil
|
||||
@@ -1924,23 +1924,37 @@ Similar to re-frame subscriptions"
|
||||
(->> (sub :sidebar/blocks)
|
||||
(filter #(= (first %) current-repo)))))
|
||||
|
||||
(defn get-current-editor-container-id
|
||||
[]
|
||||
@(:editor/container-id @state))
|
||||
|
||||
(defn- resolve-container-id
|
||||
[container-id]
|
||||
(or container-id (get-current-editor-container-id) :unknown-container))
|
||||
|
||||
(defn toggle-collapsed-block!
|
||||
[block-id]
|
||||
(let [current-repo (get-current-repo)]
|
||||
(update-state! [:ui/collapsed-blocks current-repo block-id] not)))
|
||||
([block-id] (toggle-collapsed-block! block-id nil))
|
||||
([block-id container-id]
|
||||
(let [current-repo (get-current-repo)
|
||||
container-id (resolve-container-id container-id)]
|
||||
(update-state! [:ui/collapsed-blocks current-repo container-id block-id] not))))
|
||||
|
||||
(defn set-collapsed-block!
|
||||
[block-id value]
|
||||
(let [current-repo (get-current-repo)]
|
||||
(set-state! [:ui/collapsed-blocks current-repo block-id] value)))
|
||||
([block-id value] (set-collapsed-block! block-id value nil))
|
||||
([block-id value container-id]
|
||||
(let [current-repo (get-current-repo)
|
||||
container-id (resolve-container-id container-id)]
|
||||
(set-state! [:ui/collapsed-blocks current-repo container-id block-id] value))))
|
||||
|
||||
(defn sub-block-collapsed
|
||||
[block-id]
|
||||
(sub [:ui/collapsed-blocks (get-current-repo) block-id]))
|
||||
([block-id] (sub-block-collapsed block-id nil))
|
||||
([block-id container-id]
|
||||
(sub [:ui/collapsed-blocks (get-current-repo) (resolve-container-id container-id) block-id])))
|
||||
|
||||
(defn get-block-collapsed
|
||||
[block-id]
|
||||
(get-in @state [:ui/collapsed-blocks (get-current-repo) block-id]))
|
||||
([block-id] (get-block-collapsed block-id nil))
|
||||
([block-id container-id]
|
||||
(get-in @state [:ui/collapsed-blocks (get-current-repo) (resolve-container-id container-id) block-id])))
|
||||
|
||||
(defn get-modal-id
|
||||
[]
|
||||
@@ -2048,10 +2062,6 @@ Similar to re-frame subscriptions"
|
||||
id))
|
||||
(get-next-container-id)))
|
||||
|
||||
(defn get-current-editor-container-id
|
||||
[]
|
||||
@(:editor/container-id @state))
|
||||
|
||||
(comment
|
||||
(defn remove-container-key!
|
||||
[key]
|
||||
|
||||
@@ -185,7 +185,8 @@
|
||||
["65.16" {:properties [:logseq.property.asset/external-file-name]}]
|
||||
["65.17" {:properties [:logseq.property.publish/published-url]}]
|
||||
["65.18" {:fix deprecated-ensure-graph-uuid}]
|
||||
["65.19" {:properties [:logseq.property/choice-classes :logseq.property/choice-exclusions]}]])
|
||||
["65.19" {:properties [:logseq.property/choice-classes :logseq.property/choice-exclusions]}]
|
||||
["65.20" {:properties [:logseq.property.class/bidirectional-property-title :logseq.property.class/enable-bidirectional?]}]])
|
||||
|
||||
(let [[major minor] (last (sort (map (comp (juxt :major :minor) db-schema/parse-schema-version first)
|
||||
schema-version->updates)))]
|
||||
|
||||
@@ -704,6 +704,12 @@
|
||||
(let [conn (worker-state/get-datascript-conn repo)]
|
||||
(db-view/get-property-values @conn property-ident option)))
|
||||
|
||||
(def-thread-api :thread-api/get-bidirectional-properties
|
||||
[repo {:keys [target-id]}]
|
||||
(let [conn (worker-state/get-datascript-conn repo)]
|
||||
(worker-util/profile "get-bidirectional-properties"
|
||||
(ldb/get-bidirectional-properties @conn target-id))))
|
||||
|
||||
(def-thread-api :thread-api/build-graph
|
||||
[repo option]
|
||||
(let [conn (worker-state/get-datascript-conn repo)]
|
||||
|
||||
@@ -18,4 +18,4 @@ fom = "fom"
|
||||
tne = "tne"
|
||||
Damon = "Damon"
|
||||
[files]
|
||||
extend-exclude = ["resources/*", "src/resources/*", "scripts/resources/*", "src/test/fixtures/*", "clj-e2e/resources/*"]
|
||||
extend-exclude = ["resources/*", "src/resources/*", "scripts/resources/*", "src/test/fixtures/*", "clj-e2e/resources/*", "deps/common/src/logseq/common/plural.cljs"]
|
||||
|
||||
Reference in New Issue
Block a user