diff --git a/.gitignore b/.gitignore index 2e0a2f54ff..ca297e90df 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ resources/electron.js .clj-kondo/metosin/malli .clj-kondo/rewrite-clj .clj-kondo/taoensso +.clj-kondo/funcool /libs/dist/ charlie/ .vscode diff --git a/deps/db/.carve/ignore b/deps/db/.carve/ignore index 0aea51509a..0abf81c40d 100644 --- a/deps/db/.carve/ignore +++ b/deps/db/.carve/ignore @@ -15,6 +15,8 @@ logseq.db.frontend.inputs/resolve-input ;; API logseq.db.frontend.class/build-new-class ;; API +logseq.db.frontend.class/page-children-classes +;; API logseq.db.frontend.db-ident/ensure-unique-db-ident ;; API logseq.db.sqlite.build/create-blocks diff --git a/deps/db/src/logseq/db.cljs b/deps/db/src/logseq/db.cljs index f7a2086ba3..722ffb7975 100644 --- a/deps/db/src/logseq/db.cljs +++ b/deps/db/src/logseq/db.cljs @@ -17,7 +17,8 @@ [logseq.db.sqlite.util :as sqlite-util] [logseq.db.frontend.property :as db-property] [logseq.common.util.namespace :as ns-util] - [logseq.common.util.page-ref :as page-ref]) + [logseq.common.util.page-ref :as page-ref] + [clojure.walk :as walk]) (:refer-clojure :exclude [object?])) (defonce *transact-fn (atom nil)) @@ -40,11 +41,25 @@ m)) tx-data))) +(defn assert-no-entities + [tx-data] + (walk/prewalk + (fn [f] + (if (de/entity? f) + (throw (ex-info "ldb/transact! doesn't support Entity" + {:entity f + :tx-data tx-data})) + f)) + tx-data)) + (defn transact! "`repo-or-conn`: repo for UI thread and conn for worker/node" ([repo-or-conn tx-data] (transact! repo-or-conn tx-data nil)) ([repo-or-conn tx-data tx-meta] + (when (and (exists? js/goog) + (aget js/goog "DEBUG")) + (assert-no-entities tx-data)) (let [tx-data (map (fn [m] (if (map? m) (dissoc m :block/children :block/meta :block/top? :block/bottom? :block/anchor @@ -85,6 +100,10 @@ (def object? entity-util/object?) (def asset? entity-util/asset?) (def public-built-in-property? db-property/public-built-in-property?) +(def get-entity-types entity-util/get-entity-types) +(def internal-tags db-class/internal-tags) +(def private-tags db-class/private-tags) +(def hidden-tags db-class/hidden-tags) (defn sort-by-order [blocks] @@ -176,33 +195,39 @@ (def db-based-graph? entity-util/db-based-graph?) (defn page-exists? - "Whether a page exists with the `type`." - [db page-name type'] + "Returns truthy value if page exists. + For db graphs, returns all page db ids that given title and one of the given `tags`. + For file graphs, returns page entity if it exists" + [db page-name tags] (when page-name (if (db-based-graph? db) - ;; Classes and properties are case sensitive - (if (#{"class" "property"} type') - (seq - (d/q - '[:find [?p ...] - :in $ ?name ?type - :where - [?p :block/title ?name] - [?p :block/type ?type]] - db - page-name - type')) - ;; TODO: Decouple db graphs from file specific :block/name - (seq - (d/q - '[:find [?p ...] - :in $ ?name ?type - :where - [?p :block/name ?name] - [?p :block/type ?type]] - db - (common-util/page-name-sanity-lc page-name) - type'))) + (let [tags' (if (coll? tags) (set tags) #{tags})] + ;; Classes and properties are case sensitive and can be looked up + ;; as such in case-sensitive contexts e.g. no Page + (if (and (seq tags') (every? #{:logseq.class/Tag :logseq.class/Property} tags')) + (seq + (d/q + '[:find [?p ...] + :in $ ?name [?tag-ident ...] + :where + [?p :block/title ?name] + [?p :block/tags ?tag] + [?tag :db/ident ?tag-ident]] + db + page-name + tags')) + ;; TODO: Decouple db graphs from file specific :block/name + (seq + (d/q + '[:find [?p ...] + :in $ ?name [?tag-ident ...] + :where + [?p :block/name ?name] + [?p :block/tags ?tag] + [?tag :db/ident ?tag-ident]] + db + (common-util/page-name-sanity-lc page-name) + tags')))) (d/entity db [:block/name (common-util/page-name-sanity-lc page-name)])))) (defn get-page @@ -448,7 +473,7 @@ (d/datoms db :avet :block/name) (keep (fn [d] (let [e (d/entity db (:e d))] - (when-not (hidden? e) + (when-not (or (hidden? e) (internal-tags (:db/ident e))) e)))))) (defn built-in? @@ -535,7 +560,7 @@ (defn get-all-properties [db] - (->> (d/datoms db :avet :block/type "property") + (->> (d/datoms db :avet :block/tags :logseq.class/Property) (map (fn [d] (d/entity db (:e d)))))) @@ -554,7 +579,7 @@ (defn get-title-with-parents [entity] - (if (contains? #{"page" "class"} (:block/type entity)) + (if (or (entity-util/class? entity) (entity-util/internal-page? entity)) (let [parents' (->> (get-page-parents entity) (remove (fn [e] (= :logseq.class/Root (:db/ident e)))) vec)] diff --git a/deps/db/src/logseq/db/frontend/class.cljs b/deps/db/src/logseq/db/frontend/class.cljs index 70aff139a2..201ffef9c5 100644 --- a/deps/db/src/logseq/db/frontend/class.cljs +++ b/deps/db/src/logseq/db/frontend/class.cljs @@ -2,20 +2,35 @@ "Class related fns for DB graphs and frontend/datascript usage" (:require [logseq.db.sqlite.util :as sqlite-util] [logseq.db.frontend.db-ident :as db-ident] + [clojure.set :as set] [flatland.ordered.map :refer [ordered-map]])) +;; Main class vars +;; =============== + (def ^:large-vars/data-var built-in-classes "Map of built-in classes for db graphs with their :db/ident as keys" (ordered-map :logseq.class/Root {:title "Root Tag"} - :logseq.class/Task - {:title "Task" - :schema {:properties [:logseq.task/status :logseq.task/priority :logseq.task/deadline]}} + :logseq.class/Tag {:title "Tag"} + + :logseq.class/Property {:title "Property"} + + :logseq.class/Page {:title "Page"} :logseq.class/Journal {:title "Journal" - :properties {:logseq.property.journal/title-format "MMM do, yyyy"}} + :properties {:logseq.property/parent :logseq.class/Page + :logseq.property.journal/title-format "MMM do, yyyy"}} + + :logseq.class/Whiteboard + {:title "Whiteboard" + :properties {:logseq.property/parent :logseq.class/Page}} + + :logseq.class/Task + {:title "Task" + :schema {:properties [:logseq.task/status :logseq.task/priority :logseq.task/deadline]}} :logseq.class/Query {:title "Query" @@ -66,6 +81,31 @@ ;; TODO: Add more classes such as :book, :paper, :movie, :music, :project) )) +(def page-children-classes + "Children of :logseq.class/Page" + (set + (keep (fn [[class-ident m]] + (when (= (get-in m [:properties :logseq.property/parent]) :logseq.class/Page) class-ident)) + built-in-classes))) + +(def internal-tags + "Built-in classes that are hidden on a node and all pages view" + #{:logseq.class/Page :logseq.class/Property :logseq.class/Tag :logseq.class/Root + :logseq.class/Asset}) + +(def private-tags + "Built-in classes that are private and should not be used by a user directly. + These used to be in :block/type" + (set/union internal-tags + #{:logseq.class/Journal :logseq.class/Whiteboard})) + +(def hidden-tags + "Built-in classes that are hidden in a few contexts like property values" + #{:logseq.class/Page :logseq.class/Root :logseq.class/Asset}) + +;; Helper fns +;; ========== + (defn create-user-class-ident-from-name "Creates a class :db/ident for a default user namespace. NOTE: Only use this when creating a db-ident for a new class." @@ -78,5 +118,6 @@ [db page-m] {:pre [(string? (:block/title page-m))]} (let [db-ident (create-user-class-ident-from-name (:block/title page-m)) - db-ident' (db-ident/ensure-unique-db-ident db db-ident)] + db-ident' (or (:db/ident page-m) + (db-ident/ensure-unique-db-ident db db-ident))] (sqlite-util/build-new-class (assoc page-m :db/ident db-ident')))) diff --git a/deps/db/src/logseq/db/frontend/entity_plus.cljc b/deps/db/src/logseq/db/frontend/entity_plus.cljc index 8eb511f86d..7d5bd212ea 100644 --- a/deps/db/src/logseq/db/frontend/entity_plus.cljc +++ b/deps/db/src/logseq/db/frontend/entity_plus.cljc @@ -27,7 +27,7 @@ [^Entity e k default-value] (let [db (.-db e) db-based? (db-based-graph? db)] - (if (and db-based? (= "journal" (:block/type e))) + (if (and db-based? (entity-util/journal? e)) (get-journal-title db e) (let [search? (get (.-kv e) :block.temp/search?)] (or @@ -65,7 +65,7 @@ (let [db (.-db e)] (case k :block/raw-title - (if (and (db-based-graph? db) (= "journal" (:block/type e))) + (if (and (db-based-graph? db) (entity-util/journal? e)) (get-journal-title db e) (lookup-entity e :block/title default-value)) diff --git a/deps/db/src/logseq/db/frontend/entity_util.cljs b/deps/db/src/logseq/db/frontend/entity_util.cljs index 84c57d3358..b7fa28e2ef 100644 --- a/deps/db/src/logseq/db/frontend/entity_util.cljs +++ b/deps/db/src/logseq/db/frontend/entity_util.cljs @@ -1,7 +1,8 @@ (ns logseq.db.frontend.entity-util "Lower level entity util fns used across db namespaces" (:require [datascript.core :as d] - [clojure.string :as string]) + [clojure.string :as string] + [datascript.impl.entity :as de]) (:refer-clojure :exclude [object?])) (defn db-based-graph? @@ -10,36 +11,60 @@ (when db (= "db" (:kv/value (d/entity db :logseq.kv/db-type))))) -(defn page? - [block] - (contains? #{"page" "journal" "whiteboard" "class" "property"} - (:block/type block))) +(defn- has-tag? + [entity tag-ident] + (let [tags (:block/tags entity)] + (some (fn [t] (or (= (:db/ident t) tag-ident) + (= t tag-ident))) + (if (coll? tags) tags [tags])))) (defn internal-page? [entity] - (= (:block/type entity) "page")) + (has-tag? entity :logseq.class/Page)) (defn class? [entity] - (= (:block/type entity) "class")) + (or (= (:db/ident entity) :logseq.class/Tag) + (has-tag? entity :logseq.class/Tag))) (defn property? [entity] - (= (:block/type entity) "property")) - -(defn closed-value? - [entity] - (= (:block/type entity) "closed value")) + (has-tag? entity :logseq.class/Property)) (defn whiteboard? "Given a page entity or map, check if it is a whiteboard page" - [page] - (= (:block/type page) "whiteboard")) + [entity] + (or + ;; db based graph + (has-tag? entity :logseq.class/Whiteboard) + ;; file based graph + (= "whiteboard" (:block/type entity)))) + +(defn closed-value? + [entity] + (some? (:block/closed-value-property entity))) (defn journal? "Given a page entity or map, check if it is a journal page" - [page] - (= (:block/type page) "journal")) + [entity] + (or + ;; db based graph + (has-tag? entity :logseq.class/Journal) + ;; file based graph + (= "journal" (:block/type entity)))) + +(defn page? + [entity] + (or + ;; db based graph + (internal-page? entity) + (class? entity) + (property? entity) + (whiteboard? entity) + (journal? entity) + + ;; file based graph + (contains? #{"page" "journal" "whiteboard"} (:block/type entity)))) (defn asset? "Given an entity or map, check if it is an asset block" @@ -52,9 +77,19 @@ (when page (if (string? page) (string/starts-with? page "$$$") - (when (map? page) + (when (or (map? page) (de/entity? page)) (false? (get-in page [:block/schema :public?])))))) (defn object? [node] (seq (:block/tags node))) + +(defn get-entity-types + "Get entity types from :block/tags" + [entity] + (let [ident->type {:logseq.class/Tag :class + :logseq.class/Property :property + :logseq.class/Journal :journal + :logseq.class/Whiteboard :whiteboard + :logseq.class/Page :page}] + (set (map #(ident->type (:db/ident %)) (:block/tags entity))))) \ No newline at end of file diff --git a/deps/db/src/logseq/db/frontend/malli_schema.cljs b/deps/db/src/logseq/db/frontend/malli_schema.cljs index 3872bd5787..1a4abdcb37 100644 --- a/deps/db/src/logseq/db/frontend/malli_schema.cljs +++ b/deps/db/src/logseq/db/frontend/malli_schema.cljs @@ -70,6 +70,15 @@ (= :logseq.property/empty-placeholder (:db/ident (d/entity db property-val)))) (= :logseq.property/empty-placeholder property-val))) +(defn internal-ident? + "Determines if given ident is created by Logseq. All Logseq internal idents + must start with 'block' or 'logseq' to keep Logseq internals from leaking + across namespaces and to allow for users and 3rd party plugins to choose + any other namespace" + [ident] + (or (contains? db-property/db-attribute-properties ident) + (contains? logseq-ident-namespaces (namespace ident)))) + (defn validate-property-value "Validates the property value in a property tuple. The property value is expected to be a coll if the property has a :many cardinality. validate-fn is @@ -77,7 +86,7 @@ validate-fn varies by property type" [db validate-fn [{:block/keys [schema] :as property} property-val] & {:keys [new-closed-value?]}] ;; For debugging - ;; (when (not (string/starts-with? (namespace (:db/ident property)) "logseq.")) (prn :validate-val (dissoc property :property/closed-values) property-val)) + ;; (when (not (internal-ident? (:db/ident property))) (prn :validate-val (dissoc property :property/closed-values) property-val)) (let [validate-fn' (if (db-property-type/property-types-with-db (:type schema)) (fn [value] (validate-fn db value {:new-closed-value? new-closed-value?})) @@ -104,30 +113,47 @@ (set (get-in db-class/built-in-classes [:logseq.class/Asset :schema :required-properties])) #{:logseq.property/created-from-property})) +(defn- property-entity->map + "Provide the minimal number of property attributes to validate the property + and to reduce noise in error messages. The resulting map should be the same as + what the frontend property since they both call validate-property-value" + [property] + ;; use explicit call to be nbb compatible + (let [closed-values (entity-plus/lookup-kv-then-entity property :property/closed-values)] + (cond-> (assoc (select-keys property [:db/ident :db/valueType :db/cardinality]) + :block/schema + (select-keys (:block/schema property) [:type])) + (seq closed-values) + (assoc :property/closed-values closed-values)))) + (defn update-properties-in-ents "Prepares properties in entities to be validated by DB schema" [db ents] - (mapv - (fn [ent] - (reduce (fn [m [k v]] - (if-let [property (and (db-property/property? k) - ;; This allows schemas like property-value-block to require properties in - ;; their schema that they depend on - (not (contains? required-properties k)) - (d/entity db k))] - (update m :block/properties (fnil conj []) - ;; use explicit call to be nbb compatible - [(let [closed-values (entity-plus/lookup-kv-then-entity property :property/closed-values)] - (cond-> (assoc (select-keys property [:db/ident :db/valueType :db/cardinality]) - :block/schema - (select-keys (:block/schema property) [:type])) - (seq closed-values) - (assoc :property/closed-values closed-values))) - v]) - (assoc m k v))) - {} - ent)) - ents)) + ;; required-properties allows schemas like property-value-block to require + ;; properties in their schema that they depend on + (let [exceptions-to-block-properties (conj required-properties :block/tags) + page-class-id (:db/id (d/entity db :logseq.class/Page)) + private-tag-ids (set (map #(:db/id (d/entity db %)) db-class/private-tags))] + (mapv + (fn [ent] + (reduce (fn [m [k v]] + (if-let [property (and (db-property/property? k) + (not (contains? exceptions-to-block-properties k)) + (d/entity db k))] + (update m :block/properties (fnil conj []) + [(property-entity->map property) v]) + (if (= :block/tags k) + ;; Provides additional options map to validation for data about current entity being tagged + (let [property (d/entity db :block/tags)] + (assoc m k [(property-entity->map property) + v + (merge (select-keys ent [:logseq.property/built-in?]) + {:page-class-id page-class-id + :private-tag-ids private-tag-ids})])) + (assoc m k v)))) + {} + ent)) + ents))) (defn datoms->entity-maps "Returns entity maps for given :eavt datoms indexed by db/id. Optional keys: @@ -170,15 +196,6 @@ (mapv (fn [[db-id m]] (with-meta m {:db/id db-id})) (datoms->entity-maps datoms))) -(defn internal-ident? - "Determines if given ident is created by Logseq. All Logseq internal idents - must start with 'block' or 'logseq' to keep Logseq internals from leaking - across namespaces and to allow for users and 3rd party plugins to choose - any other namespace" - [ident] - (or (contains? db-property/db-attribute-properties ident) - (contains? logseq-ident-namespaces (namespace ident)))) - (assert (every? #(re-find #"^(block|logseq\.)" (namespace %)) db-property/db-attribute-properties) "All db-attribute idents start with an internal namespace") (assert (every? #(re-find #"^logseq\." %) logseq-ident-namespaces) @@ -188,14 +205,12 @@ ;; ================== ;; These schemas should be data vars to remain as simple and reusable as possible - (def ^:dynamic *db-for-validate-fns* "Used by validate-fns which need db as input" nil) (def property-tuple - "A tuple of a property map and a property value. This schema - has 1 metadata hook which is used to inject a datascript db later" + "A tuple of a property map and a property value" (into [:multi {:dispatch #(-> % first :block/schema :type)}] (map (fn [[prop-type value-schema]] @@ -211,6 +226,24 @@ property with its property value that is valid for its type" [:sequential property-tuple]) +(def block-tags + [:and + ;; FIXME: Display error message instead of 'unknown error' + property-tuple + ;; Important to keep data integrity of built-in entities. Ensure UI doesn't accidentally modify them + [:fn {:error/message "should only have one tag for a built-in entity"} + (fn [[_k v opts]] + (if (:logseq.property/built-in? opts) + (= 1 (count v)) + true))] + ;; Ensure use of :logseq.class/Page is consistent and simple. Doing so reduces complexity elsewhere + ;; and allows for Page to exist as its own public concept later + #_[:fn {:error/message "should not have other built-in private tags when tagged with #Page"} + (fn [[_k v {:keys [page-class-id private-tag-ids]}]] + (if (contains? v page-class-id) + (empty? (set/intersection (disj v page-class-id) private-tag-ids)) + true))]]) + (def page-or-block-attrs "Common attributes for page and normal blocks" [[:block/uuid :uuid] @@ -219,8 +252,8 @@ [:block/format [:enum :markdown]] ;; Injected by update-properties-in-ents [:block/properties {:optional true} block-properties] + [:block/tags {:optional true} block-tags] [:block/refs {:optional true} [:set :int]] - [:block/tags {:optional true} [:set :int]] [:block/tx-id {:optional true} :int] [:block/collapsed? {:optional true} :boolean]]) @@ -228,12 +261,7 @@ "Common attributes for pages" [[:block/name :string] [:block/title :string] - [:block/type [:enum "page" "class" "property" "whiteboard" "journal"]] - [:block/alias {:optional true} [:set :int]] - ;; TODO: Should this be here or in common? - [:block/path-refs {:optional true} [:set :int]] - ;; file-based - [:block/namespace {:optional true} :int]]) + [:block/path-refs {:optional true} [:set :int]]]) (def property-attrs "Common attributes for properties" @@ -302,7 +330,8 @@ (vec (concat [:map - [:db/ident user-property-ident] + ;; class-ident allows for a class to be used as a property + [:db/ident [:or user-property-ident class-ident]] [:block/schema user-property-schema]] property-attrs page-attrs @@ -364,8 +393,7 @@ (vec (concat [:map] - [[:block/type [:= "closed value"]] - ;; for built-in properties + [;; for built-in properties [:db/ident {:optional true} logseq-property-ident] [:block/title {:optional true} :string] [:property.value/content {:optional true} [:or :string :double]] @@ -436,27 +464,30 @@ (into [:multi {:dispatch (fn [d] ;; order matters as some block types are a subset of others e.g. :whiteboard - (cond - (entity-util/property? d) - :property - (entity-util/class? d) - :class - (entity-util/hidden? d) - :hidden - (entity-util/whiteboard? d) - :normal-page - (entity-util/page? d) - :normal-page - (entity-util/asset? d) - :asset-block - (:file/path d) - :file-block - (:block/uuid d) - :block - (= (:db/ident d) :logseq.property/empty-placeholder) - :property-value-placeholder - (:db/ident d) - :db-ident-key-value))}] + (let [db *db-for-validate-fns* + d (if (:block/uuid d) (d/entity db [:block/uuid (:block/uuid d)]) d) + dispatch-key (cond + (entity-util/property? d) + :property + (entity-util/class? d) + :class + (entity-util/hidden? (:block/title d)) + :hidden + (entity-util/whiteboard? d) + :normal-page + (entity-util/page? d) + :normal-page + (entity-util/asset? d) + :asset-block + (:file/path d) + :file-block + (:block/uuid d) + :block + (= (:db/ident d) :logseq.property/empty-placeholder) + :property-value-placeholder + (:db/ident d) + :db-ident-key-value)] + dispatch-key))}] {:property property-page :class class-page :hidden hidden-page @@ -479,6 +510,7 @@ (let [malli-many-ref-attrs (->> (concat property-attrs page-attrs block-attrs page-or-block-attrs (rest closed-value-block*)) (filter #(= (last %) [:set :int])) (map first) + (into db-property/public-db-attribute-properties) set)] (when-let [undeclared-ref-attrs (seq (remove malli-many-ref-attrs db-schema/card-many-ref-type-attributes))] (throw (ex-info (str "The malli DB schema is missing the following cardinality-many ref attributes from datascript's schema: " diff --git a/deps/db/src/logseq/db/frontend/order.cljs b/deps/db/src/logseq/db/frontend/order.cljs index 958dd51ff8..34008c48af 100644 --- a/deps/db/src/logseq/db/frontend/order.cljs +++ b/deps/db/src/logseq/db/frontend/order.cljs @@ -10,8 +10,8 @@ (reset-max-key! *max-key key)) ([max-key-atom key] (when (and key (or (nil? @max-key-atom) - (> (compare key @max-key-atom) 0))) - (reset! max-key-atom key)))) + (> (compare key @max-key-atom) 0))) + (reset! max-key-atom key)))) (defn gen-key ([] @@ -50,7 +50,7 @@ (when (and (< (compare (:block/order e) (:block/order value)) 0) (not= (:db/id e) (:db/id value))) (:block/order e))) values)) - (let [properties (->> (d/datoms db :avet :block/type "property") + (let [properties (->> (d/datoms db :avet :block/tags :logseq.class/Property) (map (fn [d] (d/entity db (:e d)))) (sort-by :block/order) reverse)] @@ -68,7 +68,7 @@ (when (and (> (compare (:block/order e) (:block/order value)) 0) (not= (:db/id e) (:db/id value))) (:block/order e))) values)) - (let [properties (->> (d/datoms db :avet :block/type "property") + (let [properties (->> (d/datoms db :avet :block/tags :logseq.class/Property) (map (fn [d] (d/entity db (:e d)))) (sort-by :block/order))] (some (fn [property] diff --git a/deps/db/src/logseq/db/frontend/property.cljs b/deps/db/src/logseq/db/frontend/property.cljs index f0621709c2..e1362a723b 100644 --- a/deps/db/src/logseq/db/frontend/property.cljs +++ b/deps/db/src/logseq/db/frontend/property.cljs @@ -48,12 +48,6 @@ :schema {:type :any :public? false :hide? true}} - :block/type {:title "Node Type" - :attribute :block/type - :schema {:type :string - :public? false - :hide? true} - :queryable? true} :block/schema {:title "Node schema" :attribute :block/schema :schema {:type :map @@ -451,13 +445,18 @@ (def db-attribute-properties "Internal properties that are also db schema attributes" - #{:block/alias :block/tags :block/type :block/schema :block/parent + #{:block/alias :block/tags :block/schema :block/parent :block/order :block/collapsed? :block/page :block/refs :block/path-refs :block/link :block/title :block/closed-value-property :block/created-at :block/updated-at :logseq.property.attribute/kv-value :logseq.property.attribute/property-schema-classes :logseq.property.attribute/property-value-content}) +(assert (= db-attribute-properties + (set (keep (fn [[k {:keys [attribute]}]] (when attribute k)) + built-in-properties))) + "All db attribute properties are configured in built-in-properties") + (def private-db-attribute-properties "db-attribute properties that are not visible to user" (->> db-attribute-properties @@ -472,11 +471,6 @@ "Property values that shouldn't be updated" #{:logseq.property/built-in?}) -(assert (= db-attribute-properties - (set (keep (fn [[k {:keys [attribute]}]] (when attribute k)) - built-in-properties))) - "All db attribute properties are configured in built-in-properties") - (def logseq-property-namespaces #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.property.fsrs" "logseq.task" "logseq.property.linked-references" "logseq.property.asset" "logseq.property.table" "logseq.property.node" @@ -496,6 +490,11 @@ [s] (string/includes? s ".property")) +(defn user-class-namespace? + "Determines if namespace string is a user class" + [s] + (string/includes? s ".class")) + (defn property? "Determines if ident kw is a property visible to user" [k] @@ -503,6 +502,7 @@ (and k-name (or (contains? logseq-property-namespaces k-name) (user-property-namespace? k-name) + (user-class-namespace? k-name) ;; disallow private db-attribute-properties as they cause unwanted refs ;; and appear noisily in debugging contexts (and (keyword? k) (contains? public-db-attribute-properties k)))))) diff --git a/deps/db/src/logseq/db/frontend/property/build.cljs b/deps/db/src/logseq/db/frontend/property/build.cljs index 4a4b260967..a615864bdc 100644 --- a/deps/db/src/logseq/db/frontend/property/build.cljs +++ b/deps/db/src/logseq/db/frontend/property/build.cljs @@ -8,8 +8,7 @@ (defn- closed-value-new-block [block-id block-type value property] (let [property-id (:db/ident property)] - (merge {:block/type "closed value" - :block/format :markdown + (merge {:block/format :markdown :block/uuid block-id :block/page property-id :block/closed-value-property property-id diff --git a/deps/db/src/logseq/db/frontend/property/type.cljs b/deps/db/src/logseq/db/frontend/property/type.cljs index e0d13f11bc..163d63f80d 100644 --- a/deps/db/src/logseq/db/frontend/property/type.cljs +++ b/deps/db/src/logseq/db/frontend/property/type.cljs @@ -138,7 +138,7 @@ [db val] (when-let [ent (d/entity db val)] (and (some? (:block/title ent)) - (= (:block/type ent) "journal")))) + (entity-util/journal? ent)))) (def built-in-validation-schemas "Map of types to malli validation schemas that validate a property value for that type" @@ -167,10 +167,18 @@ :string string? :raw-number number? - :entity entity? - :class class-entity? - :property property-entity? - :page page-entity? + :entity [:fn + {:error/message "should be an Entity"} + entity?] + :class [:fn + {:error/message "should be a Class"} + class-entity?] + :property [:fn + {:error/message "should be a Property"} + property-entity?] + :page [:fn + {:error/message "should be a Page"} + page-entity?] :keyword keyword? :map map? ;; coll elements are ordered as it's saved as a vec diff --git a/deps/db/src/logseq/db/frontend/rules.cljc b/deps/db/src/logseq/db/frontend/rules.cljc index 3e7ab7e579..3226596ebe 100644 --- a/deps/db/src/logseq/db/frontend/rules.cljc +++ b/deps/db/src/logseq/db/frontend/rules.cljc @@ -155,8 +155,18 @@ (dissoc query-dsl-rules :namespace :page-property :has-page-property :page-tags :all-page-tags) + (dissoc rules :namespace) - {:existing-property-value + + {:between + '[(between ?b ?start ?end) + [?b :block/page ?p] + [?p :block/tags :logseq.class/Journal] + [?p :block/journal-day ?d] + [(>= ?d ?start)] + [(<= ?d ?end)]] + + :existing-property-value '[;; non-ref value [(existing-property-value ?b ?prop ?val) [?prop-e :db/ident ?prop] @@ -229,7 +239,7 @@ :has-simple-query-property '[(has-simple-query-property ?b ?prop) [?prop-e :db/ident ?prop] - [?prop-e :block/type "property"] + [?prop-e :block/tags :logseq.class/Property] (has-property-or-default-value? ?b ?prop) [?prop-e :block/schema ?prop-schema] [(get ?prop-schema :public? true) ?public] @@ -239,7 +249,7 @@ :has-private-simple-query-property '[(has-private-simple-query-property ?b ?prop) [?prop-e :db/ident ?prop] - [?prop-e :block/type "property"] + [?prop-e :block/tags :logseq.class/Property] (has-property-or-default-value? ?b ?prop)] ;; Checks if a property exists for any features that are not simple queries @@ -247,7 +257,7 @@ '[(has-property ?b ?prop) [?b ?prop _] [?prop-e :db/ident ?prop] - [?prop-e :block/type "property"] + [?prop-e :block/tags :logseq.class/Property] [?prop-e :block/schema ?prop-schema] [(get ?prop-schema :public? true) ?public] [(= true ?public)]] @@ -256,7 +266,7 @@ :property '[(property ?b ?prop ?val) [?prop-e :db/ident ?prop] - [?prop-e :block/type "property"] + [?prop-e :block/tags :logseq.class/Property] [?prop-e :block/schema ?prop-schema] [(get ?prop-schema :public? true) ?public] [(= true ?public)] @@ -276,7 +286,7 @@ :simple-query-property '[(simple-query-property ?b ?prop ?val) [?prop-e :db/ident ?prop] - [?prop-e :block/type "property"] + [?prop-e :block/tags :logseq.class/Property] [?prop-e :block/schema ?prop-schema] [(get ?prop-schema :public? true) ?public] [(get ?prop-schema :type) ?type] @@ -287,7 +297,7 @@ :private-simple-query-property '[(private-simple-query-property ?b ?prop ?val) [?prop-e :db/ident ?prop] - [?prop-e :block/type "property"] + [?prop-e :block/tags :logseq.class/Property] (property-value ?b ?prop-e ?val)] :tags diff --git a/deps/db/src/logseq/db/frontend/schema.cljs b/deps/db/src/logseq/db/frontend/schema.cljs index bf4a1cceef..226d7c525f 100644 --- a/deps/db/src/logseq/db/frontend/schema.cljs +++ b/deps/db/src/logseq/db/frontend/schema.cljs @@ -2,7 +2,7 @@ "Main datascript schemas for the Logseq app" (:require [clojure.set :as set])) -(def version 51) +(def version 52) ;; A page is a special block, a page can corresponds to multiple files with the same ":block/name". (def ^:large-vars/data-var schema @@ -111,7 +111,8 @@ (dissoc schema :block/namespace :block/properties-text-values :block/pre-block? :recent/pages :block/file :block/properties :block/properties-order :block/repeated? :block/deadline :block/scheduled :block/priority - :block/marker :block/macros) + :block/marker :block/macros + :block/type) {:block/name {:db/index true} ; remove db/unique for :block/name ;; closed value :block/closed-value-property {:db/valueType :db.type/ref diff --git a/deps/db/src/logseq/db/frontend/validate.cljs b/deps/db/src/logseq/db/frontend/validate.cljs index 00d326f246..0acabff1ac 100644 --- a/deps/db/src/logseq/db/frontend/validate.cljs +++ b/deps/db/src/logseq/db/frontend/validate.cljs @@ -19,32 +19,11 @@ [closed-schema?] (if closed-schema? closed-db-schema-explainer db-schema-explainer)) -(defn validate-ents-before-after! - [changed-ids db-before db-after tx-data tx-meta] - (let [id->ent-before (into {} - (keep (fn [id] (when-let [ent (d/entity db-before id)] [id ent]))) - changed-ids) - id->ent-after (keep (fn [id] (when-let [ent (d/entity db-after id)] [id ent])) changed-ids) - ent-before+ent-after-coll - (reduce - (fn [acc [id ent-after]] - (if-let [ent-before (id->ent-before id)] - (conj acc [ent-before ent-after]) - acc)) - [] id->ent-after)] - (doseq [[ent-before ent-after] ent-before+ent-after-coll] - (let [[type-before type-after] [(:block/type ent-before) (:block/type ent-after)]] - (when (and (some? type-before) - (nil? type-after)) - (js/console.error "Illegal :block/type change, entity id:" (:db/id ent-after)) - (prn :ent-before ent-before :ent-after ent-after :tx-data tx-data :tx-meta tx-meta)))))) - (defn validate-tx-report! "Validates the datascript tx-report for entities that have changed. Returns boolean indicating if db is valid" - [{:keys [db-before db-after tx-data tx-meta]} validate-options] + [{:keys [db-after tx-data tx-meta]} validate-options] (let [changed-ids (->> tx-data (keep :e) distinct) - _ (validate-ents-before-after! changed-ids db-before db-after tx-data tx-meta) tx-datoms (mapcat #(d/datoms db-after :eavt %) changed-ids) ent-maps* (map (fn [[db-id m]] ;; Add :db/id for debugging @@ -62,13 +41,8 @@ (let [explainer (get-schema-explainer (:closed-schema? validate-options))] (js/console.error "Invalid datascript entities detected amongst changed entity ids:" changed-ids) (doseq [m invalid-ent-maps] - (prn {:entity-map m - :errors (me/humanize (explainer [m]))}) - ;; FIXME: pprint fails sometime - ;; (pprint/pprint {;; :entity-map (map #(into {} %) m) - ;; :errors (me/humanize (m/explain db-schema [m]))}) - ) + :errors (me/humanize (explainer [m]))})) false) true))))) @@ -88,7 +62,7 @@ (:block/page ent) (update :block/page (fn [id] (select-keys (d/entity db id) - [:block/name :block/type :db/id :block/created-at]))))) + [:block/name :block/tags :db/id :block/created-at]))))) :errors errors' ;; Group by type to reduce verbosity ;; TODO: Move/remove this to another fn if unused diff --git a/deps/db/src/logseq/db/sqlite/build.cljs b/deps/db/src/logseq/db/sqlite/build.cljs index a6bff24110..084be5390c 100644 --- a/deps/db/src/logseq/db/sqlite/build.cljs +++ b/deps/db/src/logseq/db/sqlite/build.cljs @@ -357,7 +357,7 @@ {:db/id (or (:db/id page) (new-db-id)) :block/title (or (:block/title page) (string/capitalize (:block/name page))) :block/name (or (:block/name page) (common-util/page-name-sanity-lc (:block/title page))) - :block/type "page" + :block/tags #{:logseq.class/Page} :block/format :markdown} (dissoc page :build/properties :db/id :block/name :block/title :build/tags)) pvalue-tx-m (->property-value-tx-m new-page (:build/properties page) properties all-idents)] @@ -376,8 +376,9 @@ page-uuids all-idents)) (when-let [tags (:build/tags page)] - {:block/tags (mapv #(hash-map :db/ident (get-ident all-idents %)) - tags)}))))) + {:block/tags (-> (mapv #(hash-map :db/ident (get-ident all-idents %)) + tags) + (conj :logseq.class/Page))}))))) ;; blocks tx (reduce (fn [acc m] (into acc @@ -480,7 +481,6 @@ :block/title page-name :block/uuid (common-uuid/gen-uuid :journal-page-uuid date-int) - :block/type "journal" :block/tags :logseq.class/Journal}))))) m))] ;; Order matters as some steps depend on previous step having prepared blocks or pages in a certain way diff --git a/deps/db/src/logseq/db/sqlite/common_db.cljs b/deps/db/src/logseq/db/sqlite/common_db.cljs index 63e4f0fc47..32dc57e0c4 100644 --- a/deps/db/src/logseq/db/sqlite/common_db.cljs +++ b/deps/db/src/logseq/db/sqlite/common_db.cljs @@ -186,7 +186,6 @@ :in $ ?today :where [?page :block/name ?page-name] - ;; [?page :block/type "journal"] [?page :block/journal-day ?journal-day] [(<= ?journal-day ?today)]] db @@ -213,13 +212,9 @@ (defn get-structured-datoms [db] - (mapcat (fn [type'] - (->> (d/datoms db :avet :block/type type') - (mapcat (fn [d] - (d/datoms db :eavt (:e d)))))) - [;; property and class pages are pulled from `get-all-pages` already - ;; "property" "class" - "closed value"])) + (->> (d/datoms db :avet :block/closed-value-property) + (mapcat (fn [d] + (d/datoms db :eavt (:e d)))))) (defn get-favorites "Favorites page and its blocks" diff --git a/deps/db/src/logseq/db/sqlite/create_graph.cljs b/deps/db/src/logseq/db/sqlite/create_graph.cljs index 218cb09348..1f19ab6d44 100644 --- a/deps/db/src/logseq/db/sqlite/create_graph.cljs +++ b/deps/db/src/logseq/db/sqlite/create_graph.cljs @@ -10,7 +10,8 @@ [logseq.db.frontend.schema :as db-schema] [logseq.db.sqlite.util :as sqlite-util] [logseq.common.config :as common-config] - [logseq.db.frontend.order :as db-order])) + [logseq.db.frontend.order :as db-order] + [logseq.db.frontend.entity-util :as entity-util])) (defn- mark-block-as-built-in [block] (assoc block :logseq.property/built-in? true)) @@ -52,14 +53,15 @@ ;; Adding built-ins must come after initial properties [(mark-block-as-built-in' built-in-property)] (map mark-block-as-built-in' properties) - (keep #(when (= "closed value" (:block/type %)) (mark-block-as-built-in' %)) + (keep #(when (entity-util/closed-value? %) + (mark-block-as-built-in' %)) properties))] (doseq [m tx] (when-let [block-uuid (and (:db/ident m) (:block/uuid m))] (assert (string/starts-with? (str block-uuid) "00000002") m))) {:tx tx - :properties (filter #(= (:block/type %) "property") properties)})) + :properties (filter entity-util/property? properties)})) (def built-in-pages-names #{"Contents"}) @@ -69,7 +71,6 @@ (->> (keep :db/ident tx) frequencies (keep (fn [[k v]] (when (> v 1) k))) - (remove #{:logseq.class/Root}) seq)] (throw (ex-info (str "The following :db/idents are not unique and clobbered each other: " (vec conflicting-idents)) @@ -111,7 +112,7 @@ {:block/uuid page-id :block/name common-config/views-page-name :block/title common-config/views-page-name - :block/type "page" + :block/tags [:logseq.class/Page] :block/schema {:public? false} :block/format :markdown :logseq.property/built-in? true}) @@ -131,7 +132,7 @@ {:block/uuid (common-uuid/gen-uuid) :block/name common-config/favorites-page-name :block/title common-config/favorites-page-name - :block/type "page" + :block/tags [:logseq.class/Page] :block/schema {:public? false} :block/format :markdown :logseq.property/built-in? true})]) @@ -147,8 +148,7 @@ (sqlite-util/kv :logseq.kv/graph-initial-schema-version db-schema/version) (sqlite-util/kv :logseq.kv/graph-created-at (common-util/time-ms)) ;; Empty property value used by db.type/ref properties - {:db/ident :logseq.property/empty-placeholder} - {:db/ident :logseq.class/Root}] + {:db/ident :logseq.property/empty-placeholder}] import-type (into (sqlite-util/import-tx import-type))) initial-files [{:block/uuid (d/squuid) @@ -172,7 +172,15 @@ default-pages (->> (map sqlite-util/build-new-page built-in-pages-names) (map mark-block-as-built-in)) hidden-pages (concat (build-initial-views) (build-favorites-page)) - tx (vec (concat initial-data properties-tx default-classes + ;; These classes bootstrap our tags and properties as they depend on each other e.g. + ;; Root <-> Tag, classes-tx depends on logseq.property/parent, properties-tx depends on Property + bootstrap-class? (fn [c] (contains? #{:logseq.class/Root :logseq.class/Property :logseq.class/Tag} (:db/ident c))) + bootstrap-classes (filter bootstrap-class? default-classes) + bootstrap-class-ids (map #(select-keys % [:db/ident :block/uuid]) bootstrap-classes) + classes-tx (concat (map #(dissoc % :db/ident) bootstrap-classes) + (remove bootstrap-class? default-classes)) + ;; Order of tx is critical. bootstrap-class-ids bootstraps properties-tx and classes-tx + tx (vec (concat bootstrap-class-ids initial-data properties-tx classes-tx initial-files default-pages hidden-pages))] (validate-tx-for-duplicate-idents tx) tx)) diff --git a/deps/db/src/logseq/db/sqlite/util.cljs b/deps/db/src/logseq/db/sqlite/util.cljs index 93b1e500eb..4c401931be 100644 --- a/deps/db/src/logseq/db/sqlite/util.cljs +++ b/deps/db/src/logseq/db/sqlite/util.cljs @@ -91,7 +91,7 @@ (block-with-timestamps (cond-> {:db/ident db-ident' - :block/type "property" + :block/tags #{:logseq.class/Property} :block/format :markdown :block/schema (merge {:type :default} (dissoc prop-schema :classes :cardinality)) :block/name (common-util/page-name-sanity-lc (name prop-name)) @@ -115,8 +115,8 @@ {:pre [(qualified-keyword? (:db/ident block))]} (block-with-timestamps (cond-> (merge block - {:block/type "class" - :block/format :markdown}) + {:block/format :markdown + :block/tags (set (conj (:block/tags block) :logseq.class/Tag))}) (and (not= (:db/ident block) :logseq.class/Root) (nil? (:logseq.property/parent block))) (assoc :logseq.property/parent :logseq.class/Root)))) @@ -129,7 +129,7 @@ :block/title page-name :block/uuid (d/squuid) :block/format :markdown - :block/type "page"})) + :block/tags #{:logseq.class/Page}})) (defn kv "Creates a key-value pair tx with the key and value respectively stored under diff --git a/deps/db/src/logseq/db/test/helper.cljs b/deps/db/src/logseq/db/test/helper.cljs index c8ef199038..7b8e984ec1 100644 --- a/deps/db/src/logseq/db/test/helper.cljs +++ b/deps/db/src/logseq/db/test/helper.cljs @@ -5,13 +5,38 @@ [logseq.db.sqlite.create-graph :as sqlite-create-graph] [logseq.db.frontend.schema :as db-schema])) -(defn find-block-by-content [db content] - (->> content - (d/q '[:find [(pull ?b [*]) ...] - :in $ ?content - :where [?b :block/title ?content]] +(defn find-block-by-content + "Find first block by exact block string or by fuzzier regex" + [db content] + (if (instance? js/RegExp content) + (->> content + (d/q '[:find [(pull ?b [*]) ...] + :in $ ?pattern + :where + [?b :block/title ?content] + [?b :block/page] + [(re-find ?pattern ?content)]] + db) + first) + (->> content + (d/q '[:find [(pull ?b [*]) ...] + :in $ ?content + :where + [?b :block/title ?content] + [?b :block/page]] + db) + first))) + +(defn find-page-by-title + "Find first page by its title" + [db title] + (->> title + (d/q '[:find [?b ...] + :in $ ?title + :where [?b :block/title ?title]] db) - first)) + first + (d/entity db))) (defn create-conn "Create a conn for a DB graph seeded with initial data" diff --git a/deps/db/test/logseq/db/frontend/inputs_test.cljs b/deps/db/test/logseq/db/frontend/inputs_test.cljs index 96c9d61c29..a48de9a7ad 100644 --- a/deps/db/test/logseq/db/frontend/inputs_test.cljs +++ b/deps/db/test/logseq/db/frontend/inputs_test.cljs @@ -18,6 +18,7 @@ (deftest resolve-input-for-page-and-block-inputs (let [conn (d/create-conn db-schema/schema-for-db-based-graph) + _ (d/transact! conn [{:db/ident :logseq.class/Page}]) _ (sqlite-build/create-blocks conn [{:page {:block/title "page1"} diff --git a/deps/db/test/logseq/db/frontend/rules_test.cljs b/deps/db/test/logseq/db/frontend/rules_test.cljs index 5e9e4938b3..10c38579ac 100644 --- a/deps/db/test/logseq/db/frontend/rules_test.cljs +++ b/deps/db/test/logseq/db/frontend/rules_test.cljs @@ -31,10 +31,10 @@ {:properties {:foo {:block/schema {:type :default}} :foo2 {:block/schema {:type :default}}} :pages-and-blocks - [{:page {:block/title "Page" + [{:page {:block/title "Page1" :build/properties {:foo "bar"}}}]})] - (is (= ["Page"] + (is (= ["Page1"] (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (has-property ?b :user.property/foo)] @conn) (map (comp :block/title first)))) @@ -44,9 +44,9 @@ @conn) (map (comp :block/title first)))) "has-property returns no result when block doesn't have property") - (is (= [:user.property/foo] + (is (= [:user.property/foo :block/tags] (q-with-rules '[:find [?p ...] - :where (has-property ?b ?p) [?b :block/title "Page"]] + :where (has-property ?b ?p) [?b :block/title "Page1"]] @conn)) "has-property can bind to property arg"))) @@ -57,12 +57,12 @@ :number-many {:block/schema {:type :number :cardinality :many}} :page-many {:block/schema {:type :node :cardinality :many}}} :pages-and-blocks - [{:page {:block/title "Page" + [{:page {:block/title "Page1" :build/properties {:foo "bar" :number-many #{5 10} :page-many #{[:page "Page A"]}}}} {:page {:block/title "Page A" :build/properties {:foo "bar A"}}}]})] (testing "cardinality :one property" - (is (= ["Page"] + (is (= ["Page1"] (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/foo "bar")] @conn) (map (comp :block/title first)))) @@ -74,13 +74,13 @@ "property returns no result when page doesn't have property value") (is (= #{:user.property/foo} (->> (q-with-rules '[:find [?p ...] - :where (property ?b ?p "bar") [?b :block/title "Page"]] + :where (property ?b ?p "bar") [?b :block/title "Page1"]] @conn) set)) "property can bind to property arg with bound property value")) (testing "cardinality :many property" - (is (= ["Page"] + (is (= ["Page1"] (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/number-many 5)] @conn) (map (comp :block/title first)))) @@ -92,14 +92,14 @@ "property returns no result when page doesn't have property value") (is (= #{:user.property/number-many} (->> (q-with-rules '[:find [?p ...] - :where (property ?b ?p 5) [?b :block/title "Page"]] + :where (property ?b ?p 5) [?b :block/title "Page1"]] @conn) set)) "property can bind to property arg with bound property value")) ;; NOTE: Querying a ref's name is different than before and requires more than just the rule (testing ":ref property" - (is (= ["Page"] + (is (= ["Page1"] (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/page-many "Page A")] @conn) @@ -113,22 +113,23 @@ "property returns no result when page doesn't have property value")) (testing "bindings with property value" - (is (= #{:user.property/foo :user.property/number-many :user.property/page-many} + (is (= #{:user.property/foo :user.property/number-many :user.property/page-many :block/tags} (->> (q-with-rules '[:find [?p ...] - :where (property ?b ?p _) [?b :block/title "Page"]] + :where (property ?b ?p _) [?b :block/title "Page1"]] @conn) set)) "property can bind to property arg with unbound property value") (is (= #{[:user.property/number-many 10] [:user.property/number-many 5] [:user.property/foo "bar"] - [:user.property/page-many "Page A"]} + [:user.property/page-many "Page A"] + [:block/tags "Page"]} (->> (q-with-rules '[:find ?p ?val - :where (property ?b ?p ?val) [?b :block/title "Page"]] + :where (property ?b ?p ?val) [?b :block/title "Page1"]] @conn) set)) "property can bind to property and property value args") - (is (= #{"Page"} + (is (= #{"Page1"} (->> (q-with-rules '[:find (pull ?b [:block/title]) :where [?b :user.property/page-many ?pv] diff --git a/deps/db/test/logseq/db/sqlite/build_test.cljs b/deps/db/test/logseq/db/sqlite/build_test.cljs index 4e0417cc70..0f838f86b4 100644 --- a/deps/db/test/logseq/db/sqlite/build_test.cljs +++ b/deps/db/test/logseq/db/sqlite/build_test.cljs @@ -12,14 +12,14 @@ [{:page {:block/title "page1"} :blocks [{:block/title "Jrue Holiday" :build/tags [:Person]}]} {:page {:block/title "Jayson Tatum" :build/tags [:Person]}}])] - (is (= {:block/tags [{:block/title "Person", :block/type "class"}]} - (first (d/q '[:find [(pull ?b [{:block/tags [:block/title :block/type]}]) ...] + (is (= {:block/tags [{:block/title "Person"}]} + (first (d/q '[:find [(pull ?b [{:block/tags [:block/title]}]) ...] :where [?b :block/title "Jrue Holiday"]] @conn))) "Person class is created and correctly associated to a block") - (is (= {:block/tags [{:block/title "Person", :block/type "class"}]} - (first (d/q '[:find [(pull ?b [{:block/tags [:block/title :block/type]}]) ...] + (is (= {:block/tags [{:block/title "Page"} {:block/title "Person"}]} + (first (d/q '[:find [(pull ?b [{:block/tags [:block/title]}]) ...] :where [?b :block/title "Jayson Tatum"]] @conn))) "Person class is created and correctly associated to a page"))) diff --git a/deps/db/test/logseq/db/sqlite/create_graph_test.cljs b/deps/db/test/logseq/db/sqlite/create_graph_test.cljs index 37c15754b9..312479f0ac 100644 --- a/deps/db/test/logseq/db/sqlite/create_graph_test.cljs +++ b/deps/db/test/logseq/db/sqlite/create_graph_test.cljs @@ -9,15 +9,16 @@ [logseq.db.frontend.property :as db-property] [logseq.db.sqlite.build :as sqlite-build] [logseq.db :as ldb] - [logseq.db.test.helper :as db-test])) + [logseq.db.test.helper :as db-test] + [logseq.db.frontend.class :as db-class])) (deftest new-graph-db-idents (testing "a new graph follows :db/ident conventions for" (let [conn (db-test/create-conn) - ident-ents (->> (d/q '[:find (pull ?b [:db/ident :block/type]) + ident-ents (->> (d/q '[:find [?b ...] :where [?b :db/ident]] @conn) - (map first)) + (map (fn [id] (d/entity @conn id)))) default-idents (map :db/ident ident-ents)] (is (> (count default-idents) 45) "Approximate number of default idents is correct") @@ -38,7 +39,7 @@ (map #(keyword (namespace %) (string/replace (name %) #".[^.]+$" ""))) set)] (is (= [] - (remove #(= "closed value" (:block/type %)) closed-value-ents)) + (remove ldb/closed-value? closed-value-ents)) "All property names that contain a '.' are closed values") (is (= #{} (set/difference @@ -56,7 +57,7 @@ (remove #(or (= "logseq.kv" (namespace (:db/ident %))) (= :logseq.property/empty-placeholder (:db/ident %))))) pages (d/q '[:find [(pull ?b [:logseq.property/built-in? :block/title]) ...] - :where [?b :block/type "page"]] + :where [?b :block/tags :logseq.class/Page]] @conn)] (is (= [] (remove :logseq.property/built-in? idents)) "All entities with :db/ident have built-in property (except for kv idents)") @@ -73,6 +74,18 @@ (is (every? ldb/property? (:logseq.property.class/properties task)) "Each task property has correct type"))) +(deftest new-graph-initializes-default-classes-correctly + (let [conn (db-test/create-conn)] + (is (= (count db-class/built-in-classes) (count (d/datoms @conn :avet :block/tags :logseq.class/Tag))) + "All built-in classes have a :logseq.class/Tag") + + (is (= (count (dissoc db-class/built-in-classes :logseq.class/Root)) + (count (->> (d/datoms @conn :avet :block/tags :logseq.class/Tag) + (map #(d/entity @conn (:e %))) + (mapcat :logseq.property/_parent) + set))) + "Reverse lookup of :logseq.property/parent correctly fetches number of child classes"))) + (deftest new-graph-is-valid (let [conn (db-test/create-conn) validation (db-validate/validate-db! @conn)] diff --git a/deps/db/test/logseq/db_test.cljs b/deps/db/test/logseq/db_test.cljs index 9d8735bdc2..13cbfef455 100644 --- a/deps/db/test/logseq/db_test.cljs +++ b/deps/db/test/logseq/db_test.cljs @@ -29,16 +29,16 @@ (ex-message e))))))) (def class-parents-data - [{:block/type "class" + [{:block/tags :logseq.class/Tag :block/title "x" :block/name "x" :block/uuid #uuid "6c353967-f79b-4785-b804-a39b81d72461"} - {:block/type "class" + {:block/tags :logseq.class/Tag :block/title "y" :block/name "y" :block/uuid #uuid "7008db08-ba0c-4aa9-afc6-7e4783e40a99" :logseq.property/parent [:block/uuid #uuid "6c353967-f79b-4785-b804-a39b81d72461"]} - {:block/type "class" + {:block/tags :logseq.class/Tag :block/title "z" :block/name "z" :block/uuid #uuid "d95f2912-a7af-41b9-8ed5-28861f7fc0be" @@ -63,4 +63,23 @@ (is (= "Foo" (:block/title (ldb/get-case-page @conn "Foo")))) ;; Case sensitive classes (is (= "movie" (:block/title (ldb/get-case-page @conn "movie")))) - (is (= "Movie" (:block/title (ldb/get-case-page @conn "Movie")))))) \ No newline at end of file + (is (= "Movie" (:block/title (ldb/get-case-page @conn "Movie")))))) + +(deftest page-exists + (let [conn (db-test/create-conn-with-blocks + {:properties + {:foo {:block/schema {:type :default}} + :Foo {:block/schema {:type :default}}} + :classes {:movie {} :Movie {}}})] + (is (= ["foo"] + (map #(:block/title (d/entity @conn %)) (ldb/page-exists? @conn "foo" #{:logseq.class/Property}))) + "Property pages correctly found for given class") + (is (= nil + (ldb/page-exists? @conn "foo" #{:logseq.class/Tag})) + "Property pages correctly not found for given class") + (is (= ["movie"] + (map #(:block/title (d/entity @conn %)) (ldb/page-exists? @conn "movie" #{:logseq.class/Tag}))) + "Class pages correctly found for given class") + (is (= nil + (ldb/page-exists? @conn "movie" #{:logseq.class/Property})) + "Class pages correctly not found for given class"))) diff --git a/deps/graph-parser/src/logseq/graph_parser/block.cljs b/deps/graph-parser/src/logseq/graph_parser/block.cljs index 29153b9455..f7a5de0cdc 100644 --- a/deps/graph-parser/src/logseq/graph_parser/block.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/block.cljs @@ -347,10 +347,11 @@ {:block/created-at current-ms :block/updated-at current-ms})) (if journal-day - (cond-> {:block/type "journal" - :block/journal-day journal-day} + (cond-> {:block/journal-day journal-day} db-based? - (assoc :block/tags [:logseq.class/Journal])) + (assoc :block/tags [:logseq.class/Journal]) + (not db-based?) + (assoc :block/type "journal")) {}))] [page page-entity])) @@ -388,8 +389,12 @@ nil)] [page nil]))] (when page - (let [type (if class? "class" (or (:block/type page) "page"))] - (assoc page :block/type type)))))) + (if (ldb/db-based-graph? db) + (let [tags (if class? [:logseq.class/Tag] + (or (:block/tags page) + [:logseq.class/Page]))] + (assoc page :block/tags tags)) + (assoc page :block/type (or (:block/type page) "page"))))))) (defn- db-namespace-page? "Namespace page that're not journal pages" diff --git a/deps/graph-parser/src/logseq/graph_parser/exporter.cljs b/deps/graph-parser/src/logseq/graph_parser/exporter.cljs index 55547714ae..f9199d0a0d 100644 --- a/deps/graph-parser/src/logseq/graph_parser/exporter.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/exporter.cljs @@ -72,20 +72,23 @@ ([db class-name all-idents] (find-or-create-class db class-name all-idents {})) ([db class-name all-idents class-block] - (if-let [db-ident (get @all-idents (keyword class-name))] - {:db/ident db-ident} - (let [m - (if (:block/namespace class-block) - ;; Give namespaced tags a unique ident so they don't conflict with other tags - (-> (db-class/build-new-class db {:block/title (build-class-ident-name class-name)}) - (merge {:block/title class-name - :block/name (common-util/page-name-sanity-lc class-name)}) - (build-new-namespace-page)) - (db-class/build-new-class db - {:block/title class-name - :block/name (common-util/page-name-sanity-lc class-name)}))] - (swap! all-idents assoc (keyword class-name) (:db/ident m)) - (with-meta m {:new-class? true}))))) + (let [ident (keyword class-name)] + (if-let [db-ident (get @all-idents ident)] + {:db/ident db-ident} + (let [m + (if (:block/namespace class-block) + ;; Give namespaced tags a unique ident so they don't conflict with other tags + (-> (db-class/build-new-class db (merge {:block/title (build-class-ident-name class-name)} + (select-keys class-block [:block/tags]))) + (merge {:block/title class-name + :block/name (common-util/page-name-sanity-lc class-name)}) + (build-new-namespace-page)) + (db-class/build-new-class db + (assoc {:block/title class-name + :block/name (common-util/page-name-sanity-lc class-name)} + :block/tags (:block/tags class-block))))] + (swap! all-idents assoc ident (:db/ident m)) + (with-meta m {:new-class? true})))))) (defn- find-or-gen-class-uuid [page-names-to-uuids page-name db-ident & {:keys [temp-new-class?]}] (or (if temp-new-class? @@ -115,7 +118,7 @@ (if block-ns (->> (d/q '[:find [?b ...] :in $ ?name - :where [?b :block/uuid ?uuid] [?b :block/type "class"] [?b :block/name ?name]] + :where [?b :block/uuid ?uuid] [?b :block/tags :logseq.class/Tag] [?b :block/name ?name]] db (ns-util/get-last-part full-name)) (map #(d/entity db %)) @@ -127,7 +130,7 @@ (first (d/q '[:find [?uuid ...] :in $ ?name - :where [?b :block/uuid ?uuid] [?b :block/type "class"] [?b :block/name ?name]] + :where [?b :block/uuid ?uuid] [?b :block/tags :logseq.class/Tag] [?b :block/name ?name]] db full-name)))) @@ -144,21 +147,30 @@ (assert (:block/uuid class-m') "Class must have a :block/uuid") [:block/uuid (:block/uuid class-m')]) (when (convert-tag? (:block/name tag-block) user-options) - (if-let [existing-tag-uuid (find-existing-class db tag-block)] - [:block/uuid existing-tag-uuid] - ;; Creates or updates page within same tx - (let [class-m (find-or-create-class db (:block/title tag-block) all-idents tag-block) - class-m' (-> (merge tag-block class-m - (when-not (:block/uuid tag-block) - {:block/uuid (find-or-gen-class-uuid page-names-to-uuids (:block/name tag-block) (:db/ident class-m))})) - ;; override with imported timestamps - (dissoc :block/created-at :block/updated-at) - (merge (add-missing-timestamps - (select-keys tag-block [:block/created-at :block/updated-at]))) - (replace-namespace-with-parent page-names-to-uuids))] - (when (:new-class? (meta class-m)) (swap! classes-tx conj class-m')) - (assert (:block/uuid class-m') "Class must have a :block/uuid") - [:block/uuid (:block/uuid class-m')]))))) + (let [existing-tag-uuid (find-existing-class db tag-block) + internal-tag-conflict? (contains? #{"tag" "property" "page" "journal" "asset"} (:block/name tag-block))] + (cond + ;; Don't overwrite internal tags + (and existing-tag-uuid (not internal-tag-conflict?)) + [:block/uuid existing-tag-uuid] + + :else + ;; Creates or updates page within same tx + (let [class-m (find-or-create-class db (:block/title tag-block) all-idents tag-block) + class-m' (-> (merge tag-block class-m + (if internal-tag-conflict? + {:block/uuid (common-uuid/gen-uuid :db-ident-block-uuid (:db/ident class-m))} + (when-not (:block/uuid tag-block) + (let [id (find-or-gen-class-uuid page-names-to-uuids (:block/name tag-block) (:db/ident class-m))] + {:block/uuid id})))) + ;; override with imported timestamps + (dissoc :block/created-at :block/updated-at) + (merge (add-missing-timestamps + (select-keys tag-block [:block/created-at :block/updated-at]))) + (replace-namespace-with-parent page-names-to-uuids))] + (when (:new-class? (meta class-m)) (swap! classes-tx conj class-m')) + (assert (:block/uuid class-m') "Class must have a :block/uuid") + [:block/uuid (:block/uuid class-m')])))))) (defn- logseq-class-ident? [k] @@ -188,10 +200,17 @@ ;; Ignore new class tags from extract e.g. :logseq.class/Journal (logseq-class-ident? %))) (map #(vector :block/uuid (get-page-uuid (:page-names-to-uuids per-file-state) (:block/name %) {:block %}))) - set)] + set) + page-classes (into #{:logseq.class/Page} db-class/page-children-classes)] (cond-> block true (update :block/tags convert-tags-to-classes db per-file-state user-options all-idents) + ;; ensure pages are a Page + true + (update :block/tags (fn [tags] + (if (seq (set/intersection (set tags) page-classes)) + tags + (conj (vec tags) :logseq.class/Page)))) (seq page-tags) (merge {:logseq.property/page-tags page-tags}))) block)) @@ -297,9 +316,8 @@ (date-time-util/int->journal-title date-int (common-config/get-date-formatter user-config)))] (assoc page-m :block/uuid (common-uuid/gen-uuid :journal-page-uuid date-int) - :block/type "journal" :block/journal-day date-int))) - (assoc :block/tags :logseq.class/Journal))] + (assoc :block/tags #{:logseq.class/Journal}))] {:block (-> block (assoc :logseq.task/deadline [:block/uuid (:block/uuid deadline-page)]) @@ -699,8 +717,9 @@ (seq classes-from-properties) ;; Add a map of {:block.temp/new-class TAG} to be processed later (update :block/tags - (fnil into []) - (map #(hash-map :block.temp/new-class %) classes-from-properties))) + (fn [tags] + (let [tags' (if (sequential? tags) tags (set tags))] + (into tags' (map #(hash-map :block.temp/new-class %) classes-from-properties)))))) :properties-tx pvalues-tx}) {:block block :properties-tx []}) (update :block dissoc :block/properties :block/properties-text-values :block/properties-order :block/invalid-properties))) @@ -911,7 +930,7 @@ (:block/name %)) (or (:block/uuid %) (throw (ex-info (str "No uuid for existing page " (pr-str (:block/name %))) - (select-keys % [:block/name :block/type])))))) + (select-keys % [:block/name :block/tags])))))) (into {}))) (defn- build-existing-page @@ -919,13 +938,10 @@ (let [;; These attributes are not allowed to be transacted because they must not change across files disallowed-attributes [:block/name :block/uuid :block/format :block/title :block/journal-day :block/created-at :block/updated-at] - allowed-attributes (into [:block/tags :block/alias :logseq.property/parent :block/type :db/ident] + allowed-attributes (into [:block/tags :block/alias :logseq.property/parent :db/ident] (keep #(when (db-malli-schema/user-property? (key %)) (key %)) m)) - block-changes (cond-> (select-keys m allowed-attributes) - ;; disallow any type -> "page" but do allow any conversion to a non-page type - (ldb/internal-page? m) - (dissoc :block/type))] + block-changes (select-keys m allowed-attributes)] (when-let [ignored-attrs (not-empty (apply dissoc m (into disallowed-attributes allowed-attributes)))] (notify-user {:msg (str "Import ignored the following attributes on page " (pr-str (:block/title m)) ": " ignored-attrs)})) @@ -956,8 +972,8 @@ (assoc :block/uuid (d/squuid)) ;; only happens for few file built-ins like tags and alias (and (contains? all-built-in-names (keyword (:block/name page))) - (not (:block/type page))) - (assoc :block/type "page")))] + (not (:block/tags page))) + (assoc :block/tags [:logseq.class/Page])))] (cond-> page' (:block/namespace page) ((fn [block'] @@ -997,7 +1013,7 @@ (all-existing-page-uuids (::original-name m)) (all-existing-page-uuids (:block/name m)))] (build-existing-page (dissoc m ::original-name ::original-title) @conn page-uuid per-file-state options) - (when (or (= "class" (:block/type m)) + (when (or (ldb/class? m) ;; Don't build a new page if it overwrites an existing class (not (some-> (get @(:all-idents import-state) (some-> (or (::original-title m) (:block/title m)) @@ -1137,18 +1153,22 @@ (get-property-schema @(:property-schemas import-state) kw-name) {:title (name kw-name)})] (assert existing-page-uuid) - (merge (select-keys new-prop [:block/type :block/schema :db/ident :db/index :db/cardinality :db/valueType]) + (merge (select-keys new-prop [:block/tags :block/schema :db/ident :db/index :db/cardinality :db/valueType]) {:block/uuid existing-page-uuid}))) (set/intersection new-properties (set (map keyword (keys existing-pages))))) + ;; Could do this only for existing pages but the added complexity isn't worth reducing the tx noise + retract-page-tag-from-properties-tx (map #(vector :db/retract [:block/uuid (:block/uuid %)] :block/tags :logseq.class/Page) + (concat property-pages-tx converted-property-pages-tx)) ;; Save properties on new property pages separately as they can contain new properties and thus need to be ;; transacted separately the property pages property-page-properties-tx (keep (fn [b] (when-let [page-properties (not-empty (db-property/properties b))] (merge page-properties {:block/uuid (:block/uuid b) - :block/type "property"}))) + :block/tags (-> (remove #(= :logseq.class/Page %) (:block/tags page-properties)) + (conj :logseq.class/Property))}))) properties-tx)] {:pages-tx pages-tx' - :property-pages-tx (concat property-pages-tx converted-property-pages-tx) + :property-pages-tx (concat property-pages-tx converted-property-pages-tx retract-page-tag-from-properties-tx) :property-page-properties-tx property-page-properties-tx})) (defn- update-whiteboard-blocks [blocks format] @@ -1219,8 +1239,9 @@ (->> pages ;; migrate previous attribute for :block/title (map #(-> % - (assoc :block/title (:block/original-name %)) - (dissoc :block/original-name)))))) + (assoc :block/title (or (:block/original-name %) (:block/title %)) + :block/tags #{:logseq.class/Whiteboard}) + (dissoc :block/type :block/original-name)))))) (update :blocks update-whiteboard-blocks format)) :else @@ -1234,6 +1255,32 @@ [(:block/name %) (date-time-util/journal-day->ms journal-day)])) (into {}))) +(defn- clean-extra-invalid-tags + "If a page/class tx is an existing property or a new or existing class, ensure that + it only has one tag by removing :logseq.class/Page from its tx" + [db pages-tx' classes-tx] + ;; TODO: Improve perf if we tracked all created classes in atom + (let [existing-classes (->> (d/datoms db :avet :block/tags :logseq.class/Tag) + (map #(d/entity db (:e %))) + (map :block/uuid) + set) + classes (set/union existing-classes + (set (map :block/uuid classes-tx))) + existing-properties (->> (d/datoms db :avet :block/tags :logseq.class/Property) + (map #(d/entity db (:e %))) + (map :block/uuid) + set)] + {:pages-tx + (mapv (fn [page] + (if (or (contains? classes (:block/uuid page)) + (contains? existing-properties (:block/uuid page))) + (update page :block/tags (fn [tags] (vec (remove #(= % :logseq.class/Page) tags)))) + page)) + pages-tx') + :retract-page-tag-from-classes-tx + (mapv #(vector :db/retract [:block/uuid (:block/uuid %)] :block/tags :logseq.class/Page) + classes-tx)})) + (defn add-file-to-db-graph "Parse file and save parsed data to the given db graph. Options available: @@ -1271,12 +1318,15 @@ vec) {:keys [property-pages-tx property-page-properties-tx] pages-tx' :pages-tx} (split-pages-and-properties-tx pages-tx old-properties existing-pages (:import-state options)) + ;; _ (when (seq property-pages-tx) (cljs.pprint/pprint {:property-pages-tx property-pages-tx})) ;; Necessary to transact new property entities first so that block+page properties can be transacted next main-props-tx-report (d/transact! conn property-pages-tx {::new-graph? true}) classes-tx @(:classes-tx tx-options) + {:keys [retract-page-tag-from-classes-tx] pages-tx'' :pages-tx} (clean-extra-invalid-tags @conn pages-tx' classes-tx) + classes-tx' (concat classes-tx retract-page-tag-from-classes-tx) ;; Build indices - pages-index (->> (map #(select-keys % [:block/uuid]) pages-tx') + pages-index (->> (map #(select-keys % [:block/uuid]) pages-tx'') (concat (map #(select-keys % [:block/uuid]) classes-tx)) distinct) block-ids (map (fn [block] {:block/uuid (:block/uuid block)}) blocks-tx) @@ -1289,7 +1339,7 @@ blocks-index (set/union (set block-ids) (set block-refs-ids)) ;; Order matters. pages-index and blocks-index needs to come before their corresponding tx for ;; uuids to be valid. Also upstream-properties-tx comes after blocks-tx to possibly override blocks - tx (concat whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx' classes-tx blocks-index blocks-tx) + tx (concat whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx'' classes-tx' blocks-index blocks-tx) tx' (common-util/fast-remove-nils tx) ;; _ (prn :tx-counts (map count (vector whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx' classes-tx blocks-index blocks-tx))) ;; _ (when (not (seq whiteboard-pages)) (cljs.pprint/pprint {#_:property-pages-tx #_property-pages-tx :tx tx'})) @@ -1297,6 +1347,7 @@ upstream-properties-tx (build-upstream-properties-tx @conn @(:upstream-properties tx-options) (:import-state options) log-fn) + ;; _ (when (seq upstream-properties-tx) (cljs.pprint/pprint {:upstream-properties-tx upstream-properties-tx})) upstream-tx-report (when (seq upstream-properties-tx) (d/transact! conn upstream-properties-tx {::new-graph? true}))] ;; Return all tx-reports that occurred in this fn as UI needs to know what changed @@ -1392,7 +1443,7 @@ (defn- export-class-properties [conn repo-or-conn] (let [user-classes (->> (d/q '[:find (pull ?b [:db/id :db/ident]) - :where [?b :block/type "class"]] @conn) + :where [?b :block/tags :logseq.class/Tag]] @conn) (map first) (remove #(db-class/built-in-classes (:db/ident %)))) class-to-prop-uuids @@ -1404,7 +1455,7 @@ [(contains? ?user-classes ?class)] [?b ?prop _] [?prop-e :db/ident ?prop] - [?prop-e :block/type "property"]] + [?prop-e :block/tags :logseq.class/Property]] @conn (set (map :db/ident user-classes))) (remove #(ldb/built-in? (d/entity @conn (second %)))) diff --git a/deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs b/deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs index 9658abdafa..cba874234d 100644 --- a/deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs @@ -23,7 +23,6 @@ (sh ["git" "clone" "--depth" "1" "-b" branch "-c" "advice.detachedHead=false" "https://github.com/logseq/docs" dir] {}))) - ;; Fns for common test assertions ;; ============================== (defn get-top-block-properties @@ -62,7 +61,7 @@ (defn- get-journal-page-count [db] (->> (d/q '[:find (count ?b) :where - [?b :block/type "journal"] + [?b :block/journal-day] [?b :block/name] [?b :block/file]] db) diff --git a/deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs b/deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs index f6e37681e9..1f61a6b9e4 100644 --- a/deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs +++ b/deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs @@ -27,23 +27,6 @@ ;; ======= ;; some have been copied from db-import script -(defn- find-block-by-content [db content] - (if (instance? js/RegExp content) - (->> content - (d/q '[:find [(pull ?b [*]) ...] - :in $ ?pattern - :where [?b :block/title ?content] - [(missing? $ ?b :block/type)] - [(re-find ?pattern ?content)]] - db) - first) - (->> content - (d/q '[:find [(pull ?b [*]) ...] - :in $ ?content - :where [?b :block/title ?content] [(missing? $ ?b :block/type)]] - db) - first))) - (defn- extract-rules [rules] (rules/extract-rules rules/db-query-dsl-rules @@ -63,14 +46,6 @@ db property property-value (extract-rules [:property])) first)) -(defn- find-page-by-name [db name] - (->> name - (d/q '[:find [(pull ?b [*]) ...] - :in $ ?name - :where [?b :block/title ?name]] - db) - first)) - (defn- build-graph-files "Given a file graph directory, return all files including assets and adds relative paths on ::rpath since paths are absolute by default and exporter needs relative paths for @@ -177,7 +152,14 @@ (is (empty? (map :entity (:errors (db-validate/validate-db! @conn)))) "Created graph has no validation errors") - (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties"))) + (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties") + (is (= [] + (->> (d/q '[:find (pull ?b [:block/title {:block/tags [:db/ident]}]) + :where [?b :block/tags :logseq.class/Tag]] + @conn) + (map first) + (remove #(= [{:db/ident :logseq.class/Tag}] (:block/tags %))))) + "All classes only have :logseq.class/Tag as their tag (and don't have Page)"))) (deftest-async export-basic-graph-with-convert-all-tags ;; This graph will contain basic examples of different features to import @@ -195,25 +177,30 @@ ;; Counts ;; Includes journals as property values e.g. :logseq.task/deadline - (is (= 24 (count (d/q '[:find ?b :where [?b :block/type "journal"]] @conn)))) (is (= 24 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn)))) (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Task]] @conn)))) (is (= 3 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Query]] @conn)))) (is (= 2 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Card]] @conn)))) - ;; Don't count pages like url.md that have properties but no content + ;; Properties and tags aren't included in this count as they aren't a Page (is (= 10 - (count (->> (d/q '[:find [(pull ?b [:block/title :block/type]) ...] - :where [?b :block/title] [_ :block/page ?b] (not [?b :logseq.property/built-in?])] @conn) - (filter ldb/internal-page?)))) + (->> (d/q '[:find [?b ...] + :where + [?b :block/title] + [_ :block/page ?b] + (not [?b :logseq.property/built-in?])] @conn) + (map #(d/entity @conn %)) + (filter ldb/internal-page?) + #_(map #(select-keys % [:block/title :block/tags])) + count)) "Correct number of pages with block content") (is (= 11 (->> @conn (d/q '[:find [?ident ...] - :where [?b :block/type "class"] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])]) + :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])]) count)) "Correct number of user classes") - (is (= 4 (count (d/datoms @conn :avet :block/type "whiteboard")))) + (is (= 4 (count (d/datoms @conn :avet :block/tags :logseq.class/Whiteboard)))) (is (= 0 (count @(:ignored-properties import-state))) ":filters should be the only ignored property") (is (= 1 (count @assets)))) @@ -236,7 +223,7 @@ (is (= 18 (->> @conn (d/q '[:find [(pull ?b [:db/ident]) ...] - :where [?b :block/type "property"]]) + :where [?b :block/tags :logseq.class/Property]]) (remove #(db-malli-schema/internal-ident? (:db/ident %))) count)) "Correct number of user properties") @@ -248,7 +235,7 @@ {:db/ident :user.property/startedat :block/schema {:type :date}}} (->> @conn (d/q '[:find [(pull ?b [:db/ident :block/schema]) ...] - :where [?b :block/type "property"]]) + :where [?b :block/tags :logseq.class/Property]]) (filter #(contains? #{:prop-bool :prop-string :prop-num :rangeincludes :sameas :startedat} (keyword (name (:db/ident %))))) set)) @@ -263,45 +250,55 @@ (is (= {:user.property/prop-bool true :user.property/prop-num 5 :user.property/prop-string "woot"} - (update-vals (db-property/properties (find-block-by-content @conn "b1")) + (update-vals (db-property/properties (db-test/find-block-by-content @conn "b1")) (fn [v] (if (map? v) (db-property/ref->property-value-content @conn v) v)))) "Basic block has correct properties") (is (= #{"prop-num" "prop-string" "prop-bool"} - (->> (d/entity @conn (:db/id (find-block-by-content @conn "b1"))) + (->> (d/entity @conn (:db/id (db-test/find-block-by-content @conn "b1"))) :block/refs (map :block/title) set)) "Block with properties has correct refs") - (is (= {:user.property/prop-num2 10} - (readable-properties @conn (find-page-by-name @conn "new page"))) + (is (= {:user.property/prop-num2 10 + :block/tags [:logseq.class/Page]} + (readable-properties @conn (db-test/find-page-by-title @conn "new page"))) "New page has correct properties") (is (= {:user.property/prop-bool true :user.property/prop-num 5 - :user.property/prop-string "yeehaw"} - (readable-properties @conn (find-page-by-name @conn "some page"))) + :user.property/prop-string "yeehaw" + :block/tags [:logseq.class/Page]} + (readable-properties @conn (db-test/find-page-by-title @conn "some page"))) "Existing page has correct properties") (is (= {:user.property/rating 5.5} - (readable-properties @conn (find-block-by-content @conn ":rating float"))) - "Block with float property imports as a float")) + (readable-properties @conn (db-test/find-block-by-content @conn ":rating float"))) + "Block with float property imports as a float") + + (is (= [] + (->> (d/q '[:find (pull ?b [:block/title {:block/tags [:db/ident]}]) + :where [?b :block/tags :logseq.class/Property]] + @conn) + (map first) + (remove #(= [{:db/ident :logseq.class/Property}] (:block/tags %))))) + "All properties only have :logseq.class/Property as their tag (and don't have Page)")) (testing "built-in properties" - (is (= [(:db/id (find-block-by-content @conn "original block"))] - (mapv :db/id (:block/refs (find-block-by-content @conn #"ref to")))) + (is (= [(:db/id (db-test/find-block-by-content @conn "original block"))] + (mapv :db/id (:block/refs (db-test/find-block-by-content @conn #"ref to")))) "block with a block-ref has correct :block/refs") - (let [b (find-block-by-content @conn #"MEETING TITLE")] + (let [b (db-test/find-block-by-content @conn #"MEETING TITLE")] (is (= {} (and b (readable-properties @conn b))) ":template properties are ignored to not invalidate its property types")) (is (= {:logseq.task/deadline "Nov 26th, 2022"} - (readable-properties @conn (find-block-by-content @conn "only deadline"))) + (readable-properties @conn (db-test/find-block-by-content @conn "only deadline"))) "deadline block has correct journal as property value") (is (= {:logseq.task/deadline "Nov 25th, 2022"} - (readable-properties @conn (find-block-by-content @conn "only scheduled"))) + (readable-properties @conn (db-test/find-block-by-content @conn "only scheduled"))) "scheduled block converted to correct deadline") (is (= 1 (count (d/q '[:find [(pull ?b [*]) ...] @@ -311,28 +308,36 @@ "Only one journal page exists when deadline is on same day as journal") (is (= {:logseq.task/priority "High"} - (readable-properties @conn (find-block-by-content @conn "high priority"))) + (readable-properties @conn (db-test/find-block-by-content @conn "high priority"))) "priority block has correct property") (is (= {:logseq.task/status "Doing" :logseq.task/priority "Medium" :block/tags [:logseq.class/Task]} - (readable-properties @conn (find-block-by-content @conn "status test"))) + (readable-properties @conn (db-test/find-block-by-content @conn "status test"))) "status block has correct task properties and class") (is (= #{:logseq.task/status :block/tags} - (set (keys (readable-properties @conn (find-block-by-content @conn "old todo block"))))) + (set (keys (readable-properties @conn (db-test/find-block-by-content @conn "old todo block"))))) "old task properties like 'todo' are ignored") (is (= {:logseq.property/order-list-type "number"} - (readable-properties @conn (find-block-by-content @conn "list one"))) + (readable-properties @conn (db-test/find-block-by-content @conn "list one"))) "numered block has correct property") (is (= #{"gpt"} - (:block/alias (readable-properties @conn (find-page-by-name @conn "chat-gpt")))) + (:block/alias (readable-properties @conn (db-test/find-page-by-title @conn "chat-gpt")))) "alias set correctly") + (is (= ["y"] + (->> (d/q '[:find [?b ...] :where [?b :block/title "y"] [?b :logseq.property/parent]] + @conn) + first + (d/entity @conn) + :block/alias + (map :block/title))) + "alias set correctly on namespaced page") (is (= {:logseq.property.linked-references/includes #{"Oct 9th, 2024"} :logseq.property.linked-references/excludes #{"ref2"}} - (select-keys (readable-properties @conn (find-page-by-name @conn "chat-gpt")) + (select-keys (readable-properties @conn (db-test/find-page-by-title @conn "chat-gpt")) [:logseq.property.linked-references/excludes :logseq.property.linked-references/includes])) "linked ref filters set correctly")) @@ -346,36 +351,44 @@ (readable-properties @conn (find-block-by-property-value @conn :logseq.property/query "(property :prop-string)"))) "simple query block has correct query properties") (is (= "For example, here's a query with title text:" - (:block/title (find-block-by-content @conn #"query with title text"))) + (:block/title (db-test/find-block-by-content @conn #"query with title text"))) "Text around a simple query block is set as a query's title") (is (= {:logseq.property.view/type "List View" :logseq.property/query "{:query (task todo doing)}" :block/tags [:logseq.class/Query] :logseq.property.table/ordered-columns [:block/title]} - (readable-properties @conn (find-block-by-content @conn #"tasks with"))) + (readable-properties @conn (db-test/find-block-by-content @conn #"tasks with"))) "Advanced query has correct query properties") (is (= "tasks with todo and doing" - (:block/title (find-block-by-content @conn #"tasks with"))) + (:block/title (db-test/find-block-by-content @conn #"tasks with"))) "Advanced query has custom title migrated") ;; Cards (is (= {:block/tags [:logseq.class/Card]} - (readable-properties @conn (find-block-by-content @conn "card 1"))) + (readable-properties @conn (db-test/find-block-by-content @conn "card 1"))) "None of the card properties are imported since they are deprecated")) (testing "tags convert to classes" (is (= :user.class/Quotes___life - (:db/ident (find-page-by-name @conn "life"))) + (:db/ident (db-test/find-page-by-title @conn "life"))) "Namespaced tag's ident has hierarchy to make it unique") - (is (= [{:block/type "class"}] - (d/q '[:find [(pull ?b [:block/type]) ...] :where [?b :block/name "life"]] @conn)) + (is (= [:logseq.class/Tag] + (map :db/ident (:block/tags (db-test/find-page-by-title @conn "life")))) "When a class is used and referenced on the same page, there should only be one instance of it") (is (= ["life"] - (->> (:block/tags (find-block-by-content @conn #"with namespace tag")) + (->> (:block/tags (db-test/find-block-by-content @conn #"with namespace tag")) (mapv #(db-property/ref->property-value-contents @conn %)))) - "Block tagged with namespace tag is only associated with leaf child tag")) + "Block tagged with namespace tag is only associated with leaf child tag") + + (is (= [] + (->> (d/q '[:find (pull ?b [:block/title {:block/tags [:db/ident]}]) + :where [?b :block/tags :logseq.class/Tag]] + @conn) + (map first) + (remove #(= [{:db/ident :logseq.class/Tag}] (:block/tags %))))) + "All classes only have :logseq.class/Tag as their tag (and don't have Page)")) (testing "namespaces" (let [expand-children (fn expand-children [ent parent] @@ -386,25 +399,25 @@ (is (= [{:parent "n1" :child "x"} {:parent "x" :child "z"} {:parent "x" :child "y"}] - (rest (expand-children (d/entity @conn (:db/id (find-page-by-name @conn "n1"))) nil))) + (rest (expand-children (d/entity @conn (:db/id (db-test/find-page-by-title @conn "n1"))) nil))) "First namespace tests duplicate parent page name") (is (= [{:parent "n2" :child "x"} {:parent "x" :child "z"} {:parent "n2" :child "alias"}] - (rest (expand-children (d/entity @conn (:db/id (find-page-by-name @conn "n2"))) nil))) + (rest (expand-children (d/entity @conn (:db/id (db-test/find-page-by-title @conn "n2"))) nil))) "First namespace tests duplicate child page name and built-in page name"))) (testing "journal timestamps" (is (= (date-time-util/journal-day->ms 20240207) - (:block/created-at (find-page-by-name @conn "Feb 7th, 2024"))) + (:block/created-at (db-test/find-page-by-title @conn "Feb 7th, 2024"))) "journal pages are created on their journal day") (is (= (date-time-util/journal-day->ms 20240207) - (:block/created-at (find-block-by-content @conn #"Inception"))) + (:block/created-at (db-test/find-block-by-content @conn #"Inception"))) "journal blocks are created on their page's journal day")) (testing "db attributes" (is (= true - (:block/collapsed? (find-block-by-content @conn "collapsed block"))) + (:block/collapsed? (db-test/find-block-by-content @conn "collapsed block"))) "Collapsed blocks are imported")) (testing "property :type changes" @@ -419,7 +432,7 @@ (get-in (d/entity @conn :user.property/description) [:block/schema :type])) ":default property to :node (or any non :default value) remains :default") (is (= "[[Jakob]]" - (:user.property/description (readable-properties @conn (find-block-by-content @conn #":default to :node")))) + (:user.property/description (readable-properties @conn (db-test/find-block-by-content @conn #":default to :node")))) ":default to :node property saves :default property value default with full text") (testing "with changes to upstream/existing property value" @@ -427,19 +440,19 @@ (get-in (d/entity @conn :user.property/duration) [:block/schema :type])) ":number property to :default value changes to :default") (is (= "20" - (:user.property/duration (readable-properties @conn (find-block-by-content @conn "existing :number to :default")))) + (:user.property/duration (readable-properties @conn (db-test/find-block-by-content @conn "existing :number to :default")))) "existing :number property value correctly saved as :default") (is (= {:block/schema {:type :default} :db/cardinality :db.cardinality/many} (select-keys (d/entity @conn :user.property/people) [:block/schema :db/cardinality])) ":node property to :default value changes to :default and keeps existing cardinality") (is (= #{"[[Jakob]] [[Gabriel]]"} - (:user.property/people (readable-properties @conn (find-block-by-content @conn ":node people")))) + (:user.property/people (readable-properties @conn (db-test/find-block-by-content @conn ":node people")))) "existing :node property value correctly saved as :default with full text") (is (= #{"[[Gabriel]] [[Jakob]]"} - (:user.property/people (readable-properties @conn (find-block-by-content @conn #"pending block for :node")))) + (:user.property/people (readable-properties @conn (db-test/find-block-by-content @conn #"pending block for :node")))) "pending :node property value correctly saved as :default with full text") - (is (some? (find-page-by-name @conn "Jakob")) + (is (some? (db-test/find-page-by-title @conn "Jakob")) "Previous :node property value still exists") (is (= 3 (count (find-block-by-property @conn :user.property/people))) "Converted property has correct number of property values"))) @@ -448,17 +461,20 @@ (is (= #{:logseq.property/description :user.property/description} (set (d/q '[:find [?ident ...] :where [?b :db/ident ?ident] [?b :block/name "description"]] @conn))) "user description property is separate from built-in one") - (is (= #{"page" "class"} - (set (d/q '[:find [?type ...] :where [?b :block/type ?type] [?b :block/name "task"]] @conn))) + (is (= #{"Page" "Tag"} + (set (d/q '[:find [?t-title ...] :where + [?b :block/tags ?t] + [?b :block/name "task"] + [?t :block/title ?t-title]] @conn))) "user page is separate from built-in class")) (testing "multiline blocks" - (is (= "|markdown| table|\n|some|thing|" (:block/title (find-block-by-content @conn #"markdown.*table")))) - (is (= "multiline block\na 2nd\nand a 3rd" (:block/title (find-block-by-content @conn #"multiline block")))) - (is (= "logbook block" (:block/title (find-block-by-content @conn #"logbook block"))))) + (is (= "|markdown| table|\n|some|thing|" (:block/title (db-test/find-block-by-content @conn #"markdown.*table")))) + (is (= "multiline block\na 2nd\nand a 3rd" (:block/title (db-test/find-block-by-content @conn #"multiline block")))) + (is (= "logbook block" (:block/title (db-test/find-block-by-content @conn #"logbook block"))))) (testing ":block/refs and :block/path-refs" - (let [page (find-page-by-name @conn "chat-gpt")] + (let [page (db-test/find-page-by-title @conn "chat-gpt")] (is (set/subset? #{"type" "LargeLanguageModel"} (->> page :block/refs (map #(:block/title (d/entity @conn (:db/id %)))) set)) @@ -468,7 +484,7 @@ (->> page :block/path-refs (map #(:block/title (d/entity @conn (:db/id %)))) set)) "Page has correct property and property value :block/path-refs")) - (let [block (find-block-by-content @conn "old todo block")] + (let [block (db-test/find-block-by-content @conn "old todo block")] (is (set/subset? #{:logseq.task/status :logseq.class/Task} (->> block @@ -485,7 +501,7 @@ "Block has correct task tag and property :block/path-refs"))) (testing "whiteboards" - (let [block-with-props (find-block-by-content @conn #"block with props")] + (let [block-with-props (db-test/find-block-by-content @conn #"block with props")] (is (= {:user.property/prop-num 10} (readable-properties @conn block-with-props))) (is (= "block with props" (:block/title block-with-props))))))) @@ -501,7 +517,7 @@ (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties") (is (= 0 (->> @conn (d/q '[:find [?ident ...] - :where [?b :block/type "class"] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])]) + :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])]) count)) "Correct number of user classes") @@ -511,7 +527,7 @@ (testing "replacing refs in :block/title when :remove-inline-tags? set" (is (= 2 - (->> (find-block-by-content @conn #"replace with same start string") + (->> (db-test/find-block-by-content @conn #"replace with same start string") :block/title (re-seq db-content/id-ref-pattern) distinct @@ -519,7 +535,7 @@ "A block with ref names that start with same string has 2 distinct refs") (is (= 1 - (->> (find-block-by-content @conn #"replace case insensitive") + (->> (db-test/find-block-by-content @conn #"replace case insensitive") :block/title (re-seq db-content/id-ref-pattern) distinct @@ -527,9 +543,9 @@ "A block with different case of same ref names has 1 distinct ref")) (testing "tags convert to page, refs and page-tags" - (let [block (find-block-by-content @conn #"Inception") - tag-page (find-page-by-name @conn "Movie") - tagged-page (find-page-by-name @conn "Interstellar")] + (let [block (db-test/find-block-by-content @conn #"Inception") + tag-page (db-test/find-page-by-title @conn "Movie") + tagged-page (db-test/find-page-by-title @conn "Interstellar")] (is (string/starts-with? (str (:block/title block)) "Inception [[") "tagged block tag converts tag to page ref") (is (= [(:db/id tag-page)] (map :db/id (:block/refs block))) @@ -537,11 +553,11 @@ (is (and tag-page (not (ldb/class? tag-page))) "tag page is not a class") - (is (= {:logseq.property/page-tags #{"Movie"}} - (readable-properties @conn tagged-page)) + (is (= #{"Movie"} + (:logseq.property/page-tags (readable-properties @conn tagged-page))) "tagged page has existing page imported as a tag to page-tags") (is (= #{"LargeLanguageModel" "fun" "ai"} - (:logseq.property/page-tags (readable-properties @conn (find-page-by-name @conn "chat-gpt")))) + (:logseq.property/page-tags (readable-properties @conn (db-test/find-page-by-title @conn "chat-gpt")))) "tagged page has new page and other pages marked with '#' and '[[]]` imported as tags to page-tags"))))) (deftest-async export-files-with-tag-classes-option @@ -549,26 +565,25 @@ files (mapv #(node-path/join file-graph-dir %) ["journals/2024_02_07.md" "pages/Interstellar.md"]) conn (db-test/create-conn) _ (import-files-to-db files conn {:tag-classes ["movie"]})] - (is (empty? (map :entity (:errors (db-validate/validate-db! @conn)))) "Created graph has no validation errors") - (let [block (find-block-by-content @conn #"Inception") - tag-page (find-page-by-name @conn "Movie") - another-tag-page (find-page-by-name @conn "p0")] + (let [block (db-test/find-block-by-content @conn #"Inception") + tag-page (db-test/find-page-by-title @conn "Movie") + another-tag-page (db-test/find-page-by-title @conn "p0")] (is (= (:block/title block) "Inception") "tagged block with configured tag strips tag from content") (is (= [:user.class/Movie] (:block/tags (readable-properties @conn block))) "tagged block has configured tag imported as a class") - (is (= "class" (:block/type tag-page)) + (is (= [:logseq.class/Tag] (mapv :db/ident (:block/tags tag-page))) "configured tag page in :tag-classes is a class") (is (and another-tag-page (not (ldb/class? another-tag-page))) "unconfigured tag page is not a class") - (is (= {:block/tags [:user.class/Movie]} - (readable-properties @conn (find-page-by-name @conn "Interstellar"))) + (is (= {:block/tags [:logseq.class/Page :user.class/Movie]} + (readable-properties @conn (db-test/find-page-by-title @conn "Interstellar"))) "tagged page has configured tag imported as a class")))) (deftest-async export-files-with-property-classes-option @@ -586,7 +601,7 @@ (is (= #{:user.class/Property :user.class/Movie :user.class/Class :user.class/Tool} (->> @conn (d/q '[:find [?ident ...] - :where [?b :block/type "class"] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])]) + :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])]) set)) "All classes are correctly defined by :type") @@ -597,8 +612,8 @@ set)) "Properties are correctly inferred for a class") - (let [block (find-block-by-content @conn #"The Creator") - tag-page (find-page-by-name @conn "Movie")] + (let [block (db-test/find-block-by-content @conn #"The Creator") + tag-page (db-test/find-page-by-title @conn "Movie")] (is (= (:block/title block) "The Creator") "tagged block with configured tag strips tag from content") (is (= [:user.class/Movie] @@ -608,14 +623,14 @@ "tagged block can have another property that references the same class it is tagged with, without creating a duplicate class") - (is (= "class" (:block/type tag-page)) + (is (= [:logseq.class/Tag] (map :db/ident (:block/tags tag-page))) "configured tag page derived from :property-classes is a class") - (is (nil? (find-page-by-name @conn "type")) + (is (nil? (db-test/find-page-by-title @conn "type")) "No page exists for configured property") - (is (= [:user.class/Property] - (:block/tags (readable-properties @conn (find-page-by-name @conn "url")))) - "tagged page has configured tag imported as a class")))) + (is (= #{:user.class/Property :logseq.class/Property} + (set (:block/tags (readable-properties @conn (db-test/find-page-by-title @conn "url"))))) + "tagged page has correct tags including one from option")))) (deftest-async export-files-with-remove-inline-tags (p/let [file-graph-dir "test/resources/exporter-test-graph" @@ -625,7 +640,7 @@ (is (empty? (map :entity (:errors (db-validate/validate-db! @conn)))) "Created graph has no validation errors") - (is (string/starts-with? (:block/title (find-block-by-content @conn #"Inception")) + (is (string/starts-with? (:block/title (db-test/find-block-by-content @conn #"Inception")) "Inception #Movie") "block with tag preserves inline tag"))) @@ -654,7 +669,7 @@ :user.class/Class :user.class/Tool :user.class/Whiteboard___Tool} (->> @conn (d/q '[:find [?ident ...] - :where [?b :block/type "class"] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])]) + :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])]) set)) "All classes are correctly defined by :type") diff --git a/deps/graph-parser/test/logseq/graph_parser/extract_test.cljs b/deps/graph-parser/test/logseq/graph_parser/extract_test.cljs index c7936b3a1c..0eb2f2e3a9 100644 --- a/deps/graph-parser/test/logseq/graph_parser/extract_test.cljs +++ b/deps/graph-parser/test/logseq/graph_parser/extract_test.cljs @@ -2,8 +2,7 @@ (:require [cljs.test :refer [deftest is are]] [logseq.graph-parser.extract :as extract] [datascript.core :as d] - [logseq.db.frontend.schema :as db-schema] - [logseq.db :as ldb])) + [logseq.db.frontend.schema :as db-schema])) ;; This is a copy of frontend.util.fs/multiplatform-reserved-chars for reserved chars testing (def multiplatform-reserved-chars ":\\*\\?\"<>|\\#\\\\") @@ -145,6 +144,6 @@ page (first pages)] (is (= (get-in page [:block/file :file/path]) "/whiteboards/foo.edn")) (is (= (:block/name page) "foo")) - (is (ldb/whiteboard? page)) + (is (= (:block/type page) "whiteboard")) (is (= (:block/title page) "Foo")) (is (every? #(= (:block/parent %) [:block/uuid #uuid "a846e3b4-c41d-4251-80e1-be6978c36d8c"]) blocks)))) diff --git a/deps/outliner/src/logseq/outliner/core.cljs b/deps/outliner/src/logseq/outliner/core.cljs index 2259fb821d..4afec51961 100644 --- a/deps/outliner/src/logseq/outliner/core.cljs +++ b/deps/outliner/src/logseq/outliner/core.cljs @@ -80,7 +80,8 @@ (let [refs (:block/_refs page)] (and (or (zero? (count refs)) (= #{db-id} (set (map :db/id refs)))) - (not (some #{"class" "property"} (:block/type page))))))}))] + (not (ldb/class? page)) + (not (ldb/property? page)))))}))] (when (seq orphaned-pages) (let [tx (mapv (fn [page] [:db/retractEntity (:db/id page)]) orphaned-pages)] (swap! txs-state (fn [state] (vec (concat state tx))))))))) diff --git a/deps/outliner/src/logseq/outliner/property.cljs b/deps/outliner/src/logseq/outliner/property.cljs index 84ee077463..d594f06682 100644 --- a/deps/outliner/src/logseq/outliner/property.cljs +++ b/deps/outliner/src/logseq/outliner/property.cljs @@ -38,7 +38,7 @@ multiple-values-empty? (and (sequential? old-value) (contains? (set (map :db/ident old-value)) :logseq.property/empty-placeholder)) block' (assoc (outliner-core/block-with-updated-at {:db/id (:db/id block)}) - property-id value) + property-id value) block-tx-data (cond-> block' (and status? (not (ldb/class-instance? (d/entity @conn :logseq.class/Task) block))) (assoc :block/tags :logseq.class/Task))] @@ -160,6 +160,8 @@ 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'}}) (ldb/transact! conn [(sqlite-util/build-new-property db-ident' schema {:title k-name})] {:outliner-op :new-property}) @@ -281,10 +283,16 @@ _ (assert (qualified-keyword? property-id) "property-id should be a keyword") block (d/entity @conn block-eid) db-attribute? (some? (db-schema/schema-for-db-based-graph property-id))] - (if db-attribute? + (when (= property-id :block/tags) + (outliner-validate/validate-tags-property @conn [block-eid] v)) + (when (= property-id :logseq.property/parent) + (outliner-validate/validate-parent-property v [block])) + (cond + db-attribute? (when-not (and (= property-id :block/alias) (= v (:db/id block))) ; alias can't be itself (ldb/transact! conn [{:db/id (:db/id block) property-id v}] {:outliner-op :save-block})) + :else (let [property (d/entity @conn property-id) _ (assert (some? property) (str "Property " property-id " doesn't exist yet")) property-type (get-in property [:block/schema :type] :default) @@ -302,6 +310,8 @@ (assert property-id "property-id is nil") (throw-error-if-read-only-property property-id) (let [block-eids (map ->eid block-ids) + _ (when (= property-id :block/tags) + (outliner-validate/validate-tags-property @conn block-eids v)) property (d/entity @conn property-id) _ (when (= (:db/ident property) :logseq.property/parent) (outliner-validate/validate-parent-property @@ -553,7 +563,6 @@ (when (seq values) (let [value-property-tx (map (fn [id] {:db/id id - :block/type "closed value" :block/closed-value-property (:db/id property)}) (map :db/id values)) property-tx (outliner-core/block-with-updated-at {:db/id (:db/id property)})] diff --git a/deps/outliner/src/logseq/outliner/validate.cljs b/deps/outliner/src/logseq/outliner/validate.cljs index b3b1d05bfc..4e0c648ff7 100644 --- a/deps/outliner/src/logseq/outliner/validate.cljs +++ b/deps/outliner/src/logseq/outliner/validate.cljs @@ -1,11 +1,13 @@ (ns logseq.outliner.validate - "Reusable DB graph validations for outliner level and above. Most validations throw - errors so the user action stops immediately to display a notification" + "Reusable DB graph validations for outliner level and above. Most validations + throw errors so the user action stops immediately to display a notification" (:require [clojure.string :as string] [datascript.core :as d] [logseq.db :as ldb] [logseq.common.date :as common-date] - [logseq.common.util.namespace :as ns-util])) + [logseq.common.util.namespace :as ns-util] + [clojure.set :as set] + [logseq.db.frontend.class :as db-class])) (defn ^:api validate-page-title-characters "Validates characters that must not be in a page title" @@ -42,30 +44,6 @@ :payload {:message "Built-in pages can't be edited" :type :warning}})))) -(defn- validate-unique-for-property-page - [entity db new-title] - (when-let [_res (seq (d/q (if (:logseq.property/built-in? entity) - '[:find [?b ...] - :in $ ?eid ?title - :where - [?b :block/title ?title] - [?b :block/type "property"] - [(not= ?b ?eid)]] - '[:find [?b ...] - :in $ ?eid ?title - :where - [?b :block/title ?title] - [?b :block/type "property"] - [(missing? $ ?b :logseq.property/built-in?)] - [(not= ?b ?eid)]]) - db - (:db/id entity) - new-title))] - (throw (ex-info "Duplicate property" - {:type :notification - :payload {:message (str "Another property named " (pr-str new-title) " already exists") - :type :warning}})))) - (defn- validate-unique-by-parent-and-name [db entity new-title] (when-let [_res (seq (d/q '[:find [?b ...] :in $ ?eid ?type ?title @@ -88,50 +66,49 @@ (defn- validate-unique-for-page [db new-title {:block/keys [tags] :as entity}] (cond - (and (seq tags) (ldb/internal-page? entity)) - (when-let [res (seq (d/q '[:find [?b ...] - :in $ ?eid ?title [?tag-id ...] - :where - [?b :block/title ?title] - [?b :block/tags ?tag-id] - [(not= ?b ?eid)]] - db - (:db/id entity) - new-title - (map :db/id tags)))] - (throw (ex-info "Duplicate page by tag" - {:type :notification - :payload {:message (str "Another page named " (pr-str new-title) " already exists for tag " - (pr-str (->> res first (d/entity db) :block/tags first :block/title))) - :type :warning}}))) - - (ldb/property? entity) - (validate-unique-for-property-page entity db new-title) + (seq tags) + (when-let [another-id (first + (d/q (if (ldb/property? entity) + ;; Property names are unique in that they can + ;; have the same names as built-in property names + '[:find [?b ...] + :in $ ?eid ?title [?tag-id ...] + :where + [?b :block/title ?title] + [?b :block/tags ?tag-id] + [(missing? $ ?b :logseq.property/built-in?)] + [(not= ?b ?eid)]] + '[:find [?b ...] + :in $ ?eid ?title [?tag-id ...] + :where + [?b :block/title ?title] + [?b :block/tags ?tag-id] + [(not= ?b ?eid)]]) + db + (:db/id entity) + new-title + (map :db/id tags)))] + (let [another (d/entity db another-id) + this-tags (set (map :db/ident tags)) + another-tags (set (map :db/ident (:block/tags another))) + common-tag-ids (set/intersection this-tags another-tags)] + (when-not (and (= common-tag-ids #{:logseq.class/Page}) + (> (count this-tags) 1) + (> (count another-tags) 1)) + (throw (ex-info "Duplicate page" + {:type :notification + :payload {:message (str "Another page named " (pr-str new-title) " already exists for tags: " + (string/join ", " + (map (fn [id] (str "#" (:block/title (d/entity db id)))) common-tag-ids))) + :type :warning}}))))) (:logseq.property/parent entity) - (validate-unique-by-parent-and-name db entity new-title) - - :else - (when-let [_res (seq (d/q '[:find [?b ...] - :in $ ?eid ?type ?title - :where - [?b :block/title ?title] - [?b :block/type ?type] - [(not= ?b ?eid)]] - db - (:db/id entity) - (:block/type entity) - new-title))] - (throw (ex-info "Duplicate page without tag" - {:type :notification - :payload {:message (str "Another page named " (pr-str new-title) " already exists") - :type :warning}}))))) + (validate-unique-by-parent-and-name db entity new-title))) (defn ^:api validate-unique-by-name-tag-and-block-type "Validates uniqueness of nodes for the following cases: - - Page names of type 'page' are unique by tag e.g. their can be Apple #Company and Apple #Fruit - - Page names of other types are unique for their type e.g. their can be #Journal ('class') and Journal ('page') - - Property names are unique and don't consider built-in property names" + - Page names are unique for a tag e.g. their can be Apple #Company and Apple #Fruit + - Page names are unique for a :logseq.property/parent" [db new-title entity] (when (ldb/page? entity) (validate-unique-for-page db new-title entity))) @@ -153,7 +130,7 @@ (validate-unique-by-name-tag-and-block-type db new-title existing-block-entity) (validate-disallow-page-with-journal-name new-title existing-block-entity)) -(defn validate-parent-property +(defn- validate-parent-property-have-same-type "Validates whether given parent and children are valid. Allows 'class' and 'page' types to have a relationship with their own type. May consider allowing more page types if they don't cause systemic bugs" @@ -166,3 +143,56 @@ :payload {:message "Can't set this page as a parent because the child page is a different type" :type :warning} :blocks (map #(select-keys % [:db/id :block/title]) (remove ldb/class? child-ents))})))) + +(defn- disallow-built-in-class-parent-change + [_parent-ent child-ents] + (when (some #(get db-class/built-in-classes (:db/ident %)) child-ents) + (throw (ex-info "Can't change the parent of a built-in tag" + {:type :notification + :payload {:message "Can't change the parent of a built-in tag" + :type :warning}})))) + +(defn validate-parent-property + [parent-ent child-ents] + (disallow-built-in-class-parent-change parent-ent child-ents) + (validate-parent-property-have-same-type parent-ent child-ents)) + +(defn- disallow-node-cant-tag-with-built-in-non-tags + [db _block-eids v] + (let [tag-ent (d/entity db v)] + (when (and (:logseq.property/built-in? tag-ent) + (not (ldb/class? tag-ent))) + (throw (ex-info (str "Can't set tag with built-in page that isn't a tag " (pr-str (:block/title tag-ent))) + {:type :notification + :payload {:message (str "Can't set tag with built-in page that isn't a tag " (pr-str (:block/title tag-ent))) + :type :error} + :property-value v}))))) + +(defn- disallow-node-cant-tag-with-private-tags + [db block-eids v] + (when (and (ldb/private-tags (:db/ident (d/entity db v))) + ;; Allow assets to be tagged + (not (and + (every? (fn [id] (ldb/asset? (d/entity db id))) block-eids) + (= :logseq.class/Asset (:db/ident (d/entity db v)))))) + (throw (ex-info (str "Can't set tag with built-in #" (:block/title (d/entity db v))) + {:type :notification + :payload {:message (str "Can't set tag with built-in #" (:block/title (d/entity db v))) + :type :error} + :property-id :block/tags + :property-value v})))) + +(defn- disallow-tagging-a-built-in-entity + [db block-eids] + (when-let [built-in-ent (some #(when (:logseq.property/built-in? %) %) + (map #(d/entity db %) block-eids))] + (throw (ex-info (str "Can't add tag on built-in " (pr-str (:block/title built-in-ent))) + {:type :notification + :payload {:message (str "Can't add tag on built-in " (pr-str (:block/title built-in-ent))) + :type :error}})))) + +(defn validate-tags-property + [db block-eids v] + (disallow-tagging-a-built-in-entity db block-eids) + (disallow-node-cant-tag-with-private-tags db block-eids v) + (disallow-node-cant-tag-with-built-in-non-tags db block-eids v)) \ No newline at end of file diff --git a/deps/outliner/test/logseq/outliner/pipeline_test.cljs b/deps/outliner/test/logseq/outliner/pipeline_test.cljs index e6aa7e7a3f..be7dc7fbc0 100644 --- a/deps/outliner/test/logseq/outliner/pipeline_test.cljs +++ b/deps/outliner/test/logseq/outliner/pipeline_test.cljs @@ -8,14 +8,16 @@ [logseq.outliner.pipeline :as outliner-pipeline] [clojure.string :as string] [logseq.db.test.helper :as db-test] - [logseq.common.util.page-ref :as page-ref])) + [logseq.common.util.page-ref :as page-ref] + [clojure.set :as set])) (defn- get-blocks [db] (->> (d/q '[:find (pull ?b [* {:block/path-refs [:block/name :db/id]}]) :in $ - :where [?b :block/title] - [(missing? $ ?b :logseq.property/built-in?)] - [(missing? $ ?b :block/type)]] + :where + [?b :block/page] + [?b :block/title] + [(missing? $ ?b :logseq.property/built-in?)]] db) (map first))) @@ -48,13 +50,14 @@ updated-blocks (->> (get-blocks @conn) ;; Only keep enough of content to uniquely identify block (map #(hash-map :block/title (re-find #"\w+" (:block/title %)) - :path-ref-names (set (map :block/name (:block/path-refs %))))))] + :path-ref-names (set (map :block/name (:block/path-refs %)))))) + page-tag-refs #{"tags" "page"}] (is (= [{:block/title "parent" - :path-ref-names #{"page1" "bar"}} + :path-ref-names (set/union page-tag-refs #{"page1" "bar"})} {:block/title "child" - :path-ref-names #{"page1" "bar" "baz"}} + :path-ref-names (set/union page-tag-refs #{"page1" "bar" "baz"})} {:block/title "grandchild" - :path-ref-names #{"page1" "bar" "baz" "bing"}}] + :path-ref-names (set/union page-tag-refs #{"page1" "bar" "baz" "bing"})}] updated-blocks))))) (deftest block-content-refs diff --git a/deps/outliner/test/logseq/outliner/property_test.cljs b/deps/outliner/test/logseq/outliner/property_test.cljs index 172daf0541..8a36bc1232 100644 --- a/deps/outliner/test/logseq/outliner/property_test.cljs +++ b/deps/outliner/test/logseq/outliner/property_test.cljs @@ -3,7 +3,8 @@ [datascript.core :as d] [logseq.outliner.property :as outliner-property] [logseq.db.frontend.property :as db-property] - [logseq.db.test.helper :as db-test])) + [logseq.db.test.helper :as db-test] + [logseq.db :as ldb])) (deftest upsert-property! (testing "Creates a property" @@ -228,7 +229,7 @@ (testing "Add choice successfully" (let [_ (outliner-property/upsert-closed-value! conn :user.property/num {:value 3}) b (first (d/q '[:find [(pull ?b [*]) ...] :where [?b :property.value/content 3]] @conn))] - (is (= (:block/type b) "closed value")) + (is (ldb/closed-value? (d/entity @conn (:db/id b)))) (is (= [2 3] (map db-property/closed-value-content (:block/_closed-value-property (d/entity @conn :user.property/num))))))) diff --git a/deps/outliner/test/logseq/outliner/validate_test.cljs b/deps/outliner/test/logseq/outliner/validate_test.cljs index 16657c7ba5..994bc9b6ed 100644 --- a/deps/outliner/test/logseq/outliner/validate_test.cljs +++ b/deps/outliner/test/logseq/outliner/validate_test.cljs @@ -4,70 +4,64 @@ [logseq.outliner.validate :as outliner-validate] [logseq.db.test.helper :as db-test])) -(defn- find-block-by-content [conn content] - (->> content - (d/q '[:find [(pull ?b [*]) ...] - :in $ ?content - :where [?b :block/title ?content] [(missing? $ ?b :logseq.property/built-in?)]] - @conn) - first)) - (deftest validate-block-title-unique-for-properties (let [conn (db-test/create-conn-with-blocks - ;; use a property name that's same as built-in - {:properties {:background-image {:block/schema {:type :default}}}})] + {:properties {:color {:block/schema {:type :default}} + :color2 {:block/schema {:type :default}}}})] (is (nil? (outliner-validate/validate-unique-by-name-tag-and-block-type @conn - "background-color" - (assoc (find-block-by-content conn "background-image") :db/id 10000))) + (:block/title (d/entity @conn :logseq.property/background-color)) + (d/entity @conn :user.property/color))) "Allow user property to have same name as built-in property") (is (thrown-with-msg? js/Error - #"Duplicate property" + #"Duplicate page" (outliner-validate/validate-unique-by-name-tag-and-block-type @conn - "background-image" - (assoc (find-block-by-content conn "background-image") :db/id 10000))) + "color" + (d/entity @conn :user.property/color2))) "Disallow duplicate user property"))) (deftest validate-block-title-unique-for-pages (let [conn (db-test/create-conn-with-blocks [{:page {:block/title "page1"}} + {:page {:block/title "another page"}} {:page {:block/title "Apple" :build/tags [:Company]}} + {:page {:block/title "Another Company" :build/tags [:Company]}} {:page {:block/title "Banana" :build/tags [:Fruit]}}])] (is (thrown-with-msg? js/Error - #"Duplicate page by tag" + #"Duplicate page" (outliner-validate/validate-unique-by-name-tag-and-block-type @conn "Apple" - (assoc (find-block-by-content conn "Apple") :db/id 10000))) + (db-test/find-page-by-title @conn "Another Company"))) "Disallow duplicate page with tag") (is (nil? (outliner-validate/validate-unique-by-name-tag-and-block-type @conn "Apple" - (find-block-by-content conn "Banana"))) + (db-test/find-page-by-title @conn "Banana"))) "Allow page with same name for different tag") (is (thrown-with-msg? js/Error - #"Duplicate page without tag" + #"Duplicate page" (outliner-validate/validate-unique-by-name-tag-and-block-type @conn "page1" - (assoc (find-block-by-content conn "page1") :db/id 10000))) + (db-test/find-page-by-title @conn "another page"))) "Disallow duplicate page without tag") (is (nil? (outliner-validate/validate-unique-by-name-tag-and-block-type @conn "Apple" - (find-block-by-content conn "Fruit"))) + (db-test/find-page-by-title @conn "Fruit"))) "Allow class to have same name as a page"))) (deftest validate-parent-property @@ -77,11 +71,11 @@ :pages-and-blocks [{:page {:block/title "page1"}} {:page {:block/title "page2"}}]}) - page1 (find-block-by-content conn "page1") - page2 (find-block-by-content conn "page2") - class1 (find-block-by-content conn "Class1") - class2 (find-block-by-content conn "Class2") - property (find-block-by-content conn "prop1")] + page1 (db-test/find-page-by-title @conn "page1") + page2 (db-test/find-page-by-title @conn "page2") + class1 (db-test/find-page-by-title @conn "Class1") + class2 (db-test/find-page-by-title @conn "Class2") + property (db-test/find-page-by-title @conn "prop1")] (testing "valid parent and child combinations" (is (nil? (outliner-validate/validate-parent-property page1 [page2])) @@ -99,7 +93,52 @@ class1 page1 page1 class1 property page1 - property class1)))) + property class1)) + + (testing "built-in tag can't have parent changed" + (is (thrown-with-msg? + js/Error + #"Can't change.*built-in" + (outliner-validate/validate-parent-property (d/entity @conn :logseq.class/Task) + [(d/entity @conn :logseq.class/Cards)])))))) + +(deftest validate-tags-property + (let [conn (db-test/create-conn-with-blocks + {:classes {:SomeTag {}} + :pages-and-blocks + [{:page {:block/title "page1"} + :blocks [{:block/title "block"}]}]}) + block (db-test/find-block-by-content @conn "block")] + + (is (thrown-with-msg? + js/Error + #"Can't add tag.*Tag" + (outliner-validate/validate-tags-property @conn [:logseq.class/Tag] :user.class/SomeTag)) + "built-in tag must not be tagged by the user") + + (is (thrown-with-msg? + js/Error + #"Can't add tag.*Heading" + (outliner-validate/validate-tags-property @conn [:logseq.property/heading] :user.class/SomeTag)) + "built-in property must not be tagged by the user") + + (is (thrown-with-msg? + js/Error + #"Can't add tag.*Contents" + (outliner-validate/validate-tags-property @conn [(:db/id (db-test/find-page-by-title @conn "Contents"))] :user.class/SomeTag)) + "built-in page must not be tagged by the user") + + (is (thrown-with-msg? + js/Error + #"Can't set tag.*Page" + (outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.class/Page)) + "Nodes can't be tagged with built-in private tags") + + (is (thrown-with-msg? + js/Error + #"Can't set tag.*Priority" + (outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.task/priority)) + "Nodes can't be tagged with built-in non tags"))) ;; Try as many of the validations against a new graph to confirm ;; that validations make sense and are valid for a new graph @@ -107,7 +146,11 @@ (let [conn (db-test/create-conn)] (testing "Validate pages" - (let [pages (d/q '[:find [(pull ?b [*]) ...] :where [?b :block/title] [?b :block/type]] @conn) + (let [pages (->> (d/q '[:find [?b ...] :where + [?b :block/title] + [?b :block/tags]] @conn) + (map (fn [id] + (d/entity @conn id)))) page-errors (atom {})] (doseq [page pages] (try @@ -123,9 +166,10 @@ "Default pages shouldn't have any validation errors"))) (testing "Validate property relationships" - (let [parent-child-pairs (d/q '[:find (pull ?parent [:block/title :block/type]) - (pull ?child [:block/title :block/type]) + (let [parent-child-pairs (d/q '[:find ?parent ?child :where [?child :logseq.property/parent ?parent]] @conn)] - (doseq [[parent child] parent-child-pairs] - (is (nil? (outliner-validate/validate-parent-property parent [child])) - (str "Parent and child page is valid: " (pr-str (:block/title parent)) " " (pr-str (:block/title child))))))))) \ No newline at end of file + (doseq [[parent-id child-id] parent-child-pairs] + (let [parent (d/entity @conn parent-id) + child (d/entity @conn child-id)] + (is (nil? (#'outliner-validate/validate-parent-property-have-same-type parent [child])) + (str "Parent and child page is valid: " (pr-str (:block/title parent)) " " (pr-str (:block/title child)))))))))) diff --git a/deps/shui/src/logseq/shui/demo.cljs b/deps/shui/src/logseq/shui/demo.cljs index 3663fdcab7..861f734699 100644 --- a/deps/shui/src/logseq/shui/demo.cljs +++ b/deps/shui/src/logseq/shui/demo.cljs @@ -16,475 +16,488 @@ [] (let [icon #(ui/tabler-icon (name %1) {:class "scale-90 pr-1 opacity-80"})] (ui/dropdown-menu-content - {:class "w-56" - :on-click (fn [^js e] (some-> (.-target e) (.-innerText) - (#(identity ["You select: " [:b.text-red-700 %1]])) (ui/toast! :info)))} - (ui/dropdown-menu-label "My Account") - (ui/dropdown-menu-separator) - (ui/dropdown-menu-group + {:class "w-56" + :on-click (fn [^js e] (some-> (.-target e) (.-innerText) + (#(identity ["You select: " [:b.text-red-700 %1]])) (ui/toast! :info)))} + (ui/dropdown-menu-label "My Account") + (ui/dropdown-menu-separator) + (ui/dropdown-menu-group ;; items - (ui/dropdown-menu-item (icon :user) "Profile" (ui/dropdown-menu-shortcut "⌘P")) - (ui/dropdown-menu-item (icon :brand-mastercard) [:span "Billing"] (ui/dropdown-menu-shortcut "⌘B")) - (ui/dropdown-menu-item (icon :adjustments-alt) [:span "Settings"] (ui/dropdown-menu-shortcut "⌘,")) - (ui/dropdown-menu-item (icon :keyboard) [:span "Keyboard shortcuts"])) - (ui/dropdown-menu-separator) + (ui/dropdown-menu-item (icon :user) "Profile" (ui/dropdown-menu-shortcut "⌘P")) + (ui/dropdown-menu-item (icon :brand-mastercard) [:span "Billing"] (ui/dropdown-menu-shortcut "⌘B")) + (ui/dropdown-menu-item (icon :adjustments-alt) [:span "Settings"] (ui/dropdown-menu-shortcut "⌘,")) + (ui/dropdown-menu-item (icon :keyboard) [:span "Keyboard shortcuts"])) + (ui/dropdown-menu-separator) ;; group - (ui/dropdown-menu-group + (ui/dropdown-menu-group ;; items - (ui/dropdown-menu-item (icon :users) "Team") + (ui/dropdown-menu-item (icon :users) "Team") ;; sub menu - (ui/dropdown-menu-sub - (ui/dropdown-menu-sub-trigger - (icon :user-plus) [:span "Invite users"]) - (ui/dropdown-menu-sub-content - (ui/dropdown-menu-item (icon :mail) "Email") - (ui/dropdown-menu-item (icon :message) "Message") - (ui/dropdown-menu-item (icon :dots-circle-horizontal) "More..."))) + (ui/dropdown-menu-sub + (ui/dropdown-menu-sub-trigger + (icon :user-plus) [:span "Invite users"]) + (ui/dropdown-menu-sub-content + (ui/dropdown-menu-item (icon :mail) "Email") + (ui/dropdown-menu-item (icon :message) "Message") + (ui/dropdown-menu-item (icon :dots-circle-horizontal) "More..."))) ;; menu item - (ui/dropdown-menu-item (icon :plus) "New Team" (ui/dropdown-menu-shortcut "⌘+T"))) - (ui/dropdown-menu-separator) - (ui/dropdown-menu-item (icon :brand-github) "GitHub") - (ui/dropdown-menu-item {:disabled true} (icon :cloud) "Cloud API") - (ui/dropdown-menu-separator) - (ui/dropdown-menu-item (icon :logout) "Logout" (ui/dropdown-menu-shortcut "⌘+Q")) - ))) + (ui/dropdown-menu-item (icon :plus) "New Team" (ui/dropdown-menu-shortcut "⌘+T"))) + (ui/dropdown-menu-separator) + (ui/dropdown-menu-item (icon :brand-github) "GitHub") + (ui/dropdown-menu-item {:disabled true} (icon :cloud) "Cloud API") + (ui/dropdown-menu-separator) + (ui/dropdown-menu-item (icon :logout) "Logout" (ui/dropdown-menu-shortcut "⌘+Q"))))) (rum/defc sample-context-menu-content [] (let [icon #(ui/tabler-icon (name %1) {:class "scale-90 pr-1 opacity-80"})] (ui/context-menu ;; trigger - (ui/context-menu-trigger - [:div.border.px-6.py-12.border-dashed.rounded.text-center.select-none - {:key "ctx-menu-click"} - [:span.opacity-50 "Right click here"]]) + (ui/context-menu-trigger + [:div.border.px-6.py-12.border-dashed.rounded.text-center.select-none + {:key "ctx-menu-click"} + [:span.opacity-50 "Right click here"]]) ;; content - (ui/context-menu-content - {:class "w-60 max-h-[80vh] overflow-auto"} - (ui/context-menu-item - (icon "arrow-left") - "Back" - (ui/context-menu-shortcut "⌘[")) - (ui/context-menu-item {:disabled true} - (icon "arrow-right") - "Forward" - (ui/context-menu-shortcut "⌘]")) - (ui/context-menu-item - (icon "refresh") - "Reload" - (ui/context-menu-shortcut "⌘R")) + (ui/context-menu-content + {:class "w-60 max-h-[80vh] overflow-auto"} + (ui/context-menu-item + (icon "arrow-left") + "Back" + (ui/context-menu-shortcut "⌘[")) + (ui/context-menu-item {:disabled true} + (icon "arrow-right") + "Forward" + (ui/context-menu-shortcut "⌘]")) + (ui/context-menu-item + (icon "refresh") + "Reload" + (ui/context-menu-shortcut "⌘R")) ;; Sub menu - (ui/context-menu-sub - (ui/context-menu-sub-trigger {:inset true} "More tools") - (ui/context-menu-sub-content {:class "w-48"} - (ui/context-menu-item "Save page As..." - (ui/context-menu-shortcut "⇧⌘S")) - (ui/context-menu-item "Create Shortcut...") - (ui/context-menu-item "Name Window...") - (ui/context-menu-separator) - (ui/context-menu-item "Developer Tools"))) + (ui/context-menu-sub + (ui/context-menu-sub-trigger {:inset true} "More tools") + (ui/context-menu-sub-content {:class "w-48"} + (ui/context-menu-item "Save page As..." + (ui/context-menu-shortcut "⇧⌘S")) + (ui/context-menu-item "Create Shortcut...") + (ui/context-menu-item "Name Window...") + (ui/context-menu-separator) + (ui/context-menu-item "Developer Tools"))) ;; more - (ui/context-menu-separator) - (ui/context-menu-checkbox-item {:checked true} - "Show Bookmarks Bar" (ui/context-menu-shortcut "⌘⇧B")) - (ui/context-menu-checkbox-item "Show Full URLs") - (ui/context-menu-separator) - (ui/context-menu-radio-group {:value "pedro"} - (ui/context-menu-label {:inset true} "People") - (ui/context-menu-separator) - (ui/context-menu-radio-item {:value "pedro"} "Pedro Duarte") - (ui/context-menu-radio-item {:value "colm"} "Colm Tuite")))))) + (ui/context-menu-separator) + (ui/context-menu-checkbox-item {:checked true} + "Show Bookmarks Bar" (ui/context-menu-shortcut "⌘⇧B")) + (ui/context-menu-checkbox-item "Show Full URLs") + (ui/context-menu-separator) + (ui/context-menu-radio-group {:value "pedro"} + (ui/context-menu-label {:inset true} "People") + (ui/context-menu-separator) + (ui/context-menu-radio-item {:value "pedro"} "Pedro Duarte") + (ui/context-menu-radio-item {:value "colm"} "Colm Tuite")))))) + +(rum/defc sample-tabs + [] + (ui/tabs + {:defaultValue "account" + :className "w-[400px]"} + (ui/tabs-list + (ui/tabs-trigger + {:value "account"} + "Account") + (ui/tabs-trigger + {:value "password"} + "Password")) + (ui/tabs-content + {:value "account"} + "Make changes to your account here.") + (ui/tabs-content + {:value "password"} + "Change your password here."))) (rum/defc sample-form-basic [] [:div.border.p-6.rounded.bg-gray-01 (let [form-ctx (form-core/use-form - {:defaultValues {:username "" - :agreement true - :notification "all" - :bio ""} - :yupSchema (-> (.object yup) - (.shape #js {:username (-> (.string yup) (.required))}) - (.required))}) + {:defaultValues {:username "" + :agreement true + :notification "all" + :bio ""} + :yupSchema (-> (.object yup) + (.shape #js {:username (-> (.string yup) (.required))}) + (.required))}) handle-submit (:handleSubmit form-ctx) on-submit-valid (handle-submit - (fn [^js e] - (js/console.log "[form] submit: " e) - (js/alert (js/JSON.stringify e nil 2))))] + (fn [^js e] + (js/console.log "[form] submit: " e) + (js/alert (js/JSON.stringify e nil 2))))] (ui/form-provider form-ctx - [:form - {:on-submit on-submit-valid} + [:form + {:on-submit on-submit-valid} ;; field item - (ui/form-field {:name "username"} - (fn [field error] - (ui/form-item - (ui/form-label "Username") - (ui/form-control - (ui/input (merge {:placeholder "Username"} field))) - (ui/form-description - (if error - [:b.text-red-800 (:message error)] - "This is your public display name."))))) + (ui/form-field {:name "username"} + (fn [field error] + (ui/form-item + (ui/form-label "Username") + (ui/form-control + (ui/input (merge {:placeholder "Username"} field))) + (ui/form-description + (if error + [:b.text-red-800 (:message error)] + "This is your public display name."))))) - (ui/form-field {:name "bio"} - (fn [field error] - (ui/form-item - {:class "pt-4"} - (ui/form-control - (ui/textarea (merge {:placeholder "Bio text..."} field)))))) + (ui/form-field {:name "bio"} + (fn [field error] + (ui/form-item + {:class "pt-4"} + (ui/form-control + (ui/textarea (merge {:placeholder "Bio text..."} field)))))) ;; radio - (ui/form-field {:name "notification"} + (ui/form-field {:name "notification"} ;; item render - (fn [field] - (ui/form-item - {:class "space-y-3 my-4"} - (ui/form-label "Notify me about...") - (ui/form-control - (ui/radio-group - {:value (:value field) - :on-value-change (:onChange field) - :class "flex flex-col space-y-3"} - (ui/form-item - {:class "flex flex-row space-x-3 items-center space-y-0"} - (ui/form-control - (ui/radio-group-item {:value "all"})) - (ui/form-label "All")) + (fn [field] + (ui/form-item + {:class "space-y-3 my-4"} + (ui/form-label "Notify me about...") + (ui/form-control + (ui/radio-group + {:value (:value field) + :on-value-change (:onChange field) + :class "flex flex-col space-y-3"} + (ui/form-item + {:class "flex flex-row space-x-3 items-center space-y-0"} + (ui/form-control + (ui/radio-group-item {:value "all"})) + (ui/form-label "All")) - (ui/form-item - {:class "flex flex-row space-x-3 items-center space-y-0"} - (ui/form-control - (ui/radio-group-item {:value "direct"})) - (ui/form-label "Direct messages and mentions"))))))) + (ui/form-item + {:class "flex flex-row space-x-3 items-center space-y-0"} + (ui/form-control + (ui/radio-group-item {:value "direct"})) + (ui/form-label "Direct messages and mentions"))))))) - [:hr] + [:hr] ;; checkbox - (ui/form-field {:name "agreement"} - (fn [field] - (ui/form-item - {:class "flex justify-start items-center space-x-3 space-y-0 my-3 pr-3"} - (ui/form-control - (ui/checkbox {:checked (:value field) - :on-checked-change (:onChange field)})) - (ui/form-label {:class "font-normal cursor-pointer"} "Agreement terms")))) + (ui/form-field {:name "agreement"} + (fn [field] + (ui/form-item + {:class "flex justify-start items-center space-x-3 space-y-0 my-3 pr-3"} + (ui/form-control + (ui/checkbox {:checked (:value field) + :on-checked-change (:onChange field)})) + (ui/form-label {:class "font-normal cursor-pointer"} "Agreement terms")))) ;; actions - [:div.relative.px-2 - (ui/button {:type "submit" :class "!absolute right-0 top-[-40px]"} "Submit")]]))]) + [:div.relative.px-2 + (ui/button {:type "submit" :class "!absolute right-0 top-[-40px]"} "Submit")]]))]) (rum/defc sample-date-picker [] (let [[open? set-open!] (rum/use-state false) [date set-date!] (rum/use-state (js/Date.))] (ui/popover - {:open open? - :on-open-change (fn [o] (set-open! o))} + {:open open? + :on-open-change (fn [o] (set-open! o))} ;; trigger - (ui/popover-trigger - {:as-child true - :class "w-2/3"} - (ui/input - {:type :text - :placeholder "pick a date" - :default-value (.toDateString date)})) + (ui/popover-trigger + {:as-child true + :class "w-2/3"} + (ui/input + {:type :text + :placeholder "pick a date" + :default-value (.toDateString date)})) ;; content - (ui/popover-content - {:on-open-auto-focus #(.preventDefault %) - :side-offset 8 - :class "p-0"} - (ui/calendar - {:selected date - :on-day-click - (fn [^js d] - (set-date! d) - (set-open! false))}))))) + (ui/popover-content + {:on-open-auto-focus #(.preventDefault %) + :side-offset 8 + :class "p-0"} + (ui/calendar + {:selected date + :on-day-click + (fn [^js d] + (set-date! d) + (set-open! false))}))))) (rum/defc sample-dialog-basic [] (let [[open? set-open!] (rum/use-state false)] (ui/dialog - {:open open? - :on-open-change #(set-open! %)} - (ui/dialog-trigger - {:as-child true} - (ui/button {:variant :outline} - (ui/tabler-icon "notification") "Open as modal locally")) - (ui/dialog-content - (ui/dialog-header - (ui/dialog-title "Header") - (ui/dialog-description - "Description")) - [:div.max-h-96.overflow-y-auto - {:class "-mx-6"} - [:section.px-6 - (repeat 8 [:p "Your custom content"])]] - (ui/dialog-footer - (ui/button - {:on-click #(set-open! false) - :size :md} "🍄 * Footer")))))) - + {:open open? + :on-open-change #(set-open! %)} + (ui/dialog-trigger + {:as-child true} + (ui/button {:variant :outline} + (ui/tabler-icon "notification") "Open as modal locally")) + (ui/dialog-content + (ui/dialog-header + (ui/dialog-title "Header") + (ui/dialog-description + "Description")) + [:div.max-h-96.overflow-y-auto + {:class "-mx-6"} + [:section.px-6 + (repeat 8 [:p "Your custom content"])]] + (ui/dialog-footer + (ui/button + {:on-click #(set-open! false) + :size :md} "🍄 * Footer")))))) (rum/defc page [] (ui/tooltip-provider - [:div.sm:p-10 - [:hr] - [:input - {:type "checkbox" :on-change #(js/console.log "===>> onChange:" % (.-value (.-target %)))}] - (ui/checkbox {:on-click - (fn [^js e] (js/console.log "==>> click:" - (set! (. (.-target e) -checked) (.-state (.-dataset (.-target e)))) - (.-checked (.-target e)) - )) - :on-checked-change #(js/console.log "==>> on checked change:" %) - } "abc") + [:div.sm:p-10 + [:hr] + [:input + {:type "checkbox" :on-change #(js/console.log "===>> onChange:" % (.-value (.-target %)))}] + (ui/checkbox {:on-click + (fn [^js e] (js/console.log "==>> click:" + (set! (. (.-target e) -checked) (.-state (.-dataset (.-target e)))) + (.-checked (.-target e)))) + :on-checked-change #(js/console.log "==>> on checked change:" %)} "abc") - [:h1.text-3xl.font-bold "Logseq UI"] - [:hr] + [:h1.text-3xl.font-bold "Logseq UI"] + [:hr] ;; Button - (section-item "Button" - [:div.flex.flex-row.flex-wrap.gap-2 - (let [[loading? set-loading!] (rum/use-state false)] - (ui/button - {:size :sm - :on-click (fn [] - (set-loading! true) - (js/setTimeout #(set-loading! false) 5000)) - :disabled loading?} - (when loading? - (ui/tabler-icon "loader2" {:class "animate-spin"})) - "Logseq Classic Button" - (ui/tabler-icon "arrow-right"))) + (section-item "Button" + [:div.flex.flex-row.flex-wrap.gap-2 + (let [[loading? set-loading!] (rum/use-state false)] + (ui/button + {:size :sm + :on-click (fn [] + (set-loading! true) + (js/setTimeout #(set-loading! false) 5000)) + :disabled loading?} + (when loading? + (ui/tabler-icon "loader2" {:class "animate-spin"})) + "Logseq Classic Button" + (ui/tabler-icon "arrow-right"))) - (ui/button {:variant :outline :size :sm} "Outline") - (ui/button {:variant :secondary :size :sm} "Secondary") - (ui/button {:disabled true :size :sm} "Disabled") - (ui/button {:variant :destructive :size :sm} "Destructive") - (ui/button {:class "primary-green" :size :sm} "Custom (.primary-green)") - (ui/button {:variant :ghost :size :sm} "Ghost") - (ui/button {:variant :link :size :sm} "Link") - (ui/button - {:variant :icon - :size :sm} - [:a.flex.items-center.text-blue-rx-10.hover:text-blue-rx-10-alpha - {:href "https://x.com/logseq" :target "_blank"} - (ui/tabler-icon "brand-twitter" {:size 15})] - )]) + (ui/button {:variant :outline :size :sm} "Outline") + (ui/button {:variant :secondary :size :sm} "Secondary") + (ui/button {:disabled true :size :sm} "Disabled") + (ui/button {:variant :destructive :size :sm} "Destructive") + (ui/button {:class "primary-green" :size :sm} "Custom (.primary-green)") + (ui/button {:variant :ghost :size :sm} "Ghost") + (ui/button {:variant :link :size :sm} "Link") + (ui/button + {:variant :icon + :size :sm} + [:a.flex.items-center.text-blue-rx-10.hover:text-blue-rx-10-alpha + {:href "https://x.com/logseq" :target "_blank"} + (ui/tabler-icon "brand-twitter" {:size 15})])]) - ;; Toast - (section-item "Toast" - [:div.flex.flex-row.flex-wrap.gap-2 - (ui/button - {:size :md - :variant :outline - :on-click #(ui/toast! - "Check for updates ..." - (nth [:success :error :default :info :warning] (rand-int 3)) - {:title (if (odd? (js/Date.now)) "History of China" "") - :duration 3000})} - "Open random toast" - (ui/tabler-icon "arrow-right")) +;; Toast + (section-item "Toast" + [:div.flex.flex-row.flex-wrap.gap-2 + (ui/button + {:size :md + :variant :outline + :on-click #(ui/toast! + "Check for updates ..." + (nth [:success :error :default :info :warning] (rand-int 3)) + {:title (if (odd? (js/Date.now)) "History of China" "") + :duration 3000})} + "Open random toast" + (ui/tabler-icon "arrow-right")) - (ui/button - {:variant :secondary - :size :md - :on-click (fn [] - (ui/toast! - (fn [{:keys [id dismiss! update!]}] - [:b.text-red-700 - [:div.flex.items-center.gap-2 - (ui/tabler-icon "info-circle") - (str "#(" id ") ") - (.toLocaleString (js/Date.))] - [:div.flex.flex-row.gap-2 - (ui/button - {:on-click #(dismiss! id) :size :sm} - "x close") + (ui/button + {:variant :secondary + :size :md + :on-click (fn [] + (ui/toast! + (fn [{:keys [id dismiss! update!]}] + [:b.text-red-700 + [:div.flex.items-center.gap-2 + (ui/tabler-icon "info-circle") + (str "#(" id ") ") + (.toLocaleString (js/Date.))] + [:div.flex.flex-row.gap-2 + (ui/button + {:on-click #(dismiss! id) :size :sm} + "x close") - (ui/button - {:on-click #(update! {:title (js/Date.now) - :action [:b (ui/button {:on-click (fn [] (ui/toast-dismiss!))} "clear all")]}) - :size :sm} - "x update")]]) - :default - {:duration 3000 :onDismiss #(js/console.log "===>> dismiss?:" %1)}))} - (ui/tabler-icon "apps") - "Toast callback handle") + (ui/button + {:on-click #(update! {:title (js/Date.now) + :action [:b (ui/button {:on-click (fn [] (ui/toast-dismiss!))} "clear all")]}) + :size :sm} + "x update")]]) + :default + {:duration 3000 :onDismiss #(js/console.log "===>> dismiss?:" %1)}))} + (ui/tabler-icon "apps") + "Toast callback handle") - (ui/button - {:on-click #(ui/toast! "A message from SoundCloud..." - {:class "text-orange-rx-10" - :icon [:b.pl-1 (ui/tabler-icon "brand-soundcloud" {:size 20})] - :duration 3000}) - :class "primary-orange" - :size :md} - "Custom icon")]) + (ui/button + {:on-click #(ui/toast! "A message from SoundCloud..." + {:class "text-orange-rx-10" + :icon [:b.pl-1 (ui/tabler-icon "brand-soundcloud" {:size 20})] + :duration 3000}) + :class "primary-orange" + :size :md} + "Custom icon")]) - [:div.flex.flex-row.space-x-16.items-center + [:div.flex.flex-row.space-x-16.items-center ;; Tips - (section-item "Tips" - [:div.flex.flex-row.flex-wrap.gap-2 - (ui/tooltip-provider - (ui/tooltip - (ui/tooltip-trigger - (ui/button - {:variant :outline - :on-click #(dialog-core/open! [:h1.text-9xl.text-center.scale-110 "🍄"])} - "Tip for hint?")) - (ui/tooltip-content - {:class "w-42 px-8 py-4 text-xl border-green-rx-08 bg-green-rx-07-alpha"} - "🍄")))]) + (section-item "Tips" + [:div.flex.flex-row.flex-wrap.gap-2 + (ui/tooltip-provider + (ui/tooltip + (ui/tooltip-trigger + (ui/button + {:variant :outline + :on-click #(dialog-core/open! [:h1.text-9xl.text-center.scale-110 "🍄"])} + "Tip for hint?")) + (ui/tooltip-content + {:class "w-42 px-8 py-4 text-xl border-green-rx-08 bg-green-rx-07-alpha"} + "🍄")))]) ;; Avatar - (section-item "Avatar" - [:div.flex.flex-row.space-x-6.items-center - (ui/avatar - (ui/avatar-image {:src "https://avatars.githubusercontent.com/u/63385289?s=200&v=4"}) - (ui/avatar-fallback "L")) - (ui/avatar - (ui/avatar-fallback "CH"))])] + (section-item "Avatar" + [:div.flex.flex-row.space-x-6.items-center + (ui/avatar + (ui/avatar-image {:src "https://avatars.githubusercontent.com/u/63385289?s=200&v=4"}) + (ui/avatar-fallback "L")) + (ui/avatar + (ui/avatar-fallback "CH"))])] ;; Badge - (section-item "Badge" - [:div.flex.flex-row.flex-wrap.gap-2 - (ui/badge "Default") - (ui/badge {:variant :outline} "Outline") - (ui/badge {:variant :secondary} "Secondary") - (ui/badge {:variant :destructive} "Destructive") - (ui/badge {:class "primary-yellow"} "Custom (.primary-yellow)")]) + (section-item "Badge" + [:div.flex.flex-row.flex-wrap.gap-2 + (ui/badge "Default") + (ui/badge {:variant :outline} "Outline") + (ui/badge {:variant :secondary} "Secondary") + (ui/badge {:variant :destructive} "Destructive") + (ui/badge {:class "primary-yellow"} "Custom (.primary-yellow)")]) - [:div.grid.sm:grid-cols-3.sm:gap-8 + [:div.grid.sm:grid-cols-3.sm:gap-8 ;; Dropdown - (section-item "Dropdown" - (ui/dropdown-menu - (ui/tooltip - (ui/tooltip-trigger - (ui/dropdown-menu-trigger - {:as-child true} - (ui/button {:variant :outline} - (ui/tabler-icon "list") "Open dropdown menu"))) - (ui/tooltip-content "test hide?")) + (section-item "Dropdown" + (ui/dropdown-menu + (ui/tooltip + (ui/tooltip-trigger + (ui/dropdown-menu-trigger + {:as-child true} + (ui/button {:variant :outline} + (ui/tabler-icon "list") "Open dropdown menu"))) + (ui/tooltip-content "test hide?")) - (sample-dropdown-menu-content))) + (sample-dropdown-menu-content))) ;; Context menu - [:div.col-span-2 - (section-item "Context Menu" - (sample-context-menu-content))]] + [:div.col-span-2 + (section-item "Context Menu" + (sample-context-menu-content))]] + + (section-item "Tabs" (sample-tabs)) ;; Dialog - (section-item "Dialog" - [:div.flex.flex-row.flex-wrap.gap-2 - (sample-dialog-basic) - (ui/button - {:on-click #(dialog-core/open! "a modal dialog from `open!`" {:title "Title"})} - "Imperative API: open!") + (section-item "Dialog" + [:div.flex.flex-row.flex-wrap.gap-2 + (sample-dialog-basic) + (ui/button + {:on-click #(dialog-core/open! "a modal dialog from `open!`" {:title "Title"})} + "Imperative API: open!") - (ui/button - {:class "primary-yellow" - :on-click (fn [] - (-> (dialog-core/alert! - "a alert dialog from `alert!`" - {:title [:div.flex.flex-row.space-x-2.items-center - (ui/tabler-icon "alert-triangle" {:size 18}) - [:span "Alert"]]}) - (p/then #(js/console.log "=> alert (promise): " %))))} - "Imperative API: alert!") + (ui/button + {:class "primary-yellow" + :on-click (fn [] + (-> (dialog-core/alert! + "a alert dialog from `alert!`" + {:title [:div.flex.flex-row.space-x-2.items-center + (ui/tabler-icon "alert-triangle" {:size 18}) + [:span "Alert"]]}) + (p/then #(js/console.log "=> alert (promise): " %))))} + "Imperative API: alert!") - (ui/button - {:class "primary-green" - :on-click (fn [] - (-> (dialog-core/confirm! - "a alert dialog from `confirm!`" - {:title [:div.flex.flex-row.space-x-2.items-center - (ui/tabler-icon "alert-triangle" {:size 18}) - [:span "Confirm"]]}) - (p/then #(js/console.log "=> confirm (promise): " %)) - (p/catch #(js/console.log "=> confirm (promise): " %))))} - "Imperative API: confirm!")]) + (ui/button + {:class "primary-green" + :on-click (fn [] + (-> (dialog-core/confirm! + "a alert dialog from `confirm!`" + {:title [:div.flex.flex-row.space-x-2.items-center + (ui/tabler-icon "alert-triangle" {:size 18}) + [:span "Confirm"]]}) + (p/then #(js/console.log "=> confirm (promise): " %)) + (p/catch #(js/console.log "=> confirm (promise): " %))))} + "Imperative API: confirm!")]) ;; Alert - (section-item "Alert" - [:<> - (ui/alert - {:class "text-orange-rx-09 border-orange-rx-07-alpha mb-4"} - (ui/tabler-icon "brand-soundcloud") - (ui/alert-title "Title is SoundCloud") - (ui/alert-description - "content: radix colors for Logseq")) - (ui/alert - (ui/tabler-icon "brand-github") - (ui/alert-title "GitHub") - (ui/alert-description - "content: radix colors for Logseq"))]) + (section-item "Alert" + [:<> + (ui/alert + {:class "text-orange-rx-09 border-orange-rx-07-alpha mb-4"} + (ui/tabler-icon "brand-soundcloud") + (ui/alert-title "Title is SoundCloud") + (ui/alert-description + "content: radix colors for Logseq")) + (ui/alert + (ui/tabler-icon "brand-github") + (ui/alert-title "GitHub") + (ui/alert-description + "content: radix colors for Logseq"))]) ;; Slider - [:div.grid.sm:grid-cols-8.gap-4 - [:div.col-span-4.mr-6 - (section-item "Slider" (ui/slider))] - [:div.col-span-1 - (section-item "Switch" - (ui/switch {:size :sm :class "relative top-[-8px]"}))] - [:div.col-span-3.pl-4.pr-2 - (section-item "Select" - (ui/select - {:on-value-change (fn [v] (ui/toast! v :info))} + [:div.grid.sm:grid-cols-8.gap-4 + [:div.col-span-4.mr-6 + (section-item "Slider" (ui/slider))] + [:div.col-span-1 + (section-item "Switch" + (ui/switch {:size :sm :class "relative top-[-8px]"}))] + [:div.col-span-3.pl-4.pr-2 + (section-item "Select" + (ui/select + {:on-value-change (fn [v] (ui/toast! v :info))} ;; trigger - (ui/select-trigger - (ui/select-value {:placeholder "Select a fruit"})) + (ui/select-trigger + (ui/select-value {:placeholder "Select a fruit"})) ;; content - (ui/select-content - (ui/select-group - (ui/select-label "Fruits") - (ui/select-item {:value "apple"} "Apple") - (ui/select-item {:value "pear"} "Pear") - (ui/select-item {:value "grapes"} "Grapes") + (ui/select-content + (ui/select-group + (ui/select-label "Fruits") + (ui/select-item {:value "apple"} "Apple") + (ui/select-item {:value "pear"} "Pear") + (ui/select-item {:value "grapes"} "Grapes")))))]] - ))))]] - - ;; Form - (section-item "Form" - [:<> - (sample-form-basic)]) +;; Form + (section-item "Form" + [:<> + (sample-form-basic)]) ;; Card - [:div.grid.sm:grid-cols-2.sm:gap-8 - (section-item "Card" - (ui/card - (ui/card-header - (ui/card-title "Title") - (ui/card-description "Description")) - (ui/card-content "This is content") - (ui/card-footer "Footer"))) + [:div.grid.sm:grid-cols-2.sm:gap-8 + (section-item "Card" + (ui/card + (ui/card-header + (ui/card-title "Title") + (ui/card-description "Description")) + (ui/card-content "This is content") + (ui/card-footer "Footer"))) - (section-item "Skeleton" - (ui/card - (ui/card-header - (ui/card-title - (ui/skeleton {:class "h-4 w-1/2"})) - (ui/card-description - (ui/skeleton {:class "h-2 w-full"}))) - (ui/card-content - (ui/skeleton {:class "h-3 mb-1"}) - (ui/skeleton {:class "h-3 mb-1"}) - (ui/skeleton {:class "h-3 w-2/3"})) + (section-item "Skeleton" + (ui/card + (ui/card-header + (ui/card-title + (ui/skeleton {:class "h-4 w-1/2"})) + (ui/card-description + (ui/skeleton {:class "h-2 w-full"}))) + (ui/card-content + (ui/skeleton {:class "h-3 mb-1"}) + (ui/skeleton {:class "h-3 mb-1"}) + (ui/skeleton {:class "h-3 w-2/3"})) - (ui/card-footer - (ui/skeleton {:class "h-4 w-full mb-2"}))))] + (ui/card-footer + (ui/skeleton {:class "h-4 w-full mb-2"}))))] ;; Calendar - [:div.grid.sm:grid-cols-2.sm:gap-8 - (section-item "Calendar" - (ui/card - {:class "inline-flex"} - (ui/calendar {:on-day-click #(ui/toast! (.toString %) :success)}))) - (section-item "Date Picker" - (sample-date-picker))] - - [:hr.mb-80]])) + [:div.grid.sm:grid-cols-2.sm:gap-8 + (section-item "Calendar" + (ui/card + {:class "inline-flex"} + (ui/calendar {:on-day-click #(ui/toast! (.toString %) :success)}))) + (section-item "Date Picker" + (sample-date-picker))] + [:hr.mb-80]])) (defn- get-head-container [] @@ -499,44 +512,44 @@ (let [el-ref (rum/use-ref nil)] (rum/use-effect! - (fn [] - (let [^js container (get-main-scroll-container) - ^js el (rum/deref el-ref) - ^js cls (.-classList el) - *ticking? (volatile! false) - el-top (-> el (.getBoundingClientRect) (.-top)) - head-top (-> (get-head-container) (js/getComputedStyle) (.-height) (js/parseInt)) - translate (fn [offset] - (set! (. (.-style el) -transform) (str "translate3d(0, " offset "px , 0)")) - (if (zero? offset) - (.remove cls "translated") - (.add cls "translated"))) - *last-offset (volatile! 0) - handle (fn [] - (let [scroll-top (js/parseInt (.-scrollTop container)) - offset (if (> (+ scroll-top head-top) el-top) - (+ (- scroll-top el-top) head-top 1) 0) - offset (js/parseInt offset) - last-offset @*last-offset] - (if (and (not (zero? last-offset)) - (not= offset last-offset)) - (let [dir (if (neg? (- offset last-offset)) -1 1)] - (loop [offset' (+ last-offset dir)] - (translate offset') - (if (and (not= offset offset') - (< (abs (- offset offset')) 100)) - (recur (+ offset' dir)) - (translate offset)))) - (translate offset)) - (vreset! *last-offset offset))) - handler (fn [^js e] - (when (not @*ticking?) - (js/window.requestAnimationFrame - #(do (handle) (vreset! *ticking? false))) - (vreset! *ticking? true)))] - (.addEventListener container "scroll" handler) - #(.removeEventListener container "scroll" handler))) - []) + (fn [] + (let [^js container (get-main-scroll-container) + ^js el (rum/deref el-ref) + ^js cls (.-classList el) + *ticking? (volatile! false) + el-top (-> el (.getBoundingClientRect) (.-top)) + head-top (-> (get-head-container) (js/getComputedStyle) (.-height) (js/parseInt)) + translate (fn [offset] + (set! (. (.-style el) -transform) (str "translate3d(0, " offset "px , 0)")) + (if (zero? offset) + (.remove cls "translated") + (.add cls "translated"))) + *last-offset (volatile! 0) + handle (fn [] + (let [scroll-top (js/parseInt (.-scrollTop container)) + offset (if (> (+ scroll-top head-top) el-top) + (+ (- scroll-top el-top) head-top 1) 0) + offset (js/parseInt offset) + last-offset @*last-offset] + (if (and (not (zero? last-offset)) + (not= offset last-offset)) + (let [dir (if (neg? (- offset last-offset)) -1 1)] + (loop [offset' (+ last-offset dir)] + (translate offset') + (if (and (not= offset offset') + (< (abs (- offset offset')) 100)) + (recur (+ offset' dir)) + (translate offset)))) + (translate offset)) + (vreset! *last-offset offset))) + handler (fn [^js e] + (when (not @*ticking?) + (js/window.requestAnimationFrame + #(do (handle) (vreset! *ticking? false))) + (vreset! *ticking? true)))] + (.addEventListener container "scroll" handler) + #(.removeEventListener container "scroll" handler))) + []) [:div.charlie-table [:div.charlie-table-header diff --git a/deps/shui/src/logseq/shui/ui.cljs b/deps/shui/src/logseq/shui/ui.cljs index 70eb46e29a..026531f123 100644 --- a/deps/shui/src/logseq/shui/ui.cljs +++ b/deps/shui/src/logseq/shui/ui.cljs @@ -110,6 +110,12 @@ (def context-menu-sub-trigger (util/lsui-wrap "ContextMenuSubTrigger")) (def context-menu-radio-group (util/lsui-wrap "ContextMenuRadioGroup")) +;; tabs +(def tabs (util/lsui-wrap "Tabs")) +(def tabs-list (util/lsui-wrap "TabsList")) +(def tabs-trigger (util/lsui-wrap "TabsTrigger")) +(def tabs-content (util/lsui-wrap "TabsContent")) + (def dialog dialog-core/dialog) (def dialog-portal dialog-core/dialog-portal) (def dialog-overlay dialog-core/dialog-overlay) diff --git a/packages/ui/@/components/ui/tabs.tsx b/packages/ui/@/components/ui/tabs.tsx new file mode 100644 index 0000000000..f57fffdb5a --- /dev/null +++ b/packages/ui/@/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/packages/ui/package.json b/packages/ui/package.json index f96a14e7bf..61a53e5406 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -30,6 +30,7 @@ "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-toggle-group": "^1.0.4", diff --git a/packages/ui/src/ui.ts b/packages/ui/src/ui.ts index bd8b40b752..ba5ed81290 100644 --- a/packages/ui/src/ui.ts +++ b/packages/ui/src/ui.ts @@ -91,6 +91,7 @@ import { Toggle } from '@/components/ui/toggle' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import * as uniqolor from 'uniqolor' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' declare global { var LSUI: any @@ -185,6 +186,7 @@ const shadui = { TooltipContent, TooltipProvider, TooltipPortal, Toggle, ToggleGroup, ToggleGroupItem, Avatar, AvatarImage, AvatarFallback, + Tabs, TabsContent, TabsList, TabsTrigger } function setupGlobals() { @@ -203,4 +205,4 @@ setupGlobals() export { setupGlobals -} \ No newline at end of file +} diff --git a/packages/ui/yarn.lock b/packages/ui/yarn.lock index 192f0aecd5..d5faa392e1 100644 --- a/packages/ui/yarn.lock +++ b/packages/ui/yarn.lock @@ -2267,6 +2267,16 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-slot" "1.0.2" +"@radix-ui/react-collection@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed" + integrity sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-compose-refs@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae" @@ -2392,6 +2402,11 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-direction@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc" + integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg== + "@radix-ui/react-dismissable-layer@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz#35b7826fa262fd84370faef310e627161dffa76b" @@ -2753,6 +2768,21 @@ "@radix-ui/react-use-callback-ref" "1.0.1" "@radix-ui/react-use-controllable-state" "1.0.1" +"@radix-ui/react-roving-focus@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e" + integrity sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-collection" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-select@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-1.2.2.tgz#caa981fa0d672cf3c1b2a5240135524e69b32181" @@ -2879,6 +2909,20 @@ "@radix-ui/react-use-previous" "1.0.1" "@radix-ui/react-use-size" "1.0.1" +"@radix-ui/react-tabs@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz#698bd97923f6bcd629738198a73beebcc4c88b30" + integrity sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-presence" "1.1.1" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-roving-focus" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-toast@^1.1.5": version "1.1.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.1.5.tgz#f5788761c0142a5ae9eb97f0051fd3c48106d9e6" @@ -8956,8 +9000,7 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4, string-width@^4.1.0, string-width@^4.2.0, string-width@^5.0.1, string-width@^5.1.2: - name string-width-cjs +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8975,6 +9018,15 @@ string-width@4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4, string-width@^4.1.0, string-width@^4.2.0, string-width@^5.0.1, string-width@^5.1.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -8989,8 +9041,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: - name strip-ansi-cjs +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9650,8 +9708,16 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== diff --git a/resources/package.json b/resources/package.json index 5090a6aa2b..0d761d523b 100644 --- a/resources/package.json +++ b/resources/package.json @@ -43,8 +43,7 @@ "posthog-js": "1.10.2", "semver": "7.5.2", "socks-proxy-agent": "8.0.2", - "update-electron-app": "2.0.1", - "electron-devtools-installer": "3.2.0" + "update-electron-app": "2.0.1" }, "devDependencies": { "@electron-forge/cli": "^7.3.1", @@ -57,7 +56,8 @@ "@electron/rebuild": "3.2.10", "electron": "31.7.5", "electron-builder": "25.1.8", - "electron-forge-maker-appimage": "https://github.com/logseq/electron-forge-maker-appimage.git" + "electron-forge-maker-appimage": "https://github.com/logseq/electron-forge-maker-appimage.git", + "electron-devtools-installer": "^3.2.0" }, "resolutions": { "**/electron": "31.7.5", diff --git a/scripts/src/logseq/tasks/db_graph/create_graph_with_schema_org.cljs b/scripts/src/logseq/tasks/db_graph/create_graph_with_schema_org.cljs index 1d5fabc614..98ba9e4a7c 100644 --- a/scripts/src/logseq/tasks/db_graph/create_graph_with_schema_org.cljs +++ b/scripts/src/logseq/tasks/db_graph/create_graph_with_schema_org.cljs @@ -366,6 +366,7 @@ {:logseq.property.class/properties [:block/title]} {:property/schema.classes [:block/title]} {:logseq.property/parent [:block/title]} + {:block/tags [:block/title]} {:block/refs [:block/title]}]) ...] :in $ :where [?b :db/ident ?ident]] @@ -376,11 +377,12 @@ (map (fn [m] (let [props (->> (db-property/properties m) (into {}))] - (cond-> (select-keys m [:block/name :block/type :block/title :block/schema :db/ident + (cond-> (select-keys m [:block/name :block/tags :block/title :block/schema :db/ident :logseq.property.class/properties :logseq.property/parent :db/cardinality :property/schema.classes :block/refs]) (seq props) (assoc :block/properties (-> (update-keys props name) + (dissoc "tags") (update-vals (fn [v] (if (:db/id v) (db-property/property-value-content (d/entity db (:db/id v))) @@ -391,6 +393,8 @@ (update :logseq.property/parent :block/title) (seq (:property/schema.classes m)) (update :property/schema.classes #(set (map :block/title %))) + (seq (:block/tags m)) + (update :block/tags #(set (map :block/title %))) (seq (:block/refs m)) (update :block/refs #(set (map :block/title %))))))) set))))) diff --git a/scripts/src/logseq/tasks/dev/db_and_file_graphs.clj b/scripts/src/logseq/tasks/dev/db_and_file_graphs.clj index 0c3509c4eb..b16a18dcc1 100644 --- a/scripts/src/logseq/tasks/dev/db_and_file_graphs.clj +++ b/scripts/src/logseq/tasks/dev/db_and_file_graphs.clj @@ -91,7 +91,8 @@ (let [file-concepts (->> ;; from logseq.db.frontend.schema [:block/namespace :block/properties-text-values :block/pre-block :recent/pages :block/file :block/properties-order - :block/repeated :block/deadline :block/scheduled :block/priority :block/marker :block/macros] + :block/repeated :block/deadline :block/scheduled :block/priority :block/marker :block/macros + :block/type] (map str) (into [;; e.g. block/properties :title "block/properties :" diff --git a/src/main/frontend/components/all_pages.cljs b/src/main/frontend/components/all_pages.cljs index 6bdd459a90..82c7c064f7 100644 --- a/src/main/frontend/components/all_pages.cljs +++ b/src/main/frontend/components/all_pages.cljs @@ -21,13 +21,14 @@ :cell (fn [_table row _column] (component-block/page-cp {} row)) :type :string} - {:id :block/type - :name "Type" - :cell (fn [_table row _column] - (let [type (get row :block/type)] - [:div.capitalize (if (= type "class") "tag" type)])) - :get-value (fn [row] (get row :block/type)) - :type :string} + (when (not (config/db-based-graph? (state/get-current-repo))) + {:id :block/type + :name "Page type" + :cell (fn [_table row _column] + (let [type (get row :block/type)] + [:div.capitalize type])) + :get-value (fn [row] (get row :block/type)) + :type :string}) {:id :block.temp/refs-count :name (t :page/backlinks) :cell (fn [_table row _column] (:block.temp/refs-count row)) diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index ea9c2e7356..316f7dec22 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -20,7 +20,6 @@ [frontend.components.query :as query] [frontend.components.query.builder :as query-builder-component] [frontend.components.svg :as svg] - [frontend.components.title :as title] [frontend.components.select :as select] [frontend.config :as config] [frontend.context.i18n :refer [t]] @@ -701,7 +700,9 @@ (when (and show-icon? (not tag?)) (let [own-icon (get page-entity (pu/get-pid :logseq.property/icon)) emoji? (and (map? own-icon) (= (:type own-icon) :emoji))] - (when-let [icon (icon-component/get-node-icon-cp page-entity {:color? true :not-text-or-page? true})] + (when-let [icon (icon-component/get-node-icon-cp page-entity {:color? true + :not-text-or-page? true + :own-icon? true})] [:span {:class (str "icon-emoji-wrap " (when emoji? "as-emoji"))} icon]))) [:span @@ -722,7 +723,7 @@ (->elem :span (map-inline config label)) show-unique-title? - (title/block-unique-title page-entity) + (block-handler/block-unique-title page-entity) :else (let [title (:block/title page-entity) @@ -755,7 +756,7 @@ (db-content/content-id-ref->page s (:block/refs page-entity)) :else s) - s (if tag? (str "#" s) s)] + s (if (and tag? (not (:hide-tag-symbol? config))) (str "#" s) s)] (if (ldb/page? page-entity) s (block-title config page-entity))))] @@ -2588,8 +2589,9 @@ (rum/defcs block-tag < (rum/local false ::hover?) [state block tag config popup-opts] - (let [*hover? (::hover? state)] - [:div.block-tag + (let [*hover? (::hover? state) + hover? @*hover?] + [:div.block-tag.items-center {:key (str "tag-" (:db/id tag)) :on-mouse-over #(reset! *hover? true) :on-mouse-out #(reset! *hover? false) @@ -2614,18 +2616,23 @@ :on-click #(db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag))} "Remove tag")]) popup-opts))} + (if (and hover? (not (ldb/private-tags (:db/ident tag)))) + [:a.inline.close.flex.transition-opacity.duration-300.ease-in + {:class (if @*hover? "!opacity-100" "!opacity-0") + :title "Remove this tag" + :on-pointer-down + (fn [e] + (util/stop e) + (db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag)))} + (ui/icon "x" {:size 14 + :style {:margin-top 1}})] + [:a.hash-symbol {:style {:margin-left 5}} + "#"]) (page-cp (assoc config + :disable-preview? true :tag? true - :disable-preview? true) - tag) - [:a.close.flex.transition-opacity.duration-300.ease-in - {:class (if @*hover? "!opacity-100" "!opacity-0") - :title "Remove this tag" - :on-pointer-down - (fn [e] - (util/stop e) - (db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag)))} - (ui/icon "x" {:size 15})]])) + :hide-tag-symbol? true) + tag)])) (rum/defc tags-cp "Tags without inline or hidden tags" @@ -2635,14 +2642,15 @@ (:block/tags block) (remove (fn [t] (or (ldb/inline-tag? (:block/raw-title block) t) - (:logseq.property.class/hide-from-node t))))) + (:logseq.property.class/hide-from-node t) + (contains? ldb/internal-tags (:db/ident t)))))) popup-opts {:align :end :content-props {:on-click (fn [] (shui/popup-hide!)) :class "w-60"}} tags-count (count block-tags)] (when (seq block-tags) (if (< tags-count 3) - [:div.block-tags + [:div.block-tags.gap-1 (for [tag block-tags] (rum/with-key (block-tag block tag config popup-opts) @@ -2653,13 +2661,14 @@ (fn [] (for [tag block-tags] [:div.flex.flex-row.items-center.gap-1 - (shui/button - {:title "Remove tag" - :variant :ghost - :class "!p-1 text-muted-foreground" - :size :sm - :on-click #(db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag))} - (ui/icon "X" {:size 14})) + (when-not (ldb/private-tags (:db/ident tag)) + (shui/button + {:title "Remove tag" + :variant :ghost + :class "!p-1 text-muted-foreground" + :size :sm + :on-click #(db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag))} + (ui/icon "X" {:size 14}))) (page-cp (assoc config :tag? true :disable-preview? true @@ -2672,7 +2681,7 @@ :tag? true :disable-preview? true :disable-click? true) tag)]) - [:div.text-sm.opacity-50 + [:div.text-sm.opacity-50.ml-1 (str "+" (- tags-count 2))]]))))) (rum/defc block-positioned-properties diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css index e3647a6ddd..d2ca9c024c 100644 --- a/src/main/frontend/components/block.css +++ b/src/main/frontend/components/block.css @@ -949,7 +949,7 @@ html.is-mac { .positioned-properties.block-right { button { - @apply whitespace-nowrap mr-0.5; + @apply whitespace-nowrap; } .block-title-wrap { @@ -970,14 +970,18 @@ html.is-mac { } .block-tag { - @apply pr-1 flex flex-row items-center gap-1; + @apply flex flex-row items-center; } .block-tag a.tag { @apply flex text-sm font-normal items-center opacity-70; } -.block-tag a.tag:hover { +.block-tag a.hash-symbol { + @apply text-sm text-sm font-normal opacity-70; +} + +.block-tag a.tag:hover, .block-tag a.hash-symbol:hover { @apply opacity-100; } @@ -1013,7 +1017,7 @@ html.is-mac { } .ls-page-title .block-tags { - @apply relative -right-1 min-h-full; + @apply relative min-h-full; } .ls-code-editor-wrap { diff --git a/src/main/frontend/components/cmdk/core.cljs b/src/main/frontend/components/cmdk/core.cljs index fa0d756ec2..050afb853d 100644 --- a/src/main/frontend/components/cmdk/core.cljs +++ b/src/main/frontend/components/cmdk/core.cljs @@ -4,7 +4,6 @@ [electron.ipc :as ipc] [frontend.components.block :as block] [frontend.components.cmdk.list-item :as list-item] - [frontend.components.title :as title] [frontend.config :as config] [frontend.context.i18n :refer [t]] [frontend.db :as db] @@ -18,6 +17,7 @@ [frontend.handler.page :as page-handler] [frontend.handler.route :as route-handler] [frontend.handler.whiteboard :as whiteboard-handler] + [frontend.handler.block :as block-handler] [frontend.mixins :as mixins] [frontend.modules.shortcut.core :as shortcut] [frontend.modules.shortcut.utils :as shortcut-utils] @@ -235,7 +235,7 @@ "whiteboard" :else "page") - title (title/block-unique-title page) + title (block-handler/block-unique-title page) title' (if source-page (str title " -> alias: " (:block/title source-page)) title)] (hash-map :icon icon :icon-theme :gray @@ -245,7 +245,7 @@ (defn- block-item [repo block current-page !input] (let [id (:block/uuid block) - text (title/block-unique-title block) + text (block-handler/block-unique-title block) icon "letter-n"] {:icon icon :icon-theme :gray diff --git a/src/main/frontend/components/container.cljs b/src/main/frontend/components/container.cljs index 36c3ef7385..2a783855fd 100644 --- a/src/main/frontend/components/container.cljs +++ b/src/main/frontend/components/container.cljs @@ -15,7 +15,6 @@ [frontend.components.block :as block] [dommy.core :as d] [frontend.components.content :as cp-content] - [frontend.components.title :as title] [frontend.config :as config] [frontend.context.i18n :refer [t tt]] [frontend.db :as db] @@ -32,6 +31,7 @@ [frontend.handler.user :as user-handler] [frontend.handler.whiteboard :as whiteboard-handler] [frontend.handler.recent :as recent-handler] + [frontend.handler.block :as block-handler] [frontend.mixins :as mixins] [frontend.mobile.action-bar :as action-bar] [frontend.mobile.footer :as footer] @@ -139,7 +139,7 @@ :class "w-60"}}) (util/stop e))} (ldb/object? page) - (assoc :title (title/block-unique-title page))) + (assoc :title (block-handler/block-unique-title page))) [:span.page-icon icon] [:span.page-title {:class (when untitled? "opacity-50") :style {:display "ruby"}} @@ -214,7 +214,7 @@ db-based? (concat [:tag/tasks :tag/assets]) (not db-based?) - (#(cons :whiteboards %)) ) + (#(cons :whiteboards %))) [checked-navs set-checked-navs!] (rum/use-state (or (storage/get :ls-sidebar-navigations) [:whiteboards :flashcards :graph-view :all-pages]))] @@ -362,7 +362,7 @@ (for [page pages] [:li.recent-item.select-none.font-medium {:key (str "recent-" (:db/id page)) - :title (title/block-unique-title page) + :title (block-handler/block-unique-title page) :draggable true :on-drag-start (fn [event] (editor-handler/block->data-transfer! (:block/name page) event true)) :data-ref name} diff --git a/src/main/frontend/components/container.css b/src/main/frontend/components/container.css index 606bbfa882..db6d42028c 100644 --- a/src/main/frontend/components/container.css +++ b/src/main/frontend/components/container.css @@ -446,7 +446,7 @@ flex: 1; .page { - @apply px-6; + @apply px-4; } } diff --git a/src/main/frontend/components/db_based/page.cljs b/src/main/frontend/components/db_based/page.cljs index a049a08b0b..4f3c619e36 100644 --- a/src/main/frontend/components/db_based/page.cljs +++ b/src/main/frontend/components/db_based/page.cljs @@ -4,21 +4,22 @@ [frontend.db :as db] [frontend.db-mixins :as db-mixins] [logseq.shui.ui :as shui] - [rum.core :as rum])) + [rum.core :as rum] + [frontend.util :as util])) (rum/defc configure-property < rum/reactive db-mixins/query [page] (let [page (db/sub-block (:db/id page))] - [:div.pb-4.-ml-1 - (shui/button - {:variant "ghost" - :class "opacity-50 hover:opacity-90" - :size :sm - :on-click (fn [^js e] - (shui/popup-show! (.-target e) - (fn [] - (property-config/dropdown-editor page nil {:debug? (.-altKey e)})) - {:content-props {:class "ls-property-dropdown-editor as-root"} - :align "start" - :as-dropdown? true}))} - "Configure property")])) + (shui/tabs-trigger + {:value "configure" + :class "py-1 text-xs" + :on-pointer-down (fn [e] + (util/stop e)) + :on-click (fn [^js e] + (shui/popup-show! (.-target e) + (fn [] + (property-config/dropdown-editor page nil {:debug? (.-altKey e)})) + {:content-props {:class "ls-property-dropdown-editor as-root"} + :align "start" + :as-dropdown? true}))} + "Configure property"))) diff --git a/src/main/frontend/components/editor.cljs b/src/main/frontend/components/editor.cljs index 7292c7f1c8..73947e5143 100644 --- a/src/main/frontend/components/editor.cljs +++ b/src/main/frontend/components/editor.cljs @@ -6,7 +6,6 @@ [frontend.components.file-based.datetime :as datetime-comp] [frontend.components.search :as search] [frontend.components.svg :as svg] - [frontend.components.title :as title] [frontend.config :as config] [frontend.context.i18n :refer [t]] [frontend.date :as date] @@ -19,6 +18,7 @@ [frontend.handler.paste :as paste-handler] [frontend.handler.property.util :as pu] [frontend.handler.search :as search-handler] + [frontend.handler.block :as block-handler] [frontend.mixins :as mixins] [frontend.search :refer [fuzzy-search]] [frontend.state :as state] @@ -35,7 +35,8 @@ [logseq.shui.ui :as shui] [promesa.core :as p] [react-draggable] - [rum.core :as rum])) + [rum.core :as rum] + [logseq.db.frontend.class :as db-class])) (defn filter-commands [page? commands] @@ -156,7 +157,12 @@ ;; reorder, shortest and starts-with first. (let [matched-pages-with-new-page (fn [partial-matched-pages] - (if (or (db/page-exists? q (if db-tag? "class" "page")) + (if (or (db/page-exists? q (if db-tag? + #{:logseq.class/Tag} + ;; Page existence here should be the same as entity-util/page?. + ;; Don't show 'New page' if a page has any of these tags + (into #{:logseq.class/Page :logseq.class/Tag :logseq.class/Property} + db-class/page-children-classes))) (and db-tag? (some ldb/class? (:block/_alias (db/get-page q))))) partial-matched-pages (if db-tag? @@ -215,7 +221,7 @@ (if (ldb/class? target) (str (:block/title block) " -> alias: " (:block/title target)) (:block/title block))) - (title/block-unique-title block))] + (block-handler/block-unique-title block))] (search-handler/highlight-exact-query title q))]])) :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (if db-tag? "Search for a tag" diff --git a/src/main/frontend/components/file_sync.cljs b/src/main/frontend/components/file_sync.cljs index 66810ca5cc..171cafc1a7 100644 --- a/src/main/frontend/components/file_sync.cljs +++ b/src/main/frontend/components/file_sync.cljs @@ -693,7 +693,7 @@ [:div.cp__file-sync-page-histories-right [:h1.title.text-xl "Current version"] - (page/page-blocks-cp (state/get-current-repo) page-entity nil)] + (page/page-blocks-cp page-entity nil)] ;; ready loading [:div.flex.items-center.h-full.justify-center.w-full.absolute.ready-loading diff --git a/src/main/frontend/components/icon.cljs b/src/main/frontend/components/icon.cljs index 699d5e61ad..2284d2ae08 100644 --- a/src/main/frontend/components/icon.cljs +++ b/src/main/frontend/components/icon.cljs @@ -61,7 +61,9 @@ (defn get-node-icon-cp [node-entity opts] (let [opts' (merge {:size 14} opts) - node-icon (get-node-icon node-entity)] + node-icon (if (:own-icon? opts) + (get node-entity (pu/get-pid :logseq.property/icon)) + (get-node-icon node-entity))] (when-not (or (string/blank? node-icon) (and (contains? #{"letter-n" "page"} node-icon) (:not-text-or-page? opts))) [:div.icon-cp-container.flex.items-center (merge {:style {:color (or (:color node-icon) "inherit")}} diff --git a/src/main/frontend/components/objects.cljs b/src/main/frontend/components/objects.cljs index c9f037dcef..626d617c71 100644 --- a/src/main/frontend/components/objects.cljs +++ b/src/main/frontend/components/objects.cljs @@ -154,53 +154,45 @@ (if loading? (ui/skeleton) - [:div.flex.flex-col.gap-2.mt-2 - - (ui/foldable - [:div.font-medium.opacity-60.as-toggle - "Tagged Nodes"] - (fn [] - [:div.mt-2 - (views/view view-entity {:config config - :data data - :set-data! set-data! - :views-title (class-views class views view-entity {:set-view-entity! set-view-entity! - :set-views! set-views!}) - :columns columns - :add-new-object! (if (= :logseq.class/Asset (:db/ident class)) - (fn [_e] - (shui/dialog-open! - (fn [] - [:div.flex.flex-col.gap-2 - [:div.font-medium "Add assets"] - (filepicker/picker - {:on-change (fn [_e files] - (p/do! - (editor-handler/upload-asset! nil files :markdown editor-handler/*asset-uploading? true) - (set-data! (get-class-objects class)) - (shui/dialog-close!)))})]))) - #(add-new-class-object! class set-data!)) - :show-add-property? true - :add-property! (fn [] - (state/pub-event! [:editor/new-property {:block class - :class-schema? true}])) - :on-delete-rows (fn [table selected-rows] - (let [pages (filter ldb/page? selected-rows) - blocks (remove ldb/page? selected-rows)] - (p/do! - (ui-outliner-tx/transact! - {:outliner-op :delete-blocks} - (when (seq blocks) - (outliner-op/delete-blocks! blocks nil)) - (let [page-ids (map :db/id pages) - tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id class)]) page-ids)] - (when (seq tx-data) - (outliner-op/transact! tx-data {:outliner-op :save-block})))) - (set-data! (get-class-objects class)) - (when-let [f (get-in table [:data-fns :set-row-selection!])] - (f {})))))})]) - {:disable-on-pointer-down? true - :default-collapsed? (:sidebar? config)})]))) + (views/view view-entity + {:config config + :data data + :set-data! set-data! + :views-title (class-views class views view-entity {:set-view-entity! set-view-entity! + :set-views! set-views!}) + :columns columns + :add-new-object! (if (= :logseq.class/Asset (:db/ident class)) + (fn [_e] + (shui/dialog-open! + (fn [] + [:div.flex.flex-col.gap-2 + [:div.font-medium "Add assets"] + (filepicker/picker + {:on-change (fn [_e files] + (p/do! + (editor-handler/upload-asset! nil files :markdown editor-handler/*asset-uploading? true) + (set-data! (get-class-objects class)) + (shui/dialog-close!)))})]))) + #(add-new-class-object! class set-data!)) + :show-add-property? true + :add-property! (fn [] + (state/pub-event! [:editor/new-property {:block class + :class-schema? true}])) + :on-delete-rows (fn [table selected-rows] + (let [pages (filter ldb/page? selected-rows) + blocks (remove ldb/page? selected-rows)] + (p/do! + (ui-outliner-tx/transact! + {:outliner-op :delete-blocks} + (when (seq blocks) + (outliner-op/delete-blocks! blocks nil)) + (let [page-ids (map :db/id pages) + tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id class)]) page-ids)] + (when (seq tx-data) + (outliner-op/transact! tx-data {:outliner-op :save-block})))) + (set-data! (get-class-objects class)) + (when-let [f (get-in table [:data-fns :set-row-selection!])] + (f {})))))})))) (rum/defcs class-objects < rum/reactive db-mixins/query mixins/container-id [state class {:keys [current-page? sidebar?]}] @@ -251,35 +243,32 @@ []) (when (false? loading?) - (ui/foldable - [:div.font-medium.opacity-50 "Nodes with Property"] - [:div.mt-2 - (views/view view-entity {:config config - :data data - :set-data! set-data! - :title-key :views.table/property-nodes - :columns columns - :add-new-object! #(add-new-property-object! property set-data!) + (views/view view-entity + {:config config + :data data + :set-data! set-data! + :title-key :views.table/property-nodes + :columns columns + :add-new-object! #(add-new-property-object! property set-data!) ;; TODO: Add support for adding column - :show-add-property? false - :on-delete-rows (when-not (contains? #{:logseq.property/built-in? :logseq.property/parent} - (:db/ident property)) - (fn [table selected-rows] - (let [pages (filter ldb/page? selected-rows) - blocks (remove ldb/page? selected-rows)] - (p/do! - (ui-outliner-tx/transact! - {:outliner-op :delete-blocks} - (when (seq blocks) - (outliner-op/delete-blocks! blocks nil)) - (let [page-ids (map :db/id pages) - tx-data (map (fn [pid] [:db/retract pid (:db/ident property)]) page-ids)] - (when (seq tx-data) - (outliner-op/transact! tx-data {:outliner-op :save-block})))) - (set-data! (get-property-related-objects (state/get-current-repo) property)) - (when-let [f (get-in table [:data-fns :set-row-selection!])] - (f {}))))))})] - {:disable-on-pointer-down? true})))) + :show-add-property? false + :on-delete-rows (when-not (contains? #{:logseq.property/built-in? :logseq.property/parent} + (:db/ident property)) + (fn [table selected-rows] + (let [pages (filter ldb/page? selected-rows) + blocks (remove ldb/page? selected-rows)] + (p/do! + (ui-outliner-tx/transact! + {:outliner-op :delete-blocks} + (when (seq blocks) + (outliner-op/delete-blocks! blocks nil)) + (let [page-ids (map :db/id pages) + tx-data (map (fn [pid] [:db/retract pid (:db/ident property)]) page-ids)] + (when (seq tx-data) + (outliner-op/transact! tx-data {:outliner-op :save-block})))) + (set-data! (get-property-related-objects (state/get-current-repo) property)) + (when-let [f (get-in table [:data-fns :set-row-selection!])] + (f {}))))))})))) ;; Show all nodes containing the given property (rum/defcs property-related-objects < rum/reactive db-mixins/query mixins/container-id diff --git a/src/main/frontend/components/page.cljs b/src/main/frontend/components/page.cljs index c5305b44a6..ac622148be 100644 --- a/src/main/frontend/components/page.cljs +++ b/src/main/frontend/components/page.cljs @@ -196,7 +196,7 @@ (rum/defcs page-blocks-cp < rum/reactive db-mixins/query {:will-mount (fn [state] - (let [page-e (second (:rum/args state)) + (let [page-e (first (:rum/args state)) page-name (:block/name page-e)] (when (and page-name (db/journal-page? page-name) @@ -204,7 +204,7 @@ (date/journal-title->int (date/today)))) (state/pub-event! [:journal/insert-template page-name]))) state)} - [state repo page-e {:keys [sidebar? whiteboard?] :as config}] + [state page-e {:keys [sidebar? whiteboard?] :as config}] (when page-e (let [page-name (or (:block/name page-e) (str (:block/uuid page-e))) @@ -220,44 +220,35 @@ (remove (fn [b] (some? (get b (:db/ident block)))) children) :else - children) - db-based? (config/db-based-graph? repo)] - [:<> - (let [blocks (cond - (and - (not block?) - (empty? children) page-e) - (dummy-block page-e) + children)] + (cond + (and + (not block?) + (empty? children) page-e) + (dummy-block page-e) - :else - (let [document-mode? (state/sub :document/mode?) - hiccup-config (merge - {:id (if block? (str block-id) page-name) - :db/id (:db/id block) - :block? block? - :editor-box editor/box - :document/mode? document-mode?} - config) - config (common-handler/config-with-document-mode hiccup-config) - blocks (if block? [block] (db/sort-by-order children block))] - (let [add-button? (not (or config/publishing? - (let [last-child-id (model/get-block-deep-last-open-child-id (db/get-db) (:db/id (last blocks))) - block' (if last-child-id (db/entity last-child-id) (last blocks))] - (string/blank? (:block/title block')))))] - [:div - {:class (when add-button? "show-add-button")} - (page-blocks-inner page-e blocks config sidebar? whiteboard? block-id) - (let [args (if block-id - {:block-uuid block-id} - {:page page-name})] - (add-button args (:container-id config)))])))] - (if (and db-based? (or (ldb/class? block) (ldb/property? block))) - [:div.mt-4.ml-2.-mb-1 - (ui/foldable - [:div.font-medium.as-toggle {:class "pl-0.5"} "Notes"] - [:div.ml-1.-mb-2 blocks] - {:disable-on-pointer-down? true})] - blocks))]))) + :else + (let [document-mode? (state/sub :document/mode?) + hiccup-config (merge + {:id (if block? (str block-id) page-name) + :db/id (:db/id block) + :block? block? + :editor-box editor/box + :document/mode? document-mode?} + config) + config (common-handler/config-with-document-mode hiccup-config) + blocks (if block? [block] (db/sort-by-order children block))] + (let [add-button? (not (or config/publishing? + (let [last-child-id (model/get-block-deep-last-open-child-id (db/get-db) (:db/id (last blocks))) + block' (if last-child-id (db/entity last-child-id) (last blocks))] + (string/blank? (:block/title block')))))] + [:div + {:class (when add-button? "show-add-button")} + (page-blocks-inner page-e blocks config sidebar? whiteboard? block-id) + (let [args (if block-id + {:block-uuid block-id} + {:page page-name})] + (add-button args (:container-id config)))])))))) (rum/defc today-queries < rum/reactive [repo today? sidebar?] @@ -301,10 +292,11 @@ (rum/defc page-title-editor < rum/reactive [page {:keys [*input-value *title-value *edit? untitled? page-name old-name whiteboard-page?]}] (let [input-ref (rum/create-ref) + tag-idents (map :db/ident (:block/tags page)) collide? #(and (not= (util/page-name-sanity-lc page-name) (util/page-name-sanity-lc @*title-value)) - (db/page-exists? page-name (:block/type page)) - (db/page-exists? @*title-value (:block/type page))) + (db/page-exists? page-name tag-idents) + (db/page-exists? @*title-value tag-idents)) rollback-fn #(let [old-name (if untitled? "" old-name)] (reset! *title-value old-name) (gobj/set (rum/deref input-ref) "value" old-name) @@ -559,6 +551,65 @@ (plugins/hook-ui-slot :page-head-actions-slotted nil) (plugins/hook-ui-items :pagebar)]))) +(rum/defc tabs + [page opts] + (let [class? (ldb/class? page) + property? (ldb/property? page) + both? (and class? property?) + default-tab (cond + both? + "tag" + class? + "tag" + :else + "property")] + [:div.page-tabs + (shui/tabs + {:defaultValue default-tab + :class (str "w-full")} + (when (or both? property?) + [:div.flex.flex-row.gap-1.items-center + (shui/tabs-list + {:class "h-8"} + (when class? + (shui/tabs-trigger + {:value "tag" + :class "py-1 text-xs"} + "Tagged nodes")) + (when property? + (shui/tabs-trigger + {:value "property" + :class "py-1 text-xs"} + "Nodes with property")) + (when property? + (db-page/configure-property page)))]) + + (when class? + (shui/tabs-content + {:value "tag"} + (objects/class-objects page opts))) + (when property? + (shui/tabs-content + {:value "property"} + (objects/property-related-objects page (:current-page? opts)))))])) + +(rum/defc sidebar-page-properties + [config page] + (let [[collapsed? set-collapsed!] (rum/use-state true)] + [:div.ls-sidebar-page-properties.flex.flex-col.gap-2.mt-2 + [:div + (shui/button + {:variant :ghost + :size :sm + :class "px-1 text-muted-foreground" + :on-click #(set-collapsed! (not collapsed?))} + [:span.text-xs (str (if collapsed? "Open" "Hide")) " properties"])] + + (when-not collapsed? + [:<> + (component-block/db-properties-cp config page {:sidebar-properties? true}) + [:hr.my-4]])])) + ;; A page is just a logical block (rum/defcs ^:large-vars/cleanup-todo page-inner < rum/reactive db-mixins/query mixins/container-id (rum/local false ::all-collapsed?) @@ -605,9 +656,8 @@ (if (and whiteboard-page? (not sidebar?)) [:div ((state/get-component :whiteboard/tldraw-preview) (:block/uuid page))] ;; FIXME: this is not reactive - [:div.relative.page-inner - (when (or (and db-based? (not block?)) - (and (not db-based?) (not sidebar?) (not block?))) + [:div.relative.grid.gap-8.page-inner + (when-not (or block? sidebar?) [:div.flex.flex-row.space-between (when (and (or (mobile-util/native-platform?) (util/mobile?)) (not db-based?)) [:div.flex.flex-row.pr-2 @@ -625,14 +675,9 @@ :preview? preview?}))) (lsp-pagebar-slot)]) - (when (and db-based? (ldb/property? page)) - (db-page/configure-property page)) - - (when (and db-based? class-page?) - (objects/class-objects page {:current-page? option :sidebar? sidebar?})) - - (when (and db-based? (ldb/property? page)) - (objects/property-related-objects page (:current-page? option))) + (when (and db-based? sidebar?) + [:div.-mb-8 + (sidebar-page-properties config page)]) (when (and block? (not sidebar?) (not whiteboard?)) (let [config (merge config {:id "block-parent" @@ -640,10 +685,13 @@ [:div.mb-4 (component-block/breadcrumb config repo block-id {:level-limit 3})])) + (when (and db-based? (or class-page? (ldb/property? page))) + (tabs page {:current-page? option :sidebar? sidebar?})) + [:div.ls-page-blocks - (page-blocks-cp repo page (merge option {:sidebar? sidebar? - :container-id (:container-id state) - :whiteboard? whiteboard?}))]]) + (page-blocks-cp page (merge option {:sidebar? sidebar? + :container-id (:container-id state) + :whiteboard? whiteboard?}))]]) (when (not preview?) [:div {:style {:padding-left 9}} diff --git a/src/main/frontend/components/page.css b/src/main/frontend/components/page.css index c21815df6b..d46e8a1c50 100644 --- a/src/main/frontend/components/page.css +++ b/src/main/frontend/components/page.css @@ -65,8 +65,6 @@ @apply rounded-sm; &.title { - @apply mb-3; - .block-main-container { @apply gap-2; } @@ -258,5 +256,5 @@ html.is-native-ios { } .ls-page-blocks { - @apply min-h-[60px] -mb-2; -} \ No newline at end of file + @apply min-h-[60px]; +} diff --git a/src/main/frontend/components/property.cljs b/src/main/frontend/components/property.cljs index 36a71b533d..da8b434957 100644 --- a/src/main/frontend/components/property.cljs +++ b/src/main/frontend/components/property.cljs @@ -13,6 +13,7 @@ [frontend.handler.property.util :as pu] [frontend.config :as config] [frontend.db :as db] + [frontend.db.model :as db-model] [frontend.db-mixins :as db-mixins] [frontend.db.async :as db-async] [frontend.handler.db-based.property :as db-property-handler] @@ -99,7 +100,8 @@ add-class-property? (and (ldb/class? block) class-schema?)] (when *property (reset! *property property)) (p/do! - (when *show-new-property-config? (reset! *show-new-property-config? false)) + (when *show-new-property-config? + (reset! *show-new-property-config? false)) (when (= (:type schema) :node) (reset! *show-class-select? true)) (components-pu/update-property! property property-name schema) (cond @@ -142,30 +144,42 @@ (rum/defc property-select [exclude-properties select-opts] (let [[properties set-properties!] (rum/use-state nil) + [classes set-classes!] (rum/use-state nil) [excluded-properties set-excluded-properties!] (rum/use-state nil)] (rum/use-effect! (fn [] - (p/let [properties (db-async/> (db-model/get-all-classes repo) + (remove ldb/built-in?))] + (set-classes! classes) (set-properties! (remove exclude-properties properties)) (set-excluded-properties! (->> properties (filter exclude-properties) (map :block/title) set)))) []) - [:div.ls-property-add.flex.flex-row.items-center.property-key - [:div.ls-property-key - (select/select (merge - {:items (map (fn [x] - {:label (:block/title x) - :value (:block/uuid x)}) properties) - :extract-fn :label - :dropdown? false - :close-modal? false - :new-case-sensitive? true - :show-new-when-not-exact-match? true - :exact-match-exclude-items (fn [s] (contains? excluded-properties s)) - :input-default-placeholder "Add or change property"} - select-opts))]])) + (let [items (concat + (map (fn [x] + {:label (:block/title x) + :value (:block/uuid x)}) properties) + (map (fn [x] + {:label (:block/title x) + :value (:block/uuid x) + :group "Tags"}) classes))] + [:div.ls-property-add.flex.flex-row.items-center.property-key + [:div.ls-property-key + (select/select (merge + {:items items + :grouped? true + :extract-fn :label + :dropdown? false + :close-modal? false + :new-case-sensitive? true + :show-new-when-not-exact-match? true + :exact-match-exclude-items (fn [s] (contains? excluded-properties s)) + :input-default-placeholder "Add or change property"} + select-opts))]]))) (rum/defc property-icon [property property-type] @@ -194,7 +208,7 @@ (fn [{:keys [value label]}] (reset! *property-key (if (uuid? value) label value)) (let [property (when (uuid? value) (db/entity [:block/uuid value]))] - (when (and *show-new-property-config? (not property)) + (when (and *show-new-property-config? (not (ldb/property? property))) (reset! *show-new-property-config? true)) (reset! *property property) (when property @@ -221,10 +235,26 @@ (not (seq (:property/closed-values property)))) (pv/ types + (and page? (not (contains? types :page))) + (conj :page) + (empty? types) + #{:block})) exclude-properties (fn [m] - (let [view-context (get-in m [:block/schema :view-context] :all) - block-types (if (and page? (not= block-type :page)) - #{:page block-type} - #{block-type})] + (let [view-context (get-in m [:block/schema :view-context] :all)] (or (contains? #{:logseq.property/query} (:db/ident m)) (and (not page?) (contains? #{:block/alias} (:db/ident m))) ;; Filters out properties from being in wrong :view-context and :never view-contexts - (and (not= view-context :all) (not (contains? block-types view-context)))))) + (and (not= view-context :all) (not (contains? block-types view-context))) + (and (ldb/built-in? block) (contains? #{:logseq.property/parent} (:db/ident m)))))) property (rum/react *property) property-key (rum/react *property-key)] [:div.ls-property-input.flex.flex-1.flex-row.items-center.flex-wrap.gap-1 @@ -392,7 +425,7 @@ (rum/defcs new-property < rum/reactive [state block opts] - (when (and (not config/publishing?) (:class-schema? opts)) + (when-not config/publishing? [:div.ls-new-property {:style {:margin-left 6 :margin-top 1}} [:a.fade-link.flex.jtrigger {:tab-index 0 @@ -553,7 +586,7 @@ :will-remount (fn [state] (let [block (db/entity (:db/id (::block state)))] (assoc state ::classes (async-load-classes! block))))} - [state _target-block {:keys [class-schema?] :as opts}] + [state _target-block {:keys [class-schema? sidebar-properties?] :as opts}] (let [id (::id state) db-id (:db/id (::block state)) block (db/sub-block db-id) @@ -563,7 +596,6 @@ (and (set? ids) (contains? ids (:block/uuid block)))))) _ (doseq [class (::classes state)] (db/sub-block (:db/id class))) - page? (db/page? block) class? (ldb/class? block) block-properties (:block/properties block) properties (cond @@ -652,20 +684,38 @@ (when (and class? (nil? (:logseq.property.class/properties block))) [[:logseq.property.class/properties nil]])) remove-built-in-or-other-position-properties)] - (when-not (and (empty? full-properties) (not (:class-schema? opts))) - [:div.ls-properties-area - {:id id - :class (util/classnames [{:class-properties class-schema? - :ls-page-properties (and page? (not class-schema?))}]) - :tab-index 0 - :on-key-up #(when-let [block (and (= "Escape" (.-key %)) - (.closest (.-target %) "[blockid]"))] - (let [target (.-target %)] - (when-not (d/has-class? target "ls-popup-closed") - (state/set-selection-blocks! [block]) - (some-> js/document.activeElement (.blur))) - (d/remove-class! target "ls-popup-closed")))} - (let [properties' (remove (fn [[k _v]] (contains? #{:logseq.property/icon :logseq.property/query} k)) full-properties)] - (properties-section block (if class-schema? properties properties') opts)) + (cond + (and (empty? full-properties) (not (:class-schema? opts))) + (when sidebar-properties? + (rum/with-key (new-property block opts) (str id "-add-property"))) - (rum/with-key (new-property block opts) (str id "-add-property"))]))) + :else + (let [remove-properties #{:logseq.property/icon :logseq.property/query} + properties' (remove (fn [[k _v]] (contains? remove-properties k)) full-properties) + properties'' (cond->> properties' + (not class-schema?) + (remove (fn [[k _v]] (= k :logseq.property.class/properties)))) + page? (ldb/page? block)] + [:div.ls-properties-area + {:id id + :class (util/classnames [{:class-properties class-schema? + :ls-page-properties (and page? (not class-schema?))}]) + :tab-index 0 + :on-key-up #(when-let [block (and (= "Escape" (.-key %)) + (.closest (.-target %) "[blockid]"))] + (let [target (.-target %)] + (when-not (d/has-class? target "ls-popup-closed") + (state/set-selection-blocks! [block]) + (some-> js/document.activeElement (.blur))) + (d/remove-class! target "ls-popup-closed")))} + (properties-section block (if class-schema? properties properties'') opts) + + (when page? + (rum/with-key (new-property block opts) (str id "-add-property"))) + + (when page? + (let [properties'' (filter (fn [[k _v]] (= k :logseq.property.class/properties)) properties')] + (when (seq properties'') + [:<> + (when-not class-schema? [:hr.my-4]) + (properties-section block (if class-schema? properties properties'') opts)])))])))) diff --git a/src/main/frontend/components/property.css b/src/main/frontend/components/property.css index 102d69bd61..c390c13b26 100644 --- a/src/main/frontend/components/property.css +++ b/src/main/frontend/components/property.css @@ -29,9 +29,7 @@ margin-left: 0; } -.ls-block .property-pair, -.property-block .property-value, -.block-property-value { +.ls-block .property-pair, .ls-sidebar-page-properties .property-pair, .property-block .property-value, .block-property-value { margin-left: 7px; } diff --git a/src/main/frontend/components/property/config.cljs b/src/main/frontend/components/property/config.cljs index 47e0562a5b..298e84957a 100644 --- a/src/main/frontend/components/property/config.cljs +++ b/src/main/frontend/components/property/config.cljs @@ -96,7 +96,7 @@ (let [toggle-fn #(do (when (fn? on-hide) (on-hide)) (shui/popup-hide! id)) - classes (model/get-all-classes (state/get-current-repo) {:except-root-class? true}) + classes (model/get-all-readable-classes (state/get-current-repo) {:except-root-class? true}) options (map (fn [class] {:label (:block/title class) :value (:block/uuid class)}) diff --git a/src/main/frontend/components/property/value.cljs b/src/main/frontend/components/property/value.cljs index 03075ca1cc..d21f5db062 100644 --- a/src/main/frontend/components/property/value.cljs +++ b/src/main/frontend/components/property/value.cljs @@ -5,7 +5,6 @@ [dommy.core :as d] [frontend.components.icon :as icon-component] [frontend.components.select :as select] - [frontend.components.title :as title] [frontend.config :as config] [frontend.date :as date] [frontend.db :as db] @@ -34,7 +33,8 @@ [logseq.db.frontend.property.type :as db-property-type] [logseq.shui.ui :as shui] [promesa.core :as p] - [rum.core :as rum])) + [rum.core :as rum] + [clojure.set :as set])) (rum/defc property-empty-btn-value [property & opts] @@ -506,13 +506,16 @@ (when (and property-type (not= property-type :node)) (if (= property-type :page) (not (db/page? node)) - (not= property-type (some-> (:block/type node) keyword)))))) + (not (contains? (ldb/get-entity-types node) property-type)))))) result))))) options (map (fn [node] (let [id (or (:value node) (:db/id node)) [header label] (if (integer? id) - (let [title (subs (title/block-unique-title node) 0 256) + (let [node-title (if (seq (:property/schema.classes property)) + (:block/title node) + (block-handler/block-unique-title node)) + title (subs node-title 0 256) node (or (db/entity id) node) icon (get-node-icon node) header (when-not (db/page? node) @@ -529,7 +532,10 @@ :header header :label-value (:block/title node) :label label - :value id))) nodes) + :value id + :disabled? (and tags? (contains? + (set/union #{:logseq.class/Journal :logseq.class/Whiteboard} ldb/internal-tags) + (:db/ident node)))))) nodes) classes' (remove (fn [class] (= :logseq.class/Root (:db/ident class))) classes) opts' (cond-> (merge @@ -547,7 +553,10 @@ "Choose nodes" :else "Choose node") - :show-new-when-not-exact-match? (if (and parent-property? (contains? (set children-pages) (:db/id block))) + :show-new-when-not-exact-match? (if (or (and parent-property? (contains? (set children-pages) (:db/id block))) + ;; Don't allow creating private tags + (seq (set/intersection (set (map :db/ident classes)) + ldb/private-tags))) false true) :extract-chosen-fn :value @@ -815,6 +824,9 @@ (= value :logseq.property/empty-placeholder) (property-empty-btn-value property) + closed-values? + (closed-value-item value opts) + (or (ldb/page? value) (and (seq (:block/tags value)) ;; FIXME: page-cp should be renamed to node-cp and @@ -832,9 +844,6 @@ (when-let [reference (state/get-component :block/reference)] (reference {} (:block/uuid value))) - closed-values? - (closed-value-item value opts) - (de/entity? value) (when-some [content (str (db-property/property-value-content value))] (inline-text-cp content)) @@ -966,7 +975,9 @@ (let [type (get schema :type :default) date? (= type :date) *el (rum/use-ref nil) - items (if (de/entity? v) #{v} v)] + items (cond->> (if (de/entity? v) #{v} v) + (= (:db/ident property) :block/tags) + (remove (fn [v] (contains? ldb/hidden-tags (:db/ident v)))))] (rum/use-effect! (fn [] (when editing? @@ -1002,7 +1013,7 @@ (do (some-> (rum/deref *el) (.click)) (util/stop e)) :dune)) - :class "flex flex-1 flex-row items-center flex-wrap gap-x-2 gap-y-2 pr-4"} + :class "flex flex-1 flex-row items-center flex-wrap gap-x-2 gap-y-2"} (let [not-empty-value? (not= (map :db/ident items) [:logseq.property/empty-placeholder])] (if (and (seq items) not-empty-value?) (concat diff --git a/src/main/frontend/components/query/builder.cljs b/src/main/frontend/components/query/builder.cljs index dc795c4d8e..bfe46a9f93 100644 --- a/src/main/frontend/components/query/builder.cljs +++ b/src/main/frontend/components/query/builder.cljs @@ -227,7 +227,7 @@ db-based? (config/db-based-graph? repo)] (rum/use-effect! (fn [] - (let [result (db-model/get-all-classes repo {:except-root-class? true})] + (let [result (db-model/get-all-readable-classes repo {:except-root-class? true})] (set-values! result))) []) (let [items (->> values diff --git a/src/main/frontend/components/repo.cljs b/src/main/frontend/components/repo.cljs index d466a6b7ba..451589fa40 100644 --- a/src/main/frontend/components/repo.cljs +++ b/src/main/frontend/components/repo.cljs @@ -307,8 +307,8 @@ db-based? (config/db-based-graph? current-repo) repos (sort-repos-with-metadata-local repos) repos (distinct - (if (and (or (seq remotes) (seq rtc-graphs)) login?) - (repo-handler/combine-local-&-remote-graphs repos (concat remotes rtc-graphs)) repos)) + (if (and (or (seq remotes) (seq rtc-graphs)) login?) + (repo-handler/combine-local-&-remote-graphs repos (concat remotes rtc-graphs)) repos)) items-fn #(repos-dropdown-links repos current-repo downloading-graph-id opts) header-fn #(when (> (count repos) 1) ; show switch to if there are multiple repos [:div.font-medium.text-sm.opacity-50.px-1.py-1.flex.flex-row.justify-between.items-center @@ -318,14 +318,14 @@ (if remotes-loading? (ui/loading "") (shui/button - {:variant :ghost - :size :sm - :title "Refresh remote graphs" - :class "!h-6 !px-1 relative right-[-4px]" - :on-click (fn [] - (file-sync/load-session-graphs) - (rtc-handler/ (.-target e) - (.closest "a.item") - (shui/popup-show! - (fn [{:keys [id]}] (repos-dropdown-content (assoc opts :contentid id))) - {:as-dropdown? true - :auto-focus? false - :align "start" - :content-props {:class "repos-list" - :data-mode (when db-based? "db")}}))) - :title repo-name} ;; show full path on hover - [:div.flex.relative.graph-icon.rounded - (shui/tabler-icon "database" {:size 15})] + {:tab-index 0 + :class "item cp__repos-select-trigger" + :on-pointer-down + (fn [^js e] + (check-multiple-windows? state) + (some-> (.-target e) + (.closest "a.item") + (shui/popup-show! + (fn [{:keys [id]}] (repos-dropdown-content (assoc opts :contentid id))) + {:as-dropdown? true + :auto-focus? false + :align "start" + :content-props {:class "repos-list" + :data-mode (when db-based? "db")}}))) + :title repo-name} ;; show full path on hover + [:div.flex.relative.graph-icon.rounded + (shui/tabler-icon "database" {:size 15})] - [:div.repo-switch.pr-2.whitespace-nowrap - [:span.repo-name.font-medium - [:span.repo-text.overflow-hidden.text-ellipsis - (if (config/demo-graph? short-repo-name) "Demo" short-repo-name)] - (when remote? [:span.pl-1 (ui/icon "cloud")])] - [:span.dropdown-caret]]))))) + [:div.repo-switch.pr-2.whitespace-nowrap + [:span.repo-name.font-medium + [:span.repo-text.overflow-hidden.text-ellipsis + (if (config/demo-graph? short-repo-name) "Demo" short-repo-name)] + (when remote? [:span.pl-1 (ui/icon "cloud")])] + [:span.dropdown-caret]]))))) (rum/defcs graphs-selector < rum/reactive [_state] @@ -413,10 +413,10 @@ [:a.item.flex.items-center.gap-1.select-none {:on-click (fn [^js e] (shui/popup-show! (.closest (.-target e) "a") - (fn [{:keys [id]}] (repos-dropdown-content {:contentid id})) - {:as-dropdown? true - :content-props {:class "repos-list"} - :align :start}))} + (fn [{:keys [id]}] (repos-dropdown-content {:contentid id})) + {:as-dropdown? true + :content-props {:class "repos-list"} + :align :start}))} [:span.thumb (shui/tabler-icon (if remote? "cloud" (if db-based? "database" "folder")) {:size 16})] [:strong short-repo-name] (shui/tabler-icon "selector" {:size 18})]])) diff --git a/src/main/frontend/components/right_sidebar.cljs b/src/main/frontend/components/right_sidebar.cljs index 43ccce73c5..631d568aed 100644 --- a/src/main/frontend/components/right_sidebar.cljs +++ b/src/main/frontend/components/right_sidebar.cljs @@ -97,8 +97,8 @@ (let [lookup (if (integer? db-id) db-id [:block/uuid db-id]) page (db/entity repo lookup)] (if (ldb/page? page) - [[:.flex.items-center.page-title - (icon/get-node-icon-cp page {:class "text-md mr-2"}) + [[:.flex.items-center.page-title.gap-1 + (icon/get-node-icon-cp page {:class "text-md"}) [:span.overflow-hidden.text-ellipsis (:block/title page)]] (page-cp repo (str (:block/uuid page)))] (block-with-breadcrumb repo page idx [repo db-id block-type] false))) diff --git a/src/main/frontend/components/right_sidebar.css b/src/main/frontend/components/right_sidebar.css index 417ff3a01d..9207e0125e 100644 --- a/src/main/frontend/components/right_sidebar.css +++ b/src/main/frontend/components/right_sidebar.css @@ -38,7 +38,3 @@ html[data-theme=light] { @apply opacity-100; } } - -.sidebar-panel-content { - @apply pt-3; -} \ No newline at end of file diff --git a/src/main/frontend/components/select.cljs b/src/main/frontend/components/select.cljs index 296abc11d7..c47fef8d18 100644 --- a/src/main/frontend/components/select.cljs +++ b/src/main/frontend/components/select.cljs @@ -29,7 +29,8 @@ (when multiple-choices? (ui/checkbox {:checked (boolean (selected-choices (:value result))) :on-click (fn [e] - (.preventDefault e))})) + (.preventDefault e)) + :disabled (:disabled? result)})) value] (when (and (map? result) (:id result)) [:div.tip.flex @@ -67,7 +68,7 @@ :will-unmount (fn [state] (shui/dialog-close! :ls-select-modal) state)} - [state {:keys [items limit on-chosen empty-placeholder + [state {:keys [items limit on-chosen empty-placeholder grouped? prompt-key input-default-placeholder close-modal? extract-fn extract-chosen-fn host-opts on-input input-opts item-cp transform-fn tap-*input-val @@ -138,7 +139,8 @@ [:div.item-results-wrap (ui/auto-complete search-result - {:item-render (or item-cp (fn [result chosen?] + {:grouped? grouped? + :item-render (or item-cp (fn [result chosen?] (render-item result chosen? multiple-choices? *selected-choices))) :class "cp__select-results" :on-chosen (fn [raw-chosen e] diff --git a/src/main/frontend/components/title.cljs b/src/main/frontend/components/title.cljs deleted file mode 100644 index b3c16591c6..0000000000 --- a/src/main/frontend/components/title.cljs +++ /dev/null @@ -1,30 +0,0 @@ -(ns frontend.components.title - (:require [clojure.string :as string] - [frontend.db :as db] - [logseq.db :as ldb] - [datascript.impl.entity :as de])) - -(defn block-unique-title - "Multiple pages/objects may have the same `:block/title`. - Notice: this doesn't prevent for pages/objects that have the same tag or created by different clients." - [block] - (let [block-e (cond - (de/entity? block) - block - (uuid? (:block/uuid block)) - (db/entity [:block/uuid (:block/uuid block)]) - :else - block) - tags (remove (fn [t] (some-> (:block/raw-title block-e) (ldb/inline-tag? t))) - (map (fn [tag] (if (number? tag) (db/entity tag) tag)) (:block/tags block)))] - (if (and (seq tags) - (not (ldb/journal? block))) - (str (:block/title block) - " " - (string/join - ", " - (keep (fn [tag] - (when-let [title (:block/title tag)] - (str "#" title))) - tags))) - (:block/title block)))) diff --git a/src/main/frontend/components/views.cljs b/src/main/frontend/components/views.cljs index 4b12784dd4..eba8bd10e0 100644 --- a/src/main/frontend/components/views.cljs +++ b/src/main/frontend/components/views.cljs @@ -80,11 +80,11 @@ {:variant "text" :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start" :on-click #(column-toggle-sorting! column)} - (let [title (str (:name column))] - [:span {:title title - :class "max-w-full overflow-hidden text-ellipsis"} - title]) - (case asc? + (let [title (str (:name column))] + [:span {:title title + :class "max-w-full overflow-hidden text-ellipsis"} + title]) + (case asc? true (ui/icon "arrow-up") false @@ -529,7 +529,6 @@ (defn- get-property-values [rows property] (let [property-ident (:db/ident property) - block-type? (= property-ident :block/type) values (->> (mapcat (fn [e] (let [e' (db/entity (:db/id e)) v (get e' property-ident)] (if (set? v) v #{v}))) rows) @@ -537,9 +536,8 @@ (distinct))] (->> (map (fn [e] - (let [label (get-property-value-content e) - label' (if (and block-type? (= label "class")) "tag" label)] - {:label (str label') :value e})) + (let [label (get-property-value-content e)] + {:label (str label) :value e})) values) (sort-by :label)))) @@ -607,7 +605,7 @@ (do (shui/popup-hide!) (let [property internal-property - new-filter [(:db/ident property) (if (= (:db/ident property) :block/type) :is :text-contains)] + new-filter [(:db/ident property) :text-contains] filters' (if (seq filters) (conj filters new-filter) [new-filter])] @@ -685,15 +683,14 @@ [:before :after] (concat [:is :is-not] - (when-not (= :block/type (:db/ident property)) - (case (get-in property [:block/schema :type]) - (:default :url :node) - [:text-contains :text-not-contains] - (:date) - [:date-before :date-after] - :number - [:number-gt :number-lt :number-gte :number-lte :between] - nil))))) + (case (get-in property [:block/schema :type]) + (:default :url :node) + [:text-contains :text-not-contains] + (:date) + [:date-before :date-after] + :number + [:number-gt :number-lt :number-gte :number-lte :between] + nil)))) (defn- get-filter-with-changed-operator [_property operator value] @@ -821,16 +818,11 @@ {:class "!px-2 rounded-none border-r" :variant "ghost" :size :sm} - (let [block-type? (= (:db/ident property) :block/type) - value (cond + (let [value (cond (uuid? value) (db/entity [:block/uuid value]) (and (coll? value) (every? uuid? value)) (set (map #(db/entity [:block/uuid %]) value)) - (and block-type? (coll? value)) - (map (fn [v] (if (= v "class") "tag" v)) value) - (and block-type? (= value "class")) - "tag" :else value)] [:div.flex.flex-row.items-center.gap-1.text-xs @@ -1012,15 +1004,15 @@ (rum/defc new-record-button < rum/static [table view-entity] (let [asset? (and (:logseq.property/built-in? view-entity) - (= (:block/name view-entity) "asset"))] + (= (:block/name view-entity) "asset"))] (ui/tooltip - (shui/button - {:variant "ghost" - :class "!px-1 text-muted-foreground" - :size :sm - :on-click (get-in table [:data-fns :add-new-object!])} - (ui/icon (if asset? "upload" "plus"))) - [:div "New record"]))) + (shui/button + {:variant "ghost" + :class "!px-1 text-muted-foreground" + :size :sm + :on-click (get-in table [:data-fns :add-new-object!])} + (ui/icon (if asset? "upload" "plus"))) + [:div "New record"]))) (rum/defc add-new-row < rum/static [table] @@ -1084,8 +1076,8 @@ *rows-wrap (rum/use-ref nil)] (rum/use-effect! - (fn [] (set-ready? true)) - []) + (fn [] (set-ready? true)) + []) (shui/table (let [columns' (:columns table) @@ -1097,17 +1089,17 @@ (table-header table columns' option selected-rows) (ui/virtualized-list - {:ref #(reset! *scroller-ref %) - :custom-scroll-parent (or (some-> (rum/deref *rows-wrap) (.closest ".sidebar-item-list")) - (gdom/getElement "main-content-container")) - :increase-viewport-by {:top 300 :bottom 300} - :compute-item-key (fn [idx] - (let [block (nth rows idx)] - (str "table-row-" (:db/id block)))) - :total-count (count rows) - :item-content (fn [idx] - (let [row (nth rows idx)] - (table-row table row columns' {} option)))}) + {:ref #(reset! *scroller-ref %) + :custom-scroll-parent (or (some-> (rum/deref *rows-wrap) (.closest ".sidebar-item-list")) + (gdom/getElement "main-content-container")) + :increase-viewport-by {:top 300 :bottom 300} + :compute-item-key (fn [idx] + (let [block (nth rows idx)] + (str "table-row-" (:db/id block)))) + :total-count (count rows) + :item-content (fn [idx] + (let [row (nth rows idx)] + (table-row table row columns' {} option)))}) (when add-new-object! (shui/table-footer (add-new-row table)))])])))) @@ -1245,39 +1237,42 @@ [:div.flex.flex-col.gap-2.grid {:ref *view-ref} - [:div.flex.flex-wrap.items-center.justify-between.gap-1 - (when-not render-empty-title? - [:div.flex.flex-row.items-center.gap-2 - (or - views-title - [:div.font-medium.opacity-50.text-sm - (t (or title-key :views.table/default-title) - (count (:rows table)))])]) - [:div.view-actions.flex.items-center.gap-1 + (ui/foldable + [:div.flex.flex-1.flex-wrap.items-center.justify-between.gap-1 + (when-not render-empty-title? + [:div.flex.flex-row.items-center.gap-2 + (or + views-title + [:div.font-medium.opacity-50.text-sm + (t (or title-key :views.table/default-title) + (count (:rows table)))])]) + [:div.view-actions.flex.items-center.gap-1 - (filter-properties columns table) + (filter-properties columns table) - (search input {:on-change set-input! - :set-input! set-input!}) + (search input {:on-change set-input! + :set-input! set-input!}) - [:div.text-muted-foreground.text-sm - (pv/property-value view-entity (db/entity :logseq.property.view/type) - (db/entity display-type) {})] + [:div.text-muted-foreground.text-sm + (pv/property-value view-entity (db/entity :logseq.property.view/type) + (db/entity display-type) {})] - (more-actions columns table) + (more-actions columns table) - (when add-new-object! (new-record-button table view-entity))]] + (when add-new-object! (new-record-button table view-entity))]] + (fn [] + [:div.ls-view-body.flex.flex-col.gap-2.grid + (filters-row table) - (filters-row table) + (case display-type + :logseq.property.view/type.list + (list-view (:config option) view-entity (:rows table)) - (case display-type - :logseq.property.view/type.list - (list-view (:config option) view-entity (:rows table)) + :logseq.property.view/type.gallery + (gallery-view (:config option) table view-entity (:rows table) *scroller-ref) - :logseq.property.view/type.gallery - (gallery-view (:config option) table view-entity (:rows table) *scroller-ref) - - (table-view table option row-selection add-new-object! *scroller-ref))])) + (table-view table option row-selection add-new-object! *scroller-ref))]) + {:title-trigger? false})])) (rum/defcs view "Provides a view for data like query results and tagged objects, multiple diff --git a/src/main/frontend/context/i18n.cljs b/src/main/frontend/context/i18n.cljs index a2af607385..ecaefa749a 100644 --- a/src/main/frontend/context/i18n.cljs +++ b/src/main/frontend/context/i18n.cljs @@ -31,10 +31,10 @@ (defn tt [& keys] (some-> - (medley/find-first - #(not (string/starts-with? (t %) "{Missing key")) - keys) - t)) + (medley/find-first + #(not (string/starts-with? (t %) "{Missing key")) + keys) + t)) (defn- fetch-local-language [] (.. js/window -navigator -language)) diff --git a/src/main/frontend/db/async.cljs b/src/main/frontend/db/async.cljs index a142fa7b31..a259e00732 100644 --- a/src/main/frontend/db/async.cljs +++ b/src/main/frontend/db/async.cljs @@ -55,7 +55,7 @@ [graph & {:keys [remove-built-in-property? remove-non-queryable-built-in-property?] :or {remove-built-in-property? true remove-non-queryable-built-in-property? false}}] - (let [result (->> (d/datoms (db/get-db graph) :avet :block/type "property") + (let [result (->> (d/datoms (db/get-db graph) :avet :block/tags :logseq.class/Property) (map (fn [datom] (db/entity (:e datom)))) (sort-by (juxt ldb/built-in? :block/title)))] (cond->> result diff --git a/src/main/frontend/db/model.cljs b/src/main/frontend/db/model.cljs index cba6aa6ad8..0e5d7143dc 100644 --- a/src/main/frontend/db/model.cljs +++ b/src/main/frontend/db/model.cljs @@ -383,10 +383,10 @@ independent of format as format specific heading characters are stripped" (defn page-exists? "Whether a page exists." - [page-name type] + [page-name tags] (let [repo (state/get-current-repo)] (when-let [db (conn/get-db repo)] - (ldb/page-exists? db page-name type)))) + (ldb/page-exists? db page-name tags)))) (defn page-empty? "Whether a page is empty. Does it has a non-page block? @@ -531,14 +531,23 @@ independent of format as format specific heading characters are stripped" (defn get-journals-length [] (let [today (date-time-util/date->int (js/Date.))] - (d/q '[:find (count ?page) . - :in $ ?today - :where - [?page :block/type "journal"] - [?page :block/journal-day ?journal-day] - [(<= ?journal-day ?today)]] - (conn/get-db (state/get-current-repo)) - today))) + (if (config/db-based-graph?) + (d/q '[:find (count ?page) . + :in $ ?today + :where + [?page :block/tags :logseq.class/Journal] + [?page :block/journal-day ?journal-day] + [(<= ?journal-day ?today)]] + (conn/get-db (state/get-current-repo)) + today) + (d/q '[:find (count ?page) . + :in $ ?today + :where + [?page :block/type "journal"] + [?page :block/journal-day ?journal-day] + [(<= ?journal-day ?today)]] + (conn/get-db (state/get-current-repo)) + today)))) (defn get-latest-journals ([n] @@ -749,17 +758,29 @@ independent of format as format specific heading characters are stripped" (defn get-all-whiteboards [repo] - (d/q - '[:find [(pull ?page [:db/id - :block/uuid - :block/name - :block/title - :block/created-at - :block/updated-at]) ...] - :where - [?page :block/name] - [?page :block/type "whiteboard"]] - (conn/get-db repo))) + (if (config/db-based-graph?) + (d/q + '[:find [(pull ?page [:db/id + :block/uuid + :block/name + :block/title + :block/created-at + :block/updated-at]) ...] + :where + [?page :block/name] + [?page :block/tags :logseq.class/Whiteboard]] + (conn/get-db repo)) + (d/q + '[:find [(pull ?page [:db/id + :block/uuid + :block/name + :block/title + :block/created-at + :block/updated-at]) ...] + :where + [?page :block/name] + [?page :block/type "whiteboard"]] + (conn/get-db repo)))) (defn get-whiteboard-id-nonces [repo page-id] @@ -777,16 +798,27 @@ independent of format as format specific heading characters are stripped" :nonce (:nonce shape)})))))) (defn get-all-classes - [repo & {:keys [except-root-class?] - :or {except-root-class? false}}] + [repo & {:keys [except-root-class? except-private-tags?] + :or {except-root-class? false + except-private-tags? true}}] (let [db (conn/get-db repo) - classes (->> (d/datoms db :avet :block/type "class") + classes (->> (d/datoms db :avet :block/tags :logseq.class/Tag) (map (fn [d] - (db-utils/entity db (:e d)))))] + (db-utils/entity db (:e d)))) + (remove (fn [d] + (and except-private-tags? + (contains? ldb/private-tags (:db/ident d))))))] (if except-root-class? (keep (fn [e] (when-not (= :logseq.class/Root (:db/ident e)) e)) classes) classes))) +(defn get-all-readable-classes + "Gets all classes that are used in a read only context e.g. querying or used + for property value selection. This should _not_ be used in a write context e.g. + adding a tag to a node or creating a new node with a tag" + [repo opts] + (get-all-classes repo (merge opts {:except-private-tags? false}))) + (defn get-structured-children [repo eid] (->> @@ -802,13 +834,15 @@ independent of format as format specific heading characters are stripped" (defn get-class-objects [repo class-id] (when-let [class (db-utils/entity repo class-id)] - (if (first (:logseq.property/_parent class)) ; has children classes - (let [all-classes (conj (->> (get-structured-children repo class-id) - (map #(db-utils/entity repo %))) - class)] - (->> (mapcat :block/_tags all-classes) - distinct)) - (:block/_tags class)))) + (->> + (if (first (:logseq.property/_parent class)) ; has children classes + (let [all-classes (conj (->> (get-structured-children repo class-id) + (map #(db-utils/entity repo %))) + class)] + (->> (mapcat :block/_tags all-classes) + distinct)) + (:block/_tags class)) + (remove ldb/hidden?)))) (defn sub-class-objects [repo class-id] @@ -829,7 +863,8 @@ independent of format as format specific heading characters are stripped" (rules/extract-rules rules/db-query-dsl-rules [:has-property-or-default-value] {:deps rules/rules-dependencies}) (:db/ident property)) - (map #(db-utils/entity repo %))))) + (map #(db-utils/entity repo %)) + (remove ldb/hidden?)))) (defn get-all-namespace-relation [repo] @@ -849,18 +884,30 @@ independent of format as format specific heading characters are stripped" (defn get-pages-relation [repo with-journal?] (when-let [db (conn/get-db repo)] - (let [q (if with-journal? - '[:find ?p ?ref-page - :where - [?block :block/page ?p] - [?block :block/refs ?ref-page]] - '[:find ?p ?ref-page - :where - [?block :block/page ?p] - [(get-else $ ?p :block/type "N/A") ?type] - [(not= ?type "journal")] - [?block :block/refs ?ref-page]])] - (d/q q db)))) + (if (config/db-based-graph?) + (let [q (if with-journal? + '[:find ?p ?ref-page + :where + [?block :block/page ?p] + [?block :block/refs ?ref-page]] + '[:find ?p ?ref-page + :where + [?block :block/page ?p] + [?p :block/tags] + (not [?p :block/tags :logseq.class/Journal]) + [?block :block/refs ?ref-page]])] + (d/q q db)) + (let [q (if with-journal? + '[:find ?p ?ref-page + :where + [?block :block/page ?p] + [?block :block/refs ?ref-page]] + '[:find ?p ?ref-page + :where + [?block :block/page ?p] + (not [?p :block/type "journal"]) + [?block :block/refs ?ref-page]])] + (d/q q db))))) (defn get-namespace-pages "Accepts both sanitized and unsanitized namespaces" diff --git a/src/main/frontend/db/query_dsl.cljs b/src/main/frontend/db/query_dsl.cljs index d462cc771c..96d3d8074a 100644 --- a/src/main/frontend/db/query_dsl.cljs +++ b/src/main/frontend/db/query_dsl.cljs @@ -323,7 +323,7 @@ (or (some->> (name property-name) (db-utils/q '[:find [(pull ?b [:db/ident]) ...] :in $ ?title - :where [?b :block/type "property"] [?b :block/title ?title]]) + :where [?b :block/tags :logseq.class/Property] [?b :block/title ?title]]) first :db/ident) ;; Don't return nil as that incorrectly matches all properties diff --git a/src/main/frontend/handler/block.cljs b/src/main/frontend/handler/block.cljs index 7e8a88f87b..be8e893d41 100644 --- a/src/main/frontend/handler/block.cljs +++ b/src/main/frontend/handler/block.cljs @@ -22,7 +22,8 @@ [frontend.handler.property.util :as pu] [dommy.core :as dom] [goog.object :as gobj] - [promesa.core :as p])) + [promesa.core :as p] + [datascript.impl.entity :as de])) ;; Fns @@ -188,6 +189,32 @@ (-> (property-util/remove-built-in-properties format content) (drawer/remove-logbook)))) +(defn block-unique-title + "Multiple pages/objects may have the same `:block/title`. + Notice: this doesn't prevent for pages/objects that have the same tag or created by different clients." + [block] + (let [block-e (cond + (de/entity? block) + block + (uuid? (:block/uuid block)) + (db/entity [:block/uuid (:block/uuid block)]) + :else + block) + tags (remove (fn [t] + (or (some-> (:block/raw-title block-e) (ldb/inline-tag? t)) + (ldb/private-tags (:db/ident t)))) + (map (fn [tag] (if (number? tag) (db/entity tag) tag)) (:block/tags block)))] + (if (seq tags) + (str (:block/title block) + " " + (string/join + ", " + (keep (fn [tag] + (when-let [title (:block/title tag)] + (str "#" title))) + tags))) + (:block/title block)))) + (defn edit-block! [block pos & {:keys [_container-id custom-content tail-len save-code-editor?] :or {tail-len 0 diff --git a/src/main/frontend/handler/db_based/page.cljs b/src/main/frontend/handler/db_based/page.cljs index e12671abe8..d771036256 100644 --- a/src/main/frontend/handler/db_based/page.cljs +++ b/src/main/frontend/handler/db_based/page.cljs @@ -4,15 +4,16 @@ [frontend.db :as db] [frontend.handler.editor :as editor-handler] [frontend.handler.common.page :as page-common-handler] + [frontend.handler.db-based.property :as db-property-handler] [frontend.handler.notification :as notification] [frontend.state :as state] - [frontend.modules.outliner.ui :as ui-outliner-tx] [logseq.outliner.validate :as outliner-validate] [logseq.db.frontend.class :as db-class] [logseq.common.util :as common-util] [logseq.common.util.page-ref :as page-ref] [datascript.impl.entity :as de] - [promesa.core :as p])) + [promesa.core :as p] + [logseq.db])) (defn- valid-tag? "Returns a boolean indicating whether the new tag passes all valid checks. @@ -32,27 +33,23 @@ (throw e))))) (defn add-tag [repo block-id tag-entity] - (ui-outliner-tx/transact! - {:outliner-op :save-block} - (p/do! - (editor-handler/save-current-block!) - ;; Check after save-current-block to get most up to date block content - (when (valid-tag? repo (db/entity repo [:block/uuid block-id]) tag-entity) - (let [tx-data [[:db/add [:block/uuid block-id] :block/tags (:db/id tag-entity)] - ;; TODO: Move this to outliner.core to consistently add refs for tags - [:db/add [:block/uuid block-id] :block/refs (:db/id tag-entity)]]] - (db/transact! repo tx-data {:outliner-op :save-block})))))) + (p/do! + (editor-handler/save-current-block!) + ;; Check after save-current-block to get most up to date block content + (when (valid-tag? repo (db/entity repo [:block/uuid block-id]) tag-entity) + (db-property-handler/set-block-property! block-id :block/tags (:db/id tag-entity))))) (defn convert-to-tag! [page-entity] - (if (db/page-exists? (:block/title page-entity) "class") + (if (db/page-exists? (:block/title page-entity) #{:logseq.class/Tag}) (notification/show! (str "A tag with the name \"" (:block/title page-entity) "\" already exists.") :warning false) - (let [class (db-class/build-new-class (db/get-db) - {:db/id (:db/id page-entity) - :block/title (:block/title page-entity) - :block/created-at (:block/created-at page-entity)})] + (let [txs [(db-class/build-new-class (db/get-db) + {:db/id (:db/id page-entity) + :block/title (:block/title page-entity) + :block/created-at (:block/created-at page-entity)}) + [:db/retract (:db/id page-entity) :block/tags :logseq.class/Page]]] - (db/transact! (state/get-current-repo) [class] {:outliner-op :save-block})))) + (db/transact! (state/get-current-repo) txs {:outliner-op :save-block})))) (defn {:id (str (:db/id p)) - :label (title/block-unique-title p) + :label page-title :size size :color color :block/created-at (:block/created-at p)} @@ -72,8 +71,8 @@ (defn- normalize-page-name [{:keys [nodes links]}] (let [nodes' (->> (remove-uuids-and-files! nodes) - (util/distinct-by (fn [node] (:id node))) - (remove nil?))] + (util/distinct-by (fn [node] (:id node))) + (remove nil?))] {:nodes nodes' :links links})) @@ -207,15 +206,15 @@ (let [search-nodes (fn [forward?] (let [links (group-by (if forward? :source :target) links)] (loop [nodes nodes - level level] - (if (zero? level) - nodes - (recur (distinct (apply concat nodes - (map - (fn [id] - (->> (get links id) (map (if forward? :target :source)))) - nodes))) - (dec level)))))) + level level] + (if (zero? level) + nodes + (recur (distinct (apply concat nodes + (map + (fn [id] + (->> (get links id) (map (if forward? :target :source)))) + nodes))) + (dec level)))))) nodes (concat (search-nodes true) (search-nodes false)) nodes (set nodes)] (update graph :nodes diff --git a/src/main/frontend/handler/journal.cljs b/src/main/frontend/handler/journal.cljs index 8ea60a4d07..dfd2818093 100644 --- a/src/main/frontend/handler/journal.cljs +++ b/src/main/frontend/handler/journal.cljs @@ -15,7 +15,7 @@ (when (and page (state/enable-journals? (state/get-current-repo))) (p/do! (db-async/ {:on-mouse-over #(reset! control? true) :on-mouse-out #(reset! control? false)} title-trigger? (assoc :on-pointer-down on-pointer-down :class "cursor")) - [:div.flex.flex-row.items-center.ls-foldable-header + [:div.flex.flex-row.items-center.ls-foldable-header.gap-1 {:on-click (fn [^js e] (let [^js target (.-target e)] (when (some-> target (.closest ".as-toggle")) (reset! collapsed? (not @collapsed?)))))} (when-not (mobile-util/native-platform?) - [:a.block-control.opacity-50.hover:opacity-100.mr-2 - (cond-> - {:style {:width 14 - :height 16 - :margin-left -30}} - (not title-trigger?) - (assoc :on-pointer-down on-pointer-down)) - [:span {:class (if (or @control? @collapsed?) "control-show cursor-pointer" "control-hide")} - (rotating-arrow @collapsed?)]]) + (let [style {:width 14 :height 16}] + [:a.ls-foldable-title-control.block-control.opacity-50.hover:opacity-100 + (cond-> + {:style style} + (not title-trigger?) + (assoc :on-pointer-down on-pointer-down)) + [:span {:class (if (or @control? @collapsed?) "control-show cursor-pointer" "control-hide")} + (rotating-arrow @collapsed?)]])) (if (fn? header) (header @collapsed?) header)]]])) diff --git a/src/main/frontend/ui.css b/src/main/frontend/ui.css index 68d308ddec..2f98e6124b 100644 --- a/src/main/frontend/ui.css +++ b/src/main/frontend/ui.css @@ -329,4 +329,8 @@ input[type='range'] { .as-toggle { @apply opacity-60 cursor-pointer select-none active:opacity-50; } -} \ No newline at end of file +} + +.ls-foldable-title-control { + margin-left: -27px; +} diff --git a/src/main/frontend/worker/db/migrate.cljs b/src/main/frontend/worker/db/migrate.cljs index a52561f83b..13c82de276 100644 --- a/src/main/frontend/worker/db/migrate.cljs +++ b/src/main/frontend/worker/db/migrate.cljs @@ -417,6 +417,34 @@ :block/title title'}))))) datoms))) +(defn- replace-block-type-with-tags + [conn _search-db] + (let [db @conn + block-type-entity (d/entity db :block/type) + ;; Not using (d/datoms db :avet :block/type) here because some old graphs + ;; don't have :block/type indexed + datoms (->> (d/datoms db :eavt) + (filter (fn [d] (= :block/type (:a d))))) + journal-entity (d/entity db :logseq.class/Journal) + tx-data (mapcat (fn [{:keys [e _a v]}] + (let [tag (case v + "page" :logseq.class/Page + "class" :logseq.class/Tag + "property" :logseq.class/Property + "journal" :logseq.class/Journal + "whiteboard" :logseq.class/Whiteboard + "closed value" nil + (throw (ex-info "unsupported block/type" {:type v})))] + (cond-> + [[:db/retract e :block/type]] + (some? tag) + (conj [:db/add e :block/tags tag])))) datoms)] + (concat + ;; set journal's tag to `#Page` + [[:db/add (:db/id journal-entity) :block/tags :logseq.class/Page]] + tx-data + [[:db/retractEntity (:db/id block-type-entity)]]))) + (defn- deprecate-logseq-user-ns [conn _search-db] (let [db @conn] @@ -514,9 +542,10 @@ [47 {:fix replace-hidden-type-with-schema}] [48 {:properties [:logseq.property/default-value :logseq.property/scalar-default-value]}] [49 {:fix replace-special-id-ref-with-id-ref}] - [50 {:properties [:logseq.property.user/name :logseq.property.user/email :logseq.property.user/avatar]}] - [51 {:properties [:logseq.property.user/name :logseq.property.user/email :logseq.property.user/avatar] - :fix deprecate-logseq-user-ns}]]) + [50 {:properties [:logseq.property.user/name :logseq.property.user/email :logseq.property.user/avatar] + :fix deprecate-logseq-user-ns}] + [51 {:classes [:logseq.class/Property :logseq.class/Tag :logseq.class/Page :logseq.class/Whiteboard]}] + [52 {:fix replace-block-type-with-tags}]]) (let [max-schema-version (apply max (map first schema-version->updates))] (assert (<= db-schema/version max-schema-version)) @@ -534,17 +563,20 @@ (into {}) sqlite-create-graph/build-initial-properties* (map (fn [b] (assoc b :logseq.property/built-in? true)))) - new-classes (->> (select-keys db-class/built-in-classes classes) - ;; class already exists, this should never happen - (remove (fn [[k _]] - (when (d/entity db k) - (assert (str "DB migration: class already exists " k))))) + classes' (->> (concat [:logseq.class/Property :logseq.class/Tag :logseq.class/Page :logseq.class/Journal :logseq.class/Whiteboard] classes) + distinct) + new-classes (->> (select-keys db-class/built-in-classes classes') + ;; class already exists, this should never happen + (remove (fn [[k _]] (d/entity db k))) (into {}) (#(sqlite-create-graph/build-initial-classes* % (zipmap properties properties))) (map (fn [b] (assoc b :logseq.property/built-in? true)))) + new-class-idents (keep (fn [class] + (when-let [db-ident (:db/ident class)] + {:db/ident db-ident})) new-classes) fixes (when (fn? fix) (fix conn search-db)) - tx-data (if db-based? (concat new-properties new-classes fixes) fixes) + tx-data (if db-based? (concat new-class-idents new-properties new-classes fixes) fixes) tx-data' (concat [(sqlite-util/kv :logseq.kv/schema-version version)] tx-data)] @@ -555,30 +587,31 @@ "Migrate 'frontend' datascript schema and data. To add a new migration, add an entry to schema-version->updates and bump db-schema/version" [conn search-db] - (let [db @conn - version-in-db (or (:kv/value (d/entity db :logseq.kv/schema-version)) 0)] - (cond - (= version-in-db db-schema/version) - nil + (when (ldb/db-based-graph? @conn) + (let [db @conn + version-in-db (or (:kv/value (d/entity db :logseq.kv/schema-version)) 0)] + (cond + (= version-in-db db-schema/version) + nil - (< db-schema/version version-in-db) ; outdated client, db version could be synced from server + (< db-schema/version version-in-db) ; outdated client, db version could be synced from server ;; FIXME: notify users to upgrade to the latest version asap - nil + nil - (> db-schema/version version-in-db) - (try - (let [db-based? (ldb/db-based-graph? @conn) - updates (keep (fn [[v updates]] - (when (and (< version-in-db v) (<= v db-schema/version)) - [v updates])) - schema-version->updates)] - (println "DB schema migrated from" version-in-db) - (doseq [[v m] updates] - (upgrade-version! conn search-db db-based? v m))) - (catch :default e - (prn :error (str "DB migration failed to migrate to " db-schema/version " from " version-in-db ":")) - (js/console.error e) - (throw e)))))) + (> db-schema/version version-in-db) + (try + (let [db-based? (ldb/db-based-graph? @conn) + updates (keep (fn [[v updates]] + (when (and (< version-in-db v) (<= v db-schema/version)) + [v updates])) + schema-version->updates)] + (println "DB schema migrated from" version-in-db) + (doseq [[v m] updates] + (upgrade-version! conn search-db db-based? v m))) + (catch :default e + (prn :error (str "DB migration failed to migrate to " db-schema/version " from " version-in-db ":")) + (js/console.error e) + (throw e))))))) ;; Backend migrations ;; ================== diff --git a/src/main/frontend/worker/db_worker.cljs b/src/main/frontend/worker/db_worker.cljs index f79c696a17..a55d727ec7 100644 --- a/src/main/frontend/worker/db_worker.cljs +++ b/src/main/frontend/worker/db_worker.cljs @@ -318,7 +318,7 @@ (when-not db-based? (try - (when-not (ldb/page-exists? @conn common-config/views-page-name "page") + (when-not (ldb/page-exists? @conn common-config/views-page-name #{:logseq.class/Page}) (ldb/transact! conn (sqlite-create-graph/build-initial-views))) (catch :default _e))) diff --git a/src/main/frontend/worker/export.cljs b/src/main/frontend/worker/export.cljs index b037eb73fc..ed0088ac50 100644 --- a/src/main/frontend/worker/export.cljs +++ b/src/main/frontend/worker/export.cljs @@ -6,8 +6,7 @@ [logseq.graph-parser.property :as gp-property] [logseq.outliner.tree :as otree] [cljs-bean.core :as bean] - [logseq.db.sqlite.util :as sqlite-util] - [clojure.string :as string])) + [logseq.db.sqlite.util :as sqlite-util])) (defn- safe-keywordize [block] @@ -64,23 +63,17 @@ (:keys result))))) (group-by first) (mapcat (fn [[_id col]] - (let [type (some (fn [[_e a v _t]] - (when (= a :block/type) - v)) col) - ident (some (fn [[_e a v _t]] + (let [ident (some (fn [[_e a v _t]] (when (= a :db/ident) - v)) col)] + v)) col) + journal (some (fn [[_e a v _t]] + (when (= a :block/journal-day) + v)) col)] (map (fn [[e a v t]] (cond (and (contains? #{:block/title :block/name} a) - (or - ;; normal page or block - (not (contains? #{"class" "property" "journal" "closed value"} type)) - ;; class/property created by user - (and ident - (contains? #{"class" "property"} type) - (not (string/starts-with? (namespace ident) "logseq"))))) + (not (or ident journal))) [e a (str "debug " e) t] (= a :block/uuid) diff --git a/src/main/frontend/worker/handler/page.cljs b/src/main/frontend/worker/handler/page.cljs index bdb9e192d8..9ab11c0231 100644 --- a/src/main/frontend/worker/handler/page.cljs +++ b/src/main/frontend/worker/handler/page.cljs @@ -30,8 +30,8 @@ * :create-first-block? - when true, create an empty block if the page is empty. * :uuid - when set, use this uuid instead of generating a new one. - * :class? - when true, adds a :block/type 'class' - * :whiteboard? - when true, adds a :block/type 'whiteboard' + * :class? - when true, adds a :block/tags ':logseq.class/Tag' + * :whiteboard? - when true, adds a :block/tags ':logseq.class/Whiteboard' * :tags - tag uuids that are added to :block/tags * :persist-op? - when true, add an update-page op * :properties - properties to add to the page diff --git a/src/main/frontend/worker/handler/page/db_based/page.cljs b/src/main/frontend/worker/handler/page/db_based/page.cljs index 84b22f45f4..b29b014f89 100644 --- a/src/main/frontend/worker/handler/page/db_based/page.cljs +++ b/src/main/frontend/worker/handler/page/db_based/page.cljs @@ -13,30 +13,30 @@ [logseq.db.sqlite.util :as sqlite-util] [logseq.graph-parser.block :as gp-block] [logseq.graph-parser.text :as text] - [logseq.outliner.validate :as outliner-validate])) + [logseq.outliner.validate :as outliner-validate] + [logseq.db.frontend.entity-util :as entity-util] + [logseq.db.frontend.malli-schema :as db-malli-schema])) (defn- build-page-tx [conn properties page {:keys [whiteboard? class? tags]}] (when (:block/uuid page) - (let [page (assoc page :block/type (cond class? "class" - whiteboard? "whiteboard" - (:block/type page) (:block/type page) - :else "page")) - page' (cond-> page - (seq tags) - (update :block/tags - (fnil into []) - (mapv (fn [tag] - (let [v (if (uuid? tag) - (d/entity @conn [:block/uuid tag]) - tag)] - (cond - (de/entity? v) - (:db/id v) - (map? v) - (:db/id v) - :else - v))) - tags))) + (let [type-tag (cond class? :logseq.class/Tag + whiteboard? :logseq.class/Whiteboard + :else :logseq.class/Page) + tags' (if (:block/journal-day page) tags (conj tags type-tag)) + page' (update page :block/tags + (fnil into []) + (mapv (fn [tag] + (let [v (if (uuid? tag) + (d/entity @conn [:block/uuid tag]) + tag)] + (cond + (de/entity? v) + (:db/id v) + (map? v) + (:db/id v) + :else + v))) + tags')) property-vals-tx-m ;; Builds property values for built-in properties like logseq.property.pdf/file (db-property-build/build-property-values-tx-m @@ -95,10 +95,12 @@ (defn- split-namespace-pages [db page date-formatter] - (let [{:block/keys [title] block-uuid :block/uuid block-type :block/type} page] + (let [{:block/keys [title] block-uuid :block/uuid} page] (->> - (if (and (contains? #{"page" "class"} block-type) (ns-util/namespace-page? title)) - (let [class? (= block-type "class") + (if (and (or (entity-util/class? page) + (entity-util/page? page)) + (ns-util/namespace-page? title)) + (let [class? (entity-util/class? page) parts (->> (string/split title ns-util/parent-re) (map string/trim) (remove string/blank?)) @@ -168,15 +170,24 @@ (let [db @conn date-formatter (:logseq.property.journal/title-format (d/entity db :logseq.class/Journal)) title (sanitize-title title*) - type (cond class? - "class" - whiteboard? - "whiteboard" - today-journal? - "journal" - :else - "page")] - (when-not (ldb/page-exists? db title type) + types (cond class? + #{:logseq.class/Tag} + whiteboard? + #{:logseq.class/Whiteboard} + today-journal? + #{:logseq.class/Journal} + :else + #{:logseq.class/Page})] + (if-let [existing-page-id (first (ldb/page-exists? db title types))] + (let [existing-page (d/entity db existing-page-id) + tx-meta {:persist-op? persist-op? + :outliner-op :save-block}] + (when (and class? + (not (ldb/class? existing-page)) + (or (ldb/property? existing-page) (ldb/internal-page? existing-page))) + ;; Convert existing user property or page to class + (let [tx-data (db-class/build-new-class db (select-keys existing-page [:block/title :block/uuid :db/ident :block/created-at]))] + (ldb/transact! conn tx-data tx-meta)))) (let [format :markdown page (-> (gp-block/page-name->map title @conn true date-formatter {:class? class? @@ -189,9 +200,12 @@ (let [pages (split-namespace-pages db page date-formatter)] [(last pages) (butlast pages)]) [page nil])] - (when page + (when (and page (or (nil? (:db/ident page)) + ;; New page creation must not override built-in entities + (not (db-malli-schema/internal-ident? (:db/ident page))))) ;; Don't validate journal names because they can have '/' - (when (not= "journal" type) + (when-not (or (contains? types :logseq.class/Journal) + (contains? (set (:block/tags page)) :logseq.class/Journal)) (outliner-validate/validate-page-title-characters (str (:block/title page)) {:node page}) (doseq [parent parents] (outliner-validate/validate-page-title-characters (str (:block/title parent)) {:node parent}))) @@ -205,7 +219,8 @@ page-txs) (build-first-block-tx (:block/uuid (first page-txs)) format)) txs (concat - parents + ;; transact doesn't support entities + (remove de/entity? parents) page-txs first-block-tx)] (when (seq txs) diff --git a/src/main/frontend/worker/rtc/db_listener.cljs b/src/main/frontend/worker/rtc/db_listener.cljs index ca6a001a68..f7f388e707 100644 --- a/src/main/frontend/worker/rtc/db_listener.cljs +++ b/src/main/frontend/worker/rtc/db_listener.cljs @@ -22,7 +22,7 @@ (def ^:private watched-attrs #{:block/title :block/created-at :block/updated-at :block/alias - :block/tags :block/type :block/schema :block/link :block/journal-day + :block/tags :block/schema :block/link :block/journal-day :property/schema.classes :property.value/content :db/index :db/valueType :db/cardinality}) diff --git a/src/main/frontend/worker/rtc/remote_update.cljs b/src/main/frontend/worker/rtc/remote_update.cljs index 88509bc3fb..3aaf76d60b 100644 --- a/src/main/frontend/worker/rtc/remote_update.cljs +++ b/src/main/frontend/worker/rtc/remote_update.cljs @@ -366,7 +366,6 @@ :block/updated-at :block/created-at :block/alias - :block/type :block/schema :block/tags :block/link diff --git a/src/main/frontend/worker/search.cljs b/src/main/frontend/worker/search.cljs index 266359fc68..93ee92a251 100644 --- a/src/main/frontend/worker/search.cljs +++ b/src/main/frontend/worker/search.cljs @@ -332,13 +332,6 @@ DROP TRIGGER IF EXISTS blocks_au; (drop-tables-and-triggers! db) (create-tables-and-triggers! db)) -(comment - (defn- property-value-when-closed - "Returns property value if the given entity is type 'closed value' or nil" - [ent] - (when (= (:block/type ent) "closed value") - (:block/title ent)))) - (comment (defn- get-db-properties-str "Similar to db-pu/readable-properties but with a focus on making property values searchable" diff --git a/src/test/frontend/db/db_based_model_test.cljs b/src/test/frontend/db/db_based_model_test.cljs index c6f7c87a02..e66a7301f0 100644 --- a/src/test/frontend/db/db_based_model_test.cljs +++ b/src/test/frontend/db/db_based_model_test.cljs @@ -4,8 +4,10 @@ [frontend.db :as db] [frontend.test.helper :as test-helper] [datascript.core :as d] - [logseq.outliner.property :as outliner-property] - [logseq.db.frontend.class :as db-class])) + [logseq.db.frontend.class :as db-class] + [logseq.db :as ldb] + [logseq.db.test.helper :as db-test] + [frontend.db.conn :as conn])) (def repo test-helper/test-db-name-db-version) @@ -27,7 +29,9 @@ _ (test-helper/create-page! "class2" opts)] (is (= (set (concat - (map :title (vals db-class/built-in-classes)) + (map :title (vals (remove (fn [[ident _]] + (contains? ldb/private-tags ident)) + db-class/built-in-classes))) ["class1" "class2"])) (set (map :block/title (model/get-all-classes repo))))))) @@ -51,19 +55,16 @@ (:db/id (db/entity [:block/uuid sbid]))]))))) (deftest get-classes-with-property-test - (let [opts {:redirect? false :create-first-block? false :class? true} - _ (test-helper/create-page! "class1" opts) - _ (test-helper/create-page! "class2" opts) - class1 (db/get-case-page "class1") - class2 (db/get-case-page "class2") - conn (db/get-db false)] - (outliner-property/upsert-property! conn :user.property/property-1 {:type :node} {}) - (outliner-property/class-add-property! conn (:db/id class1) :user.property/property-1) - (outliner-property/class-add-property! conn (:db/id class2) :user.property/property-1) - (let [property (db/entity :user.property/property-1) - classes (model/get-classes-with-property (:db/ident property))] - (is (= (set (map :db/id classes)) - #{(:db/id class1) (:db/id class2)}))))) + (let [conn (db-test/create-conn-with-blocks + {:properties {:prop1 {:block/schema {:type :default}}} + :classes + {:Class1 {:build/schema-properties [:prop1]} + :Class2 {:build/schema-properties [:prop1]}}}) + property (d/entity @conn :user.property/prop1) + classes (with-redefs [conn/get-db (constantly @conn)] + (model/get-classes-with-property (:db/ident property)))] + (is (= ["Class1" "Class2"] + (map :block/title classes))))) (deftest hidden-page-test (let [opts {:redirect? false :create-first-block? false} diff --git a/src/test/frontend/worker/handler/page/db_based/page_test.cljs b/src/test/frontend/worker/handler/page/db_based/page_test.cljs index 68de2dabbf..e10cc71814 100644 --- a/src/test/frontend/worker/handler/page/db_based/page_test.cljs +++ b/src/test/frontend/worker/handler/page/db_based/page_test.cljs @@ -9,12 +9,8 @@ (let [conn (db-test/create-conn) _ (worker-db-page/create! conn "movie" {:class? true}) _ (worker-db-page/create! conn "Movie" {:class? true}) - movie-class (->> (d/q '[:find [(pull ?b [*]) ...] :in $ ?title :where [?b :block/title ?title]] - @conn "movie") - first) - Movie-class (->> (d/q '[:find [(pull ?b [*]) ...] :in $ ?title :where [?b :block/title ?title]] - @conn "Movie") - first)] + movie-class (ldb/get-case-page @conn "movie") + Movie-class (ldb/get-case-page @conn "Movie")] (is (ldb/class? movie-class) "Creates a class") (is (ldb/class? Movie-class) "Creates another class with a different case sensitive name") @@ -44,9 +40,12 @@ "Child class with new parent has correct parents") (worker-db-page/create! conn "foo/class1/baz3" {:split-namespace? true}) - (is (= #{"class" "page"} - (set (d/q '[:find [?type ...] - :where [?b :block/type ?type] [?b :block/title "class1"]] @conn))) + (is (= #{"Tag" "Page"} + (set (d/q '[:find [?tag-title ...] + :where + [?b :block/title "class1"] + [?b :block/tags ?t] + [?t :block/title ?tag-title]] @conn))) "Using an existing class page in a multi-parent namespace doesn't allow a page to have a class parent and instead creates a new page"))) (testing "Child pages with same name and different parents" @@ -79,10 +78,23 @@ (let [conn (db-test/create-conn) [_ page-uuid] (worker-db-page/create! conn "fooz" {})] (is (= "fooz" (:block/title (d/entity @conn [:block/uuid page-uuid]))) - "Valid page created") + "Page created correctly") (is (thrown-with-msg? js/Error #"can't include \"/" (worker-db-page/create! conn "foo/bar" {})) - "Page can't have '/'n title"))) \ No newline at end of file + "Page can't have '/'n title"))) + +(deftest create-journal + (let [conn (db-test/create-conn) + [_ page-uuid] (worker-db-page/create! conn "Dec 16th, 2024" {})] + + (is (= "Dec 16th, 2024" (:block/title (d/entity @conn [:block/uuid page-uuid]))) + "Journal created correctly") + + (is (= [:logseq.class/Journal] + (->> (d/entity @conn [:block/uuid page-uuid]) + :block/tags + (map #(:db/ident (d/entity @conn (:db/id %)))))) + "New journal only has Journal tag"))) \ No newline at end of file diff --git a/src/test/frontend/worker/rtc/db_listener_test.cljs b/src/test/frontend/worker/rtc/db_listener_test.cljs index b972fea07f..2fc8453250 100644 --- a/src/test/frontend/worker/rtc/db_listener_test.cljs +++ b/src/test/frontend/worker/rtc/db_listener_test.cljs @@ -9,16 +9,14 @@ [frontend.worker.rtc.db-listener :as subject] [frontend.worker.rtc.fixture :as r.fixture] [frontend.worker.state :as worker-state] - [logseq.db.frontend.schema :as db-schema] [logseq.outliner.batch-tx :as batch-tx] - [logseq.outliner.core :as outliner-core])) + [logseq.outliner.core :as outliner-core] + [logseq.db.test.helper :as db-test])) (t/use-fixtures :each test-helper/db-based-start-and-destroy-db-map-fixture r.fixture/listen-test-db-to-gen-rtc-ops-fixture) -(def empty-db (d/empty-db db-schema/schema-for-db-based-graph)) - (defn- tx-data=>e->a->add?->v->t [tx-data] (let [datom-vec-coll (map vec tx-data) @@ -27,11 +25,11 @@ (deftest entity-datoms=>ops-test (testing "remove whiteboard page-block" - (let [conn (d/conn-from-db empty-db) + (let [conn (db-test/create-conn) block-uuid (random-uuid) _create-whiteboard-page-block (d/transact! conn [{:block/uuid block-uuid - :block/type "whiteboard" + :block/tags :logseq.class/Whiteboard :block/name "block-name" :block/title "BLOCK-NAME"}]) remove-whiteboard-page-block @@ -44,20 +42,20 @@ (map (fn [[op-type _t op-value]] [op-type op-value]) r))))) (testing "update-schema op" - (let [conn (d/conn-from-db empty-db) - tx-data [[:db/add 69 :db/index true] - [:db/add 69 :block/uuid #uuid "66558abf-6512-469d-9e83-8f1ba0be9305"] - [:db/add 69 :db/valueType :db.type/ref] - [:db/add 69 :block/updated-at 1716882111476] - [:db/add 69 :block/created-at 1716882111476] - [:db/add 69 :block/schema {:type :number}] - [:db/add 69 :block/format :markdown] - [:db/add 69 :db/cardinality :db.cardinality/one] - [:db/add 69 :db/ident :user.property/qqq] - [:db/add 69 :block/type "property"] - [:db/add 69 :block/order "b0T"] - [:db/add 69 :block/name "qqq"] - [:db/add 69 :block/title "qqq"]] + (let [conn (db-test/create-conn) + tx-data [[:db/add 1000000 :db/index true] + [:db/add 1000000 :block/uuid #uuid "66558abf-6512-469d-9e83-8f1ba0be9305"] + [:db/add 1000000 :db/valueType :db.type/ref] + [:db/add 1000000 :block/updated-at 1716882111476] + [:db/add 1000000 :block/created-at 1716882111476] + [:db/add 1000000 :block/schema {:type :number}] + [:db/add 1000000 :block/format :markdown] + [:db/add 1000000 :db/cardinality :db.cardinality/one] + [:db/add 1000000 :db/ident :user.property/qqq] + [:db/add 1000000 :block/tags :logseq.class/Property] + [:db/add 1000000 :block/order "b0T"] + [:db/add 1000000 :block/name "qqq"] + [:db/add 1000000 :block/title "qqq"]] {:keys [db-before db-after tx-data]} (d/transact! conn tx-data) ops (#'subject/entity-datoms=>ops db-before db-after (tx-data=>e->a->add?->v->t tx-data) @@ -72,10 +70,11 @@ [:block/updated-at "[\"~#'\",1716882111476]"] [:block/created-at "[\"~#'\",1716882111476]"] [:block/schema "[\"^ \",\"~:type\",\"~:number\"]"] + [:block/tags #uuid "00000002-1038-7670-4800-000000000000"] [:block/title "[\"~#'\",\"qqq\"]"] [:db/cardinality "[\"~#'\",\"~:db.cardinality/one\"]"] ;; [:db/ident "[\"~#'\",\"~:user.property/qqq\"]"] - [:block/type "[\"~#'\",\"property\"]"]]}]] + ]}]] (map (fn [[op-type _t op-value]] [op-type (cond-> op-value (:av-coll op-value) @@ -83,16 +82,16 @@ ops))))) (testing "create user-class" - (let [conn (d/conn-from-db empty-db) - tx-data [[:db/add 62 :block/uuid #uuid "66856a29-6eb3-4122-af97-8580a853c6a6" 536870954] - [:db/add 62 :block/updated-at 1720019497643 536870954] - [:db/add 62 :logseq.property/parent 4 536870954] - [:db/add 62 :block/created-at 1720019497643 536870954] - [:db/add 62 :block/format :markdown 536870954] - [:db/add 62 :db/ident :user.class/zzz 536870954] - [:db/add 62 :block/type "class" 536870954] - [:db/add 62 :block/name "zzz" 536870954] - [:db/add 62 :block/title "zzz" 536870954]] + (let [conn (db-test/create-conn) + tx-data [[:db/add 1000000 :block/uuid #uuid "66856a29-6eb3-4122-af97-8580a853c6a6" 536870954] + [:db/add 1000000 :block/updated-at 1720019497643 536870954] + [:db/add 1000000 :logseq.property/parent :logseq.class/Root 536870954] + [:db/add 1000000 :block/created-at 1720019497643 536870954] + [:db/add 1000000 :block/format :markdown 536870954] + [:db/add 1000000 :db/ident :user.class/zzz 536870954] + [:db/add 1000000 :block/tags :logseq.class/Tag 536870954] + [:db/add 1000000 :block/name "zzz" 536870954] + [:db/add 1000000 :block/title "zzz" 536870954]] {:keys [db-before db-after tx-data]} (d/transact! conn tx-data) ops (#'subject/entity-datoms=>ops db-before db-after (tx-data=>e->a->add?->v->t tx-data) @@ -103,9 +102,9 @@ :av-coll [[:block/updated-at "[\"~#'\",1720019497643]"] [:block/created-at "[\"~#'\",1720019497643]"] + [:block/tags #uuid "00000002-5389-0208-3000-000000000000"] [:block/title "[\"~#'\",\"zzz\"]"] - [:block/type "[\"~#'\",\"class\"]"] - [:logseq.property/parent "[\"~#'\",4]"] + [:logseq.property/parent #uuid "00000002-2737-8382-7000-000000000000"] ;;1. shouldn't have :db/ident, :db/ident is special, will be handled later ]}]] (map (fn [[op-type _t op-value]] diff --git a/src/test/frontend/worker/rtc/remote_update_test.cljs b/src/test/frontend/worker/rtc/remote_update_test.cljs index ec9babf2cc..cca2a10e21 100644 --- a/src/test/frontend/worker/rtc/remote_update_test.cljs +++ b/src/test/frontend/worker/rtc/remote_update_test.cljs @@ -9,7 +9,7 @@ (deftest remote-op-value->tx-data-test (let [[block-uuid ref-uuid1 ref-uuid2] (repeatedly random-uuid) db (d/db-with (d/empty-db db-schema/schema-for-db-based-graph) - (sqlite-create-graph/build-db-initial-data ""))] + (sqlite-create-graph/build-db-initial-data "{}" {}))] (testing ":block/title" (let [db (d/db-with db [{:block/uuid block-uuid :block/title "local-content"}]) diff --git a/static/yarn.lock b/static/yarn.lock index fb7785c263..84cfff5102 100644 --- a/static/yarn.lock +++ b/static/yarn.lock @@ -1831,7 +1831,7 @@ electron-deeplink@1.0.10: electron-log "^4.2.3" node-addon-api "^2.0.0" -electron-devtools-installer@3.2.0: +electron-devtools-installer@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-3.2.0.tgz#acc48d24eb7033fe5af284a19667e73b78d406d0" integrity sha512-t3UczsYugm4OAbqvdImMCImIMVdFzJAHgbwHpkl5jmfu1izVgUcP/mnrPqJIpEeCK1uZGpt+yHgWEN+9EwoYhQ== @@ -4626,13 +4626,13 @@ stream-buffers@~2.2.0: strip-ansi "^6.0.1" string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3, string-width@^5.0.0, string-width@^5.1.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" + strip-ansi "^6.0.0" string_decoder@^1.1.1: version "1.3.0"