diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 28d8a3c50..773958039 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -352,10 +352,13 @@ [v] (and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray v)))) +;; TODO rename to decode-pgarray-into (defn decode-pgarray - ([v] (some->> ^PgArray v .getArray vec)) - ([v in] (some->> ^PgArray v .getArray (into in))) - ([v in xf] (some->> ^PgArray v .getArray (into in xf)))) + ([v] (decode-pgarray v [])) + ([v in] + (into in (some-> ^PgArray v .getArray))) + ([v in xf] + (into in xf (some-> ^PgArray v .getArray)))) (defn pgarray->set [v] diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index b78583c58..77171d345 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -254,6 +254,9 @@ {:name "0081-add-deleted-at-index-to-file-table" :fn (mg/resource "app/migrations/sql/0081-add-deleted-at-index-to-file-table.sql")} + {:name "0082-add-features-column-to-file-table" + :fn (mg/resource "app/migrations/sql/0082-add-features-column-to-file-table.sql")} + ]) diff --git a/backend/src/app/migrations/sql/0082-add-features-column-to-file-table.sql b/backend/src/app/migrations/sql/0082-add-features-column-to-file-table.sql new file mode 100644 index 000000000..3649daa4a --- /dev/null +++ b/backend/src/app/migrations/sql/0082-add-features-column-to-file-table.sql @@ -0,0 +1,2 @@ +ALTER TABLE file + ADD COLUMN features text[] DEFAULT NULL; diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj index 605df391e..3e87ef0cb 100644 --- a/backend/src/app/rpc/mutations/files.clj +++ b/backend/src/app/rpc/mutations/files.clj @@ -8,6 +8,8 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.files.features :as ffeat] + [app.common.logging :as l] [app.common.pages :as cp] [app.common.pages.migrations :as pmg] [app.common.spec :as us] @@ -24,6 +26,7 @@ [app.rpc.semaphore :as rsem] [app.storage.impl :as simpl] [app.util.blob :as blob] + [app.util.objects-map :as omap] [app.util.services :as sv] [app.util.time :as dt] [clojure.spec.alpha :as s] @@ -44,10 +47,11 @@ ;; --- Mutation: Create File +(s/def ::features ::us/set-of-strings) (s/def ::is-shared ::us/boolean) (s/def ::create-file (s/keys :req-un [::profile-id ::name ::project-id] - :opt-un [::id ::is-shared ::components-v2])) + :opt-un [::id ::is-shared ::features ::components-v2])) (sv/defmethod ::create-file [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] @@ -68,27 +72,37 @@ (defn create-file [conn {:keys [id name project-id is-shared data revn modified-at deleted-at ignore-sync-until - components-v2] + components-v2 features] :or {is-shared false revn 0} :as params}] - (let [id (or id (:id data) (uuid/next)) - data (or data (ctf/make-file-data id components-v2)) - file (db/insert! conn :file - (d/without-nils - {:id id - :project-id project-id - :name name - :revn revn - :is-shared is-shared - :data (blob/encode data) - :ignore-sync-until ignore-sync-until - :modified-at modified-at - :deleted-at deleted-at}))] + (let [id (or id (:id data) (uuid/next)) + + ;; BACKWARD COMPATIBILITY with the components-v2 param + features (cond-> (or features #{}) + components-v2 (conj "components/v2")) + + data (or data + (binding [ffeat/*current* features] + (ctf/make-file-data id))) + + features (db/create-array conn "text" features) + file (db/insert! conn :file + (d/without-nils + {:id id + :project-id project-id + :name name + :revn revn + :is-shared is-shared + :data (blob/encode data) + :features features + :ignore-sync-until ignore-sync-until + :modified-at modified-at + :deleted-at deleted-at}))] (->> (assoc params :file-id id :role :owner) (create-file-role conn)) - (assoc file :data data))) + (-> file files/decode-row))) ;; --- Mutation: Rename File @@ -309,24 +323,59 @@ (s/def ::update-file (s/and (s/keys :req-un [::id ::session-id ::profile-id ::revn] - :opt-un [::changes ::changes-with-metadata ::components-v2]) + :opt-un [::changes ::changes-with-metadata ::components-v2 ::features]) (fn [o] (or (contains? o :changes) (contains? o :changes-with-metadata))))) +(def ^:private sql:retrieve-file + "SELECT f.*, p.team_id + FROM file AS f + JOIN project AS p ON (p.id = f.project_id) + WHERE f.id = ? + AND (f.deleted_at IS NULL OR + f.deleted_at > now()) + FOR KEY SHARE") + (sv/defmethod ::update-file {::rsem/queue :update-file} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] + [{:keys [pool] :as cfg} {:keys [id profile-id components-v2] :as params}] (db/with-atomic [conn pool] (db/xact-lock! conn id) - (let [{:keys [id] :as file} (db/get-by-id conn :file id {:for-key-share true}) - team-id (retrieve-team-id conn (:project-id file))] - (files/check-edition-permissions! conn profile-id id) - (with-meta - (update-file (assoc cfg :conn conn) - (assoc params :file file)) - {::audit/props {:project-id (:project-id file) - :team-id team-id}})))) + (let [file (db/exec-one! conn [sql:retrieve-file id]) + features' (:features params #{}) + features (db/decode-pgarray (:features file) features') + + ;; BACKWARD COMPATIBILITY with the components-v2 parameter + features (cond-> features + components-v2 (conj "components/v2")) + + file (assoc file :features features)] + + (when-not file + (ex/raise :type :not-found + :code :object-not-found + :hint (format "file with id '%s' does not exists" id))) + + ;; If features are specified from params and the final feature + ;; set is different than the persisted one, update it on the + ;; database. + (when (not= features features') + (let [features (db/create-array conn "text" features)] + (db/update! conn :file + {:features features} + {:id id}))) + + (binding [ffeat/*current* features + ffeat/*wrap-objects-fn* (if (features "storate/objects-map") + omap/wrap + identity)] + (files/check-edition-permissions! conn profile-id (:id file)) + (with-meta + (update-file (assoc cfg :conn conn) + (assoc params :file file)) + {::audit/props {:project-id (:project-id file) + :team-id (:team-id file)}}))))) (defn- take-snapshot? "Defines the rule when file `data` snapshot should be saved." @@ -347,7 +396,7 @@ (defn- update-file [{:keys [conn metrics] :as cfg} - {:keys [file changes changes-with-metadata session-id profile-id components-v2] :as params}] + {:keys [file changes changes-with-metadata session-id profile-id] :as params}] (when (> (:revn params) (:revn file)) @@ -378,7 +427,8 @@ (assoc :id (:id file)) (pmg/migrate-data)) - components-v2 + + (contains? ffeat/*current* "components/v2") (ctf/migrate-to-components-v2) :always @@ -455,7 +505,8 @@ :changes changes}) (when (and (:is-shared file) (seq lchanges)) - (let [team-id (retrieve-team-id conn (:project-id file))] + (let [team-id (or (:team-id file) + (retrieve-team-id conn (:project-id file)))] ;; Asynchronously publish message to the msgbus (mbus/pub! msgbus :topic team-id diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 8ed02d1c8..57748559d 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -227,29 +227,34 @@ (d/index-by :object-id :data)))))) (defn retrieve-file - [{:keys [pool] :as cfg} id components-v2] + [{:keys [pool] :as cfg} id features] (let [file (->> (db/get-by-id pool :file id) (decode-row) (pmg/migrate-file))] - (if components-v2 + (if (contains? features "components/v2") (update file :data ctf/migrate-to-components-v2) - (if (get-in file [:data :options :components-v2]) + (if (dm/get-in file [:data :options :components-v2]) (ex/raise :type :restriction :code :feature-disabled - :hint "tried to open a components-v2 file with feature disabled") + :hint "tried to open a components/v2 file with feature disabled") file)))) +(s/def ::features ::us/set-of-strings) (s/def ::file (s/keys :req-un [::profile-id ::id] - :opt-un [::components-v2])) + :opt-un [::features ::components-v2])) (sv/defmethod ::file "Retrieve a file by its ID. Only authenticated users." - [{:keys [pool] :as cfg} {:keys [profile-id id components-v2] :as params}] - (let [perms (get-permissions pool profile-id id)] + [{:keys [pool] :as cfg} {:keys [profile-id id features components-v2] :as params}] + (let [perms (get-permissions pool profile-id id) + + ;; BACKWARD COMPATIBILTY with the components-v2 parameter + features (cond-> (or features #{}) + components-v2 (conj features "components/v2"))] (check-read-permissions! perms) - (let [file (retrieve-file cfg id components-v2) + (let [file (retrieve-file cfg id features) thumbs (retrieve-object-thumbnails cfg id)] (-> file (assoc :thumbnails thumbs) @@ -278,7 +283,7 @@ (s/def ::page (s/and (s/keys :req-un [::profile-id ::file-id] - :opt-un [::page-id ::object-id ::components-v2]) + :opt-un [::page-id ::object-id ::features ::components-v2]) (fn [obj] (if (contains? obj :object-id) (contains? obj :page-id) @@ -294,11 +299,15 @@ mandatory. Mainly used for rendering purposes." - [{:keys [pool] :as cfg} {:keys [profile-id file-id page-id object-id components-v2] :as props}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id page-id object-id features components-v2] :as props}] (check-read-permissions! pool profile-id file-id) - (let [file (retrieve-file cfg file-id components-v2) - page-id (or page-id (-> file :data :pages first)) - page (get-in file [:data :pages-index page-id])] + (let [;; BACKWARD COMPATIBILTY with the components-v2 parameter + features (cond-> (or features #{}) + components-v2 (conj features "components/v2")) + + file (retrieve-file cfg file-id features) + page-id (or page-id (-> file :data :pages first)) + page (dm/get-in file [:data :pages-index page-id])] (cond-> (prune-thumbnails page) (uuid? object-id) @@ -384,14 +393,17 @@ (s/def ::file-data-for-thumbnail (s/keys :req-un [::profile-id ::file-id] - :opt-un [::components-v2])) + :opt-un [::components-v2 ::features])) (sv/defmethod ::file-data-for-thumbnail "Retrieves the data for generate the thumbnail of the file. Used mainly for render thumbnails on dashboard." - [{:keys [pool] :as cfg} {:keys [profile-id file-id components-v2] :as props}] + [{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as props}] (check-read-permissions! pool profile-id file-id) - (let [file (retrieve-file cfg file-id components-v2)] + (let [;; BACKWARD COMPATIBILTY with the components-v2 parameter + features (cond-> (or features #{}) + components-v2 (conj features "components/v2")) + file (retrieve-file cfg file-id features)] {:file-id file-id :revn (:revn file) :page (get-file-thumbnail-data cfg file)})) @@ -567,8 +579,9 @@ ;; --- Helpers (defn decode-row - [{:keys [data changes] :as row}] + [{:keys [data changes features] :as row}] (when row (cond-> row - changes (assoc :changes (blob/decode changes)) - data (assoc :data (blob/decode data))))) + features (assoc :features (db/decode-pgarray features)) + changes (assoc :changes (blob/decode changes)) + data (assoc :data (blob/decode data))))) diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index 1eebabb9b..2e64986f4 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -23,8 +23,8 @@ (db/get-by-id pool :project id {:columns [:id :name :team-id]})) (defn- retrieve-bundle - [{:keys [pool] :as cfg} file-id profile-id components-v2] - (p/let [file (files/retrieve-file cfg file-id components-v2) + [{:keys [pool] :as cfg} file-id profile-id features] + (p/let [file (files/retrieve-file cfg file-id features) project (retrieve-project pool (:project-id file)) libs (files/retrieve-file-libraries cfg false file-id) users (comments/get-file-comments-users pool file-id profile-id) @@ -45,40 +45,49 @@ (s/def ::file-id ::us/uuid) (s/def ::profile-id ::us/uuid) (s/def ::share-id ::us/uuid) +(s/def ::features ::us/set-of-strings) + +;; TODO: deprecated, should be removed when version >= 1.18 (s/def ::components-v2 ::us/boolean) (s/def ::view-only-bundle - (s/keys :req-un [::file-id] :opt-un [::profile-id ::share-id ::components-v2])) + (s/keys :req-un [::file-id] + :opt-un [::profile-id ::share-id ::features ::components-v2])) (sv/defmethod ::view-only-bundle {:auth false} - [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id components-v2] :as params}] - (p/let [slink (slnk/retrieve-share-link pool file-id share-id) + [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id features components-v2] :as params}] + (p/let [;; BACKWARD COMPATIBILTY with the components-v2 parameter + features (cond-> (or features #{}) + components-v2 (conj features "components/v2")) + + slink (slnk/retrieve-share-link pool file-id share-id) perms (files/get-permissions pool profile-id file-id share-id) thumbs (files/retrieve-object-thumbnails cfg file-id) - bundle (p/-> (retrieve-bundle cfg file-id profile-id components-v2) + bundle (p/-> (retrieve-bundle cfg file-id profile-id features) (assoc :permissions perms) (assoc-in [:file :thumbnails] thumbs))] - ;; When we have neither profile nor share, we just return a not ;; found response to the user. - (when (and (not profile-id) - (not slink)) - (ex/raise :type :not-found - :code :object-not-found)) + (do + (when (and (not profile-id) + (not slink)) + (ex/raise :type :not-found + :code :object-not-found)) - ;; When we have only profile, we need to check read permissions - ;; on file. - (when (and profile-id (not slink)) - (files/check-read-permissions! pool profile-id file-id)) + ;; When we have only profile, we need to check read permissions + ;; on file. + (when (and profile-id (not slink)) + (files/check-read-permissions! pool profile-id file-id)) - (cond-> bundle - (some? slink) - (assoc :share slink) + (cond-> bundle + (some? slink) + (assoc :share slink) - (and (some? slink) + (and (some? slink) (not (contains? (:flags slink) "view-all-pages"))) - (update-in [:file :data] (fn [data] - (let [allowed-pages (:pages slink)] - (-> data - (update :pages (fn [pages] (filterv #(contains? allowed-pages %) pages))) - (update :pages-index (fn [index] (select-keys index allowed-pages)))))))))) + (update-in [:file :data] (fn [data] + (let [allowed-pages (:pages slink)] + (-> data + (update :pages (fn [pages] (filterv #(contains? allowed-pages %) pages))) + (update :pages-index (fn [index] (select-keys index allowed-pages))))))))))) + diff --git a/backend/test/app/test_helpers.clj b/backend/test/app/test_helpers.clj index 59ed7e40a..fea2b7075 100644 --- a/backend/test/app/test_helpers.clj +++ b/backend/test/app/test_helpers.clj @@ -148,38 +148,41 @@ (defn create-profile* ([i] (create-profile* *pool* i {})) ([i params] (create-profile* *pool* i params)) - ([conn i params] + ([pool i params] (let [params (merge {:id (mk-uuid "profile" i) :fullname (str "Profile " i) :email (str "profile" i ".test@nodomain.com") :password "123123" :is-demo false} params)] - (->> params - (cmd.auth/create-profile conn) - (cmd.auth/create-profile-relations conn))))) + (with-open [conn (db/open pool)] + (->> params + (cmd.auth/create-profile conn) + (cmd.auth/create-profile-relations conn)))))) (defn create-project* ([i params] (create-project* *pool* i params)) - ([conn i {:keys [profile-id team-id] :as params}] + ([pool i {:keys [profile-id team-id] :as params}] (us/assert uuid? profile-id) (us/assert uuid? team-id) - (->> (merge {:id (mk-uuid "project" i) - :name (str "project" i)} - params) - (#'projects/create-project conn)))) + (with-open [conn (db/open pool)] + (->> (merge {:id (mk-uuid "project" i) + :name (str "project" i)} + params) + (#'projects/create-project conn))))) (defn create-file* ([i params] (create-file* *pool* i params)) - ([conn i {:keys [profile-id project-id] :as params}] + ([pool i {:keys [profile-id project-id] :as params}] (us/assert uuid? profile-id) (us/assert uuid? project-id) - (#'files/create-file conn - (merge {:id (mk-uuid "file" i) - :name (str "file" i) - :components-v2 true} - params)))) + (with-open [conn (db/open pool)] + (#'files/create-file conn + (merge {:id (mk-uuid "file" i) + :name (str "file" i) + :components-v2 true} + params))))) (defn mark-file-deleted* ([params] (mark-file-deleted* *pool* params)) @@ -188,85 +191,95 @@ (defn create-team* ([i params] (create-team* *pool* i params)) - ([conn i {:keys [profile-id] :as params}] + ([pool i {:keys [profile-id] :as params}] (us/assert uuid? profile-id) - (let [id (mk-uuid "team" i)] - (teams/create-team conn {:id id - :profile-id profile-id - :name (str "team" i)})))) + (with-open [conn (db/open pool)] + (let [id (mk-uuid "team" i)] + (teams/create-team conn {:id id + :profile-id profile-id + :name (str "team" i)}))))) (defn create-file-media-object* ([params] (create-file-media-object* *pool* params)) - ([conn {:keys [name width height mtype file-id is-local media-id] + ([pool {:keys [name width height mtype file-id is-local media-id] :or {name "sample" width 100 height 100 mtype "image/svg+xml" is-local true}}] - (db/insert! conn :file-media-object - {:id (uuid/next) - :file-id file-id - :is-local is-local - :name name - :media-id media-id - :width width - :height height - :mtype mtype}))) + + (with-open [conn (db/open pool)] + (db/insert! conn :file-media-object + {:id (uuid/next) + :file-id file-id + :is-local is-local + :name name + :media-id media-id + :width width + :height height + :mtype mtype})))) (defn link-file-to-library* ([params] (link-file-to-library* *pool* params)) - ([conn {:keys [file-id library-id] :as params}] - (#'files/link-file-to-library conn {:file-id file-id :library-id library-id}))) + ([pool {:keys [file-id library-id] :as params}] + (with-open [conn (db/open pool)] + (#'files/link-file-to-library conn {:file-id file-id :library-id library-id})))) (defn create-complaint-for - [conn {:keys [id created-at type]}] - (db/insert! conn :profile-complaint-report - {:profile-id id - :created-at (or created-at (dt/now)) - :type (name type) - :content (db/tjson {})})) + [pool {:keys [id created-at type]}] + (with-open [conn (db/open pool)] + (db/insert! conn :profile-complaint-report + {:profile-id id + :created-at (or created-at (dt/now)) + :type (name type) + :content (db/tjson {})}))) (defn create-global-complaint-for - [conn {:keys [email type created-at]}] - (db/insert! conn :global-complaint-report - {:email email - :type (name type) - :created-at (or created-at (dt/now)) - :content (db/tjson {})})) + [pool {:keys [email type created-at]}] + (with-open [conn (db/open pool)] + (db/insert! conn :global-complaint-report + {:email email + :type (name type) + :created-at (or created-at (dt/now)) + :content (db/tjson {})}))) (defn create-team-role* ([params] (create-team-role* *pool* params)) - ([conn {:keys [team-id profile-id role] :or {role :owner}}] - (#'teams/create-team-role conn {:team-id team-id - :profile-id profile-id - :role role}))) + ([pool {:keys [team-id profile-id role] :or {role :owner}}] + (with-open [conn (db/open pool)] + (#'teams/create-team-role conn {:team-id team-id + :profile-id profile-id + :role role})))) (defn create-project-role* ([params] (create-project-role* *pool* params)) - ([conn {:keys [project-id profile-id role] :or {role :owner}}] - (#'projects/create-project-role conn {:project-id project-id - :profile-id profile-id - :role role}))) + ([pool {:keys [project-id profile-id role] :or {role :owner}}] + (with-open [conn (db/open pool)] + (#'projects/create-project-role conn {:project-id project-id + :profile-id profile-id + :role role})))) (defn create-file-role* ([params] (create-file-role* *pool* params)) - ([conn {:keys [file-id profile-id role] :or {role :owner}}] - (#'files/create-file-role conn {:file-id file-id - :profile-id profile-id - :role role}))) + ([pool {:keys [file-id profile-id role] :or {role :owner}}] + (with-open [conn (db/open pool)] + (#'files/create-file-role conn {:file-id file-id + :profile-id profile-id + :role role})))) (defn update-file* ([params] (update-file* *pool* params)) - ([conn {:keys [file-id changes session-id profile-id revn] + ([pool {:keys [file-id changes session-id profile-id revn] :or {session-id (uuid/next) revn 0}}] - (let [file (db/get-by-id conn :file file-id) - msgbus (:app.msgbus/msgbus *system*) - metrics (:app.metrics/metrics *system*)] - (#'files/update-file {:conn conn - :msgbus msgbus - :metrics metrics} - {:file file - :revn revn - :components-v2 true - :changes changes - :session-id session-id - :profile-id profile-id})))) + (with-open [conn (db/open pool)] + (let [file (db/get-by-id conn :file file-id) + msgbus (:app.msgbus/msgbus *system*) + metrics (:app.metrics/metrics *system*)] + (#'files/update-file {:conn conn + :msgbus msgbus + :metrics metrics} + {:file file + :revn revn + :components-v2 true + :changes changes + :session-id session-id + :profile-id profile-id}))))) ;; --- RPC HELPERS diff --git a/common/src/app/common/files/features.cljc b/common/src/app/common/files/features.cljc new file mode 100644 index 000000000..a51c2344f --- /dev/null +++ b/common/src/app/common/files/features.cljc @@ -0,0 +1,10 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.files.features) + +(def ^:dynamic *current* #{}) +(def ^:dynamic *wrap-objects-fn* identity) diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 1c7f580c5..227fdfa75 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -347,27 +347,12 @@ ;; -- Components (defmethod process-change :add-component - [data {:keys [id name path main-instance-id main-instance-page shapes]}] - (ctkl/add-component data - id - name - path - main-instance-id - main-instance-page - shapes)) + [data params] + (ctkl/add-component data params)) (defmethod process-change :mod-component - [data {:keys [id name path objects]}] - (update-in data [:components id] - #(cond-> % - (some? name) - (assoc :name name) - - (some? path) - (assoc :path path) - - (some? objects) - (assoc :objects objects)))) + [data params] + (ctkl/mod-component data params)) (defmethod process-change :del-component [data {:keys [id skip-undelete?]}] diff --git a/common/src/app/common/pages/changes_builder.cljc b/common/src/app/common/pages/changes_builder.cljc index b1d1b9166..985b87ed5 100644 --- a/common/src/app/common/pages/changes_builder.cljc +++ b/common/src/app/common/pages/changes_builder.cljc @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.features :as ffeat] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -50,10 +51,12 @@ (defn with-objects [changes objects] - (let [file-data (-> (ctf/make-file-data (uuid/next) uuid/zero true) - (assoc-in [:pages-index uuid/zero :objects] objects))] - (vary-meta changes assoc ::file-data file-data - ::applied-changes-count 0))) + (let [fdata (binding [ffeat/*current* #{"components/v2"}] + (ctf/make-file-data (uuid/next) uuid/zero)) + fdata (assoc-in fdata [:pages-index uuid/zero :objects] objects)] + (vary-meta changes assoc + ::file-data fdata + ::applied-changes-count 0))) (defn with-library-data [changes data] @@ -268,7 +271,7 @@ :page-id (::page-id (meta changes)) :parent-id (:parent-id shape) :shapes [(:id shape)] - :index idx})))] + :index idx})))] (-> changes (update :redo-changes conj set-parent-change) @@ -592,7 +595,7 @@ :main-instance-page main-instance-page :shapes new-shapes}) (into (map mk-change) updated-shapes)))) - (update :undo-changes + (update :undo-changes (fn [undo-changes] (-> undo-changes (d/preconj {:type :del-component diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 2614626a8..c352f981f 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -9,7 +9,7 @@ (defn instance-root? [shape] (some? (:component-id shape))) - + (defn instance-of? [shape file-id component-id] (and (some? (:component-id shape)) diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc index 64edcab1f..95abd7342 100644 --- a/common/src/app/common/types/components_list.cljc +++ b/common/src/app/common/types/components_list.cljc @@ -2,30 +2,52 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) UXBOX Labs SL +;; Copyright (c) KELEIDOS INC (ns app.common.types.components-list (:require - [app.common.data :as d])) + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.features :as feat])) (defn components-seq [file-data] (vals (:components file-data))) (defn add-component - [file-data id name path main-instance-id main-instance-page shapes] - (let [components-v2 (get-in file-data [:options :components-v2])] + [file-data {:keys [id name path main-instance-id main-instance-page shapes]}] + (let [components-v2 (dm/get-in file-data [:options :components-v2]) + wrap-object-fn feat/*wrap-objects-fn*] (cond-> file-data :always (assoc-in [:components id] {:id id :name name :path path - :objects (d/index-by :id shapes)}) + :objects (->> shapes + (d/index-by :id) + (wrap-object-fn))}) components-v2 - (update-in [:components id] assoc :main-instance-id main-instance-id - :main-instance-page main-instance-page)))) + (update-in [:components id] assoc + :main-instance-id main-instance-id + :main-instance-page main-instance-page)))) + +(defn mod-component + [file-data {:keys [id name path objects]}] + (let [wrap-objects-fn feat/*wrap-objects-fn*] + (update-in file-data [:components id] + (fn [component] + (let [objects (some-> objects wrap-objects-fn)] + (cond-> component + (some? name) + (assoc :name name) + + (some? path) + (assoc :path path) + + (some? objects) + (assoc :objects objects))))))) (defn get-component [file-data component-id] diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index 104fd0f34..0915a082e 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -6,6 +6,7 @@ (ns app.common.types.container (:require + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.spec :as us] @@ -41,8 +42,8 @@ (us/assert uuid? id) (-> (if (= type :page) - (get-in file [:pages-index id]) - (get-in file [:components id])) + (dm/get-in file [:pages-index id]) + (dm/get-in file [:components id])) (assoc :type type))) (defn get-shape diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 1ff989e50..a26333ffc 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -7,6 +7,8 @@ (ns app.common.types.file (:require [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.features :as ffeat] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.pages.common :refer [file-version]] @@ -65,16 +67,16 @@ :pages-index {}}) (defn make-file-data - ([file-id components-v2] - (make-file-data file-id (uuid/next) components-v2)) + ([file-id] + (make-file-data file-id (uuid/next))) - ([file-id page-id components-v2] + ([file-id page-id] (let [page (ctp/make-empty-page page-id "Page-1")] (cond-> (-> empty-file-data (assoc :id file-id) (ctpl/add-page page)) - components-v2 + (contains? ffeat/*current* "components/v2") (assoc-in [:options :components-v2] true))))) ;; Helpers @@ -108,7 +110,7 @@ ([libraries component-id] (some #(ctkl/get-component (:data %) component-id) (vals libraries))) ([libraries library-id component-id] - (ctkl/get-component (get-in libraries [library-id :data]) component-id))) + (ctkl/get-component (dm/get-in libraries [library-id :data]) component-id))) (defn delete-component "Delete a component and store it to be able to be recovered later. @@ -118,7 +120,7 @@ (delete-component file-data component-id false)) ([file-data component-id skip-undelete?] - (let [components-v2 (get-in file-data [:options :components-v2]) + (let [components-v2 (dm/get-in file-data [:options :components-v2]) add-to-deleted-components (fn [file-data] @@ -144,12 +146,12 @@ (defn get-deleted-component "Retrieve a component that has been deleted but still is in the safe store." [file-data component-id] - (get-in file-data [:deleted-components component-id])) + (dm/get-in file-data [:deleted-components component-id])) (defn restore-component "Recover a deleted component and put it again in place." [file-data component-id] - (let [component (-> (get-in file-data [:deleted-components component-id]) + (let [component (-> (dm/get-in file-data [:deleted-components component-id]) (dissoc :main-instance-x :main-instance-y))] (cond-> file-data (some? component) @@ -242,7 +244,7 @@ [file-data] (let [components (ctkl/components-seq file-data)] (if (or (empty? components) - (get-in file-data [:options :components-v2])) + (dm/get-in file-data [:options :components-v2])) (assoc-in file-data [:options :components-v2] true) (let [grid-gap 50 @@ -342,12 +344,12 @@ copy-component (fn [file-data] (ctkl/add-component file-data - (:id component) - (:name component) - (:path component) - (:id main-instance-shape) - page-id - (vals (:objects component)))) + {:id (:id component) + :name (:name component) + :path (:path component) + :main-instance-id (:id main-instance-shape) + :main-instance-page page-id + :shapes (vals (:objects component))})) ; Change all existing instances to point to the local file remap-instances @@ -500,10 +502,10 @@ component-file (when component-file-id (get libraries component-file-id nil)) component (when component-id (if component-file - (get-in component-file [:data :components component-id]) + (dm/get-in component-file [:data :components component-id]) (get components component-id))) component-shape (when (and component (:shape-ref shape)) - (get-in component [:objects (:shape-ref shape)]))] + (dm/get-in component [:objects (:shape-ref shape)]))] (str/format " %s--> %s%s%s" (cond (:component-root? shape) "#" (:component-id shape) "@" @@ -518,7 +520,7 @@ component-file-id (:component-file shape) component-file (when component-file-id (get libraries component-file-id nil)) component (if component-file - (get-in component-file [:data :components component-id]) + (dm/get-in component-file [:data :components component-id]) (get components component-id))] (str/format " (%s%s)" (when component-file (str/format "<%s> " (:name component-file))) diff --git a/common/src/app/common/types/page.cljc b/common/src/app/common/types/page.cljc index 975915ced..9c5594c76 100644 --- a/common/src/app/common/types/page.cljc +++ b/common/src/app/common/types/page.cljc @@ -7,6 +7,7 @@ (ns app.common.types.page (:require [app.common.data :as d] + [app.common.files.features :as ffeat] [app.common.types.page.flow :as ctpf] [app.common.types.page.grid :as ctpg] [app.common.types.page.guide :as ctpu] @@ -48,9 +49,11 @@ (defn make-empty-page [id name] - (assoc empty-page-data - :id id - :name name)) + (let [wrap-fn ffeat/*wrap-objects-fn*] + (-> empty-page-data + (assoc :id id) + (assoc :name name) + (update :objects wrap-fn)))) ;; --- Helpers for flow diff --git a/common/src/app/common/types/pages_list.cljc b/common/src/app/common/types/pages_list.cljc index b9394c476..777b6c072 100644 --- a/common/src/app/common/types/pages_list.cljc +++ b/common/src/app/common/types/pages_list.cljc @@ -2,32 +2,29 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) UXBOX Labs SL +;; Copyright (c) KALEIDOS INC (ns app.common.types.pages-list (:require - [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.pages.helpers :as cph])) (defn get-page [file-data id] - (get-in file-data [:pages-index id])) + (dm/get-in file-data [:pages-index id])) (defn add-page - [file-data page] - (let [index (:index page) - page (dissoc page :index) - - ; It's legitimate to add a page that is already there, - ; for example in an idempotent changes operation. - add-if-not-exists (fn [pages id] - (cond - (d/seek #(= % id) pages) pages - (nil? index) (conj pages id) - :else (cph/insert-at-index pages index [id])))] - (-> file-data - (update :pages add-if-not-exists (:id page)) - (update :pages-index assoc (:id page) page)))) + [file-data {:keys [id index] :as page}] + (-> file-data + ;; It's legitimate to add a page that is already there, for + ;; example in an idempotent changes operation. + (update :pages (fn [pages] + (let [exists? (some (partial = id) pages)] + (cond + exists? pages + (nil? index) (conj pages id) + :else (cph/insert-at-index pages index [id]))))) + (update :pages-index assoc id (dissoc page :index)))) (defn pages-seq [file-data] @@ -42,4 +39,3 @@ (-> file-data (update :pages (fn [pages] (filterv #(not= % page-id) pages))) (update :pages-index dissoc page-id))) - diff --git a/common/test/app/common/pages_test.cljc b/common/test/app/common/pages_test.cljc index 89264a93d..d9f5f2ee4 100644 --- a/common/test/app/common/pages_test.cljc +++ b/common/test/app/common/pages_test.cljc @@ -6,16 +6,22 @@ (ns app.common.pages-test (:require - [clojure.test :as t] - [clojure.pprint :refer [pprint]] + [app.common.files.features :as ffeat] [app.common.pages :as cp] [app.common.types.file :as ctf] - [app.common.uuid :as uuid])) + [app.common.uuid :as uuid] + [clojure.pprint :refer [pprint]] + [clojure.test :as t])) + +(defn- make-file-data + [file-id page-id] + (binding [ffeat/*current* #{"components/v2"}] + (ctf/make-file-data file-id page-id))) (t/deftest process-change-set-option (let [file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) - data (ctf/make-file-data file-id page-id true)] + data (make-file-data file-id page-id)] (t/testing "Sets option single" (let [chg {:type :set-option :page-id page-id @@ -81,7 +87,7 @@ (t/deftest process-change-add-obj (let [file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) - data (ctf/make-file-data file-id page-id true) + data (make-file-data file-id page-id) id-a (uuid/custom 2 1) id-b (uuid/custom 2 2) id-c (uuid/custom 2 3)] @@ -135,7 +141,7 @@ (t/deftest process-change-mod-obj (let [file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) - data (ctf/make-file-data file-id page-id true)] + data (make-file-data file-id page-id)] (t/testing "simple mod-obj" (let [chg {:type :mod-obj :page-id page-id @@ -162,7 +168,7 @@ (let [file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) id (uuid/custom 2 1) - data (ctf/make-file-data file-id page-id true) + data (make-file-data file-id page-id) data (-> data (assoc-in [:pages-index page-id :objects uuid/zero :shapes] [id]) (assoc-in [:pages-index page-id :objects id] @@ -206,7 +212,7 @@ file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) - data (ctf/make-file-data file-id page-id true) + data (make-file-data file-id page-id) data (update-in data [:pages-index page-id :objects] #(-> % @@ -450,7 +456,7 @@ :obj {:type :rect :name "Shape 3"}} ] - data (ctf/make-file-data file-id page-id true) + data (make-file-data file-id page-id) data (cp/process-changes data changes)] (t/testing "preserve order on multiple shape mov 1" @@ -557,7 +563,7 @@ :parent-id group-1-id :shapes [shape-1-id shape-2-id]}] - data (ctf/make-file-data file-id page-id true) + data (make-file-data file-id page-id) data (cp/process-changes data changes)] (t/testing "case 1" diff --git a/common/test/app/common/test_helpers/files.cljc b/common/test/app/common/test_helpers/files.cljc index 3c3302cba..1175907e8 100644 --- a/common/test/app/common/test_helpers/files.cljc +++ b/common/test/app/common/test_helpers/files.cljc @@ -6,16 +6,22 @@ (ns app.common.test-helpers.files (:require - [app.common.geom.point :as gpt] - [app.common.types.components-list :as ctkl] - [app.common.types.colors-list :as ctcl] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.types.pages-list :as ctpl] - [app.common.types.shape :as cts] - [app.common.types.shape-tree :as ctst] - [app.common.types.typographies-list :as ctyl] - [app.common.uuid :as uuid])) + [app.common.files.features :as ffeat] + [app.common.geom.point :as gpt] + [app.common.types.colors-list :as ctcl] + [app.common.types.components-list :as ctkl] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctst] + [app.common.types.typographies-list :as ctyl] + [app.common.uuid :as uuid])) + +(defn- make-file-data + [file-id page-id] + (binding [ffeat/*current* #{"components/v2"}] + (ctf/make-file-data file-id page-id))) (def ^:private idmap (atom {})) @@ -33,7 +39,7 @@ ([file-id page-id props] (merge {:id file-id :name (get props :name "File1") - :data (ctf/make-file-data file-id page-id true)} + :data (make-file-data file-id page-id)} props))) (defn sample-shape @@ -81,12 +87,12 @@ #(reduce (fn [page shape] (ctst/set-shape page shape)) % updated-shapes)) - (ctkl/add-component (:id component-shape) - (:name component-shape) - "" - shape-id - page-id - component-shapes)))))) + (ctkl/add-component {:id (:id component-shape) + :name (:name component-shape) + :path "" + :main-instance-id shape-id + :main-instance-page page-id + :shapes component-shapes})))))) (defn sample-instance [file label page-id library component-id] diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 35c1c6edd..8b3b682de 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -765,9 +765,14 @@ (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) - name (name (gensym (str (tr "dashboard.new-file-prefix") " "))) - components-v2 (features/active-feature? state :components-v2) - params (assoc params :name name :components-v2 components-v2)] + + name (name (gensym (str (tr "dashboard.new-file-prefix") " "))) + features (cond-> #{} + (features/active-feature? state :components-v2) + (conj "components/v2")) + params (-> params + (assoc :name name) + (assoc :features features))] (->> (rp/mutation! :create-file params) (rx/tap on-success) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index f5b2ec567..2d4ad1d25 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -7,6 +7,7 @@ (ns app.main.data.workspace.persistence (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.logging :as log] [app.common.pages :as cp] [app.common.pages.changes-spec :as pcs] @@ -137,14 +138,16 @@ (ptk/reify ::persist-changes ptk/WatchEvent (watch [_ state _] - (let [components-v2 (features/active-feature? state :components-v2) - sid (:session-id state) - file (get state :workspace-file) - params {:id (:id file) - :revn (:revn file) - :session-id sid - :changes-with-metadata (into [] changes) - :components-v2 components-v2}] + (let [features (cond-> #{} + (features/active-feature? state :components-v2) + (conj "components/v2")) + sid (:session-id state) + file (get state :workspace-file) + params {:id (:id file) + :revn (:revn file) + :session-id sid + :changes-with-metadata (into [] changes) + :features features}] (when (= file-id (:id params)) (->> (rp/mutation :update-file params) @@ -180,15 +183,17 @@ (ptk/reify ::persist-synchronous-changes ptk/WatchEvent (watch [_ state _] - (let [components-v2 (features/active-feature? state :components-v2) - sid (:session-id state) - file (get-in state [:workspace-libraries file-id]) + (let [features (cond-> #{} + (features/active-feature? state :components-v2) + (conj "components/v2")) + sid (:session-id state) + file (dm/get-in state [:workspace-libraries file-id]) params {:id (:id file) :revn (:revn file) :session-id sid :changes changes - :components-v2 components-v2}] + :features features}] (when (:id params) (->> (rp/mutation :update-file params) @@ -220,6 +225,9 @@ (ptk/reify ::changes-persisted ptk/UpdateEvent (update [_ state] + ;; NOTE: we don't set the file features context here because + ;; there are no useful context for code that need to be executed + ;; on the frontend side (let [changes (group-by :page-id changes)] (if (= file-id (:current-file-id state)) (-> state @@ -238,7 +246,6 @@ (update-in [:workspace-libraries file-id :data] cp/process-changes changes))))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Fetching & Uploading ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -278,8 +285,11 @@ ptk/WatchEvent (watch [_ state _] (let [share-id (-> state :viewer-local :share-id) - components-v2 (features/active-feature? state :components-v2)] - (->> (rx/zip (rp/query! :file-raw {:id file-id :components-v2 components-v2}) + features (cond-> #{} + (features/active-feature? state :components-v2) + (conj "components/v2"))] + + (->> (rx/zip (rp/query! :file-raw {:id file-id :features features}) (rp/query! :team-users {:file-id file-id}) (rp/query! :project {:id project-id}) (rp/query! :file-libraries {:file-id file-id}) diff --git a/frontend/src/app/main/features.cljs b/frontend/src/app/main/features.cljs index 3642edc94..c8126335e 100644 --- a/frontend/src/app/main/features.cljs +++ b/frontend/src/app/main/features.cljs @@ -72,4 +72,3 @@ (doseq [f features-list] (when (not= f :components-v2) (toggle-feature! f))))) -