Răsfoiți Sursa

Replace enum type with available choices (#10438)

* Add type schema for enum values

* Refactor: closed values instead of enum to limit values

There's no need to add the enum type, we just want to limit the
values.

* feat: :page type supports closed values

* fix: can't set property values for text/number types

* fix: don't trigger on-chosen when unmounting any select component

* fix: can't apply both page classes limitation and closed values

* fix: merge conflicts

* fix: ui not refresh when changing closed value

* fix: can't set icon for closed value
Tienson Qin 1 an în urmă
părinte
comite
6a47648c6c

+ 3 - 5
deps/db/src/logseq/db/frontend/malli_schema.cljs

@@ -130,10 +130,8 @@
    [:description {:optional true} :string]
    ;; For any types except for :checkbox :default :template :enum
    [:cardinality {:optional true} [:enum :one :many]]
-   ;; Just for :enum type
-   [:enum-config {:optional true}
-    [:map
-     [:values [:vector :uuid]]]]
+   ;; closed values
+   [:values {:optional true}  [:vector :uuid]]
    ;; Just for :enum
    [:position {:optional true} :string]
    ;; For :page and :template
@@ -239,7 +237,7 @@
   (vec
    (concat
     [:map]
-    [[:block/type [:= #{"enum value"}]]
+    [[:block/type [:= #{"closed value"}]]
      [:block/schema {:optional true}
       [:map
        [:description :string]]]

+ 6 - 3
deps/db/src/logseq/db/frontend/property/type.cljs

@@ -8,7 +8,11 @@
 
 (def user-builtin-schema-types
   "Valid schema :type for users in order they appear in the UI"
-  [:default :number :date :checkbox :url :page :template :enum])
+  [:default :number :date :checkbox :url :page :template])
+
+(def closed-values-schema-types
+  "Valid schema :type for close values"
+  #{:default :number :date :url :page})
 
 ;; TODO:
 ;; Validate && list fixes for non-validated values when updating property schema
@@ -57,7 +61,6 @@
    :template [:fn
               {:error/message "should has #template"}
               logseq-template?]
-   :enum     some?                      ; the value could be anything such as number, text, url, date, page, image, video, etc.
    ;; internal usage
    :keyword  keyword?
    :map      map?
@@ -72,4 +75,4 @@
 (assert (= (set (keys builtin-schema-types))
            (into internal-builtin-schema-types
                  user-builtin-schema-types))
-        "Built-in schema types must be equal")
+        "Built-in schema types must be equal")

+ 6 - 6
src/main/frontend/components/block.cljs

@@ -2303,12 +2303,12 @@
                        :tag? true
                        :disable-preview? true) tag))]))
 
-(rum/defc block-enum-properties
+(rum/defc block-closed-values-properties
   [block]
-  (let [enum-properties (property-handler/get-block-enum-other-position-properties (:db/id block))]
-    (when (seq enum-properties)
-    [:div.enum-properties.flex.flex-row.items-center.gap-1.select-none.h-full
-     (for [pid enum-properties]
+  (let [closed-values-properties (property-handler/get-block-other-position-properties (:db/id block))]
+    (when (seq closed-values-properties)
+    [:div.closed-values-properties.flex.flex-row.items-center.gap-1.select-none.h-full
+     (for [pid closed-values-properties]
        (when-let [property (db/entity [:block/uuid pid])]
          (pv/property-value block property (get (:block/properties block) pid) {:icon? true})))])))
 
@@ -2474,7 +2474,7 @@
         repo (state/get-current-repo)
         db-based? (config/db-based-graph? repo)]
     [:div.flex.flex-1.flex-row.flex-wrap.gap-1.items-start
-     (block-enum-properties block)
+     (block-closed-values-properties block)
      (if (and edit? editor-box)
        [:div.editor-wrapper.flex.flex-1
         {:id editor-id}

+ 41 - 36
src/main/frontend/components/property.cljs

@@ -26,10 +26,10 @@
             [frontend.components.icon :as icon-component]
             [frontend.components.dnd :as dnd]
             [dommy.core :as dom]
-            [frontend.components.property.enum :as enum]
+            [frontend.components.property.closed-value :as closed-value]
             [frontend.components.property.util :as components-pu]))
 
-(def icon enum/icon)
+(def icon closed-value/icon)
 
 (defn- create-class-if-not-exists!
   [value]
@@ -134,7 +134,8 @@
                          add-new-property?)
         class? (contains? (:block/type block) "class")
         property-type (get-in property [:block/schema :type])
-        save-property-fn (fn [] (components-pu/update-property! property @*property-name @*property-schema))]
+        save-property-fn (fn [] (components-pu/update-property! property @*property-name @*property-schema))
+        enable-closed-values? (contains? db-property-type/closed-values-schema-types (or property-type :default))]
     [: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)}
@@ -154,14 +155,14 @@
        [:label.col-span-1 "Icon:"]
        (let [icon-value (pu/get-property property :icon)]
          [:div.col-span-3
-          (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}})))})])]
+          (closed-value/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:"]
@@ -185,15 +186,27 @@
                            (swap! *property-schema assoc :type type)
                            (components-pu/update-property! property @*property-name @*property-schema))))]))]
 
+      (when-not (contains? #{:checkbox :default :template} (:type @*property-schema))
+        [:div.grid.grid-cols-4.gap-1.items-center.leading-8
+         [:label "Multiple values:"]
+         (let [many? (boolean (= :many (:cardinality @*property-schema)))]
+           (ui/checkbox {:checked many?
+                         :disabled disabled?
+                         :on-change (fn []
+                                      (swap! *property-schema assoc :cardinality (if many? :one :many))
+                                      (save-property-fn))}))])
+
+
       (case (:type @*property-schema)
         :page
-        [:div.grid.grid-cols-4.gap-1.items-center.leading-8
-         [:label "Specify classes:"]
-         (class-select *property-schema
-                       (:classes @*property-schema)
-                       (assoc opts
-                              :disabled? disabled?
-                              :save-property-fn save-property-fn))]
+        (when (empty? (:values @*property-schema))
+          [:div.grid.grid-cols-4.gap-1.items-center.leading-8
+           [:label "Specify classes:"]
+           (class-select *property-schema
+                         (:classes @*property-schema)
+                         (assoc opts
+                                :disabled? disabled?
+                                :save-property-fn save-property-fn))])
 
         :template
         [:div.grid.grid-cols-4.gap-1.items-center.leading-8
@@ -204,15 +217,15 @@
                               :disabled? disabled?
                               :save-property-fn save-property-fn))]
 
-        :enum
+        nil)
+
+      (when (and enable-closed-values? (empty? (:classes @*property-schema)))
         [:div.grid.grid-cols-4.gap-1.items-start.leading-8
-         [:label.col-span-1 "Enum choices:"]
+         [:label.col-span-1 "Available choices:"]
          [:div.col-span-3
-          (enum/enum-choices property *property-name *property-schema)]]
-
-        nil)
+          (closed-value/choices property *property-name *property-schema)]])
 
-      (when (= :enum (:type @*property-schema))
+      (when (and enable-closed-values? (seq (:values @*property-schema)))
         (let [position (:position @*property-schema)
               choices (map
                        (fn [item]
@@ -226,7 +239,7 @@
                         ;; {:label "Ending of the block"
                         ;;  :value "block-ending"}
                         ])]
-          [:div.grid.grid-cols-4.gap-1.items-start.leading-8
+          [:div.grid.grid-cols-4.gap-1.items-center.leading-8
            [:label.col-span-1 "UI position:"]
            [:div.col-span-3
             (ui/select choices
@@ -234,16 +247,6 @@
                          (swap! *property-schema assoc :position v)
                          (save-property-fn)))]]))
 
-      (when-not (contains? #{:checkbox :default :template :enum} (:type @*property-schema))
-        [:div.grid.grid-cols-4.gap-1.items-center.leading-8
-         [:label "Multiple values:"]
-         (let [many? (boolean (= :many (:cardinality @*property-schema)))]
-           (ui/checkbox {:checked many?
-                         :disabled disabled?
-                         :on-change (fn []
-                                      (swap! *property-schema assoc :cardinality (if many? :one :many))
-                                      (save-property-fn))}))])
-
       (let [hide? (:hide? @*property-schema)]
         [:div.grid.grid-cols-4.gap-1.items-center.leading-8
          [:label "Hide by default:"]
@@ -538,14 +541,16 @@
   (when (uuid? k)
     (when-let [property (db/sub-block (:db/id (db/entity [:block/uuid k])))]
       (let [type (get-in property [:block/schema :type] :default)
+            closed-values? (seq (get-in property [:block/schema :values]))
             v-block (when (uuid? v) (db/entity [:block/uuid v]))
             block? (and v-block
+                        (not closed-values?)
                         (:block/page v-block)
                         (contains? #{:default :template} type))
             collapsed? (when block? (property-collapsed? block property))
             date? (= type :date)]
         [:div {:class (cond
-                        block?
+                        (and block? (not closed-values?))
                         "flex flex-1 flex-col gap-1 property-block"
                         date?
                         "property-pair items-center"

+ 191 - 0
src/main/frontend/components/property/closed_value.cljs

@@ -0,0 +1,191 @@
+(ns frontend.components.property.closed-value
+  "Enum property config"
+  (:require [rum.core :as rum]
+            [clojure.string :as string]
+            [frontend.modules.shortcut.core :as shortcut]
+            [frontend.util :as util]
+            [frontend.ui :as ui]
+            [frontend.components.dnd :as dnd]
+            [frontend.components.icon :as icon-component]
+            [frontend.components.property.util :as pu-component]
+            [frontend.handler.property :as property-handler]
+            [frontend.components.property.value :as property-value]
+            [frontend.db :as db]
+            [frontend.state :as state]
+            [frontend.handler.property.util :as pu]))
+
+(defn- upsert-closed-value!
+  "Create new closed value and returns its block UUID."
+  [property item]
+  (let [{:keys [block-id tx-data]} (property-handler/upsert-closed-value property item)]
+    (when (seq tx-data) (db/transact! tx-data))
+    block-id))
+
+(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/defc item-value
+  [type *value]
+  (case type
+    ;; :page
+    :date
+    (let [value (if (string/blank? @*value) nil @*value)]
+      (property-value/date-picker value
+                                  {:on-change (fn [page]
+                                                (reset! *value (:block/uuid page)))}))
+    [:input.form-input.col-span-3
+     {:default-value @*value
+      :auto-focus true
+      :on-change #(reset! *value (util/evalue %))}]))
+
+(rum/defcs item-config < rum/reactive
+  shortcut/disable-all-shortcuts
+  {:init (fn [state]
+           (let [block (second (:rum/args state))
+                 value (or (str (get-in block [:block/schema :value])) "")
+                 icon (when block (pu/get-property block :icon))
+                 description (or (get-in block [:block/schema :description]) "")]
+             (assoc state
+                    ::value (atom value)
+                    ::icon (atom icon)
+                    ::description (atom description))))}
+  [state property _item {:keys [toggle-fn on-save]}]
+  (let [*value (::value state)
+        *icon (::icon state)
+        *description (::description state)
+        save-handler (fn [e]
+                       (util/stop e)
+                       (when-not (string/blank? @*value)
+                         (when on-save
+                           (let [value (if (string? @*value)
+                                         (string/trim @*value)
+                                         @*value)]
+                             (on-save value @*icon @*description)))
+                         (when toggle-fn (toggle-fn))))
+        property-type (get-in property [:block/schema :type])]
+    [:div.flex.flex-col.gap-4.p-4.whitespace-nowrap.w-96
+     {:on-key-down (fn [e]
+                     (when (= e.key "Enter")
+                       (save-handler e)))}
+     [:div.grid.grid-cols-5.gap-1.items-center.leading-8
+      [:label.col-span-2 "Value:"]
+      (item-value property-type *value)]
+     [: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 save-handler})]]))
+
+(rum/defcs choice-with-close <
+  (rum/local false ::hover?)
+  [state item {:keys [toggle-fn delete-choice update-icon]}]
+  (let [*hover? (::hover? state)
+        value (or (:block/original-name item)
+                  (get-in item [:block/schema :value]))]
+    [: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 (pu/get-property item :icon)
+            {:on-chosen (fn [_e icon]
+                          (update-icon icon))})
+      [:a {:on-click toggle-fn}
+       value]]
+     (when @*hover?
+       [:a.fade-link.flex {:on-click delete-choice
+                           :title "Delete this choice"}
+        (ui/icon "X")])]))
+
+(rum/defc choice-item-content
+  [property block dropdown-opts]
+  (let [{:block/keys [uuid]} block]
+    (ui/dropdown
+     (fn [opts]
+       (choice-with-close
+        block
+        (assoc opts
+               :delete-choice
+               (fn []
+                 (property-handler/delete-closed-value property block))
+               :update-icon
+               (fn [icon]
+                 (property-handler/set-block-property! (state/get-current-repo) (:block/uuid block) :icon icon)))))
+     (fn [opts]
+       (item-config
+        property
+        block
+        (assoc opts :on-save
+               (fn [value icon description]
+                 (upsert-closed-value! property {:id uuid
+                                                 :value value
+                                                 :description description
+                                                 :icon icon})))))
+     dropdown-opts)))
+
+(rum/defc choices < rum/reactive
+  [property *property-name *property-schema]
+  (let [schema (:block/schema property)
+        property-type (:type schema)
+        values (:values schema)
+        dropdown-opts {:modal-class (util/hiccup->class
+                                     "origin-top-right.absolute.left-0.rounded-md.shadow-lg")}]
+    [:div.closed-values.flex.flex-col
+     (let [choices (doall
+                    (keep (fn [id]
+                            (when-let [block (db/sub-block (:db/id (db/entity [:block/uuid id])))]
+                              {:id (str id)
+                               :value id
+                               :content (choice-item-content property block dropdown-opts)}))
+                          values))]
+       (dnd/items choices
+                  {:on-drag-end (fn [new-values]
+                                  (when (seq new-values)
+                                    (swap! *property-schema assoc :values new-values)
+                                    (pu-component/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]
+        (if (= :page property-type)
+          (property-value/select-page property
+                                      {:multiple-choices? false
+                                       :dropdown? false
+                                       :close-modal? false
+                                       :on-chosen (fn [chosen]
+                                                    (upsert-closed-value! property {:value chosen}))})
+          (item-config
+           property
+           nil
+           (assoc opts :on-save
+                  (fn [value icon description]
+                    (upsert-closed-value! property {:value value
+                                                    :description description
+                                                    :icon icon}))))))
+      dropdown-opts)]))

+ 0 - 196
src/main/frontend/components/property/enum.cljs

@@ -1,196 +0,0 @@
-(ns frontend.components.property.enum
-  "Enum property config"
-  (:require [rum.core :as rum]
-            [clojure.string :as string]
-            [frontend.modules.shortcut.core :as shortcut]
-            [frontend.util :as util]
-            [frontend.ui :as ui]
-            [frontend.components.dnd :as dnd]
-            [frontend.components.icon :as icon-component]
-            [frontend.components.property.util :as components-pu]
-            [frontend.handler.notification :as notification]
-            [frontend.handler.property :as property-handler]
-            [frontend.db :as db]
-            [frontend.state :as state]
-            [frontend.handler.property.util :as pu]))
-
-(defn- upsert-enum-item!
-  "Create new enum value and returns its block UUID."
-  [property item]
-  (let [{:keys [block-id tx-data]} (property-handler/upsert-enum-item property item)]
-    (when (seq tx-data) (db/transact! tx-data))
-    block-id))
-
-(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 [block (first (:rum/args state))]
-             (let [name (or (:block/content block) "")
-                   icon (pu/get-property block :icon)
-                   description (or (get-in block [:block/schema :description]) "")]
-               (assoc state
-                      ::name (atom name)
-                      ::icon (atom icon)
-                      ::description (atom 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)
-                     (when on-save (on-save (string/trim @*name) @*icon @*description))
-                     (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 (pu/get-property item :icon)
-            {: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 block dropdown-opts]
-  (let [{:block/keys [uuid content]} block]
-    (ui/dropdown
-     (fn [opts]
-       (choice-with-close
-        block
-        content
-        (assoc opts
-               :delete-choice
-               (fn []
-                 (property-handler/delete-enum-item property block))
-               :update-icon
-               (fn [icon]
-                 (property-handler/update-property! (state/get-current-repo)
-                                                    (pu/get-pid "icon")
-                                                    icon)))))
-     (fn [opts]
-       (enum-item-config
-        block
-        (assoc opts :on-save
-               (fn [name icon description]
-                 (upsert-enum-item! property {:id uuid
-                                              :name name
-                                              :description description
-                                              :icon icon})))))
-     dropdown-opts)))
-
-(rum/defc enum-choices
-  [property *property-name *property-schema]
-  (let [values (get-in property [:block/schema :enum-config :values])
-        dropdown-opts {:modal-class (util/hiccup->class
-                                     "origin-top-right.absolute.left-0.rounded-md.shadow-lg")}]
-    [:div.enum-choices.flex.flex-col
-     (let [choices (keep (fn [id]
-                           (when-let [block (db/entity [:block/uuid id])]
-                             {:id (str id)
-                              :value id
-                              :content (choice-item-content property block dropdown-opts)}))
-                         values)]
-       (dnd/items choices
-                  {:on-drag-end (fn [new-values]
-                                  (when (seq new-values)
-                                    (swap! *property-schema assoc-in [:enum-config :values] new-values)
-                                    (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 icon description]
-                                (upsert-enum-item! property {:name name
-                                                             :description description
-                                                             :icon icon})))))
-      dropdown-opts)]))

+ 194 - 141
src/main/frontend/components/property/value.cljs

@@ -21,8 +21,10 @@
             [frontend.handler.property.util :as pu]))
 
 (defn- select-type?
-  [type]
-  (contains? #{:page :number :url :date :enum} type))
+  [property type]
+  (or (contains? #{:page :number :url :date} type)
+      ;; closed values
+      (seq (get-in property [:block/schema :values]))))
 
 (defn exit-edit-property
   ([]
@@ -62,9 +64,8 @@
     (route-handler/redirect-to-page! (date/js-date->journal-title value))))
 
 (rum/defc date-picker
-  [block property value opts]
-  (let [multiple-values? (= :many (:cardinality (:block/schema property)))
-        title (when (uuid? value)
+  [value {:keys [multiple-values? on-change] :as opts}]
+  (let [title (when (uuid? value)
                 (:block/original-name (db/entity [:block/uuid value])))
         value (if title
                 (js/Date. (date/journal-title->long title))
@@ -93,15 +94,12 @@
          (when-not title (ui/icon "calendar" {:size 15}))]])
      (fn [{:keys [toggle-fn]}]
        (ui/datepicker value' {:on-change (fn [_e date]
-                                           (let [repo (state/get-current-repo)
-                                                 journal (date/js-date->journal-title date)]
+                                           (let [journal (date/js-date->journal-title date)]
                                              (when-not (db/entity [:block/name (util/page-name-sanity-lc journal)])
                                                (page-handler/create! journal {:redirect? false
                                                                               :create-first-block? false}))
-                                             (when-let [page (db/entity [:block/name (util/page-name-sanity-lc journal)])]
-                                               (property-handler/set-block-property! repo (:block/uuid block)
-                                                                                     (:block/name property)
-                                                                                     (:block/uuid page)))
+                                             (when (fn? on-change)
+                                               (on-change (db/entity [:block/name (util/page-name-sanity-lc journal)])))
                                              (exit-edit-property)
                                              (toggle-fn)
                                              (when-let [toggle (:toggle-fn opts)]
@@ -109,6 +107,20 @@
      {:modal-class (util/hiccup->class
                     "origin-top-right.absolute.left-0.rounded-md.shadow-lg.mt-2")})))
 
+
+(rum/defc property-value-date-picker
+  [block property value opts]
+  (let [multiple-values? (= :many (:cardinality (:block/schema 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)
+                                                                              (:block/name property)
+                                                                              (:block/uuid page))
+                                        (exit-edit-property)))}))))
+
 (defn- create-page-if-not-exists!
   [property classes page]
   (let [page* (string/trim page)
@@ -153,29 +165,31 @@
                           :items items'
                           k f'))))
 
-(defn- select-page
-  [block property
-   {:keys [classes multiple-choices? dropdown?] :as opts}
-   {:keys [*show-new-property-config?]}]
+(defn select-page
+  [property
+   {:keys [block classes multiple-choices? dropdown? input-opts on-chosen] :as opts}]
   (let [repo (state/get-current-repo)
         tags? (= "tags" (:block/name property))
         alias? (= "alias" (:block/name property))
         tags-or-alias? (or tags? alias?)
-        selected-choices (->>
-                          (if tags-or-alias?
-                            (->> (if (= "tags" (:block/name property))
-                                   (:block/tags block)
-                                   (:block/alias block))
-                                 (map (fn [e] (:block/original-name e))))
-                            (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?))
+        selected-choices (when block
+                           (->>
+                            (if tags-or-alias?
+                              (->> (if (= "tags" (:block/name property))
+                                     (: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 (->>
-               (if (seq classes)
+               (cond
+                 (seq classes)
                  (mapcat
                   (fn [class]
                     (if (= :logseq.class class)
@@ -184,68 +198,86 @@
                                (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)
         options (map (fn [p] {:value p}) pages)
-        opts (cond->
-              {: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? true
-               :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-names (map #(:block/original-name (db/entity repo [:block/uuid %])) classes)
-                                       descendent-classes (->> class-names
-                                                               (mapcat #(db/get-namespace-pages repo %))
-                                                               (map :block/original-name))]
-                                   (->> (concat class-names descendent-classes)
-                                        (filter #(string/includes? % class-input))
-                                        (mapv #(hash-map :value (str new-page "#" %)))))
-                                 results))
-               :input-opts (fn [_]
-                             {:on-blur (fn []
-                                         (exit-edit-property))
-                              :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 opts)] (f)))
-                                  nil))})}
-               multiple-choices?
-               (assoc :on-apply (fn [choices]
-                                  (let [pages (->> choices
-                                                   (map #(create-page-if-not-exists! property classes %))
-                                                   (map first))
-                                        values (set (map #(pu/get-page-uuid repo %) pages))]
-                                    (add-property! block (:block/original-name property) values)
-                                    (when-let [f (:on-chosen opts)] (f)))))
-               (not multiple-choices?)
-               (assoc :on-chosen (fn [chosen]
-                                   (let [page* (string/trim (if (string? chosen) chosen (:value chosen)))]
-                                     (when-not (string/blank? page*)
-                                       (let [[page id] (create-page-if-not-exists! property classes page*)
-                                             id' (or id (pu/get-page-uuid repo page))]
-                                         (add-property! block (:block/original-name property) id')
-                                         (when-let [f (:on-chosen opts)] (f))))))))]
-    (select-aux block property opts)))
+        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-names (map #(:block/original-name (db/entity repo [:block/uuid %])) classes)
+                                         descendent-classes (->> class-names
+                                                                 (mapcat #(db/get-namespace-pages repo %))
+                                                                 (map :block/original-name))]
+                                     (->> (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]
+                                   (let [pages (->> choices
+                                                    (map #(create-page-if-not-exists! property classes %))
+                                                    (map first))
+                                         values (set (map #(pu/get-page-uuid repo %) pages))]
+                                     (when on-chosen (on-chosen values)))))
+                (not multiple-choices?)
+                (assoc :on-chosen (fn [chosen]
+                                    (let [page* (string/trim (if (string? chosen) chosen (:value chosen)))]
+                                      (when-not (string/blank? page*)
+                                        (let [[page id] (create-page-if-not-exists! property classes page*)
+                                              id' (or id (pu/get-page-uuid repo page))]
+                                          (when on-chosen (on-chosen id'))))))))]
+    (select-aux block property opts')))
+
+(defn property-value-select-page
+  [block property
+   {:keys [on-chosen] :as opts}
+   {:keys [*show-new-property-config?]}]
+  (let [input-opts (fn [_]
+                     {:on-blur (fn []
+                                 (exit-edit-property))
+                      :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 opts)] (f)))
+                          nil))})
+        opts' (assoc opts
+                     :block block
+                     :input-opts input-opts
+                     :on-chosen (fn [values]
+                                  (add-property! block (:block/original-name property) values)
+                                  (when on-chosen (on-chosen))))]
+    (select-page property opts')))
 
 ;; (defn- move-cursor
 ;;   [up? opts]
@@ -328,18 +360,19 @@
   (let [schema (:block/schema property)
         property (db/sub-block (:db/id property))
         type (:type schema)
-        enum? (= :enum type)
-        items (if enum?
+        closed-values? (seq (:values schema))
+        items (if closed-values?
                 (keep (fn [id]
                         (when-let [block (when id (db/entity [:block/uuid id]))]
                           (let [icon (pu/get-property block :icon)
-                                name (:block/content block)]
+                                value (or (:block/original-name block)
+                                          (get-in block [:block/schema :value]))]
                             {:label (if icon
                                       [:div.flex.flex-row.gap-2
                                        (icon-component/icon icon)
-                                       name]
-                                      name)
-                             :value id}))) (get-in schema [:enum-config :values]))
+                                       value]
+                                      value)
+                             :value id}))) (:values schema))
                 (->> (model/get-block-property-values (:block/uuid property))
                      (mapcat (fn [[_id value]]
                                (if (coll? value)
@@ -364,7 +397,7 @@
                   :items items
                   :selected-choices selected-choices
                   :dropdown? dropdown?
-                  :show-new-when-not-exact-match? (not (contains? #{:enum} type))
+                  :show-new-when-not-exact-match? (not closed-values?)
                   :input-default-placeholder "Select"
                   :extract-chosen-fn :value
                   :input-opts (fn [_]
@@ -382,7 +415,7 @@
                                        (exit-edit-property)
                                        (when-let [f (:on-chosen opts)] (f)))
                                      nil))})}
-                  enum?
+                  closed-values?
                   (assoc :extract-fn :label)
                   multiple-choices?
                   (assoc :on-apply on-chosen)
@@ -436,24 +469,40 @@
       invalid-warning)))
 
 (rum/defc select-item
-  [type value {:keys [page-cp inline-text]}]
-  (case type
-    (:page :date)
-    (when-let [page (db/entity [:block/uuid value])]
-      (page-cp {:disable-preview? true
-                :hide-close-button? true} page))
+  [property type value {:keys [page-cp inline-text]}]
+  (let [closed-values? (seq (get-in property [:block/schema :values]))]
+    (cond
+      (contains? #{:page :date} type)
+      (when-let [page (db/entity [:block/uuid value])]
+        (page-cp {:disable-preview? true
+                  :hide-close-button? true} page))
+
+      closed-values?
+      (when-let [block (when value (db/entity [:block/uuid value]))]
+        (let [value' (get-in block [:block/schema :value])
+              icon (pu/get-property block :icon)]
+          (cond
+            (:block/name block)
+            (page-cp {:disable-preview? true
+                      :hide-close-button? true} block)
+
+            icon
+            (icon-component/icon icon)
 
-    :number
-    [:span.number (str value)]
+            (= type :number)
+            [:span.number (str value')]
 
-    :enum
-    (when-let [block (when value (db/entity [:block/uuid value]))]
-      (let [name (:block/content block)]
-        (if-let [icon (pu/get-property block :icon)]
-          (icon-component/icon icon)
-          name)))
+            (= type :url)
+            (inline-text {} :markdown (str value'))
 
-    (inline-text {} :markdown (str value))))
+            :else
+            value')))
+
+      (= type :number)
+      [:span.number (str value)]
+
+      :else
+      (inline-text {} :markdown (str value)))))
 
 (rum/defc single-value-select
   [block property value value-f select-opts {:keys [editing?] :as opts}]
@@ -467,11 +516,11 @@
         select-f (fn []
                    [:div.property-select (cond-> {} editing? (assoc :class "h-6"))
                     (case type
-                      (:number :url :date :enum)
+                      (:number :url :date :default)
                       (select block property select-opts' opts)
 
                       :page
-                      (select-page block property select-opts' opts))])
+                      (property-value-select-page block property select-opts' opts))])
         dropdown-opts {:modal-class (util/hiccup->class
                                      "origin-top-right.absolute.left-0.rounded-md.shadow-lg.mt-2")
                        :initial-open? editing?}]
@@ -505,17 +554,19 @@
         multiple-values? (= :many (:cardinality schema))
         editor-id (or editor-id (str "ls-property-" (:db/id block) "-" (:db/id property)))
         editing? (or editing? (and @*ref (state/sub-editing? @*ref)))
-        select-type? (select-type? type)
+        select-type? (select-type? property type)
+        closed-values? (seq (:values schema))
         select-opts {:on-chosen on-chosen}]
-    (if (and select-type? (not= type :date))
+    (if (and select-type?
+             (not (and (not closed-values?) (= type :date))))
       (single-value-select block property value
                            (fn []
-                             (select-item type value opts))
+                             (select-item property type value opts))
                            select-opts
                            opts)
       (case type
         :date
-        (date-picker block property value nil)
+        (property-value-date-picker block property value nil)
 
         :checkbox
         (let [add-property! (fn []
@@ -593,9 +644,9 @@
                     (if (seq items)
                       (concat
                        (for [item items]
-                         (select-item type item opts))
+                         (select-item property type item opts))
                        (when date?
-                         [(date-picker block property nil {:toggle-fn toggle-fn})]))
+                         [(property-value-date-picker block property nil {:toggle-fn toggle-fn})]))
                       (when-not editing? [:div.opacity-50.pointer.text-sm "Empty"])))
         select-cp (fn []
                     (let [select-opts {:multiple-choices? true
@@ -604,10 +655,10 @@
                                                     (when on-chosen (on-chosen)))}]
                       [:div.property-select (cond-> {} editing? (assoc :class "h-6"))
                        (if (= :page type)
-                         (select-page block property
-                                      (assoc select-opts
-                                             :classes (:classes schema))
-                                      opts)
+                         (property-value-select-page block property
+                                                     (assoc select-opts
+                                                            :classes (:classes schema))
+                                                     opts)
                          (select block property select-opts opts))]))]
     (if (and dropdown? (not editing?))
       (ui/dropdown
@@ -627,21 +678,23 @@
 
 (rum/defc property-value < rum/reactive
   [block property v opts]
-  (let [dom-id (str "ls-property-" (:db/id block) "-" (:db/id property))
-        editor-id (str dom-id "-editor")
-        schema (:block/schema property)
-        multiple-values? (= :many (:cardinality schema))
-        editor-args {:block property
-                     :parent-block block
-                     :format :markdown}]
-    (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})))))
+  (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)
+         multiple-values? (= :many (:cardinality schema))
+         editor-args {:block property
+                      :parent-block block
+                      :format :markdown}]
+     (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}))))))

+ 0 - 3
src/main/frontend/components/select.cljs

@@ -54,9 +54,6 @@
                   (atom (set (:selected-choices (first (:rum/args state)))))))
    :will-unmount (fn [state]
                    (state/set-state! [:ui/open-select] nil)
-                   (let [{:keys [multiple-choices? on-chosen]} (first (:rum/args state))]
-                     (when (and multiple-choices? on-chosen)
-                       (on-chosen @(::selected-choices state))))
                    state)}
   [state {:keys [items limit on-chosen empty-placeholder
                  prompt-key input-default-placeholder close-modal?

+ 2 - 5
src/main/frontend/db/rtc/const.cljs

@@ -19,7 +19,8 @@
 (def general-attr-set
   (into #{} (map first) general-attrs-schema-coll))
 
-(def block-type-schema [:enum "property" "class" "whiteboard" "object" "hidden" "enum value"])
+(def block-type-schema [:enum "property" "class" "whiteboard" "object" "hidden" "closed value"])
+
 (def to-ws-op-schema
   [:multi {:dispatch first :decode/string #(update % 0 keyword)}
    [:move
@@ -119,10 +120,6 @@
 (def data-from-ws-coercer (m/coercer data-from-ws-schema mt/string-transformer))
 (def data-from-ws-validator (m/validator data-from-ws-schema))
 
-
-
-
-
 (def data-to-ws-schema
   (mu/closed-schema
    [:multi {:dispatch :action}

+ 5 - 5
src/main/frontend/handler/common/page.cljs

@@ -305,14 +305,14 @@
             truncate-blocks-tx-data (mapv
                                      (fn [block]
                                        [:db.fn/retractEntity [:block/uuid (:block/uuid block)]])
-                                     blocks)]
-        (if-let [msg (and (config/db-based-graph? repo)
-                          (page-unable-to-delete repo page))]
+                                     blocks)
+            db-based? (config/db-based-graph? repo)]
+        (if-let [msg (and db-based? (page-unable-to-delete repo page))]
           (do
             (db/transact! repo truncate-blocks-tx-data
-              {:outliner-op :delete-page :persist-op? persist-op?})
+                          {:outliner-op :delete-page :persist-op? persist-op?})
             (error-handler msg))
-          (let [_ (delete-file! repo page-name delete-file?)
+          (let [_ (when-not db-based? (delete-file! repo page-name delete-file?))
                 ;; if other page alias this pagename,
                 ;; then just remove some attrs of this entity instead of retractEntity
                 delete-page-tx (cond

+ 31 - 12
src/main/frontend/handler/db_based/property.cljs

@@ -1,7 +1,6 @@
 (ns frontend.handler.db-based.property
   "Properties handler for db graphs"
-  (:require [clojure.edn :as edn]
-            [clojure.string :as string]
+  (:require [clojure.string :as string]
             [frontend.db :as db]
             [frontend.db.model :as model]
             [frontend.handler.notification :as notification]
@@ -30,12 +29,24 @@
                  [property-type property-val-schema]))
              db-property-type/builtin-schema-types)))
 
+(defn- fail-parse-long
+  [v-str]
+  (let [result (parse-long v-str)]
+    (or result
+        (throw (js/Error. (str "Can't convert \"" v-str "\" to a number"))))))
+
+(defn- fail-parse-double
+  [v-str]
+  (let [result (parse-double v-str)]
+    (or result
+        (throw (js/Error. (str "Can't convert \"" v-str "\" to a number"))))))
+
 (defn- infer-schema-from-input-string
   [v-str]
   (try
     (cond
-      (parse-long v-str) :number
-      (parse-double v-str) :number
+      (fail-parse-long v-str) :number
+      (fail-parse-double v-str) :number
       (util/uuid-string? v-str) :page
       (gp-util/url? v-str) :url
       (contains? #{"true" "false"} (string/lower-case v-str)) :checkbox
@@ -48,19 +59,19 @@
   (if (and (not (string? v-str)) (not (object? v-str)))
     v-str
     (case schema-type
-      :default
-      (if (util/uuid-string? v-str) (uuid v-str) v-str)
-
       :number
-      (edn/read-string v-str)
+      (fail-parse-double v-str)
 
       :page
       (uuid v-str)
 
       ;; these types don't need to be translated. :date expects uuid and other
       ;; types usually expect text
-      (:enum :url :date :any)
-      v-str)))
+      (:url :date :any)
+      v-str
+
+      ;; :default
+      (if (util/uuid-string? v-str) (uuid v-str) v-str))))
 
 (defn upsert-property!
   [repo k-name schema {:keys [property-uuid]}]
@@ -82,6 +93,14 @@
                              (assoc :block/schema schema)))]
                     {:outliner-op :insert-blocks}))))
 
+(defn- validate-property-value
+  [property schema value]
+  (let [values (get-in property [:block/schema :values])]
+    (if (seq values)
+      (when-not (contains? (set values) value)
+        "Value is not included in the closed values.")
+      (me/humanize (mu/explain-data schema value)))))
+
 (defn- reset-block-property-multiple-values!
   [repo block-id k-name values _opts]
   (let [block (db/entity repo [:block/uuid block-id])
@@ -120,7 +139,7 @@
                              {:block/uuid block-id
                               attribute property-value-ids}]
                             {:outliner-op :save-block}))
-            (if-let [msg (some #(me/humanize (mu/explain-data schema %)) values')]
+            (if-let [msg (some #(validate-property-value property schema %) values')]
               (let [msg' (str "\"" k-name "\"" " " (if (coll? msg) (first msg) msg))]
                 (notification/show! msg' :warning))
               (do
@@ -193,7 +212,7 @@
                               [[:db/add (:db/id block) attribute property-value-id]]
                               {:outliner-op :save-block}))
               (when-not (contains? (if (set? value) value #{value}) v*)
-                (if-let [msg (me/humanize (mu/explain-data schema v*))]
+                (if-let [msg (validate-property-value property schema v*)]
                   (let [msg' (str "\"" k-name "\"" " " (if (coll? msg) (first msg) msg))]
                     (notification/show! msg' :warning))
                   (do

+ 54 - 32
src/main/frontend/handler/property.cljs

@@ -4,6 +4,7 @@
             [frontend.handler.file-based.property :as file-property-handler]
             [frontend.handler.file-based.page-property :as file-page-property]
             [frontend.handler.notification :as notification]
+            [logseq.db.frontend.property.type :as db-property-type]
             [frontend.config :as config]
             [frontend.util :as util]
             [frontend.state :as state]
@@ -150,21 +151,20 @@
      :all-classes all-classes           ; block own classes + parent classes
      :classes-properties all-properties}))
 
-(defn enum-other-position?
+(defn closed-value-other-position?
   [property-id block-properties]
   (and
    (some? (get block-properties property-id))
    (let [schema (:block/schema (db/entity [:block/uuid property-id]))]
-     (and (= :enum (:type schema))
-          (= (:position schema) "block-beginning")))))
+     (= (:position schema) "block-beginning"))))
 
-(defn get-block-enum-other-position-properties
+(defn get-block-other-position-properties
   [eid]
   (let [block (db/entity eid)
         own-properties (keys (:block/properties block))]
     (->> (:classes-properties (get-block-classes-properties eid))
          (concat own-properties)
-         (filter (fn [id] (enum-other-position? id (:block/properties block))))
+         (filter (fn [id] (closed-value-other-position? id (:block/properties block))))
          (distinct))))
 
 (defn block-has-viewable-properties?
@@ -240,27 +240,50 @@
     {:page page-tx
      :blocks [new-block]}))
 
-(defn upsert-enum-item
-  [property {:keys [id name icon description]}]
-  (when (= :enum (get-in property [:block/schema :type]))
-    (let [icon-id (pu/get-pid "icon")
-          name (string/trim name)
+(defn upsert-closed-value
+  "id should be a block UUID or nil"
+  [property {:keys [id value icon description]}]
+  (assert (or (nil? id) (uuid? id)))
+  (when (contains? db-property-type/closed-values-schema-types (get-in property [:block/schema :type] :default))
+    (let [value (if (string? value) (string/trim value) value)
           property-schema (:block/schema property)
-          enum-values (get-in property-schema [:enum-config :values])
-          block-values (map (fn [id] (db/entity [:block/uuid id])) enum-values)
-          icon (when-not (and (string? icon) (string/blank? icon)) icon)
-          description (string/trim description)
-          description (when-not (string/blank? description) description)]
-      (if (some (fn [b] (and (= name (:block/content b))
-                             (not= id (:block/uuid b)))) block-values)
-        (notification/show! "Choice exists already." :warning)
-        (let [block (when id (db/entity [:block/uuid id]))
-              block-id (or id (db/new-block-id))
+          closed-values (:values property-schema)
+          block-values (map (fn [id] (db/entity [:block/uuid id])) closed-values)
+          resolved-value (try
+                           (db-property-handler/convert-property-input-string (:type property-schema) value)
+                           (catch :default e
+                             (js/console.error e)
+                             (notification/show! (str e) :error false)
+                             nil))
+          block (when id (db/entity [:block/uuid id]))
+          value-block (when (uuid? value) (db/entity [:block/uuid value]))]
+      (cond
+        (nil? resolved-value)
+        nil
+
+        (some (fn [b] (and (= resolved-value (get-in b [:block/metadata :value]))
+                           (not= id (:block/uuid b)))) block-values)
+        (do
+          (notification/show! "Choice already exists" :warning)
+          :value-exists)
+
+        (:block/name value-block)             ; page
+        (let [new-values (vec (conj closed-values value))]
+          {:block-id value
+           :tx-data [{:db/id (:db/id property)
+                      :block/schema (assoc property-schema :values new-values)}]})
+
+        :else
+        (let [block-id (or id (db/new-block-id))
+              icon-id (pu/get-pid "icon")
+              icon (when-not (and (string? icon) (string/blank? icon)) icon)
+              description (string/trim description)
+              description (when-not (string/blank? description) description)
               tx-data (if block
                         [(let [properties (:block/properties block)
-                               schema (:block/schema block)]
+                               schema (assoc (:block/schema block)
+                                             :value resolved-value)]
                            {:block/uuid id
-                            :block/content name
                             :block/properties (if icon
                                                 (assoc properties icon-id icon)
                                                 (dissoc properties icon-id))
@@ -277,12 +300,11 @@
                               page-id [:block/uuid (:block/uuid page)]
                               metadata {:created-from-property (:block/uuid property)}
                               new-block (cond->
-                                         {:block/type #{"enum value"}
+                                         {:block/type #{"closed value"}
                                           :block/uuid block-id
-                                          :block/format :markdown
-                                          :block/content name
                                           :block/page page-id
                                           :block/metadata metadata
+                                          :block/schema {:value resolved-value}
                                           :block/parent page-id
                                           :block/left (or (when page-entity (model/get-block-last-direct-child (db/get-db) (:db/id page-entity)))
                                                           page-id)}
@@ -290,28 +312,28 @@
                                           (assoc :block/properties {icon-id icon})
 
                                           description
-                                          (assoc :block/schema {:description description})
+                                          (update :block/schema assoc :description description)
 
                                           true
                                           outliner-core/block-with-timestamps)
-                              new-values (vec (conj enum-values block-id))]
+                              new-values (vec (conj closed-values block-id))]
                           (->> (cons page-tx [new-block
                                               {:db/id (:db/id property)
-                                               :block/schema (assoc property-schema :enum-config {:values new-values})}])
+                                               :block/schema (assoc property-schema :values new-values)}])
                                (remove nil?))))]
           {:block-id block-id
            :tx-data tx-data})))))
 
-(defn delete-enum-item
+(defn delete-closed-value
   [property item]
   (if (seq (:block/_refs item))
     (notification/show! "The choice can't be deleted because it's still used." :warning)
     (let [schema (:block/schema property)
           tx-data [[:db/retractEntity (:db/id item)]
                    {:db/id (:db/id property)
-                    :block/schema (update-in schema [:enum-config :values]
-                                             (fn [values]
-                                               (vec (remove #{(:block/uuid item)} values))))}]]
+                    :block/schema (update schema :values
+                                          (fn [values]
+                                            (vec (remove #{(:block/uuid item)} values))))}]]
       (db/transact! tx-data))))
 
 (defn get-property-block-created-block

+ 4 - 2
src/main/frontend/handler/property/util.cljs

@@ -87,7 +87,9 @@
                 [(-> prop-ent
                      :block/name
                      keyword)
-                 (if (= :enum (get-in prop-ent [:block/schema :type]))
-                   (:block/content (db/entity [:block/uuid v]))
+                 (if (seq (get-in prop-ent [:block/schema :values])) ; closed values
+                   (when-let [block (db/entity [:block/uuid v])]
+                     (or (:block/original-name block)
+                         (get-in block [:block/schema :value])))
                    v)])))
        (into {})))

+ 1 - 1
src/main/frontend/ui.cljs

@@ -140,7 +140,7 @@
   [dropdown-state _close-fn content class style-opts]
   (let [class (or class
                   (util/hiccup->class "origin-top-right.absolute.right-0.mt-2"))]
-    [:div.dropdown-wrapper
+    [:div.dropdown-wrapper.max-h-screen.overflow-y-auto
      {:style style-opts
       :class (str class " "
                   (case dropdown-state