Просмотр исходного кода

enhance: allow changing property type after created

Tienson Qin 5 месяцев назад
Родитель
Сommit
d93a4566f5

+ 5 - 3
deps/db/src/logseq/db/sqlite/util.cljs

@@ -82,9 +82,11 @@
          :block/uuid (or block-uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident'))
          :block/title (name prop-name)
          :db/index true
-         :db/cardinality (if (#{:many :db.cardinality/many} (:db/cardinality prop-schema))
-                           :db.cardinality/many
-                           :db.cardinality/one)
+         :db/cardinality (if (= :checkbox prop-type)
+                           :db.cardinality/one
+                           (if (#{:many :db.cardinality/many} (:db/cardinality prop-schema))
+                             :db.cardinality/many
+                             :db.cardinality/one))
          :block/order (db-order/gen-key)}
          (or ref-type? (contains? db-property-type/all-ref-property-types prop-type))
          (assoc :db/valueType :db.type/ref)

+ 126 - 70
deps/outliner/src/logseq/outliner/property.cljs

@@ -166,51 +166,129 @@
       (and new-type old-ref-type? (not ref-type?))
       (conj [:db/retract (:db/id property) :db/valueType]))))
 
+(defn- create-user-property-ident-from-name
+  [property-name]
+  (try (db-property/create-user-property-ident-from-name property-name)
+       (catch :default e
+         (throw (ex-info (str e)
+                         {:type :notification
+                          :payload {:message "Property failed to create. Please try a different property name."
+                                    :type :error}})))))
+
+(defn- create-property
+  [conn db-ident property-id schema {:keys [property-name properties]}]
+  (let [db @conn
+        k-name (or (and property-name (name property-name))
+                   (name property-id))
+        db-ident' (db-ident/ensure-unique-db-ident @conn db-ident)]
+    (assert (some? k-name)
+            (prn "property-id: " property-id ", property-name: " property-name))
+    (outliner-validate/validate-page-title k-name {:node {:db/ident db-ident'}})
+    (outliner-validate/validate-page-title-characters k-name {:node {:db/ident db-ident'}})
+    (let [db-id (:db/id properties)
+          opts (cond-> {:title k-name
+                        :properties properties}
+                 (integer? db-id)
+                 (assoc :block-uuid (:block/uuid (d/entity db db-id))))
+          tx-data (concat
+                   [(sqlite-util/build-new-property db-ident' schema opts)]
+                          ;; Convert page to property
+                   (when db-id
+                     [[:db/retract db-id :block/tags :logseq.class/Page]]))]
+      {:db-ident db-ident'
+       :tx-data tx-data})))
+
 (defn- update-property
   [conn db-ident property schema {:keys [property-name properties]}]
-  (when (and (some? property-name) (not= property-name (:block/title property)))
-    (outliner-validate/validate-page-title property-name {:node property})
-    (outliner-validate/validate-page-title-characters property-name {:node property})
-    (outliner-validate/validate-block-title @conn property-name property)
-    (outliner-validate/validate-property-title property-name))
-
-  (let [changed-property-attrs
-        ;; Only update property if something has changed as we are updating a timestamp
-        (cond-> (->> (dissoc schema :db/cardinality)
-                     (keep (fn [[k v]]
-                             (when-not (= (get property k) v)
-                               [k v])))
-                     (into {}))
-          (and (some? property-name) (not= property-name (:block/title property)))
-          (assoc :block/title property-name
-                 :block/name (common-util/page-name-sanity-lc property-name)))
-        property-tx-data
-        (cond-> []
-          (seq changed-property-attrs)
-          (conj (outliner-core/block-with-updated-at
-                 (merge {:db/ident db-ident}
-                        changed-property-attrs)))
-          (and (seq schema)
-               (or (not= (:logseq.property/type schema) (:logseq.property/type property))
-                   (and (:db/cardinality schema) (not= (:db/cardinality schema) (keyword (name (:db/cardinality property)))))
-                   (and (= :default (:logseq.property/type schema)) (not= :db.type/ref (:db/valueType property)))
-                   (seq (:property/closed-values property))))
-          (concat (update-datascript-schema property schema)))
-        tx-data (concat property-tx-data
-                        (when (seq properties)
-                          (mapcat
-                           (fn [[property-id v]]
-                             (build-property-value-tx-data conn property property-id v)) properties)))
-        many->one? (and (db-property/many? property) (= :one (:db/cardinality schema)))]
-    (when (and many->one? (seq (d/datoms @conn :avet db-ident)))
-      (throw (ex-info "Disallowed many to one conversion"
-                      {:type :notification
-                       :payload {:message "This property can't change from multiple values to one value because it has existing data."
-                                 :type :warning}})))
-    (when (seq tx-data)
-      (ldb/transact! conn tx-data {:outliner-op :update-property
-                                   :property-id (:db/id property)}))
-    property))
+  (if (not= (:logseq.property/type schema) (:logseq.property/type property))
+    ;; Property type changed
+    ;; 1. create a new property
+    ;; 2. remove all existing property datoms
+    ;; 3. update tag properties to the new one
+    ;; 4. mark the old one deprecated
+    (let [new-db-ident' (create-user-property-ident-from-name (:block/title property))
+          m (create-property conn new-db-ident' nil
+                             (-> (select-keys schema [:logseq.property/type])
+                                 (assoc :db/cardinality (:db/cardinality property)))
+                             {:property-name (:block/title property)
+                              :properties (->> (dissoc (:block/properties property)
+                                                       :logseq.property/type
+                                                       :db.type/ref
+                                                       :db/cardinality
+                                                       :logseq.property/default-value
+                                                       :logseq.property/scalar-default-value)
+                                               (map (fn [[k v]]
+                                                      (let [v' (cond
+                                                                 (de/entity? v)
+                                                                 (or (:db/id v) (:db/ident v))
+                                                                 (every? de/entity? v)
+                                                                 (map (fn [v] (or (:db/id v) (:db/ident v))) v)
+                                                                 :else
+                                                                 v)]
+                                                        [k v'])))
+                                               (into {}))})
+          new-db-ident (:db-ident m)
+          new-property-tx-data (:tx-data m)
+          db @conn
+          retract-old-tx-data (->> (d/datoms db :avet db-ident)
+                                   (map (fn [d]
+                                          [:db/retract (:e d) db-ident])))
+          mark-old-property-as-deprecated [{:db/id (:db/id property)
+                                            :logseq.property/deprecated? true}]
+          class-properties-tx-data (->> (d/datoms db :avet :logseq.property.class/properties (:db/id property))
+                                        (mapcat (fn [d]
+                                                  [[:db/retract (:e d) :logseq.property.class/properties (:db/id property)]
+                                                   [:db/add (:e d) :logseq.property.class/properties new-db-ident]])))
+          full-tx-data (concat
+                        new-property-tx-data
+                        retract-old-tx-data
+                        mark-old-property-as-deprecated
+                        class-properties-tx-data)]
+      (ldb/transact! conn full-tx-data {:outliner-op :update-property-type})
+      (d/entity @conn new-db-ident))
+    (do
+      (when (and (some? property-name) (not= property-name (:block/title property)))
+        (outliner-validate/validate-page-title property-name {:node property})
+        (outliner-validate/validate-page-title-characters property-name {:node property})
+        (outliner-validate/validate-block-title @conn property-name property)
+        (outliner-validate/validate-property-title property-name))
+
+      (let [changed-property-attrs
+            ;; Only update property if something has changed as we are updating a timestamp
+            (cond-> (->> (dissoc schema :db/cardinality)
+                         (keep (fn [[k v]]
+                                 (when-not (= (get property k) v)
+                                   [k v])))
+                         (into {}))
+              (and (some? property-name) (not= property-name (:block/title property)))
+              (assoc :block/title property-name
+                     :block/name (common-util/page-name-sanity-lc property-name)))
+            property-tx-data
+            (cond-> []
+              (seq changed-property-attrs)
+              (conj (outliner-core/block-with-updated-at
+                     (merge {:db/ident db-ident}
+                            changed-property-attrs)))
+              (and (seq schema)
+                   (or (and (:db/cardinality schema) (not= (:db/cardinality schema) (keyword (name (:db/cardinality property)))))
+                       (and (= :default (:logseq.property/type schema)) (not= :db.type/ref (:db/valueType property)))
+                       (seq (:property/closed-values property))))
+              (concat (update-datascript-schema property schema)))
+            tx-data (concat property-tx-data
+                            (when (seq properties)
+                              (mapcat
+                               (fn [[property-id v]]
+                                 (build-property-value-tx-data conn property property-id v)) properties)))
+            many->one? (and (db-property/many? property) (= :one (:db/cardinality schema)))]
+        (when (and many->one? (seq (d/datoms @conn :avet db-ident)))
+          (throw (ex-info "Disallowed many to one conversion"
+                          {:type :notification
+                           :payload {:message "This property can't change from multiple values to one value because it has existing data."
+                                     :type :warning}})))
+        (when (seq tx-data)
+          (ldb/transact! conn tx-data {:outliner-op :update-property
+                                       :property-id (:db/id property)}))
+        property))))
 
 (defn- validate-property-value-aux
   [schema value {:keys [many?]}]
@@ -487,38 +565,16 @@
   "Updates property if property-id is given. Otherwise creates a property
    with the given property-id or :property-name option. When a property is created
    it is ensured to have a unique :db/ident"
-  [conn property-id schema {:keys [property-name properties] :as opts}]
+  [conn property-id schema {:keys [property-name _properties] :as opts}]
   (let [db @conn
         db-ident (or property-id
-                     (try (db-property/create-user-property-ident-from-name property-name)
-                          (catch :default e
-                            (throw (ex-info (str e)
-                                            {:type :notification
-                                             :payload {:message "Property failed to create. Please try a different property name."
-                                                       :type :error}})))))]
+                     (create-user-property-ident-from-name property-name))]
     (assert (qualified-keyword? db-ident))
     (if-let [property (and (qualified-keyword? property-id) (d/entity db db-ident))]
       (update-property conn db-ident property schema opts)
-      (let [k-name (or (and property-name (name property-name))
-                       (name property-id))
-            db-ident' (db-ident/ensure-unique-db-ident @conn db-ident)]
-        (assert (some? k-name)
-                (prn "property-id: " property-id ", property-name: " property-name))
-        (outliner-validate/validate-page-title k-name {:node {:db/ident db-ident'}})
-        (outliner-validate/validate-page-title-characters k-name {:node {:db/ident db-ident'}})
-        (let [db-id (:db/id properties)
-              opts (cond-> {:title k-name
-                            :properties properties}
-                     (integer? db-id)
-                     (assoc :block-uuid (:block/uuid (d/entity db db-id))))]
-          (ldb/transact! conn
-                         (concat
-                          [(sqlite-util/build-new-property db-ident' schema opts)]
-                          ;; Convert page to property
-                          (when db-id
-                            [[:db/retract db-id :block/tags :logseq.class/Page]]))
-                         {:outliner-op :upsert-property}))
-        (d/entity @conn db-ident')))))
+      (let [{:keys [db-ident tx-data]} (create-property conn db-ident property-id schema opts)]
+        (ldb/transact! conn tx-data {:outliner-op :upsert-property})
+        (d/entity @conn db-ident)))))
 
 (defn batch-delete-property-value!
   "batch delete value when a property has multiple values"

+ 20 - 19
src/main/frontend/components/property/config.cljs

@@ -541,13 +541,15 @@
   [property {:keys [id set-sub-open! _position]}]
   (let [handle-select! (fn [^js e]
                          (when-let [v (some-> (.-target e) (.-dataset) (.-value))]
-                           (p/do!
-                            (db-property-handler/upsert-property!
-                             (:db/ident property)
-                             {:logseq.property/type (keyword v)}
-                             {})
-                            (set-sub-open! false)
-                            (restore-root-highlight-item! id))))
+                           (p/let [property' (db-property-handler/upsert-property!
+                                              (:db/ident property)
+                                              {:logseq.property/type (keyword v)}
+                                              {})
+                                   new-property (db/entity (:db/id property'))]
+                             (set-sub-open! false)
+                             (restore-root-highlight-item! id)
+                             ;; redirect to the new property page
+                             (route-handler/redirect-to-page! (:block/uuid new-property)))))
         item-props {:on-select handle-select!}
         schema-types (->> db-property-type/user-built-in-property-types
                           (map (fn [type]
@@ -601,18 +603,17 @@
       (when-not special-built-in-prop?
         (dropdown-editor-menuitem {:icon :pencil :title "Property name" :desc [:span.flex.items-center.gap-1 icon title]
                                    :submenu-content (fn [ops] (name-edit-pane property (assoc ops :disabled? disabled?)))}))
-      (let [disabled?' (or disabled? (and property-type (seq values)))]
-        (dropdown-editor-menuitem {:icon :letter-t
-                                   :title "Property type"
-                                   :desc (if disabled?'
-                                           (ui/tooltip
-                                            [:span (str property-type-label')]
-                                            [:div.w-96
-                                             "The type of this property is locked once you start using it. This is to make sure all your existing information stays correct if the property type is changed later. To unlock, all uses of a property must be deleted."])
-                                           (str property-type-label'))
-                                   :disabled? disabled?'
-                                   :submenu-content (fn [ops]
-                                                      (property-type-sub-pane property ops))}))
+      (dropdown-editor-menuitem {:icon :letter-t
+                                 :title "Property type"
+                                 :desc (if disabled?
+                                         (ui/tooltip
+                                          [:span (str property-type-label')]
+                                          [:div.w-96
+                                           "The type of this property is locked."])
+                                         (str property-type-label'))
+                                 :disabled? disabled?
+                                 :submenu-content (fn [ops]
+                                                    (property-type-sub-pane property ops))})
 
       (when (and (= property-type :node)
                  (not (contains? #{:logseq.property.class/extends} (:db/ident property))))

+ 9 - 8
src/main/frontend/worker/handler/page.cljs

@@ -49,14 +49,15 @@
       (when (seq refs)
         (let [tx-data (mapcat (fn [{:block/keys [raw-title] :as ref}]
                                 ;; block content
-                                (let [content' (id-ref->page raw-title)
-                                      content-tx (when (not= raw-title content')
-                                                   {:db/id (:db/id ref)
-                                                    :block/title content'})
-                                      tx content-tx]
-                                  (concat
-                                   [[:db/retract (:db/id ref) :block/refs (:db/id page-entity)]]
-                                   (when tx [tx])))) refs)]
+                                (when raw-title
+                                  (let [content' (id-ref->page raw-title)
+                                        content-tx (when (not= raw-title content')
+                                                     {:db/id (:db/id ref)
+                                                      :block/title content'})
+                                        tx content-tx]
+                                    (concat
+                                     [[:db/retract (:db/id ref) :block/refs (:db/id page-entity)]]
+                                     (when tx [tx]))))) refs)]
           tx-data)))))
 
 (defn delete!