(ns frontend.components.views "Different views of blocks" (:require [cljs-bean.core :as bean] [cljs-time.coerce :as tc] [cljs-time.core :as t] [cljs-time.format :as tf] [clojure.set :as set] [clojure.string :as string] [datascript.impl.entity :as de] [dommy.core :as dom] [frontend.common.missionary :as c.m] [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] [frontend.components.selection :as selection] [frontend.config :as config] [frontend.context.i18n :refer [t]] [frontend.date :as date] [frontend.db :as db] [frontend.db-mixins :as db-mixins] [frontend.db.async :as db-async] [frontend.db.react :as react] [frontend.handler.db-based.export :as db-export-handler] [frontend.handler.db-based.property :as db-property-handler] [frontend.handler.editor :as editor-handler] [frontend.handler.property :as property-handler] [frontend.handler.property.util :as pu] [frontend.handler.route :as route-handler] [frontend.handler.ui :as ui-handler] [frontend.mixins :as mixins] [frontend.modules.outliner.op :as outliner-op] [frontend.modules.outliner.ui :as ui-outliner-tx] [frontend.state :as state] [frontend.ui :as ui] [frontend.util :as util] [goog.dom :as gdom] [logseq.common.config :as common-config] [logseq.db :as ldb] [logseq.db.common.view :as db-view] [logseq.db.frontend.property :as db-property] [logseq.shui.hooks :as hooks] [logseq.shui.ui :as shui] [medley.core :as medley] [missionary.core :as m] [promesa.core :as p] [rum.core :as rum])) (defn- get-scroll-parent [config] (if (:sidebar? config) (dom/sel1 ".sidebar-item-list") (gdom/getElement "main-content-container"))) (rum/defc header-checkbox < rum/static [{:keys [selected-all? selected-some? toggle-selected-all!] :as table}] (let [[show? set-show!] (rum/use-state false)] [:label.h-8.w-8.flex.items-center.justify-center.cursor-pointer {:html-for "header-checkbox" :on-mouse-over #(set-show! true) :on-mouse-out #(set-show! false)} (shui/checkbox {:id "header-checkbox" :checked (or selected-all? (and selected-some? "indeterminate")) :on-checked-change (fn [value] (p/do (when value (db-async/ (shui/dropdown-menu-item {:key "asc" :on-click #(column-set-sorting! sorting column true)} [:div.flex.flex-row.items-center.gap-1 (ui/icon "arrow-up" {:size 15}) [:div "Sort ascending"]]) (shui/dropdown-menu-item {:key "desc" :on-click #(column-set-sorting! sorting column false)} [:div.flex.flex-row.items-center.gap-1 (ui/icon "arrow-down" {:size 15}) [:div "Sort descending"]]) (when property (shui/dropdown-menu-item {:on-click #(shui/popup-show! (.-target %) (fn [] [:div.ls-property-dropdown-editor.-m-1 (property-config/dropdown-editor property nil {})]) {:align "start"})} [:div.flex.flex-row.items-center.gap-1 (ui/icon "adjustments" {:size 15}) "Configure"])) (when (and db-based? property) (shui/dropdown-menu-item {:on-click (fn [_e] (if pinned? (db-property-handler/delete-property-value! (:db/id view-entity) :logseq.property.table/pinned-columns (:db/id property)) (property-handler/set-block-property! (state/get-current-repo) (:db/id view-entity) :logseq.property.table/pinned-columns (:db/id property))) (shui/popup-hide! id))} [:div.flex.flex-row.items-center.gap-1 (ui/icon "pin" {:size 15}) [:div (if pinned? "Unpin" "Pin")]]))])] (shui/button {:variant "text" :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start" :on-mouse-up (fn [^js e] (when-let [^js el (some-> (.-target e) (.closest "[aria-roledescription=sortable]"))] (when (and (or (nil? @*last-header-action-target) (not= el @*last-header-action-target)) (string/blank? (some-> el (.-style) (.-transform)))) (shui/popup-show! el sub-content {:align "start" :as-dropdown? true :on-before-hide (fn [] (reset! *last-header-action-target el) (js/setTimeout #(reset! *last-header-action-target nil) 128))}))))} (let [title (str (:name column))] [:span {:title title :class "max-w-full overflow-hidden text-ellipsis"} title]) (case asc? true (ui/icon "arrow-up") false (ui/icon "arrow-down") nil)))) (defn- timestamp-cell-cp [_table row column] (some-> (get row (:id column)) date/int->local-time-2)) (defn- get-property-value-content [entity] (db-view/get-property-value-content (db/get-db) entity)) (rum/defc block-container [config row] (let [container (state/get-component :block/container) config' (cond-> config (not (:popup? config)) (assoc :view? true))] [:div.relative.w-full {:style {:min-height 24}} (if row (container config' row) [:div])])) (rum/defc block-title "Used on table view" [block* {:keys [create-new-block width row property]}] (let [*ref (hooks/use-ref nil) [opacity set-opacity!] (hooks/use-state 0) [focus-timeout set-focus-timeout!] (hooks/use-state nil) inline-title (state/get-component :block/inline-title) many? (db-property/many? property) block (if many? (first block*) block*) add-to-sidebar! #(state/sidebar-add-block! (state/get-current-repo) (or (and many? (:db/id row)) (:db/id block)) :block) redirect! #(some-> (:block/uuid block) route-handler/redirect-to-page!)] (hooks/use-effect! (fn [] #(some-> focus-timeout js/clearTimeout)) []) [:div.table-block-title.relative.flex.items-center.w-full.h-full.cursor-pointer.items-center {:ref *ref :on-mouse-over #(set-opacity! 100) :on-mouse-out #(set-opacity! 0) :on-click (fn [e] (p/let [block (or block (and (fn? create-new-block) (create-new-block)))] (when block (cond (util/meta-key? e) (redirect!) (.-shiftKey e) (add-to-sidebar!) :else (let [popup (fn [] (let [width (-> (max 160 width) (- 18))] (if many? [:div.ls-table-block.flex.flex-row.items-start {:style {:width width :max-width width :margin-right "6px"} :on-click util/stop-propagation} (pv/property-value row property {})] [:div.ls-table-block.flex.flex-row.items-start {:style {:width width :max-width width :margin-right "6px"} :on-click util/stop-propagation} (block-container {:popup? true :view? true :table-block-title? true} block)])))] (p/do! (shui/popup-show! (.closest (.-target e) ".ls-table-cell") popup {:id :ls-table-block-editor :as-mask? true :on-after-hide (fn [] (let [node (rum/deref *ref) cell (util/rec-get-node node "ls-table-cell")] (p/do! (editor-handler/save-current-block!) (state/exit-editing-and-set-selected-blocks! [cell]) (set-focus-timeout! (js/setTimeout #(.focus cell) 100)))))}) (editor-handler/edit-block! block :max {:container-id :unknown-container})))))))} (if block [:div.flex.flex-row (let [render (fn [block] [:div (inline-title (some->> (:block/title block) string/trim string/split-lines first))])] (if many? (->> (map render block*) (interpose [:div.mr-1 ","])) (render block*)))] [:div]) (let [class (str "h-6 w-6 !p-1 text-muted-foreground transition-opacity duration-100 ease-in bg-gray-01 " "opacity-" opacity)] [:div.absolute.-right-1 [:div.flex.flex-row.items-center (shui/button {:variant :ghost :title "Open" :on-click (fn [e] (util/stop-propagation e) (redirect!)) :class class} (ui/icon "arrow-right")) (shui/button {:variant :ghost :title "Open in sidebar" :class class :on-click (fn [e] (util/stop-propagation e) (add-to-sidebar!))} (ui/icon "layout-sidebar-right"))]])])) (defn build-columns [config properties & {:keys [with-object-name? with-id? add-tags-column?] :or {with-object-name? true with-id? true add-tags-column? true}}] (let [;; FIXME: Shouldn't file graphs have :block/tags? add-tags-column?' (and (config/db-based-graph? (state/get-current-repo)) add-tags-column?) properties' (->> (if (or (some #(= (:db/ident %) :block/tags) properties) (not add-tags-column?')) properties (conj properties (db/entity :block/tags))) (remove nil?))] (->> (concat [{:id :select :name "Select" :header (fn [table _column] (header-checkbox table)) :cell (fn [table row column] (row-checkbox table row column)) :column-list? false :resizable? false} (when with-id? {:id :id :name "ID" :header (fn [_table _column] (header-index)) :cell (fn [table row _column] (inc (.indexOf (:rows table) (:db/id row)))) :resizable? false}) (when with-object-name? {:id :block/title :name "Name" :type :string :header header-cp :cell (fn [_table row _column style] (block-title row {:property-ident :block/title :sidebar? (:sidebar? config) :width (:width style)})) :disable-hide? true})] (keep (fn [property] (when-let [ident (or (:db/ident property) (:id property))] ;; Hide properties that shouldn't ever be editable or that do not display well in a table (when-not (or (contains? #{:logseq.property/built-in? :logseq.property.asset/checksum :logseq.property.class/properties :block/created-at :block/updated-at :block/order :block/collapsed? :logseq.property/created-from-property} ident) (and with-object-name? (= :block/title ident)) (contains? #{:map :entity} (:logseq.property/type property))) (let [property (if (de/entity? property) property (or (merge (db/entity ident) property) property)) ; otherwise, :cell/:header/etc. will be removed get-value (when (de/entity? property) (fn [row] (db-view/get-property-value-for-search row property)))] {:id ident :name (or (:name property) (:block/title property)) :header (or (:header property) header-cp) :cell (or (:cell property) (when (de/entity? property) (fn [_table row _column style] (pv/property-value row property {:view? true :table-view? true :table-text-property-render (fn [block opts] (block-title block (assoc opts :row row :property property :width (:width style) :sidebar? (:sidebar? config))))})))) :get-value get-value :type (:type property)})))) properties') [{:id :block/created-at :name (t :page/created-at) :type :datetime :header header-cp :cell timestamp-cell-cp} {:id :block/updated-at :name (t :page/updated-at) :type :datetime :header header-cp :cell timestamp-cell-cp}]) (remove nil?)))) (defn- sort-columns [columns ordered-column-ids] (if (seq ordered-column-ids) (let [id->columns (zipmap (map :id columns) columns) ordered-id-set (set ordered-column-ids)] (concat (keep (fn [id] (get id->columns id)) ordered-column-ids) (remove (fn [column] (ordered-id-set (:id column))) columns))) columns)) (rum/defc more-actions [view-entity columns {:keys [column-visible? rows column-toggle-visibility]} {:keys [group-by-property-ident]}] (let [display-type (:db/ident (:logseq.property.view/type view-entity)) table? (= display-type :logseq.property.view/type.table) group-by-columns (concat (when (or (contains? #{:linked-references :unlinked-references} (:logseq.property.view/feature-type view-entity)) (:logseq.property/query view-entity)) [{:id :block/page :name "Block Page"}]) (filter (fn [column] (when (:id column) (when-let [p (db/entity (:id column))] (and (not (db-property/many? p)) (contains? #{:default :number :checkbox :url :node :date} (:logseq.property/type p)))))) columns))] (shui/dropdown-menu (shui/dropdown-menu-trigger {:asChild true} (shui/button {:variant "ghost" :class "text-muted-foreground !px-1" :size :sm} (ui/icon "dots" {:size 15}))) (shui/dropdown-menu-content {:align "end"} (shui/dropdown-menu-group (when table? (shui/dropdown-menu-sub (shui/dropdown-menu-sub-trigger "Columns visibility") (shui/dropdown-menu-sub-content (for [column (remove #(or (false? (:column-list? %)) (:disable-hide? %)) columns)] (shui/dropdown-menu-checkbox-item {:key (str (:id column)) :className "capitalize" :checked (column-visible? column) :onCheckedChange #(column-toggle-visibility column %) :onSelect (fn [e] (.preventDefault e))} (:name column)))))) (when (seq group-by-columns) (shui/dropdown-menu-sub (shui/dropdown-menu-sub-trigger "Group by") (shui/dropdown-menu-sub-content (for [column group-by-columns] (shui/dropdown-menu-checkbox-item {:key (str (:id column)) :className "capitalize" :checked (= (:id column) group-by-property-ident) :onCheckedChange (fn [result] (if result (db-property-handler/set-block-property! (:db/id view-entity) :logseq.property.view/group-by-property (:db/id (db/entity (:id column)))) (db-property-handler/remove-block-property! (:db/id view-entity) :logseq.property.view/group-by-property))) :onSelect (fn [e] (.preventDefault e))} (:name column)))))) (shui/dropdown-menu-item {:key "export-edn" :on-click #(db-export-handler/export-view-nodes-data rows)} "Export EDN")))))) (defn- get-column-size [column sized-columns] (let [id (:id column) size (get sized-columns id)] (cond (= id :id) 48 (number? size) size (= id :logseq.property/query) 400 :else (case id :select 32 :add-property 160 (:block/title :block/name) 360 (:block/created-at :block/updated-at) 160 180)))) (rum/defc add-property-button < rum/static [] [:div.ls-table-header-cell.!border-0 (shui/button {:variant "text" :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"} (ui/icon "plus") "New property")]) (rum/defc action-bar < rum/static [table selected-rows {:keys [on-delete-rows]}] (shui/table-actions {} [:div (str (count selected-rows) " selected")] (selection/action-bar {:on-cut #(on-delete-rows table selected-rows) :selected-blocks selected-rows :hide-dots? true :button-border? true}))) (rum/defc column-resizer [_column on-sized!] (let [*el (rum/use-ref nil) [dx set-dx!] (rum/use-state nil) [width set-width!] (rum/use-state nil) add-resizing-class #(dom/add-class! js/document.documentElement "is-resizing-buf") remove-resizing-class #(dom/remove-class! js/document.documentElement "is-resizing-buf")] (hooks/use-effect! (fn [] (when (number? dx) (some-> (rum/deref *el) (dom/set-style! :transform (str "translate3D(" dx "px , 0, 0)"))))) [dx]) (hooks/use-effect! (fn [] (when-let [el (and (fn? js/window.interact) (rum/deref *el))] (let [*field-rect (atom nil) min-width 40 max-width 500] (-> (js/interact el) (.draggable (bean/->js {:listeners {:start (fn [] (let [{:keys [width right] :as rect} (bean/->clj (.toJSON (.getBoundingClientRect (.closest el ".ls-table-header-cell")))) left-dx (if (>= width min-width) (- min-width width) 0) right-dx (if (<= width max-width) (- max-width width) 0)] (reset! *field-rect rect) (swap! *field-rect assoc ;; calculate left/right boundary :left-dx left-dx :right-dx right-dx :left-b (inc (+ left-dx right)) :right-b (inc (+ right-dx right))) (dom/add-class! el "is-active"))) :move (fn [^js e] (let [dx (.-dx e) pointer-x (js/Math.floor (.-clientX e)) {:keys [left-b right-b]} @*field-rect left-b (js/Math.floor left-b) right-b (js/Math.floor right-b)] (when (and (> pointer-x left-b) (< pointer-x right-b)) (set-dx! (fn [dx'] (if (contains? #{min-width max-width} (abs dx')) dx' (let [to-dx (+ (or dx' 0) dx) {:keys [left-dx right-dx]} @*field-rect] (cond ;; left (neg? to-dx) (if (> (abs left-dx) (abs to-dx)) to-dx left-dx) ;; right (pos? to-dx) (if (> right-dx to-dx) to-dx right-dx))))))))) :end (fn [] (set-dx! (fn [dx] (let [w (js/Math.round (+ dx (:width @*field-rect)))] (set-width! (cond (< w min-width) min-width (> w max-width) max-width :else w))) (reset! *field-rect nil) (dom/remove-class! el "is-active") 0)))}})) (.styleCursor false) (.on "dragstart" add-resizing-class) (.on "dragend" remove-resizing-class) (.on "mousedown" util/stop-propagation))))) []) (hooks/use-effect! (fn [] (when (number? width) (on-sized! width))) [width]) [:a.ls-table-resize-handle {:data-no-dnd true :ref *el}])) (defn- table-header-cell [table column] (let [header-fn (:header column) sized-columns (get-in table [:state :sized-columns]) set-sized-columns! (get-in table [:data-fns :set-sized-columns!]) width (get-column-size column sized-columns) select? (= :select (:id column))] [:div.ls-table-header-cell {:style {:width width :min-width width} :class (when select? "!border-0")} (if (fn? header-fn) (header-fn table column) header-fn) ;; resize handle (when-not (false? (:resizable? column)) (column-resizer column (fn [size] (set-sized-columns! (assoc sized-columns (:id column) size)))))])) (defn- on-delete-rows [view-parent view-feature-type table selected-ids] (let [selected-rows (->> (map db/entity selected-ids) (remove :logseq.property/built-in?)) pages (filter ldb/page? selected-rows) blocks (remove ldb/page? selected-rows) page-ids (map :db/id pages) {:keys [set-data! set-row-selection!]} (:data-fns table) update-table-state! (fn [] (let [data (:full-data table) selected-ids (set (map :db/id selected-rows)) new-data (if (every? number? data) (remove selected-ids data) ;; group (map (fn [[by-value col]] [by-value (remove selected-ids col)]) data))] (set-data! new-data) (set-row-selection! {})))] (p/do! (ui-outliner-tx/transact! {:outliner-op :delete-blocks} (when (seq blocks) (outliner-op/delete-blocks! blocks nil)) (case view-feature-type :class-objects (when (seq page-ids) (let [tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id view-parent)]) page-ids)] (when (seq tx-data) (outliner-op/transact! tx-data {:outliner-op :save-block})))) :property-objects ;; Relationships with built-in properties must not be deleted e.g. built-in? or parent (when-not (:logseq.property/built-in? view-parent) (let [tx-data (map (fn [pid] [:db/retract pid (:db/ident view-parent)]) page-ids)] (when (seq tx-data) (outliner-op/transact! tx-data {:outliner-op :save-block})))) :query-result (doseq [page pages] (when-let [id (:block/uuid page)] (outliner-op/delete-page! id))) :all-pages (state/pub-event! [:page/show-delete-dialog selected-rows update-table-state!]) nil)) (when-not (or (= view-feature-type :all-pages) (and (= view-feature-type :property-objects) (:logseq.property/built-in? view-parent))) (update-table-state!))))) (defn- table-header [table {:keys [show-add-property? add-property! view-parent view-feature-type] :as option} selected-rows] (let [set-ordered-columns! (get-in table [:data-fns :set-ordered-columns!]) pinned (get-in table [:state :pinned-columns]) unpinned (get-in table [:state :unpinned-columns]) build-item (fn [column] {:id (:name column) :value (:id column) :content (table-header-cell table column) :disabled? (= (:id column) :select)}) pinned-items (mapv build-item pinned) unpinned-items (if show-add-property? (conj (mapv build-item unpinned) {:id "add property" :prop {:style {:width "-webkit-fill-available" :min-width 160} :on-click (fn [] (when (fn? add-property!) (add-property!)))} :value :add-new-property :content (add-property-button) :disabled? true}) (mapv build-item unpinned)) selection-rows-count (count selected-rows)] (shui/table-header (when (seq pinned-items) [:div.sticky-columns.flex.flex-row (dnd/items pinned-items {:vertical? false :on-drag-end (fn [ordered-columns _m] (set-ordered-columns! ordered-columns))})]) (when (seq unpinned-items) [:div.flex.flex-row (dnd/items unpinned-items {:vertical? false :on-drag-end (fn [ordered-columns _m] (set-ordered-columns! ordered-columns))})]) (when (pos? selection-rows-count) [:div.table-action-bar.absolute.top-0.left-8 (action-bar table selected-rows (assoc option :on-delete-rows (fn [table selected-ids] (on-delete-rows view-parent view-feature-type table selected-ids))))])))) (rum/defc lazy-table-cell [cell-render-f cell-placeholder] (let [^js state (ui/useInView #js {:rootMargin "0px"}) in-view? (.-inView state)] [:div.h-full {:ref (.-ref state)} (if in-view? (cell-render-f) cell-placeholder)])) (defn- click-cell [node] (when-let [trigger (or (dom/sel1 node ".jtrigger") (dom/sel1 node ".table-block-title"))] (.click trigger))) (defn navigate-to-cell [e cell direction] (util/stop e) (let [row (util/rec-get-node cell "ls-table-row") cells (dom/sel row ".ls-table-cell") idx (.indexOf cells cell) rows-container (util/rec-get-node row "ls-table-rows") rows (dom/sel rows-container ".ls-table-row") row-idx (.indexOf rows row) container-left (.-left (.getBoundingClientRect rows-container)) next-cell (case direction :left (if (> idx 1) ; don't focus on checkbox (nth cells (dec idx)) ;; last cell in the prev row (let [prev-row (when (> row-idx 0) (nth rows (dec row-idx)))] (when prev-row (let [cells (dom/sel prev-row ".ls-table-cell")] (last cells))))) :right (if (< idx (dec (count cells))) (nth cells (inc idx)) ;; first cell in the next row (let [next-row (when (< row-idx (dec (count rows))) (nth rows (inc row-idx)))] (when next-row (let [cells (dom/sel next-row ".ls-table-cell")] (second cells))))) :up (let [prev-row (when (> row-idx 0) (nth rows (dec row-idx)))] (when prev-row (let [cells (dom/sel prev-row ".ls-table-cell")] (nth cells idx)))) :down (let [next-row (when (< row-idx (dec (count rows))) (nth rows (inc row-idx)))] (when next-row (let [cells (dom/sel next-row ".ls-table-cell")] (nth cells idx)))))] (when next-cell (let [next-cell-left (.-left (.getBoundingClientRect next-cell))] (state/clear-selection!) (dom/add-class! next-cell "selected") (.focus next-cell) (when (< next-cell-left container-left) (.scrollIntoView next-cell #js {:inline "center" :block "nearest"})))))) (rum/defc table-cell-container [cell-opts body] (let [*ref (hooks/use-ref nil)] (shui/table-cell (assoc cell-opts :tabIndex 0 :ref *ref :on-click (fn [e] (when-not (dom/has-class? (.-target e) "jtrigger") (click-cell (rum/deref *ref)))) :on-key-down (fn [e] (let [container (rum/deref *ref)] (case (util/ekey e) "Escape" (do (if (util/input? (.-target e)) (do (state/exit-editing-and-set-selected-blocks! [container]) (.focus container)) (do (dom/remove-class! container "selected") (let [row (util/rec-get-node container "ls-table-row")] (state/exit-editing-and-set-selected-blocks! [row])))) (util/stop e)) "Enter" (do (if (util/input? (.-target e)) ; number (do (state/exit-editing-and-set-selected-blocks! [container]) (.focus container)) (click-cell container)) (util/stop e)) "ArrowUp" (navigate-to-cell e container :up) "ArrowDown" (navigate-to-cell e container :down) "ArrowLeft" (navigate-to-cell e container :left) "ArrowRight" (navigate-to-cell e container :right) nil)))) body))) (rum/defc table-row-inner < rum/static [{:keys [row-selected?] :as table} row props {:keys [show-add-property? scrolling?]}] (let [*ref (hooks/use-ref nil) pinned-columns (get-in table [:state :pinned-columns]) unpinned (get-in table [:state :unpinned-columns]) unpinned-columns (if show-add-property? (conj (vec unpinned) {:id :add-property :cell (fn [_table _row _column])}) unpinned) sized-columns (get-in table [:state :sized-columns]) row-cell-f (fn [column {:keys [_lazy?]}] (let [id (str (:id row) "-" (:id column)) width (get-column-size column sized-columns) select? (= (:id column) :select) add-property? (= (:id column) :add-property) style {:width width :min-width width} cell-opts {:key id :select? select? :add-property? add-property? :style style} cell-placeholder (table-cell-container cell-opts nil)] (if (and scrolling? (not (:block/title row))) cell-placeholder (when-let [render (get column :cell)] (lazy-table-cell (fn [] (table-cell-container cell-opts (render table row column style))) cell-placeholder)))))] (shui/table-row (merge props {:key (str (:db/id row)) :tabIndex 0 :ref *ref :data-state (when (row-selected? row) "selected") :data-id (:db/id row) :blockid (str (:block/uuid row)) :on-pointer-down (fn [_e] (db-async/> (dom/sel container ".ls-table-cell") (remove (fn [node] (some? (dom/sel1 node ".ui__checkbox")))) first)] (state/clear-selection!) (dom/add-class! cell "selected") (.focus cell)) (util/stop e)) "ArrowRight" (do (when-let [cell (->> (dom/sel container ".ls-table-cell") (remove (fn [node] (some? (dom/sel1 node ".ui__checkbox")))) last)] (state/clear-selection!) (dom/remove-class! container "selected") (dom/add-class! cell "selected") (.focus cell)) (util/stop e)) "Escape" (do (state/clear-selection!) (util/stop e)) nil))))}) (when (seq pinned-columns) [:div.sticky-columns.flex.flex-row (map #(row-cell-f % {}) pinned-columns)]) (when (seq unpinned-columns) [:div.flex.flex-row (map #(row-cell-f % {:lazy? true}) unpinned-columns)])))) (rum/defc table-row < rum/reactive db-mixins/query [table row props option] (let [block (db/sub-block (:db/id row)) row' (if (:block.temp/fully-loaded? block) (assoc block :block.temp/refs-count (:block.temp/refs-count row)) row)] (table-row-inner table row' props option))) (rum/defc search [input {:keys [on-change set-input!]}] (let [[show-input? set-show-input!] (rum/use-state false)] (if show-input? [:div.flex.flex-row.items-center (shui/input {:placeholder "Type to search" :auto-focus true :value input :on-change (fn [e] (let [value (util/evalue e)] (on-change value))) :on-key-down (fn [e] (when (= "Escape" (util/ekey e)) (set-show-input! false) (set-input! ""))) :class "max-w-sm !h-7 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"}) (shui/button {:variant "ghost" :class "text-muted-foreground !px-1" :size :sm :on-click #(do (set-show-input! false) (set-input! ""))} (ui/icon "x"))] (shui/button {:variant "ghost" ;; FIXME: remove ring when focused :class "text-muted-foreground !px-1" :size :sm :on-click #(set-show-input! true)} (ui/icon "search" {:size 15}))))) (defn datetime-property? [property] (or (= :datetime (:logseq.property/type property)) (contains? #{:block/created-at :block/updated-at} (:db/ident property)))) (def timestamp-options [{:value "1 day ago" :label "1 day ago"} {:value "3 days ago" :label "3 days ago"} {:value "1 week ago" :label "1 week ago"} {:value "1 month ago" :label "1 month ago"} {:value "3 months ago" :label "3 months ago"} {:value "1 year ago" :label "1 year ago"} {:value "Custom date" :label "Custom date"}]) (rum/defc filter-property < rum/static [view-entity columns {:keys [data-fns] :as table} opts] (let [[property set-property!] (rum/use-state nil) [values set-values!] (rum/use-state nil) schema (:schema (db/get-db)) timestamp? (datetime-property? property) set-filters! (:set-filters! data-fns) filters (get-in table [:state :filters]) columns (remove #(or (false? (:column-list? %)) (= :id (:id %))) columns) items (map (fn [column] {:label (:name column) :value column}) columns) option {:input-default-placeholder "Filter" :input-opts {:class "!px-2 !py-1"} :items items :extract-fn :label :extract-chosen-fn :value :on-chosen (fn [column] (let [id (:id column) property (db/entity id) internal-property {:db/ident (:id column) :block/title (:name column) :logseq.property/type (:type column)}] (if (or property (= :db.cardinality/many (:db/cardinality (get schema id))) (not= (:type column) :string)) (set-property! (or property internal-property)) (do (shui/popup-hide!) (let [property internal-property new-filter [(:db/ident property) :text-contains] filters' (if (seq (:filters filters)) (conj (:filters filters) new-filter) [new-filter])] (set-filters! {:or? (:or? filters) :filters filters'}))))))} checkbox? (= :checkbox (:logseq.property/type property)) property-ident (:db/ident property)] (hooks/use-effect! (fn [] (when (and view-entity property-ident (not (or timestamp? checkbox?))) (p/let [data (db-async/text [operator] (case operator :is "is" :is-not "is not" :text-contains "text contains" :text-not-contains "text not contains" :date-before "date before" :date-after "date after" :before "before" :after "after" :number-gt ">" :number-lt "<" :number-gte ">=" :number-lte "<=" :between "between")) (defn get-property-operators [property] (if (datetime-property? property) [:before :after] (concat [:is :is-not] (case (:logseq.property/type property) (:default :url :node) [:text-contains :text-not-contains] (:date) [:date-before :date-after] :number [:number-gt :number-lt :number-gte :number-lte :between] nil)))) (defn- get-filter-with-changed-operator [_property operator value] (case operator (:is :is-not) (when (set? value) value) (:text-contains :text-not-contains) (when (string? value) value) (:number-gt :number-lt :number-gte :number-lte) (when (number? value) value) :between (when (and (vector? value) (every? number? value)) value) (:date-before :date-after :before :after) ;; FIXME: should be a valid date number (when (number? value) value))) (rum/defc filter-operator < rum/static [property operator filters set-filters! idx] (shui/dropdown-menu (shui/dropdown-menu-trigger {:asChild true} (shui/button {:class "!px-2 rounded-none border-r" :variant "ghost" :size :sm} [:span.text-xs (operator->text operator)])) (shui/dropdown-menu-content {:align "start"} (let [operators (get-property-operators property)] (for [operator operators] (shui/dropdown-menu-item {:on-click (fn [] (let [new-filters (update filters :filters (fn [col] (update col idx (fn [[property _old-operator value]] (let [value' (get-filter-with-changed-operator property operator value)] (if value' [property operator value'] [property operator]))))))] (set-filters! new-filters)))} (operator->text operator))))))) (rum/defc between < rum/static [_property [start end] filters set-filters! idx] [:<> (shui/input {:auto-focus true :placeholder "from" :value (str start) :onChange (fn [e] (let [input-value (util/evalue e) number-value (when-not (string/blank? input-value) (util/safe-parse-float input-value)) value [number-value end] value (if (every? nil? value) nil value)] (let [new-filters (update filters :filters (fn [col] (update col idx (fn [[property operator _old_value]] (if (nil? value) [property operator] [property operator value])))))] (set-filters! new-filters)))) :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"}) (shui/input {:value (str end) :placeholder "to" :onChange (fn [e] (let [input-value (util/evalue e) number-value (when-not (string/blank? input-value) (util/safe-parse-float input-value)) value [start number-value] value (if (every? nil? value) nil value)] (let [new-filters (update filters :filters (fn [col] (update col idx (fn [[property operator _old_value]] (if (nil? value) [property operator] [property operator value])))))] (set-filters! new-filters)))) :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})]) (rum/defc ^:large-vars/cleanup-todo filter-value-select < rum/static [view-entity {:keys [data-fns] :as table} property value operator idx opts] (let [type (:logseq.property/type property) property-ident (:db/ident property)] (hooks/use-effect! (fn [] (let [values (if (coll? value) value [value]) ids (filter #(and (uuid? %) (nil? (db/entity [:block/uuid %]))) values)] (when (seq ids) (db-async/ {:input-default-placeholder (:block/title property) :input-opts {:class "!px-3 !py-1"} :items items :extract-fn :label :extract-chosen-fn :value :on-chosen (fn [value _selected? selected e] (when-not many? (shui/popup-hide!)) (let [value' (if many? selected value) set-filters-fn (fn [value'] (let [new-filters (update filters :filters (fn [col] (update col idx (fn [[property operator _value]] [property operator value']))))] (set-filters! new-filters)))] (if (= value "Custom date") (shui/popup-show! (.-target e) (ui/nlp-calendar {:initial-focus true :datetime? false :on-day-click (fn [value] (set-filters-fn value) (shui/popup-hide!))}) {}) (set-filters-fn value'))))} many? (assoc :multiple-choices? true :selected-choices value))] (select/select option))) {:align :start})))} (let [value (cond (uuid? value) (db/entity [:block/uuid value]) (instance? js/Date value) (some->> (tc/to-date value) (t/to-default-time-zone) (tf/unparse (tf/formatter "yyyy-MM-dd"))) (and (coll? value) (every? uuid? value)) (keep #(db/entity [:block/uuid %]) value) :else value)] [:div.flex.flex-row.items-center.gap-1.text-xs (cond (de/entity? value) [:div (get-property-value-content value)] (string? value) [:div value] (boolean? value) [:div (str value)] (seq value) (->> (map (fn [v] [:div (get-property-value-content v)]) value) (interpose [:div "or"])) :else "All")]))))) (rum/defc filter-value < rum/static [view-entity table property operator value filters set-filters! idx opts] (let [number-operator? (string/starts-with? (name operator) "number-")] (case operator :between (between property value filters set-filters! idx) (:text-contains :text-not-contains :number-gt :number-lt :number-gte :number-lte) (shui/input {:auto-focus false :value (or value "") :onChange (fn [e] (let [value (util/evalue e) number-value (and number-operator? (when-not (string/blank? value) (util/safe-parse-float value)))] (let [new-filters (update filters :filters (fn [col] (update col idx (fn [[property operator _value]] (if (and number-operator? (nil? number-value)) [property operator] [property operator (or number-value value)])))))] (set-filters! new-filters)))) :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"}) (filter-value-select view-entity table property value operator idx opts)))) (rum/defc filters-row < rum/static ; [view-entity {:keys [data-fns columns] :as table} opts] (let [filters (get-in table [:state :filters]) {:keys [set-filters!]} data-fns] (when (seq (:filters filters)) [:div.filters-row.flex.flex-row.items-center.gap-4.justify-between.flex-wrap.py-2 [:div.flex.flex-row.items-center.gap-2 (map-indexed (fn [idx filter'] (let [[property-ident operator value] filter' property (if (= property-ident :block/title) {:db/ident property-ident :block/title "Name"} (or (db/entity property-ident) (some (fn [column] (when (= (:id column) property-ident) {:db/ident (:id column) :block/title (:name column)})) columns)))] [:div.flex.flex-row.items-center.border.rounded (shui/button {:class "!px-2 rounded-none border-r" :variant "ghost" :size :sm :disabled true} [:span.text-xs (:block/title property)]) (filter-operator property operator filters set-filters! idx) (filter-value view-entity table property operator value filters set-filters! idx opts) (shui/button {:class "!px-1 rounded-none text-muted-foreground" :variant "ghost" :size :sm :on-click (fn [_e] (let [new-filters (update filters :filters (fn [col] (vec (remove #{filter'} col))))] (set-filters! new-filters)))} (ui/icon "x"))])) (:filters filters))] (when (> (count (:filters filters)) 1) [:div (shui/select {:default-value (if (:or? filters) "or" "and") :on-value-change (fn [v] (set-filters! (assoc filters :or? (= v "or"))))} (shui/select-trigger {:class "opacity-75 hover:opacity-100 !px-2 !py-0 !h-6"} (shui/select-value {:placeholder "Match"})) (shui/select-content (shui/select-group (shui/select-item {:value "and"} "Match all filters") (shui/select-item {:value "or"} "Match any filter"))))])]))) (rum/defc new-record-button < rum/static [table view-entity] (let [asset? (and (:logseq.property/built-in? view-entity) (= (:block/name view-entity) "asset"))] (ui/tooltip (shui/button {:variant "ghost" :class "!px-1 text-muted-foreground" :size :sm :on-click (fn [_] (let [f (get-in table [:data-fns :add-new-object!])] (f view-entity table)))} (ui/icon (if asset? "upload" "plus"))) [:div "New node"]))) (rum/defc add-new-row < rum/static [view-entity table] [:div.py-1.px-2.cursor-pointer.flex.flex-row.items-center.gap-1.text-muted-foreground.hover:text-foreground.w-full.text-sm.border-b {:on-click (fn [_] (let [f (get-in table [:data-fns :add-new-object!])] (f view-entity table)))} (ui/icon "plus" {:size 14}) [:div "New"]]) (defn- table-filters->persist-state [filters] (mapv (fn [[property operator matches]] (let [matches' (cond (de/entity? matches) (:block/uuid matches) (and (coll? matches) (every? de/entity? matches)) (set (map :block/uuid matches)) :else matches)] (if (some? matches') [property operator matches'] [property operator]))) filters)) (defn- db-set-table-state! [entity {:keys [set-sorting! set-filters! set-visible-columns! set-ordered-columns! set-sized-columns!]}] (let [repo (state/get-current-repo) db-based? (config/db-based-graph?)] {:set-sorting! (fn [sorting] (p/do! (when db-based? (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/sorting sorting)) (set-sorting! sorting))) :set-filters! (fn [filters] (let [filters (-> (update filters :filters table-filters->persist-state) (update :or? boolean))] (p/do! (when db-based? (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/filters filters)) (set-filters! filters)))) :set-visible-columns! (fn [columns] (let [hidden-columns (vec (keep (fn [[column visible?]] (when (false? visible?) column)) columns))] (p/do! (when db-based? (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/hidden-columns hidden-columns)) (set-visible-columns! columns)))) :set-ordered-columns! (fn [ordered-columns] (let [ids (vec (remove #{:select} ordered-columns))] (p/do! (when db-based? (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/ordered-columns ids)) (set-ordered-columns! ordered-columns)))) :set-sized-columns! (fn [sized-columns] (p/do! (when db-based? (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/sized-columns sized-columns)) (set-sized-columns! sized-columns)))})) (rum/defc lazy-item [data idx {:keys [properties list-view? scrolling?]} item-render] (let [item (util/nth-safe data idx) db-id (cond (map? item) (:db/id item) (number? item) item :else nil) [item set-item!] (hooks/use-state nil) opts (if list-view? {:skip-refresh? true :children? false} {:children? false :properties properties :skip-transact? true :skip-refresh? true})] (hooks/use-effect! #(c.m/run-task* (m/sp (when (and db-id (not item) (not scrolling?)) (let [block (c.m/> (:logseq.property/_view-for entity) (filter (fn [view] (= view-feature-type (:logseq.property.view/feature-type view)))))] (ldb/sort-by-order views))) (defn- create-view! [view-parent view-feature-type] (when-let [page (db/get-case-page common-config/views-page-name)] (p/let [properties (cond-> {:logseq.property/view-for (:db/id view-parent) :logseq.property.view/feature-type view-feature-type} (contains? #{:linked-references :unlinked-references} view-feature-type) (assoc :logseq.property.view/type (:db/id (db/entity :logseq.property.view/type.list)) :logseq.property.view/group-by-property (:db/id (db/entity :block/page)))) view-exists? (seq (get-views view-parent view-feature-type)) view-title (if view-exists? "" (case view-feature-type :linked-references "Linked references" :unlinked-references "Unlinked references" :class-objects "All" :property-objects "All" :all-pages "All pages" "")) result (editor-handler/api-insert-new-block! view-title {:page (:block/uuid page) :properties properties :edit-block? false})] (db/entity [:block/uuid (:block/uuid result)])))) (rum/defc views-tab < rum/reactive db-mixins/query [view-parent current-view {:keys [views data items-count set-view-entity! set-data! set-views! view-feature-type show-items-count? references? opacity]}] [:div.views (for [view* views] (let [view (db/sub-block (:db/id view*)) current-view? (= (:db/id current-view) (:db/id view))] (shui/button {:variant :text :size :sm :class (str "text-sm px-0 py-0 h-6 " (when-not current-view? "text-muted-foreground")) :on-click (fn [e] (if (and current-view? (not= (:db/id view) (:db/id view-parent))) (shui/popup-show! (.-target e) (fn [] [:<> (shui/dropdown-menu-sub (shui/dropdown-menu-sub-trigger "Rename") (shui/dropdown-menu-sub-content (when-let [block-container-cp (state/get-component :block/container)] (block-container-cp {} view)))) (shui/dropdown-menu-item {:key "Delete" :on-click (fn [] (p/do! (editor-handler/delete-block-aux! view) (let [views' (remove (fn [v] (= (:db/id v) (:db/id view))) views)] (set-views! views') (set-view-entity! (first views')) (shui/popup-hide!))))} "Delete")]) {:as-dropdown? true :align "start" :content-props {:onClick shui/popup-hide!}}) (do (set-view-entity! view) (set-data! nil))))} (when-not references? (let [display-type (or (:db/ident (get view :logseq.property.view/type)) :logseq.property.view/type.table)] (when-let [icon (:logseq.property/icon (db/entity display-type))] (icon-component/icon icon {:color? true :size 15})))) (let [title (:block/title view)] (if (= title "") "New view" title)) (when (and current-view? show-items-count? (> items-count 0) (seq data)) [:span.text-muted-foreground.text-xs items-count])))) (shui/button {:variant :text :size :sm :title "Add new view" :class (str "!px-1 -ml-1 text-muted-foreground hover:text-foreground transition-opacity ease-in duration-300 " opacity) :on-click (fn [] (p/let [view (create-view! view-parent view-feature-type)] (set-views! (concat views [view]))))} (ui/icon "plus" {:size 15}))]) (rum/defc view-head < rum/static [view-parent view-entity table columns input sorting set-input! add-new-object! {:keys [view-feature-type title-key additional-actions] :as option}] (let [[hover? set-hover?] (hooks/use-state nil) db-based? (config/db-based-graph? (state/get-current-repo)) references? (contains? #{:linked-references :unlinked-references} view-feature-type) opacity (cond (and references? (not hover?)) "opacity-0" hover? "opacity-100" :else "opacity-75")] [:div.flex.flex-1.flex-nowrap.items-center.justify-between.gap-1.overflow-hidden {:on-mouse-over #(set-hover? true) :on-mouse-out #(set-hover? false)} [:div.flex.flex-row.items-center.gap-2 (if db-based? (if (= view-feature-type :query-result) [:div.font-medium.opacity-50.text-sm (t (or title-key :views.table/default-title) (count (:rows table)))] (views-tab view-parent view-entity (assoc option :hover? hover? :opacity opacity :references? references?))) [:div.font-medium.text-sm [:span (case view-feature-type :all-pages "All pages" :linked-references "Linked references" :unlinked-references "Unlinked references" "Nodes")] [:span.ml-1 (count (:rows table))]])] [:div.view-actions.flex.items-center.gap-1.transition-opacity.ease-in.duration-300 {:class opacity} (when (seq additional-actions) [:<> (for [action additional-actions] (if (fn? action) (action option) action))]) (when (and db-based? (seq sorting)) (view-sorting table columns sorting)) (when db-based? (filter-properties view-entity columns table option)) (search input {:on-change set-input! :set-input! set-input!}) (when db-based? [:div.text-muted-foreground.text-sm (pv/property-value view-entity (db/entity :logseq.property.view/type) {})]) (when db-based? (more-actions view-entity columns table option)) (when (and db-based? add-new-object!) (new-record-button table view-entity))]])) (rum/defc ^:large-vars/cleanup-todo view-inner < rum/static [view-entity {:keys [view-parent data full-data set-data! columns add-new-object! foldable-options input set-input! sorting set-sorting! filters set-filters! display-type group-by-property-ident] :as option*} *scroller-ref] (let [db-based? (config/db-based-graph?) option (assoc option* :properties (-> (remove #{:id :select} (map :id columns)) (conj :block/uuid :block/name) vec)) default-visible-columns (if-let [hidden-columns (conj (:logseq.property.table/hidden-columns view-entity) :id)] (zipmap hidden-columns (repeat false)) ;; This case can happen for imported tables (if (seq (:logseq.property.table/ordered-columns view-entity)) (zipmap (set/difference (set (map :id columns)) (set (:logseq.property.table/ordered-columns view-entity)) #{:select :block/created-at :block/updated-at}) (repeat false)) {})) [visible-columns set-visible-columns!] (rum/use-state default-visible-columns) ordered-columns (vec (concat [:select] (:logseq.property.table/ordered-columns view-entity))) sized-columns (:logseq.property.table/sized-columns view-entity) [ordered-columns set-ordered-columns!] (rum/use-state ordered-columns) [sized-columns set-sized-columns!] (rum/use-state sized-columns) {:keys [set-sorting! set-filters! set-visible-columns! set-ordered-columns! set-sized-columns!]} (db-set-table-state! view-entity {:set-sorting! set-sorting! :set-filters! set-filters! :set-visible-columns! set-visible-columns! :set-sized-columns! set-sized-columns! :set-ordered-columns! set-ordered-columns!}) [row-selection set-row-selection!] (rum/use-state {}) [last-selected-idx set-last-selected-idx!] (rum/use-state nil) columns (sort-columns columns ordered-columns) select? (first (filter (fn [item] (= (:id item) :select)) columns)) id? (first (filter (fn [item] (= (:id item) :id)) columns)) pinned-properties (set (cond->> (map :db/ident (:logseq.property.table/pinned-columns view-entity)) id? (cons :id) select? (cons :select))) {pinned true unpinned false} (group-by (fn [item] (contains? pinned-properties (:id item))) (remove (fn [column] (false? (get visible-columns (:id column)))) columns)) group-by-property (or (:logseq.property.view/group-by-property view-entity) (db/entity group-by-property-ident)) table-map {:view-entity view-entity :data data :full-data full-data :columns columns :state {:sorting sorting :filters filters :row-selection row-selection :visible-columns visible-columns :sized-columns sized-columns :ordered-columns ordered-columns :pinned-columns pinned :unpinned-columns unpinned :group-by-property group-by-property :last-selected-idx last-selected-idx} :data-fns {:set-data! set-data! :set-filters! set-filters! :set-sorting! set-sorting! :set-visible-columns! set-visible-columns! :set-ordered-columns! set-ordered-columns! :set-sized-columns! set-sized-columns! :set-row-selection! set-row-selection! :add-new-object! add-new-object! :set-last-selected-idx! set-last-selected-idx!}} table (shui/table-option table-map) *view-ref (rum/use-ref nil) gallery? (= display-type :logseq.property.view/type.gallery) list-view? (= display-type :logseq.property.view/type.list)] (run-effects! option table-map *scroller-ref gallery?) [:div.flex.flex-col.gap-2.grid {:ref *view-ref} (ui/foldable (view-head view-parent view-entity table columns input sorting set-input! add-new-object! option) [:div.ls-view-body.flex.flex-col.gap-2.grid.mt-1 (filters-row view-entity table option) (let [view-opts {:*scroller-ref *scroller-ref :display-type display-type :row-selection row-selection :add-new-object! add-new-object!}] (if (and group-by-property-ident (not (number? (first (:rows table))))) (when (seq (:rows table)) [:div.flex.flex-col.border-t.pt-2.gap-2 (map-indexed (fn [idx [value group]] (let [add-new-object! (when (fn? add-new-object!) (fn [_] (add-new-object! view-entity table {:properties {(:db/ident group-by-property) (or (and (map? value) (:db/id value)) value)}}))) table' (shui/table-option (-> table-map (assoc-in [:data-fns :add-new-object!] add-new-object!) (assoc :data group ; data for this group ))) readable-property-value #(cond (and (map? %) (or (:block/title %) (:logseq.property/value %))) (db-property/property-value-content %) (= (:db/ident %) :logseq.property/empty-placeholder) "Empty" :else (str %)) group-by-page? (or (= :block/page group-by-property-ident) (and (not db-based?) (contains? #{:linked-references :unlinked-references} display-type)))] (rum/with-key (ui/foldable [:div {:class (when-not list-view? "my-4")} (cond group-by-page? (if value (let [c (state/get-component :block/page-cp)] (c {:disable-preview? true} value)) [:div.text-muted-foreground.text-sm "Pages"]) (some? value) (let [icon (pu/get-block-property-value value :logseq.property/icon)] [:div.flex.flex-row.gap-1.items-center (when icon (icon-component/icon icon {:color? true})) (readable-property-value value)]) :else (str "No " (:block/title group-by-property)))] (let [render (view-cp view-entity (assoc table' :rows group) option view-opts)] (if list-view? [:div.-ml-2 render] render)) {:title-trigger? false}) (str (:db/id view-entity) "-group-idx-" idx)))) (:rows table))]) (view-cp view-entity table (assoc option :group-by-property-ident group-by-property-ident) view-opts)))] (merge {:title-trigger? false} foldable-options))])) (rum/defcs view-container "Provides a view for data like query results and tagged objects, multiple layouts such as table and list are supported. Args: * view-entity: a db Entity * option: * title-key: dict key defaults to `:views.table/default-title` * data: a collections of entities * set-data!: `fn` to update `data` * columns: view columns including properties and db attributes, which could be built by `build-columns` * add-new-object!: `fn` to create a new object (or row) * show-add-property?: whether to show `Add property` * add-property!: `fn` to add a new property (or column)" < (rum/local nil ::scroller-ref) [state view-entity option] (rum/with-key (view-inner view-entity (cond-> option (or config/publishing? (:logseq.property.view/group-by-property view-entity)) (dissoc :add-new-object!)) (::scroller-ref state)) (str "view-" (:db/id view-entity)))) (defn {:view-for-id (or (:db/id (:logseq.property/view-for view-entity)) (:db/id view-parent)) :view-feature-type view-feature-type :group-by-property-ident group-by-property-ident :input input :filters filters :sorting sorting} query? (assoc :query-entity-ids query-entity-ids))))] (set-data! data) (when ref-pages-count (set-ref-pages-count! ref-pages-count))) (finally (set-loading! false)))))))))] (let [sorting-filters {:sorting sorting :filters filters}] (hooks/use-effect! load-view-data [(:db/id view-entity) (hooks/use-debounced-value input 300) sorting-filters group-by-property-ident (:db/id (:logseq.property.view/type view-entity)) ;; page filters (:logseq.property.linked-references/includes view-parent) (:logseq.property.linked-references/excludes view-parent) (:filters view-parent) query-entity-ids (:data-changes-version option)])) (if loading? [:div.flex.flex-col.space-2.gap-2.my-2 (repeat 3 (shui/skeleton {:class "h-6 w-full"}))] [:div.flex.flex-col.gap-2 (view-container view-entity (assoc option :data data :full-data data :filters filters :sorting sorting :set-filters! set-filters! :set-sorting! set-sorting! :set-data! set-data! :set-input! set-input! :input input :items-count (if (every? number? data) (count data) ;; grouped (reduce (fn [total [_ col]] (+ total (count col))) 0 data)) :group-by-property-ident group-by-property-ident :ref-pages-count ref-pages-count :display-type display-type :load-view-data load-view-data :set-view-entity! set-view-entity!))]))) (defn sub-view-data-changes [view-parent view-feature-type] (when view-parent (when-let [repo (state/get-current-repo)] (when-let [k (case view-feature-type :class-objects :frontend.worker.react/objects :linked-references :frontend.worker.react/refs nil)] (let [*version (atom 0)] (react/q repo [k (:db/id view-parent)] {:query-fn (fn [_] (swap! *version inc))} nil)))))) (rum/defc sub-view < rum/reactive db-mixins/query [view-entity option] (let [view (or (some-> (:db/id view-entity) db/sub-block) view-entity) data-changes-version (some-> (sub-view-data-changes (:view-parent option) (:view-feature-type option)) rum/react)] (view-aux view (assoc option :data-changes-version data-changes-version)))) (rum/defc view < rum/static [{:keys [view-parent view-feature-type view-entity] :as option}] (let [[views set-views!] (hooks/use-state nil) [view-entity set-view-entity!] (hooks/use-state view-entity) query? (= view-feature-type :query-result) db-based? (config/db-based-graph?)] (hooks/use-effect! #(c.m/run-task* (m/sp (when-not query? (let [repo (state/get-current-repo)] (when (and db-based? (not view-entity)) (c.m/