diff --git a/src/main/frontend/components/property.cljs b/src/main/frontend/components/property.cljs index e2b2c6f721..9386cd0ee0 100644 --- a/src/main/frontend/components/property.cljs +++ b/src/main/frontend/components/property.cljs @@ -25,32 +25,11 @@ [frontend.handler.route :as route-handler] [frontend.components.icon :as icon-component] [frontend.components.dnd :as dnd] - [dommy.core :as dom])) + [dommy.core :as dom] + [frontend.components.property.enum :as enum] + [frontend.components.property.util :as components-pu])) -(defn- update-property! - [property property-name property-schema] - (property-handler/update-property! - (state/get-current-repo) - (:block/uuid property) - {:property-name property-name - :property-schema property-schema})) - -(rum/defc icon - [icon {:keys [disabled? on-chosen]}] - (ui/dropdown - (fn [{:keys [toggle-fn]}] - [:button.flex {:on-click #(when-not disabled? (toggle-fn))} - (if icon - (icon-component/icon icon) - [:span.bullet-container.cursor [:span.bullet]])]) - (fn [{:keys [toggle-fn]}] - [:div.p-4 - (icon-component/icon-search - {:on-chosen (fn [e icon] - (on-chosen e icon) - (toggle-fn))})]) - {:modal-class (util/hiccup->class - "origin-top-right.absolute.left-0.rounded-md.shadow-lg")})) +(def icon enum/icon) (defn- create-class-if-not-exists! [value] @@ -122,185 +101,6 @@ {:modal-class (util/hiccup->class "origin-top-right.absolute.left-0.rounded-md.shadow-lg.mt-2")})]) -(rum/defcs enum-item-config < rum/reactive - shortcut/disable-all-shortcuts - {:init (fn [state] - (let [{:keys [name icon description]} (first (:rum/args state))] - (assoc state - ::name (atom (or name "")) - ::icon (atom icon) - ::description (atom (or description "")))))} - [state _item {:keys [toggle-fn on-save]}] - (let [*name (::name state) - *icon (::icon state) - *description (::description state)] - [:div.flex.flex-col.gap-4.p-4.whitespace-nowrap.w-96 - [:div.grid.grid-cols-5.gap-1.items-center.leading-8 - [:label.col-span-2 "Name:"] - [:input.form-input.col-span-3 - {:default-value @*name - :on-change #(reset! *name (util/evalue %))}]] - [:div.grid.grid-cols-5.gap-1.items-center.leading-8 - [:label.col-span-2 "Icon:"] - [:div.col-span-3 - (icon (rum/react *icon) - {:on-chosen (fn [_e icon] - (reset! *icon icon))})]] - [:div.grid.grid-cols-5.gap-1.items-start.leading-8 - [:label.col-span-2 "Description:"] - [:div.col-span-3 - (ui/ls-textarea - {:on-change #(reset! *description (util/evalue %)) - :default-value @*description})]] - [:div - (ui/button - "Save" - :on-click (fn [e] - (util/stop e) - (when-not (string/blank? @*name) - (let [result (when on-save (on-save (string/trim @*name) @*icon @*description))] - (if (= :value-exists result) - (notification/show! (str "Choice already exist") :warning) - (when toggle-fn (toggle-fn)))))))]])) - -(rum/defcs enum-new-item < - (rum/local "" ::name) - (rum/local "" ::icon) - (rum/local "" ::description) - [state {:keys [toggle-fn on-save]}] - (let [*name (::name state) - *icon (::icon state) - *description (::description state)] - [:div.flex.flex-col.gap-4.p-4.whitespace-nowrap.w-96 - [:div.grid.grid-cols-5.gap-1.items-center.leading-8 - [:label.col-span-2 "Name:"] - [:input.form-input.col-span-3 - {:default-value "" - :auto-focus true - :on-change (fn [e] (reset! *name (util/evalue e)))}]] - [:div.grid.grid-cols-5.gap-1.items-center.leading-8 - [:label.col-span-2 "Icon:"] - [:div.col-span-3 - (icon nil {:on-chosen (fn [_e icon] - (reset! *icon icon))})]] - [:div.grid.grid-cols-5.gap-1.items-start.leading-8 - [:label.col-span-2 "Description:"] - [:div.col-span-3 - (ui/ls-textarea - {:on-change #(reset! *description (util/evalue %)) - :default-value @*description})]] - [:div - (ui/button - "Save" - :on-click (fn [e] - (util/stop e) - (when-not (string/blank? @*name) - (let [result (when on-save (on-save (string/trim @*name) - @*icon - @*description))] - (if (= :value-exists result) - (notification/show! (str "Choice already exist") :warning) - (when toggle-fn (toggle-fn)))))))]])) - -(rum/defcs choice-with-close < - (rum/local false ::hover?) - [state item name {:keys [toggle-fn delete-choice update-icon]}] - (let [*hover? (::hover? state)] - [:div.flex.flex-1.flex-row.items-center.gap-2.justify-between - {:on-mouse-over #(reset! *hover? true) - :on-mouse-out #(reset! *hover? false)} - [:div.flex.flex-row.items-center.gap-2 - (icon (:icon item) - {:on-chosen (fn [_e icon] - (update-icon icon))}) - [:a {:on-click toggle-fn} - name]] - (when @*hover? - [:a.fade-link.flex {:on-click delete-choice - :title "Delete this choice"} - (ui/icon "X")])])) - -(rum/defc choice-item-content - [property item values order *property-schema *property-name dropdown-opts] - (let [{:keys [id name]} item] - (ui/dropdown - (fn [opts] - (choice-with-close - item - name - (assoc opts - :delete-choice - (fn [] - (let [new-values (dissoc values id) - new-order (vec (remove #{id} order))] - (swap! *property-schema assoc :enum-config {:values new-values - :order new-order}) - ;; FIXME: how to handle block properties with this value? - ;; 1. delete the blocks' property that has this value - ;; 2. update exist values to the default value if exists - ;; 3. soft delete, users can still see it in some existing blocks, - ;; but they will not see it when adding or updating this property - (update-property! property @*property-name @*property-schema))) - :update-icon - (fn [icon] - (let [new-values (assoc-in values [id :icon] icon)] - (swap! *property-schema assoc :enum-config {:values new-values - :order order}) - (update-property! property @*property-name @*property-schema)))))) - (fn [opts] - (enum-item-config - item - (assoc opts :on-save - (fn [name icon description] - (if (some (fn [[vid m]] (and (not= vid id) (= name (:name m)))) values) - :value-exists - (let [new-values (assoc values id {:name name - :icon icon - :description description})] - (swap! *property-schema assoc :enum-config {:values new-values - :order order}) - (update-property! property @*property-name @*property-schema))))))) - dropdown-opts))) - -(rum/defc enum-choices - [property *property-name *property-schema {:keys [values order] :as _config}] - (let [dropdown-opts {:modal-class (util/hiccup->class - "origin-top-right.absolute.left-0.rounded-md.shadow-lg")} - order (if (not= (count order) (count values)) - (vec (concat order (remove (set order) (keys values)))) - order)] - [:div.enum-choices.flex.flex-col - (let [choices (mapv (fn [id] - (let [item (assoc (get values id) :id id)] - {:id (str id) - :value id - :content (choice-item-content property item values order *property-schema *property-name dropdown-opts)})) - order)] - (dnd/items choices - {:on-drag-end (fn [new-order] - (when (seq new-order) - (swap! *property-schema assoc :enum-config {:values values - :order new-order}) - (update-property! property @*property-name @*property-schema)))})) - (ui/dropdown - (fn [{:keys [toggle-fn]}] - [:a.fade-link.flex.flex-row.items-center.gap-1.leading-8 {:on-click toggle-fn} - (ui/icon "plus" {:size 16}) - "Add choice"]) - (fn [opts] - (enum-new-item (assoc opts :on-save - (fn [name description] - (if (contains? (set (map :name (vals values))) name) - :value-exists - (let [id (random-uuid) - new-values (assoc values id {:name name - :description description}) - new-order (vec (conj order id))] - (swap! *property-schema assoc :enum-config {:values new-values - :order new-order}) - (update-property! property @*property-name @*property-schema))))))) - dropdown-opts)])) - (defn- property-type-label [property-type] (if (= property-type :default) @@ -333,7 +133,7 @@ add-new-property?) class? (contains? (:block/type block) "class") property-type (get-in property [:block/schema :type]) - save-property-fn (fn [] (update-property! property @*property-name @*property-schema))] + save-property-fn (fn [] (components-pu/update-property! property @*property-name @*property-schema))] [:div.property-configure.flex.flex-1.flex-col {:on-mouse-down #(state/set-state! :editor/mouse-down-from-property-configure? true) :on-mouse-up #(state/set-state! :editor/mouse-down-from-property-configure? nil)} @@ -353,14 +153,14 @@ [:label.col-span-1 "Icon:"] (let [icon-value (pu/get-property property :icon)] [:div.col-span-3 - (icon icon-value - {:disabled? disabled? - :on-chosen (fn [_e icon] - (let [icon-property-id (pu/get-built-in-property-uuid :icon)] - (property-handler/update-property! - (state/get-current-repo) - (:block/uuid property) - {:properties {icon-property-id icon}})))})])] + (enum/icon icon-value + {:disabled? disabled? + :on-chosen (fn [_e icon] + (let [icon-property-id (pu/get-built-in-property-uuid :icon)] + (property-handler/update-property! + (state/get-current-repo) + (:block/uuid property) + {:properties {icon-property-id icon}})))})])] [:div.grid.grid-cols-4.gap-1.items-center.leading-8 [:label.col-span-1 "Schema type:"] @@ -379,7 +179,7 @@ (fn [_e v] (let [type (keyword (string/lower-case v))] (swap! *property-schema assoc :type type) - (update-property! property @*property-name @*property-schema)))))])] + (components-pu/update-property! property @*property-name @*property-schema)))))])] (case (:type @*property-schema) :page @@ -404,7 +204,7 @@ [:div.grid.grid-cols-4.gap-1.items-start.leading-8 [:label.col-span-1 "Enum choices:"] [:div.col-span-3 - (enum-choices property *property-name *property-schema (:enum-config @*property-schema))]] + (enum/enum-choices property *property-name *property-schema (:enum-config @*property-schema))]] nil) diff --git a/src/main/frontend/components/property/enum.cljs b/src/main/frontend/components/property/enum.cljs new file mode 100644 index 0000000000..393ee7c3a8 --- /dev/null +++ b/src/main/frontend/components/property/enum.cljs @@ -0,0 +1,207 @@ +(ns frontend.components.property.enum + "Enum property config" + (:require [rum.core :as rum] + [frontend.components.dnd :as dnd] + [frontend.modules.shortcut.core :as shortcut] + [frontend.util :as util] + [frontend.ui :as ui] + [frontend.components.icon :as icon-component] + [clojure.string :as string] + [frontend.handler.notification :as notification] + [frontend.components.property.util :as components-pu])) + +(rum/defc icon + [icon {:keys [disabled? on-chosen]}] + (ui/dropdown + (fn [{:keys [toggle-fn]}] + [:button.flex {:on-click #(when-not disabled? (toggle-fn))} + (if icon + (icon-component/icon icon) + [:span.bullet-container.cursor [:span.bullet]])]) + (fn [{:keys [toggle-fn]}] + [:div.p-4 + (icon-component/icon-search + {:on-chosen (fn [e icon] + (on-chosen e icon) + (toggle-fn))})]) + {:modal-class (util/hiccup->class + "origin-top-right.absolute.left-0.rounded-md.shadow-lg")})) + +(rum/defcs enum-item-config < rum/reactive + shortcut/disable-all-shortcuts + {:init (fn [state] + (let [{:keys [name icon description]} (first (:rum/args state))] + (assoc state + ::name (atom (or name "")) + ::icon (atom icon) + ::description (atom (or description "")))))} + [state _item {:keys [toggle-fn on-save]}] + (let [*name (::name state) + *icon (::icon state) + *description (::description state)] + [:div.flex.flex-col.gap-4.p-4.whitespace-nowrap.w-96 + [:div.grid.grid-cols-5.gap-1.items-center.leading-8 + [:label.col-span-2 "Name:"] + [:input.form-input.col-span-3 + {:default-value @*name + :on-change #(reset! *name (util/evalue %))}]] + [:div.grid.grid-cols-5.gap-1.items-center.leading-8 + [:label.col-span-2 "Icon:"] + [:div.col-span-3 + (icon (rum/react *icon) + {:on-chosen (fn [_e icon] + (reset! *icon icon))})]] + [:div.grid.grid-cols-5.gap-1.items-start.leading-8 + [:label.col-span-2 "Description:"] + [:div.col-span-3 + (ui/ls-textarea + {:on-change #(reset! *description (util/evalue %)) + :default-value @*description})]] + [:div + (ui/button + "Save" + :on-click (fn [e] + (util/stop e) + (when-not (string/blank? @*name) + (let [result (when on-save (on-save (string/trim @*name) @*icon @*description))] + (if (= :value-exists result) + (notification/show! (str "Choice already exist") :warning) + (when toggle-fn (toggle-fn)))))))]])) + +(rum/defcs enum-new-item < + (rum/local "" ::name) + (rum/local "" ::icon) + (rum/local "" ::description) + [state {:keys [toggle-fn on-save]}] + (let [*name (::name state) + *icon (::icon state) + *description (::description state)] + [:div.flex.flex-col.gap-4.p-4.whitespace-nowrap.w-96 + [:div.grid.grid-cols-5.gap-1.items-center.leading-8 + [:label.col-span-2 "Name:"] + [:input.form-input.col-span-3 + {:default-value "" + :auto-focus true + :on-change (fn [e] (reset! *name (util/evalue e)))}]] + [:div.grid.grid-cols-5.gap-1.items-center.leading-8 + [:label.col-span-2 "Icon:"] + [:div.col-span-3 + (icon nil {:on-chosen (fn [_e icon] + (reset! *icon icon))})]] + [:div.grid.grid-cols-5.gap-1.items-start.leading-8 + [:label.col-span-2 "Description:"] + [:div.col-span-3 + (ui/ls-textarea + {:on-change #(reset! *description (util/evalue %)) + :default-value @*description})]] + [:div + (ui/button + "Save" + :on-click (fn [e] + (util/stop e) + (when-not (string/blank? @*name) + (let [result (when on-save (on-save (string/trim @*name) + @*icon + @*description))] + (if (= :value-exists result) + (notification/show! (str "Choice already exist") :warning) + (when toggle-fn (toggle-fn)))))))]])) + +(rum/defcs choice-with-close < + (rum/local false ::hover?) + [state item name {:keys [toggle-fn delete-choice update-icon]}] + (let [*hover? (::hover? state)] + [:div.flex.flex-1.flex-row.items-center.gap-2.justify-between + {:on-mouse-over #(reset! *hover? true) + :on-mouse-out #(reset! *hover? false)} + [:div.flex.flex-row.items-center.gap-2 + (icon (:icon item) + {:on-chosen (fn [_e icon] + (update-icon icon))}) + [:a {:on-click toggle-fn} + name]] + (when @*hover? + [:a.fade-link.flex {:on-click delete-choice + :title "Delete this choice"} + (ui/icon "X")])])) + +(rum/defc choice-item-content + [property item values order *property-schema *property-name dropdown-opts] + (let [{:keys [id name]} item] + (ui/dropdown + (fn [opts] + (choice-with-close + item + name + (assoc opts + :delete-choice + (fn [] + (let [new-values (dissoc values id) + new-order (vec (remove #{id} order))] + (swap! *property-schema assoc :enum-config {:values new-values + :order new-order}) + ;; FIXME: how to handle block properties with this value? + ;; 1. delete the blocks' property that has this value + ;; 2. update exist values to the default value if exists + ;; 3. soft delete, users can still see it in some existing blocks, + ;; but they will not see it when adding or updating this property + (components-pu/update-property! property @*property-name @*property-schema))) + :update-icon + (fn [icon] + (let [new-values (assoc-in values [id :icon] icon)] + (swap! *property-schema assoc :enum-config {:values new-values + :order order}) + (components-pu/update-property! property @*property-name @*property-schema)))))) + (fn [opts] + (enum-item-config + item + (assoc opts :on-save + (fn [name icon description] + (if (some (fn [[vid m]] (and (not= vid id) (= name (:name m)))) values) + :value-exists + (let [new-values (assoc values id {:name name + :icon icon + :description description})] + (swap! *property-schema assoc :enum-config {:values new-values + :order order}) + (components-pu/update-property! property @*property-name @*property-schema))))))) + dropdown-opts))) + +(rum/defc enum-choices + [property *property-name *property-schema {:keys [values order] :as _config}] + (let [dropdown-opts {:modal-class (util/hiccup->class + "origin-top-right.absolute.left-0.rounded-md.shadow-lg")} + order (if (not= (count order) (count values)) + (vec (concat order (remove (set order) (keys values)))) + order)] + [:div.enum-choices.flex.flex-col + (let [choices (mapv (fn [id] + (let [item (assoc (get values id) :id id)] + {:id (str id) + :value id + :content (choice-item-content property item values order *property-schema *property-name dropdown-opts)})) + order)] + (dnd/items choices + {:on-drag-end (fn [new-order] + (when (seq new-order) + (swap! *property-schema assoc :enum-config {:values values + :order new-order}) + (components-pu/update-property! property @*property-name @*property-schema)))})) + (ui/dropdown + (fn [{:keys [toggle-fn]}] + [:a.fade-link.flex.flex-row.items-center.gap-1.leading-8 {:on-click toggle-fn} + (ui/icon "plus" {:size 16}) + "Add choice"]) + (fn [opts] + (enum-new-item (assoc opts :on-save + (fn [name description] + (if (contains? (set (map :name (vals values))) name) + :value-exists + (let [id (random-uuid) + new-values (assoc values id {:name name + :description description}) + new-order (vec (conj order id))] + (swap! *property-schema assoc :enum-config {:values new-values + :order new-order}) + (components-pu/update-property! property @*property-name @*property-schema))))))) + dropdown-opts)])) diff --git a/src/main/frontend/components/property/util.cljs b/src/main/frontend/components/property/util.cljs new file mode 100644 index 0000000000..621ca6d9ae --- /dev/null +++ b/src/main/frontend/components/property/util.cljs @@ -0,0 +1,12 @@ +(ns frontend.components.property.util + "Property component utils" + (:require [frontend.state :as state] + [frontend.handler.property :as property-handler])) + +(defn- update-property! + [property property-name property-schema] + (property-handler/update-property! + (state/get-current-repo) + (:block/uuid property) + {:property-name property-name + :property-schema property-schema}))