mirror of
https://github.com/penpot/penpot.git
synced 2025-05-31 09:16:14 +02:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
d48616d510
45 changed files with 1348 additions and 352 deletions
|
@ -56,6 +56,9 @@
|
||||||
- Workspace-palette items stay hidden when opening with keyboard-shortcut [Taiga #7489](https://tree.taiga.io/project/penpot/issue/7489)
|
- 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 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)
|
- 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
|
## 2.0.3
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
|
[ring.request :as rreq]
|
||||||
[ring.response :as-alias rres]))
|
[ring.response :as-alias rres]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -470,6 +471,9 @@
|
||||||
(some? (:invitation-token state))
|
(some? (:invitation-token state))
|
||||||
(assoc :invitation-token (: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
|
;; If state token comes with props, merge them. The state token
|
||||||
;; props can contain pm_ and utm_ prefixed query params.
|
;; props can contain pm_ and utm_ prefixed query params.
|
||||||
(map? (:props state))
|
(map? (:props state))
|
||||||
|
@ -560,13 +564,16 @@
|
||||||
{:iss :auth
|
{:iss :auth
|
||||||
:exp (dt/in-future "15m")
|
:exp (dt/in-future "15m")
|
||||||
:props (:props info)
|
:props (:props info)
|
||||||
:profile-id (:id profile)}))]
|
: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/submit! cfg {::audit/type "command"
|
||||||
::audit/name "login-with-oidc"
|
::audit/name "login-with-oidc"
|
||||||
::audit/profile-id (:id profile)
|
::audit/profile-id (:id profile)
|
||||||
::audit/ip-addr (audit/parse-client-ip request)
|
::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)
|
(->> (redirect-to-verify-token token)
|
||||||
(sxf request))))
|
(sxf request))))
|
||||||
|
@ -588,9 +595,11 @@
|
||||||
(defn- auth-handler
|
(defn- auth-handler
|
||||||
[cfg {:keys [params] :as request}]
|
[cfg {:keys [params] :as request}]
|
||||||
(let [props (audit/extract-utm-params params)
|
(let [props (audit/extract-utm-params params)
|
||||||
|
esid (rreq/get-header request "x-external-session-id")
|
||||||
state (tokens/generate (::setup/props cfg)
|
state (tokens/generate (::setup/props cfg)
|
||||||
{:iss :oauth
|
{:iss :oauth
|
||||||
:invitation-token (:invitation-token params)
|
:invitation-token (:invitation-token params)
|
||||||
|
:external-session-id esid
|
||||||
:props props
|
:props props
|
||||||
:exp (dt/in-future "4h")})
|
:exp (dt/in-future "4h")})
|
||||||
uri (build-auth-uri cfg state)]
|
uri (build-auth-uri cfg state)]
|
||||||
|
|
|
@ -86,6 +86,13 @@
|
||||||
(remove #(contains? reserved-props (key %))))
|
(remove #(contains? reserved-props (key %))))
|
||||||
props))
|
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
|
;; --- SPECS
|
||||||
|
|
||||||
|
|
||||||
|
@ -140,7 +147,7 @@
|
||||||
(::rpc/profile-id params)
|
(::rpc/profile-id params)
|
||||||
uuid/zero)
|
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)
|
props (-> (or (::replace-props resultm)
|
||||||
(-> params
|
(-> params
|
||||||
(merge (::props resultm))
|
(merge (::props resultm))
|
||||||
|
|
|
@ -102,13 +102,13 @@
|
||||||
{::mdef/name "penpot_tasks_timing"
|
{::mdef/name "penpot_tasks_timing"
|
||||||
::mdef/help "Background tasks timing (milliseconds)."
|
::mdef/help "Background tasks timing (milliseconds)."
|
||||||
::mdef/labels ["name"]
|
::mdef/labels ["name"]
|
||||||
::mdef/type :summary}
|
::mdef/type :histogram}
|
||||||
|
|
||||||
:redis-eval-timing
|
:redis-eval-timing
|
||||||
{::mdef/name "penpot_redis_eval_timing"
|
{::mdef/name "penpot_redis_eval_timing"
|
||||||
::mdef/help "Redis EVAL commands execution timings (ms)"
|
::mdef/help "Redis EVAL commands execution timings (ms)"
|
||||||
::mdef/labels ["name"]
|
::mdef/labels ["name"]
|
||||||
::mdef/type :summary}
|
::mdef/type :histogram}
|
||||||
|
|
||||||
:rpc-climit-queue
|
:rpc-climit-queue
|
||||||
{::mdef/name "penpot_rpc_climit_queue"
|
{::mdef/name "penpot_rpc_climit_queue"
|
||||||
|
@ -126,7 +126,7 @@
|
||||||
{::mdef/name "penpot_rpc_climit_timing"
|
{::mdef/name "penpot_rpc_climit_timing"
|
||||||
::mdef/help "Summary of the time between queuing and executing on the CLIMIT"
|
::mdef/help "Summary of the time between queuing and executing on the CLIMIT"
|
||||||
::mdef/labels ["name"]
|
::mdef/labels ["name"]
|
||||||
::mdef/type :summary}
|
::mdef/type :histogram}
|
||||||
|
|
||||||
:audit-http-handler-queue-size
|
:audit-http-handler-queue-size
|
||||||
{::mdef/name "penpot_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/name "penpot_audit_http_handler_timing"
|
||||||
::mdef/help "Summary of the time between queuing and executing on the audit log http handler"
|
::mdef/help "Summary of the time between queuing and executing on the audit log http handler"
|
||||||
::mdef/labels []
|
::mdef/labels []
|
||||||
::mdef/type :summary}
|
::mdef/type :histogram}
|
||||||
|
|
||||||
:executors-active-threads
|
:executors-active-threads
|
||||||
{::mdef/name "penpot_executors_active_threads"
|
{::mdef/name "penpot_executors_active_threads"
|
||||||
|
|
|
@ -79,8 +79,12 @@
|
||||||
profile-id (or (::session/profile-id request)
|
profile-id (or (::session/profile-id request)
|
||||||
(::actoken/profile-id request))
|
(::actoken/profile-id request))
|
||||||
|
|
||||||
|
session-id (rreq/get-header request "x-external-session-id")
|
||||||
|
|
||||||
data (-> params
|
data (-> params
|
||||||
|
(assoc ::handler-name handler-name)
|
||||||
(assoc ::request-at (dt/now))
|
(assoc ::request-at (dt/now))
|
||||||
|
(assoc ::external-session-id session-id)
|
||||||
(assoc ::session/id (::session/id request))
|
(assoc ::session/id (::session/id request))
|
||||||
(assoc ::cond/key etag)
|
(assoc ::cond/key etag)
|
||||||
(cond-> (uuid? profile-id)
|
(cond-> (uuid? profile-id)
|
||||||
|
@ -188,10 +192,10 @@
|
||||||
(defn- wrap-all
|
(defn- wrap-all
|
||||||
[cfg f mdata]
|
[cfg f mdata]
|
||||||
(as-> f $
|
(as-> f $
|
||||||
(wrap-metrics cfg $ mdata)
|
|
||||||
(cond/wrap cfg $ mdata)
|
(cond/wrap cfg $ mdata)
|
||||||
(retry/wrap-retry cfg $ mdata)
|
(retry/wrap-retry cfg $ mdata)
|
||||||
(climit/wrap cfg $ mdata)
|
(climit/wrap cfg $ mdata)
|
||||||
|
(wrap-metrics cfg $ mdata)
|
||||||
(rlimit/wrap cfg $ mdata)
|
(rlimit/wrap cfg $ mdata)
|
||||||
(wrap-audit cfg $ mdata)
|
(wrap-audit cfg $ mdata)
|
||||||
(wrap-spec-conform cfg $ mdata)
|
(wrap-spec-conform cfg $ mdata)
|
||||||
|
|
|
@ -321,18 +321,14 @@
|
||||||
(sv/defmethod ::delete-file-object-thumbnail
|
(sv/defmethod ::delete-file-object-thumbnail
|
||||||
{::doc/added "1.19"
|
{::doc/added "1.19"
|
||||||
::doc/module :files
|
::doc/module :files
|
||||||
::doc/deprecated "1.20"
|
|
||||||
::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id]
|
|
||||||
[:file-thumbnail-ops/global]]
|
|
||||||
::audit/skip true}
|
::audit/skip true}
|
||||||
[cfg {:keys [::rpc/profile-id file-id object-id]}]
|
[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}]
|
(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
|
(-> cfg
|
||||||
(update ::sto/storage media/configure-assets-storage conn)
|
(update ::sto/storage media/configure-assets-storage conn)
|
||||||
(delete-file-object-thumbnail! file-id object-id))
|
(delete-file-object-thumbnail! file-id object-id))
|
||||||
nil))))
|
nil)))
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: create-file-thumbnail
|
;; --- MUTATION COMMAND: create-file-thumbnail
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http.sse :as sse]
|
[app.http.sse :as sse]
|
||||||
|
[app.loggers.audit :as audit]
|
||||||
[app.loggers.webhooks :as-alias webhooks]
|
[app.loggers.webhooks :as-alias webhooks]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
|
@ -397,17 +398,32 @@
|
||||||
;; --- COMMAND: Clone Template
|
;; --- COMMAND: Clone Template
|
||||||
|
|
||||||
(defn- clone-template
|
(defn- clone-template
|
||||||
[{:keys [::wrk/executor ::bf.v1/project-id] :as cfg} template]
|
[cfg {:keys [project-id ::rpc/profile-id] :as params} template]
|
||||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
|
||||||
;; NOTE: the importation process performs some operations that
|
;; NOTE: the importation process performs some operations that
|
||||||
;; are not very friendly with virtual threads, and for avoid
|
;; are not very friendly with virtual threads, and for avoid
|
||||||
;; unexpected blocking of other concurrent operations we
|
;; unexpected blocking of other concurrent operations we
|
||||||
;; dispatch that operation to a dedicated executor.
|
;; 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
|
(db/update! conn :project
|
||||||
{:modified-at (dt/now)}
|
{:modified-at (dt/now)}
|
||||||
{:id project-id})
|
{: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
|
(def ^:private
|
||||||
schema:clone-template
|
schema:clone-template
|
||||||
|
@ -425,16 +441,14 @@
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}]
|
[{: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]})
|
(let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]})
|
||||||
_ (teams/check-edition-permissions! pool profile-id (:team-id project))
|
_ (teams/check-edition-permissions! pool profile-id (:team-id project))
|
||||||
template (tmpl/get-template-stream cfg template-id)
|
template (tmpl/get-template-stream cfg template-id)]
|
||||||
params (-> cfg
|
|
||||||
(assoc ::bf.v1/project-id (:id project))
|
|
||||||
(assoc ::bf.v1/profile-id profile-id))]
|
|
||||||
(when-not template
|
(when-not template
|
||||||
(ex/raise :type :not-found
|
(ex/raise :type :not-found
|
||||||
:code :template-not-found
|
:code :template-not-found
|
||||||
:hint "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
|
;; --- COMMAND: Get list of builtin templates
|
||||||
|
|
||||||
|
|
|
@ -763,6 +763,7 @@
|
||||||
{:id (:id member)}))
|
{:id (:id member)}))
|
||||||
|
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
(let [id (uuid/next)
|
(let [id (uuid/next)
|
||||||
expire (dt/in-future "168h") ;; 7 days
|
expire (dt/in-future "168h") ;; 7 days
|
||||||
invitation (db/exec-one! conn [sql:upsert-team-invitation id
|
invitation (db/exec-one! conn [sql:upsert-team-invitation id
|
||||||
|
@ -783,14 +784,19 @@
|
||||||
(when (contains? cf/flags :log-invitation-tokens)
|
(when (contains? cf/flags :log-invitation-tokens)
|
||||||
(l/info :hint "invitation token" :token itoken))
|
(l/info :hint "invitation token" :token itoken))
|
||||||
|
|
||||||
|
|
||||||
|
(let [props (-> (dissoc tprops :profile-id)
|
||||||
|
(audit/clean-props))
|
||||||
|
context (audit/params->context params)]
|
||||||
|
|
||||||
(audit/submit! cfg
|
(audit/submit! cfg
|
||||||
{::audit/type "action"
|
{::audit/type "action"
|
||||||
::audit/name (if updated?
|
::audit/name (if updated?
|
||||||
"update-team-invitation"
|
"update-team-invitation"
|
||||||
"create-team-invitation")
|
"create-team-invitation")
|
||||||
::audit/profile-id (:id profile)
|
::audit/profile-id (:id profile)
|
||||||
::audit/props (-> (dissoc tprops :profile-id)
|
::audit/props props
|
||||||
(d/without-nils))})
|
::audit/context context}))
|
||||||
|
|
||||||
(eml/send! {::eml/conn conn
|
(eml/send! {::eml/conn conn
|
||||||
::eml/factory eml/invite-to-team
|
::eml/factory eml/invite-to-team
|
||||||
|
@ -850,10 +856,11 @@
|
||||||
;; We don't re-send inviation to already existing members
|
;; We don't re-send inviation to already existing members
|
||||||
(remove (partial contains? members))
|
(remove (partial contains? members))
|
||||||
(map (fn [email]
|
(map (fn [email]
|
||||||
{:email email
|
(-> params
|
||||||
:team team
|
(assoc :email email)
|
||||||
:profile profile
|
(assoc :team team)
|
||||||
:role role}))
|
(assoc :profile profile)
|
||||||
|
(assoc :role role))))
|
||||||
(keep (partial create-invitation cfg)))
|
(keep (partial create-invitation cfg)))
|
||||||
emails)]
|
emails)]
|
||||||
(with-meta {:total (count invitations)
|
(with-meta {:total (count invitations)
|
||||||
|
@ -879,9 +886,11 @@
|
||||||
|
|
||||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||||
(cfeat/check-client-features! (:features params)))
|
(cfeat/check-client-features! (:features params)))
|
||||||
params (assoc params
|
|
||||||
:profile-id profile-id
|
params (-> params
|
||||||
:features features)
|
(assoc :profile-id profile-id)
|
||||||
|
(assoc :features features))
|
||||||
|
|
||||||
cfg (assoc cfg ::db/conn conn)
|
cfg (assoc cfg ::db/conn conn)
|
||||||
team (create-team cfg params)
|
team (create-team cfg params)
|
||||||
profile (db/get-by-id conn :profile profile-id)
|
profile (db/get-by-id conn :profile profile-id)
|
||||||
|
@ -890,10 +899,11 @@
|
||||||
;; Create invitations for all provided emails.
|
;; Create invitations for all provided emails.
|
||||||
(->> emails
|
(->> emails
|
||||||
(map (fn [email]
|
(map (fn [email]
|
||||||
{:team team
|
(-> params
|
||||||
:profile profile
|
(assoc :team team)
|
||||||
:email email
|
(assoc :profile profile)
|
||||||
:role role}))
|
(assoc :email email)
|
||||||
|
(assoc :role role))))
|
||||||
(run! (partial create-invitation cfg)))
|
(run! (partial create-invitation cfg)))
|
||||||
|
|
||||||
(run! (partial quotes/check-quote! conn)
|
(run! (partial quotes/check-quote! conn)
|
||||||
|
|
|
@ -801,15 +801,6 @@
|
||||||
(update :undo-changes conj {:type :del-component
|
(update :undo-changes conj {:type :del-component
|
||||||
:id id
|
:id id
|
||||||
:main-instance main-instance})))
|
: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
|
(defn reorder-grid-children
|
||||||
[changes ids]
|
[changes ids]
|
||||||
|
|
|
@ -473,9 +473,14 @@
|
||||||
|
|
||||||
(defn setup-rect
|
(defn setup-rect
|
||||||
"Initializes the selrect and points for a shape."
|
"Initializes the selrect and points for a shape."
|
||||||
[{:keys [selrect points] :as shape}]
|
[{:keys [selrect points transform] :as shape}]
|
||||||
(let [selrect (or selrect (gsh/shape->rect shape))
|
(let [selrect (or selrect (gsh/shape->rect shape))
|
||||||
points (or points (grc/rect->points selrect))]
|
center (grc/rect->center selrect)
|
||||||
|
transform (or transform (gmt/matrix))
|
||||||
|
points (or points
|
||||||
|
(-> selrect
|
||||||
|
(grc/rect->points)
|
||||||
|
(gsh/transform-points center transform)))]
|
||||||
(-> shape
|
(-> shape
|
||||||
(assoc :selrect selrect)
|
(assoc :selrect selrect)
|
||||||
(assoc :points points))))
|
(assoc :points points))))
|
||||||
|
|
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
|
@ -10,5 +10,4 @@ node_modules/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
visual-dashboard.spec.js-snapshots
|
/playwright/**/visual-specs/**/*.png
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
105
frontend/playwright/data/assets/get-file-with-assets.json
Normal file
105
frontend/playwright/data/assets/get-file-with-assets.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,6 @@
|
||||||
import { BasePage } from "./BasePage";
|
import { BasePage } from "./BasePage";
|
||||||
|
|
||||||
export class LoginPage extends BasePage {
|
export class LoginPage extends BasePage {
|
||||||
static async initWithLoggedOutUser(page) {
|
|
||||||
await BasePage.mockRPC(page, "get-profile", "get-profile-anonymous.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(page) {
|
constructor(page) {
|
||||||
super(page);
|
super(page);
|
||||||
this.loginButton = page.getByRole("button", { name: "Login" });
|
this.loginButton = page.getByRole("button", { name: "Login" });
|
||||||
|
@ -24,6 +20,10 @@ export class LoginPage extends BasePage {
|
||||||
await this.loginButton.click();
|
await this.loginButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async initWithLoggedOutUser() {
|
||||||
|
await this.mockRPC("get-profile", "get-profile-anonymous.json");
|
||||||
|
}
|
||||||
|
|
||||||
async setupLoggedInUser() {
|
async setupLoggedInUser() {
|
||||||
await this.mockRPC("get-profile", "logged-in-user/get-profile-logged-in.json");
|
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");
|
await this.mockRPC("get-teams", "logged-in-user/get-teams-default.json");
|
||||||
|
|
|
@ -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;
|
#ws = null;
|
||||||
|
|
||||||
constructor(page) {
|
constructor(page) {
|
||||||
|
@ -56,5 +76,11 @@ export class ViewerPage extends BaseWebSocketPage {
|
||||||
.filter({ hasText: number.toString() })
|
.filter({ hasText: number.toString() })
|
||||||
.click(clickOptions);
|
.click(clickOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async showCode(clickOptions = {}) {
|
||||||
|
await this.page
|
||||||
|
.getByRole("button", { name: 'Inspect (G I)' })
|
||||||
|
.click(clickOptions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||||
this.layers = page.getByTestId("layer-tree");
|
this.layers = page.getByTestId("layer-tree");
|
||||||
this.palette = page.getByTestId("palette");
|
this.palette = page.getByTestId("palette");
|
||||||
this.sidebar = page.getByTestId("left-sidebar");
|
this.sidebar = page.getByTestId("left-sidebar");
|
||||||
|
this.rightSidebar = page.getByTestId("right-sidebar");
|
||||||
this.selectionRect = page.getByTestId("workspace-selection-rect");
|
this.selectionRect = page.getByTestId("workspace-selection-rect");
|
||||||
this.horizontalScrollbar = page.getByTestId("horizontal-scrollbar");
|
this.horizontalScrollbar = page.getByTestId("horizontal-scrollbar");
|
||||||
this.librariesModal = page.getByTestId("libraries-modal");
|
this.librariesModal = page.getByTestId("libraries-modal");
|
||||||
|
@ -119,7 +120,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveSelectionToShape(name) {
|
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.page.mouse.down();
|
||||||
await this.viewport.getByTestId(name).first().hover({ force: true });
|
await this.viewport.getByTestId(name).first().hover({ force: true });
|
||||||
await this.page.mouse.up();
|
await this.page.mouse.up();
|
||||||
|
@ -170,9 +171,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickColorPalette(clickOptions = {}) {
|
async clickColorPalette(clickOptions = {}) {
|
||||||
await this.palette
|
await this.palette.getByRole("button", { name: "Color Palette (Alt+P)" }).click(clickOptions);
|
||||||
.getByRole("button", { name: "Color Palette (Alt+P)" })
|
|
||||||
.click(clickOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickTogglePalettesVisibility(clickOptions = {}) {
|
async clickTogglePalettesVisibility(clickOptions = {}) {
|
||||||
|
|
|
@ -2,7 +2,9 @@ import { test, expect } from "@playwright/test";
|
||||||
import { LoginPage } from "../pages/LoginPage";
|
import { LoginPage } from "../pages/LoginPage";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await LoginPage.initWithLoggedOutUser(page);
|
const login = new LoginPage(page);
|
||||||
|
await login.initWithLoggedOutUser();
|
||||||
|
|
||||||
await page.goto("/#/auth/login");
|
await page.goto("/#/auth/login");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,21 +8,11 @@ test.beforeEach(async ({ page }) => {
|
||||||
const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1";
|
const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1";
|
||||||
const singleBoardPageId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb2";
|
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 }) => {
|
test("Comment is shown with scroll and valid position", async ({ page }) => {
|
||||||
const viewer = new ViewerPage(page);
|
const viewer = new ViewerPage(page);
|
||||||
await viewer.setupLoggedInUser();
|
await viewer.setupLoggedInUser();
|
||||||
await setupFileWithSingleBoard(viewer);
|
await viewer.setupFileWithComments();
|
||||||
|
|
||||||
await viewer.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId });
|
await viewer.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId });
|
||||||
await viewer.showComments();
|
await viewer.showComments();
|
||||||
|
|
|
@ -8,15 +8,6 @@ test.beforeEach(async ({ page }) => {
|
||||||
const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1";
|
const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1";
|
||||||
const singleBoardPageId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb2";
|
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 }) => {
|
test("Clips link area of the logo", async ({ page }) => {
|
||||||
const viewerPage = new ViewerPage(page);
|
const viewerPage = new ViewerPage(page);
|
||||||
await viewerPage.setupLoggedInUser();
|
await viewerPage.setupLoggedInUser();
|
||||||
|
@ -37,7 +28,7 @@ test("Clips link area of the logo", async ({ page }) => {
|
||||||
test("Updates URL with zoom type", async ({ page }) => {
|
test("Updates URL with zoom type", async ({ page }) => {
|
||||||
const viewer = new ViewerPage(page);
|
const viewer = new ViewerPage(page);
|
||||||
await viewer.setupLoggedInUser();
|
await viewer.setupLoggedInUser();
|
||||||
await setupFileWithSingleBoard(viewer);
|
await viewer.setupFileWithSingleBoard(viewer);
|
||||||
|
|
||||||
await viewer.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId });
|
await viewer.goToViewer({ fileId: singleBoardFileId, pageId: singleBoardPageId });
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
Binary file not shown.
Before Width: | Height: | Size: 152 KiB |
|
@ -3,11 +3,7 @@ import DashboardPage from "../pages/DashboardPage";
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await DashboardPage.init(page);
|
await DashboardPage.init(page);
|
||||||
await DashboardPage.mockRPC(
|
await DashboardPage.mockRPC(page, "get-profile", "logged-in-user/get-profile-logged-in-no-onboarding.json");
|
||||||
page,
|
|
||||||
"get-profile",
|
|
||||||
"logged-in-user/get-profile-logged-in-no-onboarding.json",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("User goes to an empty dashboard", async ({ page }) => {
|
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.setupDashboardFull();
|
||||||
|
|
||||||
await dashboardPage.goToSearch();
|
await dashboardPage.goToSearch();
|
||||||
|
|
||||||
await expect(dashboardPage.searchInput).toBeVisible();
|
await expect(dashboardPage.searchInput).toBeVisible();
|
||||||
|
|
||||||
await dashboardPage.searchInput.fill("New");
|
await dashboardPage.searchInput.fill("New");
|
||||||
|
|
||||||
await expect(dashboardPage.searchTitle).toBeVisible();
|
await expect(dashboardPage.searchTitle).toBeVisible();
|
||||||
|
|
||||||
await expect(dashboardPage.newFileName).toBeVisible();
|
await expect(dashboardPage.newFileName).toBeVisible();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -141,9 +134,7 @@ test("User opens user account", async ({ page }) => {
|
||||||
const dashboardPage = new DashboardPage(page);
|
const dashboardPage = new DashboardPage(page);
|
||||||
|
|
||||||
await dashboardPage.goToDashboard();
|
await dashboardPage.goToDashboard();
|
||||||
|
|
||||||
await expect(dashboardPage.userAccount).toBeVisible();
|
await expect(dashboardPage.userAccount).toBeVisible();
|
||||||
|
|
||||||
await dashboardPage.goToAccount();
|
await dashboardPage.goToAccount();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
|
@ -153,11 +144,9 @@ test("User goes to user profile", async ({ page }) => {
|
||||||
const dashboardPage = new DashboardPage(page);
|
const dashboardPage = new DashboardPage(page);
|
||||||
|
|
||||||
await dashboardPage.goToDashboard();
|
await dashboardPage.goToDashboard();
|
||||||
|
|
||||||
await dashboardPage.goToAccount();
|
await dashboardPage.goToAccount();
|
||||||
|
|
||||||
await expect(dashboardPage.userAccountTitle).toBeVisible();
|
await expect(dashboardPage.userAccountTitle).toBeVisible();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -165,13 +154,11 @@ test("User goes to password management section", async ({ page }) => {
|
||||||
const dashboardPage = new DashboardPage(page);
|
const dashboardPage = new DashboardPage(page);
|
||||||
|
|
||||||
await dashboardPage.goToDashboard();
|
await dashboardPage.goToDashboard();
|
||||||
|
|
||||||
await dashboardPage.goToAccount();
|
await dashboardPage.goToAccount();
|
||||||
|
|
||||||
await page.getByText("Password").click();
|
await page.getByText("Password").click();
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Change Password" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Change Password" })).toBeVisible();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -179,91 +166,11 @@ test("User goes to settings section", async ({ page }) => {
|
||||||
const dashboardPage = new DashboardPage(page);
|
const dashboardPage = new DashboardPage(page);
|
||||||
|
|
||||||
await dashboardPage.goToDashboard();
|
await dashboardPage.goToDashboard();
|
||||||
|
|
||||||
await dashboardPage.goToAccount();
|
await dashboardPage.goToAccount();
|
||||||
|
|
||||||
await page.getByTestId("settings-profile").click();
|
await page.getByTestId("settings-profile").click();
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
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();
|
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);
|
const dashboardPage = new DashboardPage(page);
|
||||||
|
|
||||||
await dashboardPage.goToDashboard();
|
await dashboardPage.goToDashboard();
|
||||||
|
|
||||||
await expect(dashboardPage.titleLabel).toBeVisible();
|
await expect(dashboardPage.titleLabel).toBeVisible();
|
||||||
|
|
||||||
await dashboardPage.teamDropdown.click();
|
await dashboardPage.teamDropdown.click();
|
||||||
|
|
||||||
await expect(page.getByText("Create new team")).toBeVisible();
|
await expect(page.getByText("Create new team")).toBeVisible();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
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.setupDashboardFull();
|
||||||
|
|
||||||
await dashboardPage.goToDashboard();
|
await dashboardPage.goToDashboard();
|
||||||
|
|
||||||
await expect(dashboardPage.titleLabel).toBeVisible();
|
await expect(dashboardPage.titleLabel).toBeVisible();
|
||||||
|
|
||||||
await dashboardPage.teamDropdown.click();
|
await dashboardPage.teamDropdown.click();
|
||||||
|
|
||||||
await expect(page.getByText("Second Team")).toBeVisible();
|
await expect(page.getByText("Second Team")).toBeVisible();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("User goes to second team", async ({ page }) => {
|
test("User goes to second team", async ({ page }) => {
|
||||||
const dashboardPage = new DashboardPage(page);
|
const dashboardPage = new DashboardPage(page);
|
||||||
await dashboardPage.setupDashboardFull();
|
await dashboardPage.setupDashboardFull();
|
||||||
|
|
||||||
await dashboardPage.goToDashboard();
|
await dashboardPage.goToDashboard();
|
||||||
|
|
||||||
await dashboardPage.teamDropdown.click();
|
await dashboardPage.teamDropdown.click();
|
||||||
|
|
||||||
await expect(page.getByText("Second Team")).toBeVisible();
|
await expect(page.getByText("Second Team")).toBeVisible();
|
||||||
|
|
||||||
await page.getByText("Second Team").click();
|
await page.getByText("Second Team").click();
|
||||||
|
|
||||||
await expect(page.getByText("Team Up")).toBeVisible();
|
await expect(page.getByText("Team Up")).toBeVisible();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -320,13 +220,11 @@ test("User opens team management dropdown", async ({ page }) => {
|
||||||
await dashboardPage.setupDashboardFull();
|
await dashboardPage.setupDashboardFull();
|
||||||
|
|
||||||
await dashboardPage.goToSecondTeamDashboard();
|
await dashboardPage.goToSecondTeamDashboard();
|
||||||
|
|
||||||
await expect(page.getByText("Team Up")).toBeVisible();
|
await expect(page.getByText("Team Up")).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "team-management" }).click();
|
await page.getByRole("button", { name: "team-management" }).click();
|
||||||
|
|
||||||
await expect(page.getByTestId("team-members")).toBeVisible();
|
await expect(page.getByTestId("team-members")).toBeVisible();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -365,24 +263,20 @@ test("User goes to a complete invitations section", async ({ page }) => {
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test("User invite people to the team", async ({ page }) => {
|
test("User invite people to the team", async ({ page }) => {
|
||||||
const dashboardPage = new DashboardPage(page);
|
const dashboardPage = new DashboardPage(page);
|
||||||
await dashboardPage.setupDashboardFull();
|
await dashboardPage.setupDashboardFull();
|
||||||
await dashboardPage.setupTeamInvitationsEmpty();
|
await dashboardPage.setupTeamInvitationsEmpty();
|
||||||
|
|
||||||
await dashboardPage.goToSecondTeamInvitationsSection();
|
await dashboardPage.goToSecondTeamInvitationsSection();
|
||||||
|
|
||||||
await expect(page.getByTestId("invite-member")).toBeVisible();
|
await expect(page.getByTestId("invite-member")).toBeVisible();
|
||||||
|
|
||||||
await page.getByTestId("invite-member").click();
|
await page.getByTestId("invite-member").click();
|
||||||
|
|
||||||
await expect(page.getByText("Invite with the role")).toBeVisible();
|
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(page.getByText("Send invitation")).not.toBeDisabled();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -394,7 +288,6 @@ test("User goes to an empty webhook section", async ({ page }) => {
|
||||||
await dashboardPage.goToSecondTeamWebhooksSection();
|
await dashboardPage.goToSecondTeamWebhooksSection();
|
||||||
|
|
||||||
await expect(page.getByText("No webhooks created so far.")).toBeVisible();
|
await expect(page.getByText("No webhooks created so far.")).toBeVisible();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -406,7 +299,6 @@ test("User goes to a complete webhook section", async ({ page }) => {
|
||||||
await dashboardPage.goToSecondTeamWebhooksSection();
|
await dashboardPage.goToSecondTeamWebhooksSection();
|
||||||
|
|
||||||
await expect(page.getByText("https://www.google.com")).toBeVisible();
|
await expect(page.getByText("https://www.google.com")).toBeVisible();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -418,6 +310,5 @@ test("User goes to the team settings section", async ({ page }) => {
|
||||||
await dashboardPage.goToSecondTeamSettingsSection();
|
await dashboardPage.goToSecondTeamSettingsSection();
|
||||||
|
|
||||||
await expect(page.getByText("TEAM INFO")).toBeVisible();
|
await expect(page.getByText("TEAM INFO")).toBeVisible();
|
||||||
|
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
37
frontend/playwright/ui/visual-specs/visual-login.spec.js
Normal file
37
frontend/playwright/ui/visual-specs/visual-login.spec.js
Normal file
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
117
frontend/playwright/ui/visual-specs/visual-viewer.spec.js
Normal file
117
frontend/playwright/ui/visual-specs/visual-viewer.spec.js
Normal file
|
@ -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();
|
||||||
|
});
|
124
frontend/playwright/ui/visual-specs/workspace.spec.js
Normal file
124
frontend/playwright/ui/visual-specs/workspace.spec.js
Normal file
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -69,7 +69,8 @@
|
||||||
:enable-onboarding-questions
|
:enable-onboarding-questions
|
||||||
:enable-onboarding-newsletter
|
:enable-onboarding-newsletter
|
||||||
:enable-dashboard-templates-section
|
:enable-dashboard-templates-section
|
||||||
:enable-google-fonts-provider])
|
:enable-google-fonts-provider
|
||||||
|
:enable-component-thumbnails])
|
||||||
|
|
||||||
(defn- parse-flags
|
(defn- parse-flags
|
||||||
[global]
|
[global]
|
||||||
|
|
|
@ -29,18 +29,24 @@
|
||||||
(def commit?
|
(def commit?
|
||||||
(ptk/type? ::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
|
"Given a commit, send the changes to the worker for updating the
|
||||||
indexes."
|
indexes."
|
||||||
[{:keys [changes] :as commit}]
|
[commit attr]
|
||||||
(ptk/reify ::update-indexes
|
(ptk/reify ::update-indexes
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ _ _]
|
||||||
(let [changes (->> changes
|
(let [changes (->> (get commit attr)
|
||||||
(map (fn [{:keys [id type page] :as change}]
|
(map fix-page-id)
|
||||||
(cond-> change
|
|
||||||
(and (page-change? type) (nil? (:page-id change)))
|
|
||||||
(assoc :page-id (or id (:id page))))))
|
|
||||||
(filter :page-id)
|
(filter :page-id)
|
||||||
(group-by :page-id))]
|
(group-by :page-id))]
|
||||||
|
|
||||||
|
@ -58,37 +64,12 @@
|
||||||
(map (d/getf (:index persistence)))
|
(map (d/getf (:index persistence)))
|
||||||
(not-empty)))
|
(not-empty)))
|
||||||
|
|
||||||
(defn commit
|
(def ^:private xf:map-page-id
|
||||||
"Create a commit event instance"
|
(map :page-id))
|
||||||
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
|
|
||||||
file-id file-revn undo-group tags stack-undo? source]}]
|
|
||||||
|
|
||||||
(dm/assert!
|
|
||||||
"expect valid vector of changes"
|
|
||||||
(and (cpc/check-changes! redo-changes)
|
|
||||||
(cpc/check-changes! undo-changes)))
|
|
||||||
|
|
||||||
(let [commit-id (or commit-id (uuid/next))
|
|
||||||
source (d/nilv source :local)
|
|
||||||
commit {:id commit-id
|
|
||||||
:created-at (dt/now)
|
|
||||||
:source source
|
|
||||||
:origin (ptk/type origin)
|
|
||||||
:features features
|
|
||||||
:file-id file-id
|
|
||||||
:file-revn file-revn
|
|
||||||
:changes redo-changes
|
|
||||||
:redo-changes redo-changes
|
|
||||||
:undo-changes undo-changes
|
|
||||||
:save-undo? save-undo?
|
|
||||||
:undo-group undo-group
|
|
||||||
:tags tags
|
|
||||||
:stack-undo? stack-undo?}]
|
|
||||||
|
|
||||||
(ptk/reify ::commit
|
|
||||||
cljs.core/IDeref
|
|
||||||
(-deref [_] commit)
|
|
||||||
|
|
||||||
|
(defn- apply-changes-localy
|
||||||
|
[{:keys [file-id redo-changes] :as commit} pending]
|
||||||
|
(ptk/reify ::apply-changes-localy
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [current-file-id (get state :current-file-id)
|
(let [current-file-id (get state :current-file-id)
|
||||||
|
@ -96,11 +77,6 @@
|
||||||
[:workspace-data]
|
[:workspace-data]
|
||||||
[:workspace-libraries file-id :data])
|
[:workspace-libraries file-id :data])
|
||||||
|
|
||||||
not-local? (not= source :local)
|
|
||||||
pending (if not-local?
|
|
||||||
(get-pending-commits state)
|
|
||||||
nil)
|
|
||||||
|
|
||||||
undo-changes (if pending
|
undo-changes (if pending
|
||||||
(->> pending
|
(->> pending
|
||||||
(map :undo-changes)
|
(map :undo-changes)
|
||||||
|
@ -119,8 +95,56 @@
|
||||||
(fn [file]
|
(fn [file]
|
||||||
(let [file (cpc/process-changes file undo-changes false)
|
(let [file (cpc/process-changes file undo-changes false)
|
||||||
file (cpc/process-changes file redo-changes false)
|
file (cpc/process-changes file redo-changes false)
|
||||||
pids (into #{} (map :page-id) redo-changes)]
|
pids (into #{} xf:map-page-id redo-changes)]
|
||||||
(reduce #(ctst/update-object-indices %1 %2) file pids)))))))))
|
(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
|
||||||
|
file-id file-revn undo-group tags stack-undo? source]}]
|
||||||
|
|
||||||
|
(dm/assert!
|
||||||
|
"expect valid vector of changes"
|
||||||
|
(and (cpc/check-changes! redo-changes)
|
||||||
|
(cpc/check-changes! undo-changes)))
|
||||||
|
|
||||||
|
(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
|
||||||
|
:origin (ptk/type origin)
|
||||||
|
:features features
|
||||||
|
:file-id file-id
|
||||||
|
:file-revn file-revn
|
||||||
|
:changes redo-changes
|
||||||
|
:redo-changes redo-changes
|
||||||
|
:undo-changes undo-changes
|
||||||
|
:save-undo? save-undo?
|
||||||
|
:undo-group undo-group
|
||||||
|
:tags tags
|
||||||
|
:stack-undo? stack-undo?}]
|
||||||
|
|
||||||
|
(ptk/reify ::commit
|
||||||
|
cljs.core/IDeref
|
||||||
|
(-deref [_] commit)
|
||||||
|
|
||||||
|
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
|
(defn- resolve-file-revn
|
||||||
[state file-id]
|
[state file-id]
|
||||||
|
|
|
@ -211,14 +211,6 @@
|
||||||
(update-status :pending)))
|
(update-status :pending)))
|
||||||
(rx/take-until stoper-s))
|
(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
|
;; Here we watch for local commits, buffer them in a small
|
||||||
;; chunks (very near in time commits) and append them to the
|
;; chunks (very near in time commits) and append them to the
|
||||||
;; persistence queue
|
;; persistence queue
|
||||||
|
@ -237,6 +229,5 @@
|
||||||
(rx/map deref)
|
(rx/map deref)
|
||||||
(rx/filter #(= :remote (:source %)))
|
(rx/filter #(= :remote (:source %)))
|
||||||
(rx/mapcat (fn [{:keys [file-id file-revn] :as commit}]
|
(rx/mapcat (fn [{:keys [file-id file-revn] :as commit}]
|
||||||
(rx/of (update-file-revn file-id file-revn)
|
(rx/of (update-file-revn file-id file-revn))))
|
||||||
(dch/update-indexes commit))))
|
|
||||||
(rx/take-until stoper-s)))))))
|
(rx/take-until stoper-s)))))))
|
||||||
|
|
|
@ -605,11 +605,12 @@
|
||||||
|
|
||||||
(-> state
|
(-> state
|
||||||
(dissoc :gradient :stops :editing-stop)
|
(dissoc :gradient :stops :editing-stop)
|
||||||
(cond-> (not= :image (:type state))
|
(cond-> (not= :image type)
|
||||||
(assoc :type :color))))))))
|
(assoc :type :color))))))))
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(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))]
|
(let [formated-color (get-color-from-colorpicker-state (:colorpicker state))]
|
||||||
(rx/of (dwl/add-recent-color formated-color)))))))
|
(rx/of (dwl/add-recent-color formated-color)))))))
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
[app.common.types.shape.layout :as ctl]
|
[app.common.types.shape.layout :as ctl]
|
||||||
[app.common.types.typography :as ctt]
|
[app.common.types.typography :as ctt]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
[app.main.data.changes :as dch]
|
[app.main.data.changes :as dch]
|
||||||
[app.main.data.comments :as dc]
|
[app.main.data.comments :as dc]
|
||||||
[app.main.data.events :as ev]
|
[app.main.data.events :as ev]
|
||||||
|
@ -1203,7 +1204,7 @@
|
||||||
(rx/debounce 5000)
|
(rx/debounce 5000)
|
||||||
(rx/tap #(log/trc :hint "buffer initialized")))]
|
(rx/tap #(log/trc :hint "buffer initialized")))]
|
||||||
|
|
||||||
(when components-v2?
|
(when (and components-v2? (contains? cf/flags :component-thumbnails))
|
||||||
(->> (rx/merge
|
(->> (rx/merge
|
||||||
changes-s
|
changes-s
|
||||||
|
|
||||||
|
|
|
@ -217,31 +217,7 @@
|
||||||
(-deref [_] {:changes changes})
|
(-deref [_] {:changes changes})
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ _ _]
|
||||||
(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
|
;; The commit event is responsible to apply the data localy
|
||||||
;; and update the persistence internal state with the updated
|
;; and update the persistence internal state with the updated
|
||||||
;; file-revn
|
;; file-revn
|
||||||
|
@ -249,8 +225,8 @@
|
||||||
:file-revn revn
|
:file-revn revn
|
||||||
:save-undo? false
|
:save-undo? false
|
||||||
:source :remote
|
:source :remote
|
||||||
:redo-changes changes
|
:redo-changes (vec changes)
|
||||||
:undo-changes []}))))))
|
:undo-changes []})))))
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:handle-library-change
|
schema:handle-library-change
|
||||||
|
|
|
@ -46,8 +46,8 @@
|
||||||
|
|
||||||
(defn update-shapes
|
(defn update-shapes
|
||||||
([ids update-fn] (update-shapes ids update-fn nil))
|
([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?]
|
([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-remote? false ignore-touched false with-objects? false}}]
|
:or {reg-objects? false save-undo? true stack-undo? false ignore-touched false with-objects? false}}]
|
||||||
|
|
||||||
(dm/assert!
|
(dm/assert!
|
||||||
"expected a valid coll of uuid's"
|
"expected a valid coll of uuid's"
|
||||||
|
@ -84,8 +84,7 @@
|
||||||
changes (add-undo-group changes state)]
|
changes (add-undo-group changes state)]
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(if (seq (:redo-changes changes))
|
(if (seq (:redo-changes changes))
|
||||||
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))
|
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))]
|
||||||
changes (cond-> changes ignore-remote? (pcb/ignore-remote))]
|
|
||||||
(rx/of (dch/commit-changes changes)))
|
(rx/of (dch/commit-changes changes)))
|
||||||
(rx/empty))
|
(rx/empty))
|
||||||
|
|
||||||
|
|
|
@ -486,7 +486,6 @@
|
||||||
(rx/of (dwu/start-undo-transaction undo-id)
|
(rx/of (dwu/start-undo-transaction undo-id)
|
||||||
(dwsh/update-shapes ids update-fn {:reg-objects? true
|
(dwsh/update-shapes ids update-fn {:reg-objects? true
|
||||||
:stack-undo? true
|
:stack-undo? true
|
||||||
:ignore-remote? true
|
|
||||||
:ignore-touched true})
|
:ignore-touched true})
|
||||||
(ptk/data-event :layout/update {:ids ids})
|
(ptk/data-event :layout/update {:ids ids})
|
||||||
(dwu/commit-undo-transaction undo-id))))))))
|
(dwu/commit-undo-transaction undo-id))))))))
|
||||||
|
@ -631,7 +630,7 @@
|
||||||
(fn [shape]
|
(fn [shape]
|
||||||
(-> shape
|
(-> shape
|
||||||
(assoc :position-data (get position-data (:id 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]
|
(rx/of (fn [state]
|
||||||
(dissoc state ::update-position-data-debounce ::update-position-data))))))))
|
(dissoc state ::update-position-data-debounce ::update-position-data))))))))
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.thumbnails :as thc]
|
[app.common.thumbnails :as thc]
|
||||||
[app.config :as cf]
|
[app.common.uuid :as uuid]
|
||||||
[app.main.data.changes :as dch]
|
[app.main.data.changes :as dch]
|
||||||
[app.main.data.persistence :as-alias dps]
|
[app.main.data.persistence :as-alias dps]
|
||||||
[app.main.data.workspace.notifications :as-alias wnt]
|
[app.main.data.workspace.notifications :as-alias wnt]
|
||||||
|
@ -19,7 +19,6 @@
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.render :as render]
|
[app.main.render :as render]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.util.http :as http]
|
|
||||||
[app.util.queue :as q]
|
[app.util.queue :as q]
|
||||||
[app.util.time :as tp]
|
[app.util.time :as tp]
|
||||||
[app.util.timers :as tm]
|
[app.util.timers :as tm]
|
||||||
|
@ -149,34 +148,34 @@
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(l/dbg :hint "update thumbnail" :requester requester :object-id object-id :tag tag)
|
(l/dbg :hint "update thumbnail" :requester requester :object-id object-id :tag tag)
|
||||||
|
(let [tp (tp/tpoint-ms)]
|
||||||
;; Send the update to the back-end
|
;; Send the update to the back-end
|
||||||
(->> (request-thumbnail state file-id page-id frame-id tag)
|
(->> (request-thumbnail state file-id page-id frame-id tag)
|
||||||
(rx/mapcat (fn [blob]
|
(rx/mapcat (fn [blob]
|
||||||
;; Send the data to backend
|
(let [uri (wapi/create-uri blob)
|
||||||
(let [params {:file-id file-id
|
params {:file-id file-id
|
||||||
:object-id object-id
|
:object-id object-id
|
||||||
:media blob
|
:media blob
|
||||||
:tag (or tag "frame")}]
|
:tag (or tag "frame")}]
|
||||||
(rp/cmd! :create-file-object-thumbnail params))))
|
|
||||||
|
|
||||||
(rx/mapcat (fn [{:keys [object-id media-id]}]
|
(rx/merge
|
||||||
(let [uri (cf/resolve-media media-id)]
|
(rx/of (assoc-thumbnail object-id uri))
|
||||||
;; We perform this request just for
|
(->> (rp/cmd! :create-file-object-thumbnail params)
|
||||||
;; populate the browser CACHE and avoid
|
(rx/catch rx/empty)
|
||||||
;; unnecesary image flickering
|
(rx/ignore))))))
|
||||||
(->> (http/send! {:uri uri :method :get})
|
|
||||||
(rx/map #(assoc-thumbnail object-id uri))))))
|
|
||||||
|
|
||||||
(rx/catch (fn [cause]
|
(rx/catch (fn [cause]
|
||||||
(.error js/console cause)
|
(.error js/console cause)
|
||||||
(rx/empty)))
|
(rx/empty)))
|
||||||
|
|
||||||
|
(rx/tap #(l/trc :hint "thumbnail updated" :elapsed (dm/str (tp) "ms")))
|
||||||
|
|
||||||
;; We cancel all the stream if user starts editing while
|
;; We cancel all the stream if user starts editing while
|
||||||
;; thumbnail is generating
|
;; thumbnail is generating
|
||||||
(rx/take-until
|
(rx/take-until
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? ::clear-thumbnail))
|
(rx/filter (ptk/type? ::clear-thumbnail))
|
||||||
(rx/filter #(= (deref %) object-id)))))))))
|
(rx/filter #(= (deref %) object-id))))))))))
|
||||||
|
|
||||||
(defn- extract-root-frame-changes
|
(defn- extract-root-frame-changes
|
||||||
"Process a changes set in a commit to extract the frames that are changing"
|
"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 %)))
|
:mov-objects (->> (:shapes change) (map #(vector page-id %)))
|
||||||
[]))
|
[]))
|
||||||
|
|
||||||
get-frame-id
|
get-frame-ids
|
||||||
(fn [[_ id]]
|
(fn get-frame-ids [id]
|
||||||
(let [old-objects (wsh/lookup-data-objects old-data page-id)
|
(let [old-objects (wsh/lookup-data-objects old-data page-id)
|
||||||
new-objects (wsh/lookup-data-objects new-data page-id)
|
new-objects (wsh/lookup-data-objects new-data page-id)
|
||||||
|
|
||||||
|
@ -208,12 +207,21 @@
|
||||||
(conj old-frame-id)
|
(conj old-frame-id)
|
||||||
|
|
||||||
(cfh/root-frame? new-objects new-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 #{}
|
(into #{}
|
||||||
(comp (mapcat extract-ids)
|
(comp (mapcat extract-ids)
|
||||||
(filter (fn [[page-id']] (= page-id page-id')))
|
(filter (fn [[page-id']] (= page-id page-id')))
|
||||||
(mapcat get-frame-id))
|
(map (fn [[_ id]] id))
|
||||||
|
(mapcat get-frame-ids))
|
||||||
changes)))
|
changes)))
|
||||||
|
|
||||||
(defn watch-state-changes
|
(defn watch-state-changes
|
||||||
|
|
|
@ -137,6 +137,7 @@
|
||||||
(->> (http/send! {:method :post
|
(->> (http/send! {:method :post
|
||||||
:uri uri
|
:uri uri
|
||||||
:credentials "include"
|
:credentials "include"
|
||||||
|
:headers {"x-external-session-id" (cf/external-session-id)}
|
||||||
:query params})
|
:query params})
|
||||||
(rx/map http/conditional-decode-transit)
|
(rx/map http/conditional-decode-transit)
|
||||||
(rx/mapcat handle-response))))
|
(rx/mapcat handle-response))))
|
||||||
|
@ -146,6 +147,7 @@
|
||||||
(->> (http/send! {:method :post
|
(->> (http/send! {:method :post
|
||||||
:uri (u/join cf/public-uri "api/export")
|
:uri (u/join cf/public-uri "api/export")
|
||||||
:body (http/transit-data (dissoc params :blob?))
|
:body (http/transit-data (dissoc params :blob?))
|
||||||
|
:headers {"x-external-session-id" (cf/external-session-id)}
|
||||||
:credentials "include"
|
:credentials "include"
|
||||||
:response-type (if blob? :blob :text)})
|
:response-type (if blob? :blob :text)})
|
||||||
(rx/map http/conditional-decode-transit)
|
(rx/map http/conditional-decode-transit)
|
||||||
|
@ -165,6 +167,7 @@
|
||||||
(->> (http/send! {:method :post
|
(->> (http/send! {:method :post
|
||||||
:uri (u/join cf/public-uri "api/rpc/command/" (name id))
|
:uri (u/join cf/public-uri "api/rpc/command/" (name id))
|
||||||
:credentials "include"
|
:credentials "include"
|
||||||
|
:headers {"x-external-session-id" (cf/external-session-id)}
|
||||||
:body (http/form-data params)})
|
:body (http/form-data params)})
|
||||||
(rx/map http/conditional-decode-transit)
|
(rx/map http/conditional-decode-transit)
|
||||||
(rx/mapcat handle-response)))
|
(rx/mapcat handle-response)))
|
||||||
|
|
|
@ -105,7 +105,7 @@
|
||||||
{::ev/name "onboarding-step"
|
{::ev/name "onboarding-step"
|
||||||
:label "team:create-team-and-invite-later"
|
:label "team:create-team-and-invite-later"
|
||||||
:team-name name
|
:team-name name
|
||||||
:step 7})
|
:step 8})
|
||||||
(ptk/data-event ::ev/event
|
(ptk/data-event ::ev/event
|
||||||
{::ev/name "onboarding-finish"})))))
|
{::ev/name "onboarding-finish"})))))
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@
|
||||||
:invites (count emails)
|
:invites (count emails)
|
||||||
:team-name name
|
:team-name name
|
||||||
:role (:role params)
|
:role (:role params)
|
||||||
:step 7})
|
:step 8})
|
||||||
(ptk/data-event ::ev/event
|
(ptk/data-event ::ev/event
|
||||||
{::ev/name "onboarding-finish"})))))
|
{::ev/name "onboarding-finish"})))))
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,7 @@
|
||||||
(st/emit! (dcm/update-options {:show-sidebar? (not mode)})))))]
|
(st/emit! (dcm/update-options {:show-sidebar? (not mode)})))))]
|
||||||
|
|
||||||
[:div {:class (stl/css :view-options)
|
[:div {:class (stl/css :view-options)
|
||||||
|
:data-testid "viewer-comments-dropdown"
|
||||||
:on-click toggle-dropdown}
|
:on-click toggle-dropdown}
|
||||||
[:span {:class (stl/css :dropdown-title)} (tr "labels.comments")]
|
[:span {:class (stl/css :dropdown-title)} (tr "labels.comments")]
|
||||||
[:span {:class (stl/css :icon-dropdown)} i/arrow]
|
[:span {:class (stl/css :icon-dropdown)} i/arrow]
|
||||||
|
|
|
@ -29,7 +29,8 @@
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(defn fix-position [shape]
|
(defn fix-position
|
||||||
|
[shape]
|
||||||
(if-let [modifiers (:modifiers shape)]
|
(if-let [modifiers (:modifiers shape)]
|
||||||
(let [shape' (gsh/transform-shape shape modifiers)
|
(let [shape' (gsh/transform-shape shape modifiers)
|
||||||
|
|
||||||
|
|
|
@ -169,6 +169,7 @@
|
||||||
:expanded (> size 276))
|
:expanded (> size 276))
|
||||||
|
|
||||||
:id "right-sidebar-aside"
|
:id "right-sidebar-aside"
|
||||||
|
:data-testid "right-sidebar"
|
||||||
:data-size (str size)
|
:data-size (str size)
|
||||||
:style #js {"--width" (if can-be-expanded? (dm/str size "px") 276)}}
|
:style #js {"--width" (if can-be-expanded? (dm/str size "px") 276)}}
|
||||||
(when can-be-expanded?
|
(when can-be-expanded?
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
[app.common.types.component :as ctk]
|
[app.common.types.component :as ctk]
|
||||||
[app.common.types.container :as ctn]
|
[app.common.types.container :as ctn]
|
||||||
[app.common.types.file :as ctf]
|
[app.common.types.file :as ctf]
|
||||||
|
[app.config :as cf]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
[app.main.data.workspace.libraries :as dwl]
|
[app.main.data.workspace.libraries :as dwl]
|
||||||
|
@ -287,7 +288,7 @@
|
||||||
(when (< @retry 3)
|
(when (< @retry 3)
|
||||||
(inc retry))))]
|
(inc retry))))]
|
||||||
|
|
||||||
(if (some? thumbnail-uri)
|
(if (and (some? thumbnail-uri) (contains? cf/flags :component-thumbnails))
|
||||||
[:& component-svg-thumbnail
|
[:& component-svg-thumbnail
|
||||||
{:thumbnail-uri thumbnail-uri
|
{:thumbnail-uri thumbnail-uri
|
||||||
:class class
|
:class class
|
||||||
|
|
|
@ -520,9 +520,11 @@
|
||||||
:name "listing-style"}
|
:name "listing-style"}
|
||||||
[:& radio-button {:icon i/view-as-list
|
[:& radio-button {:icon i/view-as-list
|
||||||
:value "list"
|
:value "list"
|
||||||
|
:title (tr "workspace.assets.list-view")
|
||||||
:id "opt-list"}]
|
:id "opt-list"}]
|
||||||
[:& radio-button {:icon i/flex-grid
|
[:& radio-button {:icon i/flex-grid
|
||||||
:value "grid"
|
:value "grid"
|
||||||
|
:title (tr "workspace.assets.grid-view")
|
||||||
:id "opt-grid"}]]])
|
:id "opt-grid"}]]])
|
||||||
|
|
||||||
(when (and components-v2 (not read-only?) local?)
|
(when (and components-v2 (not read-only?) local?)
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
(let [{:keys [text-align]} values
|
(let [{:keys [text-align]} values
|
||||||
handle-change
|
handle-change
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-blur)
|
(mf/deps on-change on-blur)
|
||||||
(fn [value]
|
(fn [value]
|
||||||
(on-change {:text-align value})
|
(on-change {:text-align value})
|
||||||
(when (some? on-blur) (on-blur))))]
|
(when (some? on-blur) (on-blur))))]
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
(let [direction (:text-direction values)
|
(let [direction (:text-direction values)
|
||||||
handle-change
|
handle-change
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps direction)
|
(mf/deps on-change on-blur direction)
|
||||||
(fn [value]
|
(fn [value]
|
||||||
(let [dir (if (= value direction)
|
(let [dir (if (= value direction)
|
||||||
"none"
|
"none"
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
vertical-align (or vertical-align "top")
|
vertical-align (or vertical-align "top")
|
||||||
handle-change
|
handle-change
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-blur)
|
(mf/deps on-change on-blur)
|
||||||
(fn [value]
|
(fn [value]
|
||||||
(on-change {:vertical-align value})
|
(on-change {:vertical-align value})
|
||||||
(when (some? on-blur) (on-blur))))]
|
(when (some? on-blur) (on-blur))))]
|
||||||
|
@ -154,7 +154,7 @@
|
||||||
(let [text-decoration (or (:text-decoration values) "none")
|
(let [text-decoration (or (:text-decoration values) "none")
|
||||||
handle-change
|
handle-change
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps text-decoration)
|
(mf/deps on-change on-blur text-decoration)
|
||||||
(fn [value]
|
(fn [value]
|
||||||
(let [decoration (if (= value text-decoration)
|
(let [decoration (if (= value text-decoration)
|
||||||
"none"
|
"none"
|
||||||
|
|
|
@ -2558,11 +2558,11 @@ msgstr "Prototyping"
|
||||||
|
|
||||||
#: src/app/main/ui/onboarding/questions.cljs
|
#: src/app/main/ui/onboarding/questions.cljs
|
||||||
msgid "onboarding.questions.start-with.ds"
|
msgid "onboarding.questions.start-with.ds"
|
||||||
msgstr "Creating Desing Systems"
|
msgstr "Creating Design Systems"
|
||||||
|
|
||||||
#: src/app/main/ui/onboarding/questions.cljs
|
#: src/app/main/ui/onboarding/questions.cljs
|
||||||
msgid "onboarding.questions.start-with.code"
|
msgid "onboarding.questions.start-with.code"
|
||||||
msgstr "Generating real code designs"
|
msgstr "Generating real code from designs"
|
||||||
|
|
||||||
#: src/app/main/ui/onboarding/questions.cljs
|
#: src/app/main/ui/onboarding/questions.cljs
|
||||||
msgid "onboarding.questions.step5.title"
|
msgid "onboarding.questions.step5.title"
|
||||||
|
@ -3396,6 +3396,14 @@ msgstr "Sort"
|
||||||
msgid "workspace.assets.typography"
|
msgid "workspace.assets.typography"
|
||||||
msgstr "Typographies"
|
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
|
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
|
||||||
msgid "workspace.assets.typography.font-id"
|
msgid "workspace.assets.typography.font-id"
|
||||||
msgstr "Font"
|
msgstr "Font"
|
||||||
|
|
|
@ -3465,6 +3465,14 @@ msgstr "Ordenar"
|
||||||
msgid "workspace.assets.typography"
|
msgid "workspace.assets.typography"
|
||||||
msgstr "Tipografías"
|
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
|
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs
|
||||||
msgid "workspace.assets.typography.font-id"
|
msgid "workspace.assets.typography.font-id"
|
||||||
msgstr "Fuente"
|
msgstr "Fuente"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue