Compare commits

...

46 Commits

Author SHA1 Message Date
Elena Torro
4baa894ee4 Support undo and redo on text 2025-11-18 13:58:28 +01:00
Xaviju
64b892f82d ♻️ Copy shorthands using user selected color space (#7752)
* ♻️ Copy shorthands using user selected color space

* ♻️ Add tests to ensure color space changes affect all properties
2025-11-18 10:54:10 +01:00
Alejandro Alonso
04185b3544 Merge pull request #7762 from penpot/alotor-fix-selection
🐛 Fix problem with selection and text shapes for new render
2025-11-18 10:39:36 +01:00
alonso.torres
0a01fc8af9 🐛 Fix problem with selection and text shapes for new render 2025-11-18 09:34:17 +01:00
Alejandro Alonso
ae624b3728 Merge pull request #7760 from penpot/elenatorro-12533-fix-selection-and-paste-and-word-deletion
🐛 Fix text editor select all functionality and inner paste corner cases
2025-11-18 09:31:57 +01:00
Alejandro Alonso
a48b719966 Merge pull request #7748 from penpot/elenatorro-12586-fix-offset-y-on-new-lines
🐛 Fix new lines spacing between paragraphs
2025-11-18 09:23:22 +01:00
Elena Torró
6425c0cb7d Merge pull request #7757 from penpot/superalex-fix-apply-shadow-and-blur-bounds
🐛 Fix apply shadow and blur bounds
2025-11-17 16:50:15 +01:00
Elena Torro
368f4cfe81 🐛 Fix text editor select all functionality and inner paste corner cases 2025-11-17 16:24:52 +01:00
Alejandro Alonso
fdffa14d75 🐛 Fix apply shadow and blur bounds 2025-11-17 15:20:22 +01:00
Eva Marco
7fe965a870 🎉 Add new form system on workspace (#7738)
* 🎉 Add new form system on border-radius token modals

* ♻️ Create new namespace and separate components

* ♻️ Refactor submit button

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-11-17 13:44:56 +01:00
Elena Torro
127fa931c7 🐛 Fix new lines spacing between paragraphs 2025-11-14 12:00:39 +01:00
Andrey Antukh
30413dbc66 Add small changes to the auth/login button label (#7754)
* 📎 Update changelog

*  Update login button label

* 📎 Adapt playwright tests
2025-11-14 11:35:10 +01:00
Andrey Antukh
2810ae681f ⬆️ Update yarn requirement on library module 2025-11-14 11:15:26 +01:00
Andrey Antukh
d706bb7c8d 🐛 Fix validation issues with dtcg-node schema 2025-11-14 11:15:26 +01:00
Andrey Antukh
ef271db879 🎉 Add addTokensLib method to the library 2025-11-14 11:15:26 +01:00
Andrey Antukh
ec5e814a72 ⬆️ Update npm deps on library 2025-11-14 11:15:26 +01:00
Andrey Antukh
c44fd2dd1d 💄 Use correct comments style on tokens-lib 2025-11-14 11:15:26 +01:00
Andrey Antukh
6aa797f51b Normalize token theme serialization to JSON 2025-11-14 11:15:26 +01:00
Andrés Moya
3cc54fd988 🎉 Add design tokens to plugins API (#7602)
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2025-11-14 11:14:56 +01:00
Xaviju
2233f34a15 🎉 Set default button behaviour as type button instead of submit (#7741) 2025-11-14 10:25:38 +01:00
Andrey Antukh
839bb470df Merge remote-tracking branch 'origin/staging' into develop 2025-11-14 09:55:14 +01:00
Eva Marco
450ce869ba 🐛 Fix gap on export section on sidebar 2025-11-14 09:08:33 +01:00
Xaviju
665587d492 ♻️ Review inspect tab UI (#7727)
* ♻️ Review inspect tab UI

* ♻️ Capitalize English strings and remove from styles

* ♻️ Set a minimum size por color space selector and adjust visually the UI

* 🐛 Fix error on hooks order when selecting texts

* 🐛 Set minim size to inspect tab element

* 🐛 Fix broken typography panel

* ♻️ Design review
2025-11-13 22:19:43 +01:00
Elena Torró
8aaa953604 Merge pull request #7730 from penpot/alotor-fixes-layouts
 Fix new render problems with layout
2025-11-13 16:38:20 +01:00
Marina López
a2cb84ba0d Add improvements payment flow 2025-11-13 13:48:27 +01:00
alonso.torres
639952abc8 🐛 Fix problems with text positioning in layout 2025-11-13 12:31:26 +01:00
alonso.torres
2d63730bfa Improved performance in modifiers 2025-11-13 12:31:26 +01:00
alonso.torres
c1638817b2 🐛 Fix problem with frame titles not moving 2025-11-13 12:31:26 +01:00
alonso.torres
76f6f71e02 🐛 Fix z-ordering for flex elements 2025-11-13 12:31:26 +01:00
alonso.torres
0a700864c9 🐛 Fix problem with grid layout modifiers 2025-11-13 12:31:26 +01:00
Yamila Moreno
04ce4c3233 🔧 Fix repository name in release.yml (#7731) 2025-11-13 11:42:33 +01:00
Andrey Antukh
befcca86df 📚 Update changelog 2025-11-12 21:37:16 +01:00
Andrey Antukh
b7bae3850b 🐛 Fix webp exportation on exporter docker image (#7739) 2025-11-12 21:31:19 +01:00
Elena Torró
3f05dae455 Merge pull request #7735 from penpot/superalex-fix-create-empty-text
🐛 Fix some text issues
2025-11-12 17:48:41 +01:00
Aitor Moreno
4a887840c6 Merge pull request #7737 from penpot/sueralex-fix-shadows-clipping
🐛 Fix shadows clipping
2025-11-12 16:58:06 +01:00
Elena Torró
10cf2c7f35 Merge pull request #7729 from penpot/ladybenko-12514-fix-font-variants
🐛 Fix downloading wrong font variant
2025-11-12 15:30:08 +01:00
Belén Albeza
d048a251f1 🐛 Fix render of text baseline (wasm) 2025-11-12 14:59:57 +01:00
Belén Albeza
0b3fc6a663 🔧 Fix broken playwright tests (wasm render) 2025-11-12 14:48:31 +01:00
Andrey Antukh
363b4e3778 ♻️ Make the SSO code more modular (#7575)
* 📎 Disable by default social auth on devenv

* 🎉 Add the ability to import profile picture from SSO provider

* 📎 Add srepl helper for insert custom sso config

* 🎉 Add custom SSO auth flow
2025-11-12 12:49:10 +01:00
Andrey Antukh
f248ab5644 🐛 Relax schema for importing plain path data related to curve-to command 2025-11-12 12:13:17 +01:00
Alejandro Alonso
33da6fbec2 🐛 Fix shadows clipping 2025-11-12 11:47:53 +01:00
Belén Albeza
07bede8ba2 🐛 Fix unicode ranges for codepoints that need surrogate pairs 2025-11-12 10:11:19 +01:00
Alejandro Alonso
718f42aa94 🐛 Fix deselect and delete events for empty texts 2025-11-12 08:33:17 +01:00
Alejandro Alonso
7594f1883b 🐛 Fix create empty text 2025-11-12 08:20:58 +01:00
Belén Albeza
5c2dde7308 🐛 Fix font family not being updated when changed from dropdown 2025-11-11 15:52:18 +01:00
Belén Albeza
483a1bd703 🐛 Fix downloading wrong font variant 2025-11-11 14:44:56 +01:00
154 changed files with 5674 additions and 1669 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 \

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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]

View File

@@ -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

View File

@@ -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))))

View File

@@ -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}))))

View File

@@ -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]

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -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]

View File

@@ -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);

View File

@@ -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;

View File

@@ -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)]

View File

@@ -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]))))

View File

@@ -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)})))

View File

@@ -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))))))

View File

@@ -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

View File

@@ -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

View File

@@ -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)}))))

View File

@@ -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)))

View File

@@ -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]}]

View File

@@ -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]

View File

@@ -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))

View File

@@ -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))))))

View File

@@ -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)))))

View File

@@ -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

View File

@@ -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"}

View File

@@ -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]

View File

@@ -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})

View File

@@ -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)

View File

@@ -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]

View File

@@ -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))))

View File

@@ -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.

View File

@@ -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)

View File

@@ -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"}}]

View File

@@ -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

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -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"
}
}
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -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,
);
}

View File

@@ -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(

View File

@@ -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();
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 318 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

After

Width:  |  Height:  |  Size: 335 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -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");

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 247 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 216 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -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);

View File

@@ -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

View File

@@ -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)]))

View File

@@ -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))]

View File

@@ -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)

View File

@@ -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)))

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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"

View File

@@ -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)}]

View File

@@ -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}])])

View File

@@ -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

View File

@@ -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]}]

View File

@@ -93,6 +93,7 @@
.highlighted .cta-title {
@include t.use-typography("body-medium");
margin-block-end: 0;
}
.cta-text {

View File

@@ -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)))})]

View File

@@ -28,6 +28,7 @@ export default {
children: "Lorem ipsum",
disabled: false,
variant: undefined,
type: "button",
},
parameters: {
controls: { exclude: ["children"] },

View File

@@ -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])]))

View File

@@ -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);
}
}

View 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]]))

View File

@@ -21,4 +21,5 @@
.workspace-element-options {
height: calc(100vh - px2rem(200)); // TODO: Fix this hardcoded value
padding-inline: var(--sp-m);
}

View File

@@ -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

View File

@@ -15,6 +15,7 @@
overflow-y: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
padding-inline: var(--sp-m);
}
.viewer-code-block {

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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]]]))])]))]))

View File

@@ -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;
}

View File

@@ -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")]])]))

View File

@@ -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}])]

View File

@@ -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);
}

View File

@@ -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")

View File

@@ -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?

View File

@@ -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

View File

@@ -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")]

View File

@@ -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

View File

@@ -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}]]]

View File

@@ -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")])

View File

@@ -22,6 +22,7 @@
.element-set-content {
@include sidebar.option-grid-structure;
gap: var(--sp-xs);
}
.multiple-exports {

View File

@@ -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")]]]]))

View File

@@ -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)]

View File

@@ -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")]]]]))

View File

@@ -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;
}

Some files were not shown because too many files have changed in this diff Show More