(ns frontend.components.property.value (:require [cljs-time.coerce :as tc] [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] [goog.dom :as gdom] [lambdaisland.glogi :as log] [rum.core :as rum] [frontend.handler.route :as route-handler] [frontend.handler.property.util :as pu] [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])) (defn- select-type? [property type] (or (contains? #{:page :number :url :date} type) ;; closed values (seq (get-in property [:block/schema :values])))) (defn exit-edit-property [] (state/set-state! :editor/new-property-key nil) (state/set-state! :editor/new-property-input-id nil) (state/set-state! :editor/properties nil) (state/set-state! :editor/editing-property-value-id {}) (state/clear-edit!)) (defn set-editing! [property editor-id dom-id v opts] (let [v (str v) cursor-range (if dom-id (some-> (gdom/getElement dom-id) util/caret-range) "")] (state/set-editing! editor-id v property cursor-range opts))) (defn journal-title value)))) (rum/defc date-picker [value {:keys [on-change editing? multiple-values?]}] (let [;; FIXME: Remove ignore when editing bug is fixed #_:clj-kondo/ignore [open? set-open!] (rum/use-state editing?) page (when (uuid? value) (db/entity [:block/uuid value])) title (when page (:block/original-name page)) value (if title (js/Date. (date/journal-title->long title)) value) value' (when-not (string/blank? value) (when-not (uuid? value) (try (tc/to-local-date value) (catch :default e (js/console.error e))))) initial-day (some-> value' (.getTime) (js/Date.)) initial-month (when value' (js/Date. (.getYear value') (.getMonth value')))] [:div.flex.flex-row.gap-1.items-center (when page (when-let [page-cp (state/get-component :block/page-cp)] (page-cp {:disable-preview? true :hide-close-button? true} page))) (let [content-fn (fn [{:keys [id]}] (let [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! (when-not (db/entity [:block/name (util/page-name-sanity-lc journal)]) (page-handler/> selected-choices (remove nil?) (remove #(= :property/empty-placeholder %))) clear-value (str "No " (:block/original-name property)) items' (->> (if (and (seq selected-choices) (not multiple-choices?)) (cons {:value clear-value :label clear-value} items) items) (remove #(= :property/empty-placeholder (:value %)))) k (if multiple-choices? :on-apply :on-chosen) f (get opts k) f' (fn [chosen] (if (or (and (not multiple-choices?) (= chosen clear-value)) (and multiple-choices? (= chosen [clear-value]))) (property-handler/remove-block-property! (state/get-current-repo) (:block/uuid block) (:block/original-name property)) (f chosen)))] (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 select-page [property {:keys [block classes multiple-choices? dropdown? input-opts on-chosen] :as opts}] (let [repo (state/get-current-repo) tags? (= :logseq.property/tags (:db/ident property)) alias? (= :logseq.property/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] (:block/original-name e)))) (when-let [v (get-in block [:block/properties (:block/uuid property)])] (if (coll? v) (map (fn [id] (:block/original-name (db/entity [:block/uuid id]))) v) [(:block/original-name (db/entity [:block/uuid v]))]))) (remove nil?))) closed-values (seq (get-in property [:block/schema :values])) pages (->> (cond (seq classes) (mapcat (fn [class] (if (= :logseq.class class) (map first (model/get-all-classes repo)) (some->> (:db/id (db/entity [:block/uuid class])) (model/get-class-objects repo) (map #(:block/original-name (db/entity %)))))) classes) (and block closed-values) (map (fn [id] (:block/original-name (db/entity [:block/uuid id]))) closed-values) :else (model/get-all-page-original-names repo)) distinct (remove (fn [p] (or (ldb/hidden-page? p) (util/uuid-string? (str p)))))) options (map (fn [p] {:value p}) pages) string-classes (remove #(= :logseq.class %) classes) opts' (cond-> (merge opts {:multiple-choices? multiple-choices? :items options :selected-choices selected-choices :dropdown? dropdown? :input-default-placeholder (cond tags? "Set tags" alias? "Set alias" multiple-choices? "Choose pages" :else "Choose page") :show-new-when-not-exact-match? (not (and block closed-values)) :extract-chosen-fn :value ;; Provides additional completion for inline classes on new pages :transform-fn (fn [results input] (if-let [[_ new-page class-input] (and (empty? results) (re-find #"(.*)#(.*)$" input))] (let [repo (state/get-current-repo) class-ents (map #(db/entity repo [:block/uuid %]) string-classes) class-names (map :block/original-name class-ents) descendent-classes (->> class-ents (mapcat #(model/get-class-children repo (:db/id %))) (map #(:block/original-name (db/entity repo %))))] (->> (concat class-names descendent-classes) (filter #(string/includes? % class-input)) (mapv #(hash-map :value (str new-page "#" %))))) results)) :input-opts input-opts}) multiple-choices? (assoc :on-apply (fn [choices] (p/let [page-ids (p/all (map #(> values (mapcat (fn [[_id value]] (if (coll? value) (map (fn [v] {:value v}) value) [{:value value}]))) (distinct))) items (->> (if (= :date type) (map (fn [m] (let [label (:block/original-name (db/entity [:block/uuid (:value m)]))] (when label (assoc m :label label)))) items) items) (remove nil?)) add-property-f #( {: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 :content-props content-props :input-opts (fn [_] {:on-blur (fn [] (exit-edit-property) (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 (exit-edit-property) (when-let [f (:on-chosen select-opts)] (f))) nil))})} closed-values? (assoc :extract-fn :label) multiple-choices? (assoc :on-apply on-chosen) (not multiple-choices?) (assoc :on-chosen on-chosen))))))) (rum/defc property-normal-block-value [parent block-cp editor-box] (let [children (model/sort-by-left (:block/_parent (db/entity (:db/id parent))) parent)] (when (seq children) [:div.property-block-container.w-full (block-cp children {:id (str (:block/uuid parent)) :editor-box editor-box})]))) (rum/defc property-template-value < rum/reactive {:init (fn [state] (let [block-id (second (:rum/args state))] (db-async/ (assoc select-opts :multiple-choices? false :on-chosen #(set-open! false)) (= type :page) (assoc :classes (:classes schema)))] (shui/dropdown-menu {:open open?} (shui/dropdown-menu-trigger {:class "jtrigger flex flex-1 w-full" :on-click (if config/publishing? (constantly nil) #(set-open! (not open?))) :on-key-down (fn [^js e] (case (util/ekey e) (" " "Enter") (do (set-open! true) (util/stop e)) :dune))} (if (string/blank? value) [:div.opacity-50.pointer.text-sm.cursor-pointer "Empty"] (value-f))) (shui/dropdown-menu-content {:align "start" :on-interact-outside #(set-open! false) :onEscapeKeyDown #(set-open! false)} [:div.property-select (case type (:number :url :date :default) (select block property select-opts' opts) :page (property-value-select-page block property select-opts' opts))])))) (rum/defcs property-scalar-value < rum/reactive db-mixins/query (rum/local nil ::ref) [state block property value {:keys [inline-text block-cp page-cp editor-id dom-id row? editor-box editor-args editing? on-chosen] :as opts}] (let [*ref (::ref state) property (model/sub-block (:db/id property)) repo (state/get-current-repo) schema (:block/schema property) type (get schema :type :default) multiple-values? (= :many (:cardinality schema)) editing? (or editing? (state/sub-property-value-editing? editor-id) (and @*ref (state/sub-editing? @*ref))) select-type? (select-type? property type) closed-values? (seq (:values schema)) select-opts {:on-chosen on-chosen} value (if (= value :property/empty-placeholder) nil value)] (if (and select-type? (not (and (not closed-values?) (= type :date)))) (single-value-select block property value (fn [] (select-item property type value opts)) select-opts (assoc opts :editing? editing?)) (case type :date (property-value-date-picker block property value {:editing? editing?}) :checkbox (let [add-property! (fn [] ( config multiple-values? (assoc :property-value value)))]))] (let [class (str (when-not row? "flex flex-1 ") (when multiple-values? "property-value-content")) type (or (when (and (= type :default) (uuid? value)) :block) type :default) type (if (= :block type) (let [v-block (db/entity [:block/uuid value])] (if (get-in v-block [:block/properties (:block/uuid (db/entity :logseq.property/created-from-template))]) :template type)) type) template? (= :template type)] [:div.cursor-text.jtrigger {:id (or dom-id (random-uuid)) :tabIndex 0 :class class :style {:min-height 24} :on-click (fn [] (when (and (= type :default) (not (uuid? value))) (set-editing! property editor-id dom-id value {:ref @*ref})))} (if (string/blank? value) (if template? (let [id (first (:classes schema)) template (when id (db/entity [:block/uuid id]))] (when template [:a.fade-link.pointer.text-sm.jtrigger {:on-click (fn [e] (util/stop e) ( {} editing? (assoc :class "h-6")) (if (= :page type) (property-value-select-page block property (assoc select-opts :classes (:classes schema)) opts) (select block property select-opts opts))]))] (rum/use-effect! (fn [] (when editing? (prn "TODO: editing multiple select immediately show..."))) [editing?]) (if (and dropdown? (not editing?)) (let [toggle-fn #(shui/popup-hide!) content-fn (fn [{:keys [_id content-props]}] (select-cp {:content-props content-props}))] [:div.multi-values.jtrigger {:tab-index "0" :ref *el :on-click (fn [^js e] (when-not (.closest (.-target e) ".select-item") (if config/publishing? nil (shui/popup-show! (rum/deref *el) content-fn {:as-dropdown? true :as-content? false :align "start" :auto-focus? true})))) :on-key-down (fn [^js e] (case (.-key e) (" " "Enter") (do (some-> (rum/deref *el) (.click)) (util/stop e)) :dune)) :class "flex flex-1 flex-row items-center flex-wrap gap-x-2 gap-y-2 pr-4"} (values-cp toggle-fn)]) (select-cp {:content-props nil})))) (rum/defc property-value < rum/reactive [block property v opts] (ui/catch-error (ui/block-error "Something wrong" {}) (let [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? (= :many (:cardinality schema)) empty-value? (= :property/empty-placeholder v) editor-args {:block property :parent-block block :format :markdown}] [:div.property-value-inner.w-full {:data-type type :class (when empty-value? "empty-value")} (cond multiple-values? (multiple-values block property v opts schema) :else (property-scalar-value block property v (merge opts {:editor-args editor-args :editor-id editor-id :dom-id dom-id})))])))