(ns frontend.components.property.value (:require [cljs-time.coerce :as tc] [cljs-time.core :as t] [cljs-time.local :as local] [clojure.set :as set] [clojure.string :as string] [datascript.impl.entity :as de] [dommy.core :as d] [frontend.components.icon :as icon-component] [frontend.components.select :as select] [frontend.config :as config] [frontend.date :as date] [frontend.db :as db] [frontend.db-mixins :as db-mixins] [frontend.db.async :as db-async] [frontend.db.model :as model] [frontend.handler.block :as block-handler] [frontend.handler.db-based.page :as db-page-handler] [frontend.handler.db-based.property :as db-property-handler] [frontend.handler.editor :as editor-handler] [frontend.handler.page :as page-handler] [frontend.handler.property :as property-handler] [frontend.handler.property.util :as pu] [frontend.handler.route :as route-handler] [frontend.hooks :as hooks] [frontend.modules.outliner.ui :as ui-outliner-tx] [frontend.search :as search] [frontend.state :as state] [frontend.ui :as ui] [frontend.util :as util] [goog.dom :as gdom] [goog.functions :refer [debounce]] [lambdaisland.glogi :as log] [logseq.common.util.macro :as macro-util] [logseq.db :as ldb] [logseq.db.frontend.entity-util :as entity-util] [logseq.db.frontend.property :as db-property] [logseq.db.frontend.property.type :as db-property-type] [logseq.outliner.property :as outliner-property] [logseq.shui.ui :as shui] [promesa.core :as p] [rum.core :as rum])) (rum/defc property-empty-btn-value [property & opts] (let [text (cond (= (:db/ident property) :logseq.property/description) "Add description" :else "Empty")] (if (= text "Empty") (shui/button (merge {:class "empty-btn" :variant :text} opts) text) (shui/button (merge {:class "empty-btn !text-base" :variant :text} opts) text)))) (rum/defc property-empty-text-value [property {:keys [property-position]}] [:span.inline-flex.items-center.cursor-pointer (merge {:class "empty-text-btn" :variant :text}) (if property-position (if-let [icon (:logseq.property/icon property)] (icon-component/icon icon {:color? true}) (ui/icon "line-dashed")) "Empty")]) (rum/defc icon-row [block editing?] (let [icon-value (:logseq.property/icon block) clear-overlay! (fn [] (shui/dialog-close!) (shui/popup-hide-all!)) on-chosen! (fn [_e icon] (if icon (db-property-handler/set-block-property! (:db/id block) :logseq.property/icon (select-keys icon [:type :id :color])) (db-property-handler/remove-block-property! (:db/id block) :logseq.property/icon)) (clear-overlay!))] (hooks/use-effect! (fn [] (when editing? (clear-overlay!) (let [^js container (or (some-> js/document.activeElement (.closest ".page")) (gdom/getElement "main-content-container")) icon (get block (pu/get-pid :logseq.property/icon))] (util/schedule (fn [] (when-let [^js target (some-> (.querySelector container (str "#ls-block-" (str (:block/uuid block)))) (.querySelector ".block-main-container"))] (shui/popup-show! target #(icon-component/icon-search {:on-chosen on-chosen! :icon-value icon :del-btn? (some? icon)}) {:id :ls-icon-picker :align :start}))))))) [editing?]) [:div.col-span-3.flex.flex-row.items-center.gap-2 (icon-component/icon-picker icon-value {:disabled? config/publishing? :del-btn? (some? icon-value) :on-chosen on-chosen!})])) (defn- select-type? [block property type] (or (contains? #{:node :number :date :page :class :property} type) ;; closed values (seq (:property/closed-values property)) (and (= (:db/ident property) :logseq.property/default-value) (= (:logseq.property/type block) :number)))) (defn > (state/get-selection-block-ids) (map (fn [id] (db/entity [:block/uuid id]))) (seq) block-handler/get-top-level-blocks (remove ldb/property?))] (or (seq selected-blocks) [block]))) (defn > (outliner-property/get-block-full-properties (db/get-db) (:db/id block)) (filter (fn [property] (and (not (ldb/built-in? property)) (>= (count (:property/closed-values property)) 2)))) (concat [(db/entity :logseq.task/status)]) (util/distinct-by :db/id)) status-property (or (:logseq.task/recur-status-property block) (db/entity :logseq.task/status)) property-id (:db/id status-property) done-choice (or (some (fn [choice] (when (true? (:logseq.property/choice-checkbox-state choice)) choice)) (:property/closed-values status-property)) (db/entity :logseq.task/status.done))] [:div.flex.flex-col.gap-2 [:div.text-muted-foreground "When"] (shui/select (cond-> {:on-value-change (fn [v] (db-property-handler/set-block-property! (:db/id block) :logseq.task/recur-status-property v))} property-id (assoc :default-value property-id)) (shui/select-trigger (shui/select-value {:placeholder "Select a property"})) (shui/select-content (map (fn [choice] (shui/select-item {:key (str (:db/id choice)) :value (:db/id choice)} (:block/title choice))) properties))) [:div.flex.flex-row.gap-1 [:div.text-muted-foreground "is:"] (when done-choice (db-property/property-value-content done-choice))]])])) (rum/defcs calendar-inner < rum/reactive db-mixins/query (rum/local (str "calendar-inner-" (js/Date.now)) ::identity) {:init (fn [state] (state/set-editor-action! :property-set-date) state) :will-mount (fn [state] (js/setTimeout #(some-> @(::identity state) (js/document.getElementById) (.querySelector "[aria-selected=true]") (.focus)) 16) state) :will-unmount (fn [state] (shui/popup-hide!) (shui/dialog-close!) (state/set-editor-action! nil) state)} [state id {:keys [block property datetime? on-change del-btn? on-delete]}] (let [block (db/sub-block (:db/id block)) value (get block (:db/ident property)) value (cond (map? value) (when-let [day (:block/journal-day value)] (let [t (date/journal-day->utc-ms day)] (js/Date. t))) (number? value) (js/Date. value) :else (let [d (js/Date.)] (.setHours d 0 0 0) d)) *ident (::identity state) initial-day value initial-month (when value (let [d (tc/to-date-time value)] (js/Date. (t/last-day-of-the-month (t/date-time (t/year d) (t/month d)))))) select-handler! (fn [^js d] (when d (let [journal (date/js-date->journal-title d)] (p/do! (when-not (db/get-page journal) (page-handler/ {:initial-focus true :datetime? datetime? :selected initial-day :id @*ident :del-btn? del-btn? :on-delete on-delete :on-day-click select-handler!} initial-month (assoc :default-month initial-month)))] (shui/separator {:orientation "vertical"}) (repeat-setting block property)])) (rum/defc overdue [date content] (let [[current-time set-current-time!] (rum/use-state (t/now))] (hooks/use-effect! (fn [] (let [timer (js/setInterval (fn [] (set-current-time! (t/now))) (* 1000 60 3))] #(js/clearInterval timer))) []) (let [overdue? (when date (t/after? current-time (t/plus date (t/seconds 59))))] [:div (cond-> {} overdue? (assoc :class "overdue" :title "Overdue")) content]))) (defn- human-date-label [date] (let [given-date (date/start-of-day date) now (local/local-now) today (date/start-of-day now) tomorrow (t/plus today (t/days 1)) yesterday (t/minus today (t/days 1))] (cond (and (t/before? given-date today) (not (t/before? given-date yesterday))) "Yesterday" (and (not (t/before? given-date today)) (t/before? given-date tomorrow)) "Today" (and (not (t/before? given-date tomorrow)) (t/before? given-date (t/plus tomorrow (t/days 1)))) "Tomorrow" :else nil))) (rum/defc datetime-value [value property-id repeated-task?] (when-let [date (t/to-default-time-zone (tc/from-long value))] (let [content [:div.ls-datetime.flex.flex-row.gap-1.items-center (when-let [page-cp (state/get-component :block/page-cp)] (let [page-title (date/journal-name date)] (rum/with-key (page-cp {:disable-preview? true :show-non-exists-page? true :label (human-date-label date)} {:block/name page-title}) page-title))) (let [date (js/Date. value) hours (.getHours date) minutes (.getMinutes date)] [:span.select-none (str (util/zero-pad hours) ":" (util/zero-pad minutes))])]] (if (or repeated-task? (contains? #{:logseq.task/deadline :logseq.task/scheduled} property-id)) (overdue date content) content)))) (rum/defc date-picker [value {:keys [block property datetime? on-change on-delete del-btn? editing? multiple-values? other-position?]}] (let [*trigger-ref (rum/use-ref nil) content-fn (fn [{:keys [id]}] (calendar-inner id {:block block :property property :on-change on-change :value value :del-btn? del-btn? :on-delete on-delete :datetime? datetime?})) open-popup! (fn [e] (when-not (or (util/meta-key? e) (util/shift-key? e)) (util/stop e) (editor-handler/save-current-block!) (when-not config/publishing? (shui/popup-show! (.-target e) content-fn {:align "start" :auto-focus? true})))) repeated-task? (:logseq.task/repeated? block)] (hooks/use-effect! (fn [] (when editing? (js/setTimeout #(some-> (rum/deref *trigger-ref) (.click)) 32))) [editing?]) (if multiple-values? (shui/button {:class "jtrigger h-6 empty-btn" :ref *trigger-ref :variant :text :size :sm :on-click open-popup!} (ui/icon "calendar-plus" {:size 16})) (shui/trigger-as :div.flex.flex-1.flex-row.gap-1.items-center.flex-wrap {:tabIndex 0 :class "jtrigger min-h-[24px]" ; FIXME: min-h-6 not works :ref *trigger-ref :on-click open-popup!} [:div.flex.flex-row.gap-1.items-center (when repeated-task? (ui/icon "repeat" {:size 14 :class "opacity-40"})) (cond (map? value) (let [date (tc/to-date-time (date/journal-day->utc-ms (:block/journal-day value))) compare-value (some-> date (t/plus (t/days 1)) (t/minus (t/seconds 1))) content (when-let [page-cp (state/get-component :block/page-cp)] (rum/with-key (page-cp {:disable-preview? true :meta-click? other-position? :label (human-date-label (t/to-default-time-zone date))} value) (:db/id value)))] (if (or repeated-task? (contains? #{:logseq.task/deadline :logseq.task/scheduled} (:db/id property))) (overdue compare-value content) content)) (number? value) (datetime-value value (:db/ident property) repeated-task?) :else (property-empty-btn-value nil))])))) (rum/defc property-value-date-picker [block property value opts] (let [multiple-values? (db-property/many? property) repo (state/get-current-repo) datetime? (= :datetime (:logseq.property/type property))] (date-picker value (merge opts {:block block :property property :datetime? datetime? :multiple-values? multiple-values? :on-change (fn [value] (property-handler/set-block-property! repo (:block/uuid block) (:db/ident property) (if datetime? value (:db/id value)))) :del-btn? (some? value) :on-delete (fn [] (property-handler/set-block-property! repo (:block/uuid block) (:db/ident property) nil) (shui/popup-hide!))})))) (defn- > selected-choices (remove nil?) (remove #(= :logseq.property/empty-placeholder %))) clear-value (str "No " (:block/title property)) clear-value-label [:div.flex.flex-row.items-center.gap-2 (ui/icon "x") [:div clear-value]] items' (->> (if (and (seq selected-choices) (not multiple-choices?) (not (and (ldb/class? block) (= (:db/ident property) :logseq.property/parent))) (not= (:db/ident property) :logseq.property.view/type)) (concat items [{:value clear-value :label clear-value-label :clear? true}]) items) (remove #(= :logseq.property/empty-placeholder (:value %)))) k :on-chosen f (get opts k) f' (fn [chosen selected?] (if (or (and (not multiple-choices?) (= chosen clear-value)) (and multiple-choices? (= chosen [clear-value]))) (p/do! (let [blocks (get-operating-blocks block) block-ids (map :block/uuid blocks)] (property-handler/batch-remove-block-property! (state/get-current-repo) block-ids (:db/ident property))) (when-not (false? (:exit-edit? opts)) (shui/popup-hide!))) (f chosen selected?)))] (select/select (assoc opts :selected-choices selected-choices :items items' k f')))) (defn- get-node-icon [node] (cond (ldb/class? node) "hash" (ldb/property? node) "letter-p" (entity-util/page? node) "page" :else "letter-n")) (rum/defc ^:large-vars/cleanup-todo select-node < rum/static [property {:keys [block multiple-choices? dropdown? input-opts on-input] :as opts} result] (let [[refresh-count set-refresh-count!] (rum/use-state 0) repo (state/get-current-repo) classes (:logseq.property/classes property) tags? (= :block/tags (:db/ident property)) alias? (= :block/alias (:db/ident property)) tags-or-alias? (or tags? alias?) block (db/entity (:db/id block)) selected-choices (when block (when-let [v (get block (:db/ident property))] (if (every? de/entity? v) (map :db/id v) [(:db/id v)]))) parent-property? (= (:db/ident property) :logseq.property/parent) children-pages (when parent-property? (model/get-structured-children repo (:db/id block))) nodes (->> (cond parent-property? (let [;; Disallows cyclic hierarchies exclude-ids (-> (set (map (fn [id] (:block/uuid (db/entity id))) children-pages)) (conj (:block/uuid block))) ; break cycle options (if (ldb/class? block) (model/get-all-classes repo) (when (ldb/internal-page? block) (cond->> (->> (model/get-all-pages repo) (filter ldb/internal-page?) (remove ldb/built-in?))))) excluded-options (remove (fn [e] (contains? exclude-ids (:block/uuid e))) options)] excluded-options) (seq classes) (->> (mapcat (fn [class] (if (= :logseq.class/Root (:db/ident class)) (model/get-all-classes repo {:except-root-class? true}) (model/get-class-objects repo (:db/id class)))) classes) distinct) :else (let [property-type (:logseq.property/type property)] (if (empty? result) (let [v (get block (:db/ident property))] (remove #(= :logseq.property/empty-placeholder (:db/ident %)) (if (every? de/entity? v) v [v]))) (remove (fn [node] (or (= (:db/id block) (:db/id node)) ;; A page's alias can't be itself (and alias? (= (or (:db/id (:block/page block)) (:db/id block)) (:db/id node))) (when (and property-type (not= property-type :node)) (if (= property-type :page) (not (db/page? node)) (not (contains? (ldb/get-entity-types node) property-type)))))) result))))) options (map (fn [node] (let [id (or (:value node) (:db/id node)) [header label] (if (integer? id) (let [node-title (if (seq (:logseq.property/classes property)) (:block/title node) (block-handler/block-unique-title node)) title (subs node-title 0 256) node (or (db/entity id) node) icon (get-node-icon node) header (when-not (db/page? node) (when-let [breadcrumb (state/get-component :block/breadcrumb)] [:div.text-xs.opacity-70 (breadcrumb {:search? true} (state/get-current-repo) (:block/uuid block) {})])) label [:div.flex.flex-row.items-center.gap-1 (when-not (:logseq.property/classes property) (ui/icon icon {:size 14})) [:div title]]] [header label]) [nil (or (:label node) (:block/title node))])] (assoc node :header header :label-value (:block/title node) :label label :value id :disabled? (and tags? (contains? (set/union #{:logseq.class/Journal :logseq.class/Whiteboard} ldb/internal-tags) (:db/ident node)))))) nodes) classes' (remove (fn [class] (= :logseq.class/Root (:db/ident class))) classes) opts' (cond-> (merge opts {:multiple-choices? multiple-choices? :items options :selected-choices selected-choices :dropdown? dropdown? :input-default-placeholder (cond tags? "Set tags" alias? "Set alias" multiple-choices? "Choose nodes" :else "Choose node") :show-new-when-not-exact-match? (if (or (and parent-property? (contains? (set children-pages) (:db/id block))) ;; Don't allow creating private tags (seq (set/intersection (set (map :db/ident classes)) ldb/private-tags))) false true) :extract-chosen-fn :value :extract-fn (fn [x] (or (:label-value x) (:label x))) :input-opts input-opts :on-input (debounce on-input 50) :on-chosen (fn [chosen selected?] (p/let [[id new?] (if (integer? chosen) [chosen false] (when-not (string/blank? (string/trim chosen)) (p/let [result (> classes' (mapcat #(model/get-structured-children repo (:db/id %))) (map #(db/entity repo %)))] (->> (concat classes' descendent-classes) (filter #(string/includes? (:block/title %) class-input)) (mapv (fn [p] {:value (str new-page "#" (:block/title p)) :label (str new-page "#" (:block/title p))})))) results))))] (select-aux block property opts'))) (rum/defc property-value-select-node < rum/static [block property opts {:keys [*show-new-property-config?]}] (let [[result set-result!] (rum/use-state nil) input-opts (fn [_] {:on-click (fn [] (when *show-new-property-config? (reset! *show-new-property-config? false))) :on-key-down (fn [e] (case (util/ekey e) "Escape" (when-let [f (:on-chosen opts)] (f)) nil))}) opts' (assoc opts :block block :input-opts input-opts :on-input (fn [v] (if (string/blank? v) (set-result! nil) (p/let [result (search/block-search (state/get-current-repo) v {:enable-snippet? false :built-in? false})] (set-result! result))))) repo (state/get-current-repo) classes (:logseq.property/classes property) non-root-classes (remove (fn [c] (= (:db/ident c) :logseq.class/Root)) classes) parent-property? (= (:db/ident property) :logseq.property/parent)] (when (and (not parent-property?) (seq non-root-classes)) ;; effect runs once (hooks/use-effect! (fn [] (p/let [result (p/all (map (fn [class] (db-async/> (:property/closed-values property) date? (remove (fn [b] (contains? #{:logseq.task/recur-unit.minute :logseq.task/recur-unit.hour} (:db/ident b)))))] (keep (fn [block] (let [icon (pu/get-block-property-value block :logseq.property/icon) value (db-property/closed-value-content block)] {:label (if icon [:div.flex.flex-row.gap-1.items-center (icon-component/icon icon {:color? true}) value] value) :value (:db/id block) :label-value value})) values)) (->> values (mapcat (fn [value] (if (coll? value) (map (fn [v] {:value v}) value) [{:value value}]))) (map (fn [{:keys [value]}] (if (and ref-type? (number? value)) (when-let [e (db/entity value)] {:label (db-property/property-value-content e) :value value}) {:label value :value value}))) (distinct))) items (->> (if (= :date type) (map (fn [m] (let [label (:block/title (db/entity (:value m)))] (when label (assoc m :label label)))) items) items) (remove nil?)) on-chosen (fn [chosen selected?] (let [value (if (map? chosen) (:value chosen) chosen)] (add-or-remove-property-value block property value selected? {:exit-edit? exit-edit? :refresh-result-f refresh-result-f}))) selected-choices' (get block (:db/ident property)) selected-choices (if (every? de/entity? selected-choices') (map :db/id selected-choices') [selected-choices'])] (select-aux block property {:multiple-choices? multiple-choices? :items items :selected-choices selected-choices :dropdown? dropdown? :show-new-when-not-exact-match? (not (or closed-values? (= :date type))) :input-default-placeholder "Select" :extract-chosen-fn :value :extract-fn (fn [x] (or (:label-value x) (:label x))) :content-props content-props :on-chosen on-chosen :input-opts (fn [_] {:on-blur (fn [] (when-let [f (:on-chosen select-opts)] (f))) :on-click (fn [] (when *show-new-property-config? (reset! *show-new-property-config? false))) :on-key-down (fn [e] (case (util/ekey e) "Escape" (when-let [f (:on-chosen select-opts)] (f)) nil))})}))))) (rum/defcs property-normal-block-value < {:init (fn [state] (assoc state :container-id (state/get-next-container-id)))} [state block property value-block] (let [container-id (:container-id state) multiple-values? (db-property/many? property) block-container (state/get-component :block/container) blocks-container (state/get-component :block/blocks-container) value-block (if (and (coll? value-block) (every? de/entity? value-block)) (set (remove #(= (:db/ident %) :logseq.property/empty-placeholder) value-block)) value-block) default-value (:logseq.property/default-value property) default-value? (and (:db/id default-value) (= (:db/id value-block) (:db/id default-value)) (not= (:db/ident property) :logseq.property/default-value))] (if (seq value-block) [:div.property-block-container.content.w-full (let [config {:id (str (if multiple-values? (:block/uuid block) (:block/uuid value-block))) :container-id container-id :editor-box (state/get-component :editor/box) :property-block? true :on-block-content-pointer-down (when default-value? (fn [_e] ( (set (map :db/id (:logseq.property/checkbox-display-properties block))) (contains? (:db/id property)))) (conj classes block)) (seq (:property/closed-values property)) (boolean? (:logseq.property/choice-checkbox-state value*)))] (if display-as-checkbox? (let [checked? (:logseq.property/choice-checkbox-state value*)] (shui/checkbox {:checked checked? :class "mt-1" :on-checked-change (fn [value] (let [choices (:property/closed-values property) choice (some (fn [choice] (when (= value (:logseq.property/choice-checkbox-state choice)) choice)) choices)] (when choice (db-property-handler/set-block-property! (:db/id block) (:db/ident property) (:db/id choice)))))})) (single-value-select block property value (fn [] (select-item property type value opts)) select-opts (assoc opts :editing? editing?)))) (case type (:date :datetime) (property-value-date-picker block property value (merge opts {:editing? editing?})) :checkbox (let [add-property! (fn [] (let [value' (boolean (not value))] (> (if (de/entity? v) #{v} v) (= (:db/ident property) :block/tags) (remove (fn [v] (contains? ldb/hidden-tags (:db/ident v)))))] (hooks/use-effect! (fn [] (when editing? (.click (rum/deref *el)))) [editing?]) (let [select-cp (fn [select-opts] (let [select-opts (merge {:multiple-choices? true :on-chosen (fn [] (when on-chosen (on-chosen)))} select-opts {:dropdown? false})] [:div.property-select (if (contains? #{:node :page :class :property} type) (property-value-select-node block property select-opts opts) (select block property select-opts opts))]))] (let [toggle-fn shui/popup-hide! content-fn (fn [{:keys [_id content-props]}] (select-cp {:content-props content-props}))] [:div.multi-values.jtrigger {:tab-index "0" :ref *el :on-click (fn [^js e] (let [target (.-target e)] (when-not (or (util/link? target) (.closest target "a") config/publishing?) (shui/popup-show! (rum/deref *el) content-fn {:as-dropdown? true :as-content? false :align "start" :auto-focus? true})))) :on-key-down (fn [^js e] (case (.-key e) (" " "Enter") (do (some-> (rum/deref *el) (.click)) (util/stop e)) :dune)) :class "flex flex-1 flex-row items-center flex-wrap gap-x-2 gap-y-2"} (let [not-empty-value? (not= (map :db/ident items) [:logseq.property/empty-placeholder])] (if (and (seq items) not-empty-value?) (concat (->> (for [item items] (rum/with-key (select-item property type item opts) (or (:block/uuid item) (str item)))) (interpose [:span.opacity-50.-ml-2 ","])) (when date? [(property-value-date-picker block property nil {:toggle-fn toggle-fn})])) (if date? (property-value-date-picker block property nil {:toggle-fn toggle-fn}) (property-empty-text-value property opts))))])))) (rum/defc multiple-values < rum/reactive db-mixins/query [block property opts] (let [block (db/sub-block (:db/id block)) value (get block (:db/ident property)) value' (if (coll? value) value (when (some? value) #{value}))] (multiple-values-inner block property value' opts))) (rum/defcs property-value < rum/reactive db-mixins/query [state block property {:keys [show-tooltip?] :as opts}] (ui/catch-error (ui/block-error "Something wrong" {}) (let [block (db/sub-block (:db/id block)) block-cp (state/get-component :block/blocks-container) properties-cp (state/get-component :block/properties-cp) opts (merge opts {:page-cp (state/get-component :block/page-cp) :inline-text (state/get-component :block/inline-text) :editor-box (state/get-component :editor/box) :block-cp block-cp :properties-cp :properties-cp}) dom-id (str "ls-property-" (:db/id block) "-" (:db/id property)) editor-id (str dom-id "-editor") type (:logseq.property/type property) multiple-values? (db-property/many? property) v (get block (:db/ident property)) v (cond (and multiple-values? (or (set? v) (and (coll? v) (empty? v)) (nil? v))) v multiple-values? #{v} (set? v) (first v) :else v) empty-value? (when (coll? v) (= :logseq.property/empty-placeholder (:db/ident (first v)))) closed-values? (seq (:property/closed-values property)) property-ident (:db/ident property) value-cp [:div.property-value-inner {:data-type type :class (str (when empty-value? "empty-value") (when-not (:other-position? opts) " w-full"))} (cond (= property-ident :logseq.property.class/properties) (properties-cp {} block {:selected? false :class-schema? true}) (and multiple-values? (contains? #{:default :url} type) (not closed-values?)) (property-normal-block-value block property v) multiple-values? (multiple-values block property opts) :else (let [parent? (= property-ident :logseq.property/parent) value-cp (property-scalar-value block property v (merge opts {:editor-id editor-id :dom-id dom-id})) page-ancestors (when parent? (let [ancestor-pages (loop [parents [block]] (if-let [parent (:logseq.property/parent (last parents))] (when-not (contains? (set parents) parent) (recur (conj parents parent))) parents))] (->> (reverse ancestor-pages) (remove (fn [e] (= (:db/id block) (:db/id e)))) butlast)))] (if (seq page-ancestors) [:div.flex.flex-1.items-center.gap-1 (interpose [:span.opacity-50.text-sm " > "] (concat (map (fn [{title :block/title :as ancestor}] [:a.whitespace-nowrap {:on-click #(route-handler/redirect-to-page! (:block/uuid ancestor))} title]) page-ancestors) [value-cp]))] value-cp)))]] (if show-tooltip? (shui/tooltip-provider (shui/tooltip {:delayDuration 1200} (shui/tooltip-trigger {:onFocusCapture #(util/stop-propagation %)} value-cp) (shui/tooltip-content (str "Change " (:block/title property))))) value-cp))))