diff --git a/CHANGES.md b/CHANGES.md index e06010181c..e428dc0015 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -56,6 +56,9 @@ - Workspace-palette items stay hidden when opening with keyboard-shortcut [Taiga #7489](https://tree.taiga.io/project/penpot/issue/7489) - Fix SVG attrs are not handled correctly when exporting/importing in .zip [Taiga #7920](https://tree.taiga.io/project/penpot/issue/7920) - Fix validation error when detaching with two nested copies and a swap [Taiga #8095](https://tree.taiga.io/project/penpot/issue/8095) +- Export shapes that are rotated act a bit strange when reimported [Taiga #7585](https://tree.taiga.io/project/penpot/issue/7585) +- Penpot crashes when a new colorpicker is created while uploading an image to another instance [Taiga #8119](https://tree.taiga.io/project/penpot/issue/8119) +- Removing Underline and Strikethrough Affects the Previous Text Object [Taiga #8103](https://tree.taiga.io/project/penpot/issue/8103) ## 2.0.3 diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index a8434a23a0..69f7eb7d00 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -32,6 +32,7 @@ [clojure.spec.alpha :as s] [cuerdas.core :as str] [integrant.core :as ig] + [ring.request :as rreq] [ring.response :as-alias rres])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -470,6 +471,9 @@ (some? (:invitation-token state)) (assoc :invitation-token (:invitation-token state)) + (some? (:external-session-id state)) + (assoc :external-session-id (:external-session-id state)) + ;; If state token comes with props, merge them. The state token ;; props can contain pm_ and utm_ prefixed query params. (map? (:props state)) @@ -554,19 +558,22 @@ (redirect-to-register cfg info request)) :else - (let [sxf (session/create-fn cfg (:id profile)) - token (or (:invitation-token info) - (tokens/generate (::setup/props cfg) - {:iss :auth - :exp (dt/in-future "15m") - :props (:props info) - :profile-id (:id profile)}))] + (let [sxf (session/create-fn cfg (:id profile)) + token (or (:invitation-token info) + (tokens/generate (::setup/props cfg) + {:iss :auth + :exp (dt/in-future "15m") + :props (:props info) + :profile-id (:id profile)})) + props (audit/profile->props profile) + context (d/without-nils {:external-session-id (:external-session-id info)})] (audit/submit! cfg {::audit/type "command" ::audit/name "login-with-oidc" ::audit/profile-id (:id profile) ::audit/ip-addr (audit/parse-client-ip request) - ::audit/props (audit/profile->props profile)}) + ::audit/props props + ::audit/context context}) (->> (redirect-to-verify-token token) (sxf request)))) @@ -588,9 +595,11 @@ (defn- auth-handler [cfg {:keys [params] :as request}] (let [props (audit/extract-utm-params params) + esid (rreq/get-header request "x-external-session-id") state (tokens/generate (::setup/props cfg) {:iss :oauth :invitation-token (:invitation-token params) + :external-session-id esid :props props :exp (dt/in-future "4h")}) uri (build-auth-uri cfg state)] diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 9abd4803fd..08167da873 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -86,6 +86,13 @@ (remove #(contains? reserved-props (key %)))) props)) +(defn params->context + "Extract default context properties from RPC params object" + [params] + (d/without-nils + {:external-session-id (::rpc/external-session-id params) + :triggered-by (::rpc/handler-name params)})) + ;; --- SPECS @@ -140,7 +147,7 @@ (::rpc/profile-id params) uuid/zero) - session-id (rreq/get-header request "x-external-session-id") + session-id (get params ::rpc/external-session-id) props (-> (or (::replace-props resultm) (-> params (merge (::props resultm)) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 7e963bb169..5a959d39b7 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -102,13 +102,13 @@ {::mdef/name "penpot_tasks_timing" ::mdef/help "Background tasks timing (milliseconds)." ::mdef/labels ["name"] - ::mdef/type :summary} + ::mdef/type :histogram} :redis-eval-timing {::mdef/name "penpot_redis_eval_timing" ::mdef/help "Redis EVAL commands execution timings (ms)" ::mdef/labels ["name"] - ::mdef/type :summary} + ::mdef/type :histogram} :rpc-climit-queue {::mdef/name "penpot_rpc_climit_queue" @@ -126,7 +126,7 @@ {::mdef/name "penpot_rpc_climit_timing" ::mdef/help "Summary of the time between queuing and executing on the CLIMIT" ::mdef/labels ["name"] - ::mdef/type :summary} + ::mdef/type :histogram} :audit-http-handler-queue-size {::mdef/name "penpot_audit_http_handler_queue_size" @@ -144,7 +144,7 @@ {::mdef/name "penpot_audit_http_handler_timing" ::mdef/help "Summary of the time between queuing and executing on the audit log http handler" ::mdef/labels [] - ::mdef/type :summary} + ::mdef/type :histogram} :executors-active-threads {::mdef/name "penpot_executors_active_threads" diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 89eee548d4..c2a85413d9 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -79,8 +79,12 @@ profile-id (or (::session/profile-id request) (::actoken/profile-id request)) + session-id (rreq/get-header request "x-external-session-id") + data (-> params + (assoc ::handler-name handler-name) (assoc ::request-at (dt/now)) + (assoc ::external-session-id session-id) (assoc ::session/id (::session/id request)) (assoc ::cond/key etag) (cond-> (uuid? profile-id) @@ -188,10 +192,10 @@ (defn- wrap-all [cfg f mdata] (as-> f $ - (wrap-metrics cfg $ mdata) (cond/wrap cfg $ mdata) (retry/wrap-retry cfg $ mdata) (climit/wrap cfg $ mdata) + (wrap-metrics cfg $ mdata) (rlimit/wrap cfg $ mdata) (wrap-audit cfg $ mdata) (wrap-spec-conform cfg $ mdata) diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj index 57c9d933d7..760e3bcd3a 100644 --- a/backend/src/app/rpc/commands/files_thumbnails.clj +++ b/backend/src/app/rpc/commands/files_thumbnails.clj @@ -321,18 +321,14 @@ (sv/defmethod ::delete-file-object-thumbnail {::doc/added "1.19" ::doc/module :files - ::doc/deprecated "1.20" - ::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id] - [:file-thumbnail-ops/global]] ::audit/skip true} [cfg {:keys [::rpc/profile-id file-id object-id]}] + (files/check-edition-permissions! cfg profile-id file-id) (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] - (files/check-edition-permissions! conn profile-id file-id) - (when-not (db/read-only? conn) - (-> cfg - (update ::sto/storage media/configure-assets-storage conn) - (delete-file-object-thumbnail! file-id object-id)) - nil)))) + (-> cfg + (update ::sto/storage media/configure-assets-storage conn) + (delete-file-object-thumbnail! file-id object-id)) + nil))) ;; --- MUTATION COMMAND: create-file-thumbnail diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index 5d01d9ec60..bf7883175c 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -16,6 +16,7 @@ [app.config :as cf] [app.db :as db] [app.http.sse :as sse] + [app.loggers.audit :as audit] [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] @@ -397,17 +398,32 @@ ;; --- COMMAND: Clone Template (defn- clone-template - [{:keys [::wrk/executor ::bf.v1/project-id] :as cfg} template] - (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + [cfg {:keys [project-id ::rpc/profile-id] :as params} template] + (db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}] ;; NOTE: the importation process performs some operations that ;; are not very friendly with virtual threads, and for avoid ;; unexpected blocking of other concurrent operations we ;; dispatch that operation to a dedicated executor. - (let [result (px/submit! executor (partial bf.v1/import-files! cfg template))] + (let [cfg (-> cfg + (assoc ::bf.v1/project-id project-id) + (assoc ::bf.v1/profile-id profile-id)) + result (px/invoke! executor (partial bf.v1/import-files! cfg template))] + (db/update! conn :project {:modified-at (dt/now)} {:id project-id}) - (deref result))))) + + (let [props (audit/clean-props params) + context (audit/params->context params)] + (doseq [file-id result] + (audit/submit! cfg + {::audit/type "action" + ::audit/name "create-file" + ::audit/profile-id profile-id + ::audit/props (assoc props :id file-id) + ::audit/context context}))) + + result)))) (def ^:private schema:clone-template @@ -425,16 +441,14 @@ [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}] (let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]}) _ (teams/check-edition-permissions! pool profile-id (:team-id project)) - template (tmpl/get-template-stream cfg template-id) - params (-> cfg - (assoc ::bf.v1/project-id (:id project)) - (assoc ::bf.v1/profile-id profile-id))] + template (tmpl/get-template-stream cfg template-id)] + (when-not template (ex/raise :type :not-found :code :template-not-found :hint "template not found")) - (sse/response #(clone-template params template)))) + (sse/response #(clone-template cfg params template)))) ;; --- COMMAND: Get list of builtin templates diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 8eb41386f3..ed018fa8e1 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -763,6 +763,7 @@ {:id (:id member)})) nil) + (let [id (uuid/next) expire (dt/in-future "168h") ;; 7 days invitation (db/exec-one! conn [sql:upsert-team-invitation id @@ -783,14 +784,19 @@ (when (contains? cf/flags :log-invitation-tokens) (l/info :hint "invitation token" :token itoken)) - (audit/submit! cfg - {::audit/type "action" - ::audit/name (if updated? - "update-team-invitation" - "create-team-invitation") - ::audit/profile-id (:id profile) - ::audit/props (-> (dissoc tprops :profile-id) - (d/without-nils))}) + + (let [props (-> (dissoc tprops :profile-id) + (audit/clean-props)) + context (audit/params->context params)] + + (audit/submit! cfg + {::audit/type "action" + ::audit/name (if updated? + "update-team-invitation" + "create-team-invitation") + ::audit/profile-id (:id profile) + ::audit/props props + ::audit/context context})) (eml/send! {::eml/conn conn ::eml/factory eml/invite-to-team @@ -850,10 +856,11 @@ ;; We don't re-send inviation to already existing members (remove (partial contains? members)) (map (fn [email] - {:email email - :team team - :profile profile - :role role})) + (-> params + (assoc :email email) + (assoc :team team) + (assoc :profile profile) + (assoc :role role)))) (keep (partial create-invitation cfg))) emails)] (with-meta {:total (count invitations) @@ -879,9 +886,11 @@ (let [features (-> (cfeat/get-enabled-features cf/flags) (cfeat/check-client-features! (:features params))) - params (assoc params - :profile-id profile-id - :features features) + + params (-> params + (assoc :profile-id profile-id) + (assoc :features features)) + cfg (assoc cfg ::db/conn conn) team (create-team cfg params) profile (db/get-by-id conn :profile profile-id) @@ -890,10 +899,11 @@ ;; Create invitations for all provided emails. (->> emails (map (fn [email] - {:team team - :profile profile - :email email - :role role})) + (-> params + (assoc :team team) + (assoc :profile profile) + (assoc :email email) + (assoc :role role)))) (run! (partial create-invitation cfg))) (run! (partial quotes/check-quote! conn) diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 0540cfcdfd..12e927ac0f 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -801,15 +801,6 @@ (update :undo-changes conj {:type :del-component :id id :main-instance main-instance}))) -(defn ignore-remote - [changes] - (letfn [(add-ignore-remote - [change-list] - (->> change-list - (mapv #(assoc % :ignore-remote? true))))] - (-> changes - (update :redo-changes add-ignore-remote) - (update :undo-changes add-ignore-remote)))) (defn reorder-grid-children [changes ids] diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 1a4ed092a8..e22353e750 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -473,9 +473,14 @@ (defn setup-rect "Initializes the selrect and points for a shape." - [{:keys [selrect points] :as shape}] - (let [selrect (or selrect (gsh/shape->rect shape)) - points (or points (grc/rect->points selrect))] + [{:keys [selrect points transform] :as shape}] + (let [selrect (or selrect (gsh/shape->rect shape)) + center (grc/rect->center selrect) + transform (or transform (gmt/matrix)) + points (or points + (-> selrect + (grc/rect->points) + (gsh/transform-points center transform)))] (-> shape (assoc :selrect selrect) (assoc :points points)))) diff --git a/frontend/.gitignore b/frontend/.gitignore index 8d2f604e12..dd3776ebd3 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -10,5 +10,4 @@ node_modules/ /playwright-report/ /blob-report/ /playwright/.cache/ -visual-dashboard.spec.js-snapshots - +/playwright/**/visual-specs/**/*.png diff --git a/frontend/playwright/data/assets/get-file-fragment-with-assets-components.json b/frontend/playwright/data/assets/get-file-fragment-with-assets-components.json new file mode 100644 index 0000000000..4f8cfb6300 --- /dev/null +++ b/frontend/playwright/data/assets/get-file-fragment-with-assets-components.json @@ -0,0 +1,31 @@ +{ + "~:id": "~u015fda4f-caa6-8103-8004-862a9e4b4d4b", + "~:file-id": "~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:created-at": "~m1718718436639", + "~:content": { + "~ue117f7f6-433c-807e-8004-862a38e1823d": { + "~:id": "~ue117f7f6-433c-807e-8004-862a38e1823d", + "~:name": "Button", + "~:path": "", + "~:modified-at": "~m1718718335855", + "~:main-instance-id": "~ue117f7f6-433c-807e-8004-862a38e0099a", + "~:main-instance-page": "~u015fda4f-caa6-8103-8004-862a00ddbe94" + }, + "~ue117f7f6-433c-807e-8004-862a51a90ef5": { + "~:id": "~ue117f7f6-433c-807e-8004-862a51a90ef5", + "~:name": "Badge", + "~:path": "", + "~:modified-at": "~m1718718361245", + "~:main-instance-id": "~ue117f7f6-433c-807e-8004-862a51a84a91", + "~:main-instance-page": "~u015fda4f-caa6-8103-8004-862a00ddbe94" + }, + "~ue117f7f6-433c-807e-8004-862a9b541a46": { + "~:id": "~ue117f7f6-433c-807e-8004-862a9b541a46", + "~:name": "Avatar", + "~:path": "", + "~:modified-at": "~m1718718436652", + "~:main-instance-id": "~ue117f7f6-433c-807e-8004-862a9b5374b6", + "~:main-instance-page": "~u015fda4f-caa6-8103-8004-862a00ddbe94" + } + } +} \ No newline at end of file diff --git a/frontend/playwright/data/assets/get-file-fragmnet-with-assets-page.json b/frontend/playwright/data/assets/get-file-fragmnet-with-assets-page.json new file mode 100644 index 0000000000..99e01ce342 --- /dev/null +++ b/frontend/playwright/data/assets/get-file-fragmnet-with-assets-page.json @@ -0,0 +1,630 @@ +{ + "~:id": "~u015fda4f-caa6-8103-8004-862a9e4ad279", + "~:file-id": "~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:created-at": "~m1718718436639", + "~:content": { + "~:options": {}, + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1.0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~ue117f7f6-433c-807e-8004-862a38e0099a", + "~ue117f7f6-433c-807e-8004-862a51a84a91", + "~ue117f7f6-433c-807e-8004-862a9b5374b6" + ] + } + }, + "~ue117f7f6-433c-807e-8004-862a18bba46f": { + "~#shape": { + "~:y": 220, + "~:rx": 0, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Button", + "~:width": 120, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 663, + "~:y": 220 + } + }, + { + "~#point": { + "~:x": 783, + "~:y": 220 + } + }, + { + "~#point": { + "~:x": 783, + "~:y": 274 + } + }, + { + "~#point": { + "~:x": 663, + "~:y": 274 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:constraints-v": "~:scale", + "~:constraints-h": "~:scale", + "~:id": "~ue117f7f6-433c-807e-8004-862a18bba46f", + "~:parent-id": "~ue117f7f6-433c-807e-8004-862a38e0099a", + "~:frame-id": "~ue117f7f6-433c-807e-8004-862a38e0099a", + "~:strokes": [], + "~:x": 663, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 663, + "~:y": 220, + "~:width": 120, + "~:height": 54, + "~:x1": 663, + "~:y1": 220, + "~:x2": 783, + "~:y2": 274 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:ry": 0, + "~:height": 54, + "~:flip-y": null + } + }, + "~ue117f7f6-433c-807e-8004-862a38e0099a": { + "~#shape": { + "~:y": 220, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": true, + "~:name": "Button", + "~:width": 120, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 663, + "~:y": 220 + } + }, + { + "~#point": { + "~:x": 783, + "~:y": 220 + } + }, + { + "~#point": { + "~:x": 783, + "~:y": 274 + } + }, + { + "~#point": { + "~:x": 663, + "~:y": 274 + } + } + ], + "~:component-root": true, + "~:show-content": true, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~ue117f7f6-433c-807e-8004-862a38e0099a", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:component-id": "~ue117f7f6-433c-807e-8004-862a38e1823d", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 663, + "~:main-instance": true, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 663, + "~:y": 220, + "~:width": 120, + "~:height": 54, + "~:x1": 663, + "~:y1": 220, + "~:x2": 783, + "~:y2": 274 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": 54, + "~:component-file": "~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:flip-y": null, + "~:shapes": [ + "~ue117f7f6-433c-807e-8004-862a18bba46f" + ] + } + }, + "~ue117f7f6-433c-807e-8004-862a40b7caca": { + "~#shape": { + "~:y": 188, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Badge", + "~:width": 61, + "~:type": "~:circle", + "~:points": [ + { + "~#point": { + "~:x": 860, + "~:y": 188 + } + }, + { + "~#point": { + "~:x": 921, + "~:y": 188 + } + }, + { + "~#point": { + "~:x": 921, + "~:y": 247 + } + }, + { + "~#point": { + "~:x": 860, + "~:y": 247 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:constraints-v": "~:scale", + "~:constraints-h": "~:scale", + "~:id": "~ue117f7f6-433c-807e-8004-862a40b7caca", + "~:parent-id": "~ue117f7f6-433c-807e-8004-862a51a84a91", + "~:frame-id": "~ue117f7f6-433c-807e-8004-862a51a84a91", + "~:strokes": [], + "~:x": 860, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 860, + "~:y": 188, + "~:width": 61, + "~:height": 59, + "~:x1": 860, + "~:y1": 188, + "~:x2": 921, + "~:y2": 247 + } + }, + "~:fills": [ + { + "~:fill-color": "#7798ff", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 59, + "~:flip-y": null + } + }, + "~ue117f7f6-433c-807e-8004-862a51a84a91": { + "~#shape": { + "~:y": 188, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": true, + "~:name": "Badge", + "~:width": 61, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 860, + "~:y": 188 + } + }, + { + "~#point": { + "~:x": 921, + "~:y": 188 + } + }, + { + "~#point": { + "~:x": 921, + "~:y": 247 + } + }, + { + "~#point": { + "~:x": 860, + "~:y": 247 + } + } + ], + "~:component-root": true, + "~:show-content": true, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~ue117f7f6-433c-807e-8004-862a51a84a91", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:component-id": "~ue117f7f6-433c-807e-8004-862a51a90ef5", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 860, + "~:main-instance": true, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 860, + "~:y": 188, + "~:width": 61, + "~:height": 59, + "~:x1": 860, + "~:y1": 188, + "~:x2": 921, + "~:y2": 247 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": 59, + "~:component-file": "~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:flip-y": null, + "~:shapes": [ + "~ue117f7f6-433c-807e-8004-862a40b7caca" + ] + } + }, + "~ue117f7f6-433c-807e-8004-862a8c166257": { + "~#shape": { + "~:y": 97, + "~:rx": 0, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Avatar", + "~:width": 66, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 554, + "~:y": 97 + } + }, + { + "~#point": { + "~:x": 620, + "~:y": 97 + } + }, + { + "~#point": { + "~:x": 620, + "~:y": 163 + } + }, + { + "~#point": { + "~:x": 554, + "~:y": 163 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:constraints-v": "~:scale", + "~:constraints-h": "~:scale", + "~:id": "~ue117f7f6-433c-807e-8004-862a8c166257", + "~:parent-id": "~ue117f7f6-433c-807e-8004-862a9b5374b6", + "~:frame-id": "~ue117f7f6-433c-807e-8004-862a9b5374b6", + "~:strokes": [], + "~:x": 554, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 554, + "~:y": 97, + "~:width": 66, + "~:height": 66, + "~:x1": 554, + "~:y1": 97, + "~:x2": 620, + "~:y2": 163 + } + }, + "~:fills": [ + { + "~:fill-color": "#ff6ffc", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:ry": 0, + "~:height": 66, + "~:flip-y": null + } + }, + "~ue117f7f6-433c-807e-8004-862a9b5374b6": { + "~#shape": { + "~:y": 97, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": true, + "~:name": "Avatar", + "~:width": 66, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 554, + "~:y": 97 + } + }, + { + "~#point": { + "~:x": 620, + "~:y": 97 + } + }, + { + "~#point": { + "~:x": 620, + "~:y": 163 + } + }, + { + "~#point": { + "~:x": 554, + "~:y": 163 + } + } + ], + "~:component-root": true, + "~:show-content": true, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~ue117f7f6-433c-807e-8004-862a9b5374b6", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:component-id": "~ue117f7f6-433c-807e-8004-862a9b541a46", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 554, + "~:main-instance": true, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 554, + "~:y": 97, + "~:width": 66, + "~:height": 66, + "~:x1": 554, + "~:y1": 97, + "~:x2": 620, + "~:y2": 163 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": 66, + "~:component-file": "~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:flip-y": null, + "~:shapes": [ + "~ue117f7f6-433c-807e-8004-862a8c166257" + ] + } + } + }, + "~:id": "~u015fda4f-caa6-8103-8004-862a00ddbe94", + "~:name": "Page 1" + } +} \ No newline at end of file diff --git a/frontend/playwright/data/assets/get-file-with-assets.json b/frontend/playwright/data/assets/get-file-with-assets.json new file mode 100644 index 0000000000..29758d1a09 --- /dev/null +++ b/frontend/playwright/data/assets/get-file-with-assets.json @@ -0,0 +1,105 @@ +{ + "~:features":{ + "~#set":[ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions":{ + "~:type":"~:membership", + "~:is-owner":true, + "~:is-admin":true, + "~:can-edit":true, + "~:can-read":true, + "~:is-logged":true + }, + "~:has-media-trimmed":false, + "~:comment-thread-seqn":0, + "~:name":"Lorem ipsum", + "~:revn":14, + "~:modified-at":"~m1718718464651", + "~:id":"~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:is-shared":false, + "~:version":49, + "~:project-id":"~u0515a066-e303-8169-8004-73eb401b5d55", + "~:created-at":"~m1718718275492", + "~:data":{ + "~:colors":{ + "~ue117f7f6-433c-807e-8004-862aa7732f9c":{ + "~:path":"", + "~:color":"#ff6ffc", + "~:name":"Rosita", + "~:modified-at":"~m1718718452317", + "~:opacity":1, + "~:id":"~ue117f7f6-433c-807e-8004-862aa7732f9c" + }, + "~ue117f7f6-433c-807e-8004-862ab306fa2b":{ + "~:path":"", + "~:color":"#7798ff", + "~:name":"#7798ff", + "~:modified-at":"~m1718718461420", + "~:opacity":1, + "~:id":"~ue117f7f6-433c-807e-8004-862ab306fa2b" + } + }, + "~:typographies":{ + "~ue117f7f6-433c-807e-8004-862ab6ae29d8":{ + "~:line-height":"1.2", + "~:font-style":"normal", + "~:text-transform":"none", + "~:font-id":"sourcesanspro", + "~:font-size":"14", + "~:font-weight":"400", + "~:name":"Source Sans Pro Regular", + "~:modified-at":"~m1718718464655", + "~:font-variant-id":"regular", + "~:id":"~ue117f7f6-433c-807e-8004-862ab6ae29d8", + "~:letter-spacing":"0", + "~:font-family":"sourcesanspro" + } + }, + "~:pages":[ + "~u015fda4f-caa6-8103-8004-862a00ddbe94" + ], + "~:components":{ + "~#penpot/pointer":[ + "~u015fda4f-caa6-8103-8004-862a9e4b4d4b", + { + "~:created-at":"~m1718718436653" + } + ] + }, + "~:id":"~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:options":{ + "~:components-v2":true + }, + "~:recent-colors":[ + { + "~:color":"#b5b1b4", + "~:opacity":1 + }, + { + "~:color":"#ff6ffc", + "~:opacity":1 + }, + { + "~:color":"#7798ff", + "~:opacity":1 + } + ], + "~:pages-index":{ + "~u015fda4f-caa6-8103-8004-862a00ddbe94":{ + "~#penpot/pointer":[ + "~u015fda4f-caa6-8103-8004-862a9e4ad279", + { + "~:created-at":"~m1718718436653" + } + ] + } + } + } +} \ No newline at end of file diff --git a/frontend/playwright/ui/pages/LoginPage.js b/frontend/playwright/ui/pages/LoginPage.js index 5e94c10caa..9657197145 100644 --- a/frontend/playwright/ui/pages/LoginPage.js +++ b/frontend/playwright/ui/pages/LoginPage.js @@ -1,10 +1,6 @@ import { BasePage } from "./BasePage"; export class LoginPage extends BasePage { - static async initWithLoggedOutUser(page) { - await BasePage.mockRPC(page, "get-profile", "get-profile-anonymous.json"); - } - constructor(page) { super(page); this.loginButton = page.getByRole("button", { name: "Login" }); @@ -24,6 +20,10 @@ export class LoginPage extends BasePage { await this.loginButton.click(); } + async initWithLoggedOutUser() { + await this.mockRPC("get-profile", "get-profile-anonymous.json"); + } + async setupLoggedInUser() { await this.mockRPC("get-profile", "logged-in-user/get-profile-logged-in.json"); await this.mockRPC("get-teams", "logged-in-user/get-teams-default.json"); diff --git a/frontend/playwright/ui/pages/ViewerPage.js b/frontend/playwright/ui/pages/ViewerPage.js index a7e377a889..fa06226412 100644 --- a/frontend/playwright/ui/pages/ViewerPage.js +++ b/frontend/playwright/ui/pages/ViewerPage.js @@ -27,6 +27,26 @@ export class ViewerPage extends BaseWebSocketPage { ); } + async setupFileWithSingleBoard() { + await this.mockRPC(/get\-view\-only\-bundle\?/, "viewer/get-view-only-bundle-single-board.json"); + await this.mockRPC("get-comment-threads?file-id=*", "workspace/get-comment-threads-empty.json"); + await this.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "viewer/get-file-fragment-single-board.json", + ); + }; + + async setupFileWithComments() { + await this.mockRPC(/get\-view\-only\-bundle\?/, "viewer/get-view-only-bundle-single-board.json"); + await this.mockRPC("get-comment-threads?file-id=*", "workspace/get-comment-threads-not-empty.json"); + await this.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "viewer/get-file-fragment-single-board.json", + ); + await this.mockRPC("get-comments?thread-id=*", "workspace/get-thread-comments.json"); + await this.mockRPC("update-comment-thread-status", "workspace/update-comment-thread-status.json"); + }; + #ws = null; constructor(page) { @@ -56,5 +76,11 @@ export class ViewerPage extends BaseWebSocketPage { .filter({ hasText: number.toString() }) .click(clickOptions); } + + async showCode(clickOptions = {}) { + await this.page + .getByRole("button", { name: 'Inspect (G I)' }) + .click(clickOptions); + } } diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index c227f09b66..6e6c8b597e 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -43,13 +43,14 @@ export class WorkspacePage extends BaseWebSocketPage { this.presentUserListItems = page.getByTestId("active-users-list").getByAltText("Princesa Leia"); this.viewport = page.getByTestId("viewport"); this.rootShape = page.locator(`[id="shape-00000000-0000-0000-0000-000000000000"]`); - this.toolbarOptions = page.getByTestId("toolbar-options"); + this.toolbarOptions = page.getByTestId("toolbar-options"); this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" }); this.toggleToolbarButton = page.getByRole("button", { name: "Toggle toolbar" }); this.colorpicker = page.getByTestId("colorpicker"); this.layers = page.getByTestId("layer-tree"); this.palette = page.getByTestId("palette"); this.sidebar = page.getByTestId("left-sidebar"); + this.rightSidebar = page.getByTestId("right-sidebar"); this.selectionRect = page.getByTestId("workspace-selection-rect"); this.horizontalScrollbar = page.getByTestId("horizontal-scrollbar"); this.librariesModal = page.getByTestId("libraries-modal"); @@ -119,7 +120,7 @@ export class WorkspacePage extends BaseWebSocketPage { } async moveSelectionToShape(name) { - await this.page.locator('rect.viewport-selrect').hover(); + await this.page.locator("rect.viewport-selrect").hover(); await this.page.mouse.down(); await this.viewport.getByTestId(name).first().hover({ force: true }); await this.page.mouse.up(); @@ -170,9 +171,7 @@ export class WorkspacePage extends BaseWebSocketPage { } async clickColorPalette(clickOptions = {}) { - await this.palette - .getByRole("button", { name: "Color Palette (Alt+P)" }) - .click(clickOptions); + await this.palette.getByRole("button", { name: "Color Palette (Alt+P)" }).click(clickOptions); } async clickTogglePalettesVisibility(clickOptions = {}) { diff --git a/frontend/playwright/ui/specs/login.spec.js b/frontend/playwright/ui/specs/login.spec.js index dd259cf772..d730fb91e4 100644 --- a/frontend/playwright/ui/specs/login.spec.js +++ b/frontend/playwright/ui/specs/login.spec.js @@ -2,7 +2,9 @@ import { test, expect } from "@playwright/test"; import { LoginPage } from "../pages/LoginPage"; test.beforeEach(async ({ page }) => { - await LoginPage.initWithLoggedOutUser(page); + const login = new LoginPage(page); + await login.initWithLoggedOutUser(); + await page.goto("/#/auth/login"); }); diff --git a/frontend/playwright/ui/specs/viewer-comments.spec.js b/frontend/playwright/ui/specs/viewer-comments.spec.js index 13591e6053..7d7b637eab 100644 --- a/frontend/playwright/ui/specs/viewer-comments.spec.js +++ b/frontend/playwright/ui/specs/viewer-comments.spec.js @@ -8,21 +8,11 @@ test.beforeEach(async ({ page }) => { const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1"; const singleBoardPageId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb2"; -const setupFileWithSingleBoard = async (viewer) => { - await viewer.mockRPC(/get\-view\-only\-bundle\?/, "viewer/get-view-only-bundle-single-board.json"); - await viewer.mockRPC("get-comment-threads?file-id=*", "workspace/get-comment-threads-not-empty.json"); - await viewer.mockRPC( - "get-file-fragment?file-id=*&fragment-id=*", - "viewer/get-file-fragment-single-board.json", - ); - await viewer.mockRPC("get-comments?thread-id=*", "workspace/get-thread-comments.json"); - await viewer.mockRPC("update-comment-thread-status", "workspace/update-comment-thread-status.json"); -}; test("Comment is shown with scroll and valid position", async ({ page }) => { const viewer = new ViewerPage(page); await viewer.setupLoggedInUser(); - await setupFileWithSingleBoard(viewer); + await viewer.setupFileWithComments(); await viewer.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId }); await viewer.showComments(); diff --git a/frontend/playwright/ui/specs/viewer-header.spec.js b/frontend/playwright/ui/specs/viewer-header.spec.js index 48f282965e..81c01ee058 100644 --- a/frontend/playwright/ui/specs/viewer-header.spec.js +++ b/frontend/playwright/ui/specs/viewer-header.spec.js @@ -8,15 +8,6 @@ test.beforeEach(async ({ page }) => { const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1"; const singleBoardPageId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb2"; -const setupFileWithSingleBoard = async (viewer) => { - await viewer.mockRPC(/get\-view\-only\-bundle\?/, "viewer/get-view-only-bundle-single-board.json"); - await viewer.mockRPC("get-comment-threads?file-id=*", "workspace/get-comment-threads-empty.json"); - await viewer.mockRPC( - "get-file-fragment?file-id=*&fragment-id=*", - "viewer/get-file-fragment-single-board.json", - ); -}; - test("Clips link area of the logo", async ({ page }) => { const viewerPage = new ViewerPage(page); await viewerPage.setupLoggedInUser(); @@ -37,7 +28,7 @@ test("Clips link area of the logo", async ({ page }) => { test("Updates URL with zoom type", async ({ page }) => { const viewer = new ViewerPage(page); await viewer.setupLoggedInUser(); - await setupFileWithSingleBoard(viewer); + await viewer.setupFileWithSingleBoard(viewer); await viewer.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId }); diff --git a/frontend/playwright/ui/visual-specs/example.spec.js b/frontend/playwright/ui/visual-specs/example.spec.js deleted file mode 100644 index e4c344eec8..0000000000 --- a/frontend/playwright/ui/visual-specs/example.spec.js +++ /dev/null @@ -1,10 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { LoginPage } from "../pages/LoginPage"; - -test("Shows login form correctly", async ({ page }) => { - await LoginPage.initWithLoggedOutUser(page); - const loginPage = new LoginPage(page); - await page.goto("/#/auth/login"); - - await expect(page).toHaveScreenshot(); -}); diff --git a/frontend/playwright/ui/visual-specs/example.spec.js-snapshots/Shows-login-form-correctly-1-ds-linux.png b/frontend/playwright/ui/visual-specs/example.spec.js-snapshots/Shows-login-form-correctly-1-ds-linux.png deleted file mode 100644 index bbe1dc4027..0000000000 Binary files a/frontend/playwright/ui/visual-specs/example.spec.js-snapshots/Shows-login-form-correctly-1-ds-linux.png and /dev/null differ diff --git a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js index faefbeb1bd..f7eeeb01a5 100644 --- a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js +++ b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js @@ -3,11 +3,7 @@ import DashboardPage from "../pages/DashboardPage"; test.beforeEach(async ({ page }) => { await DashboardPage.init(page); - await DashboardPage.mockRPC( - page, - "get-profile", - "logged-in-user/get-profile-logged-in-no-onboarding.json", - ); + await DashboardPage.mockRPC(page, "get-profile", "logged-in-user/get-profile-logged-in-no-onboarding.json"); }); test("User goes to an empty dashboard", async ({ page }) => { @@ -123,15 +119,12 @@ test("User goes to an full search page", async ({ page }) => { await dashboardPage.setupDashboardFull(); await dashboardPage.goToSearch(); - await expect(dashboardPage.searchInput).toBeVisible(); await dashboardPage.searchInput.fill("New"); await expect(dashboardPage.searchTitle).toBeVisible(); - await expect(dashboardPage.newFileName).toBeVisible(); - await expect(dashboardPage.page).toHaveScreenshot(); }); @@ -141,9 +134,7 @@ test("User opens user account", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.goToDashboard(); - await expect(dashboardPage.userAccount).toBeVisible(); - await dashboardPage.goToAccount(); await expect(dashboardPage.page).toHaveScreenshot(); @@ -153,11 +144,9 @@ test("User goes to user profile", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.goToDashboard(); - await dashboardPage.goToAccount(); await expect(dashboardPage.userAccountTitle).toBeVisible(); - await expect(dashboardPage.page).toHaveScreenshot(); }); @@ -165,13 +154,11 @@ test("User goes to password management section", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.goToDashboard(); - await dashboardPage.goToAccount(); await page.getByText("Password").click(); await expect(page.getByRole("heading", { name: "Change Password" })).toBeVisible(); - await expect(dashboardPage.page).toHaveScreenshot(); }); @@ -179,91 +166,11 @@ test("User goes to settings section", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.goToDashboard(); - await dashboardPage.goToAccount(); await page.getByTestId("settings-profile").click(); await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible(); - - await expect(dashboardPage.page).toHaveScreenshot(); -}); - -test("User goes to an empty access tokens secction", async ({ page }) => { - const dashboardPage = new DashboardPage(page); - - await dashboardPage.goToDashboard(); - - await dashboardPage.setupAccessTokensEmpty(); - - await dashboardPage.goToAccount(); - - await page.getByText("Access tokens").click(); - - await expect(page.getByRole("heading", { name: "Personal access tokens" })).toBeVisible(); - - await expect(dashboardPage.page).toHaveScreenshot(); -}); - -test("User can create an access token", async ({ page }) => { - const dashboardPage = new DashboardPage(page); - - await dashboardPage.goToDashboard(); - - await dashboardPage.setupAccessTokensEmpty(); - - await dashboardPage.goToAccount(); - - await page.getByText("Access tokens").click(); - - await expect(page.getByRole("heading", { name: "Personal access tokens" })).toBeVisible(); - - await page.getByRole("button", { name: "Generate New Token" }).click(); - - await dashboardPage.createAccessToken(); - - await expect(page.getByPlaceholder("The name can help to know")).toBeVisible(); - - await page.getByPlaceholder("The name can help to know").fill("New token"); - - await expect(page.getByRole("button", { name: "Create token" })).not.toBeDisabled(); - - await page.getByRole("button", { name: "Create token" }).click(); - - await expect(page.getByRole("button", { name: "Create token" })).not.toBeVisible(); - - await expect(dashboardPage.page).toHaveScreenshot(); -}); - -test("User goes to a full access tokens secction", async ({ page }) => { - const dashboardPage = new DashboardPage(page); - - await dashboardPage.goToDashboard(); - - await dashboardPage.setupAccessTokens(); - - await dashboardPage.goToAccount(); - - await page.getByText("Access tokens").click(); - - await expect(page.getByRole("heading", { name: "Personal access tokens" })).toBeVisible(); - - await expect(page.getByText("new token", { exact: true })).toBeVisible(); - - await expect(dashboardPage.page).toHaveScreenshot(); -}); - -test("User goes to the feedback secction", async ({ page }) => { - const dashboardPage = new DashboardPage(page); - - await dashboardPage.goToDashboard(); - - await dashboardPage.goToAccount(); - - await page.getByText("Give feedback").click(); - - await expect(page.getByRole("heading", { name: "Email" })).toBeVisible(); - await expect(dashboardPage.page).toHaveScreenshot(); }); @@ -273,13 +180,11 @@ test("User opens teams selector with only one team", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.goToDashboard(); - await expect(dashboardPage.titleLabel).toBeVisible(); await dashboardPage.teamDropdown.click(); await expect(page.getByText("Create new team")).toBeVisible(); - await expect(dashboardPage.page).toHaveScreenshot(); }); @@ -288,30 +193,25 @@ test("User opens teams selector with more than one team", async ({ page }) => { await dashboardPage.setupDashboardFull(); await dashboardPage.goToDashboard(); - await expect(dashboardPage.titleLabel).toBeVisible(); await dashboardPage.teamDropdown.click(); await expect(page.getByText("Second Team")).toBeVisible(); - await expect(dashboardPage.page).toHaveScreenshot(); }); test("User goes to second team", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.setupDashboardFull(); - await dashboardPage.goToDashboard(); await dashboardPage.teamDropdown.click(); - await expect(page.getByText("Second Team")).toBeVisible(); await page.getByText("Second Team").click(); await expect(page.getByText("Team Up")).toBeVisible(); - await expect(dashboardPage.page).toHaveScreenshot(); }); @@ -320,13 +220,11 @@ test("User opens team management dropdown", async ({ page }) => { await dashboardPage.setupDashboardFull(); await dashboardPage.goToSecondTeamDashboard(); - await expect(page.getByText("Team Up")).toBeVisible(); await page.getByRole("button", { name: "team-management" }).click(); await expect(page.getByTestId("team-members")).toBeVisible(); - await expect(dashboardPage.page).toHaveScreenshot(); }); @@ -365,24 +263,20 @@ test("User goes to a complete invitations section", async ({ page }) => { await expect(dashboardPage.page).toHaveScreenshot(); }); - test("User invite people to the team", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.setupDashboardFull(); await dashboardPage.setupTeamInvitationsEmpty(); await dashboardPage.goToSecondTeamInvitationsSection(); - await expect(page.getByTestId("invite-member")).toBeVisible(); await page.getByTestId("invite-member").click(); - await expect(page.getByText("Invite with the role")).toBeVisible(); - await page.getByPlaceholder('Emails, comma separated').fill("test5@mail.com"); + await page.getByPlaceholder("Emails, comma separated").fill("test5@mail.com"); await expect(page.getByText("Send invitation")).not.toBeDisabled(); - await expect(dashboardPage.page).toHaveScreenshot(); }); @@ -394,7 +288,6 @@ test("User goes to an empty webhook section", async ({ page }) => { await dashboardPage.goToSecondTeamWebhooksSection(); await expect(page.getByText("No webhooks created so far.")).toBeVisible(); - await expect(dashboardPage.page).toHaveScreenshot(); }); @@ -406,7 +299,6 @@ test("User goes to a complete webhook section", async ({ page }) => { await dashboardPage.goToSecondTeamWebhooksSection(); await expect(page.getByText("https://www.google.com")).toBeVisible(); - await expect(dashboardPage.page).toHaveScreenshot(); }); @@ -418,6 +310,5 @@ test("User goes to the team settings section", async ({ page }) => { await dashboardPage.goToSecondTeamSettingsSection(); await expect(page.getByText("TEAM INFO")).toBeVisible(); - await expect(dashboardPage.page).toHaveScreenshot(); }); diff --git a/frontend/playwright/ui/visual-specs/visual-login.spec.js b/frontend/playwright/ui/visual-specs/visual-login.spec.js new file mode 100644 index 0000000000..b3b63a0c56 --- /dev/null +++ b/frontend/playwright/ui/visual-specs/visual-login.spec.js @@ -0,0 +1,37 @@ +import { test, expect } from "@playwright/test"; +import { LoginPage } from "../pages/LoginPage"; + +test.beforeEach(async ({ page }) => { + const login = new LoginPage(page); + await login.initWithLoggedOutUser(); + await login.page.goto("/#/auth/login"); +}); + +test.describe("Login form", () => { + test("Shows the login form correctly", async ({ page }) => { + const login = new LoginPage(page); + await expect(login.page).toHaveScreenshot(); + }); + + test("Shows form error messages correctly ", async ({ page }) => { + const login = new LoginPage(page); + await login.setupLoginSuccess(); + + await login.fillEmailAndPasswordInputs("foo", "lorenIpsum"); + + await expect(login.invalidEmailError).toBeVisible(); + await expect(login.page).toHaveScreenshot(); + }); + + test("Shows error toasts correctly", async ({ page }) => { + const login = new LoginPage(page); + await login.setupLoginError(); + + await login.fillEmailAndPasswordInputs("test@example.com", "loremipsum"); + await login.clickLoginButton(); + + await expect(login.invalidCredentialsError).toBeVisible(); + await expect(login.page).toHaveURL(/auth\/login$/); + await expect(login.page).toHaveScreenshot(); + }); +}); diff --git a/frontend/playwright/ui/visual-specs/visual-viewer.spec.js b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js new file mode 100644 index 0000000000..48afc1a60f --- /dev/null +++ b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js @@ -0,0 +1,117 @@ +import { test, expect } from "@playwright/test"; +import { ViewerPage } from "../pages/ViewerPage"; + +test.beforeEach(async ({ page }) => { + await ViewerPage.init(page); + +}); + +const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1"; +const singleBoardPageId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb2"; + +test("User goes to an empty Viewer", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupEmptyFile(); + + await viewerPage.goToViewer(); + + await expect(viewerPage.page.getByTestId("penpot-logo-link")).toBeVisible(); + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User goes to the Viewer", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithSingleBoard(); + + await viewerPage.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId }); + + await expect(viewerPage.page.getByTestId("penpot-logo-link")).toBeVisible(); + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User goes to the Viewer and opens zoom modal", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithSingleBoard(); + + await viewerPage.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId }); + + await viewerPage.page.getByTitle("Zoom").click(); + + await expect(viewerPage.page.getByTestId("penpot-logo-link")).toBeVisible(); + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User goes to the Viewer Comments", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithComments(); + + await viewerPage.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId }); + + await viewerPage.showComments(); + await viewerPage.showCommentsThread(1); + await expect(viewerPage.page.getByRole("textbox", { name: "Reply" })).toBeVisible(); + + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User opens Viewer comment list", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithComments(); + + await viewerPage.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId }); + + await viewerPage.showComments(); + await viewerPage.page.getByTestId("viewer-comments-dropdown").click(); + + await viewerPage.page.getByText("Show comments list").click(); + + await expect(viewerPage.page.getByRole("button", { name: "Show all comments" })).toBeVisible(); + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User goes to the Viewer Inspect code", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithComments(); + + await viewerPage.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId }); + + await viewerPage.showCode(); + + await expect(viewerPage.page.getByText("Size and position")).toBeVisible(); + + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User goes to the Viewer Inspect code, code tab", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithComments(); + + await viewerPage.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId }); + + await viewerPage.showCode(); + await viewerPage.page.getByTestId("code").click(); + + await expect(viewerPage.page.getByRole("button", { name: "Copy all code" })).toBeVisible(); + + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User opens Share modal", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithSingleBoard(); + + await viewerPage.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId }); + + await viewerPage.page.getByRole("button", { name: "Share" }).click(); + + await expect(viewerPage.page.getByRole("button", { name: "Get link" })).toBeVisible(); + await expect(viewerPage.page).toHaveScreenshot(); +}); diff --git a/frontend/playwright/ui/visual-specs/workspace.spec.js b/frontend/playwright/ui/visual-specs/workspace.spec.js new file mode 100644 index 0000000000..b21f139967 --- /dev/null +++ b/frontend/playwright/ui/visual-specs/workspace.spec.js @@ -0,0 +1,124 @@ +import { test, expect } from "@playwright/test"; +import { WorkspacePage } from "../pages/WorkspacePage"; + +test.beforeEach(async ({ page }) => { + await WorkspacePage.init(page); +}); + +const setupFileWithAssets = async (workspace) => { + const fileId = "015fda4f-caa6-8103-8004-862a00dd4f31"; + const pageId = "015fda4f-caa6-8103-8004-862a00ddbe94"; + const fragments = { + "015fda4f-caa6-8103-8004-862a9e4b4d4b": "assets/get-file-fragment-with-assets-components.json", + "015fda4f-caa6-8103-8004-862a9e4ad279": "assets/get-file-fragmnet-with-assets-page.json", + }; + + await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "assets/get-file-with-assets.json"); + + for (const [id, fixture] of Object.entries(fragments)) { + await workspace.mockRPC(`get-file-fragment?file-id=*&fragment-id=${id}`, fixture); + } + + return { fileId, pageId }; +}; + +test("Shows the workspace correctly for a blank file", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + + await workspace.goToWorkspace(); + + await expect(workspace.page).toHaveScreenshot(); +}); + +test.describe("Design tab", () => { + test("Shows the design tab when selecting a shape", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json"); + + await workspace.goToWorkspace({ + fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", + pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", + }); + + await workspace.clickLeafLayer("Rectangle"); + + await expect(workspace.page).toHaveScreenshot(); + }); + + test("Shows expanded sections of the design tab", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json"); + + await workspace.goToWorkspace({ + fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", + pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", + }); + + await workspace.clickLeafLayer("Rectangle"); + await workspace.rightSidebar.getByTestId("add-stroke").click(); + + await expect(workspace.page).toHaveScreenshot(); + }); +}); + +test.describe("Assets tab", () => { + test("Shows the libraries modal correctly", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockRPC("link-file-to-library", "workspace/link-file-to-library.json"); + await workspace.mockRPC( + "get-team-shared-files?team-id=*", + "workspace/get-team-shared-libraries-non-empty.json", + ); + + await workspace.goToWorkspace(); + await workspace.clickAssets(); + await workspace.openLibrariesModal(); + + await expect(workspace.page).toHaveScreenshot(); + + await workspace.clickLibrary("Testing library 1"); + + await expect(workspace.page).toHaveScreenshot(); + }); + + test("Shows the assets correctly", async ({ page }) => { + const workspace = new WorkspacePage(page); + const { fileId, pageId } = await setupFileWithAssets(workspace); + + await workspace.goToWorkspace({ fileId, pageId }); + + await workspace.clickAssets(); + await workspace.sidebar.getByRole("button", { name: "Components" }).click(); + await workspace.sidebar.getByRole("button", { name: "Colors" }).click(); + await workspace.sidebar.getByRole("button", { name: "Typographies" }).click(); + + await expect(workspace.page).toHaveScreenshot(); + + await workspace.sidebar.getByTitle("List view").click(); + + await expect(workspace.page).toHaveScreenshot(); + }); +}); + +test.describe("Palette", () => { + test("Shows the bottom palette expanded and collapsed", async ({ page }) => { + const workspace = new WorkspacePage(page); + const { fileId, pageId } = await setupFileWithAssets(workspace); + + await workspace.goToWorkspace({ fileId, pageId }); + + await expect(workspace.page).toHaveScreenshot(); + + await workspace.palette.getByRole("button", { name: "Typographies" }).click(); + await expect(workspace.palette.getByText("Source Sans Pro Regular")).toBeVisible(); + await expect(workspace.page).toHaveScreenshot(); + + await workspace.palette.getByRole("button", { name: "Color Palette" }).click(); + await expect(workspace.palette.getByRole("button", { name: "#7798ff" })).toBeVisible(); + }); +}); diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 608be7e2a7..b2e3d95c4f 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -69,7 +69,8 @@ :enable-onboarding-questions :enable-onboarding-newsletter :enable-dashboard-templates-section - :enable-google-fonts-provider]) + :enable-google-fonts-provider + :enable-component-thumbnails]) (defn- parse-flags [global] diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs index d16cfafc57..5f8229eb1e 100644 --- a/frontend/src/app/main/data/changes.cljs +++ b/frontend/src/app/main/data/changes.cljs @@ -29,18 +29,24 @@ (def commit? (ptk/type? ::commit)) -(defn update-indexes +(defn- fix-page-id + "For events that modifies the page, page-id does not comes + as a property so we assign it from the `id` property." + [{:keys [id type page] :as change}] + (cond-> change + (and (page-change? type) + (nil? (:page-id change))) + (assoc :page-id (or id (:id page))))) + +(defn- update-indexes "Given a commit, send the changes to the worker for updating the indexes." - [{:keys [changes] :as commit}] + [commit attr] (ptk/reify ::update-indexes ptk/WatchEvent (watch [_ _ _] - (let [changes (->> changes - (map (fn [{:keys [id type page] :as change}] - (cond-> change - (and (page-change? type) (nil? (:page-id change))) - (assoc :page-id (or id (:id page)))))) + (let [changes (->> (get commit attr) + (map fix-page-id) (filter :page-id) (group-by :page-id))] @@ -58,6 +64,41 @@ (map (d/getf (:index persistence))) (not-empty))) +(def ^:private xf:map-page-id + (map :page-id)) + +(defn- apply-changes-localy + [{:keys [file-id redo-changes] :as commit} pending] + (ptk/reify ::apply-changes-localy + ptk/UpdateEvent + (update [_ state] + (let [current-file-id (get state :current-file-id) + path (if (= file-id current-file-id) + [:workspace-data] + [:workspace-libraries file-id :data]) + + undo-changes (if pending + (->> pending + (map :undo-changes) + (reverse) + (mapcat identity) + (vec)) + nil) + + redo-changes (if pending + (into redo-changes + (mapcat :redo-changes) + pending) + redo-changes)] + + (d/update-in-when state path + (fn [file] + (let [file (cpc/process-changes file undo-changes false) + file (cpc/process-changes file redo-changes false) + pids (into #{} xf:map-page-id redo-changes)] + (reduce #(ctst/update-object-indices %1 %2) file pids)))))))) + + (defn commit "Create a commit event instance" [{:keys [commit-id redo-changes undo-changes origin save-undo? features @@ -70,6 +111,7 @@ (let [commit-id (or commit-id (uuid/next)) source (d/nilv source :local) + local? (= source :local) commit {:id commit-id :created-at (dt/now) :source source @@ -89,38 +131,20 @@ cljs.core/IDeref (-deref [_] commit) - ptk/UpdateEvent - (update [_ state] - (let [current-file-id (get state :current-file-id) - path (if (= file-id current-file-id) - [:workspace-data] - [:workspace-libraries file-id :data]) - - not-local? (not= source :local) - pending (if not-local? - (get-pending-commits state) - nil) - - undo-changes (if pending - (->> pending - (map :undo-changes) - (reverse) - (mapcat identity) - (vec)) - nil) - - redo-changes (if pending - (into redo-changes - (mapcat :redo-changes) - pending) - redo-changes)] - - (d/update-in-when state path - (fn [file] - (let [file (cpc/process-changes file undo-changes false) - file (cpc/process-changes file redo-changes false) - pids (into #{} (map :page-id) redo-changes)] - (reduce #(ctst/update-object-indices %1 %2) file pids))))))))) + ptk/WatchEvent + (watch [_ state _] + (let [pending (when-not local? + (get-pending-commits state))] + (rx/concat + (rx/of (apply-changes-localy commit pending)) + (if pending + (rx/concat + (->> (rx/from (reverse pending)) + (rx/map (fn [commit] (update-indexes commit :undo-changes)))) + (rx/of (update-indexes commit :redo-changes)) + (->> (rx/from pending) + (rx/map (fn [commit] (update-indexes commit :redo-changes))))) + (rx/of (update-indexes commit :redo-changes))))))))) (defn- resolve-file-revn [state file-id] diff --git a/frontend/src/app/main/data/persistence.cljs b/frontend/src/app/main/data/persistence.cljs index 22f1cdb655..2e917e7844 100644 --- a/frontend/src/app/main/data/persistence.cljs +++ b/frontend/src/app/main/data/persistence.cljs @@ -211,14 +211,6 @@ (update-status :pending))) (rx/take-until stoper-s)) - (->> local-commits-s - (rx/buffer-time 200) - (rx/mapcat merge-commit) - (rx/map dch/update-indexes) - (rx/take-until stoper-s) - (rx/finalize (fn [] - (log/debug :hint "finalize persistence: changes watcher [index]")))) - ;; Here we watch for local commits, buffer them in a small ;; chunks (very near in time commits) and append them to the ;; persistence queue @@ -237,6 +229,5 @@ (rx/map deref) (rx/filter #(= :remote (:source %))) (rx/mapcat (fn [{:keys [file-id file-revn] :as commit}] - (rx/of (update-file-revn file-id file-revn) - (dch/update-indexes commit)))) + (rx/of (update-file-revn file-id file-revn)))) (rx/take-until stoper-s))))))) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 71a3903b8f..081ec53397 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -590,7 +590,7 @@ (update [_ state] (update state :colorpicker (fn [state] - (let [type (:type state) + (let [type (:type state) state (-> state (update :current-color merge changes) (update :current-color materialize-color-components) @@ -605,11 +605,12 @@ (-> state (dissoc :gradient :stops :editing-stop) - (cond-> (not= :image (:type state)) + (cond-> (not= :image type) (assoc :type :color)))))))) ptk/WatchEvent (watch [_ state _] - (when add-recent? + ;; Type can be null, because the colorpicker can be closed while a color image finish its upload + (when (and add-recent? (some? (:type state))) (let [formated-color (get-color-from-colorpicker-state (:colorpicker state))] (rx/of (dwl/add-recent-color formated-color))))))) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 2064d43416..db73c1d147 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -24,6 +24,7 @@ [app.common.types.shape.layout :as ctl] [app.common.types.typography :as ctt] [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.changes :as dch] [app.main.data.comments :as dc] [app.main.data.events :as ev] @@ -1203,7 +1204,7 @@ (rx/debounce 5000) (rx/tap #(log/trc :hint "buffer initialized")))] - (when components-v2? + (when (and components-v2? (contains? cf/flags :component-thumbnails)) (->> (rx/merge changes-s diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index c00cf7d6c6..932e9ccfac 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -217,40 +217,16 @@ (-deref [_] {:changes changes}) ptk/WatchEvent - (watch [_ state _] - (let [page-id (:current-page-id state) - - position-data-operation? - (fn [{:keys [type attr]}] - (and (= :set type) - (= attr :position-data))) - - update-position-data - (fn [change] - ;; Remove the position data from remote operations. Will be changed localy, otherwise - ;; creates a strange "out-of-sync" behaviour. - (cond-> change - (and (= page-id (:page-id change)) - (= :mod-obj (:type change))) - (update :operations #(d/removev position-data-operation? %)))) - - ;; We update `position-data` from the incoming message - changes (->> changes - (map update-position-data) - (remove (fn [change] - (and (= page-id (:page-id change)) - (:ignore-remote? change)))) - (vec))] - - ;; The commit event is responsible to apply the data localy - ;; and update the persistence internal state with the updated - ;; file-revn - (rx/of (dch/commit {:file-id file-id - :file-revn revn - :save-undo? false - :source :remote - :redo-changes changes - :undo-changes []})))))) + (watch [_ _ _] + ;; The commit event is responsible to apply the data localy + ;; and update the persistence internal state with the updated + ;; file-revn + (rx/of (dch/commit {:file-id file-id + :file-revn revn + :save-undo? false + :source :remote + :redo-changes (vec changes) + :undo-changes []}))))) (def ^:private schema:handle-library-change diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 81c12872a7..fecb3f8e02 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -46,8 +46,8 @@ (defn update-shapes ([ids update-fn] (update-shapes ids update-fn nil)) - ([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-remote? ignore-touched undo-group with-objects?] - :or {reg-objects? false save-undo? true stack-undo? false ignore-remote? false ignore-touched false with-objects? false}}] + ([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-touched undo-group with-objects?] + :or {reg-objects? false save-undo? true stack-undo? false ignore-touched false with-objects? false}}] (dm/assert! "expected a valid coll of uuid's" @@ -84,8 +84,7 @@ changes (add-undo-group changes state)] (rx/concat (if (seq (:redo-changes changes)) - (let [changes (cond-> changes reg-objects? (pcb/resize-parents ids)) - changes (cond-> changes ignore-remote? (pcb/ignore-remote))] + (let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))] (rx/of (dch/commit-changes changes))) (rx/empty)) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index bceaeacf3f..4d9785b672 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -486,7 +486,6 @@ (rx/of (dwu/start-undo-transaction undo-id) (dwsh/update-shapes ids update-fn {:reg-objects? true :stack-undo? true - :ignore-remote? true :ignore-touched true}) (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))))) @@ -631,7 +630,7 @@ (fn [shape] (-> shape (assoc :position-data (get position-data (:id shape))))) - {:stack-undo? true :reg-objects? false :ignore-remote? true})) + {:stack-undo? true :reg-objects? false})) (rx/of (fn [state] (dissoc state ::update-position-data-debounce ::update-position-data)))))))) diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index c4cde6e2de..625c207c62 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -10,7 +10,7 @@ [app.common.files.helpers :as cfh] [app.common.logging :as l] [app.common.thumbnails :as thc] - [app.config :as cf] + [app.common.uuid :as uuid] [app.main.data.changes :as dch] [app.main.data.persistence :as-alias dps] [app.main.data.workspace.notifications :as-alias wnt] @@ -19,7 +19,6 @@ [app.main.refs :as refs] [app.main.render :as render] [app.main.repo :as rp] - [app.util.http :as http] [app.util.queue :as q] [app.util.time :as tp] [app.util.timers :as tm] @@ -149,34 +148,34 @@ ptk/WatchEvent (watch [_ state stream] (l/dbg :hint "update thumbnail" :requester requester :object-id object-id :tag tag) - ;; Send the update to the back-end - (->> (request-thumbnail state file-id page-id frame-id tag) - (rx/mapcat (fn [blob] - ;; Send the data to backend - (let [params {:file-id file-id - :object-id object-id - :media blob - :tag (or tag "frame")}] - (rp/cmd! :create-file-object-thumbnail params)))) + (let [tp (tp/tpoint-ms)] + ;; Send the update to the back-end + (->> (request-thumbnail state file-id page-id frame-id tag) + (rx/mapcat (fn [blob] + (let [uri (wapi/create-uri blob) + params {:file-id file-id + :object-id object-id + :media blob + :tag (or tag "frame")}] - (rx/mapcat (fn [{:keys [object-id media-id]}] - (let [uri (cf/resolve-media media-id)] - ;; We perform this request just for - ;; populate the browser CACHE and avoid - ;; unnecesary image flickering - (->> (http/send! {:uri uri :method :get}) - (rx/map #(assoc-thumbnail object-id uri)))))) + (rx/merge + (rx/of (assoc-thumbnail object-id uri)) + (->> (rp/cmd! :create-file-object-thumbnail params) + (rx/catch rx/empty) + (rx/ignore)))))) - (rx/catch (fn [cause] - (.error js/console cause) - (rx/empty))) + (rx/catch (fn [cause] + (.error js/console cause) + (rx/empty))) - ;; We cancel all the stream if user starts editing while - ;; thumbnail is generating - (rx/take-until - (->> stream - (rx/filter (ptk/type? ::clear-thumbnail)) - (rx/filter #(= (deref %) object-id))))))))) + (rx/tap #(l/trc :hint "thumbnail updated" :elapsed (dm/str (tp) "ms"))) + + ;; We cancel all the stream if user starts editing while + ;; thumbnail is generating + (rx/take-until + (->> stream + (rx/filter (ptk/type? ::clear-thumbnail)) + (rx/filter #(= (deref %) object-id)))))))))) (defn- extract-root-frame-changes "Process a changes set in a commit to extract the frames that are changing" @@ -192,8 +191,8 @@ :mov-objects (->> (:shapes change) (map #(vector page-id %))) [])) - get-frame-id - (fn [[_ id]] + get-frame-ids + (fn get-frame-ids [id] (let [old-objects (wsh/lookup-data-objects old-data page-id) new-objects (wsh/lookup-data-objects new-data page-id) @@ -208,12 +207,21 @@ (conj old-frame-id) (cfh/root-frame? new-objects new-frame-id) - (conj new-frame-id))))] + (conj new-frame-id) + + (and (uuid? (:frame-id old-shape)) + (not= uuid/zero (:frame-id old-shape))) + (into (get-frame-ids (:frame-id old-shape))) + + (and (uuid? (:frame-id new-shape)) + (not= uuid/zero (:frame-id new-shape))) + (into (get-frame-ids (:frame-id new-shape))))))] (into #{} (comp (mapcat extract-ids) (filter (fn [[page-id']] (= page-id page-id'))) - (mapcat get-frame-id)) + (map (fn [[_ id]] id)) + (mapcat get-frame-ids)) changes))) (defn watch-state-changes diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index b6ff8dc1e2..7d9bee599d 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -137,6 +137,7 @@ (->> (http/send! {:method :post :uri uri :credentials "include" + :headers {"x-external-session-id" (cf/external-session-id)} :query params}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response)))) @@ -146,6 +147,7 @@ (->> (http/send! {:method :post :uri (u/join cf/public-uri "api/export") :body (http/transit-data (dissoc params :blob?)) + :headers {"x-external-session-id" (cf/external-session-id)} :credentials "include" :response-type (if blob? :blob :text)}) (rx/map http/conditional-decode-transit) @@ -165,6 +167,7 @@ (->> (http/send! {:method :post :uri (u/join cf/public-uri "api/rpc/command/" (name id)) :credentials "include" + :headers {"x-external-session-id" (cf/external-session-id)} :body (http/form-data params)}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs index c1b939e4fa..4525d7b711 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.cljs +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -105,7 +105,7 @@ {::ev/name "onboarding-step" :label "team:create-team-and-invite-later" :team-name name - :step 7}) + :step 8}) (ptk/data-event ::ev/event {::ev/name "onboarding-finish"}))))) @@ -122,7 +122,7 @@ :invites (count emails) :team-name name :role (:role params) - :step 7}) + :step 8}) (ptk/data-event ::ev/event {::ev/name "onboarding-finish"}))))) diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs index a83d47b4ac..fe72f96b3b 100644 --- a/frontend/src/app/main/ui/viewer/comments.cljs +++ b/frontend/src/app/main/ui/viewer/comments.cljs @@ -65,6 +65,7 @@ (st/emit! (dcm/update-options {:show-sidebar? (not mode)})))))] [:div {:class (stl/css :view-options) + :data-testid "viewer-comments-dropdown" :on-click toggle-dropdown} [:span {:class (stl/css :dropdown-title)} (tr "labels.comments")] [:span {:class (stl/css :icon-dropdown)} i/arrow] diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs index e9a4137140..681dffd372 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs @@ -29,7 +29,8 @@ [promesa.core :as p] [rumext.v2 :as mf])) -(defn fix-position [shape] +(defn fix-position + [shape] (if-let [modifiers (:modifiers shape)] (let [shape' (gsh/transform-shape shape modifiers) diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index 1aaf88c7b0..f83b566869 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -169,6 +169,7 @@ :expanded (> size 276)) :id "right-sidebar-aside" + :data-testid "right-sidebar" :data-size (str size) :style #js {"--width" (if can-be-expanded? (dm/str size "px") 276)}} (when can-be-expanded? diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs index e3a1cf3727..2e247e0f17 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs @@ -14,6 +14,7 @@ [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.config :as cf] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.libraries :as dwl] @@ -287,7 +288,7 @@ (when (< @retry 3) (inc retry))))] - (if (some? thumbnail-uri) + (if (and (some? thumbnail-uri) (contains? cf/flags :component-thumbnails)) [:& component-svg-thumbnail {:thumbnail-uri thumbnail-uri :class class diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs index 72e01e609f..371716d7ec 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs @@ -520,9 +520,11 @@ :name "listing-style"} [:& radio-button {:icon i/view-as-list :value "list" + :title (tr "workspace.assets.list-view") :id "opt-list"}] [:& radio-button {:icon i/flex-grid :value "grid" + :title (tr "workspace.assets.grid-view") :id "opt-grid"}]]]) (when (and components-v2 (not read-only?) local?) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 88a14cff0e..2f6f6b74c4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -32,7 +32,7 @@ (let [{:keys [text-align]} values handle-change (mf/use-fn - (mf/deps on-blur) + (mf/deps on-change on-blur) (fn [value] (on-change {:text-align value}) (when (some? on-blur) (on-blur))))] @@ -64,7 +64,7 @@ (let [direction (:text-direction values) handle-change (mf/use-fn - (mf/deps direction) + (mf/deps on-change on-blur direction) (fn [value] (let [dir (if (= value direction) "none" @@ -93,7 +93,7 @@ vertical-align (or vertical-align "top") handle-change (mf/use-fn - (mf/deps on-blur) + (mf/deps on-change on-blur) (fn [value] (on-change {:vertical-align value}) (when (some? on-blur) (on-blur))))] @@ -154,7 +154,7 @@ (let [text-decoration (or (:text-decoration values) "none") handle-change (mf/use-fn - (mf/deps text-decoration) + (mf/deps on-change on-blur text-decoration) (fn [value] (let [decoration (if (= value text-decoration) "none" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 0195d362dc..7a972067de 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2558,11 +2558,11 @@ msgstr "Prototyping" #: src/app/main/ui/onboarding/questions.cljs msgid "onboarding.questions.start-with.ds" -msgstr "Creating Desing Systems" +msgstr "Creating Design Systems" #: src/app/main/ui/onboarding/questions.cljs msgid "onboarding.questions.start-with.code" -msgstr "Generating real code designs" +msgstr "Generating real code from designs" #: src/app/main/ui/onboarding/questions.cljs msgid "onboarding.questions.step5.title" @@ -3396,6 +3396,14 @@ msgstr "Sort" msgid "workspace.assets.typography" msgstr "Typographies" +#: src/app/main/ui/workspace/sidebar/assets/components.cljs, src/app/main/wui/workspace/sidebar/options/menus/component.cljs +msgid "workspace.assets.grid-view" +msgstr "Grid view" + +#: src/app/main/ui/workspace/sidebar/assets/components.cljs, src/app/main/wui/workspace/sidebar/options/menus/component.cljs +msgid "workspace.assets.list-view" +msgstr "List view" + #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.font-id" msgstr "Font" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 5bea21ce57..76a2264ca0 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3465,6 +3465,14 @@ msgstr "Ordenar" msgid "workspace.assets.typography" msgstr "Tipografías" +#: src/app/main/ui/workspace/sidebar/assets/components.cljs, src/app/main/wui/workspace/sidebar/options/menus/component.cljs +msgid "workspace.assets.grid-view" +msgstr "Ver como rejilla" + +#: src/app/main/ui/workspace/sidebar/assets/components.cljs, src/app/main/wui/workspace/sidebar/options/menus/component.cljs +msgid "workspace.assets.list-view" +msgstr "Ver como lista" + #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs msgid "workspace.assets.typography.font-id" msgstr "Fuente"