Compare commits
46 Commits
niwinz-dev
...
elenatorro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4baa894ee4 | ||
|
|
64b892f82d | ||
|
|
04185b3544 | ||
|
|
0a01fc8af9 | ||
|
|
ae624b3728 | ||
|
|
a48b719966 | ||
|
|
6425c0cb7d | ||
|
|
368f4cfe81 | ||
|
|
fdffa14d75 | ||
|
|
7fe965a870 | ||
|
|
127fa931c7 | ||
|
|
30413dbc66 | ||
|
|
2810ae681f | ||
|
|
d706bb7c8d | ||
|
|
ef271db879 | ||
|
|
ec5e814a72 | ||
|
|
c44fd2dd1d | ||
|
|
6aa797f51b | ||
|
|
3cc54fd988 | ||
|
|
2233f34a15 | ||
|
|
839bb470df | ||
|
|
450ce869ba | ||
|
|
665587d492 | ||
|
|
8aaa953604 | ||
|
|
a2cb84ba0d | ||
|
|
639952abc8 | ||
|
|
2d63730bfa | ||
|
|
c1638817b2 | ||
|
|
76f6f71e02 | ||
|
|
0a700864c9 | ||
|
|
04ce4c3233 | ||
|
|
befcca86df | ||
|
|
b7bae3850b | ||
|
|
3f05dae455 | ||
|
|
4a887840c6 | ||
|
|
10cf2c7f35 | ||
|
|
d048a251f1 | ||
|
|
0b3fc6a663 | ||
|
|
363b4e3778 | ||
|
|
f248ab5644 | ||
|
|
33da6fbec2 | ||
|
|
07bede8ba2 | ||
|
|
718f42aa94 | ||
|
|
7594f1883b | ||
|
|
5c2dde7308 | ||
|
|
483a1bd703 |
4
.github/workflows/release.yml
vendored
@@ -68,12 +68,12 @@ jobs:
|
||||
for image in "${IMAGES[@]}"; do
|
||||
skopeo copy --all \
|
||||
docker://$DOCKER_REGISTRY/$image:$TAG \
|
||||
docker://docker.io/$PUB_DOCKER_USERNAME/$image:$TAG
|
||||
docker://docker.io/penpotapp/$image:$TAG
|
||||
|
||||
for alias in main latest; do
|
||||
skopeo copy --all \
|
||||
docker://$DOCKER_REGISTRY/$image:$TAG \
|
||||
docker://docker.io/$PUB_DOCKER_USERNAME/$image:$alias
|
||||
docker://docker.io/penpotapp/$image:$alias
|
||||
done
|
||||
done
|
||||
|
||||
|
||||
50
CHANGES.md
@@ -4,11 +4,46 @@
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
- The backend RPC API URLS are changed from `/api/rpc/command/<name>`
|
||||
to `/api/main/methods/<name>` (the previou PATH is preserved for
|
||||
backward compatibility; however, if you are a user of this API, it
|
||||
is strongly recommended that you adapt your code to use the new
|
||||
PATH.
|
||||
#### Backend RPC API changes
|
||||
|
||||
The backend RPC API URLS are changed from `/api/rpc/command/<name>` to
|
||||
`/api/main/methods/<name>` (the previou PATH is preserved for backward
|
||||
compatibility; however, if you are a user of this API, it is strongly
|
||||
recommended that you adapt your code to use the new PATH.
|
||||
|
||||
|
||||
#### Updated SSO Callback URL
|
||||
|
||||
The OAuth / Single Sign-On (SSO) callback endpoint has changed to
|
||||
align with the new OpenID Connect (OIDC) implementation.
|
||||
|
||||
Old callback URL:
|
||||
|
||||
```
|
||||
https://<your_domain>/api/auth/oauth/<oauth_provider>/callback
|
||||
```
|
||||
|
||||
New callback URL:
|
||||
|
||||
```
|
||||
https://<your_domain>/api/auth/oidc/callback
|
||||
```
|
||||
|
||||
**Action required:**
|
||||
|
||||
If you have SSO/Social-Auth configured on your on-premise instance,
|
||||
the following actions are required before update:
|
||||
|
||||
Update your OAuth or SSO provider configuration (e.g., Okta, Google,
|
||||
Azure AD, etc.) to use the new callback URL. Failure to update may
|
||||
result in authentication failures after upgrading.
|
||||
|
||||
**Reason for change:**
|
||||
|
||||
This update standardizes all authentication flows under the single URL
|
||||
and makis it more modular, enabling the ability to configure SSO auth
|
||||
provider dinamically.
|
||||
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
@@ -18,6 +53,7 @@
|
||||
|
||||
- Select boards to export as PDF [Taiga #12320](https://tree.taiga.io/project/penpot/issue/12320)
|
||||
- Toggle for switching boolean property values [Taiga #12341](https://tree.taiga.io/project/penpot/us/12341)
|
||||
- Add auth flow changes [Taiga #12333](https://tree.taiga.io/project/penpot/us/12333)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -33,6 +69,10 @@
|
||||
- Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312)
|
||||
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
|
||||
|
||||
## 2.11.1
|
||||
|
||||
- Fix WEBP shape export on docker images [Taiga #3838](https://tree.taiga.io/project/penpot/issue/3838)
|
||||
|
||||
## 2.11.0
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
@@ -7,12 +7,12 @@ export PENPOT_HOST=devenv
|
||||
|
||||
export PENPOT_FLAGS="\
|
||||
$PENPOT_FLAGS \
|
||||
enable-login-with-ldap \
|
||||
enable-login-with-password
|
||||
enable-login-with-oidc \
|
||||
enable-login-with-google \
|
||||
enable-login-with-github \
|
||||
enable-login-with-gitlab \
|
||||
disable-login-with-ldap \
|
||||
disable-login-with-oidc \
|
||||
disable-login-with-google \
|
||||
disable-login-with-github \
|
||||
disable-login-with-gitlab \
|
||||
enable-backend-worker \
|
||||
enable-backend-asserts \
|
||||
disable-feature-fdata-pointer-map \
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
[:google-client-id {:optional true} :string]
|
||||
[:google-client-secret {:optional true} :string]
|
||||
[:oidc-client-id {:optional true} :string]
|
||||
[:oidc-user-info-source {:optional true} :keyword]
|
||||
[:oidc-user-info-source {:optional true} [:enum "auto" "userinfo" "token"]]
|
||||
[:oidc-client-secret {:optional true} :string]
|
||||
[:oidc-base-uri {:optional true} :string]
|
||||
[:oidc-token-uri {:optional true} :string]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.auth :as-alias http.auth]
|
||||
[app.http :as-alias http]
|
||||
[app.main :as-alias main]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]))
|
||||
@@ -33,25 +33,26 @@
|
||||
(defn- get-token-data
|
||||
[pool claims]
|
||||
(when-not (db/read-only? pool)
|
||||
(when-let [token-id (-> (deref claims) (get :tid))]
|
||||
(when-let [token-id (get claims :tid)]
|
||||
(some-> (db/exec-one! pool [sql:get-token-data token-id])
|
||||
(update :perms db/decode-pgarray #{})))))
|
||||
|
||||
(defn- wrap-authz
|
||||
[handler {:keys [::db/pool]}]
|
||||
(fn [{:keys [::http.auth/token-type] :as request}]
|
||||
(if (= :token token-type)
|
||||
(let [{:keys [perms profile-id expires-at]} (some->> (get request ::http.auth/claims)
|
||||
(get-token-data pool))]
|
||||
(handler (cond-> request
|
||||
(some? perms)
|
||||
(assoc ::perms perms)
|
||||
(some? profile-id)
|
||||
(assoc ::profile-id profile-id)
|
||||
(some? expires-at)
|
||||
(assoc ::expires-at expires-at))))
|
||||
(fn [request]
|
||||
(let [{:keys [type claims]} (get request ::http/auth-data)]
|
||||
(if (= :token type)
|
||||
(let [{:keys [perms profile-id expires-at]} (some->> claims (get-token-data pool))]
|
||||
;; FIXME: revisit this, this data looks unused
|
||||
(handler (cond-> request
|
||||
(some? perms)
|
||||
(assoc ::perms perms)
|
||||
(some? profile-id)
|
||||
(assoc ::profile-id profile-id)
|
||||
(some? expires-at)
|
||||
(assoc ::expires-at expires-at))))
|
||||
|
||||
(handler request))))
|
||||
(handler request)))))
|
||||
|
||||
(def authz
|
||||
{:name ::authz
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[integrant.core :as ig]
|
||||
[java-http-clj.core :as http]
|
||||
[promesa.core :as p])
|
||||
[java-http-clj.core :as http])
|
||||
(:import
|
||||
java.net.http.HttpClient))
|
||||
|
||||
@@ -29,14 +28,9 @@
|
||||
|
||||
(defn send!
|
||||
([client req] (send! client req {}))
|
||||
([client req {:keys [response-type sync?] :or {response-type :string sync? false}}]
|
||||
([client req {:keys [response-type] :or {response-type :string}}]
|
||||
(assert (client? client) "expected valid http client")
|
||||
(if sync?
|
||||
(http/send req {:client client :as response-type})
|
||||
(try
|
||||
(http/send-async req {:client client :as response-type})
|
||||
(catch Throwable cause
|
||||
(p/rejected cause))))))
|
||||
(http/send req {:client client :as response-type})))
|
||||
|
||||
(defn- resolve-client
|
||||
[params]
|
||||
@@ -56,8 +50,8 @@
|
||||
([cfg-or-client request]
|
||||
(let [client (resolve-client cfg-or-client)
|
||||
request (update request :uri str)]
|
||||
(send! client request {:sync? true})))
|
||||
(send! client request {})))
|
||||
([cfg-or-client request options]
|
||||
(let [client (resolve-client cfg-or-client)
|
||||
request (update request :uri str)]
|
||||
(send! client request (merge {:sync? true} options)))))
|
||||
(send! client request options))))
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
(defn request->context
|
||||
"Extracts error report relevant context data from request."
|
||||
[request]
|
||||
(let [claims (some-> (get request ::auth/claims) deref)]
|
||||
(let [{:keys [claims] :as auth} (get request ::http/auth-data)]
|
||||
(-> (cf/logging-context)
|
||||
(assoc :request/path (:path request))
|
||||
(assoc :request/method (:method request))
|
||||
@@ -31,6 +31,7 @@
|
||||
(assoc :request/user-agent (yreq/get-header request "user-agent"))
|
||||
(assoc :request/ip-addr (inet/parse-request request))
|
||||
(assoc :request/profile-id (get claims :uid))
|
||||
(assoc :request/auth-data auth)
|
||||
(assoc :version/frontend (or (yreq/get-header request "x-frontend-version") "unknown")))))
|
||||
|
||||
(defmulti handle-error
|
||||
@@ -59,7 +60,6 @@
|
||||
::yres/body data}
|
||||
|
||||
(binding [l/*context* (request->context request)]
|
||||
(l/wrn :hint "restriction error" :cause err)
|
||||
{::yres/status 400
|
||||
::yres/body data}))))
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
[app.common.schema :as-alias sm]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cf]
|
||||
[app.http.auth :as-alias auth]
|
||||
[app.http :as-alias http]
|
||||
[app.http.errors :as errors]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[cuerdas.core :as str]
|
||||
@@ -242,6 +242,7 @@
|
||||
(handler request)
|
||||
{::yres/status 405}))))))})
|
||||
|
||||
|
||||
(defn- wrap-auth
|
||||
[handler decoders]
|
||||
(let [token-re
|
||||
@@ -252,30 +253,28 @@
|
||||
(when-let [[_ token-type token] (some->> (yreq/get-header request "authorization")
|
||||
(re-matches token-re))]
|
||||
(if (= "token" (str/lower token-type))
|
||||
[:token token]
|
||||
[:bearer token])))
|
||||
{:type :token
|
||||
:token token}
|
||||
{:type :bearer
|
||||
:token token})))
|
||||
|
||||
get-token-from-cookie
|
||||
(fn [request]
|
||||
(let [cname (cf/get :auth-token-cookie-name)
|
||||
token (some-> (yreq/get-cookie request cname) :value)]
|
||||
(when-not (str/empty? token)
|
||||
[:cookie token])))
|
||||
{:type :cookie
|
||||
:token token})))
|
||||
|
||||
get-token
|
||||
(some-fn get-token-from-cookie get-token-from-authorization)
|
||||
|
||||
process-request
|
||||
(fn [request]
|
||||
(if-let [[token-type token] (get-token request)]
|
||||
(let [request (-> request
|
||||
(assoc ::auth/token token)
|
||||
(assoc ::auth/token-type token-type))
|
||||
decoder (get decoders token-type)]
|
||||
|
||||
(if (fn? decoder)
|
||||
(assoc request ::auth/claims (delay (decoder token)))
|
||||
request))
|
||||
(if-let [{:keys [type token] :as auth} (get-token request)]
|
||||
(if-let [decode-fn (get decoders type)]
|
||||
(assoc request ::http/auth-data (assoc auth :claims (decode-fn token)))
|
||||
(assoc request ::http/auth-data auth))
|
||||
request))]
|
||||
|
||||
(fn [request]
|
||||
|
||||
@@ -11,17 +11,19 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.http :as-alias http]
|
||||
[app.http.auth :as-alias http.auth]
|
||||
[app.http.session.tasks :as-alias tasks]
|
||||
[app.main :as-alias main]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[yetti.request :as yreq]))
|
||||
[yetti.request :as yreq]
|
||||
[yetti.response :as yres]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; DEFAULTS
|
||||
@@ -38,10 +40,10 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defprotocol ISessionManager
|
||||
(read [_ key])
|
||||
(write! [_ key data])
|
||||
(update! [_ data])
|
||||
(delete! [_ key]))
|
||||
(read-session [_ id])
|
||||
(create-session [_ params])
|
||||
(update-session [_ session])
|
||||
(delete-session [_ id]))
|
||||
|
||||
(defn manager?
|
||||
[o]
|
||||
@@ -56,71 +58,82 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:params
|
||||
[:map {:title "session-params"}
|
||||
[:user-agent ::sm/text]
|
||||
[:map {:title "SessionParams" :closed true}
|
||||
[:profile-id ::sm/uuid]
|
||||
[:created-at ::ct/inst]])
|
||||
[:user-agent {:optional true} ::sm/text]
|
||||
[:sso-provider-id {:optional true} ::sm/uuid]
|
||||
[:sso-session-id {:optional true} :string]])
|
||||
|
||||
(def ^:private valid-params?
|
||||
(sm/validator schema:params))
|
||||
|
||||
(defn- prepare-session-params
|
||||
[params key]
|
||||
(assert (string? key) "expected key to be a string")
|
||||
(assert (not (str/blank? key)) "expected key to be not empty")
|
||||
(assert (valid-params? params) "expected valid params")
|
||||
|
||||
{:user-agent (:user-agent params)
|
||||
:profile-id (:profile-id params)
|
||||
:created-at (:created-at params)
|
||||
:updated-at (:created-at params)
|
||||
:id key})
|
||||
|
||||
(defn- database-manager
|
||||
[pool]
|
||||
(reify ISessionManager
|
||||
(read [_ token]
|
||||
(db/exec-one! pool (sql/select :http-session {:id token})))
|
||||
(read-session [_ id]
|
||||
(if (string? id)
|
||||
;; Backward compatibility
|
||||
(let [session (db/exec-one! pool (sql/select :http-session {:id id}))]
|
||||
(-> session
|
||||
(assoc :modified-at (:updated-at session))
|
||||
(dissoc :updated-at)))
|
||||
(db/exec-one! pool (sql/select :http-session-v2 {:id id}))))
|
||||
|
||||
(write! [_ key params]
|
||||
(let [params (-> params
|
||||
(assoc :created-at (ct/now))
|
||||
(prepare-session-params key))]
|
||||
(db/insert! pool :http-session params)
|
||||
params))
|
||||
(create-session [_ params]
|
||||
(assert (valid-params? params) "expect valid session params")
|
||||
|
||||
(update! [_ params]
|
||||
(let [updated-at (ct/now)]
|
||||
(db/update! pool :http-session
|
||||
{:updated-at updated-at}
|
||||
{:id (:id params)})
|
||||
(assoc params :updated-at updated-at)))
|
||||
(let [now (ct/now)
|
||||
params (-> params
|
||||
(assoc :id (uuid/next))
|
||||
(assoc :created-at now)
|
||||
(assoc :modified-at now))]
|
||||
(db/insert! pool :http-session-v2 params
|
||||
{::db/return-keys true})))
|
||||
|
||||
(delete! [_ token]
|
||||
(db/delete! pool :http-session {:id token})
|
||||
(update-session [_ session]
|
||||
(let [modified-at (ct/now)]
|
||||
(if (string? (:id session))
|
||||
(let [params (-> session
|
||||
(assoc :id (uuid/next))
|
||||
(assoc :created-at modified-at)
|
||||
(assoc :modified-at modified-at))]
|
||||
(db/insert! pool :http-session-v2 params))
|
||||
|
||||
(db/update! pool :http-session-v2
|
||||
{:modified-at modified-at}
|
||||
{:id (:id session)}))))
|
||||
|
||||
(delete-session [_ id]
|
||||
(if (string? id)
|
||||
(db/delete! pool :http-session {:id id} {::db/return-keys false})
|
||||
(db/delete! pool :http-session-v2 {:id id} {::db/return-keys false}))
|
||||
nil)))
|
||||
|
||||
(defn inmemory-manager
|
||||
[]
|
||||
(let [cache (atom {})]
|
||||
(reify ISessionManager
|
||||
(read [_ token]
|
||||
(get @cache token))
|
||||
(read-session [_ id]
|
||||
(get @cache id))
|
||||
|
||||
(write! [_ key params]
|
||||
(let [params (-> params
|
||||
(assoc :created-at (ct/now))
|
||||
(prepare-session-params key))]
|
||||
(swap! cache assoc key params)
|
||||
params))
|
||||
(create-session [_ params]
|
||||
(assert (valid-params? params) "expect valid session params")
|
||||
|
||||
(update! [_ params]
|
||||
(let [updated-at (ct/now)]
|
||||
(swap! cache update (:id params) assoc :updated-at updated-at)
|
||||
(assoc params :updated-at updated-at)))
|
||||
(let [now (ct/now)
|
||||
session (-> params
|
||||
(assoc :id (uuid/next))
|
||||
(assoc :created-at now)
|
||||
(assoc :modified-at now))]
|
||||
(swap! cache assoc (:id session) session)
|
||||
session))
|
||||
|
||||
(delete! [_ token]
|
||||
(swap! cache dissoc token)
|
||||
(update-session [_ session]
|
||||
(let [modified-at (ct/now)]
|
||||
(swap! cache update (:id session) assoc :modified-at modified-at)
|
||||
(assoc session :modified-at modified-at)))
|
||||
|
||||
(delete-session [_ id]
|
||||
(swap! cache dissoc id)
|
||||
nil))))
|
||||
|
||||
(defmethod ig/assert-key ::manager
|
||||
@@ -140,43 +153,48 @@
|
||||
;; MANAGER IMPL
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare ^:private assign-auth-token-cookie)
|
||||
(declare ^:private clear-auth-token-cookie)
|
||||
(declare ^:private gen-token)
|
||||
(declare ^:private assign-session-cookie)
|
||||
(declare ^:private clear-session-cookie)
|
||||
|
||||
(defn- assign-token
|
||||
[cfg session]
|
||||
(let [token (tokens/generate cfg
|
||||
{:iss "authentication"
|
||||
:aud "penpot"
|
||||
:sid (:id session)
|
||||
:iat (:modified-at session)
|
||||
:uid (:profile-id session)
|
||||
:sso-provider-id (:sso-provider-id session)
|
||||
:sso-session-id (:sso-session-id session)})]
|
||||
(assoc session :token token)))
|
||||
|
||||
(defn create-fn
|
||||
[{:keys [::manager] :as cfg} profile-id]
|
||||
[{:keys [::manager] :as cfg} {profile-id :id :as profile}
|
||||
& {:keys [sso-provider-id sso-session-id]}]
|
||||
|
||||
(assert (manager? manager) "expected valid session manager")
|
||||
(assert (uuid? profile-id) "expected valid uuid for profile-id")
|
||||
|
||||
(fn [request response]
|
||||
(let [uagent (yreq/get-header request "user-agent")
|
||||
params {:profile-id profile-id
|
||||
:user-agent uagent}
|
||||
token (gen-token cfg params)
|
||||
session (write! manager token params)]
|
||||
(l/trc :hint "create" :profile-id (str profile-id))
|
||||
(-> response
|
||||
(assign-auth-token-cookie session)))))
|
||||
session (->> {:user-agent uagent
|
||||
:profile-id profile-id
|
||||
:sso-provider-id sso-provider-id
|
||||
:sso-session-id sso-session-id}
|
||||
(d/without-nils)
|
||||
(create-session manager)
|
||||
(assign-token cfg))]
|
||||
|
||||
(l/trc :hint "create" :id (str (:id session)) :profile-id (str profile-id))
|
||||
(assign-session-cookie response session))))
|
||||
|
||||
(defn delete-fn
|
||||
[{:keys [::manager]}]
|
||||
(assert (manager? manager) "expected valid session manager")
|
||||
(fn [request response]
|
||||
(let [cname (cf/get :auth-token-cookie-name)
|
||||
cookie (yreq/get-cookie request cname)]
|
||||
(l/trc :hint "delete" :profile-id (:profile-id request))
|
||||
(some->> (:value cookie) (delete! manager))
|
||||
(-> response
|
||||
(assoc :status 204)
|
||||
(assoc :body nil)
|
||||
(clear-auth-token-cookie)))))
|
||||
(some->> (get request ::id) (delete-session manager))
|
||||
(clear-session-cookie response)))
|
||||
|
||||
(defn- gen-token
|
||||
[cfg {:keys [profile-id created-at]}]
|
||||
(tokens/generate cfg {:iss "authentication"
|
||||
:iat created-at
|
||||
:uid profile-id}))
|
||||
(defn decode-token
|
||||
[cfg token]
|
||||
(try
|
||||
@@ -186,44 +204,63 @@
|
||||
:token token
|
||||
:cause cause))))
|
||||
|
||||
(defn get-session
|
||||
[request]
|
||||
(get request ::session))
|
||||
|
||||
(defn invalidate-others
|
||||
[cfg session]
|
||||
(let [sql "delete from http_session_v2 where profile_id = ? and id != ?"]
|
||||
(-> (db/exec-one! cfg [sql (:profile-id session) (:id session)])
|
||||
(db/get-update-count))))
|
||||
|
||||
(defn- renew-session?
|
||||
[{:keys [updated-at] :as session}]
|
||||
(and (ct/inst? updated-at)
|
||||
(let [elapsed (ct/diff updated-at (ct/now))]
|
||||
(neg? (compare default-renewal-max-age elapsed)))))
|
||||
[{:keys [id modified-at] :as session}]
|
||||
(or (string? id)
|
||||
(and (ct/inst? modified-at)
|
||||
(let [elapsed (ct/diff modified-at (ct/now))]
|
||||
(neg? (compare default-renewal-max-age elapsed))))))
|
||||
|
||||
(defn- wrap-authz
|
||||
[handler {:keys [::manager] :as cfg}]
|
||||
(assert (manager? manager) "expected valid session manager")
|
||||
(fn [{:keys [::http.auth/token-type] :as request}]
|
||||
(cond
|
||||
(= token-type :cookie)
|
||||
(let [session (some->> (get request ::http.auth/token)
|
||||
(read manager))
|
||||
request (cond-> request
|
||||
(some? session)
|
||||
(-> (assoc ::profile-id (:profile-id session))
|
||||
(assoc ::id (:id session))))
|
||||
(fn [request]
|
||||
(let [{:keys [type token claims]} (get request ::http/auth-data)]
|
||||
(cond
|
||||
(= type :cookie)
|
||||
(let [session (if-let [sid (:sid claims)]
|
||||
(read-session manager sid)
|
||||
;; BACKWARD COMPATIBILITY WITH OLD TOKENS
|
||||
(read-session manager token))
|
||||
|
||||
response (handler request)]
|
||||
request (cond-> request
|
||||
(some? session)
|
||||
(-> (assoc ::profile-id (:profile-id session))
|
||||
(assoc ::session session)))
|
||||
|
||||
(if (renew-session? session)
|
||||
(let [session (update! manager session)]
|
||||
(-> response
|
||||
(assign-auth-token-cookie session)))
|
||||
response))
|
||||
response (handler request)]
|
||||
|
||||
(= token-type :bearer)
|
||||
(let [session (some->> (get request ::http.auth/token)
|
||||
(read manager))
|
||||
request (cond-> request
|
||||
(some? session)
|
||||
(-> (assoc ::profile-id (:profile-id session))
|
||||
(assoc ::id (:id session))))]
|
||||
(handler request))
|
||||
(if (renew-session? session)
|
||||
(let [session (->> session
|
||||
(update-session manager)
|
||||
(assign-token cfg))]
|
||||
(assign-session-cookie response session))
|
||||
response))
|
||||
|
||||
:else
|
||||
(handler request))))
|
||||
(= type :bearer)
|
||||
(let [session (if-let [sid (:sid claims)]
|
||||
(read-session manager sid)
|
||||
;; BACKWARD COMPATIBILITY WITH OLD TOKENS
|
||||
(read-session manager token))
|
||||
|
||||
request (cond-> request
|
||||
(some? session)
|
||||
(-> (assoc ::profile-id (:profile-id session))
|
||||
(assoc ::session session)))]
|
||||
(handler request))
|
||||
|
||||
:else
|
||||
(handler request)))))
|
||||
|
||||
(def authz
|
||||
{:name ::authz
|
||||
@@ -231,10 +268,10 @@
|
||||
|
||||
;; --- IMPL
|
||||
|
||||
(defn- assign-auth-token-cookie
|
||||
[response {token :id updated-at :updated-at}]
|
||||
(defn- assign-session-cookie
|
||||
[response {token :token modified-at :modified-at}]
|
||||
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
|
||||
created-at updated-at
|
||||
created-at modified-at
|
||||
renewal (ct/plus created-at default-renewal-max-age)
|
||||
expires (ct/plus created-at max-age)
|
||||
secure? (contains? cf/flags :secure-session-cookies)
|
||||
@@ -249,12 +286,12 @@
|
||||
:comment comment
|
||||
:same-site (if cors? :none (if strict? :strict :lax))
|
||||
:secure secure?}]
|
||||
(update response :cookies assoc name cookie)))
|
||||
(update response ::yres/cookies assoc name cookie)))
|
||||
|
||||
(defn- clear-auth-token-cookie
|
||||
(defn- clear-session-cookie
|
||||
[response]
|
||||
(let [cname (cf/get :auth-token-cookie-name)]
|
||||
(update response :cookies assoc cname {:path "/" :value "" :max-age 0})))
|
||||
(update response ::yres/cookies assoc cname {:path "/" :value "" :max-age 0})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TASK: SESSION GC
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
[app.util.inet :as inet]
|
||||
[app.util.services :as-alias sv]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]))
|
||||
[cuerdas.core :as str]
|
||||
[yetti.request :as yreq]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
@@ -90,6 +91,22 @@
|
||||
::ip-addr (::rpc/ip-addr params)
|
||||
::context (d/without-nils context)}))
|
||||
|
||||
(defn get-external-session-id
|
||||
[request]
|
||||
(when-let [session-id (yreq/get-header request "x-external-session-id")]
|
||||
(when-not (or (> (count session-id) 256)
|
||||
(= session-id "null")
|
||||
(str/blank? session-id))
|
||||
session-id)))
|
||||
|
||||
(defn- get-external-event-origin
|
||||
[request]
|
||||
(when-let [origin (yreq/get-header request "x-event-origin")]
|
||||
(when-not (or (> (count origin) 256)
|
||||
(= origin "null")
|
||||
(str/blank? origin))
|
||||
origin)))
|
||||
|
||||
;; --- SPECS
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -126,8 +143,6 @@
|
||||
(::rpc/profile-id params)
|
||||
uuid/zero)
|
||||
|
||||
session-id (get params ::rpc/external-session-id)
|
||||
event-origin (get params ::rpc/external-event-origin)
|
||||
props (-> (or (::replace-props resultm)
|
||||
(-> params
|
||||
(merge (::props resultm))
|
||||
@@ -138,8 +153,10 @@
|
||||
|
||||
token-id (::actoken/id request)
|
||||
context (-> (::context resultm)
|
||||
(assoc :external-session-id session-id)
|
||||
(assoc :external-event-origin event-origin)
|
||||
(assoc :external-session-id
|
||||
(get-external-session-id request))
|
||||
(assoc :external-event-origin
|
||||
(get-external-event-origin request))
|
||||
(assoc :access-token-id (some-> token-id str))
|
||||
(d/without-nils))
|
||||
|
||||
|
||||
@@ -259,14 +259,17 @@
|
||||
::oidc.providers/generic
|
||||
{::http.client/client (ig/ref ::http.client/client)}
|
||||
|
||||
::oidc/providers
|
||||
[(ig/ref ::oidc.providers/google)
|
||||
(ig/ref ::oidc.providers/github)
|
||||
(ig/ref ::oidc.providers/gitlab)
|
||||
(ig/ref ::oidc.providers/generic)]
|
||||
|
||||
::oidc/routes
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::setup/props (ig/ref ::setup/props)
|
||||
::oidc/providers {:google (ig/ref ::oidc.providers/google)
|
||||
:github (ig/ref ::oidc.providers/github)
|
||||
:gitlab (ig/ref ::oidc.providers/gitlab)
|
||||
:oidc (ig/ref ::oidc.providers/generic)}
|
||||
::oidc/providers (ig/ref ::oidc/providers)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::email/blacklist (ig/ref ::email/blacklist)
|
||||
::email/whitelist (ig/ref ::email/whitelist)}
|
||||
@@ -298,6 +301,7 @@
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::mbus/msgbus (ig/ref ::mbus/msgbus)
|
||||
::setup/props (ig/ref ::setup/props)
|
||||
::session/manager (ig/ref ::session/manager)}
|
||||
|
||||
:app.http.assets/routes
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as-alias db]
|
||||
[app.http.client :as http]
|
||||
[app.storage :as-alias sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[buddy.core.bytes :as bb]
|
||||
@@ -37,6 +38,9 @@
|
||||
org.im4java.core.IMOperation
|
||||
org.im4java.core.Info))
|
||||
|
||||
(def default-max-file-size
|
||||
(* 1024 1024 10)) ; 10 MiB
|
||||
|
||||
(def schema:upload
|
||||
[:map {:title "Upload"}
|
||||
[:filename :string]
|
||||
@@ -241,7 +245,7 @@
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-svg-file
|
||||
:hint "uploaded svg does not provides dimensions"))
|
||||
(merge input info {:ts (ct/now)}))
|
||||
(merge input info {:ts (ct/now) :size (fs/size path)}))
|
||||
|
||||
(let [instance (Info. (str path))
|
||||
mtype' (.getProperty instance "Mime type")]
|
||||
@@ -261,6 +265,7 @@
|
||||
(assoc input
|
||||
:width width
|
||||
:height height
|
||||
:size (fs/size path)
|
||||
:ts (ct/now)))))))
|
||||
|
||||
(defmethod process-error org.im4java.core.InfoException
|
||||
@@ -270,6 +275,54 @@
|
||||
:hint "invalid image"
|
||||
:cause error))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; IMAGE HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn download-image
|
||||
"Download an image from the provided URI and return the media input object"
|
||||
[{:keys [::http/client]} uri]
|
||||
(letfn [(parse-and-validate [{:keys [headers] :as response}]
|
||||
(let [size (some-> (get headers "content-length") d/parse-integer)
|
||||
mtype (get headers "content-type")
|
||||
format (cm/mtype->format mtype)
|
||||
max-size (cf/get :media-max-file-size default-max-file-size)]
|
||||
|
||||
(when-not size
|
||||
(ex/raise :type :validation
|
||||
:code :unknown-size
|
||||
:hint "seems like the url points to resource with unknown size"))
|
||||
|
||||
(when (> size max-size)
|
||||
(ex/raise :type :validation
|
||||
:code :file-too-large
|
||||
:hint (str/ffmt "the file size % is greater than the maximum %"
|
||||
size
|
||||
default-max-file-size)))
|
||||
|
||||
(when (nil? format)
|
||||
(ex/raise :type :validation
|
||||
:code :media-type-not-allowed
|
||||
:hint "seems like the url points to an invalid media object"))
|
||||
|
||||
{:size size :mtype mtype :format format}))]
|
||||
|
||||
(let [{:keys [body] :as response} (http/req! client
|
||||
{:method :get :uri uri}
|
||||
{:response-type :input-stream})
|
||||
{:keys [size mtype]} (parse-and-validate response)
|
||||
path (tmp/tempfile :prefix "penpot.media.download.")
|
||||
written (io/write* path body :size size)]
|
||||
|
||||
(when (not= written size)
|
||||
(ex/raise :type :internal
|
||||
:code :mismatch-write-size
|
||||
:hint "unexpected state: unable to write to file"))
|
||||
|
||||
{;; :size size
|
||||
:path path
|
||||
:mtype mtype})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; FONTS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -450,7 +450,13 @@
|
||||
:fn (mg/resource "app/migrations/sql/0141-add-idx-to-file-library-rel.sql")}
|
||||
|
||||
{:name "0141-add-file-data-table.sql"
|
||||
:fn (mg/resource "app/migrations/sql/0141-add-file-data-table.sql")}])
|
||||
:fn (mg/resource "app/migrations/sql/0141-add-file-data-table.sql")}
|
||||
|
||||
{:name "0142-add-sso-provider-table"
|
||||
:fn (mg/resource "app/migrations/sql/0142-add-sso-provider-table.sql")}
|
||||
|
||||
{:name "0143-http-session-v2-table"
|
||||
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
CREATE TABLE sso_provider (
|
||||
id uuid PRIMARY KEY,
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
modified_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
is_enabled boolean NOT NULL DEFAULT true,
|
||||
|
||||
type text NOT NULL CHECK (type IN ('oidc')),
|
||||
domain text NOT NULL,
|
||||
|
||||
client_id text NOT NULL,
|
||||
client_secret text NOT NULL,
|
||||
|
||||
base_uri text NOT NULL,
|
||||
token_uri text NULL,
|
||||
auth_uri text NULL,
|
||||
user_uri text NULL,
|
||||
jwks_uri text NULL,
|
||||
logout_uri text NULL,
|
||||
|
||||
roles_attr text NULL,
|
||||
email_attr text NULL,
|
||||
name_attr text NULL,
|
||||
user_info_source text NOT NULL DEFAULT 'token'
|
||||
CHECK (user_info_source IN ('token', 'userinfo', 'auto')),
|
||||
|
||||
scopes text[] NULL,
|
||||
roles text[] NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX sso_provider__domain__idx
|
||||
ON sso_provider(domain);
|
||||
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE http_session_v2 (
|
||||
id uuid PRIMARY KEY,
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
modified_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
profile_id uuid REFERENCES profile(id) ON DELETE CASCADE,
|
||||
user_agent text NULL,
|
||||
|
||||
sso_provider_id uuid NULL REFERENCES sso_provider(id) ON DELETE CASCADE,
|
||||
sso_session_id text NULL
|
||||
);
|
||||
|
||||
CREATE INDEX http_session_v2__profile_id__idx
|
||||
ON http_session_v2(profile_id);
|
||||
|
||||
CREATE INDEX http_session_v2__sso_provider_id__idx
|
||||
ON http_session_v2(sso_provider_id)
|
||||
WHERE sso_provider_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX http_session_v2__sso_session_id__idx
|
||||
ON http_session_v2(sso_session_id)
|
||||
WHERE sso_session_id IS NOT NULL;
|
||||
@@ -68,33 +68,21 @@
|
||||
response (if (fn? result)
|
||||
(result request)
|
||||
(let [result (rph/unwrap result)
|
||||
status (::http/status mdata 200)
|
||||
status (or (::http/status mdata)
|
||||
(if (nil? result)
|
||||
204
|
||||
200))
|
||||
headers (cond-> (::http/headers mdata {})
|
||||
(yres/stream-body? result)
|
||||
(assoc "content-type" "application/octet-stream"))]
|
||||
{::yres/status status
|
||||
::yres/headers headers
|
||||
::yres/body result}))]
|
||||
|
||||
(-> response
|
||||
(handle-response-transformation request mdata)
|
||||
(handle-before-comple-hook mdata))))
|
||||
|
||||
(defn get-external-session-id
|
||||
[request]
|
||||
(when-let [session-id (yreq/get-header request "x-external-session-id")]
|
||||
(when-not (or (> (count session-id) 256)
|
||||
(= session-id "null")
|
||||
(str/blank? session-id))
|
||||
session-id)))
|
||||
|
||||
(defn- get-external-event-origin
|
||||
[request]
|
||||
(when-let [origin (yreq/get-header request "x-event-origin")]
|
||||
(when-not (or (> (count origin) 256)
|
||||
(= origin "null")
|
||||
(str/blank? origin))
|
||||
origin)))
|
||||
|
||||
(defn- make-rpc-handler
|
||||
"Ring handler that dispatches cmd requests and convert between
|
||||
internal async flow into ring async flow."
|
||||
@@ -105,23 +93,19 @@
|
||||
etag (yreq/get-header request "if-none-match")
|
||||
profile-id (or (::session/profile-id request)
|
||||
(::actoken/profile-id request))
|
||||
|
||||
ip-addr (inet/parse-request request)
|
||||
session-id (get-external-session-id request)
|
||||
event-origin (get-external-event-origin request)
|
||||
|
||||
data (-> params
|
||||
(assoc ::handler-name handler-name)
|
||||
(assoc ::ip-addr ip-addr)
|
||||
(assoc ::request-at (ct/now))
|
||||
(assoc ::external-session-id session-id)
|
||||
(assoc ::external-event-origin event-origin)
|
||||
(assoc ::session/id (::session/id request))
|
||||
(assoc ::cond/key etag)
|
||||
(cond-> (uuid? profile-id)
|
||||
(assoc ::profile-id profile-id)))
|
||||
|
||||
data (vary-meta data assoc ::http/request request)
|
||||
data (with-meta data
|
||||
{::http/request request})
|
||||
|
||||
handler-fn (get methods (keyword handler-name) default-handler)]
|
||||
|
||||
(when (and (or (= method :get)
|
||||
@@ -367,7 +351,6 @@
|
||||
(let [public-uri (cf/get :public-uri)]
|
||||
["/api"
|
||||
|
||||
|
||||
["/management"
|
||||
["/methods/:type"
|
||||
{:middleware [[mw/shared-key-auth (cf/get :management-api-shared-key)]
|
||||
|
||||
@@ -7,21 +7,24 @@
|
||||
(ns app.rpc.commands.auth
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.auth.oidc :as oidc]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.email :as eml]
|
||||
[app.email.blacklist :as email.blacklist]
|
||||
[app.email.whitelist :as email.whitelist]
|
||||
[app.http :as-alias http]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.media :as media]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
@@ -30,6 +33,7 @@
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.setup :as-alias setup]
|
||||
[app.setup.welcome-file :refer [create-welcome-file]]
|
||||
[app.storage :as sto]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]
|
||||
[app.worker :as wrk]
|
||||
@@ -109,7 +113,7 @@
|
||||
(assoc profile :is-admin (let [admins (cf/get :admins)]
|
||||
(contains? admins (:email profile)))))]
|
||||
(-> response
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-transform (session/create-fn cfg profile))
|
||||
(rph/with-meta {::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))))]
|
||||
|
||||
@@ -145,7 +149,24 @@
|
||||
[cfg params]
|
||||
(if (= (:profile-id params)
|
||||
(::rpc/profile-id params))
|
||||
(rph/with-transform {} (session/delete-fn cfg))
|
||||
(let [{:keys [claims]}
|
||||
(rph/get-auth-data params)
|
||||
|
||||
provider
|
||||
(some->> (get claims :sso-provider-id)
|
||||
(oidc/get-provider cfg))
|
||||
|
||||
response
|
||||
(if (and provider (:logout-uri provider))
|
||||
(let [params {"logout_hint" (get claims :sso-session-id)
|
||||
"client_id" (get provider :client-id)
|
||||
"post_logout_redirect_uri" (str (cf/get :public-uri))}
|
||||
uri (-> (u/uri (:logout-uri provider))
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
{:redirect-uri uri})
|
||||
{})]
|
||||
|
||||
(rph/with-transform response (session/delete-fn cfg)))
|
||||
{}))
|
||||
|
||||
;; ---- COMMAND: Recover Profile
|
||||
@@ -271,11 +292,29 @@
|
||||
|
||||
;; ---- COMMAND: Register Profile
|
||||
|
||||
(defn create-profile!
|
||||
(defn import-profile-picture
|
||||
[cfg uri]
|
||||
(try
|
||||
(let [storage (sto/resolve cfg)
|
||||
input (media/download-image cfg uri)
|
||||
input (media/run {:cmd :info :input input})
|
||||
hash (sto/calculate-hash (:path input))
|
||||
content (-> (sto/content (:path input) (:size input))
|
||||
(sto/wrap-with-hash hash))
|
||||
sobject (sto/put-object! storage {::sto/content content
|
||||
::sto/deduplicate? true
|
||||
:bucket "profile"
|
||||
:content-type (:mtype input)})]
|
||||
(:id sobject))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "unable to import profile picture"
|
||||
:cause cause)
|
||||
nil)))
|
||||
|
||||
(defn create-profile
|
||||
"Create the profile entry on the database with limited set of input
|
||||
attrs (all the other attrs are filled with default values)."
|
||||
[conn {:keys [email] :as params}]
|
||||
(dm/assert! ::sm/email email)
|
||||
[{:keys [::db/conn] :as cfg} {:keys [email] :as params}]
|
||||
(let [id (or (:id params) (uuid/next))
|
||||
props (-> (audit/extract-utm-params params)
|
||||
(merge (:props params))
|
||||
@@ -283,8 +322,7 @@
|
||||
:viewed-walkthrough? false
|
||||
:nudge {:big 10 :small 1}
|
||||
:v2-info-shown true
|
||||
:release-notes-viewed (:main cf/version)})
|
||||
(db/tjson))
|
||||
:release-notes-viewed (:main cf/version)}))
|
||||
|
||||
password (or (:password params) "!")
|
||||
|
||||
@@ -299,6 +337,12 @@
|
||||
theme (:theme params nil)
|
||||
email (str/lower email)
|
||||
|
||||
photo-id (some->> (or (:oidc/picture props)
|
||||
(:google/picture props)
|
||||
(:github/picture props)
|
||||
(:gitlab/picture props))
|
||||
(import-profile-picture cfg))
|
||||
|
||||
params {:id id
|
||||
:fullname (:fullname params)
|
||||
:email email
|
||||
@@ -306,11 +350,13 @@
|
||||
:lang locale
|
||||
:password password
|
||||
:deleted-at (:deleted-at params)
|
||||
:props props
|
||||
:props (db/tjson props)
|
||||
:theme theme
|
||||
:photo-id photo-id
|
||||
:is-active is-active
|
||||
:is-muted is-muted
|
||||
:is-demo is-demo}]
|
||||
|
||||
(try
|
||||
(-> (db/insert! conn :profile params)
|
||||
(profile/decode-row))
|
||||
@@ -323,7 +369,7 @@
|
||||
(throw cause))))))
|
||||
|
||||
|
||||
(defn create-profile-rels!
|
||||
(defn create-profile-rels
|
||||
[conn {:keys [id] :as profile}]
|
||||
(let [features (cfeat/get-enabled-features cf/flags)
|
||||
team (teams/create-team conn
|
||||
@@ -373,12 +419,13 @@
|
||||
;; to detect if the profile is already registered
|
||||
(or (profile/get-profile-by-email conn (:email claims))
|
||||
(let [is-active (or (boolean (:is-active claims))
|
||||
(boolean (:email-verified claims))
|
||||
(not (contains? cf/flags :email-verification)))
|
||||
params (-> params
|
||||
(assoc :is-active is-active)
|
||||
(update :password auth/derive-password))
|
||||
profile (->> (create-profile! conn params)
|
||||
(create-profile-rels! conn))]
|
||||
profile (->> (create-profile cfg params)
|
||||
(create-profile-rels conn))]
|
||||
(vary-meta profile assoc :created true))))
|
||||
|
||||
created? (-> profile meta :created true?)
|
||||
@@ -416,10 +463,10 @@
|
||||
(and (some? invitation)
|
||||
(= (:email profile)
|
||||
(:member-email invitation)))
|
||||
(let [claims (assoc invitation :member-id (:id profile))
|
||||
token (tokens/generate cfg claims)]
|
||||
(let [invitation (assoc invitation :member-id (:id profile))
|
||||
token (tokens/generate cfg invitation)]
|
||||
(-> {:invitation-token token}
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-transform (session/create-fn cfg profile claims))
|
||||
(rph/with-meta {::audit/replace-props props
|
||||
::audit/context {:action "accept-invitation"}
|
||||
::audit/profile-id (:id profile)})))
|
||||
@@ -430,7 +477,7 @@
|
||||
created?
|
||||
(if (:is-active profile)
|
||||
(-> (profile/strip-private-attrs profile)
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-transform (session/create-fn cfg profile claims))
|
||||
(rph/with-defer create-welcome-file-when-needed)
|
||||
(rph/with-meta
|
||||
{::audit/replace-props props
|
||||
@@ -559,4 +606,32 @@
|
||||
[cfg params]
|
||||
(db/tx-run! cfg request-profile-recovery params))
|
||||
|
||||
;; --- COMMAND: get-sso-config
|
||||
|
||||
(defn- extract-domain
|
||||
"Extract the domain part from email"
|
||||
[email]
|
||||
(let [at (str/last-index-of email "@")]
|
||||
(when (and (>= at 0)
|
||||
(< at (dec (count email))))
|
||||
(-> (subs email (inc at))
|
||||
(str/trim)
|
||||
(str/lower)))))
|
||||
|
||||
(def ^:private schema:get-sso-provider
|
||||
[:map {:title "get-sso-config"}
|
||||
[:email ::sm/email]])
|
||||
|
||||
(def ^:private schema:get-sso-provider-result
|
||||
[:map {:title "SSOProvider"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::get-sso-provider
|
||||
{::rpc/auth false
|
||||
::doc/added "2.12"
|
||||
::sm/params schema:get-sso-provider
|
||||
::sm/result schema:get-sso-provider-result}
|
||||
[cfg {:keys [email]}]
|
||||
(when-let [domain (extract-domain email)]
|
||||
(when-let [config (db/get* cfg :sso-provider {:domain domain})]
|
||||
(select-keys config [:id]))))
|
||||
|
||||
@@ -49,9 +49,9 @@
|
||||
:deleted-at (ct/in-future (cf/get-deletion-delay))
|
||||
:password (derive-password password)
|
||||
:props {}}
|
||||
profile (db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(->> (auth/create-profile! conn params)
|
||||
(auth/create-profile-rels! conn))))]
|
||||
profile (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(->> (auth/create-profile cfg params)
|
||||
(auth/create-profile-rels conn))))]
|
||||
(with-meta {:email email
|
||||
:password password}
|
||||
{::audit/profile-id (:id profile)})))
|
||||
|
||||
@@ -66,12 +66,12 @@
|
||||
:member-email (:email profile))
|
||||
token (tokens/generate cfg claims)]
|
||||
(-> {:invitation-token token}
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-transform (session/create-fn cfg profile))
|
||||
(rph/with-meta {::audit/props (:props profile)
|
||||
::audit/profile-id (:id profile)})))
|
||||
|
||||
(-> (profile/strip-private-attrs profile)
|
||||
(rph/with-transform (session/create-fn cfg (:id profile)))
|
||||
(rph/with-transform (session/create-fn cfg profile))
|
||||
(rph/with-meta {::audit/props (:props profile)
|
||||
::audit/profile-id (:id profile)}))))))
|
||||
|
||||
@@ -83,6 +83,6 @@
|
||||
(profile/clean-email)
|
||||
(profile/get-profile-by-email conn))
|
||||
(->> (assoc info :is-active true :is-demo false)
|
||||
(auth/create-profile! conn)
|
||||
(auth/create-profile-rels! conn)
|
||||
(auth/create-profile cfg)
|
||||
(auth/create-profile-rels conn)
|
||||
(profile/strip-private-attrs))))))
|
||||
|
||||
@@ -7,14 +7,10 @@
|
||||
(ns app.rpc.commands.media
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http.client :as http]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.media :as media]
|
||||
[app.rpc :as-alias rpc]
|
||||
@@ -22,13 +18,7 @@
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.storage :as sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.services :as sv]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.io :as io]))
|
||||
|
||||
(def default-max-file-size
|
||||
(* 1024 1024 10)) ; 10 MiB
|
||||
[app.util.services :as sv]))
|
||||
|
||||
(def thumbnail-options
|
||||
{:width 100
|
||||
@@ -197,56 +187,12 @@
|
||||
|
||||
mobj))
|
||||
|
||||
(defn download-image
|
||||
[{:keys [::http/client]} uri]
|
||||
(letfn [(parse-and-validate [{:keys [headers] :as response}]
|
||||
(let [size (some-> (get headers "content-length") d/parse-integer)
|
||||
mtype (get headers "content-type")
|
||||
format (cm/mtype->format mtype)
|
||||
max-size (cf/get :media-max-file-size default-max-file-size)]
|
||||
|
||||
(when-not size
|
||||
(ex/raise :type :validation
|
||||
:code :unknown-size
|
||||
:hint "seems like the url points to resource with unknown size"))
|
||||
|
||||
(when (> size max-size)
|
||||
(ex/raise :type :validation
|
||||
:code :file-too-large
|
||||
:hint (str/ffmt "the file size % is greater than the maximum %"
|
||||
size
|
||||
default-max-file-size)))
|
||||
|
||||
(when (nil? format)
|
||||
(ex/raise :type :validation
|
||||
:code :media-type-not-allowed
|
||||
:hint "seems like the url points to an invalid media object"))
|
||||
|
||||
{:size size :mtype mtype :format format}))]
|
||||
|
||||
(let [{:keys [body] :as response} (http/req! client
|
||||
{:method :get :uri uri}
|
||||
{:response-type :input-stream :sync? true})
|
||||
{:keys [size mtype]} (parse-and-validate response)
|
||||
path (tmp/tempfile :prefix "penpot.media.download.")
|
||||
written (io/write* path body :size size)]
|
||||
|
||||
(when (not= written size)
|
||||
(ex/raise :type :internal
|
||||
:code :mismatch-write-size
|
||||
:hint "unexpected state: unable to write to file"))
|
||||
|
||||
{:filename "tempfile"
|
||||
:size size
|
||||
:path path
|
||||
:mtype mtype})))
|
||||
|
||||
(defn- create-file-media-object-from-url
|
||||
[cfg {:keys [url name] :as params}]
|
||||
(let [content (download-image cfg url)
|
||||
(let [content (media/download-image cfg url)
|
||||
params (-> params
|
||||
(assoc :content content)
|
||||
(assoc :name (or name (:filename content))))]
|
||||
(assoc :name (d/nilv name "unknown")))]
|
||||
|
||||
;; NOTE: we use the climit here in a dynamic invocation because we
|
||||
;; don't want saturate the process-image limit with IO (download
|
||||
|
||||
@@ -154,7 +154,6 @@
|
||||
|
||||
(declare validate-password!)
|
||||
(declare update-profile-password!)
|
||||
(declare invalidate-profile-session!)
|
||||
|
||||
(def ^:private
|
||||
schema:update-profile-password
|
||||
@@ -169,8 +168,7 @@
|
||||
::climit/id :auth/global
|
||||
::db/transaction true}
|
||||
[cfg {:keys [::rpc/profile-id password] :as params}]
|
||||
(let [profile (validate-password! cfg (assoc params :profile-id profile-id))
|
||||
session-id (::session/id params)]
|
||||
(let [profile (validate-password! cfg (assoc params :profile-id profile-id))]
|
||||
|
||||
(when (= (:email profile) (str/lower (:password params)))
|
||||
(ex/raise :type :validation
|
||||
@@ -178,14 +176,12 @@
|
||||
:hint "you can't use your email as password"))
|
||||
|
||||
(update-profile-password! cfg (assoc profile :password password))
|
||||
(invalidate-profile-session! cfg profile-id session-id)
|
||||
nil))
|
||||
|
||||
(defn- invalidate-profile-session!
|
||||
"Removes all sessions except the current one."
|
||||
[{:keys [::db/conn]} profile-id session-id]
|
||||
(let [sql "delete from http_session where profile_id = ? and id != ?"]
|
||||
(:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id]))))
|
||||
(->> (rph/get-request params)
|
||||
(session/get-session)
|
||||
(session/invalidate-others cfg))
|
||||
|
||||
nil))
|
||||
|
||||
(defn- validate-password!
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id old-password] :as params}]
|
||||
@@ -284,9 +280,9 @@
|
||||
:file-path (str (:path file))
|
||||
:file-mtype (:mtype file)}}))))
|
||||
|
||||
(defn- generate-thumbnail!
|
||||
[_ file]
|
||||
(let [input (media/run {:cmd :info :input file})
|
||||
(defn- generate-thumbnail
|
||||
[_ input]
|
||||
(let [input (media/run {:cmd :info :input input})
|
||||
thumb (media/run {:cmd :profile-thumbnail
|
||||
:format :jpeg
|
||||
:quality 85
|
||||
@@ -307,7 +303,7 @@
|
||||
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
|
||||
[:process-image/global]])
|
||||
(assoc ::climit/label "upload-photo")
|
||||
(climit/invoke! generate-thumbnail! file))]
|
||||
(climit/invoke! generate-thumbnail file))]
|
||||
(sto/put-object! storage params)))
|
||||
|
||||
;; --- MUTATION: Request Email Change
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
{:id (:id profile)}))
|
||||
|
||||
(-> claims
|
||||
(rph/with-transform (session/create-fn cfg profile-id))
|
||||
(rph/with-transform (session/create-fn cfg profile))
|
||||
(rph/with-meta {::audit/name "verify-profile-email"
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))))
|
||||
|
||||
@@ -83,3 +83,16 @@
|
||||
"A convenience allias for yetti.response/stream-body"
|
||||
[f]
|
||||
(yres/stream-body f))
|
||||
|
||||
(defn get-request
|
||||
"Get http request from RPC params"
|
||||
[params]
|
||||
(assert (contains? params ::rpc/request-at) "rpc params required")
|
||||
(-> (meta params)
|
||||
(get ::http/request)))
|
||||
|
||||
(defn get-auth-data
|
||||
"Get http auth-data from RPC params"
|
||||
[params]
|
||||
(-> (get-request params)
|
||||
(get ::http/auth-data)))
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
:is-active is-active
|
||||
:password password
|
||||
:props {}}]
|
||||
(->> (cmd.auth/create-profile! conn params)
|
||||
(cmd.auth/create-profile-rels! conn)))))))
|
||||
(->> (cmd.auth/create-profile system params)
|
||||
(cmd.auth/create-profile-rels conn)))))))
|
||||
|
||||
(defmethod exec-command "update-profile"
|
||||
[{:keys [fullname email password is-active]}]
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.features.file-snapshots :as fsnap]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as main]
|
||||
[app.msgbus :as mbus]
|
||||
@@ -843,10 +844,33 @@
|
||||
:deleted-at deleted-at
|
||||
:id id})))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SSO
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn add-sso-config
|
||||
[& {:keys [base-uri client-id client-secret domain]}]
|
||||
|
||||
(assert (and (string? base-uri) (str/starts-with? base-uri "http")) "expected a valid base-uri")
|
||||
(assert (string? client-id) "expected a valid client-id")
|
||||
(assert (string? client-secret) "expected a valid client-secret")
|
||||
(assert (string? domain) "expected a valid domain")
|
||||
(db/insert! main/system :sso-provider
|
||||
{:id (uuid/next)
|
||||
:type "oidc"
|
||||
:client-id client-id
|
||||
:client-secret client-secret
|
||||
:domain domain
|
||||
:base-uri base-uri}))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; MISC
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn decode-session-token
|
||||
[token]
|
||||
(session/decode-token main/system token))
|
||||
|
||||
(defn instrument-var
|
||||
[var]
|
||||
(alter-var-root var (fn [f]
|
||||
|
||||
@@ -104,13 +104,8 @@
|
||||
(assoc-in [:app.rpc/methods :app.setup/templates] templates)
|
||||
(dissoc :app.srepl/server
|
||||
:app.http/server
|
||||
:app.http/router
|
||||
:app.auth.oidc.providers/google
|
||||
:app.auth.oidc.providers/gitlab
|
||||
:app.auth.oidc.providers/github
|
||||
:app.auth.oidc.providers/generic
|
||||
:app.http/route
|
||||
:app.setup/templates
|
||||
:app.auth.oidc/routes
|
||||
:app.http.oauth/handler
|
||||
:app.notifications/handler
|
||||
:app.loggers.mattermost/reporter
|
||||
@@ -182,10 +177,10 @@
|
||||
:is-demo false}
|
||||
params)]
|
||||
(db/run! system
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(->> params
|
||||
(cmd.auth/create-profile! conn)
|
||||
(cmd.auth/create-profile-rels! conn)))))))
|
||||
(cmd.auth/create-profile cfg)
|
||||
(cmd.auth/create-profile-rels conn)))))))
|
||||
|
||||
(defn create-project*
|
||||
([i params] (create-project* *system* i params))
|
||||
|
||||
@@ -22,17 +22,6 @@
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
|
||||
(t/deftest authenticate-method
|
||||
(let [profile (th/create-profile* 1)
|
||||
token (#'sess/gen-token th/*system* {:profile-id (:id profile)})
|
||||
request {:params {:token token}}
|
||||
response (#'mgmt/authenticate th/*system* request)]
|
||||
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (= "authentication" (-> response ::yres/body :iss)))
|
||||
(t/is (= (:id profile) (-> response ::yres/body :uid)))))
|
||||
|
||||
(t/deftest get-customer-method
|
||||
(let [profile (th/create-profile* 1)
|
||||
request {:params {:id (:id profile)}}
|
||||
@@ -89,7 +78,3 @@
|
||||
|
||||
(let [subs' (-> response ::yres/body :subscription)]
|
||||
(t/is (= subs' subs))))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
(:require
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.http :as-alias http]
|
||||
[app.http.access-token]
|
||||
[app.http.auth :as-alias auth]
|
||||
[app.http.middleware :as mw]
|
||||
[app.http.session :as session]
|
||||
[app.main :as-alias main]
|
||||
@@ -42,13 +42,14 @@
|
||||
|
||||
(handler (->DummyRequest {} {}))
|
||||
|
||||
(t/is (nil? (::auth/token-type @request)))
|
||||
(t/is (nil? (::auth/token @request)))
|
||||
(t/is (nil? (::http/auth-data @request)))
|
||||
|
||||
(handler (->DummyRequest {"authorization" "Token aaaa"} {}))
|
||||
|
||||
(t/is (= :token (::auth/token-type @request)))
|
||||
(t/is (= "aaaa" (::auth/token @request)))))
|
||||
(let [{:keys [token claims] token-type :type} (get @request ::http/auth-data)]
|
||||
(t/is (= :token token-type))
|
||||
(t/is (= "aaaa" token))
|
||||
(t/is (nil? claims)))))
|
||||
|
||||
(t/deftest auth-middleware-2
|
||||
(let [request (volatile! nil)
|
||||
@@ -57,16 +58,14 @@
|
||||
{})]
|
||||
|
||||
(handler (->DummyRequest {} {}))
|
||||
|
||||
(t/is (nil? (::auth/token-type @request)))
|
||||
(t/is (nil? (::auth/token @request)))
|
||||
(t/is (nil? (::auth/claims @request)))
|
||||
(t/is (nil? (::http/auth-data @request)))
|
||||
|
||||
(handler (->DummyRequest {"authorization" "Bearer aaaa"} {}))
|
||||
|
||||
(t/is (= :bearer (::auth/token-type @request)))
|
||||
(t/is (= "aaaa" (::auth/token @request)))
|
||||
(t/is (nil? (::auth/claims @request)))))
|
||||
(let [{:keys [token claims] token-type :type} (get @request ::http/auth-data)]
|
||||
(t/is (= :bearer token-type))
|
||||
(t/is (= "aaaa" token))
|
||||
(t/is (nil? claims)))))
|
||||
|
||||
(t/deftest auth-middleware-3
|
||||
(let [request (volatile! nil)
|
||||
@@ -75,35 +74,14 @@
|
||||
{})]
|
||||
|
||||
(handler (->DummyRequest {} {}))
|
||||
|
||||
(t/is (nil? (::auth/token-type @request)))
|
||||
(t/is (nil? (::auth/token @request)))
|
||||
(t/is (nil? (::auth/claims @request)))
|
||||
(t/is (nil? (::http/auth-data @request)))
|
||||
|
||||
(handler (->DummyRequest {} {"auth-token" "foobar"}))
|
||||
|
||||
(t/is (= :cookie (::auth/token-type @request)))
|
||||
(t/is (= "foobar" (::auth/token @request)))
|
||||
(t/is (nil? (::auth/claims @request)))))
|
||||
|
||||
(t/deftest auth-middleware-4
|
||||
(let [request (volatile! nil)
|
||||
handler (#'app.http.middleware/wrap-auth
|
||||
(fn [req] (vreset! request req))
|
||||
{:cookie (fn [_] "foobaz")})]
|
||||
|
||||
(handler (->DummyRequest {} {}))
|
||||
|
||||
(t/is (nil? (::auth/token-type @request)))
|
||||
(t/is (nil? (::auth/token @request)))
|
||||
(t/is (nil? (::auth/claims @request)))
|
||||
|
||||
(handler (->DummyRequest {} {"auth-token" "foobar"}))
|
||||
|
||||
(t/is (= :cookie (::auth/token-type @request)))
|
||||
(t/is (= "foobar" (::auth/token @request)))
|
||||
(t/is (delay? (::auth/claims @request)))
|
||||
(t/is (= "foobaz" (-> @request ::auth/claims deref)))))
|
||||
(let [{:keys [token claims] token-type :type} (get @request ::http/auth-data)]
|
||||
(t/is (= :cookie token-type))
|
||||
(t/is (= "foobar" token))
|
||||
(t/is (nil? claims)))))
|
||||
|
||||
(t/deftest shared-key-auth
|
||||
(let [handler (#'app.http.middleware/wrap-shared-key-auth
|
||||
@@ -122,40 +100,36 @@
|
||||
(t/deftest access-token-authz
|
||||
(let [profile (th/create-profile* 1)
|
||||
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
|
||||
request (volatile! {})
|
||||
handler (#'app.http.access-token/wrap-authz identity th/*system*)]
|
||||
|
||||
handler (#'app.http.access-token/wrap-authz
|
||||
(fn [req] (vreset! request req))
|
||||
th/*system*)]
|
||||
(let [response (handler nil)]
|
||||
(t/is (nil? response)))
|
||||
|
||||
(handler nil)
|
||||
(t/is (nil? @request))
|
||||
|
||||
(handler {::auth/claims (delay {:tid (:id token)})
|
||||
::auth/token-type :token})
|
||||
|
||||
(t/is (= #{} (:app.http.access-token/perms @request)))
|
||||
(t/is (= (:id profile) (:app.http.access-token/profile-id @request)))))
|
||||
(let [response (handler {::http/auth-data {:type :token :token "foobar" :claims {:tid (:id token)}}})]
|
||||
(t/is (= #{} (:app.http.access-token/perms response)))
|
||||
(t/is (= (:id profile) (:app.http.access-token/profile-id response))))))
|
||||
|
||||
(t/deftest session-authz
|
||||
(let [manager (session/inmemory-manager)
|
||||
profile (th/create-profile* 1)
|
||||
handler (-> (fn [req] req)
|
||||
(#'session/wrap-authz {::session/manager manager})
|
||||
(#'mw/wrap-auth {}))]
|
||||
(let [cfg th/*system*
|
||||
manager (session/inmemory-manager)
|
||||
profile (th/create-profile* 1)
|
||||
handler (-> (fn [req] req)
|
||||
(#'session/wrap-authz {::session/manager manager})
|
||||
(#'mw/wrap-auth {:bearer (partial session/decode-token cfg)
|
||||
:cookie (partial session/decode-token cfg)}))
|
||||
|
||||
session (->> (session/create-session manager {:profile-id (:id profile)
|
||||
:user-agent "user agent"})
|
||||
(#'session/assign-token cfg))
|
||||
|
||||
(let [response (handler (->DummyRequest {} {"auth-token" "foobar"}))]
|
||||
(t/is (= :cookie (::auth/token-type response)))
|
||||
(t/is (= "foobar" (::auth/token response))))
|
||||
response (handler (->DummyRequest {} {"auth-token" (:token session)}))
|
||||
|
||||
{:keys [token claims] token-type :type}
|
||||
(get response ::http/auth-data)]
|
||||
|
||||
(session/write! manager "foobar" {:profile-id (:id profile)
|
||||
:user-agent "user agent"
|
||||
:created-at (ct/now)})
|
||||
|
||||
(let [response (handler (->DummyRequest {} {"auth-token" "foobar"}))]
|
||||
(t/is (= :cookie (::auth/token-type response)))
|
||||
(t/is (= "foobar" (::auth/token response)))
|
||||
(t/is (= (:id profile) (::session/profile-id response)))
|
||||
(t/is (= "foobar" (::session/id response))))))
|
||||
(t/is (= :cookie token-type))
|
||||
(t/is (= (:token session) token))
|
||||
(t/is (= "authentication" (:iss claims)))
|
||||
(t/is (= "penpot" (:aud claims)))
|
||||
(t/is (= (:id session) (:sid claims)))
|
||||
(t/is (= (:id profile) (:uid claims)))))
|
||||
|
||||
@@ -485,6 +485,13 @@
|
||||
(commit-change change1)
|
||||
(commit-change change2))))
|
||||
|
||||
(defn add-tokens-lib
|
||||
[state tokens-lib]
|
||||
(-> state
|
||||
(commit-change
|
||||
{:type :set-tokens-lib
|
||||
:tokens-lib tokens-lib})))
|
||||
|
||||
(defn delete-shape
|
||||
[file id]
|
||||
(commit-change
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
[:set-tokens-lib
|
||||
[:map {:title "SetTokensLib"}
|
||||
[:type [:= :set-tokens-lib]]
|
||||
[:tokens-lib ::sm/any]]] ;; TODO: we should define a plain object schema for tokens-lib
|
||||
[:tokens-lib ctob/schema:tokens-lib]]]
|
||||
|
||||
[:set-token
|
||||
[:map {:title "SetTokenChange"}
|
||||
|
||||
@@ -72,9 +72,11 @@
|
||||
(= :bool (dm/get-prop shape :type))))
|
||||
|
||||
(defn text-shape?
|
||||
[shape]
|
||||
(and (some? shape)
|
||||
(= :text (dm/get-prop shape :type))))
|
||||
([shape]
|
||||
(and (some? shape)
|
||||
(= :text (dm/get-prop shape :type))))
|
||||
([objects id]
|
||||
(text-shape? (get objects id))))
|
||||
|
||||
(defn rect-shape?
|
||||
[shape]
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
:login-with-ldap
|
||||
;; Uses any generic authentication provider that implements OIDC protocol as credentials.
|
||||
:login-with-oidc
|
||||
;; Allows registration with Open ID
|
||||
;; Enables custom SSO flow
|
||||
:login-with-custom-sso
|
||||
;; Allows registration with OIDC (takes effect only when general `registration` is disabled)
|
||||
:oidc-registration
|
||||
;; This logs to console the invitation tokens. It's useful in case the SMTP is not configured.
|
||||
:log-invitation-tokens})
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
(dm/export gtr/inverse-transform-matrix)
|
||||
(dm/export gtr/transform-rect)
|
||||
(dm/export gtr/calculate-geometry)
|
||||
(dm/export gtr/calculate-selrect)
|
||||
(dm/export gtr/update-group-selrect)
|
||||
(dm/export gtr/update-mask-selrect)
|
||||
(dm/export gtr/apply-transform)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.types.tokens-lib :as ctob]))
|
||||
|
||||
(defn generate-update-active-sets
|
||||
(defn- generate-update-active-sets
|
||||
"Copy the active sets from the currently active themes and move them
|
||||
to the hidden token theme and update the theme with
|
||||
`update-theme-fn`.
|
||||
@@ -28,12 +28,45 @@
|
||||
(pcb/set-token-theme (ctob/get-id hidden-theme)
|
||||
hidden-theme'))))
|
||||
|
||||
(defn generate-set-enabled-token-set
|
||||
"Enable or disable a token set at `set-name` in `tokens-lib` without modifying a user theme."
|
||||
[changes tokens-lib set-name enabled?]
|
||||
(if enabled?
|
||||
(generate-update-active-sets changes tokens-lib #(ctob/enable-set % set-name))
|
||||
(generate-update-active-sets changes tokens-lib #(ctob/disable-set % set-name))))
|
||||
|
||||
(defn generate-toggle-token-set
|
||||
"Toggle a token set at `set-name` in `tokens-lib` without modifying a
|
||||
user theme."
|
||||
"Toggle a token set at `set-name` in `tokens-lib` without modifying a user theme."
|
||||
[changes tokens-lib set-name]
|
||||
(generate-update-active-sets changes tokens-lib #(ctob/toggle-set % set-name)))
|
||||
|
||||
(defn- generate-update-active-token-theme
|
||||
"Change the active state of a theme in `tokens-lib`. If after the change there is
|
||||
any active theme other than the hidden one, deactivate the hidden theme."
|
||||
[changes tokens-lib update-fn]
|
||||
(let [active-token-themes (some-> tokens-lib
|
||||
(update-fn)
|
||||
(ctob/get-active-theme-paths))
|
||||
active-token-themes' (if (= active-token-themes #{ctob/hidden-theme-path})
|
||||
active-token-themes
|
||||
(disj active-token-themes ctob/hidden-theme-path))]
|
||||
(pcb/set-active-token-themes changes active-token-themes')))
|
||||
|
||||
(defn generate-set-active-token-theme
|
||||
"Activate or deactivate a token theme in `tokens-lib`."
|
||||
[changes tokens-lib id active?]
|
||||
(if active?
|
||||
(generate-update-active-token-theme changes tokens-lib
|
||||
#(ctob/activate-theme % id))
|
||||
(generate-update-active-token-theme changes tokens-lib
|
||||
#(ctob/deactivate-theme % id))))
|
||||
|
||||
(defn generate-toggle-token-theme
|
||||
"Toggle the active state of a token theme in `tokens-lib`."
|
||||
[changes tokens-lib id]
|
||||
(generate-update-active-token-theme changes tokens-lib
|
||||
#(ctob/toggle-theme-active % id)))
|
||||
|
||||
(defn toggle-token-set-group
|
||||
"Toggle a token set group at `group-path` in `tokens-lib` for a `tokens-lib-theme`."
|
||||
[group-path tokens-lib tokens-lib-theme]
|
||||
|
||||
@@ -732,89 +732,89 @@
|
||||
[shape scale-text-content value]
|
||||
(update shape :content scale-text-content value))
|
||||
|
||||
(defn scale-text-content
|
||||
[content value]
|
||||
(->> content
|
||||
(txt/transform-nodes txt/is-text-node? (partial transform-text-node value))
|
||||
(txt/transform-nodes txt/is-paragraph-node? (partial transform-paragraph-node value))))
|
||||
|
||||
(defn apply-scale-content
|
||||
[shape value]
|
||||
;; Scale can only be positive
|
||||
(let [value (mth/abs value)]
|
||||
(cond-> shape
|
||||
(cfh/text-shape? shape)
|
||||
(update-text-content scale-text-content value)
|
||||
|
||||
:always
|
||||
(gsc/update-corners-scale value)
|
||||
|
||||
(d/not-empty? (:strokes shape))
|
||||
(gss/update-strokes-width value)
|
||||
|
||||
(d/not-empty? (:shadow shape))
|
||||
(gse/update-shadows-scale value)
|
||||
|
||||
(some? (:blur shape))
|
||||
(gse/update-blur-scale value)
|
||||
|
||||
(ctl/flex-layout? shape)
|
||||
(ctl/update-flex-scale value)
|
||||
|
||||
(ctl/grid-layout? shape)
|
||||
(ctl/update-grid-scale value)
|
||||
|
||||
:always
|
||||
(ctl/update-flex-child value))))
|
||||
|
||||
(defn remove-children-set
|
||||
[shapes children-to-remove]
|
||||
(let [remove? (set children-to-remove)]
|
||||
(d/removev remove? shapes)))
|
||||
|
||||
(defn apply-modifier
|
||||
[shape operation]
|
||||
(let [type (dm/get-prop operation :type)]
|
||||
(case type
|
||||
:rotation
|
||||
(let [rotation (dm/get-prop operation :value)]
|
||||
(update shape :rotation #(mod (+ (or % 0) rotation) 360)))
|
||||
|
||||
:add-children
|
||||
(let [value (dm/get-prop operation :value)
|
||||
index (dm/get-prop operation :index)
|
||||
|
||||
shape
|
||||
(if (some? index)
|
||||
(update shape :shapes
|
||||
(fn [shapes]
|
||||
(if (vector? shapes)
|
||||
(d/insert-at-index shapes index value)
|
||||
(d/concat-vec shapes value))))
|
||||
(update shape :shapes d/concat-vec value))]
|
||||
|
||||
;; Remove duplication
|
||||
(update shape :shapes #(into [] (apply d/ordered-set %))))
|
||||
|
||||
:remove-children
|
||||
(let [value (dm/get-prop operation :value)]
|
||||
(update shape :shapes remove-children-set value))
|
||||
|
||||
:scale-content
|
||||
(let [value (dm/get-prop operation :value)]
|
||||
(apply-scale-content shape value))
|
||||
|
||||
:change-property
|
||||
(let [property (dm/get-prop operation :property)
|
||||
value (dm/get-prop operation :value)]
|
||||
(assoc shape property value))
|
||||
|
||||
;; :default => no change to shape
|
||||
shape)))
|
||||
|
||||
(defn apply-structure-modifiers
|
||||
"Apply structure changes to a shape"
|
||||
[shape modifiers]
|
||||
(letfn [(scale-text-content
|
||||
[content value]
|
||||
(->> content
|
||||
(txt/transform-nodes txt/is-text-node? (partial transform-text-node value))
|
||||
(txt/transform-nodes txt/is-paragraph-node? (partial transform-paragraph-node value))))
|
||||
|
||||
(apply-scale-content
|
||||
[shape value]
|
||||
;; Scale can only be positive
|
||||
(let [value (mth/abs value)]
|
||||
(cond-> shape
|
||||
(cfh/text-shape? shape)
|
||||
(update-text-content scale-text-content value)
|
||||
|
||||
:always
|
||||
(gsc/update-corners-scale value)
|
||||
|
||||
(d/not-empty? (:strokes shape))
|
||||
(gss/update-strokes-width value)
|
||||
|
||||
(d/not-empty? (:shadow shape))
|
||||
(gse/update-shadows-scale value)
|
||||
|
||||
(some? (:blur shape))
|
||||
(gse/update-blur-scale value)
|
||||
|
||||
(ctl/flex-layout? shape)
|
||||
(ctl/update-flex-scale value)
|
||||
|
||||
(ctl/grid-layout? shape)
|
||||
(ctl/update-grid-scale value)
|
||||
|
||||
:always
|
||||
(ctl/update-flex-child value))))]
|
||||
|
||||
(let [remove-children
|
||||
(fn [shapes children-to-remove]
|
||||
(let [remove? (set children-to-remove)]
|
||||
(d/removev remove? shapes)))
|
||||
|
||||
apply-modifier
|
||||
(fn [shape operation]
|
||||
(let [type (dm/get-prop operation :type)]
|
||||
(case type
|
||||
:rotation
|
||||
(let [rotation (dm/get-prop operation :value)]
|
||||
(update shape :rotation #(mod (+ (or % 0) rotation) 360)))
|
||||
|
||||
:add-children
|
||||
(let [value (dm/get-prop operation :value)
|
||||
index (dm/get-prop operation :index)
|
||||
|
||||
shape
|
||||
(if (some? index)
|
||||
(update shape :shapes
|
||||
(fn [shapes]
|
||||
(if (vector? shapes)
|
||||
(d/insert-at-index shapes index value)
|
||||
(d/concat-vec shapes value))))
|
||||
(update shape :shapes d/concat-vec value))]
|
||||
|
||||
;; Remove duplication
|
||||
(update shape :shapes #(into [] (apply d/ordered-set %))))
|
||||
|
||||
:remove-children
|
||||
(let [value (dm/get-prop operation :value)]
|
||||
(update shape :shapes remove-children value))
|
||||
|
||||
:scale-content
|
||||
(let [value (dm/get-prop operation :value)]
|
||||
(apply-scale-content shape value))
|
||||
|
||||
:change-property
|
||||
(let [property (dm/get-prop operation :property)
|
||||
value (dm/get-prop operation :value)]
|
||||
(assoc shape property value))
|
||||
|
||||
;; :default => no change to shape
|
||||
shape)))]
|
||||
|
||||
(as-> shape $
|
||||
(reduce apply-modifier $ (dm/get-prop modifiers :structure-parent))
|
||||
(reduce apply-modifier $ (dm/get-prop modifiers :structure-child))))))
|
||||
(as-> shape $
|
||||
(reduce apply-modifier $ (dm/get-prop modifiers :structure-parent))
|
||||
(reduce apply-modifier $ (dm/get-prop modifiers :structure-child))))
|
||||
|
||||
@@ -310,6 +310,10 @@
|
||||
schema:text-decoration
|
||||
schema:dimensions])
|
||||
|
||||
(defn token-attr?
|
||||
[attr]
|
||||
(contains? all-keys attr))
|
||||
|
||||
(defn shape-attr->token-attrs
|
||||
([shape-attr] (shape-attr->token-attrs shape-attr nil))
|
||||
([shape-attr changed-sub-attr]
|
||||
@@ -403,15 +407,15 @@
|
||||
:text text-attributes
|
||||
nil))
|
||||
|
||||
(defn appliable-attrs
|
||||
(defn appliable-attrs-for-shape
|
||||
"Returns intersection of shape `attributes` for `shape-type`."
|
||||
[attributes shape-type is-layout]
|
||||
(set/intersection attributes (shape-type->attributes shape-type is-layout)))
|
||||
|
||||
(defn any-appliable-attr?
|
||||
(defn any-appliable-attr-for-shape?
|
||||
"Checks if `token-type` supports given shape `attributes`."
|
||||
[attributes token-type is-layout]
|
||||
(seq (appliable-attrs attributes token-type is-layout)))
|
||||
(d/not-empty? (appliable-attrs-for-shape attributes token-type is-layout)))
|
||||
|
||||
;; Token attrs that are set inside content blocks of text shapes, instead
|
||||
;; at the shape level.
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
(ns app.common.types.tokens-lib
|
||||
(:require
|
||||
#?(:clj [app.common.fressian :as fres])
|
||||
#?(:clj [clojure.data.json :as json])
|
||||
#?(:clj [clojure.data.json :as c.json])
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.json :as json]
|
||||
[app.common.path-names :as cpn]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
@@ -198,8 +199,8 @@
|
||||
:tokens tokens})
|
||||
|
||||
#?@(:clj
|
||||
[json/JSONWriter
|
||||
(-write [this writter options] (json/-write (datafy this) writter options))])
|
||||
[c.json/JSONWriter
|
||||
(-write [this writter options] (c.json/-write (datafy this) writter options))])
|
||||
|
||||
INamedItem
|
||||
(get-id [_]
|
||||
@@ -758,7 +759,7 @@
|
||||
(theme-active? [_ id] "predicate if token theme is active")
|
||||
(activate-theme [_ id] "adds theme from the active-themes")
|
||||
(deactivate-theme [_ id] "removes theme from the active-themes")
|
||||
(toggle-theme-active? [_ id] "toggles theme in the active-themes")
|
||||
(toggle-theme-active [_ id] "toggles theme in the active-themes")
|
||||
(get-hidden-theme [_] "get the hidden temporary theme"))
|
||||
|
||||
(def schema:token-themes
|
||||
@@ -901,6 +902,7 @@
|
||||
(delete-token [_ set-id token-id] "delete a token from a set")
|
||||
(toggle-set-in-theme [_ theme-id set-name] "toggle a set used / not used in a theme")
|
||||
(get-active-themes-set-names [_] "set of set names that are active in the the active themes")
|
||||
(token-set-active? [_ set-name] "if a set is active in any of the active themes")
|
||||
(sets-at-path-all-active? [_ group-path] "compute active state for child sets at `group-path`.
|
||||
Will return a value that matches this schema:
|
||||
`:none` None of the nested sets are active
|
||||
@@ -911,6 +913,7 @@ Will return a value that matches this schema:
|
||||
(get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name"))
|
||||
|
||||
(declare parse-multi-set-dtcg-json)
|
||||
(declare read-multi-set-dtcg)
|
||||
(declare export-dtcg-json)
|
||||
|
||||
(deftype TokensLib [sets themes active-themes]
|
||||
@@ -922,23 +925,23 @@ Will return a value that matches this schema:
|
||||
:active-themes active-themes})
|
||||
|
||||
#?@(:clj
|
||||
[json/JSONWriter
|
||||
(-write [this writter options] (json/-write (export-dtcg-json this) writter options))])
|
||||
[c.json/JSONWriter
|
||||
(-write [this writter options] (c.json/-write (export-dtcg-json this) writter options))])
|
||||
|
||||
ITokenSets
|
||||
; Naming conventions:
|
||||
; (TODO: this will disappear after refactoring the internal structure of TokensLib).
|
||||
; Set name: the complete name as a string, without prefix \"some-group/some-subgroup/some-set\".
|
||||
; Set final name or fname: the last part of the name \"some-set\".
|
||||
; Set path: the groups part of the name, as a vector [\"some-group\" \"some-subgroup\"].
|
||||
; Set path str: the set path as a string \"some-group/some-subgroup\".
|
||||
; Set full path: the path including the fname, as a vector [\"some-group\", \"some-subgroup\", \"some-set\"].
|
||||
; Set full path str: the set full path as a string \"some-group/some-subgroup/some-set\".
|
||||
|
||||
; Set prefix: the two-characters prefix added to a full path item \"G-\" / \"S-\".
|
||||
; Prefixed set path or ppath: a path wit added prefixes [\"G-some-group\", \"G-some-subgroup\"].
|
||||
; Prefixed set full path or pfpath: a full path wit prefixes [\"G-some-group\", \"G-some-subgroup\", \"S-some-set\"].
|
||||
; Prefixed set final name or pfname: a final name with prefix \"S-some-set\".
|
||||
;; Naming conventions:
|
||||
;; (TODO: this will disappear after refactoring the internal structure of TokensLib).
|
||||
;; Set name: the complete name as a string, without prefix \"some-group/some-subgroup/some-set\".
|
||||
;; Set final name or fname: the last part of the name \"some-set\".
|
||||
;; Set path: the groups part of the name, as a vector [\"some-group\" \"some-subgroup\"].
|
||||
;; Set path str: the set path as a string \"some-group/some-subgroup\".
|
||||
;; Set full path: the path including the fname, as a vector [\"some-group\", \"some-subgroup\", \"some-set\"].
|
||||
;; Set full path str: the set full path as a string \"some-group/some-subgroup/some-set\".
|
||||
;
|
||||
;; Set prefix: the two-characters prefix added to a full path item \"G-\" / \"S-\".
|
||||
;; Prefixed set path or ppath: a path wit added prefixes [\"G-some-group\", \"G-some-subgroup\"].
|
||||
;; Prefixed set full path or pfpath: a full path wit prefixes [\"G-some-group\", \"G-some-subgroup\", \"S-some-set\"].
|
||||
;; Prefixed set final name or pfname: a final name with prefix \"S-some-set\".
|
||||
(add-set [_ token-set]
|
||||
(assert (token-set? token-set) "expected valid token-set")
|
||||
(let [path (get-set-prefixed-path token-set)]
|
||||
@@ -1206,7 +1209,7 @@ Will return a value that matches this schema:
|
||||
(when-let [theme (get-theme this id)]
|
||||
(contains? active-themes (get-theme-path theme))))
|
||||
|
||||
(toggle-theme-active? [this id]
|
||||
(toggle-theme-active [this id]
|
||||
(if (theme-active? this id)
|
||||
(deactivate-theme this id)
|
||||
(activate-theme this id)))
|
||||
@@ -1270,6 +1273,10 @@ Will return a value that matches this schema:
|
||||
(mapcat :sets)
|
||||
(get-active-themes this)))
|
||||
|
||||
(token-set-active? [this set-name]
|
||||
(let [set-names (get-active-themes-set-names this)]
|
||||
(contains? set-names set-name)))
|
||||
|
||||
(sets-at-path-all-active? [this group-path]
|
||||
(let [active-set-names (get-active-themes-set-names this)
|
||||
prefixed-path-str (set-group-path->set-group-prefixed-path-str group-path)]
|
||||
@@ -1404,7 +1411,11 @@ Will return a value that matches this schema:
|
||||
;; function that is declared but not defined; so we need to pass
|
||||
;; an anonymous function and delegate the resolution to runtime
|
||||
{:encode/json #(export-dtcg-json %)
|
||||
:decode/json #(parse-multi-set-dtcg-json %)}}))
|
||||
:decode/json #(read-multi-set-dtcg %)
|
||||
;; FIXME: add better, more reallistic generator
|
||||
:gen/gen (->> (sg/small-int)
|
||||
(sg/fmap (fn [_]
|
||||
(make-tokens-lib))))}}))
|
||||
|
||||
(defn duplicate-set
|
||||
"Make a new set with a unique name, copying data from the given set in the lib."
|
||||
@@ -1448,18 +1459,23 @@ Will return a value that matches this schema:
|
||||
["value" :map]
|
||||
["type" :string]]]))
|
||||
|
||||
(def ^:private schema:dtcg-node
|
||||
[:schema {:registry
|
||||
{::simple-value
|
||||
[:or :string :int :double]
|
||||
::value
|
||||
[:or
|
||||
[:ref ::simple-value]
|
||||
[:vector ::simple-value]
|
||||
[:map-of :string [:or
|
||||
[:ref ::simple-value]
|
||||
[:vector ::simple-value]]]]}}
|
||||
[:map
|
||||
["$type" :string]
|
||||
["$value" [:ref ::value]]]])
|
||||
|
||||
(def ^:private dtcg-node?
|
||||
(sm/validator
|
||||
[:or
|
||||
[:map
|
||||
["$value" :string]
|
||||
["$type" :string]]
|
||||
[:map
|
||||
["$value" [:sequential [:map ["$type" :string]]]]
|
||||
["$type" :string]]
|
||||
[:map
|
||||
["$value" :map]
|
||||
["$type" :string]]]))
|
||||
(sm/validator schema:dtcg-node))
|
||||
|
||||
(defn- get-json-format
|
||||
"Searches through decoded token file and returns:
|
||||
@@ -1646,6 +1662,43 @@ Will return a value that matches this schema:
|
||||
(assert (= (get-json-format decoded-json-tokens) :json-format/legacy) "expected a legacy format for `decoded-json-tokens`")
|
||||
(parse-single-set-dtcg-json set-name (legacy-json->dtcg-json decoded-json-tokens)))
|
||||
|
||||
(def ^:private schema:multi-set-dtcg
|
||||
"Schema for penpot multi-set dtcg json decoded data/
|
||||
|
||||
Mainly used for validate the structure of the incoming data before
|
||||
proceed to parse it to our internal data structures."
|
||||
[:schema {:registry
|
||||
{::node
|
||||
[:or
|
||||
[:map-of :string [:ref ::node]]
|
||||
schema:dtcg-node]}}
|
||||
[:map
|
||||
["$themes" {:optional true}
|
||||
[:vector
|
||||
[:map {:title "Theme"}
|
||||
["id" {:optional true} :string]
|
||||
["name" :string]
|
||||
["description" :string]
|
||||
["isSource" :boolean]
|
||||
["selectedTokenSets"
|
||||
[:map-of :string [:enum "enabled" "disabled"]]]]]]
|
||||
["$metadata" {:optional true}
|
||||
[:map {:title "Metadata"}
|
||||
["tokenSetOrder" {:optional true} [:vector :string]]
|
||||
["activeThemes" {:optional true} [:vector :string]]
|
||||
["activeSets" {:optional true} [:vector :string]]]]
|
||||
|
||||
[:malli.core/default
|
||||
[:map-of :string [:ref ::node]]]]])
|
||||
|
||||
(def ^:private check-multi-set-dtcg-data
|
||||
(sm/check-fn schema:multi-set-dtcg))
|
||||
|
||||
(def ^:private decode-multi-set-dtcg-data
|
||||
(sm/decoder schema:multi-set-dtcg
|
||||
sm/json-transformer))
|
||||
|
||||
;; FIXME: remove `-json` suffix
|
||||
(defn parse-multi-set-dtcg-json
|
||||
"Parse a decoded json file with multi sets in DTCG format into a TokensLib."
|
||||
[decoded-json]
|
||||
@@ -1685,10 +1738,10 @@ Will return a value that matches this schema:
|
||||
(uuid/next))
|
||||
:name (get theme "name")
|
||||
:group (get theme "group")
|
||||
:is-source (get theme "is-source")
|
||||
:is-source (or (get theme "isSource")
|
||||
;; NOTE: backward compatibility
|
||||
(get theme "is-source"))
|
||||
:external-id (get theme "id")
|
||||
:modified-at (some-> (get theme "modified-at")
|
||||
(ct/inst))
|
||||
:sets (into #{}
|
||||
(comp (map key)
|
||||
xf-normalize-set-name
|
||||
@@ -1736,6 +1789,23 @@ Will return a value that matches this schema:
|
||||
|
||||
library))
|
||||
|
||||
(defn read-multi-set-dtcg
|
||||
"Read penpot multi-set dctg tokens. Accepts string or JSON decoded
|
||||
data (without any case transformation). Used as schema decoder and
|
||||
in the SDK."
|
||||
[data]
|
||||
(let [data (if (string? data)
|
||||
(json/decode data :key-fn identity)
|
||||
data)
|
||||
data #?(:cljs (if (object? data)
|
||||
(json/->clj data :key-fn identity)
|
||||
data)
|
||||
:clj data)
|
||||
|
||||
data (decode-multi-set-dtcg-data data)]
|
||||
(-> (check-multi-set-dtcg-data data)
|
||||
(parse-multi-set-dtcg-json))))
|
||||
|
||||
(defn- parse-multi-set-legacy-json
|
||||
"Parse a decoded json file with multi sets in legacy format into a TokensLib."
|
||||
[decoded-json]
|
||||
@@ -1748,6 +1818,7 @@ Will return a value that matches this schema:
|
||||
(parse-multi-set-dtcg-json (merge other-data
|
||||
dtcg-sets-data))))
|
||||
|
||||
;; FIXME: remove `-json` suffix
|
||||
(defn parse-decoded-json
|
||||
"Guess the format and content type of the decoded json file and parse it into a TokensLib.
|
||||
The `file-name` is used to determine the set name when the json file contains a single set."
|
||||
@@ -1817,15 +1888,15 @@ Will return a value that matches this schema:
|
||||
(filter #(and (instance? TokenTheme %)
|
||||
(not (hidden-theme? %))))
|
||||
(map (fn [token-theme]
|
||||
(let [theme-map (->> token-theme
|
||||
(into {})
|
||||
walk/stringify-keys)]
|
||||
(-> theme-map
|
||||
(set/rename-keys {"sets" "selectedTokenSets"
|
||||
"external-id" "id"})
|
||||
(update "selectedTokenSets" (fn [sets]
|
||||
(->> (for [s sets] [s "enabled"])
|
||||
(into {})))))))))
|
||||
;; NOTE: this probaly can be implemented as type method
|
||||
(d/without-nils
|
||||
{"id" (:external-id token-theme)
|
||||
"name" (:name token-theme)
|
||||
"group" (:group token-theme)
|
||||
"description" (:description token-theme)
|
||||
"isSource" (:is-source token-theme)
|
||||
"selectedTokenSets" (reduce #(assoc %1 %2 "enabled") {} (:sets token-theme))}))))
|
||||
|
||||
themes
|
||||
(->> (get-theme-tree tokens-lib)
|
||||
(tree-seq d/ordered-map? vals)
|
||||
@@ -1835,29 +1906,34 @@ Will return a value that matches this schema:
|
||||
active-themes
|
||||
(-> (get-active-theme-paths tokens-lib)
|
||||
(disj hidden-theme-path))]
|
||||
{:themes themes
|
||||
:active-themes active-themes}))
|
||||
[themes active-themes]))
|
||||
|
||||
(defn export-dtcg-multi-file
|
||||
"Convert a TokensLib into a plain clojure map, suitable to be encoded as a multi json files each encoded in DTCG format."
|
||||
[tokens-lib]
|
||||
(let [{:keys [themes active-themes]} (dtcg-export-themes tokens-lib)
|
||||
sets (->> (get-sets tokens-lib)
|
||||
(map (fn [token-set]
|
||||
(let [name (get-name token-set)
|
||||
tokens (get-tokens- token-set)]
|
||||
[(str name ".json") (tokens-tree tokens :update-token-fn token->dtcg-token)])))
|
||||
(into {}))]
|
||||
(let [[themes active-themes]
|
||||
(dtcg-export-themes tokens-lib)
|
||||
|
||||
sets
|
||||
(->> (get-sets tokens-lib)
|
||||
(map (fn [token-set]
|
||||
(let [name (get-name token-set)
|
||||
tokens (get-tokens- token-set)]
|
||||
[(str name ".json") (tokens-tree tokens :update-token-fn token->dtcg-token)])))
|
||||
(into {}))]
|
||||
|
||||
(-> sets
|
||||
(assoc "$themes.json" themes)
|
||||
(assoc "$metadata.json" {"tokenSetOrder" (get-set-names tokens-lib)
|
||||
"activeThemes" active-themes
|
||||
"activeSets" (get-active-themes-set-names tokens-lib)}))))
|
||||
(assoc "$metadata.json"
|
||||
{"tokenSetOrder" (get-set-names tokens-lib)
|
||||
"activeThemes" active-themes
|
||||
"activeSets" (get-active-themes-set-names tokens-lib)}))))
|
||||
|
||||
(defn export-dtcg-json
|
||||
"Convert a TokensLib into a plain clojure map, suitable to be encoded as a multi sets json string in DTCG format."
|
||||
[tokens-lib]
|
||||
(let [{:keys [themes active-themes]} (dtcg-export-themes tokens-lib)
|
||||
(let [[themes active-themes]
|
||||
(dtcg-export-themes tokens-lib)
|
||||
|
||||
name-set-tuples
|
||||
(->> (get-set-tree tokens-lib)
|
||||
|
||||
@@ -1440,8 +1440,7 @@
|
||||
result (ctob/export-dtcg-json tokens-lib)
|
||||
expected {"$themes" [{"description" ""
|
||||
"group" "group-1"
|
||||
"is-source" false
|
||||
"modified-at" now
|
||||
"isSource" false
|
||||
"id" "test-id-00"
|
||||
"name" "theme-1"
|
||||
"selectedTokenSets" {"core" "enabled"}}]
|
||||
@@ -1558,12 +1557,11 @@
|
||||
:external-id "test-id-01"
|
||||
:modified-at now
|
||||
:sets #{"core"}))
|
||||
(ctob/toggle-theme-active? (thi/id :theme-1)))
|
||||
(ctob/toggle-theme-active (thi/id :theme-1)))
|
||||
result (ctob/export-dtcg-json tokens-lib)
|
||||
expected {"$themes" [{"description" ""
|
||||
"group" "group-1"
|
||||
"is-source" false
|
||||
"modified-at" now
|
||||
"isSource" false
|
||||
"id" "test-id-01"
|
||||
"name" "theme-1"
|
||||
"selectedTokenSets" {"core" "enabled"}}]
|
||||
@@ -1612,12 +1610,11 @@
|
||||
:external-id "test-id-01"
|
||||
:modified-at now
|
||||
:sets #{"some/set"}))
|
||||
(ctob/toggle-theme-active? (thi/id :theme-1)))
|
||||
(ctob/toggle-theme-active (thi/id :theme-1)))
|
||||
result (ctob/export-dtcg-multi-file tokens-lib)
|
||||
expected {"$themes.json" [{"description" ""
|
||||
"group" "group-1"
|
||||
"is-source" false
|
||||
"modified-at" now
|
||||
"isSource" false
|
||||
"id" "test-id-01"
|
||||
"name" "theme-1"
|
||||
"selectedTokenSets" {"some/set" "enabled"}}]
|
||||
|
||||
@@ -5,7 +5,7 @@ ENV LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
NODE_VERSION=v22.21.1 \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
PATH=/opt/node/bin:$PATH
|
||||
PATH=/opt/node/bin:/opt/imagick/bin:$PATH
|
||||
|
||||
RUN set -ex; \
|
||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||
@@ -62,6 +62,22 @@ RUN set -ex; \
|
||||
libxfixes3 \
|
||||
libxkbcommon0 \
|
||||
libxrandr2 \
|
||||
\
|
||||
libgomp1 \
|
||||
libheif1 \
|
||||
libjpeg-turbo8 \
|
||||
liblcms2-2 \
|
||||
libopenexr-3-1-30 \
|
||||
libopenjp2-7 \
|
||||
libpng16-16 \
|
||||
librsvg2-2 \
|
||||
libtiff6 \
|
||||
libwebp7 \
|
||||
libwebpdemux2 \
|
||||
libwebpmux3 \
|
||||
libxml2 \
|
||||
libzip4t64 \
|
||||
libzstd1 \
|
||||
; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
@@ -91,6 +107,7 @@ RUN set -eux; \
|
||||
|
||||
ARG BUNDLE_PATH="./bundle-exporter/"
|
||||
COPY --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/exporter/
|
||||
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
|
||||
|
||||
WORKDIR /opt/penpot/exporter
|
||||
USER penpot:penpot
|
||||
|
||||
|
After Width: | Height: | Size: 9.7 KiB |
@@ -0,0 +1,890 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"fdata/path-data",
|
||||
"plugins/runtime",
|
||||
"design-tokens/v1",
|
||||
"variants/v1",
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"render-wasm/v1",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:team-id": "~ueba8fa2e-4140-8084-8005-448635d7a724",
|
||||
"~: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": "New File 1",
|
||||
"~:revn": 1,
|
||||
"~:modified-at": "~m1762943590499",
|
||||
"~:vern": 0,
|
||||
"~:id": "~ub4133204-a015-80ed-8007-192a65398b0c",
|
||||
"~:is-shared": false,
|
||||
"~:migrations": {
|
||||
"~#ordered-set": [
|
||||
"legacy-2",
|
||||
"legacy-3",
|
||||
"legacy-5",
|
||||
"legacy-6",
|
||||
"legacy-7",
|
||||
"legacy-8",
|
||||
"legacy-9",
|
||||
"legacy-10",
|
||||
"legacy-11",
|
||||
"legacy-12",
|
||||
"legacy-13",
|
||||
"legacy-14",
|
||||
"legacy-16",
|
||||
"legacy-17",
|
||||
"legacy-18",
|
||||
"legacy-19",
|
||||
"legacy-25",
|
||||
"legacy-26",
|
||||
"legacy-27",
|
||||
"legacy-28",
|
||||
"legacy-29",
|
||||
"legacy-31",
|
||||
"legacy-32",
|
||||
"legacy-33",
|
||||
"legacy-34",
|
||||
"legacy-36",
|
||||
"legacy-37",
|
||||
"legacy-38",
|
||||
"legacy-39",
|
||||
"legacy-40",
|
||||
"legacy-41",
|
||||
"legacy-42",
|
||||
"legacy-43",
|
||||
"legacy-44",
|
||||
"legacy-45",
|
||||
"legacy-46",
|
||||
"legacy-47",
|
||||
"legacy-48",
|
||||
"legacy-49",
|
||||
"legacy-50",
|
||||
"legacy-51",
|
||||
"legacy-52",
|
||||
"legacy-53",
|
||||
"legacy-54",
|
||||
"legacy-55",
|
||||
"legacy-56",
|
||||
"legacy-57",
|
||||
"legacy-59",
|
||||
"legacy-62",
|
||||
"legacy-65",
|
||||
"legacy-66",
|
||||
"legacy-67",
|
||||
"0001-remove-tokens-from-groups",
|
||||
"0002-normalize-bool-content-v2",
|
||||
"0002-clean-shape-interactions",
|
||||
"0003-fix-root-shape",
|
||||
"0003-convert-path-content-v2",
|
||||
"0005-deprecate-image-type",
|
||||
"0006-fix-old-texts-fills",
|
||||
"0008-fix-library-colors-v4",
|
||||
"0009-clean-library-colors",
|
||||
"0009-add-partial-text-touched-flags",
|
||||
"0010-fix-swap-slots-pointing-non-existent-shapes",
|
||||
"0011-fix-invalid-text-touched-flags",
|
||||
"0012-fix-position-data",
|
||||
"0013-fix-component-path",
|
||||
"0013-clear-invalid-strokes-and-fills",
|
||||
"0014-fix-tokens-lib-duplicate-ids",
|
||||
"0014-clear-components-nil-objects",
|
||||
"0015-fix-text-attrs-blank-strings",
|
||||
"0015-clean-shadow-color",
|
||||
"0016-copy-fills-from-position-data-to-text-node"
|
||||
]
|
||||
},
|
||||
"~:version": 67,
|
||||
"~:project-id": "~ueba8fa2e-4140-8084-8005-448635da32b4",
|
||||
"~:created-at": "~m1762943119590",
|
||||
"~:backend": "legacy-db",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~ub4133204-a015-80ed-8007-192a65398b0d"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~ub4133204-a015-80ed-8007-192a65398b0d": {
|
||||
"~:objects": {
|
||||
"~u00000000-0000-0000-0000-000000000000": {
|
||||
"~#shape": {
|
||||
"~:y": 0,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 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
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 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,
|
||||
"~:r4": 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": [
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5ab7",
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5ab9",
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5abb",
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5abc"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5ab7": {
|
||||
"~#shape": {
|
||||
"~:y": 492.000000032425,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Board",
|
||||
"~:width": 348,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 354,
|
||||
"~:y": 492.000000032425
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 702,
|
||||
"~:y": 492.000000032425
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 702,
|
||||
"~:y": 829.000000032425
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 354,
|
||||
"~:y": 829.000000032425
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:show-content": false,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u8d80d76b-68f7-803d-8007-192c2e0a5ab7",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 354,
|
||||
"~:proportion": 1,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 354,
|
||||
"~:y": 492.000000032425,
|
||||
"~:width": 348,
|
||||
"~:height": 337,
|
||||
"~:x1": 354,
|
||||
"~:y1": 492.000000032425,
|
||||
"~:x2": 702,
|
||||
"~:y2": 829.000000032425
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#abf22a",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 337,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": [
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5ab8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5ab8": {
|
||||
"~#shape": {
|
||||
"~:y": 532.999984773636,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Rectangle",
|
||||
"~:width": 200,
|
||||
"~:type": "~:rect",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 421,
|
||||
"~:y": 532.999984773636
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 621,
|
||||
"~:y": 532.999984773636
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 621,
|
||||
"~:y": 712.999984773636
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 421,
|
||||
"~:y": 712.999984773636
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u8d80d76b-68f7-803d-8007-192c2e0a5ab8",
|
||||
"~:parent-id": "~u8d80d76b-68f7-803d-8007-192c2e0a5ab7",
|
||||
"~:frame-id": "~u8d80d76b-68f7-803d-8007-192c2e0a5ab7",
|
||||
"~:strokes": [],
|
||||
"~:x": 421,
|
||||
"~:proportion": 1,
|
||||
"~:shadow": [
|
||||
{
|
||||
"~:id": "~u005d085d-63c2-80b6-8007-18ffe15ad03b",
|
||||
"~:style": "~:drop-shadow",
|
||||
"~:color": {
|
||||
"~:color": "#f40000",
|
||||
"~:opacity": 1
|
||||
},
|
||||
"~:offset-x": 50,
|
||||
"~:offset-y": 50,
|
||||
"~:blur": 100,
|
||||
"~:spread": 0,
|
||||
"~:hidden": false
|
||||
}
|
||||
],
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 421,
|
||||
"~:y": 532.999984773636,
|
||||
"~:width": 200,
|
||||
"~:height": 180,
|
||||
"~:x1": 421,
|
||||
"~:y1": 532.999984773636,
|
||||
"~:x2": 621,
|
||||
"~:y2": 712.999984773636
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#003ef9",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 180,
|
||||
"~:flip-y": null
|
||||
}
|
||||
},
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5ab9": {
|
||||
"~#shape": {
|
||||
"~:y": 491.999999162674,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Board",
|
||||
"~:width": 348,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 958.999967498779,
|
||||
"~:y": 491.999999162674
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1306.99996749878,
|
||||
"~:y": 491.999999162674
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1306.99996749878,
|
||||
"~:y": 828.999999162674
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 958.999967498779,
|
||||
"~:y": 828.999999162674
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:show-content": true,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u8d80d76b-68f7-803d-8007-192c2e0a5ab9",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 958.999967498779,
|
||||
"~:proportion": 1,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 958.999967498779,
|
||||
"~:y": 491.999999162674,
|
||||
"~:width": 348,
|
||||
"~:height": 337,
|
||||
"~:x1": 958.999967498779,
|
||||
"~:y1": 491.999999162674,
|
||||
"~:x2": 1306.99996749878,
|
||||
"~:y2": 828.999999162674
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#abf22a",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 337,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": [
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5aba"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5aba": {
|
||||
"~#shape": {
|
||||
"~:y": 532.999999162674,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Rectangle",
|
||||
"~:width": 200,
|
||||
"~:type": "~:rect",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1025.99998275757,
|
||||
"~:y": 532.999999162674
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1225.99998275757,
|
||||
"~:y": 532.999999162674
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1225.99998275757,
|
||||
"~:y": 712.999999162674
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1025.99998275757,
|
||||
"~:y": 712.999999162674
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u8d80d76b-68f7-803d-8007-192c2e0a5aba",
|
||||
"~:parent-id": "~u8d80d76b-68f7-803d-8007-192c2e0a5ab9",
|
||||
"~:frame-id": "~u8d80d76b-68f7-803d-8007-192c2e0a5ab9",
|
||||
"~:strokes": [],
|
||||
"~:x": 1025.99998275757,
|
||||
"~:proportion": 1,
|
||||
"~:shadow": [
|
||||
{
|
||||
"~:id": "~u005d085d-63c2-80b6-8007-18ffe15ad03b",
|
||||
"~:style": "~:drop-shadow",
|
||||
"~:color": {
|
||||
"~:color": "#f40000",
|
||||
"~:opacity": 1
|
||||
},
|
||||
"~:offset-x": 50,
|
||||
"~:offset-y": 50,
|
||||
"~:blur": 100,
|
||||
"~:spread": 0,
|
||||
"~:hidden": false
|
||||
}
|
||||
],
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 1025.99998275757,
|
||||
"~:y": 532.999999162674,
|
||||
"~:width": 200,
|
||||
"~:height": 180,
|
||||
"~:x1": 1025.99998275757,
|
||||
"~:y1": 532.999999162674,
|
||||
"~:x2": 1225.99998275757,
|
||||
"~:y2": 712.999999162674
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#003ef9",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 180,
|
||||
"~:flip-y": null
|
||||
}
|
||||
},
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5abb": {
|
||||
"~#shape": {
|
||||
"~:y": 450.000000032425,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "hjocz4oksb",
|
||||
"~:children": [
|
||||
{
|
||||
"~:type": "paragraph-set",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "9xn5ujq7hr",
|
||||
"~:font-size": "14",
|
||||
"~:font-weight": "400",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "CLIPPING"
|
||||
}
|
||||
],
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "2x8x661yyn",
|
||||
"~:font-size": "14",
|
||||
"~:font-weight": "400",
|
||||
"~:text-direction": "ltr",
|
||||
"~:type": "paragraph",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "CLIPPING",
|
||||
"~:width": 194,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 359,
|
||||
"~:y": 450.000000032425
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 553,
|
||||
"~:y": 450.000000032425
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 553,
|
||||
"~:y": 475.000000032425
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 359,
|
||||
"~:y": 475.000000032425
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:id": "~u8d80d76b-68f7-803d-8007-192c2e0a5abb",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 359,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 359,
|
||||
"~:y": 450.000000032425,
|
||||
"~:width": 194,
|
||||
"~:height": 25,
|
||||
"~:x1": 359,
|
||||
"~:y1": 450.000000032425,
|
||||
"~:x2": 553,
|
||||
"~:y2": 475.000000032425
|
||||
}
|
||||
},
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 25,
|
||||
"~:flip-y": null
|
||||
}
|
||||
},
|
||||
"~u8d80d76b-68f7-803d-8007-192c2e0a5abc": {
|
||||
"~#shape": {
|
||||
"~:y": 450,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "hjocz4oksb",
|
||||
"~:children": [
|
||||
{
|
||||
"~:type": "paragraph-set",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "",
|
||||
"~:font-style": "normal",
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "9xn5ujq7hr",
|
||||
"~:font-size": "14",
|
||||
"~:font-weight": "400",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "NO CLIPPING"
|
||||
}
|
||||
],
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "2x8x661yyn",
|
||||
"~:font-size": "0",
|
||||
"~:font-weight": "400",
|
||||
"~:text-direction": "ltr",
|
||||
"~:type": "paragraph",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "CLIPPING",
|
||||
"~:width": 194,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 958.999966363907,
|
||||
"~:y": 450
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1152.99996636391,
|
||||
"~:y": 450
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 1152.99996636391,
|
||||
"~:y": 475
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 958.999966363907,
|
||||
"~:y": 475
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:id": "~u8d80d76b-68f7-803d-8007-192c2e0a5abc",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 958.999966363907,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 958.999966363907,
|
||||
"~:y": 450,
|
||||
"~:width": 194,
|
||||
"~:height": 25,
|
||||
"~:x1": 958.999966363907,
|
||||
"~:y1": 450,
|
||||
"~:x2": 1152.99996636391,
|
||||
"~:y2": 475
|
||||
}
|
||||
},
|
||||
"~:fills": [],
|
||||
"~:flip-x": null,
|
||||
"~:height": 25,
|
||||
"~:flip-y": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~ub4133204-a015-80ed-8007-192a65398b0d",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~ub4133204-a015-80ed-8007-192a65398b0c",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,18 @@ export class BasePage {
|
||||
);
|
||||
}
|
||||
|
||||
static async mockFileMediaAsset(page, assetId, assetFilename, options) {
|
||||
static async mockFileMediaAsset(
|
||||
page,
|
||||
assetId,
|
||||
assetFilename,
|
||||
assetThumbnailFilename,
|
||||
options,
|
||||
) {
|
||||
const ids = Array.isArray(assetId) ? assetId : [assetId];
|
||||
|
||||
for (const id of ids) {
|
||||
const url = `**/assets/by-file-media-id/${id}`;
|
||||
const thumbnailUrl = `${url}/thumbnail`;
|
||||
|
||||
await page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
@@ -36,6 +43,16 @@ export class BasePage {
|
||||
...options,
|
||||
}),
|
||||
);
|
||||
|
||||
if (assetThumbnailFilename) {
|
||||
await page.route(thumbnailUrl, (route) =>
|
||||
route.fulfill({
|
||||
path: `playwright/data/${assetThumbnailFilename}`,
|
||||
status: 200,
|
||||
...options,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,22 +72,6 @@ export class BasePage {
|
||||
}
|
||||
}
|
||||
|
||||
static async mockFileMediaAsset(page, assetId, assetFilename, options) {
|
||||
const ids = Array.isArray(assetId) ? assetId : [assetId];
|
||||
|
||||
for (const id of ids) {
|
||||
const url = `**/assets/by-file-media-id/${id}`;
|
||||
|
||||
await page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
path: `playwright/data/${assetFilename}`,
|
||||
status: 200,
|
||||
...options,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static async mockConfigFlags(page, flags) {
|
||||
const url = "**/js/config.js?ts=*";
|
||||
return await page.route(url, (route) =>
|
||||
@@ -100,11 +101,17 @@ export class BasePage {
|
||||
return BasePage.mockConfigFlags(this.page, flags);
|
||||
}
|
||||
|
||||
async mockFileMediaAsset(assetId, assetFilename, options) {
|
||||
async mockFileMediaAsset(
|
||||
assetId,
|
||||
assetFilename,
|
||||
assetThumbnailFilename,
|
||||
options,
|
||||
) {
|
||||
return BasePage.mockFileMediaAsset(
|
||||
this.page,
|
||||
assetId,
|
||||
assetFilename,
|
||||
assetThumbnailFilename,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BasePage } from "./BasePage";
|
||||
export class LoginPage extends BasePage {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
this.loginButton = page.getByRole("button", { name: "Login" });
|
||||
this.loginButton = page.getByRole("button", { name: "Continue" });
|
||||
this.password = page.getByLabel("Password");
|
||||
this.userName = page.getByLabel("Email");
|
||||
this.invalidCredentialsError = page.getByText(
|
||||
|
||||
@@ -36,6 +36,7 @@ test("Renders a file with solid, gradient and image fills", async ({
|
||||
"1ebcea38-f1bf-8101-8006-4c8f579da49c",
|
||||
],
|
||||
"render-wasm/assets/penguins.jpg",
|
||||
"render-wasm/assets/pattern-thumbnail.png", // FIXME: get real thumbnail
|
||||
);
|
||||
await workspace.mockGetFile("render-wasm/get-file-shapes-fills.json");
|
||||
|
||||
@@ -58,6 +59,7 @@ test("Renders a file with strokes", async ({ page }) => {
|
||||
"202c1104-9385-81d3-8006-507560ce29e3",
|
||||
],
|
||||
"render-wasm/assets/penguins.jpg",
|
||||
"render-wasm/assets/pattern-thumbnail.png", // FIXME: get real thumbnail
|
||||
);
|
||||
await workspace.mockGetFile("render-wasm/get-file-shapes-strokes.json");
|
||||
|
||||
@@ -88,6 +90,11 @@ test("Renders a file with shapes with multiple fills", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-multiple-fills.json");
|
||||
await workspace.mockFileMediaAsset(
|
||||
["c0939f58-37bc-805d-8006-51cda84a405a"],
|
||||
"render-wasm/assets/penguins.jpg",
|
||||
"render-wasm/assets/pattern-thumbnail.png", // FIXME: get real thumbnail
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "c0939f58-37bc-805d-8006-51cd3a51c255",
|
||||
@@ -127,6 +134,7 @@ test("Renders shapes with exif rotated images fills and strokes", async ({
|
||||
"27270c45-35b4-80f3-8006-63a3ea82557f",
|
||||
],
|
||||
"render-wasm/assets/landscape.jpg",
|
||||
"render-wasm/assets/pattern-thumbnail.png", // FIXME: get real thumbnail
|
||||
);
|
||||
await workspace.mockGetFile(
|
||||
"render-wasm/get-file-shapes-exif-rotated-fills.json",
|
||||
@@ -170,6 +178,15 @@ test("Renders a file with blurs applied to any kind of shape", async ({
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-blurs.json");
|
||||
await workspace.mockFileMediaAsset(
|
||||
[
|
||||
"aa0a383a-7553-808a-8006-ae13a3c575eb",
|
||||
"aa0a383a-7553-808a-8006-ae13c84d6e3a",
|
||||
"aa0a383a-7553-808a-8006-ae131157fc26",
|
||||
],
|
||||
"render-wasm/assets/pattern.png",
|
||||
"render-wasm/assets/pattern-thumbnail.png", // FIXME: get real thumbnail
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "aa0a383a-7553-808a-8006-ae1237b52cf9",
|
||||
@@ -212,10 +229,7 @@ test("Renders a file with a closed path shape with multiple segments using strok
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
||||
test("Renders a file with paths and svg attrs", async ({
|
||||
page,
|
||||
}) => {
|
||||
test("Renders a file with paths and svg attrs", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-svg-attrs.json");
|
||||
@@ -234,7 +248,9 @@ test("Renders a file with nested frames with inherited blur", async ({
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-frame-with-nested-blur.json");
|
||||
await workspace.mockGetFile(
|
||||
"render-wasm/get-file-frame-with-nested-blur.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "58c5cc60-d124-81bd-8007-0ee4e5030609",
|
||||
@@ -244,3 +260,19 @@ test("Renders a file with nested frames with inherited blur", async ({
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a clipped frame with a large blur drop shadow", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-large-blur-shadow.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "b4133204-a015-80ed-8007-192a65398b0c",
|
||||
pageId: "b4133204-a015-80ed-8007-192a65398b0d",
|
||||
});
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 318 KiB |
|
Before Width: | Height: | Size: 322 KiB After Width: | Height: | Size: 335 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 44 KiB |
@@ -141,6 +141,7 @@ test("Renders a file with texts with images", async ({ page }) => {
|
||||
"4f89252d-ebbc-813e-8006-8699e4170e18",
|
||||
],
|
||||
"render-wasm/assets/pattern.png",
|
||||
"render-wasm/assets/pattern-thumbnail.png",
|
||||
);
|
||||
await mockGetEmojiFont(workspace);
|
||||
await mockGetJapaneseFont(workspace);
|
||||
@@ -179,6 +180,7 @@ test("Renders a file with text decoration", async ({ page }) => {
|
||||
await workspace.mockFileMediaAsset(
|
||||
["d6c33e7b-7b64-80f3-8006-78509a3a2d21"],
|
||||
"render-wasm/assets/pattern.png",
|
||||
"render-wasm/assets/pattern-thumbnail.png",
|
||||
);
|
||||
await mockGetEmojiFont(workspace);
|
||||
await mockGetJapaneseFont(workspace);
|
||||
@@ -281,14 +283,10 @@ test("Renders a file with different text shadows combinations", async ({
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with multiple text shadows in order", async ({
|
||||
page,
|
||||
}) => {
|
||||
test("Renders a file with multiple text shadows in order", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile(
|
||||
"render-wasm/get-file-text-shadows-order.json",
|
||||
);
|
||||
await workspace.mockGetFile("render-wasm/get-file-text-shadows-order.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "48ffa82f-6950-81b5-8006-e49a2a39657f",
|
||||
@@ -337,7 +335,9 @@ test("Renders a file with texts with with text spans of different sizes", async
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-text-spans-different-sizes.json");
|
||||
await workspace.mockGetFile(
|
||||
"render-wasm/get-file-text-spans-different-sizes.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "a0b1a70e-0d02-8082-8006-ff6d160f15ce",
|
||||
@@ -347,9 +347,25 @@ test("Renders a file with texts with with text spans of different sizes", async
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with texts with tabs", async ({
|
||||
test("Renders a file with texts with paragraphs and breaking lines", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile(
|
||||
"render-wasm/get-file-text-paragraph-new-lines.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "a5f238bd-dd8a-8164-8007-1bc3481eaf05",
|
||||
pageId: "a5f238bd-dd8a-8164-8007-1bc3481eaf06",
|
||||
});
|
||||
await workspace.waitForFirstRender();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
// TODO: enable this test once we use the wasm renderer in the new editor
|
||||
test.skip("Renders a file with texts with tabs", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-text-tabs.json");
|
||||
@@ -367,9 +383,8 @@ test("Renders a file with texts with tabs", async ({
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with texts with empty lines", async ({
|
||||
page,
|
||||
}) => {
|
||||
// TODO: enable this test once we use the wasm renderer in the new editor
|
||||
test.skip("Renders a file with texts with empty lines", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-empty-lines.json");
|
||||
@@ -387,9 +402,8 @@ test("Renders a file with texts with empty lines", async ({
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with texts with breaking words", async ({
|
||||
page,
|
||||
}) => {
|
||||
// TODO: enable this test once we use the wasm renderer in the new editor
|
||||
test.skip("Renders a file with texts with breaking words", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-empty-lines.json");
|
||||
|
||||
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 104 KiB |
@@ -120,6 +120,16 @@ const openInspectTab = async (workspacePage) => {
|
||||
await inspectButton.click();
|
||||
};
|
||||
|
||||
const selectColorSpace = async (workspacePage, colorSpace) => {
|
||||
const sidebar = workspacePage.page.getByTestId("right-sidebar");
|
||||
const colorSpaceSelector = sidebar.getByLabel("Select color space");
|
||||
await colorSpaceSelector.click();
|
||||
const colorSpaceOption = sidebar.getByRole("option", {
|
||||
name: colorSpace,
|
||||
});
|
||||
await colorSpaceOption.click();
|
||||
};
|
||||
|
||||
test.describe("Inspect tab - Styles", () => {
|
||||
test("Open Inspect tab", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
@@ -383,6 +393,46 @@ test.describe("Inspect tab - Styles", () => {
|
||||
expect(propertyRowCount).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("Change color space and ensure fill and shorthand changes", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
await selectLayer(workspacePage, shapeToLayerName.fill.solid);
|
||||
await openInspectTab(workspacePage);
|
||||
const panel = await getPanelByTitle(workspacePage, "Fill");
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
const propertyRow = panel.getByTestId("property-row");
|
||||
const backgroundRow = propertyRow.filter({
|
||||
hasText: "Background",
|
||||
});
|
||||
await expect(backgroundRow).toBeVisible();
|
||||
|
||||
// Ensure initial value and copied value are in HEX format
|
||||
expect(backgroundRow).toContainText("#0438d5 100%");
|
||||
|
||||
await copyPropertyFromPropertyRow(panel, "Background");
|
||||
|
||||
const backgroundHEX = await page.evaluate(() =>
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
expect(backgroundHEX).toContain("background: #0438d5FF;");
|
||||
|
||||
// Change color space to RGBA
|
||||
await selectColorSpace(workspacePage, "rgba");
|
||||
|
||||
// Ensure new value and copied value are in RGBA format
|
||||
expect(backgroundRow).toContainText("4, 56, 213, 1");
|
||||
|
||||
await copyPropertyFromPropertyRow(panel, "Background");
|
||||
const backgroundRGBA = await page.evaluate(() =>
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
expect(backgroundRGBA).toContain("background: rgba(4, 56, 213, 1);");
|
||||
});
|
||||
|
||||
test("Shape - Fill - Gradient", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupFile(workspacePage);
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"Auth related data events"
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -148,9 +147,7 @@
|
||||
(defn login-with-ldap
|
||||
[params]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid params"
|
||||
(sm/check schema:login-with-ldap params))
|
||||
(assert (sm/check schema:login-with-ldap params))
|
||||
|
||||
(ptk/reify ::login-with-ldap
|
||||
ptk/WatchEvent
|
||||
@@ -166,6 +163,32 @@
|
||||
(logged-in))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
(def ^:private schema:login-with-sso
|
||||
[:map {:title "login-with-sso"}
|
||||
[:provider [:or :string ::sm/uuid]]])
|
||||
|
||||
(defn login-with-sso
|
||||
"Start the SSO flow"
|
||||
[params]
|
||||
(assert (sm/check schema:login-with-sso params))
|
||||
(ptk/reify ::login-with-sso
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :login-with-oidc params)
|
||||
(rx/map (fn [{:keys [redirect-uri] :as rsp}]
|
||||
(if redirect-uri
|
||||
(rt/nav-raw :uri redirect-uri)
|
||||
(ex/raise :type :internal
|
||||
:code :unexpected-response
|
||||
:hint "unexpected response from OIDC method"
|
||||
:resp (pr-str rsp)))))
|
||||
(rx/catch (fn [cause]
|
||||
(let [{:keys [type code] :as error} (ex-data cause)]
|
||||
(if (and (= type :restriction)
|
||||
(= code :provider-not-configured))
|
||||
(rx/of (ntf/error (tr "errors.auth-provider-not-configured")))
|
||||
(rx/throw cause)))))))))
|
||||
|
||||
(defn login-from-token
|
||||
"Used mainly as flow continuation after token validation."
|
||||
[{:keys [profile] :as tdata}]
|
||||
@@ -201,7 +224,7 @@
|
||||
;; --- EVENT: logout
|
||||
|
||||
(defn logged-out
|
||||
[]
|
||||
[{:keys [redirect-uri]}]
|
||||
(ptk/reify ::logged-out
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -209,12 +232,16 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/merge
|
||||
;; NOTE: We need the `effect` of the current event to be
|
||||
;; executed before the redirect.
|
||||
(->> (rx/of (rt/nav :auth-login))
|
||||
(rx/observe-on :async))
|
||||
(rx/of (ws/finalize))))
|
||||
(if redirect-uri
|
||||
(->> (rx/of (rt/nav-raw :uri (str redirect-uri)))
|
||||
(rx/observe-on :async))
|
||||
|
||||
(rx/merge
|
||||
;; NOTE: We need the `effect` of the current event to be
|
||||
;; executed before the redirect.
|
||||
(->> (rx/of (rt/nav :auth-login))
|
||||
(rx/observe-on :async))
|
||||
(rx/of (ws/finalize)))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
@@ -235,7 +262,7 @@
|
||||
(rx/mapcat (fn [_]
|
||||
(->> (rp/cmd! :logout {:profile-id profile-id})
|
||||
(rx/delay-at-least 300)
|
||||
(rx/catch (constantly (rx/of 1))))))
|
||||
(rx/catch (constantly (rx/of nil))))))
|
||||
(rx/map logged-out))))))
|
||||
|
||||
;; --- Update Profile
|
||||
@@ -248,9 +275,7 @@
|
||||
(defn request-profile-recovery
|
||||
[data]
|
||||
|
||||
(dm/assert!
|
||||
"expected valid parameters"
|
||||
(sm/check schema:request-profile-recovery data))
|
||||
(assert (sm/check schema:request-profile-recovery data))
|
||||
|
||||
(ptk/reify ::request-profile-recovery
|
||||
ptk/WatchEvent
|
||||
@@ -273,9 +298,7 @@
|
||||
|
||||
(defn recover-profile
|
||||
[data]
|
||||
(dm/assert!
|
||||
"expected valid arguments"
|
||||
(sm/check schema:recover-profile data))
|
||||
(assert (sm/check schema:recover-profile data))
|
||||
|
||||
(ptk/reify ::recover-profile
|
||||
ptk/WatchEvent
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.types.path :as path]))
|
||||
@@ -207,3 +208,12 @@
|
||||
:projects
|
||||
(filter #(= team-id (:team-id (val %))))
|
||||
(into {}))))
|
||||
|
||||
(defn get-selrect
|
||||
[selrect-transform shape]
|
||||
(if (some? selrect-transform)
|
||||
(let [{:keys [center width height transform]} selrect-transform]
|
||||
[(gsh/center->rect center width height)
|
||||
(gmt/transform-in center transform)])
|
||||
[(dm/get-prop shape :selrect)
|
||||
(gsh/transform-matrix shape)]))
|
||||
|
||||
@@ -212,13 +212,14 @@
|
||||
;; Create a new objects only with the temporary modifications
|
||||
objects-changed
|
||||
(->> wasm-props
|
||||
(group-by first)
|
||||
(reduce
|
||||
(fn [objects [id properties]]
|
||||
(let [shape
|
||||
(->> properties
|
||||
(reduce
|
||||
(fn [shape {:keys [property value]}]
|
||||
(assoc shape property value))
|
||||
(fn [shape [_ operation]]
|
||||
(ctm/apply-modifier shape operation))
|
||||
(get objects id)))]
|
||||
(assoc objects id shape)))
|
||||
objects))]
|
||||
|
||||
@@ -96,6 +96,16 @@
|
||||
(->> (rx/from ids)
|
||||
(rx/map resize-wasm-text)))))
|
||||
|
||||
;; -- Content helpers
|
||||
|
||||
(defn- v2-content-has-text?
|
||||
[content]
|
||||
(boolean
|
||||
(when content
|
||||
(some (fn [node]
|
||||
(not (str/blank? (:text node ""))))
|
||||
(txt/node-seq txt/is-text-node? content)))))
|
||||
|
||||
;; -- Editor
|
||||
|
||||
(defn update-editor
|
||||
@@ -948,28 +958,34 @@
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
shape (get objects id)
|
||||
new-shape? (nil? (:content shape))]
|
||||
(rx/of
|
||||
(dwsh/update-shapes
|
||||
[id]
|
||||
(fn [shape]
|
||||
(let [new-shape (-> shape
|
||||
(assoc :content content)
|
||||
(cond-> (and update-name? (some? name))
|
||||
(assoc :name name)))]
|
||||
new-shape))
|
||||
{:undo-group (when new-shape? id)})
|
||||
(rx/concat
|
||||
(rx/of
|
||||
(dwsh/update-shapes
|
||||
[id]
|
||||
(fn [shape]
|
||||
(let [new-shape (-> shape
|
||||
(assoc :content content)
|
||||
(cond-> (and update-name? (some? name))
|
||||
(assoc :name name)))]
|
||||
new-shape))
|
||||
{:undo-group (when new-shape? id)})
|
||||
|
||||
(if (and (not= :fixed (:grow-type shape)) finalize?)
|
||||
(dwm/apply-wasm-modifiers
|
||||
(resize-wasm-text-modifiers shape content)
|
||||
{:undo-group (when new-shape? id)})
|
||||
(if (and (not= :fixed (:grow-type shape)) finalize?)
|
||||
(dwm/apply-wasm-modifiers
|
||||
(resize-wasm-text-modifiers shape content)
|
||||
{:undo-group (when new-shape? id)})
|
||||
|
||||
(dwm/set-wasm-modifiers
|
||||
(resize-wasm-text-modifiers shape content)
|
||||
{:undo-group (when new-shape? id)}))
|
||||
(dwm/set-wasm-modifiers
|
||||
(resize-wasm-text-modifiers shape content)
|
||||
{:undo-group (when new-shape? id)})))
|
||||
|
||||
(when finalize?
|
||||
(dwt/finish-transform))))
|
||||
(rx/concat
|
||||
(when (and (not (v2-content-has-text? content)) (some? id))
|
||||
(rx/of
|
||||
(dws/deselect-shape id)
|
||||
(dwsh/delete-shapes #{id})))
|
||||
(rx/of (dwt/finish-transform))))))
|
||||
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
shape (get objects id)
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
(declare token-properties)
|
||||
(declare update-layout-item-margin)
|
||||
(declare all-attrs-appliable-for-token?)
|
||||
|
||||
;; Events to update the value of attributes with applied tokens ---------------------------------------------------------
|
||||
|
||||
@@ -519,7 +520,8 @@
|
||||
(or
|
||||
(and (ctsl/any-layout-immediate-child? objects shape)
|
||||
(some ctt/spacing-margin-keys attributes))
|
||||
(ctt/any-appliable-attr? attributes (:type shape) (:layout shape))))))
|
||||
(and (ctt/any-appliable-attr-for-shape? attributes (:type shape) (:layout shape))
|
||||
(all-attrs-appliable-for-token? attributes (:type token)))))))
|
||||
shape-ids (d/nilv (keys shapes) [])
|
||||
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
||||
|
||||
@@ -596,6 +598,7 @@
|
||||
(watch [_ state _]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
shapes (into [] (keep (d/getf objects)) shape-ids)
|
||||
|
||||
shapes
|
||||
(if expand-with-children
|
||||
(into []
|
||||
@@ -605,10 +608,15 @@
|
||||
[shape])))
|
||||
shapes)
|
||||
shapes)
|
||||
|
||||
{:keys [attributes all-attributes on-update-shape]}
|
||||
(get token-properties (:type token))
|
||||
|
||||
unapply-tokens?
|
||||
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))]
|
||||
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
|
||||
|
||||
shape-ids (map :id shapes)]
|
||||
|
||||
(if unapply-tokens?
|
||||
(rx/of
|
||||
(unapply-token {:attributes (or attrs all-attributes attributes)
|
||||
@@ -620,7 +628,7 @@
|
||||
(apply-spacing-token {:token token
|
||||
:attr attrs
|
||||
:shapes shapes})
|
||||
(apply-token {:attributes (or attrs attributes)
|
||||
(apply-token {:attributes (if (empty? attrs) attributes attrs)
|
||||
:token token
|
||||
:shape-ids shape-ids
|
||||
:on-update-shape on-update-shape}))))))))
|
||||
@@ -808,3 +816,22 @@
|
||||
|
||||
(defn get-token-properties [token]
|
||||
(get token-properties (:type token)))
|
||||
|
||||
(defn get-update-shape-fn
|
||||
"Get the function that updates the attributes of a shape if this token is applied."
|
||||
[token]
|
||||
(when token
|
||||
(-> (get-token-properties token)
|
||||
:on-update-shape)))
|
||||
|
||||
(defn appliable-attributes-for-token
|
||||
"Get the attributes to which this token type can be applied."
|
||||
[token-type]
|
||||
(let [props (get token-properties token-type)]
|
||||
(or (:all-attributes props)
|
||||
(:attributes props))))
|
||||
|
||||
(defn all-attrs-appliable-for-token?
|
||||
"Check if any of the given attributes can be applied for the given token type."
|
||||
[attributes token-type]
|
||||
(set/subset? attributes (appliable-attributes-for-token token-type)))
|
||||
|
||||
@@ -84,7 +84,8 @@
|
||||
new-token-theme))]
|
||||
(rx/of (dch/commit-changes changes)))))))))
|
||||
|
||||
(defn update-token-theme [id token-theme]
|
||||
(defn update-token-theme
|
||||
[id token-theme]
|
||||
(ptk/reify ::update-token-theme
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
@@ -101,27 +102,38 @@
|
||||
(pcb/set-token-theme (ctob/get-id token-theme) token-theme))]
|
||||
(rx/of (dch/commit-changes changes))))))))
|
||||
|
||||
(defn toggle-token-theme-active? [id]
|
||||
(ptk/reify ::toggle-token-theme-active?
|
||||
(defn set-token-theme-active
|
||||
[id active?]
|
||||
(assert (uuid? id) "expected a uuid for `id`")
|
||||
(assert (boolean? active?) "expected a boolean for `active?`")
|
||||
(ptk/reify ::set-token-theme-active
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (get-tokens-lib state)
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data data)
|
||||
(clt/generate-set-active-token-theme tokens-lib id active?))]
|
||||
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(dwtp/propagate-workspace-tokens))))))
|
||||
|
||||
(defn toggle-token-theme-active
|
||||
[id]
|
||||
(ptk/reify ::toggle-token-theme-active
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
|
||||
tokens-lib (get-tokens-lib state)
|
||||
active-token-themes (some-> tokens-lib
|
||||
(ctob/toggle-theme-active? id)
|
||||
(ctob/get-active-theme-paths))
|
||||
active-token-themes' (if (= active-token-themes #{ctob/hidden-theme-path})
|
||||
active-token-themes
|
||||
(disj active-token-themes ctob/hidden-theme-path))
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-active-token-themes active-token-themes'))]
|
||||
(clt/generate-toggle-token-theme tokens-lib id))]
|
||||
(rx/of
|
||||
(dch/commit-changes changes)
|
||||
(dwtp/propagate-workspace-tokens))))))
|
||||
|
||||
(defn delete-token-theme [id]
|
||||
(defn delete-token-theme
|
||||
[id]
|
||||
(ptk/reify ::delete-token-theme
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
@@ -134,7 +146,7 @@
|
||||
(dwtp/propagate-workspace-tokens))))))
|
||||
|
||||
(defn create-token-set
|
||||
[set-name]
|
||||
[token-set]
|
||||
(ptk/reify ::create-token-set
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -145,20 +157,20 @@
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tokens-lib (get data :tokens-lib)
|
||||
set-name (ctob/normalize-set-name set-name)]
|
||||
(if (and tokens-lib (ctob/get-set-by-name tokens-lib set-name))
|
||||
token-set (ctob/rename token-set (ctob/normalize-set-name (ctob/get-name token-set)))]
|
||||
(if (and tokens-lib (ctob/get-set-by-name tokens-lib (ctob/get-name token-set)))
|
||||
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 9000}))
|
||||
(let [token-set (ctob/make-token-set :name set-name)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set (ctob/get-id token-set) token-set))]
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token-set (ctob/get-id token-set) token-set))]
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))))
|
||||
|
||||
(defn rename-token-set-group [set-group-path set-group-fname]
|
||||
(defn rename-token-set-group
|
||||
[set-group-path set-group-fname]
|
||||
(ptk/reify ::rename-token-set-group
|
||||
ptk/WatchEvent
|
||||
(watch [it _state _]
|
||||
@@ -203,6 +215,22 @@
|
||||
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
|
||||
(dch/commit-changes changes))))))))
|
||||
|
||||
(defn set-enabled-token-set
|
||||
[name enabled?]
|
||||
(assert (string? name) "expected a string for `name`")
|
||||
(assert (boolean? enabled?) "expected a boolean for `enabled?`")
|
||||
(ptk/reify ::set-enabled-token-set
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
tlib (get-tokens-lib state)
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-library-data data)
|
||||
(clt/generate-set-enabled-token-set tlib name enabled?))]
|
||||
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(dwtp/propagate-workspace-tokens))))))
|
||||
|
||||
(defn toggle-token-set
|
||||
[name]
|
||||
(assert (string? name) "expected a string for `name`")
|
||||
@@ -218,7 +246,8 @@
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(dwtp/propagate-workspace-tokens))))))
|
||||
|
||||
(defn toggle-token-set-group [group-path]
|
||||
(defn toggle-token-set-group
|
||||
[group-path]
|
||||
(ptk/reify ::toggle-token-set-group
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
@@ -230,7 +259,8 @@
|
||||
(dch/commit-changes changes)
|
||||
(dwtp/propagate-workspace-tokens))))))
|
||||
|
||||
(defn import-tokens-lib [lib]
|
||||
(defn import-tokens-lib
|
||||
[lib]
|
||||
(ptk/reify ::import-tokens-lib
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
@@ -265,7 +295,8 @@
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(dwtp/propagate-workspace-tokens))))))
|
||||
|
||||
(defn drop-error [{:keys [error to-path]}]
|
||||
(defn drop-error
|
||||
[{:keys [error to-path]}]
|
||||
(ptk/reify ::drop-error
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
@@ -282,7 +313,8 @@
|
||||
|
||||
;; FIXME: add schema for params
|
||||
|
||||
(defn drop-token-set-group [drop-opts]
|
||||
(defn drop-token-set-group
|
||||
[drop-opts]
|
||||
(ptk/reify ::drop-token-set-group
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
@@ -344,47 +376,52 @@
|
||||
(set-selected-token-set-id (ctob/get-id token-set)))))))
|
||||
|
||||
(defn create-token
|
||||
[params]
|
||||
(let [token (ctob/make-token params)]
|
||||
(ptk/reify ::create-token
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(if-let [token-set (lookup-token-set state)]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
token-type (:type token)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token (ctob/get-id token-set)
|
||||
(:id token)
|
||||
token))]
|
||||
([token] (create-token nil token))
|
||||
([set-id token]
|
||||
(ptk/reify ::create-token
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(if-let [token-set (if set-id
|
||||
(lookup-token-set state set-id)
|
||||
(lookup-token-set state))]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
token-type (:type token)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token (ctob/get-id token-set)
|
||||
(:id token)
|
||||
token))]
|
||||
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(ptk/data-event ::ev/event {::ev/name "create-token" :type token-type})))
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(ptk/data-event ::ev/event {::ev/name "create-token" :type token-type})))
|
||||
|
||||
(rx/of (create-token-with-set token)))))))
|
||||
(rx/of (create-token-with-set token)))))))
|
||||
|
||||
(defn update-token
|
||||
[id params]
|
||||
(assert (uuid? id) "expected uuid for `id`")
|
||||
([id params] (update-token nil id params))
|
||||
([set-id id params]
|
||||
(assert (uuid? id) "expected uuid for `id`")
|
||||
|
||||
(ptk/reify ::update-token
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [token-set (lookup-token-set state)
|
||||
data (dsh/lookup-file-data state)
|
||||
token (-> (get-tokens-lib state)
|
||||
(ctob/get-token (ctob/get-id token-set) id))
|
||||
token' (->> (merge token params)
|
||||
(into {})
|
||||
(ctob/make-token))
|
||||
token-type (:type token)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token (ctob/get-id token-set)
|
||||
id
|
||||
token'))]
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(ptk/data-event ::ev/event {::ev/name "edit-token" :type token-type}))))))
|
||||
(ptk/reify ::update-token
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [token-set (if set-id
|
||||
(lookup-token-set state set-id)
|
||||
(lookup-token-set state))
|
||||
data (dsh/lookup-file-data state)
|
||||
token (-> (get-tokens-lib state)
|
||||
(ctob/get-token (ctob/get-id token-set) id))
|
||||
token' (->> (merge token params)
|
||||
(into {})
|
||||
(ctob/make-token))
|
||||
token-type (:type token)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token (ctob/get-id token-set)
|
||||
id
|
||||
token'))]
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(ptk/data-event ::ev/event {::ev/name "edit-token" :type token-type})))))))
|
||||
|
||||
(defn delete-token
|
||||
[set-id token-id]
|
||||
@@ -413,10 +450,11 @@
|
||||
(let [tokens (vals (ctob/get-tokens tokens-lib (ctob/get-id token-set)))
|
||||
unames (map :name tokens)
|
||||
suffix (tr "workspace.tokens.duplicate-suffix")
|
||||
copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix)]
|
||||
(rx/of (create-token (assoc token
|
||||
:id (uuid/next)
|
||||
:name copy-name))))))))))
|
||||
copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix)
|
||||
new-token (-> token
|
||||
(ctob/reid (uuid/next))
|
||||
(ctob/rename copy-name))]
|
||||
(rx/of (create-token new-token)))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKEN UI OPS
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.main.data.workspace :as-alias dw]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.main.worker]
|
||||
[app.util.globals :as glob]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.timers :as ts]
|
||||
@@ -94,6 +95,9 @@
|
||||
(let [data (exception->error-data error)]
|
||||
(ptk/handle-error data))))
|
||||
|
||||
;; Inject dependency to remove circular dependency
|
||||
(set! app.main.worker/on-error on-error)
|
||||
|
||||
;; Set the main potok error handler
|
||||
(reset! st/on-error on-error)
|
||||
|
||||
|
||||
@@ -154,6 +154,9 @@
|
||||
"All tokens related ephimeral state"
|
||||
(l/derived :workspace-tokens st/state))
|
||||
|
||||
(def workspace-selrect
|
||||
(l/derived :workspace-selrect st/state))
|
||||
|
||||
;; WARNING: Don't use directly from components, this is a proxy to
|
||||
;; improve performance of selected-shapes and
|
||||
(def ^:private selected-shapes-data
|
||||
|
||||
@@ -171,9 +171,8 @@
|
||||
(send! id params nil))
|
||||
|
||||
(defmethod cmd! :login-with-oidc
|
||||
[_ {:keys [provider] :as params}]
|
||||
(let [uri (u/join cf/public-uri "api/auth/oauth/" (d/name provider))
|
||||
params (dissoc params :provider)]
|
||||
[_ params]
|
||||
(let [uri (u/join cf/public-uri "api/auth/oidc")]
|
||||
(->> (http/send! {:method :post
|
||||
:uri uri
|
||||
:credentials "include"
|
||||
|
||||
@@ -23,12 +23,11 @@
|
||||
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[app.util.storage :as s]
|
||||
[beicon.v2.core :as rx]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def show-alt-login-buttons?
|
||||
(def ^:const show-sso-login-buttons?
|
||||
(some (partial contains? cf/flags)
|
||||
[:login-with-google
|
||||
:login-with-github
|
||||
@@ -47,53 +46,36 @@
|
||||
(st/emit! (da/create-demo-profile)))
|
||||
|
||||
(defn- store-login-redirect
|
||||
[save-login-redirect]
|
||||
[]
|
||||
(binding [s/*sync* true]
|
||||
(if (some? save-login-redirect)
|
||||
;; Save the current login raw uri for later redirect user back to
|
||||
;; the same page, we need it to be synchronous because the user is
|
||||
;; going to be redirected instantly to the oidc provider uri
|
||||
(swap! s/session assoc :login-redirect (rt/get-current-href))
|
||||
;; Clean the login redirect
|
||||
(swap! s/session dissoc :login-redirect))))
|
||||
;; Save the current login raw uri for later redirect user back to
|
||||
;; the same page, we need it to be synchronous because the user is
|
||||
;; going to be redirected instantly to the oidc provider uri
|
||||
(swap! s/session assoc :login-redirect (rt/get-current-href))))
|
||||
|
||||
(defn- login-with-oidc
|
||||
[event provider params]
|
||||
(dom/prevent-default event)
|
||||
(defn- clear-login-redirect
|
||||
[]
|
||||
(binding [s/*sync* true]
|
||||
(swap! s/session dissoc :login-redirect)))
|
||||
|
||||
(store-login-redirect (:save-login-redirect params))
|
||||
|
||||
;; FIXME: this code should be probably moved outside of the UI
|
||||
(->> (rp/cmd! :login-with-oidc (assoc params :provider provider))
|
||||
(rx/subs! (fn [{:keys [redirect-uri] :as rsp}]
|
||||
(if redirect-uri
|
||||
(st/emit! (rt/nav-raw :uri redirect-uri))
|
||||
(log/error :hint "unexpected response from OIDC method"
|
||||
:resp (pr-str rsp))))
|
||||
(fn [cause]
|
||||
(let [{:keys [type code] :as error} (ex-data cause)]
|
||||
(cond
|
||||
(and (= type :restriction)
|
||||
(= code :provider-not-configured))
|
||||
(st/emit! (ntf/error (tr "errors.auth-provider-not-configured")))
|
||||
|
||||
:else
|
||||
(st/emit! (ntf/error (tr "errors.generic")))))))))
|
||||
(defn- login-with-sso
|
||||
[provider params]
|
||||
(let [params (assoc params :provider provider)]
|
||||
(st/emit! (da/login-with-sso params))))
|
||||
|
||||
(def ^:private schema:login-form
|
||||
[:map {:title "LoginForm"}
|
||||
[:email [::sm/email {:error/code "errors.invalid-email"}]]
|
||||
[:password [:string {:min 1}]]
|
||||
[:password {:optional true} [:string {:min 1}]]
|
||||
[:invitation-token {:optional true}
|
||||
[:string {:min 1}]]])
|
||||
|
||||
(mf/defc login-form
|
||||
[{:keys [params on-success-callback on-recovery-request origin] :as props}]
|
||||
(mf/defc login-form*
|
||||
[{:keys [params handle-redirect on-success-callback on-recovery-request origin] :as props}]
|
||||
(let [initial (mf/with-memo [params] params)
|
||||
error (mf/use-state false)
|
||||
form (fm/use-form :schema schema:login-form
|
||||
:initial initial)
|
||||
|
||||
on-error
|
||||
(fn [cause]
|
||||
(let [cause (ex-data cause)]
|
||||
@@ -121,20 +103,41 @@
|
||||
:else
|
||||
(reset! error (tr "errors.generic")))))
|
||||
|
||||
show-password-field*
|
||||
(mf/use-state #(not (contains? cf/flags :login-with-custom-sso)))
|
||||
|
||||
show-password-field?
|
||||
(deref show-password-field*)
|
||||
|
||||
on-success
|
||||
(fn [data]
|
||||
(when (fn? on-success-callback)
|
||||
(on-success-callback data)))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps show-password-field? params)
|
||||
(fn [form _event]
|
||||
(store-login-redirect (:save-login-redirect params))
|
||||
(reset! error nil)
|
||||
(let [params (with-meta (:clean-data @form)
|
||||
{:on-error on-error
|
||||
:on-success on-success})]
|
||||
(st/emit! (da/login params)))))
|
||||
|
||||
(let [data (:clean-data @form)]
|
||||
(if show-password-field?
|
||||
(let [params (-> (merge params data)
|
||||
(with-meta {:on-error on-error
|
||||
:on-success on-success}))]
|
||||
(st/emit! (da/login params)))
|
||||
|
||||
(let [params (merge params data)]
|
||||
(->> (rp/cmd! :get-sso-provider {:email (:email params)})
|
||||
(rx/map :id)
|
||||
(rx/catch (fn [cause]
|
||||
(log/error :hint "error on retrieving sso provider" :cause cause)
|
||||
(rx/of nil)))
|
||||
(rx/subs! (fn [sso-provider-id]
|
||||
(if sso-provider-id
|
||||
(let [params {:provider sso-provider-id}]
|
||||
(st/emit! (da/login-with-sso params)))
|
||||
(reset! show-password-field* true))))))))))
|
||||
|
||||
on-submit-ldap
|
||||
(mf/use-callback
|
||||
@@ -150,12 +153,15 @@
|
||||
:on-success on-success})]
|
||||
(st/emit! (da/login-with-ldap params)))))
|
||||
|
||||
default-recovery-req
|
||||
(mf/use-fn
|
||||
#(st/emit! (rt/nav :auth-recovery-request)))
|
||||
on-recovery-request
|
||||
(or on-recovery-request
|
||||
#(st/emit! (rt/nav :auth-recovery-request)))]
|
||||
|
||||
on-recovery-request (or on-recovery-request
|
||||
default-recovery-req)]
|
||||
|
||||
(mf/with-effect [handle-redirect]
|
||||
(if handle-redirect
|
||||
(store-login-redirect)
|
||||
(clear-login-redirect)))
|
||||
|
||||
[:*
|
||||
(when-let [message @error]
|
||||
@@ -165,6 +171,7 @@
|
||||
[:& fm/form {:on-submit on-submit
|
||||
:class (stl/css :login-form)
|
||||
:form form}
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input
|
||||
{:name :email
|
||||
@@ -172,12 +179,14 @@
|
||||
:label (tr "auth.work-email")
|
||||
:class (stl/css :form-field)}]]
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input
|
||||
{:type "password"
|
||||
:name :password
|
||||
:label (tr "auth.password")
|
||||
:class (stl/css :form-field)}]]
|
||||
(when show-password-field?
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
[:& fm/input
|
||||
{:type "password"
|
||||
:name :password
|
||||
:auto-focus? true
|
||||
:label (tr "auth.password")
|
||||
:class (stl/css :form-field)}]])
|
||||
|
||||
(when (and (not= origin :viewer)
|
||||
(or (contains? cf/flags :login)
|
||||
@@ -192,7 +201,7 @@
|
||||
(when (or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password))
|
||||
[:> fm/submit-button*
|
||||
{:label (tr "auth.login-submit")
|
||||
{:label (tr "labels.continue")
|
||||
:data-testid "login-submit"
|
||||
:class (stl/css :login-button)}])
|
||||
|
||||
@@ -202,12 +211,12 @@
|
||||
:class (stl/css :login-ldap-button)
|
||||
:on-click on-submit-ldap}])]]]))
|
||||
|
||||
(mf/defc login-buttons
|
||||
(mf/defc login-sso-buttons*
|
||||
[{:keys [params] :as props}]
|
||||
(let [login-with-google (mf/use-fn (mf/deps params) #(login-with-oidc % :google params))
|
||||
login-with-github (mf/use-fn (mf/deps params) #(login-with-oidc % :github params))
|
||||
login-with-gitlab (mf/use-fn (mf/deps params) #(login-with-oidc % :gitlab params))
|
||||
login-with-oidc (mf/use-fn (mf/deps params) #(login-with-oidc % :oidc params))]
|
||||
(let [login-with-google (mf/use-fn (mf/deps params) #(login-with-sso "google" params))
|
||||
login-with-github (mf/use-fn (mf/deps params) #(login-with-sso "github" params))
|
||||
login-with-gitlab (mf/use-fn (mf/deps params) #(login-with-sso "gitlab" params))
|
||||
login-with-oidc (mf/use-fn (mf/deps params) #(login-with-sso "oidc" params))]
|
||||
|
||||
[:div {:class (stl/css :auth-buttons)}
|
||||
(when (contains? cf/flags :login-with-google)
|
||||
@@ -234,32 +243,12 @@
|
||||
:label (tr "auth.login-with-oidc-submit")
|
||||
:class (stl/css :login-btn :btn-oidc-auth)}])]))
|
||||
|
||||
(mf/defc login-button-oidc
|
||||
(mf/defc login-dialog*
|
||||
[{:keys [params] :as props}]
|
||||
(let [login-oidc
|
||||
(mf/use-fn
|
||||
(mf/deps params)
|
||||
(fn [event]
|
||||
(login-with-oidc event :oidc params)))
|
||||
|
||||
handle-key-down
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(when (k/enter? event)
|
||||
(login-oidc event))))]
|
||||
(when (contains? cf/flags :login-with-oidc)
|
||||
[:button {:tab-index "0"
|
||||
:class (stl/css :link-entry :link-oidc)
|
||||
:on-key-down handle-key-down
|
||||
:on-click login-oidc}
|
||||
(tr "auth.login-with-oidc-submit")])))
|
||||
|
||||
(mf/defc login-methods
|
||||
[{:keys [params on-success-callback on-recovery-request origin] :as props}]
|
||||
[:*
|
||||
(when show-alt-login-buttons?
|
||||
(when show-sso-login-buttons?
|
||||
[:*
|
||||
[:& login-buttons {:params params}]
|
||||
[:> login-sso-buttons* {:params params}]
|
||||
|
||||
(when (or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password)
|
||||
@@ -269,7 +258,7 @@
|
||||
(when (or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password)
|
||||
(contains? cf/flags :login-with-ldap))
|
||||
[:& login-form {:params params :on-success-callback on-success-callback :on-recovery-request on-recovery-request :origin origin}])])
|
||||
[:> login-form* props])])
|
||||
|
||||
(mf/defc login-page
|
||||
[{:keys [params] :as props}]
|
||||
@@ -287,7 +276,7 @@
|
||||
(when (contains? cf/flags :demo-warning)
|
||||
[:& demo-warning])
|
||||
|
||||
[:& login-methods {:params params}]
|
||||
[:> login-dialog* {:params params}]
|
||||
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
|
||||
|
||||
@@ -191,9 +191,9 @@
|
||||
{::mf/props :obj}
|
||||
[{:keys [params hide-separator on-success-callback]}]
|
||||
[:*
|
||||
(when login/show-alt-login-buttons?
|
||||
[:& login/login-buttons {:params params}])
|
||||
(when (or login/show-alt-login-buttons? (false? hide-separator))
|
||||
(when login/show-sso-login-buttons?
|
||||
[:> login/login-sso-buttons* {:params params}])
|
||||
(when (or login/show-sso-login-buttons? (false? hide-separator))
|
||||
[:hr {:class (stl/css :separator)}])
|
||||
(when (contains? cf/flags :login-with-password)
|
||||
[:& register-form {:params params :on-success-callback on-success-callback}])])
|
||||
|
||||
@@ -426,7 +426,7 @@
|
||||
(dom/prevent-default event)
|
||||
(when (fn? on-submit)
|
||||
(on-submit form event))))]
|
||||
[:& (mf/provider form-ctx) {:value form}
|
||||
[:> (mf/provider form-ctx) {:value form}
|
||||
[:form {:class class :on-submit on-submit'} children]]))
|
||||
|
||||
(defn- conj-dedup
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu-item*]]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.product.cta :refer [cta*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
@@ -48,7 +48,10 @@
|
||||
[:div {:class (stl/css :content)}
|
||||
[:span {:class (stl/css :cta-title)} top-title]
|
||||
[:span {:class (stl/css :cta-text) :data-testid "subscription-name"} top-description]]
|
||||
(when has-dropdown [:span {:class (stl/css :icon-dropdown)} deprecated-icon/arrow])]
|
||||
(when has-dropdown
|
||||
[:> icon* {:icon-id (if (and has-dropdown show-data) i/arrow-up i/arrow-down)
|
||||
:class (stl/css :icon-dropdown)
|
||||
:size "s"}])]
|
||||
|
||||
(when (and has-dropdown show-data)
|
||||
[:div {:class (stl/css :cta-bottom-section)}
|
||||
@@ -154,8 +157,8 @@
|
||||
(tr "subscription.dashboard.power-up.enterprise-plan"))
|
||||
:data-testid "subscription-icon"}
|
||||
(case subscription-type
|
||||
"unlimited" deprecated-icon/character-u
|
||||
"enterprise" deprecated-icon/character-e)])
|
||||
"unlimited" i/character-u
|
||||
"enterprise" i/character-e)])
|
||||
|
||||
(mf/defc main-menu-power-up*
|
||||
[{:keys [close-sub-menu]}]
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
|
||||
.highlighted .cta-title {
|
||||
@include t.use-typography("body-medium");
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.cta-text {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
(def ^:private schema:button
|
||||
[:map
|
||||
[:class {:optional true} :string]
|
||||
[:type {:optional true} [:maybe [:enum "button" "submit" "reset"]]]
|
||||
[:icon {:optional true}
|
||||
[:and :string [:fn #(contains? icon-list %)]]]
|
||||
[:on-ref {:optional true} fn?]
|
||||
@@ -24,7 +25,7 @@
|
||||
|
||||
(mf/defc button*
|
||||
{::mf/schema schema:button}
|
||||
[{:keys [variant icon children class on-ref to] :rest props}]
|
||||
[{:keys [variant icon children class on-ref to type] :rest props}]
|
||||
(let [variant (d/nilv variant "primary")
|
||||
element (if to "a" "button")
|
||||
internal-class (stl/css-case :button true
|
||||
@@ -35,6 +36,7 @@
|
||||
:button-destructive (= variant "destructive"))
|
||||
props (mf/spread-props props {:class [class internal-class]
|
||||
:href to
|
||||
:type (d/nilv type "button")
|
||||
:ref (fn [node]
|
||||
(when on-ref
|
||||
(on-ref node)))})]
|
||||
|
||||
@@ -28,6 +28,7 @@ export default {
|
||||
children: "Lorem ipsum",
|
||||
disabled: false,
|
||||
variant: undefined,
|
||||
type: "button",
|
||||
},
|
||||
parameters: {
|
||||
controls: { exclude: ["children"] },
|
||||
|
||||
@@ -48,10 +48,12 @@
|
||||
|
||||
(when cancel-label
|
||||
[:> button* {:variant "secondary"
|
||||
:type "button"
|
||||
:on-click on-cancel}
|
||||
cancel-label])
|
||||
|
||||
(when accept-label
|
||||
[:> button* {:variant (if (= variant "default") "primary" "destructive")
|
||||
:type "button"
|
||||
:on-click on-accept}
|
||||
accept-label])]))
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
border: $b-1 solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
&:focus {
|
||||
&:focus-visible {
|
||||
--border-color: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
82
frontend/src/app/main/ui/forms.cljs
Normal file
@@ -0,0 +1,82 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.forms
|
||||
(:require
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.keyboard :as k]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def context (mf/create-context nil))
|
||||
|
||||
(mf/defc form-input*
|
||||
[{:keys [name] :rest props}]
|
||||
|
||||
(let [form (mf/use-ctx context)
|
||||
input-name name
|
||||
|
||||
touched? (and (contains? (:data @form) input-name)
|
||||
(get-in @form [:touched input-name]))
|
||||
error (get-in @form [:errors input-name])
|
||||
|
||||
value (get-in @form [:data input-name] "")
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(mf/deps input-name)
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-input-value)]
|
||||
(fm/on-input-change form input-name value true))))
|
||||
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-change on-change
|
||||
:default-value value})
|
||||
|
||||
props
|
||||
(if (and error touched?)
|
||||
(mf/spread-props props {:hint-type "error"
|
||||
:hint-message (:message error)})
|
||||
props)]
|
||||
|
||||
[:> input* props]))
|
||||
|
||||
(mf/defc form-submit*
|
||||
[{:keys [disabled on-submit] :rest props}]
|
||||
|
||||
(let [form (mf/use-ctx context)
|
||||
disabled? (or (and (some? form)
|
||||
(or (not (:valid @form))
|
||||
(seq (:external-errors @form))))
|
||||
(true? disabled))
|
||||
handle-key-down-save
|
||||
(mf/use-fn
|
||||
(mf/deps on-submit form)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(dom/prevent-default e)
|
||||
(on-submit form e))))
|
||||
|
||||
props
|
||||
(mf/spread-props props {:disabled disabled?
|
||||
:on-key-down handle-key-down-save
|
||||
:type "submit"})]
|
||||
|
||||
[:> button* props]))
|
||||
|
||||
(mf/defc form*
|
||||
[{:keys [on-submit form children class]}]
|
||||
(let [on-submit' (mf/use-fn
|
||||
(mf/deps on-submit)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(when (fn? on-submit)
|
||||
(on-submit form event))))]
|
||||
[:> (mf/provider context) {:value form}
|
||||
[:form {:class class :on-submit on-submit'} children]]))
|
||||
@@ -21,4 +21,5 @@
|
||||
|
||||
.workspace-element-options {
|
||||
height: calc(100vh - px2rem(200)); // TODO: Fix this hardcoded value
|
||||
padding-inline: var(--sp-m);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
[:img {:class (stl/css :resolved-image) :src (cf/resolve-file-media image)}]]
|
||||
|
||||
[:> button* {:class (stl/css :download-button)
|
||||
:type "button"
|
||||
:variant "secondary"
|
||||
:target "_blank"
|
||||
:download name
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-gutter: stable;
|
||||
padding-inline: var(--sp-m);
|
||||
}
|
||||
|
||||
.viewer-code-block {
|
||||
|
||||
@@ -122,7 +122,8 @@
|
||||
(fn []
|
||||
(if (seq shapes)
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "inspect-mode-click-element"}))
|
||||
(handle-change-tab (if (contains? cf/flags :inspect-styles) :styles :info)))))
|
||||
(handle-change-tab (if (contains? cf/flags :inspect-styles) :styles :info)))
|
||||
(reset! color-space* "hex")))
|
||||
|
||||
[:aside {:class (stl/css-case :settings-bar-right true
|
||||
:viewer-code (= from :viewer))}
|
||||
@@ -165,12 +166,15 @@
|
||||
[:span {:class (stl/css :inspect-tab-switcher-label)} (tr "inspect.tabs.switcher.label")]
|
||||
[:div {:class (stl/css :inspect-tab-switcher-controls)}
|
||||
[:div {:class (stl/css :inspect-tab-switcher-controls-color-space)}
|
||||
[:> select* {:options color-spaces
|
||||
[:> select* {:class (stl/css :inspect-tab-switcher-controls-color-space-select)
|
||||
:aria-label (tr "inspect.tabs.switcher.color-space.label")
|
||||
:options color-spaces
|
||||
:default-selected "hex"
|
||||
:variant "ghost"
|
||||
:on-change handle-change-color-space}]]
|
||||
[:div {:class (stl/css :inspect-tab-switcher-controls-tab)}
|
||||
[:> select* {:options tabs
|
||||
:aria-label (tr "inspect.tabs.switcher.inspect-tab.label")
|
||||
:default-selected (name @section)
|
||||
:on-change handle-change-tab}]]]]
|
||||
nil)
|
||||
|
||||
@@ -7,65 +7,69 @@
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.settings-bar-right {
|
||||
min-width: deprecated.$s-252;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
min-inline-size: px2rem(252);
|
||||
inline-size: 100%;
|
||||
block-size: 100vh;
|
||||
position: relative;
|
||||
left: unset;
|
||||
right: unset;
|
||||
inset-inline-start: unset;
|
||||
inset-inline-end: unset;
|
||||
grid-area: right-sidebar;
|
||||
overflow: hidden;
|
||||
|
||||
&.viewer-code {
|
||||
height: calc(100vh - deprecated.$s-48);
|
||||
block-size: calc(100vh - $sz-48);
|
||||
}
|
||||
}
|
||||
|
||||
.viewer-code {
|
||||
padding-inline-start: deprecated.$s-8;
|
||||
padding-inline-start: var(--sp-s);
|
||||
}
|
||||
|
||||
.tool-windows {
|
||||
height: 100%;
|
||||
block-size: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-8;
|
||||
padding-left: var(--sp-m);
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.shape-info {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: deprecated.$s-8;
|
||||
gap: var(--sp-s);
|
||||
align-items: center;
|
||||
height: deprecated.$s-32;
|
||||
block-size: $sz-32;
|
||||
padding-inline-start: var(--sp-m);
|
||||
}
|
||||
|
||||
.shape-info-subtitle {
|
||||
height: fit-content;
|
||||
block-size: fit-content;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.layers-icon,
|
||||
.shape-icon {
|
||||
@include deprecated.flexCenter;
|
||||
height: deprecated.$s-32;
|
||||
--icon-stroke-color: var(--color-foreground-primary);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
block-size: $sz-32;
|
||||
}
|
||||
|
||||
.layer-title {
|
||||
@include deprecated.bodySmallTypography;
|
||||
@include deprecated.textEllipsis;
|
||||
height: deprecated.$s-32;
|
||||
padding: deprecated.$s-8 0;
|
||||
block-size: $sz-32;
|
||||
padding: var(--sp-s) 0;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.layer-title-with-subtitle {
|
||||
height: fit-content;
|
||||
padding-bottom: 0;
|
||||
block-size: fit-content;
|
||||
padding-block-end: 0;
|
||||
}
|
||||
|
||||
.layer-subtitle {
|
||||
@@ -78,8 +82,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: deprecated.$s-40;
|
||||
padding-top: deprecated.$s-24;
|
||||
gap: $sz-40;
|
||||
padding-top: $sz-24;
|
||||
}
|
||||
|
||||
.code-info,
|
||||
@@ -87,8 +91,8 @@
|
||||
@include deprecated.flexColumn;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: deprecated.$s-12;
|
||||
margin-right: deprecated.$s-8;
|
||||
gap: var(--sp-m);
|
||||
margin-inline-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
@@ -98,44 +102,52 @@
|
||||
.placeholder-label {
|
||||
@include deprecated.bodySmallTypography;
|
||||
text-align: center;
|
||||
width: deprecated.$s-200;
|
||||
inline-size: px2rem(200);
|
||||
color: var(--empty-message-foreground-color);
|
||||
}
|
||||
|
||||
.more-info-btn {
|
||||
@extend .button-secondary;
|
||||
@include deprecated.uppercaseTitleTipography;
|
||||
height: deprecated.$s-32;
|
||||
padding: deprecated.$s-8 deprecated.$s-24;
|
||||
block-size: $sz-32;
|
||||
padding: var(--sp-s) var(--sp-xxl);
|
||||
}
|
||||
|
||||
.inspect-tab-switcher {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-block: var(--sp-s);
|
||||
padding-inline-end: var(--sp-m);
|
||||
padding: var(--sp-s) var(--sp-m) var(--sp-s) var(--sp-m);
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.inspect-tab-switcher-label {
|
||||
@include use-typography("body-medium");
|
||||
color: var(--color-foreground-primary);
|
||||
flex: 1;
|
||||
flex: 0;
|
||||
min-inline-size: fit-content;
|
||||
}
|
||||
|
||||
.inspect-tab-switcher-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 2;
|
||||
flex: 1;
|
||||
gap: var(--sp-s);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.inspect-tab-switcher-controls-color-space {
|
||||
flex: 1 0 $sz-24;
|
||||
// 65px is the minimum size of the switcher to avoid text ellipsis on the options
|
||||
flex: 0 0 65px;
|
||||
}
|
||||
|
||||
.inspect-tab-switcher-controls-color-space-select {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.inspect-tab-switcher-controls-tab {
|
||||
flex: 2;
|
||||
// 110px is the minimum size of the switcher to avoid text ellipsis on the options
|
||||
flex: 0 0 110px;
|
||||
}
|
||||
|
||||
.inspect-content {
|
||||
@@ -147,14 +159,6 @@
|
||||
--tabs-nav-padding-inline-start: 0;
|
||||
--tabs-nav-padding-inline-end: var(--sp-m);
|
||||
|
||||
/* same height as .element-options in workspace/sidebar/options.scss */
|
||||
/* which is one of the parents of this component */
|
||||
--max-inspect-tab-height: var(--sidebar-element-options-height);
|
||||
max-block-size: var(--max-inspect-tab-height);
|
||||
|
||||
block-size: calc(100vh - px2rem(200)); // TODO: Fix this hardcoded value
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.viewer-tab-switcher-layout {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.main.ui.inspect.styles.panels.fill
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.fills :as types.fills]
|
||||
[app.config :as cfg]
|
||||
[app.main.ui.inspect.attributes.common :as cmm]
|
||||
@@ -36,20 +35,14 @@
|
||||
(= 0 idx)))
|
||||
|
||||
(defn- generate-fill-shorthand
|
||||
[shape]
|
||||
[shape color-space]
|
||||
(reduce
|
||||
(fn [acc fill]
|
||||
(let [color-type (types.fills/fill->color fill)
|
||||
color-value (:color color-type)
|
||||
color-gradient (:gradient color-type)
|
||||
gradient-data {:type color-type
|
||||
:stops (:stops color-gradient)}
|
||||
color-image (:image color-type)
|
||||
prefix (if color-value "background-color: " "background-image: ")
|
||||
(let [color (types.fills/fill->color fill)
|
||||
prefix (if (:color color) "background-color: " "background-image: ")
|
||||
value (cond
|
||||
(:color color-type) (dm/str color-value)
|
||||
color-gradient (uc/gradient->css gradient-data)
|
||||
color-image (str "url(\"" (cfg/resolve-file-media color-image) "\")")
|
||||
(or (:color color) (:gradient color)) (uc/color->format->background color (keyword color-space))
|
||||
(:image color) (str "url('" (cfg/resolve-file-media (:image color)) "')")
|
||||
:else "")
|
||||
full-value (str prefix value ";")]
|
||||
(if (empty? acc)
|
||||
@@ -60,12 +53,12 @@
|
||||
|
||||
(mf/defc fill-panel*
|
||||
[{:keys [shapes resolved-tokens color-space on-fill-shorthand]}]
|
||||
(let [shorthand* (mf/use-state #(generate-fill-shorthand (first shapes)))
|
||||
(let [shorthand* (mf/use-state #(generate-fill-shorthand (first shapes) color-space))
|
||||
shorthand (deref shorthand*)]
|
||||
(mf/use-effect
|
||||
(mf/deps shorthand on-fill-shorthand shapes)
|
||||
(fn []
|
||||
(reset! shorthand* (generate-fill-shorthand (first shapes)))
|
||||
(reset! shorthand* (generate-fill-shorthand (first shapes) color-space))
|
||||
(on-fill-shorthand {:panel :fill
|
||||
:property shorthand})))
|
||||
[:div {:class (stl/css :fill-panel)}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
(is-first-element? idx)))
|
||||
|
||||
(defn- generate-stroke-shorthand
|
||||
[shapes]
|
||||
[shapes color-space]
|
||||
(when (= (count shapes) 1)
|
||||
(let [shape (first shapes)]
|
||||
(reduce
|
||||
@@ -62,12 +62,13 @@
|
||||
stroke-width (:stroke-width stroke)
|
||||
stroke-style (:stroke-style stroke)
|
||||
color-value (:color stroke-type)
|
||||
formatted-color-value (uc/color->format->background stroke-type (keyword color-space))
|
||||
color-gradient (:gradient stroke-type)
|
||||
gradient-data {:type (get-in stroke-type [:gradient :type])
|
||||
:stops (get-in stroke-type [:gradient :stops])}
|
||||
color-image (:image stroke-type)
|
||||
value (cond
|
||||
color-value (dm/str "border: " stroke-width "px " (d/name stroke-style) " " color-value ";")
|
||||
color-value (dm/str "border: " stroke-width "px " (d/name stroke-style) " " formatted-color-value ";")
|
||||
color-gradient (dm/str "border-image: " (uc/gradient->css gradient-data) " 100 / " stroke-width "px;")
|
||||
color-image (dm/str "border-image: url(" (cfg/resolve-file-media color-image) ") 100 / " stroke-width "px;")
|
||||
:else "")]
|
||||
@@ -79,12 +80,12 @@
|
||||
|
||||
(mf/defc stroke-panel*
|
||||
[{:keys [shapes objects resolved-tokens color-space on-stroke-shorthand]}]
|
||||
(let [shorthand* (mf/use-state #(generate-stroke-shorthand shapes))
|
||||
(let [shorthand* (mf/use-state #(generate-stroke-shorthand shapes color-space))
|
||||
shorthand (deref shorthand*)]
|
||||
(mf/use-effect
|
||||
(mf/deps shorthand on-stroke-shorthand shapes)
|
||||
(fn []
|
||||
(reset! shorthand* (generate-stroke-shorthand shapes))
|
||||
(reset! shorthand* (generate-stroke-shorthand shapes color-space))
|
||||
(on-stroke-shorthand {:panel :stroke
|
||||
:property shorthand})))
|
||||
[:div {:class (stl/css :stroke-panel)}
|
||||
|
||||
@@ -41,20 +41,19 @@
|
||||
|
||||
(defn- generate-typography-shorthand
|
||||
[shapes]
|
||||
(when (= (count shapes) 1)
|
||||
(let [shape (first shapes)
|
||||
style-text-blocks (get-style-text shape)]
|
||||
(reduce
|
||||
(fn [acc [style _]]
|
||||
(let [font-style (:font-style style)
|
||||
font-family (dm/str (:font-family style))
|
||||
font-size (:font-size style)
|
||||
font-weight (:font-weight style)
|
||||
line-height (:line-height style)
|
||||
text-transform (:text-transform style)]
|
||||
(dm/str acc "font:" font-style " " text-transform " " font-weight " " font-size "/" line-height " " \" font-family \" ";")))
|
||||
""
|
||||
style-text-blocks))))
|
||||
(let [shape (first shapes)
|
||||
style-text-blocks (get-style-text shape)]
|
||||
(reduce
|
||||
(fn [acc [style _]]
|
||||
(let [font-style (:font-style style)
|
||||
font-family (dm/str (:font-family style))
|
||||
font-size (:font-size style)
|
||||
font-weight (:font-weight style)
|
||||
line-height (:line-height style)
|
||||
text-transform (:text-transform style)]
|
||||
(dm/str acc "font:" font-style " " text-transform " " font-weight " " font-size "/" line-height " " \" font-family \" ";")))
|
||||
""
|
||||
style-text-blocks)))
|
||||
|
||||
(mf/defc typography-name-block*
|
||||
[{:keys [style]}]
|
||||
@@ -76,6 +75,123 @@
|
||||
:format color-space
|
||||
:copiable true}]))
|
||||
|
||||
(mf/defc style-text-block*
|
||||
[{:keys [shape style text resolved-tokens color-space]}]
|
||||
(let [copied* (mf/use-state false)
|
||||
copied (deref copied*)
|
||||
text (str/trim text)
|
||||
copy-text
|
||||
(mf/use-fn
|
||||
(mf/deps copied)
|
||||
(fn []
|
||||
(let [formatted-text (if (= (:text-transform style) "uppercase")
|
||||
(.toUpperCase text)
|
||||
text)]
|
||||
(reset! copied* true)
|
||||
(wapi/write-to-clipboard formatted-text)
|
||||
(tm/schedule 1000 #(reset! copied* false)))))
|
||||
composite-typography-token (get-resolved-token :typography shape resolved-tokens)]
|
||||
[:div {:class (stl/css :text-properties)}
|
||||
|
||||
(when (:fills style)
|
||||
(for [[idx fill] (map-indexed vector (:fills style))]
|
||||
[:> typography-color-row* {:key idx
|
||||
:fill fill
|
||||
:shape shape
|
||||
:resolved-tokens resolved-tokens
|
||||
:color-space color-space}]))
|
||||
|
||||
;; Typography style
|
||||
(when (and (not composite-typography-token)
|
||||
(:typography-ref-id style))
|
||||
[:> typography-name-block* {:style style}])
|
||||
|
||||
;; Composite Typography token
|
||||
(when (and (not (:typography-ref-id style))
|
||||
composite-typography-token)
|
||||
[:> properties-row* {:term "Typography"
|
||||
:detail (:name composite-typography-token)
|
||||
:token composite-typography-token
|
||||
:property (:name composite-typography-token)
|
||||
:copiable true}])
|
||||
|
||||
(when (:font-id style)
|
||||
(let [name (get (fonts/get-font-data (:font-id style)) :name)
|
||||
resolved-token (get-resolved-token :font-family shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Font Family"
|
||||
:detail name
|
||||
:token resolved-token
|
||||
:property (str "font-family: \"" name "\";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:font-style style)
|
||||
[:> properties-row* {:term "Font Style"
|
||||
:detail (:font-style style)
|
||||
:property (str "font-style: " (:font-style style) ";")
|
||||
:copiable true}])
|
||||
|
||||
(when (:font-size style)
|
||||
(let [font-size (fmt/format-pixels (:font-size style))
|
||||
resolved-token (get-resolved-token :font-size shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Font Size"
|
||||
:detail font-size
|
||||
:token resolved-token
|
||||
:property (str "font-size: " font-size ";")
|
||||
:copiable true}]))
|
||||
(when (:font-weight style)
|
||||
(let [resolved-token (get-resolved-token :font-weight shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Font Weight"
|
||||
:detail (:font-weight style)
|
||||
:token resolved-token
|
||||
:property (str "font-weight: " (:font-weight style) ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:line-height style)
|
||||
(let [line-height (:line-height style)
|
||||
resolved-token (get-resolved-token :line-height shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Line Height"
|
||||
:detail (str line-height)
|
||||
:token resolved-token
|
||||
:property (str "line-height: " line-height ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:letter-spacing style)
|
||||
(let [letter-spacing (fmt/format-pixels (:letter-spacing style))
|
||||
resolved-token (get-resolved-token :letter-spacing shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Letter Spacing"
|
||||
:detail letter-spacing
|
||||
:token resolved-token
|
||||
:property (str "letter-spacing: " letter-spacing ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:text-decoration style)
|
||||
(let [resolved-token (get-resolved-token :text-decoration shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Text Decoration"
|
||||
:detail (:text-decoration style)
|
||||
:token resolved-token
|
||||
:property (str "text-decoration: " (:text-decoration style) ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:text-transform style)
|
||||
(let [resolved-token (get-resolved-token :text-case shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Text Transform"
|
||||
:detail (:text-transform style)
|
||||
:token resolved-token
|
||||
:property (str "text-transform: " (:text-transform style) ";")
|
||||
:copiable true}]))
|
||||
|
||||
[:pre {:class (stl/css :text-content-wrapper)
|
||||
:role "presentation"}
|
||||
[:> property-detail-copiable* {:copied copied
|
||||
:on-click copy-text}
|
||||
[:span {:class (stl/css :text-content)
|
||||
:style {:font-family (:font-family style)
|
||||
:font-weight (:font-weight style)
|
||||
:text-transform (:text-transform style)
|
||||
:letter-spacing (fmt/format-pixels (:letter-spacing style))
|
||||
:font-style (:font-style style)}}
|
||||
text]]]]))
|
||||
|
||||
(mf/defc text-panel*
|
||||
[{:keys [shapes resolved-tokens color-space on-font-shorthand]}]
|
||||
(let [shorthand* (mf/use-state #(generate-typography-shorthand shapes))
|
||||
@@ -88,123 +204,13 @@
|
||||
:property shorthand})))
|
||||
[:div {:class (stl/css :text-panel)}
|
||||
(for [shape shapes]
|
||||
(let [style-text-blocks (get-style-text shape)
|
||||
composite-typography-token (get-resolved-token :typography shape resolved-tokens)]
|
||||
(let [style-text-blocks (get-style-text shape)]
|
||||
[:div {:key (:id shape) :class (stl/css :text-shape)}
|
||||
(for [[style text] style-text-blocks]
|
||||
[:> style-text-block* {:key (:id shape)
|
||||
:shape shape
|
||||
:style style
|
||||
:text text
|
||||
:resolved-tokens resolved-tokens
|
||||
:color-space color-space}])]))]))
|
||||
|
||||
[:div {:key (:id shape) :class (stl/css :text-properties)}
|
||||
|
||||
(when (:fills style)
|
||||
(for [[idx fill] (map-indexed vector (:fills style))]
|
||||
[:> typography-color-row* {:key idx
|
||||
:fill fill
|
||||
:shape shape
|
||||
:resolved-tokens resolved-tokens
|
||||
:color-space color-space}]))
|
||||
|
||||
;; Typography style
|
||||
(when (and (not composite-typography-token)
|
||||
(:typography-ref-id style))
|
||||
[:> typography-name-block* {:style style}])
|
||||
|
||||
;; Composite Typography token
|
||||
(when (and (not (:typography-ref-id style))
|
||||
composite-typography-token)
|
||||
[:> properties-row* {:term "Typography"
|
||||
:detail (:name composite-typography-token)
|
||||
:token composite-typography-token
|
||||
:property (:name composite-typography-token)
|
||||
:copiable true}])
|
||||
|
||||
(when (:font-id style)
|
||||
(let [name (get (fonts/get-font-data (:font-id style)) :name)
|
||||
resolved-token (get-resolved-token :font-family shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Font Family"
|
||||
:detail name
|
||||
:token resolved-token
|
||||
:property (str "font-family: \"" name "\";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:font-style style)
|
||||
[:> properties-row* {:term "Font Style"
|
||||
:detail (:font-style style)
|
||||
:property (str "font-style: " (:font-style style) ";")
|
||||
:copiable true}])
|
||||
|
||||
(when (:font-size style)
|
||||
(let [font-size (fmt/format-pixels (:font-size style))
|
||||
resolved-token (get-resolved-token :font-size shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Font Size"
|
||||
:detail font-size
|
||||
:token resolved-token
|
||||
:property (str "font-size: " font-size ";")
|
||||
:copiable true}]))
|
||||
(when (:font-weight style)
|
||||
(let [resolved-token (get-resolved-token :font-weight shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Font Weight"
|
||||
:detail (:font-weight style)
|
||||
:token resolved-token
|
||||
:property (str "font-weight: " (:font-weight style) ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:line-height style)
|
||||
(let [line-height (:line-height style)
|
||||
resolved-token (get-resolved-token :line-height shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Line Height"
|
||||
:detail (str line-height)
|
||||
:token resolved-token
|
||||
:property (str "line-height: " line-height ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:letter-spacing style)
|
||||
(let [letter-spacing (fmt/format-pixels (:letter-spacing style))
|
||||
resolved-token (get-resolved-token :letter-spacing shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Letter Spacing"
|
||||
:detail letter-spacing
|
||||
:token resolved-token
|
||||
:property (str "letter-spacing: " letter-spacing ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:text-decoration style)
|
||||
(let [resolved-token (get-resolved-token :text-decoration shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Text Decoration"
|
||||
:detail (:text-decoration style)
|
||||
:token resolved-token
|
||||
:property (str "text-decoration: " (:text-decoration style) ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:text-transform style)
|
||||
(let [resolved-token (get-resolved-token :text-case shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Text Transform"
|
||||
:detail (:text-transform style)
|
||||
:token resolved-token
|
||||
:property (str "text-transform: " (:text-transform style) ";")
|
||||
:copiable true}]))
|
||||
(when text
|
||||
(let [copied* (mf/use-state false)
|
||||
copied (deref copied*)
|
||||
|
||||
text (str/trim text)
|
||||
|
||||
copy-text
|
||||
(mf/use-fn
|
||||
(mf/deps copied)
|
||||
(fn []
|
||||
(let [formatted-text (if (= (:text-transform style) "uppercase")
|
||||
(.toUpperCase text)
|
||||
text)]
|
||||
(reset! copied* true)
|
||||
(wapi/write-to-clipboard formatted-text)
|
||||
(tm/schedule 1000 #(reset! copied* false)))))]
|
||||
[:pre {:class (stl/css :text-content-wrapper)
|
||||
:role "presentation"}
|
||||
[:> property-detail-copiable* {:copied copied
|
||||
:on-click copy-text}
|
||||
[:span {:class (stl/css :text-content)
|
||||
:style {:font-family (:font-family style)
|
||||
:font-weight (:font-weight style)
|
||||
:text-transform (:text-transform style)
|
||||
:letter-spacing (fmt/format-pixels (:letter-spacing style))
|
||||
:font-style (:font-style style)}}
|
||||
text]]]))])]))]))
|
||||
|
||||
@@ -53,6 +53,12 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
:where(.property-detail-text, .property-detail-text-token) {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.property-detail-text {
|
||||
color: var(--detail-color);
|
||||
}
|
||||
@@ -62,7 +68,4 @@
|
||||
--detail-color: var(--color-token-foreground);
|
||||
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
(ns app.main.ui.inspect.styles.rows.color-properties-row
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.color :as cc]
|
||||
@@ -51,15 +52,16 @@
|
||||
formatted-color-value (mf/use-memo
|
||||
(mf/deps color format color-opacity)
|
||||
#(cond
|
||||
(some? (:color color)) (case format
|
||||
"hex" (dm/str color-value " " color-opacity)
|
||||
"rgba" (let [[r g b a] (cc/hex->rgba color-value color-opacity)
|
||||
result (cc/format-rgba [r g b a])]
|
||||
result)
|
||||
"hsla" (let [[h s l a] (cc/hex->hsla color-value color-opacity)
|
||||
result (cc/format-hsla [h s l a])]
|
||||
result)
|
||||
color-value)
|
||||
(some? (:color color))
|
||||
(case format
|
||||
"hex" (dm/str color-value " " color-opacity)
|
||||
"rgba" (let [[r g b a] (cc/hex->rgba color-value color-opacity)
|
||||
result (cc/format-rgba [r g b a])]
|
||||
result)
|
||||
"hsla" (let [[h s l a] (cc/hex->hsla color-value color-opacity)
|
||||
result (cc/format-hsla [h s l a])]
|
||||
result)
|
||||
color-value)
|
||||
(some? (:gradient color)) (uc/gradient-type->string (:type color-gradient))
|
||||
(some? (:image color)) (tr "media.image")
|
||||
:else "none"))
|
||||
@@ -71,13 +73,11 @@
|
||||
(str/replace #"^-" ""))
|
||||
|
||||
copiable-value (mf/use-memo
|
||||
(mf/deps color formatted-color-value color-opacity color-image-url token)
|
||||
(mf/deps color token format color-opacity)
|
||||
#(if (some? token)
|
||||
(:name token)
|
||||
(cond
|
||||
(:color color) (if (= format "hex")
|
||||
(dm/str css-term ": " color-value "; opacity: " color-opacity ";")
|
||||
(dm/str css-term ": " formatted-color-value ";"))
|
||||
(:color color) (dm/str css-term ": " (uc/color->format->background color (keyword format)) ";")
|
||||
(:gradient color) (dm/str css-term ": " (uc/color->background color) ";")
|
||||
(:image color) (dm/str css-term ": url(" color-image-url ") no-repeat center center / cover;")
|
||||
:else "none")))
|
||||
@@ -120,6 +120,7 @@
|
||||
:alt (tr "inspect.attributes.image.preview")}]]
|
||||
[:> button* {:variant "secondary"
|
||||
:to color-image-url
|
||||
:type "button"
|
||||
:target "_blank"
|
||||
:download color-image-name}
|
||||
(tr "inspect.attributes.image.download")]])]))
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
:blur (tr "labels.blur")
|
||||
:shadow (tr "labels.shadow")
|
||||
:layout (tr "labels.layout")
|
||||
:flex-element "Flex element"
|
||||
:grid-element "Grid element"
|
||||
:flex-element "Flex Element"
|
||||
:grid-element "Grid Element"
|
||||
:layout-element "Layout Element"
|
||||
:visibility (tr "labels.visibility")
|
||||
:svg (tr "labels.svg")
|
||||
@@ -64,6 +64,7 @@
|
||||
[:span {:class (stl/css :panel-title)} title]
|
||||
(when shorthand
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:tooltip-placement "top-left"
|
||||
:aria-label (tr "inspect.tabs.styles.panel.copy-style-shorthand")
|
||||
:on-click copy-shorthand
|
||||
:icon i/clipboard}])]
|
||||
|
||||
@@ -11,8 +11,16 @@
|
||||
--title-padding: var(--sp-s);
|
||||
--title-color: var(--color-foreground-primary);
|
||||
--arrow-color: var(--color-foreground-secondary);
|
||||
--box-border-color: var(--color-background-primary);
|
||||
|
||||
// TODO: this must be a custom property in the design system
|
||||
--lowEmphasis-background: #121214;
|
||||
|
||||
padding-block: var(--sp-s);
|
||||
padding-inline: var(--sp-m);
|
||||
background-color: var(--lowEmphasis-background);
|
||||
|
||||
border-block-end: 2px solid var(--box-border-color);
|
||||
}
|
||||
|
||||
.disclosure-header {
|
||||
@@ -36,7 +44,6 @@
|
||||
|
||||
.panel-title {
|
||||
@include use-typography("headline-small");
|
||||
text-transform: capitalize;
|
||||
flex: 1;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.dashboard.subscription :refer [get-subscription-type]]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
|
||||
[app.main.ui.notifications.badge :refer [badge-notification]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr c]]
|
||||
@@ -40,7 +41,10 @@
|
||||
:plan-card-highlight recommended)}
|
||||
[:div {:class (stl/css :plan-card-header)}
|
||||
[:div {:class (stl/css :plan-card-title-container)}
|
||||
(when card-title-icon [:span {:class (stl/css :plan-title-icon)} card-title-icon])
|
||||
(when card-title-icon
|
||||
[:> icon* {:icon-id card-title-icon
|
||||
:class (stl/css :plan-title-icon)
|
||||
:size "s"}])
|
||||
[:h4 {:class (stl/css :plan-card-title)} card-title]
|
||||
(when recommended
|
||||
[:& badge-notification {:content (tr "subscription.settings.recommended")
|
||||
@@ -55,8 +59,11 @@
|
||||
[:ul {:class (stl/css :benefits-list)}
|
||||
(for [benefit benefits]
|
||||
[:li {:key (dm/str benefit) :class (stl/css :benefit)} "- " benefit])]
|
||||
(when (and cta-link-with-icon cta-text-with-icon) [:button {:class (stl/css :cta-button :more-info)
|
||||
:on-click cta-link-with-icon} cta-text-with-icon deprecated-icon/open-link])
|
||||
(when (and cta-link-with-icon cta-text-with-icon)
|
||||
[:button {:class (stl/css :cta-button :more-info)
|
||||
:on-click cta-link-with-icon} cta-text-with-icon
|
||||
[:> icon* {:icon-id "open-link"
|
||||
:size "s"}]])
|
||||
(when (and cta-link cta-text (not show-button-cta)) [:button {:class (stl/css-case :cta-button true
|
||||
:bottom-link (not (and cta-link-trial cta-text-trial)))
|
||||
:on-click cta-link} cta-text])
|
||||
@@ -135,7 +142,9 @@
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog)}
|
||||
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} deprecated-icon/close]
|
||||
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog}
|
||||
[:> icon* {:icon-id "close"
|
||||
:size "m"}]]
|
||||
[:div {:class (stl/css :modal-title :subscription-title)}
|
||||
(tr "subscription.settings.management.dialog.title" subscription-name)]
|
||||
|
||||
@@ -145,7 +154,9 @@
|
||||
(tr "subscription.settings.management.dialog.currently-editors-title" (c (count editors)))]
|
||||
[:button {:class (stl/css :cta-button :show-editors-button) :on-click handle-click}
|
||||
(tr "subscription.settings.management.dialog.editors")
|
||||
[:span {:class (stl/css :icon-dropdown)} deprecated-icon/arrow]]
|
||||
[:> icon* {:icon-id (if show-editors-list i/arrow-up i/arrow-down)
|
||||
:class (stl/css :icon-dropdown)
|
||||
:size "s"}]]
|
||||
(when show-editors-list
|
||||
[:*
|
||||
[:p {:class (stl/css :editors-text :editors-list-warning)}
|
||||
@@ -239,12 +250,12 @@
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog :subscription-success)}
|
||||
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} deprecated-icon/close]
|
||||
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog}
|
||||
[:> icon* {:icon-id "close"
|
||||
:size "m"}]]
|
||||
[:div {:class (stl/css :modal-success-content)}
|
||||
[:div {:class (stl/css :modal-start)}
|
||||
(if (= "light" (:theme profile))
|
||||
deprecated-icon/logo-subscription-light
|
||||
deprecated-icon/logo-subscription)]
|
||||
[:> raw-svg* {:id (if (= "light" (:theme profile)) "logo-subscription-light" "logo-subscription")}]]
|
||||
|
||||
[:div {:class (stl/css :modal-end)}
|
||||
[:div {:class (stl/css :modal-title)} (tr "subscription.settings.sucess.dialog.title" subscription-name)]
|
||||
@@ -377,7 +388,7 @@
|
||||
"unlimited"
|
||||
(if subscription-is-trial?
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited-trial")
|
||||
:card-title-icon deprecated-icon/character-u
|
||||
:card-title-icon i/character-u
|
||||
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits"),
|
||||
:benefits [(tr "subscription.settings.unlimited.storage-benefit")
|
||||
(tr "subscription.settings.unlimited.autosave-benefit"),
|
||||
@@ -389,7 +400,7 @@
|
||||
:editors (-> profile :props :subscription :quantity)}]
|
||||
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited")
|
||||
:card-title-icon deprecated-icon/character-u
|
||||
:card-title-icon i/character-u
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
|
||||
:benefits [(tr "subscription.settings.unlimited.storage-benefit"),
|
||||
(tr "subscription.settings.unlimited.autosave-benefit"),
|
||||
@@ -401,7 +412,7 @@
|
||||
"enterprise"
|
||||
(if subscription-is-trial?
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise-trial")
|
||||
:card-title-icon deprecated-icon/character-e
|
||||
:card-title-icon i/character-e
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits"),
|
||||
:benefits [(tr "subscription.settings.enterprise.unlimited-storage-benefit"),
|
||||
(tr "subscription.settings.enterprise.autosave"),
|
||||
@@ -411,7 +422,7 @@
|
||||
:cta-text-trial (tr "subscription.settings.add-payment-to-continue")
|
||||
:cta-link-trial go-to-payments}]
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
|
||||
:card-title-icon deprecated-icon/character-e
|
||||
:card-title-icon i/character-e
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits"),
|
||||
:benefits [(tr "subscription.settings.enterprise.unlimited-storage-benefit"),
|
||||
(tr "subscription.settings.enterprise.autosave"),
|
||||
@@ -422,12 +433,16 @@
|
||||
[:div {:class (stl/css :membership-container)}
|
||||
(when (and subscribed-since (not= subscription-type "professional"))
|
||||
[:div {:class (stl/css :membership)}
|
||||
[:span {:class (stl/css :subscription-member)} deprecated-icon/crown]
|
||||
[:> icon* {:class (stl/css :subscription-member)
|
||||
:icon-id "crown"
|
||||
:size "m"}]
|
||||
[:span {:class (stl/css :membership-date)}
|
||||
(tr "subscription.settings.support-us-since" subscribed-since)]])
|
||||
|
||||
[:div {:class (stl/css :membership)}
|
||||
[:span {:class (stl/css :penpot-member)} deprecated-icon/user]
|
||||
[:> icon* {:class (stl/css :penpot-member)
|
||||
:icon-id "user"
|
||||
:size "m"}]
|
||||
[:span {:class (stl/css :membership-date)}
|
||||
(tr "subscription.settings.member-since" member-since)]]]]
|
||||
|
||||
@@ -447,7 +462,7 @@
|
||||
|
||||
(when (not= subscription-type "unlimited")
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.unlimited")
|
||||
:card-title-icon deprecated-icon/character-u
|
||||
:card-title-icon i/character-u
|
||||
:price-value "$7"
|
||||
:price-period (tr "subscription.settings.price-editor-month")
|
||||
:benefits-title (tr "subscription.settings.benefits.all-professional-benefits")
|
||||
@@ -463,7 +478,7 @@
|
||||
|
||||
(when (not= subscription-type "enterprise")
|
||||
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
|
||||
:card-title-icon deprecated-icon/character-e
|
||||
:card-title-icon i/character-e
|
||||
:price-value "$950"
|
||||
:price-period (tr "subscription.settings.price-organization-month")
|
||||
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.auth.login :refer [login-methods]]
|
||||
[app.main.ui.auth.login :refer [login-dialog*]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]]
|
||||
[app.main.ui.auth.register :as register]
|
||||
[app.main.ui.dashboard.sidebar :refer [sidebar*]]
|
||||
@@ -75,7 +75,8 @@
|
||||
[:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")]
|
||||
[:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])
|
||||
|
||||
(mf/defc login-dialog*
|
||||
(mf/defc login-modal*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [current-section (mf/use-state :login)
|
||||
user-email (mf/use-state "")
|
||||
@@ -136,9 +137,9 @@
|
||||
[:*
|
||||
[:div {:class (stl/css :logo-title)} (tr "labels.login")]
|
||||
[:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.free")]
|
||||
[:& login-methods {:on-recovery-request set-section-recovery
|
||||
[:> login-dialog* {:on-recovery-request set-section-recovery
|
||||
:on-success-callback success-login
|
||||
:params {:save-login-redirect true}}]
|
||||
:handle-redirect true}]
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :change-section)}
|
||||
(tr "auth.register")
|
||||
@@ -559,7 +560,7 @@
|
||||
:is-dashboard dashboard?
|
||||
:is-viewer view?
|
||||
:profile profile}
|
||||
[:> login-dialog* {}]]
|
||||
[:> login-modal* {}]]
|
||||
(when (get info :loaded false)
|
||||
(if request-access?
|
||||
[:> context-wrapper* {:is-workspace workspace?
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[app.common.logging :as log]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.auth.login :refer [login-methods]]
|
||||
[app.main.ui.auth.login :refer [login-dialog*]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page]]
|
||||
[app.main.ui.auth.register :refer [register-methods register-success-page terms-register register-validate-form]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
@@ -75,7 +75,9 @@
|
||||
(case current-section
|
||||
:login
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:& login-methods {:on-success-callback success-login :origin :viewer}]
|
||||
[:> login-dialog*
|
||||
{:on-success-callback success-login
|
||||
:origin :viewer}]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :recovery-request)}
|
||||
[:a {:on-click set-section
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
|
||||
[:> button* {:class (stl/css :open-button)
|
||||
:variant "secondary"
|
||||
:type "button"
|
||||
:on-click handle-open-click
|
||||
:title (when-not can-open? (tr "workspace.plugins.error.need-editor"))
|
||||
:disabled (not can-open?)} (tr "workspace.plugins.button-open")]
|
||||
|
||||
@@ -7,19 +7,18 @@
|
||||
(ns app.main.ui.workspace.shapes.text.text-edition-outline
|
||||
(:require
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc text-edition-outline
|
||||
[{:keys [shape zoom modifiers]}]
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [transform (gsh/transform-str shape)
|
||||
{:keys [id x y grow-type]} shape
|
||||
{:keys [width height]} (if (= :fixed grow-type) shape (wasm.api/get-text-dimensions id))]
|
||||
(let [selrect-transform (mf/deref refs/workspace-selrect)
|
||||
[{:keys [x y width height]} transform] (dsh/get-selrect selrect-transform shape)]
|
||||
[:rect.main.viewport-selrect
|
||||
{:x x
|
||||
:y y
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.text :as gst]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.color :as color]
|
||||
[app.common.types.text :as txt]
|
||||
[app.config :as cf]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.features :as features]
|
||||
@@ -226,8 +228,8 @@
|
||||
(stl/css :text-editor-container))
|
||||
:ref container-ref
|
||||
:data-testid "text-editor-container"
|
||||
:style {:width (:width shape)
|
||||
:height (:height shape)}
|
||||
:style {:width "var(--editor-container-width)"
|
||||
:height "var(--editor-container-height)"}
|
||||
;; We hide the editor when is blurred because otherwise the
|
||||
;; selection won't let us see the underlying text. Use opacity
|
||||
;; because display or visibility won't allow to recover focus
|
||||
@@ -303,12 +305,22 @@
|
||||
(some? modifiers)
|
||||
(gsh/transform-shape modifiers))
|
||||
|
||||
[x y width height]
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id)
|
||||
{:keys [x y]} (:selrect shape)]
|
||||
render-wasm? (mf/use-memo #(features/active-feature? @st/state "render-wasm/v1"))
|
||||
|
||||
[x y width height])
|
||||
[{:keys [x y width height]} transform]
|
||||
(if render-wasm?
|
||||
(let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id)
|
||||
selrect-transform (mf/deref refs/workspace-selrect)
|
||||
[selrect transform] (dsh/get-selrect selrect-transform shape)
|
||||
|
||||
valign (-> shape :content :vertical-align)
|
||||
|
||||
y (:y selrect)
|
||||
y (case valign
|
||||
"bottom" (- y (- height (:height selrect)))
|
||||
"center" (- y (/ (- height (:height selrect)) 2))
|
||||
y)]
|
||||
[(assoc selrect :y y :width width :height height) transform])
|
||||
|
||||
(let [bounds (gst/shape->rect shape)
|
||||
x (mth/min (dm/get-prop bounds :x)
|
||||
@@ -319,12 +331,24 @@
|
||||
(dm/get-prop shape :width))
|
||||
height (mth/max (dm/get-prop bounds :height)
|
||||
(dm/get-prop shape :height))]
|
||||
[x y width height]))
|
||||
[(grc/make-rect x y width height) (gsh/transform-matrix shape)]))
|
||||
|
||||
style
|
||||
(cond-> #js {:pointerEvents "all"}
|
||||
render-wasm?
|
||||
(obj/merge!
|
||||
#js {"--editor-container-width" (dm/str width "px")
|
||||
"--editor-container-height" (dm/str height "px")})
|
||||
|
||||
(not (cf/check-browser? :safari))
|
||||
(not render-wasm?)
|
||||
(obj/merge!
|
||||
#js {"--editor-container-width" (dm/str (:width shape) "px")
|
||||
"--editor-container-height" (dm/str (:height shape) "px")})
|
||||
|
||||
;; Transform is necessary when there is a text overflow and the vertical
|
||||
;; aligment is center or bottom.
|
||||
(and (not render-wasm?)
|
||||
(not (cf/check-browser? :safari)))
|
||||
(obj/merge!
|
||||
#js {:transform (dm/fmt "translate(%px, %px)" (- (dm/get-prop shape :x) x) (- (dm/get-prop shape :y) y))})
|
||||
|
||||
@@ -345,7 +369,7 @@
|
||||
(dm/fmt "scale(%)" maybe-zoom))}))]
|
||||
|
||||
[:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id)
|
||||
:transform (dm/str (gsh/transform-matrix shape))}
|
||||
:transform (dm/str transform)}
|
||||
[:defs
|
||||
[:clipPath {:id clip-id}
|
||||
[:rect {:x x :y y :width width :height height}]]]
|
||||
|
||||
@@ -1089,6 +1089,7 @@
|
||||
|
||||
(when (and multi all-main? (not any-variant?))
|
||||
[:> button* {:variant "secondary"
|
||||
:type "button"
|
||||
:class (stl/css :component-combine)
|
||||
:on-click on-combine-as-variants}
|
||||
(tr "workspace.shape.menu.combine-as-variants")])
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
.element-set-content {
|
||||
@include sidebar.option-grid-structure;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.multiple-exports {
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
|
||||
[:div {:class (stl/css :button-row)}
|
||||
[:> button* {:variant "primary"
|
||||
:type "button"
|
||||
:class (stl/css :modal-accept-btn)
|
||||
:on-click on-close}
|
||||
(tr "ds.confirm-ok")]]]]))
|
||||
|
||||
@@ -55,10 +55,7 @@
|
||||
|
||||
(defn generic-attribute-actions [attributes title {:keys [token selected-shapes on-update-shape hint allowed-shape-attributes]}]
|
||||
(let [allowed-attributes (set/intersection attributes allowed-shape-attributes)
|
||||
on-update-shape-fn
|
||||
(or on-update-shape
|
||||
(-> (dwta/get-token-properties token)
|
||||
(:on-update-shape)))
|
||||
on-update-shape-fn (or on-update-shape (dwta/get-update-shape-fn token))
|
||||
|
||||
{:keys [selected-pred shape-ids]}
|
||||
(attribute-actions token selected-shapes allowed-attributes)]
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.create.border-radius
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.constants :refer [max-input-length]]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.data.workspace.tokens.propagation :as dwtp]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.main.ui.workspace.tokens.management.create.form-input-token :refer [form-input-token*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
(sm/schema
|
||||
[:and
|
||||
[:map
|
||||
[:name
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
(sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
|
||||
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||
#(not (cft/token-name-path-exists? % tokens-tree))]]]
|
||||
|
||||
[:value ::sm/text]
|
||||
|
||||
[:resolved-value ::sm/any]
|
||||
|
||||
[:description {:optional true}
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
|
||||
|
||||
[:fn {:error/field :value
|
||||
:error/fn #(tr "workspace.tokens.self-reference")}
|
||||
(fn [{:keys [name value]}]
|
||||
(when (and name value)
|
||||
(nil? (cto/token-value-self-reference? name value))))]]))
|
||||
|
||||
(mf/defc form*
|
||||
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
|
||||
|
||||
(let [token
|
||||
(mf/with-memo [token]
|
||||
(or token {:type :border-radius}))
|
||||
|
||||
token-type
|
||||
(get token :type)
|
||||
|
||||
token-properties
|
||||
(dwta/get-token-properties token)
|
||||
|
||||
token-title (str/lower (:title token-properties))
|
||||
|
||||
tokens
|
||||
(mf/deref refs/workspace-active-theme-sets-tokens)
|
||||
|
||||
tokens
|
||||
(mf/with-memo [tokens]
|
||||
;; Ensure that the resolved value uses the currently editing token
|
||||
;; even if the name has been overriden by a token with the same name
|
||||
;; in another set below.
|
||||
(cond-> tokens
|
||||
(and (:name token) (:value token))
|
||||
(assoc (:name token) token)))
|
||||
|
||||
schema
|
||||
(mf/with-memo [tokens-tree-in-selected-set]
|
||||
(make-schema tokens-tree-in-selected-set))
|
||||
|
||||
initial
|
||||
(mf/with-memo [token]
|
||||
{:name (:name token "")
|
||||
:value (:value token "")
|
||||
:description (:description token "")})
|
||||
|
||||
form
|
||||
(fm/use-form :schema schema
|
||||
:initial initial)
|
||||
|
||||
warning-name-change?
|
||||
(not= (get-in @form [:data :name])
|
||||
(:name initial))
|
||||
|
||||
on-cancel
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)))
|
||||
|
||||
on-delete-token
|
||||
(mf/use-fn
|
||||
(mf/deps selected-token-set-id token)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)
|
||||
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
|
||||
|
||||
handle-key-down-delete
|
||||
(mf/use-fn
|
||||
(mf/deps on-delete-token)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-delete-token e))))
|
||||
|
||||
handle-key-down-cancel
|
||||
(mf/use-fn
|
||||
(mf/deps on-cancel)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-cancel e))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps validate-token token tokens token-type)
|
||||
(fn [form _event]
|
||||
(let [name (get-in @form [:clean-data :name])
|
||||
description (get-in @form [:clean-data :description])
|
||||
value (get-in @form [:clean-data :value])]
|
||||
(->> (validate-token {:token-value value
|
||||
:token-name name
|
||||
:token-description description
|
||||
:prev-token token
|
||||
:tokens tokens})
|
||||
(rx/subs!
|
||||
(fn [valid-token]
|
||||
(st/emit!
|
||||
(if is-create
|
||||
(dwtl/create-token (ctob/make-token {:name name
|
||||
:type token-type
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
|
||||
(dwtl/update-token (:id token)
|
||||
{:name name
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide))))))))]
|
||||
|
||||
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||
:form form
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
||||
(tr "workspace.tokens.create-token" token-type)]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-name"
|
||||
:name :name
|
||||
:label (tr "workspace.tokens.token-name")
|
||||
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:auto-focus true}]
|
||||
|
||||
(when (and warning-name-change? (= action "edit"))
|
||||
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
|
||||
[:> context-notification*
|
||||
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> form-input-token*
|
||||
{:placeholder (tr "workspace.tokens.token-value-enter")
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:token token
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-description"
|
||||
:name :description
|
||||
:label (tr "workspace.tokens.token-description")
|
||||
:placeholder (tr "workspace.tokens.token-description")
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:is-optional true}]]
|
||||
|
||||
[:div {:class (stl/css-case :button-row true
|
||||
:with-delete (= action "edit"))}
|
||||
(when (= action "edit")
|
||||
[:> button* {:on-click on-delete-token
|
||||
:on-key-down handle-key-down-delete
|
||||
:class (stl/css :delete-btn)
|
||||
:type "button"
|
||||
:icon i/delete
|
||||
:variant "secondary"}
|
||||
(tr "labels.delete")])
|
||||
|
||||
[:> button* {:on-click on-cancel
|
||||
:on-key-down handle-key-down-cancel
|
||||
:type "button"
|
||||
:id "token-modal-cancel"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
:on-submit on-submit}
|
||||
(tr "labels.save")]]]]))
|
||||
@@ -0,0 +1,58 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
|
||||
.form-wrapper {
|
||||
width: $sz-384;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.token-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.form-modal-title {
|
||||
@include t.use-typography("headline-medium");
|
||||
color: var(--color-foreground-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
justify-content: end;
|
||||
gap: var(--sp-m);
|
||||
padding-block-start: var(--sp-s);
|
||||
}
|
||||
|
||||
.with-delete {
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
|
||||
.warning-name-change-notification-wrapper {
|
||||
margin-block-start: var(--sp-l);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
justify-self: start;
|
||||
}
|
||||