From 19450bcc778993d720b0d169c87ec08c8ebb5387 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 24 Apr 2026 00:02:53 +0800 Subject: [PATCH] enhance(ux): bottom properties --- .../src/logseq/outliner/property.cljs | 54 ++- .../test/logseq/outliner/property_test.cljs | 20 ++ src/main/frontend/components/block.cljs | 87 +++-- src/main/frontend/components/block.css | 50 ++- src/main/frontend/components/property.cljs | 310 +++++++++++------- src/main/frontend/components/property.css | 100 +++++- .../frontend/components/property/value.cljs | 31 +- src/main/frontend/handler/editor.cljs | 2 +- 8 files changed, 457 insertions(+), 197 deletions(-) diff --git a/deps/outliner/src/logseq/outliner/property.cljs b/deps/outliner/src/logseq/outliner/property.cljs index 8886ad272d..a42751db4e 100644 --- a/deps/outliner/src/logseq/outliner/property.cljs +++ b/deps/outliner/src/logseq/outliner/property.cljs @@ -767,23 +767,37 @@ (= "logseq.property.class" property-ns) (contains? db-property/schema-properties property-id)))) +(defn- tag-class-page? + [db block] + (or (= :logseq.class/Tag (:db/ident block)) + (and db + (ldb/class-instance? (entity-plus/entity-memoized db :logseq.class/Tag) block)))) + (defn- bottom-position-property? - [property] + [db block property] (let [property-id (:db/ident property) property-type (:logseq.property/type property)] - (and (not= :url property-type) - (or (not= :default property-type) - (seq (:property/closed-values property))) - (not (schema-or-tag-related-property? property-id))))) + (if (tag-class-page? db block) + (or (contains? #{:logseq.property.class/extends + :logseq.property.class/enable-bidirectional?} + property-id) + (and (not= :url property-type) + (or (not= :default property-type) + (seq (:property/closed-values property))) + (not (schema-or-tag-related-property? property-id)))) + (and (not= :url property-type) + (or (not= :default property-type) + (seq (:property/closed-values property))) + (not (schema-or-tag-related-property? property-id)))))) (defn- resolved-property-position - [property] + [db block property] (let [ui-position (:logseq.property/ui-position property)] (cond (contains? #{:block-left :block-right :block-below} ui-position) ui-position - (bottom-position-property? property) + (bottom-position-property? db block property) :block-below :else @@ -792,7 +806,7 @@ (defn- property-with-position? [db property-id block position] (when-let [property (entity-plus/entity-memoized db property-id)] - (let [property-position' (resolved-property-position property)] + (let [property-position' (resolved-property-position db block property)] (and (= property-position' position) (not (and (:logseq.property/hide-empty-value property) @@ -800,20 +814,28 @@ (not (:logseq.property/hide? property)) (not (and (= property-position' :block-below) - (nil? (get block property-id)))))))) + (nil? (get block property-id)) + (not (tag-class-page? db block)))))))) (defn property-with-other-position? - [property] - (not= :properties (resolved-property-position property))) + [db block property] + (not= :properties (resolved-property-position db block property))) (defn get-block-positioned-properties [db eid position] (let [block (d/entity db eid) - own-properties (:block.temp/property-keys block)] - (->> (:classes-properties (get-block-classes-properties db eid)) - (map :db/ident) - (concat own-properties) - (distinct) + own-properties (:block.temp/property-keys block) + tag-page-pill-properties (when (tag-class-page? db block) + [:logseq.property.class/extends + :logseq.property.class/enable-bidirectional?]) + positioned-property-ids (if (tag-class-page? db block) + (->> (concat own-properties tag-page-pill-properties) + distinct) + (->> (:classes-properties (get-block-classes-properties db eid)) + (map :db/ident) + (concat own-properties) + distinct))] + (->> positioned-property-ids (filter (fn [id] (property-with-position? db id block position))) (map #(d/entity db %)) (ldb/sort-by-order)))) diff --git a/deps/outliner/test/logseq/outliner/property_test.cljs b/deps/outliner/test/logseq/outliner/property_test.cljs index 2a6053c88d..575fffa350 100644 --- a/deps/outliner/test/logseq/outliner/property_test.cljs +++ b/deps/outliner/test/logseq/outliner/property_test.cljs @@ -350,6 +350,8 @@ (testing "explicit non-properties ui-position remains other-position" (is (true? (outliner-property/property-with-other-position? + nil + {} {:db/ident :user.property/p1 :logseq.property/type :number :logseq.property/ui-position :block-left})))) @@ -357,12 +359,16 @@ (testing "number property without explicit ui-position defaults to bottom position" (is (true? (outliner-property/property-with-other-position? + nil + {} {:db/ident :user.property/p1 :logseq.property/type :number})))) (testing "default property without closed values stays in normal property rows" (is (false? (outliner-property/property-with-other-position? + nil + {} {:db/ident :user.property/p1 :logseq.property/type :default :property/closed-values []})))) @@ -370,6 +376,8 @@ (testing "default property with closed values is positioned in bottom row" (is (true? (outliner-property/property-with-other-position? + nil + {} {:db/ident :user.property/p1 :logseq.property/type :default :property/closed-values [{:db/id 1}]})))) @@ -377,12 +385,16 @@ (testing "url property without explicit ui-position stays in normal property rows" (is (false? (outliner-property/property-with-other-position? + nil + {} {:db/ident :user.property/p1 :logseq.property/type :url})))) (testing "explicit left/right ui-position remains in positioned rows" (is (true? (outliner-property/property-with-other-position? + nil + {} {:db/ident :user.property/p1 :logseq.property/type :url :logseq.property/ui-position :block-left})))) @@ -390,20 +402,28 @@ (testing "bidirectional config property stays in normal property rows" (is (false? (outliner-property/property-with-other-position? + nil + {} {:db/ident :logseq.property.class/enable-bidirectional? :logseq.property/type :checkbox})))) (testing "tag properties and schema-related properties stay in normal property rows" (is (false? (outliner-property/property-with-other-position? + nil + {} {:db/ident :block/tags :logseq.property/type :class}))) (is (false? (outliner-property/property-with-other-position? + nil + {} {:db/ident :logseq.property.class/properties :logseq.property/type :property}))) (is (false? (outliner-property/property-with-other-position? + nil + {} {:db/ident :logseq.property/public? :logseq.property/type :checkbox})))) diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 0627ef4364..914d32f7f2 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -2585,49 +2585,64 @@ (rum/defc block-positioned-properties [config block position] (let [properties (outliner-property/get-block-positioned-properties (db/get-db) (:db/id block) position) + has-viewable-properties? (seq properties) opts (merge config {:icon? true :page-cp page-cp :block-cp blocks-container :inline-text inline-text :other-position? true - :property-position position})] - (when (seq properties) - (case position + :property-position position}) + show-page-hidden-properties-toggle? (and (ldb/page? block) + (not config/publishing?)) + show-hidden-properties-toggle? (and has-viewable-properties? + show-page-hidden-properties-toggle? + (property-component/has-hidden-properties? block config)) + show-page-add-property? (and (ldb/page? block) + (not (ldb/class? block)) + (not config/publishing?)) + show-add-property-button? (and has-viewable-properties? + show-page-add-property?)] + (case position :block-below - [:div.positioned-properties.block-below.flex.flex-row.gap-2.items-center.flex-wrap.text-sm.overflow-x-hidden - (for [[idx property] (map-indexed vector properties)] - [:div.bottom-property-pair.flex.flex-row.items-center.gap-1 - {:key (str (:db/id block) "-" (:db/id property))} - [:div.flex.flex-row.items-center - (property-component/property-key-cp block property opts) - [:div.select-none ":"]] - [:div.bottom-property-content.ls-block.property-value-container - {:style {:min-height 20}} - (pv/property-value block property opts) - (when (and (contains? #{:date :number} (:logseq.property/type property)) - (not config/publishing?)) - [:button.bottom-property-edit-icon.select-none - {:type "button" - :on-click (fn [e] - (util/stop e) - (when-let [trigger - (some-> (.-currentTarget e) - (.closest ".bottom-property-content") - (.querySelector ".jtrigger"))] - (.click trigger) - (some-> trigger .focus)))} - (ui/icon "edit" {:size 14})])] - (when (< idx (dec (count properties))) - [:div.bottom-property-separator.select-none ","])]) - (when (and (ldb/page? block) (not config/publishing?)) - (property-component/hidden-properties-toggle-button block))] - [:div.positioned-properties.flex.flex-row.gap-1.select-none.h-6.self-start - {:class (name position)} - (for [property properties] - (rum/with-key - (pv/property-value block property (assoc opts :show-tooltip? true)) - (str (:db/id block) "-" (:db/id property))))])))) + (when has-viewable-properties? + [:div.positioned-properties.block-below.flex.flex-col.gap-1.text-sm.overflow-x-hidden + [:div.bottom-properties-row.flex.flex-row.gap-2.items-center.flex-wrap.overflow-x-hidden + (for [property properties] + [:div.bottom-property-pill + {:key (str (:db/id block) "-" (:db/id property))} + [:div.flex.flex-row.items-center + (property-component/property-key-cp block property opts) + [:div.select-none ":"]] + [:div.bottom-property-content.ls-block.property-value-container + {:style {:min-height 20}} + (pv/property-value block property opts) + (when (contains? #{:number :date :datetime} (:logseq.property/type property)) + [:button.bottom-property-edit-icon.select-none + {:type "button" + :on-click (fn [e] + (util/stop e) + (when-let [trigger + (some-> (.-currentTarget e) + (.closest ".bottom-property-content") + (.querySelector ".jtrigger"))] + (.click trigger) + (some-> trigger .focus)))} + (ui/icon "edit" {:size 14})])]]) + (when show-hidden-properties-toggle? + (property-component/hidden-properties-toggle-button block {:icon-only? true})) + (when show-add-property-button? + (property-component/new-property block (assoc opts + :property-position :block-below + :icon-only? true)))]]) + + (when (seq properties) + [:div.positioned-properties.flex.flex-row.gap-1.select-none.h-6.self-start + {:class (name position)} + (for [property properties] + (rum/with-key + (pv/property-value block property (assoc opts :show-tooltip? true)) + (str (:db/id block) "-" (:db/id property))))])))) (rum/defc block-reactions < rum/reactive db-mixins/query [block] diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css index 9e6311186c..32a54c5e1c 100644 --- a/src/main/frontend/components/block.css +++ b/src/main/frontend/components/block.css @@ -1059,29 +1059,58 @@ html.is-mac { @apply flex; } - .property-k { - color: var(--ls-primary-text-color); + .bottom-properties-row { + @apply gap-1.5 py-1; } - .bottom-property-pair { - @apply min-w-0; + .bottom-property-pill { + @apply inline-flex items-center gap-1 rounded-full px-2 py-0.5 h-6 max-w-full; + background-color: var(--ls-secondary-background-color); + box-shadow: inset 0 0 0 1px var(--ls-border-color); } .bottom-property-content { @apply flex items-center gap-1 min-w-0; } - .bottom-property-separator { - @apply text-muted-foreground; + .bottom-property-pill .property-key-inner { + @apply flex items-center min-w-0; + } + + .bottom-property-pill .property-k { + @apply w-auto flex-none whitespace-nowrap leading-[20px]; + max-width: none; + color: var(--ls-primary-text-color); + } + + .bottom-property-pill a.property-k, + .bottom-property-pill .property-key-inner a { + color: var(--ls-primary-text-color); + } + + .ls-new-property { + @apply m-0; + } + + .ls-new-property .jtrigger, + .bottom-property-control-btn { + @apply inline-flex items-center rounded-full px-1.5 py-0.5 h-6 text-xs text-muted-foreground; + box-shadow: inset 0 0 0 1px var(--ls-border-color); } .bottom-property-edit-icon { - @apply text-muted-foreground inline-flex items-center justify-center p-0 bg-transparent border-0 cursor-pointer; + @apply inline-flex items-center justify-center p-0 bg-transparent border-0 text-muted-foreground cursor-pointer; } .ls-block.property-value-container { min-width: 0; - flex-shrink: 1; + flex: 0 1 auto; + width: auto; + min-height: 20px; + } + + .property-value { + @apply pl-0 min-h-[20px]; } .property-value-inner, @@ -1096,6 +1125,11 @@ html.is-mac { @apply self-center; vertical-align: middle; } + + .bullet-container, + .property-icon { + display: none; + } } .block-tags { diff --git a/src/main/frontend/components/property.cljs b/src/main/frontend/components/property.cljs index 892272f802..98e8eb20f2 100644 --- a/src/main/frontend/components/property.cljs +++ b/src/main/frontend/components/property.cljs @@ -3,7 +3,6 @@ (:require [clojure.set :as set] [clojure.string :as string] [frontend.components.dnd :as dnd] - [frontend.components.icon :as icon-component] [frontend.components.property.config :as property-config] [frontend.components.property.value :as pv] [frontend.components.select :as select] @@ -307,48 +306,13 @@ (db-property/built-in-display-title property t))) (rum/defc property-key-cp < rum/static - [block property {:keys [other-position? class-schema?]}] - (let [icon (:logseq.property/icon property)] - [:div.property-key-inner.jtrigger-view - ;; icon picker - (when-not other-position? - (let [content-fn (fn [{:keys [id]}] - (icon-component/icon-search - {:on-chosen - (fn [_e icon] - (if icon - (db-property-handler/set-block-property! (:db/id property) - :logseq.property/icon icon) - (db-property-handler/remove-block-property! (:db/id property) - :logseq.property/icon)) - (shui/popup-hide! id)) - :icon-value icon - :del-btn? (boolean icon)}))] - - [:div.property-icon - (shui/trigger-as - :button.property-m - (-> (when-not config/publishing? - {:on-click (fn [^js e] - (shui/popup-show! (.-target e) content-fn - {:as-dropdown? true :auto-focus? true - :content-props {:onEscapeKeyDown #(.preventDefault %)}}))}) - (assoc :class "flex items-center")) - (if icon - (icon-component/icon icon {:size 15 :color? true}) - (property-icon property nil)))])) - - (if config/publishing? - [:a.property-k.flex.select-none.jtrigger - {:on-click #(route-handler/redirect-to-page! (:block/uuid property))} - (db-property/built-in-display-title property t)] - (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}))) + [block property {:keys [class-schema?]}] + [:div.property-key-inner.jtrigger-view + (if config/publishing? + [:a.property-k.flex.select-none.jtrigger + {:on-click #(route-handler/redirect-to-page! (:block/uuid property))} + (db-property/built-in-display-title property t)] + (property-key-title block property class-schema?))]) (rum/defcs bidirectional-values-cp < rum/static {:init (fn [state] @@ -370,11 +334,9 @@ [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-pair.property-panel-row {:key (str "bidirectional-" title)} + [:div.property-key-panel [: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] @@ -382,8 +344,11 @@ (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 + [:div.ls-block.property-value-container.property-value-panel + [:div.property-panel-bullet {:aria-hidden true} + [:span.bullet-container + [:span.bullet]]] + [:div.property-value.property-value-panel-inner.flex.flex-1 (bidirectional-values-cp entities)]]]))) (rum/defcs ^:large-vars/cleanup-todo property-input < rum/reactive @@ -481,21 +446,25 @@ (rum/defcs new-property < rum/reactive [state block opts] (when-not config/publishing? - (let [add-new-property! (fn [e] + (let [icon-only? (:icon-only? opts) + add-new-property! (fn [e] (state/pub-event! [:editor/new-property (merge opts {:block block :target (.-target e)})]))] - [:div.ls-new-property {:style {:margin-left 7 :margin-top 1 :font-size 15}} - [:a.flex.jtrigger - {:tab-index 0 - :on-click add-new-property! - :on-key-press (fn [e] - (when (contains? #{"Enter" " "} (util/ekey e)) - (.preventDefault e) - (add-new-property! e)))} - [:div.flex.flex-row.items-center.shrink-0 - (ui/icon "plus" {:size 15 :class "opacity-50"}) - [:div.ml-1 {:style {:margin-top 1}} - (t :property/add-new)]]]]))) + [:div.ls-new-property + (shui/button + {:variant :outline + :size :small + :class "jtrigger flex !px-2 !py-1" + :tab-index 0 + :title (t :property/add-new) + :on-click add-new-property! + :on-key-press (fn [e] + (when (contains? #{"Enter" " "} (util/ekey e)) + (.preventDefault e) + (add-new-property! e)))} + (ui/icon "plus") + (when-not icon-only? + (t :property/add-new)))]))) (defn- resolve-linked-block-if-exists "Properties will be updated for the linked page instead of the refed block. @@ -511,59 +480,64 @@ (db/sub-block (:db/id linked-block)) (db/sub-block (:db/id block)))) +(defn- empty-panel-property-value? + [value] + (or (nil? value) + (and (map? value) + (= :logseq.property/empty-placeholder (:db/ident value))) + (and (coll? value) + (or (empty? value) + (every? (fn [item] + (and (map? item) + (= :logseq.property/empty-placeholder (:db/ident item)))) + value))))) + (rum/defc property-cp < rum/reactive db-mixins/query - [block k v {:keys [inline-text page-cp sortable-opts] :as opts}] + [block k v {:keys [sortable-opts] :as opts}] (when (keyword? k) (when-let [property (db/sub-block (:db/id (db/entity k)))] (let [type (get property :logseq.property/type :default) - closed-values? (seq (:property/closed-values property)) - block? (and v - (not closed-values?) - (or (and (map? v) (:block/page v)) - (and (coll? v) - (map? (first v)) - (or (:block/page (first v)) - (= :logseq.property/empty-placeholder (:db/ident (first v)))))) - (contains? #{:default :url} type)) - date? (= type :date) - datetime? (= type :datetime) - checkbox? (= type :checkbox) - number-type? (= type :number) - property-key-cp' (property-key-cp block property (assoc (select-keys opts [:class-schema?]) - :block? block? - :inline-text inline-text - :page-cp page-cp))] + default-or-url? (contains? #{:default :url} type) + empty-value? (empty-panel-property-value? v) + show-panel-bullet? (or (not default-or-url?) + empty-value?) + property-key-cp' (property-key-cp block property (select-keys opts [:class-schema?]))] [:div {:key (str "property-pair-" (:db/id block) "-" (:db/id property)) - :class (cond - (or date? datetime? checkbox? number-type?) - "property-pair items-center" - :else - "property-pair items-start") + :class (util/classnames ["property-pair property-panel-row" + {:property-panel-row-empty empty-value?}]) :data-property-title (:block/title property) :data-property-type (name type)} (if (seq sortable-opts) - (dnd/sortable-item (assoc sortable-opts :class "property-key") property-key-cp') - [:div.property-key property-key-cp']) + (dnd/sortable-item + (assoc sortable-opts :class "property-key-panel") + property-key-cp') + [:div.property-key-panel + property-key-cp']) - (let [property-desc (when-not (= (:db/ident property) :logseq.property/description) - (:logseq.property/description property)) - block' (assoc block (:db/ident property) v)] - [:div.ls-block.property-value-container.flex.flex-row.gap-1 - {:class (if (contains? #{:checkbox :date :datetime} type) - "items-center" - "items-start")} - - (when-not (or block? (and property-desc (:class-schema? opts))) - [:div.flex.items-center {:style {:height 28}} - [:div {:class "pl-1.5 -mr-[3px] opacity-60"} - [:span.bullet-container [:span.bullet]]]]) - [:div.flex.flex-1 - [:div.property-value.flex.flex-1 - (if (:class-schema? opts) - (pv/property-value property (db/entity :logseq.property/description) opts) - (pv/property-value block' property opts))]]])])))) + (let [block' (assoc block (:db/ident property) v)] + [:div.ls-block.property-value-container.property-value-panel + (when show-panel-bullet? + [:div.property-panel-bullet {:aria-hidden true} + [:span.bullet-container + [:span.bullet]]]) + [:div.property-value.property-value-panel-inner.flex.flex-1 + (if (:class-schema? opts) + (pv/property-value property (db/entity :logseq.property/description) opts) + (pv/property-value block' property (assoc opts :suppress-inline-edit-icon? true)))] + (when (contains? #{:number :date :datetime} type) + [:button.property-panel-edit-btn.select-none + {:type "button" + :on-click (fn [e] + (util/stop e) + (when-let [trigger + (some-> (.-currentTarget e) + (.closest ".property-value-panel") + (.querySelector ".jtrigger"))] + (.click trigger) + (some-> trigger .focus)))} + (ui/icon "edit" {:size 14})])])])))) (defn- entity-ref-value? [value] @@ -696,24 +670,120 @@ [block-uuid] (contains? @*show-hidden-properties-block-ids block-uuid)) +(defn has-hidden-properties? + [block {:keys [gallery-view?]}] + (let [current-db (db/get-db) + show-empty-and-hidden-state (some-> @state/state + (get :ui/show-empty-and-hidden-properties?) + deref) + {:keys [mode show? ids]} show-empty-and-hidden-state + show-empty-and-hidden-properties? (and show? + (or (= mode :global) + (and (set? ids) (contains? ids (:block/uuid 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))) + {:keys [properties recycled-only-property-ids]} + (sanitize-property-values-for-display properties*) + remove-built-in-or-other-position-properties + (fn [property-pairs show-in-hidden-properties?] + (remove (fn [property] + (let [id (if (vector? property) (first property) property)] + (or + (= id :block/tags) + (when-let [ent (db/entity id)] + (or + ;; built-in + (and (not (ldb/public-built-in-property? ent)) + (ldb/built-in? ent)) + ;; other position + (when-not (or + show-empty-and-hidden-properties? + show-in-hidden-properties?) + (outliner-property/property-with-other-position? current-db block ent)) + (and gallery-view? + (contains? #{:logseq.property.class/properties} (:db/ident ent)))))))) + property-pairs)) + {:keys [all-classes classes-properties]} (outliner-property/get-block-classes-properties current-db (:db/id block)) + classes-properties-set (set (map :db/ident classes-properties)) + block-own-properties (->> properties + (remove (fn [[id _]] (contains? recycled-only-property-ids id))) + (remove (fn [[id _]] (classes-properties-set id)))) + state-hide-empty-properties? (:ui/hide-empty-properties? (state/get-config)) + hide-with-property-id (fn [property-id] + (let [property (db/entity property-id)] + (boolean + (cond + show-empty-and-hidden-properties? + false + state-hide-empty-properties? + (nil? (get properties property-id)) + (and (:logseq.property/hide-empty-value property) + (nil? (get properties property-id))) + true + :else + (boolean (:logseq.property/hide? property)))))) + property-hide-f (cond + config/publishing? + ;; Publishing is read only so hide all blank properties as they + ;; won't be edited and distract from properties that have values + (fn [[property-id property-value]] + (or (nil? property-value) + (hide-with-property-id property-id))) + state-hide-empty-properties? + (fn [[property-id property-value]] + ;; User's selection takes precedence over config + (if (:logseq.property/hide? (db/entity property-id)) + (hide-with-property-id property-id) + (nil? property-value))) + :else + (comp hide-with-property-id first)) + {block-hidden-properties true + block-own-properties' false} (group-by property-hide-f block-own-properties) + class-properties (loop [classes all-classes + existing-properties (set (map first block-own-properties')) + result []] + (if-let [class (first classes)] + (let [cur-properties (->> (db-property/get-class-ordered-properties class) + (map :db/ident) + (remove existing-properties))] + (recur (rest classes) + (set/union existing-properties (set cur-properties)) + (if (seq cur-properties) + (into result cur-properties) + result))) + result)) + class-property-pairs (->> class-properties + (map (fn [p] [p (get properties p)])) + (remove (fn [[property-id _]] + (contains? recycled-only-property-ids property-id)))) + hidden-properties (-> (concat block-hidden-properties + (filter property-hide-f class-property-pairs)) + (remove-built-in-or-other-position-properties true))] + (boolean (seq hidden-properties)))) + (rum/defc hidden-properties-toggle-button - [block] + [block {:keys [icon-only?] :as _opts}] (let [block-uuid (:block/uuid block)] (when block-uuid (shui/button {:variant :ghost :size :sm - :class "px-1 text-muted-foreground h-6 text-xs" + :class "bottom-property-control-btn px-1 text-muted-foreground h-6 text-xs" + :title (t :property/hidden-properties) :on-click (fn [e] (util/stop e) (toggle-hidden-properties-visibility! block-uuid))} - (t :property/hidden-properties))))) + (if icon-only? + [:span.select-none "⋯"] + (t :property/hidden-properties)))))) (rum/defc hidden-properties-cp [block hidden-properties {:keys [show-hidden-properties?] :as opts}] (when (and show-hidden-properties? (seq hidden-properties)) - [:div.mt-1 - (properties-section block hidden-properties opts)])) + (properties-section block hidden-properties opts))) (rum/defc load-bidirectional-properties < rum/static [block root-block? set-bidirectional-properties!] @@ -737,6 +807,7 @@ (let [*bidirectional-properties (::bidirectional-properties state) bidirectional-properties @*bidirectional-properties id (::id state) + current-db (db/get-db) db-id (:db/id (::block state)) block (db/sub-block db-id) show-properties? (or sidebar-properties? tag-dialog?) @@ -768,7 +839,7 @@ (when-not (or show-empty-and-hidden-properties? show-in-hidden-properties?) - (outliner-property/property-with-other-position? ent)) + (outliner-property/property-with-other-position? current-db block ent)) (and (:gallery-view? opts) (contains? #{:logseq.property.class/properties} (:db/ident ent)))))))) @@ -846,15 +917,12 @@ (not has-bidirectional-properties?)) nil - (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)))) + show-properties-panel? (or (seq properties') (seq bidirectional-properties)) page? (entity-util/page? block) class? (entity-util/class? block)] [:div.ls-properties-area @@ -862,16 +930,15 @@ :class (util/classnames [{:ls-page-properties page?}]) :tab-index 0} [:<> - (properties-section block properties' opts) - (bidirectional-properties-section bidirectional-properties) + (when show-properties-panel? + [:div.properties-panel + (properties-section block properties' opts) + (bidirectional-properties-section bidirectional-properties)]) (when-not class? (hidden-properties-cp block hidden-properties (assoc opts :show-hidden-properties? show-hidden-properties?))) - (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)]))) @@ -883,8 +950,9 @@ (property-key-cp block (db/entity :logseq.property.class/properties) {})]] [:div.text-muted-foreground {:style {:margin-left 26}} (t :class/tag-properties-desc)]] - [:div.ml-4 + [:div.ml-4.gap-1.flex.flex-col (properties-section block properties opts') (hidden-properties-cp block hidden-properties (assoc opts :show-hidden-properties? show-hidden-properties?)) - (rum/with-key (new-property block opts') (str id "-class-add-property"))]]))]])))])) + [:div.ml-3 + (rum/with-key (new-property block opts') (str id "-class-add-property"))]]]))]])))])) diff --git a/src/main/frontend/components/property.css b/src/main/frontend/components/property.css index 8efe9d199e..bf12ab8ac0 100644 --- a/src/main/frontend/components/property.css +++ b/src/main/frontend/components/property.css @@ -62,9 +62,70 @@ } .ls-properties-area { - .property-pair { - @apply flex flex-row flex-wrap space-x-1; + .properties-panel { + @apply rounded-md overflow-hidden; + } + .properties-panel-header { + @apply px-3 py-2 text-xs font-medium text-muted-foreground border-b border-gray-04; + } + + .property-pair.property-panel-row { + @apply grid items-start px-3 gap-3 w-full; + grid-template-columns: fit-content(260px) minmax(0, 1fr); + margin-left: 0; + } + + .property-key-panel { + @apply flex items-center min-w-0; + min-height: 28px; + min-width: 150px; + font-size: inherit; + color: var(--ls-primary-text-color); + } + + .property-key-panel .property-key-inner { + @apply w-full min-w-0; + } + + .property-key-panel .property-k { + @apply block min-w-0 max-w-full overflow-hidden text-ellipsis whitespace-nowrap; + } + + .property-value-panel.ls-block.property-value-container { + @apply flex flex-row items-start gap-1 w-full; + min-width: 0; + flex-shrink: 1; + min-height: 28px; + } + + .property-pair.property-panel-row.property-panel-row-empty .property-value-panel.ls-block.property-value-container { + @apply items-center; + } + + .property-pair.property-panel-row[data-property-type=node] .property-value-panel.ls-block.property-value-container, + .property-pair.property-panel-row[data-property-type=node] .property-value.property-value-panel-inner { + @apply items-center; + } + + .property-pair.property-panel-row .property-icon { + display: none; + } + + .property-panel-bullet { + @apply inline-flex items-center opacity-60; + margin-left: 2px; + } + + .property-panel-bullet .bullet-container { + @apply relative left-0 top-0; + } + + .property-panel-edit-btn { + @apply inline-flex items-center justify-center p-0 bg-transparent border-0 text-muted-foreground cursor-pointer; + } + + .property-pair { .jtrigger-view { > .jtrigger-id { @apply left-[-11px]; @@ -122,6 +183,39 @@ } } + .property-pair.property-panel-row .property-k { + @apply leading-[20px]; + font-size: inherit; + color: var(--ls-primary-text-color); + } + + .property-pair.property-panel-row a.property-k, + .property-key-panel a { + color: var(--ls-primary-text-color); + } + + .property-value.property-value-panel-inner { + @apply pl-0 min-h-[20px] w-full min-w-0; + color: var(--ls-primary-text-color); + } + + .property-value.property-value-panel-inner > .property-value-inner { + @apply w-full min-w-0; + } + + .property-value.property-value-panel-inner .jtrigger { + @apply w-full min-w-0; + } + + .property-value.property-value-panel-inner .select-item { + @apply min-w-0; + } + + .property-pair.property-panel-row[data-property-type=node] .property-value.property-value-panel-inner .page-reference, + .property-pair.property-panel-row[data-property-type=node] .property-value.property-value-panel-inner .page-ref { + line-height: 20px; + } + .add-button-link { opacity: 0.5; } @@ -474,4 +568,4 @@ a.control-link { @apply py-1; } } -} \ No newline at end of file +} diff --git a/src/main/frontend/components/property/value.cljs b/src/main/frontend/components/property/value.cljs index 6090e50f57..4ddd2c375e 100644 --- a/src/main/frontend/components/property/value.cljs +++ b/src/main/frontend/components/property/value.cljs @@ -431,7 +431,7 @@ :else nil))) (rum/defc datetime-value - [value property-id repeated-task?] + [value property-id repeated-task? {:keys [datetime? other-position? suppress-inline-edit-icon?]}] (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)] @@ -442,15 +442,17 @@ :label (human-date-label value)} {:block/name page-title}) page-title))) - (let [date (js/Date. value) - hours (.getHours date) - minutes (.getMinutes date)] - [:span.select-none - (if (= 0 hours minutes) - (ui/icon "edit" {:size 14 :class "text-muted-foreground hover:text-foreground align-middle"}) - (str (util/zero-pad hours) - ":" - (util/zero-pad minutes)))])]] + (when datetime? + (let [date (js/Date. value) + hours (.getHours date) + minutes (.getMinutes date)] + [:span.select-none + (if (= 0 hours minutes) + (when-not (or other-position? suppress-inline-edit-icon?) + (ui/icon "edit" {:size 14 :class "text-muted-foreground hover:text-foreground align-middle"})) + (str (util/zero-pad hours) + ":" + (util/zero-pad minutes)))]))]] (if (or repeated-task? (contains? #{:logseq.property/deadline :logseq.property/scheduled} property-id)) (overdue date content) content)))) @@ -462,7 +464,7 @@ (:db/ident property))) (rum/defc date-picker - [value {:keys [block property datetime? on-change on-delete del-btn? editing? multiple-values? other-position?]}] + [value {:keys [block property datetime? on-change on-delete del-btn? editing? multiple-values? other-position? suppress-inline-edit-icon?]}] (let [*el (hooks/use-ref nil) content-fn (fn [{:keys [id]}] (calendar-inner id {:block block @@ -526,7 +528,12 @@ content)) (number? value) - (datetime-value value (:db/ident property) repeated-task?) + (datetime-value value + (:db/ident property) + repeated-task? + {:datetime? datetime? + :other-position? other-position? + :suppress-inline-edit-icon? suppress-inline-edit-icon?}) :else (property-empty-btn-value nil))]))))) diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index 1074916840..49b8a02a47 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -3039,7 +3039,7 @@ (remove (fn [e] (attributes (:db/ident e)))) (remove (fn [k] (when-not page-title? - (outliner-property/property-with-other-position? k)))) + (outliner-property/property-with-other-position? db block k)))) (remove (fn [e] (:logseq.property/hide? e))) (remove nil?))] (or (seq properties)