(ns frontend.components.property.value (:require [clojure.string :as string] [frontend.components.select :as select] [frontend.components.icon :as icon-component] [frontend.config :as config] [frontend.date :as date] [frontend.db :as db] [frontend.db-mixins :as db-mixins] [frontend.db.model :as model] [frontend.handler.editor :as editor-handler] [frontend.handler.page :as page-handler] [frontend.handler.property :as property-handler] [frontend.handler.db-based.property :as db-property-handler] [frontend.state :as state] [frontend.ui :as ui] [logseq.shui.ui :as shui] [frontend.util :as util] [lambdaisland.glogi :as log] [rum.core :as rum] [promesa.core :as p] [frontend.db.async :as db-async] [logseq.common.util.macro :as macro-util] [logseq.db :as ldb] [logseq.db.frontend.property :as db-property] [datascript.impl.entity :as de] [frontend.handler.property.util :as pu] [logseq.db.frontend.property.type :as db-property-type] [dommy.core :as d])) (rum/defc property-empty-btn-value [& {:as opts}] (shui/button (merge {:class "empty-btn" :variant :text} opts) "Empty")) (rum/defc property-empty-text-value [& {:as opts}] [:span.inline-flex.items-center.cursor-pointer (merge {:class "empty-text-btn" :variant :text} opts) "Empty"]) (rum/defc icon-row < rum/reactive [block] (let [icon-value (:logseq.property/icon block)] [:div.col-span-3.flex.flex-row.items-center.gap-2 (icon-component/icon-picker icon-value {:disabled? config/publishing? :on-chosen (fn [_e icon] (db-property-handler/set-block-property! (:db/id block) :logseq.property/icon (select-keys icon [:type :id])))}) (when (and icon-value (not config/publishing?)) [:a.fade-link.flex {:on-click (fn [_e] (db-property-handler/remove-block-property! (:db/id block) :logseq.property/icon)) :title "Delete this icon"} (ui/icon "X")])])) (defn- select-type? [property type] (or (contains? #{:page :object :number :url :date} type) ;; closed values (seq (:property/closed-values property)))) (defn value (.getTime) (js/Date.)) initial-month (when value (js/Date. (.getFullYear value) (.getMonth value))) select-handler! (fn [^js d] ;; force local to UTC (when d (let [gd (goog.date.Date. (.getFullYear d) (.getMonth d) (.getDate d))] (let [journal (date/js-date->journal-title gd)] (p/do! (shui/popup-hide! id) (when-not (db/get-case-page journal) (page-handler/ {:mode "single" :initial-focus true :selected initial-day :class-names {:months ""} :on-day-key-down (fn [^js d _ ^js e] (when (= "Enter" (.-key e)) (select-handler! d))) :on-select select-handler!} initial-month (assoc :default-month initial-month))))) (rum/defc date-picker [value {:keys [on-change editing? multiple-values? other-position?]}] (let [*trigger-ref (rum/use-ref nil) page value title (when page (:block/original-name page)) value' (when title (js/Date. (date/journal-title->long title))) content-fn (fn [{:keys [id]}] (calendar-inner id on-change value')) open-popup! (fn [e] (when-not (or (util/meta-key? e) (util/shift-key? e)) (util/stop e) (when-not config/publishing? (shui/popup-show! (.-target e) content-fn {:align "start" :auto-focus? true}))))] (rum/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!} (if page (when-let [page-cp (state/get-component :block/page-cp)] (rum/with-key (page-cp {:disable-preview? true :hide-close-button? true :meta-click? other-position?} page) (:db/id page))) (when-not multiple-values? (property-empty-btn-value))))))) (rum/defc property-value-date-picker [block property value opts] (let [multiple-values? (db-property/many? property)] (date-picker value (merge opts {:multiple-values? multiple-values? :on-change (fn [page] (let [repo (state/get-current-repo)] (property-handler/set-block-property! repo (:block/uuid block) (:db/ident property) (:db/id page))))})))) (defn- > selected-choices (remove nil?) (remove #(= :logseq.property/empty-placeholder %))) clear-value (str "No " (:block/original-name 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?)) (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! (property-handler/remove-block-property! (state/get-current-repo) (:block/uuid block) (:db/ident property)) (shui/popup-hide!)) (f chosen selected?)))] (select/select (assoc opts :selected-choices selected-choices :items items' k f')) ;(shui/multi-select-content ; (map #(let [{:keys [value label]} %] ; {:id value :value label}) items') nil opts) )) (defn- get-title [e] (or (:block/original-name e) (:block/content e))) (defn select-page [property {:keys [block multiple-choices? dropdown? input-opts] :as opts}] (let [repo (state/get-current-repo) object? (= :object (get-in property [:block/schema :type])) classes (:property/schema.classes property) tags? (= :block/tags (:db/ident property)) alias? (= :block/alias (:db/ident property)) tags-or-alias? (or tags? alias?) selected-choices (when block (if tags-or-alias? (->> (if tags? (:block/tags block) (:block/alias block)) (map (fn [e] (:db/id e)))) (when-let [v (get block (:db/ident property))] (if (every? de/entity? v) (map :db/id v) [(:db/id v)])))) objects-or-pages (->> (cond (seq classes) (mapcat (fn [class] (if (= :logseq.class/Root (:db/ident class)) (->> (model/get-all-classes repo) (keep (fn [[_ id]] (let [e (db/entity [:block/uuid id])] (when-not (= :logseq.class/Root (:db/ident e)) e))))) (->> (model/get-class-objects repo (:db/id class)) (keep db/entity)))) classes) :else (remove ldb/built-in? (model/get-all-pages repo)))) options (map (fn [object] {:label (get-title object) :value (:db/id object)}) objects-or-pages) 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? (str "Choose " (if object? "objects" "pages")) :else (str "Choose " (if object? "object" "page"))) :show-new-when-not-exact-match? true :extract-chosen-fn :value :extract-fn :label :input-opts input-opts :on-chosen (fn [chosen selected?] (p/let [id (if (integer? chosen) chosen (when-not (string/blank? (string/trim chosen)) (> classes' (mapcat #(model/get-class-children repo (:db/id %))) (map #(db/entity repo %)))] (->> (concat classes' descendent-classes) (filter #(string/includes? (:block/original-name %) class-input)) (mapv (fn [p] {:value (str new-page "#" (:block/original-name p)) :label (str new-page "#" (:block/original-name p))})))) results))))] (select-aux block property opts'))) (defn property-value-select-page [block property opts {:keys [*show-new-property-config?]}] (let [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" (do (shui/dialog-close!) (when-let [f (:on-chosen opts)] (f))) nil))}) opts' (assoc opts :block block :input-opts input-opts)] (select-page property opts'))) (defn > 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 (or (:block/content e) (:block/original-name e)) :value value}) {:label value :value value}))) (distinct))) items (->> (if (= :date type) (map (fn [m] (let [label (:block/original-name (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?))) 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 :label :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" (do (shui/dialog-close!) (when-let [f (:on-chosen select-opts)] (f))) nil))})}))))) (rum/defc property-normal-block-value [block property value-block block-cp editor-box opts] (let [multiple-values? (db-property/many? property)] (if value-block [:div.property-block-container.content (let [config {:id (str (if multiple-values? (:block/uuid block) (:block/uuid value-block))) :container-id (:container-id opts) :editor-box editor-box :property-block? true}] (block-cp config [value-block]))] (property-empty-btn-value)))) (rum/defc property-template-value < rum/reactive {:init (fn [state] (when-let [block-id (second (:rum/args state))] (db-async/ (rum/deref *el) (.click)) (util/stop e)) :dune)) :class "flex flex-1 flex-row items-center flex-wrap gap-x-2 gap-y-2 pr-4"} (values-cp toggle-fn)])))) (rum/defcs property-value < rum/reactive [state block property v opts] (ui/catch-error (ui/block-error "Something wrong" {}) (let [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 (state/get-component :block/blocks-container) :properties-cp (state/get-component :block/properties-cp)}) dom-id (str "ls-property-" (:db/id block) "-" (:db/id property)) editor-id (str dom-id "-editor") schema (:block/schema property) type (some-> schema (get :type :default)) multiple-values? (db-property/many? property) empty-value? (= :logseq.property/empty-placeholder v) 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)] [:div.property-value-inner {:data-type type :class (str (when empty-value? "empty-value") (when-not (:other-position? opts) " w-full"))} (cond multiple-values? (multiple-values block property v opts schema) :else (property-scalar-value block property v (merge opts {:editor-id editor-id :dom-id dom-id})))])))