mirror of
https://github.com/penpot/penpot.git
synced 2026-01-27 07:42:03 -05:00
Compare commits
2 Commits
niwinz-dev
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2523096fdd | ||
|
|
8e63c4e3e8 |
@@ -1,12 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
|
|
||||||
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
|
|
||||||
export PENPOT_SECRET_KEY=super-secret-devenv-key
|
|
||||||
|
|
||||||
# DEPRECATED: only used for subscriptions
|
|
||||||
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
|
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
|
||||||
|
export PENPOT_SECRET_KEY=super-secret-devenv-key
|
||||||
export PENPOT_HOST=devenv
|
export PENPOT_HOST=devenv
|
||||||
export PENPOT_PUBLIC_URI=https://localhost:3449
|
export PENPOT_PUBLIC_URI=https://localhost:3449
|
||||||
|
|
||||||
|
|||||||
@@ -102,8 +102,6 @@
|
|||||||
[:http-server-io-threads {:optional true} ::sm/int]
|
[:http-server-io-threads {:optional true} ::sm/int]
|
||||||
[:http-server-max-worker-threads {:optional true} ::sm/int]
|
[:http-server-max-worker-threads {:optional true} ::sm/int]
|
||||||
|
|
||||||
[:exporter-shared-key {:optional true} :string]
|
|
||||||
[:nitrate-shared-key {:optional true} :string]
|
|
||||||
[:management-api-key {:optional true} :string]
|
[:management-api-key {:optional true} :string]
|
||||||
|
|
||||||
[:telemetry-uri {:optional true} :string]
|
[:telemetry-uri {:optional true} :string]
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
|
[app.http.middleware :as mw]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.rpc.commands.profile :as cmd.profile]
|
[app.rpc.commands.profile :as cmd.profile]
|
||||||
[app.setup :as-alias setup]
|
[app.setup :as-alias setup]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.worker :as-alias wrk]
|
[app.worker :as-alias wrk]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[yetti.request :as yreq]
|
|
||||||
[yetti.response :as-alias yres]))
|
[yetti.response :as-alias yres]))
|
||||||
|
|
||||||
;; ---- ROUTES
|
;; ---- ROUTES
|
||||||
@@ -49,40 +49,28 @@
|
|||||||
(fn [cfg request]
|
(fn [cfg request]
|
||||||
(db/tx-run! cfg handler request)))))})
|
(db/tx-run! cfg handler request)))))})
|
||||||
|
|
||||||
(def ^:private shared-key-auth
|
|
||||||
{:name ::shared-key-auth
|
|
||||||
:compile
|
|
||||||
(fn [_ _]
|
|
||||||
(fn [handler key]
|
|
||||||
(if key
|
|
||||||
(fn [request]
|
|
||||||
(if-let [key' (yreq/get-header request "x-shared-key")]
|
|
||||||
(if (= key key')
|
|
||||||
(handler request)
|
|
||||||
{::yres/status 403})
|
|
||||||
{::yres/status 403}))
|
|
||||||
(fn [_ _]
|
|
||||||
{::yres/status 403}))))})
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::routes
|
(defmethod ig/init-key ::routes
|
||||||
[_ cfg]
|
[_ {:keys [::setup/props] :as cfg}]
|
||||||
|
|
||||||
["" {:middleware [[shared-key-auth (cf/get :management-api-key)]
|
(let [management-key (or (cf/get :management-api-key)
|
||||||
[default-system cfg]
|
(get props :management-key))]
|
||||||
[transaction]]}
|
|
||||||
["/authenticate"
|
|
||||||
{:handler authenticate
|
|
||||||
:allowed-methods #{:post}}]
|
|
||||||
|
|
||||||
["/get-customer"
|
["" {:middleware [[mw/shared-key-auth management-key]
|
||||||
{:handler get-customer
|
[default-system cfg]
|
||||||
:transaction true
|
[transaction]]}
|
||||||
:allowed-methods #{:post}}]
|
["/authenticate"
|
||||||
|
{:handler authenticate
|
||||||
|
:allowed-methods #{:post}}]
|
||||||
|
|
||||||
["/update-customer"
|
["/get-customer"
|
||||||
{:handler update-customer
|
{:handler get-customer
|
||||||
:allowed-methods #{:post}
|
:transaction true
|
||||||
:transaction true}]])
|
:allowed-methods #{:post}}]
|
||||||
|
|
||||||
|
["/update-customer"
|
||||||
|
{:handler update-customer
|
||||||
|
:allowed-methods #{:post}
|
||||||
|
:transaction true}]]))
|
||||||
|
|
||||||
;; ---- HELPERS
|
;; ---- HELPERS
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
[app.http.errors :as errors]
|
[app.http.errors :as errors]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.pointer-map :as pmap]
|
[app.util.pointer-map :as pmap]
|
||||||
|
[buddy.core.codecs :as bc]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[yetti.adapter :as yt]
|
[yetti.adapter :as yt]
|
||||||
[yetti.middleware :as ymw]
|
[yetti.middleware :as ymw]
|
||||||
@@ -300,20 +301,16 @@
|
|||||||
:compile (constantly wrap-auth)})
|
:compile (constantly wrap-auth)})
|
||||||
|
|
||||||
(defn- wrap-shared-key-auth
|
(defn- wrap-shared-key-auth
|
||||||
[handler keys]
|
[handler shared-key]
|
||||||
(if (seq keys)
|
(if shared-key
|
||||||
(fn [request]
|
(let [shared-key (if (string? shared-key)
|
||||||
(if-let [[key-id key] (some-> (yreq/get-header request "x-shared-key")
|
shared-key
|
||||||
(str/split #"\s+" 2))]
|
(bc/bytes->b64-str shared-key true))]
|
||||||
(let [key-id (str/lower key-id)]
|
(fn [request]
|
||||||
(if (and (string? key)
|
(let [key (yreq/get-header request "x-shared-key")]
|
||||||
(contains? keys key-id)
|
(if (= key shared-key)
|
||||||
(= key (get keys key-id)))
|
(handler (assoc request ::http/auth-with-shared-key true))
|
||||||
(-> request
|
{::yres/status 403}))))
|
||||||
(assoc ::http/auth-key-id key-id)
|
|
||||||
(handler))
|
|
||||||
{::yres/status 403}))
|
|
||||||
{::yres/status 403}))
|
|
||||||
(fn [_ _]
|
(fn [_ _]
|
||||||
{::yres/status 403})))
|
{::yres/status 403})))
|
||||||
|
|
||||||
|
|||||||
@@ -140,14 +140,10 @@
|
|||||||
client-version (get-client-version request)
|
client-version (get-client-version request)
|
||||||
client-user-agent (get-client-user-agent request)
|
client-user-agent (get-client-user-agent request)
|
||||||
session-id (get-external-session-id request)
|
session-id (get-external-session-id request)
|
||||||
key-id (::http/auth-key-id request)
|
token-id (::actoken/id request)]
|
||||||
token-id (::actoken/id request)
|
|
||||||
token-type (::actoken/type request)]
|
|
||||||
(d/without-nils
|
(d/without-nils
|
||||||
{:external-session-id session-id
|
{:external-session-id session-id
|
||||||
:initiator (or key-id "app")
|
|
||||||
:access-token-id (some-> token-id str)
|
:access-token-id (some-> token-id str)
|
||||||
:access-token-type (some-> token-type str)
|
|
||||||
:client-event-origin client-event-origin
|
:client-event-origin client-event-origin
|
||||||
:client-user-agent client-user-agent
|
:client-user-agent client-user-agent
|
||||||
:client-version client-version
|
:client-version client-version
|
||||||
|
|||||||
@@ -275,7 +275,8 @@
|
|||||||
::email/whitelist (ig/ref ::email/whitelist)}
|
::email/whitelist (ig/ref ::email/whitelist)}
|
||||||
|
|
||||||
::mgmt/routes
|
::mgmt/routes
|
||||||
{::db/pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)
|
||||||
|
::setup/props (ig/ref ::setup/props)}
|
||||||
|
|
||||||
:app.http/router
|
:app.http/router
|
||||||
{::session/manager (ig/ref ::session/manager)
|
{::session/manager (ig/ref ::session/manager)
|
||||||
@@ -340,8 +341,7 @@
|
|||||||
::email/whitelist (ig/ref ::email/whitelist)}
|
::email/whitelist (ig/ref ::email/whitelist)}
|
||||||
|
|
||||||
:app.nitrate/client
|
:app.nitrate/client
|
||||||
{::http.client/client (ig/ref ::http.client/client)
|
{::http.client/client (ig/ref ::http.client/client)}
|
||||||
::setup/shared-keys (ig/ref ::setup/shared-keys)}
|
|
||||||
|
|
||||||
:app.rpc/management-methods
|
:app.rpc/management-methods
|
||||||
{::http.client/client (ig/ref ::http.client/client)
|
{::http.client/client (ig/ref ::http.client/client)
|
||||||
@@ -357,13 +357,13 @@
|
|||||||
::setup/props (ig/ref ::setup/props)}
|
::setup/props (ig/ref ::setup/props)}
|
||||||
|
|
||||||
::rpc/routes
|
::rpc/routes
|
||||||
{::rpc/methods (ig/ref :app.rpc/methods)
|
{::rpc/methods (ig/ref :app.rpc/methods)
|
||||||
::rpc/management-methods (ig/ref :app.rpc/management-methods)
|
::rpc/management-methods (ig/ref :app.rpc/management-methods)
|
||||||
|
|
||||||
;; FIXME: revisit if db/pool is necessary here
|
;; FIXME: revisit if db/pool is necessary here
|
||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::session/manager (ig/ref ::session/manager)
|
::session/manager (ig/ref ::session/manager)
|
||||||
::setup/shared-keys (ig/ref ::setup/shared-keys)}
|
::setup/props (ig/ref ::setup/props)}
|
||||||
|
|
||||||
::wrk/registry
|
::wrk/registry
|
||||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||||
@@ -451,11 +451,6 @@
|
|||||||
;; module requires the migrations to run before initialize.
|
;; module requires the migrations to run before initialize.
|
||||||
::migrations (ig/ref :app.migrations/migrations)}
|
::migrations (ig/ref :app.migrations/migrations)}
|
||||||
|
|
||||||
::setup/shared-keys
|
|
||||||
{::setup/props (ig/ref ::setup/props)
|
|
||||||
:nitrate (cf/get :nitrate-shared-key)
|
|
||||||
:exporter (cf/get :exporter-shared-key)}
|
|
||||||
|
|
||||||
::setup/clock
|
::setup/clock
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
;; 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.nitrate
|
(ns app.nitrate
|
||||||
"Module that make calls to the external nitrate aplication"
|
"Module that make calls to the external nitrate aplication"
|
||||||
(:require
|
(:require
|
||||||
@@ -11,17 +17,18 @@
|
|||||||
[clojure.core :as c]
|
[clojure.core :as c]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; HELPERS
|
;; HELPERS
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn- request-builder
|
(defn- request-builder
|
||||||
[cfg method uri shared-key profile-id]
|
[cfg method uri management-key profile-id]
|
||||||
(fn []
|
(fn []
|
||||||
(http/req! cfg {:method method
|
(http/req! cfg {:method method
|
||||||
:headers {"content-type" "application/json"
|
:headers {"content-type" "application/json"
|
||||||
"accept" "application/json"
|
"accept" "application/json"
|
||||||
"x-shared-key" shared-key
|
"x-shared-key" management-key
|
||||||
"x-profile-id" (str profile-id)}
|
"x-profile-id" (str profile-id)}
|
||||||
:uri uri
|
:uri uri
|
||||||
:version :http1.1})))
|
:version :http1.1})))
|
||||||
@@ -47,9 +54,9 @@
|
|||||||
|
|
||||||
(defn- with-validate [handler uri schema]
|
(defn- with-validate [handler uri schema]
|
||||||
(fn []
|
(fn []
|
||||||
(let [coercer-http (sm/coercer schema
|
(let [coercer-http (sm/coercer schema
|
||||||
:type :validation
|
:type :validation
|
||||||
:hint (str "invalid data received calling " uri))]
|
:hint (str "invalid data received calling " uri))]
|
||||||
(try
|
(try
|
||||||
(coercer-http (-> (handler) :body json/decode))
|
(coercer-http (-> (handler) :body json/decode))
|
||||||
(catch Exception e
|
(catch Exception e
|
||||||
@@ -58,9 +65,8 @@
|
|||||||
nil)))))
|
nil)))))
|
||||||
|
|
||||||
(defn- request-to-nitrate
|
(defn- request-to-nitrate
|
||||||
[cfg method uri schema {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::management-key] :as cfg} method uri schema {:keys [::rpc/profile-id] :as params}]
|
||||||
(let [shared-key (-> cfg ::setup/shared-keys :nitrate)
|
(let [full-http-call (-> (request-builder cfg method uri management-key profile-id)
|
||||||
full-http-call (-> (request-builder cfg method uri shared-key profile-id)
|
|
||||||
(with-retries 3)
|
(with-retries 3)
|
||||||
(with-validate uri schema))]
|
(with-validate uri schema))]
|
||||||
(full-http-call)))
|
(full-http-call)))
|
||||||
@@ -97,15 +103,26 @@
|
|||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||||
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
|
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; INITIALIZATION
|
;; INITIALIZATION
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defmethod ig/init-key ::client
|
(defmethod ig/init-key ::client
|
||||||
[_ cfg]
|
[_ {:keys [::setup/props] :as cfg}]
|
||||||
(when (contains? cf/flags :nitrate)
|
(if (contains? cf/flags :nitrate)
|
||||||
{:get-team-org (partial get-team-org cfg)
|
(let [management-key (or (cf/get :management-api-key)
|
||||||
:is-valid-user (partial is-valid-user cfg)}))
|
(get props :management-key))
|
||||||
|
cfg (assoc cfg ::management-key management-key)]
|
||||||
|
{:get-team-org (partial get-team-org cfg)
|
||||||
|
:is-valid-user (partial is-valid-user cfg)})
|
||||||
|
{}))
|
||||||
|
|
||||||
|
(defmethod ig/halt-key! ::client
|
||||||
|
[_ {:keys []}]
|
||||||
|
(do :stuff))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; UTILS
|
;; UTILS
|
||||||
@@ -127,4 +144,4 @@
|
|||||||
[cfg team params]
|
[cfg team params]
|
||||||
(let [params (assoc (or params {}) :team-id (:id team))
|
(let [params (assoc (or params {}) :team-id (:id team))
|
||||||
org (call cfg :get-team-org params)]
|
org (call cfg :get-team-org params)]
|
||||||
(assoc team :organization-id (:id org) :organization-name (:name org))))
|
(assoc team :organization-id (:id org) :organization-name (:name org))))
|
||||||
@@ -92,11 +92,11 @@
|
|||||||
(fn [{:keys [params path-params method] :as request}]
|
(fn [{:keys [params path-params method] :as request}]
|
||||||
(let [handler-name (:type path-params)
|
(let [handler-name (:type path-params)
|
||||||
etag (yreq/get-header request "if-none-match")
|
etag (yreq/get-header request "if-none-match")
|
||||||
|
|
||||||
key-id (get request ::http/auth-key-id)
|
|
||||||
profile-id (or (::session/profile-id request)
|
profile-id (or (::session/profile-id request)
|
||||||
(::actoken/profile-id request)
|
(::actoken/profile-id request)
|
||||||
(if key-id uuid/zero nil))
|
(if (::http/auth-with-shared-key request)
|
||||||
|
uuid/zero
|
||||||
|
nil))
|
||||||
|
|
||||||
ip-addr (inet/parse-request request)
|
ip-addr (inet/parse-request request)
|
||||||
|
|
||||||
@@ -298,12 +298,11 @@
|
|||||||
|
|
||||||
(defn- resolve-management-methods
|
(defn- resolve-management-methods
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)
|
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
|
||||||
mods (cond->> (list 'app.rpc.management.exporter)
|
(->> (sv/scan-ns
|
||||||
(contains? cf/flags :nitrate)
|
'app.rpc.management.subscription
|
||||||
(cons 'app.rpc.management.nitrate))]
|
'app.rpc.management.nitrate
|
||||||
|
'app.rpc.management.exporter)
|
||||||
(->> (apply sv/scan-ns mods)
|
|
||||||
(map (partial process-method cfg "management" wrap-management))
|
(map (partial process-method cfg "management" wrap-management))
|
||||||
(into {}))))
|
(into {}))))
|
||||||
|
|
||||||
@@ -347,20 +346,23 @@
|
|||||||
|
|
||||||
(defmethod ig/assert-key ::routes
|
(defmethod ig/assert-key ::routes
|
||||||
[_ params]
|
[_ params]
|
||||||
(assert (map? (::setup/shared-keys params)))
|
|
||||||
(assert (db/pool? (::db/pool params)) "expect valid database pool")
|
(assert (db/pool? (::db/pool params)) "expect valid database pool")
|
||||||
|
(assert (some? (::setup/props params)))
|
||||||
(assert (session/manager? (::session/manager params)) "expect valid session manager")
|
(assert (session/manager? (::session/manager params)) "expect valid session manager")
|
||||||
(assert (valid-methods? (::methods params)) "expect valid methods map")
|
(assert (valid-methods? (::methods params)) "expect valid methods map")
|
||||||
(assert (valid-methods? (::management-methods params)) "expect valid methods map"))
|
(assert (valid-methods? (::management-methods params)) "expect valid methods map"))
|
||||||
|
|
||||||
(defmethod ig/init-key ::routes
|
(defmethod ig/init-key ::routes
|
||||||
[_ {:keys [::methods ::management-methods ::setup/shared-keys] :as cfg}]
|
[_ {:keys [::methods ::management-methods ::setup/props] :as cfg}]
|
||||||
|
|
||||||
|
(let [public-uri (cf/get :public-uri)
|
||||||
|
management-key (or (cf/get :management-api-key)
|
||||||
|
(get props :management-key))]
|
||||||
|
|
||||||
(let [public-uri (cf/get :public-uri)]
|
|
||||||
["/api"
|
["/api"
|
||||||
["/management"
|
["/management"
|
||||||
["/methods/:type"
|
["/methods/:type"
|
||||||
{:middleware [[mw/shared-key-auth shared-keys]
|
{:middleware [[mw/shared-key-auth management-key]
|
||||||
[session/authz cfg]]
|
[session/authz cfg]]
|
||||||
:handler (make-rpc-handler management-methods)}]
|
:handler (make-rpc-handler management-methods)}]
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
;; Copyright (c) KALEIDOS INC
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
(ns app.rpc.management.nitrate
|
(ns app.rpc.management.nitrate
|
||||||
"Internal Nitrate HTTP RPC API. Provides authenticated access to
|
"Internal Nitrate HTTP API.
|
||||||
organization management and token validation endpoints."
|
Provides authenticated access to organization management and token validation endpoints.
|
||||||
|
All requests must include a valid shared key token in the `x-shared-key` header, and
|
||||||
|
a cookie `auth-token` with the user token.
|
||||||
|
They will return `401 Unauthorized` if the shared key or user token are invalid."
|
||||||
(:require
|
(:require
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.types.profile :refer [schema:profile]]
|
|
||||||
[app.common.types.team :refer [schema:team]]
|
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
@@ -22,14 +23,22 @@
|
|||||||
[app.util.services :as sv]))
|
[app.util.services :as sv]))
|
||||||
|
|
||||||
;; ---- API: authenticate
|
;; ---- API: authenticate
|
||||||
|
(def ^:private schema:profile
|
||||||
|
[:map
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:name :string]
|
||||||
|
[:email :string]
|
||||||
|
[:photo-url :string]])
|
||||||
|
|
||||||
(sv/defmethod ::authenticate
|
(sv/defmethod ::authenticate
|
||||||
"Authenticate the current user"
|
"Authenticate an user
|
||||||
{::doc/added "2.14"
|
@api GET /authenticate
|
||||||
::sm/params [:map]
|
@returns
|
||||||
|
200 OK: Returns the authenticated user."
|
||||||
|
{::doc/added "2.12"
|
||||||
::sm/result schema:profile}
|
::sm/result schema:profile}
|
||||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||||
(let [profile (profile/get-profile cfg profile-id)]
|
(let [profile (profile/get-profile cfg profile-id)]
|
||||||
{:id (get profile :id)
|
{:id (get profile :id)
|
||||||
:name (get profile :fullname)
|
:name (get profile :fullname)
|
||||||
:email (get profile :email)
|
:email (get profile :email)
|
||||||
@@ -42,22 +51,30 @@
|
|||||||
FROM team AS t
|
FROM team AS t
|
||||||
JOIN team_profile_rel AS tpr ON t.id = tpr.team_id
|
JOIN team_profile_rel AS tpr ON t.id = tpr.team_id
|
||||||
WHERE tpr.profile_id = ?
|
WHERE tpr.profile_id = ?
|
||||||
AND tpr.is_owner IS TRUE
|
AND tpr.is_owner = 't'
|
||||||
AND t.is_default IS FALSE
|
AND t.is_default = 'f'
|
||||||
AND t.deleted_at IS NULL;")
|
AND t.deleted_at is null;")
|
||||||
|
|
||||||
|
(def ^:private schema:team
|
||||||
|
[:map
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:name :string]])
|
||||||
|
|
||||||
(def ^:private schema:get-teams-result
|
(def ^:private schema:get-teams-result
|
||||||
[:vector schema:team])
|
[:vector schema:team])
|
||||||
|
|
||||||
(sv/defmethod ::get-teams
|
(sv/defmethod ::get-teams
|
||||||
"List teams for which current user is owner"
|
"List teams for which current user is owner.
|
||||||
{::doc/added "2.14"
|
@api GET /get-teams
|
||||||
::sm/params [:map]
|
@returns
|
||||||
|
200 OK: Returns the list of teams for the user."
|
||||||
|
{::doc/added "2.12"
|
||||||
::sm/result schema:get-teams-result}
|
::sm/result schema:get-teams-result}
|
||||||
[cfg {:keys [::rpc/profile-id]}]
|
[cfg {:keys [::rpc/profile-id]}]
|
||||||
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
|
(when (contains? cf/flags :nitrate)
|
||||||
(->> (db/exec! cfg [sql:get-teams current-user-id])
|
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
|
||||||
(map #(select-keys % [:id :name])))))
|
(->> (db/exec! cfg [sql:get-teams current-user-id])
|
||||||
|
(map #(select-keys % [:id :name]))))))
|
||||||
|
|
||||||
;; ---- API: notify-team-change
|
;; ---- API: notify-team-change
|
||||||
|
|
||||||
@@ -66,18 +83,30 @@
|
|||||||
[:id ::sm/uuid]
|
[:id ::sm/uuid]
|
||||||
[:organization-id ::sm/text]])
|
[:organization-id ::sm/text]])
|
||||||
|
|
||||||
|
|
||||||
(sv/defmethod ::notify-team-change
|
(sv/defmethod ::notify-team-change
|
||||||
"Notify to Penpot a team change from nitrate"
|
"Notify to Penpot a team change from nitrate
|
||||||
{::doc/added "2.14"
|
@api POST /notify-team-change
|
||||||
|
@returns
|
||||||
|
200 OK"
|
||||||
|
{::doc/added "2.12"
|
||||||
::sm/params schema:notify-team-change
|
::sm/params schema:notify-team-change
|
||||||
::rpc/auth false}
|
::rpc/auth false}
|
||||||
[cfg {:keys [id organization-id organization-name]}]
|
[cfg {:keys [id organization-id organization-name]}]
|
||||||
(let [msgbus (::mbus/msgbus cfg)]
|
(when (contains? cf/flags :nitrate)
|
||||||
(mbus/pub! msgbus
|
(let [msgbus (::mbus/msgbus cfg)]
|
||||||
;;TODO There is a bug on dashboard with teams notifications.
|
(mbus/pub! msgbus
|
||||||
;;For now we send it to uuid/zero instead of team-id
|
;;TODO There is a bug on dashboard with teams notifications.
|
||||||
:topic uuid/zero
|
;;For now we send it to uuid/zero instead of team-id
|
||||||
:message {:type :team-org-change
|
:topic uuid/zero
|
||||||
:team-id id
|
:message {:type :team-org-change
|
||||||
:organization-id organization-id
|
:team-id id
|
||||||
:organization-name organization-name})))
|
:organization-id organization-id
|
||||||
|
:organization-name organization-name}))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
183
backend/src/app/rpc/management/subscription.clj
Normal file
183
backend/src/app/rpc/management/subscription.clj
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
;; 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.rpc.management.subscription
|
||||||
|
(:require
|
||||||
|
[app.common.logging :as l]
|
||||||
|
[app.common.schema :as sm]
|
||||||
|
[app.common.schema.generators :as sg]
|
||||||
|
[app.common.time :as ct]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.commands.profile :as profile]
|
||||||
|
[app.rpc.doc :as doc]
|
||||||
|
[app.util.services :as sv]))
|
||||||
|
|
||||||
|
;; ---- RPC METHOD: AUTHENTICATE
|
||||||
|
|
||||||
|
(def ^:private
|
||||||
|
schema:authenticate-params
|
||||||
|
[:map {:title "authenticate-params"}])
|
||||||
|
|
||||||
|
(def ^:private
|
||||||
|
schema:authenticate-result
|
||||||
|
[:map {:title "authenticate-result"}
|
||||||
|
[:profile-id ::sm/uuid]])
|
||||||
|
|
||||||
|
(sv/defmethod ::auth
|
||||||
|
{::doc/added "2.12"
|
||||||
|
::sm/params schema:authenticate-params
|
||||||
|
::sm/result schema:authenticate-result}
|
||||||
|
[_ {:keys [::rpc/profile-id]}]
|
||||||
|
{:profile-id profile-id})
|
||||||
|
|
||||||
|
;; ---- RPC METHOD: GET-CUSTOMER
|
||||||
|
|
||||||
|
;; FIXME: move to app.common.time
|
||||||
|
(def ^:private schema:timestamp
|
||||||
|
(sm/type-schema
|
||||||
|
{:type ::timestamp
|
||||||
|
:pred ct/inst?
|
||||||
|
:type-properties
|
||||||
|
{:title "inst"
|
||||||
|
:description "The same as :app.common.time/inst but encodes to epoch"
|
||||||
|
:error/message "should be an instant"
|
||||||
|
:gen/gen (->> (sg/small-int)
|
||||||
|
(sg/fmap (fn [v] (ct/inst v))))
|
||||||
|
:decode/string #(some-> % ct/inst)
|
||||||
|
:encode/string #(some-> % inst-ms)
|
||||||
|
:decode/json #(some-> % ct/inst)
|
||||||
|
:encode/json #(some-> % inst-ms)}}))
|
||||||
|
|
||||||
|
(def ^:private schema:subscription
|
||||||
|
[:map {:title "Subscription"}
|
||||||
|
[:id ::sm/text]
|
||||||
|
[:customer-id ::sm/text]
|
||||||
|
[:type [:enum
|
||||||
|
"unlimited"
|
||||||
|
"professional"
|
||||||
|
"enterprise"]]
|
||||||
|
[:status [:enum
|
||||||
|
"active"
|
||||||
|
"canceled"
|
||||||
|
"incomplete"
|
||||||
|
"incomplete_expired"
|
||||||
|
"past_due"
|
||||||
|
"paused"
|
||||||
|
"trialing"
|
||||||
|
"unpaid"]]
|
||||||
|
|
||||||
|
[:billing-period [:enum
|
||||||
|
"month"
|
||||||
|
"day"
|
||||||
|
"week"
|
||||||
|
"year"]]
|
||||||
|
[:quantity :int]
|
||||||
|
[:description [:maybe ::sm/text]]
|
||||||
|
[:created-at schema:timestamp]
|
||||||
|
[:start-date [:maybe schema:timestamp]]
|
||||||
|
[:ended-at [:maybe schema:timestamp]]
|
||||||
|
[:trial-end [:maybe schema:timestamp]]
|
||||||
|
[:trial-start [:maybe schema:timestamp]]
|
||||||
|
[:cancel-at [:maybe schema:timestamp]]
|
||||||
|
[:canceled-at [:maybe schema:timestamp]]
|
||||||
|
[:current-period-end [:maybe schema:timestamp]]
|
||||||
|
[:current-period-start [:maybe schema:timestamp]]
|
||||||
|
[:cancel-at-period-end :boolean]
|
||||||
|
|
||||||
|
[:cancellation-details
|
||||||
|
[:map {:title "CancellationDetails"}
|
||||||
|
[:comment [:maybe ::sm/text]]
|
||||||
|
[:reason [:maybe ::sm/text]]
|
||||||
|
[:feedback [:maybe
|
||||||
|
[:enum
|
||||||
|
"customer_service"
|
||||||
|
"low_quality"
|
||||||
|
"missing_feature"
|
||||||
|
"other"
|
||||||
|
"switched_service"
|
||||||
|
"too_complex"
|
||||||
|
"too_expensive"
|
||||||
|
"unused"]]]]]])
|
||||||
|
|
||||||
|
(def ^:private sql:get-customer-slots
|
||||||
|
"WITH teams AS (
|
||||||
|
SELECT tpr.team_id AS id,
|
||||||
|
tpr.profile_id AS profile_id
|
||||||
|
FROM team_profile_rel AS tpr
|
||||||
|
WHERE tpr.is_owner IS true
|
||||||
|
AND tpr.profile_id = ?
|
||||||
|
), teams_with_slots AS (
|
||||||
|
SELECT tpr.team_id AS id,
|
||||||
|
count(*) AS total
|
||||||
|
FROM team_profile_rel AS tpr
|
||||||
|
WHERE tpr.team_id IN (SELECT id FROM teams)
|
||||||
|
AND tpr.can_edit IS true
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY 2
|
||||||
|
)
|
||||||
|
SELECT max(total) AS total FROM teams_with_slots;")
|
||||||
|
|
||||||
|
(defn- get-customer-slots
|
||||||
|
[cfg profile-id]
|
||||||
|
(let [result (db/exec-one! cfg [sql:get-customer-slots profile-id])]
|
||||||
|
(:total result)))
|
||||||
|
|
||||||
|
(def ^:private schema:get-customer-params
|
||||||
|
[:map])
|
||||||
|
|
||||||
|
(def ^:private schema:get-customer-result
|
||||||
|
[:map
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:name :string]
|
||||||
|
[:num-editors ::sm/int]
|
||||||
|
[:subscription {:optional true} schema:subscription]])
|
||||||
|
|
||||||
|
(sv/defmethod ::get-customer
|
||||||
|
{::doc/added "2.12"
|
||||||
|
::sm/params schema:get-customer-params
|
||||||
|
::sm/result schema:get-customer-result}
|
||||||
|
[cfg {:keys [::rpc/profile-id]}]
|
||||||
|
(let [profile (profile/get-profile cfg profile-id)]
|
||||||
|
{:id (get profile :id)
|
||||||
|
:name (get profile :fullname)
|
||||||
|
:email (get profile :email)
|
||||||
|
:num-editors (get-customer-slots cfg profile-id)
|
||||||
|
:subscription (-> profile :props :subscription)}))
|
||||||
|
|
||||||
|
|
||||||
|
;; ---- RPC METHOD: GET-CUSTOMER
|
||||||
|
|
||||||
|
(def ^:private schema:update-customer-params
|
||||||
|
[:map
|
||||||
|
[:subscription [:maybe schema:subscription]]])
|
||||||
|
|
||||||
|
(def ^:private schema:update-customer-result
|
||||||
|
[:map])
|
||||||
|
|
||||||
|
(sv/defmethod ::update-customer
|
||||||
|
{::doc/added "2.12"
|
||||||
|
::sm/params schema:update-customer-params
|
||||||
|
::sm/result schema:update-customer-result}
|
||||||
|
[cfg {:keys [::rpc/profile-id subscription]}]
|
||||||
|
(let [{:keys [props] :as profile}
|
||||||
|
(profile/get-profile cfg profile-id ::db/for-update true)
|
||||||
|
|
||||||
|
props
|
||||||
|
(assoc props :subscription subscription)]
|
||||||
|
|
||||||
|
(l/dbg :hint "update customer"
|
||||||
|
:profile-id (str profile-id)
|
||||||
|
:subscription-type (get subscription :type)
|
||||||
|
:subscription-status (get subscription :status)
|
||||||
|
:subscription-quantity (get subscription :quantity))
|
||||||
|
|
||||||
|
(db/update! cfg :profile
|
||||||
|
{:props (db/tjson props)}
|
||||||
|
{:id profile-id}
|
||||||
|
{::db/return-keys false})
|
||||||
|
|
||||||
|
nil))
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
[app.setup.templates]
|
[app.setup.templates]
|
||||||
[buddy.core.codecs :as bc]
|
[buddy.core.codecs :as bc]
|
||||||
[buddy.core.nonce :as bn]
|
[buddy.core.nonce :as bn]
|
||||||
[cuerdas.core :as str]
|
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
(defn- generate-random-key
|
(defn- generate-random-key
|
||||||
@@ -89,38 +88,7 @@
|
|||||||
(-> (get-all-props conn)
|
(-> (get-all-props conn)
|
||||||
(assoc :secret-key secret)
|
(assoc :secret-key secret)
|
||||||
(assoc :tokens-key (keys/derive secret :salt "tokens"))
|
(assoc :tokens-key (keys/derive secret :salt "tokens"))
|
||||||
|
(assoc :management-key (keys/derive secret :salt "management"))
|
||||||
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
|
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
|
||||||
|
|
||||||
(sm/register! ::props [:map-of :keyword ::sm/any])
|
(sm/register! ::props [:map-of :keyword ::sm/any])
|
||||||
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::shared-keys
|
|
||||||
[_ {:keys [::props] :as cfg}]
|
|
||||||
(let [secret (get props :secret-key)]
|
|
||||||
(d/without-nils
|
|
||||||
{"exporter"
|
|
||||||
(let [key (or (get cfg :exporter)
|
|
||||||
(-> (keys/derive secret :salt "exporter")
|
|
||||||
(bc/bytes->b64-str true)))]
|
|
||||||
(if (or (str/empty? key)
|
|
||||||
(str/blank? key))
|
|
||||||
(do
|
|
||||||
(l/wrn :hint "exporter key is disabled because empty string found")
|
|
||||||
nil)
|
|
||||||
(do
|
|
||||||
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
|
|
||||||
key)))
|
|
||||||
|
|
||||||
"nitrate"
|
|
||||||
(let [key (or (get cfg :nitrate)
|
|
||||||
(-> (keys/derive secret :salt "nitrate")
|
|
||||||
(bc/bytes->b64-str true)))]
|
|
||||||
(if (or (str/empty? key)
|
|
||||||
(str/blank? key))
|
|
||||||
(do
|
|
||||||
(l/wrn :hint "nitrate key is disabled because empty string found")
|
|
||||||
nil)
|
|
||||||
(do
|
|
||||||
(l/inf :hint "nitrate key initialized" :key (d/obfuscate-string key))
|
|
||||||
key)))})))
|
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
(t/deftest shared-key-auth
|
(t/deftest shared-key-auth
|
||||||
(let [handler (#'app.http.middleware/wrap-shared-key-auth
|
(let [handler (#'app.http.middleware/wrap-shared-key-auth
|
||||||
(fn [req] {::yres/status 200})
|
(fn [req] {::yres/status 200})
|
||||||
{"test1" "secret-key"})]
|
"secret-key")]
|
||||||
|
|
||||||
(let [response (handler (->DummyRequest {} {}))]
|
(let [response (handler (->DummyRequest {} {}))]
|
||||||
(t/is (= 403 (::yres/status response))))
|
(t/is (= 403 (::yres/status response))))
|
||||||
@@ -95,9 +95,6 @@
|
|||||||
(t/is (= 403 (::yres/status response))))
|
(t/is (= 403 (::yres/status response))))
|
||||||
|
|
||||||
(let [response (handler (->DummyRequest {"x-shared-key" "secret-key"} {}))]
|
(let [response (handler (->DummyRequest {"x-shared-key" "secret-key"} {}))]
|
||||||
(t/is (= 403 (::yres/status response))))
|
|
||||||
|
|
||||||
(let [response (handler (->DummyRequest {"x-shared-key" "test1 secret-key"} {}))]
|
|
||||||
(t/is (= 200 (::yres/status response))))))
|
(t/is (= 200 (::yres/status response))))))
|
||||||
|
|
||||||
(t/deftest access-token-authz
|
(t/deftest access-token-authz
|
||||||
|
|||||||
@@ -19,10 +19,3 @@
|
|||||||
|
|
||||||
(def schema:role
|
(def schema:role
|
||||||
[::sm/one-of {:title "TeamRole"} valid-roles])
|
[::sm/one-of {:title "TeamRole"} valid-roles])
|
||||||
|
|
||||||
;; FIXME: specify more fields
|
|
||||||
(def schema:team
|
|
||||||
[:map {:title "Team"}
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:name :string]])
|
|
||||||
|
|
||||||
|
|||||||
@@ -398,6 +398,7 @@ COPY files/Caddyfile /home/
|
|||||||
COPY files/selfsigned.crt /home/
|
COPY files/selfsigned.crt /home/
|
||||||
COPY files/selfsigned.key /home/
|
COPY files/selfsigned.key /home/
|
||||||
COPY files/start-tmux.sh /home/start-tmux.sh
|
COPY files/start-tmux.sh /home/start-tmux.sh
|
||||||
|
COPY files/start-tmux-back.sh /home/start-tmux-back.sh
|
||||||
COPY files/entrypoint.sh /home/entrypoint.sh
|
COPY files/entrypoint.sh /home/entrypoint.sh
|
||||||
COPY files/init.sh /home/init.sh
|
COPY files/init.sh /home/init.sh
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
localhost:3449 {
|
localhost:3449 {
|
||||||
reverse_proxy localhost:4449
|
reverse_proxy localhost:4449
|
||||||
tls /home/selfsigned.crt /home/selfsigned.key
|
tls /home/selfsigned.crt /home/selfsigned.key
|
||||||
header -Strict-Transport-Security
|
|
||||||
}
|
}
|
||||||
|
|
||||||
http://localhost:3450 {
|
http://localhost:3450 {
|
||||||
|
|||||||
@@ -12,14 +12,11 @@
|
|||||||
["node:process" :as process]
|
["node:process" :as process]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.flags :as flags]
|
[app.common.flags :as flags]
|
||||||
[app.common.logging :as l]
|
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.version :as v]
|
[app.common.version :as v]
|
||||||
[cljs.core :as c]
|
[cljs.core :as c]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
(l/set-level! :info)
|
|
||||||
|
|
||||||
(def ^:private defaults
|
(def ^:private defaults
|
||||||
{:public-uri "http://localhost:3449"
|
{:public-uri "http://localhost:3449"
|
||||||
:tenant "default"
|
:tenant "default"
|
||||||
@@ -33,7 +30,7 @@
|
|||||||
[:map {:title "config"}
|
[:map {:title "config"}
|
||||||
[:secret-key :string]
|
[:secret-key :string]
|
||||||
[:public-uri {:optional true} ::sm/uri]
|
[:public-uri {:optional true} ::sm/uri]
|
||||||
[:exporter-shared-key {:optional true} :string]
|
[:management-api-key {:optional true} :string]
|
||||||
[:host {:optional true} :string]
|
[:host {:optional true} :string]
|
||||||
[:tenant {:optional true} :string]
|
[:tenant {:optional true} :string]
|
||||||
[:flags {:optional true} [::sm/set :keyword]]
|
[:flags {:optional true} [::sm/set :keyword]]
|
||||||
@@ -101,10 +98,8 @@
|
|||||||
(c/get config key default)))
|
(c/get config key default)))
|
||||||
|
|
||||||
(def management-key
|
(def management-key
|
||||||
(let [key (or (c/get config :exporter-shared-key)
|
(or (c/get config :management-api-key)
|
||||||
(let [secret-key (c/get config :secret-key)
|
(let [secret-key (c/get config :secret-key)
|
||||||
derived-key (crypto/hkdfSync "blake2b512" secret-key, "exporter" "" 32)]
|
derived-key (crypto/hkdfSync "blake2b512" secret-key, "management" "" 32)]
|
||||||
(-> (.from buffer/Buffer derived-key)
|
(-> (.from buffer/Buffer derived-key)
|
||||||
(.toString "base64url"))))]
|
(.toString "base64url")))))
|
||||||
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
|
|
||||||
key))
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
(p/mcat (fn [blob]
|
(p/mcat (fn [blob]
|
||||||
(let [fdata (new http/FormData)
|
(let [fdata (new http/FormData)
|
||||||
agent (new http/Agent #js {:connect #js {:rejectUnauthorized false}})
|
agent (new http/Agent #js {:connect #js {:rejectUnauthorized false}})
|
||||||
headers #js {"X-Shared-Key" (str "exporter " cf/management-key)
|
headers #js {"X-Shared-Key" cf/management-key
|
||||||
"Authorization" (str "Bearer " auth-token)}
|
"Authorization" (str "Bearer " auth-token)}
|
||||||
|
|
||||||
request #js {:headers headers
|
request #js {:headers headers
|
||||||
|
|||||||
@@ -12,88 +12,118 @@ test.beforeEach(async ({ page }) => {
|
|||||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Tokens: Remapping Feature", () => {
|
const createToken = async (page, type, name, textFieldName, value) => {
|
||||||
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
|
||||||
|
const { tokensUpdateCreateModal } = await setupTokensFile(page, {
|
||||||
|
flags: ["enable-token-shadow"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create base token
|
||||||
|
await tokensTabPanel
|
||||||
|
.getByRole("button", { name: `Add Token: ${type}` })
|
||||||
|
.click();
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
|
await nameField.fill(name);
|
||||||
|
|
||||||
|
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||||
|
name: textFieldName,
|
||||||
|
});
|
||||||
|
await colorField.fill(value);
|
||||||
|
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
await submitButton.click();
|
||||||
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renameToken = async (page, oldName, newName) => {
|
||||||
|
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
|
||||||
|
await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||||
|
|
||||||
|
const baseToken = tokensSidebar.getByRole("button", {
|
||||||
|
name: oldName,
|
||||||
|
});
|
||||||
|
await baseToken.click({ button: "right" });
|
||||||
|
await tokenContextMenuForToken.getByText("Edit token").click();
|
||||||
|
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
|
await nameField.fill(newName);
|
||||||
|
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
await submitButton.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCompositeDerivedToken = async (page, type, name, reference) => {
|
||||||
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
|
||||||
|
const { tokensUpdateCreateModal } = await setupTokensFile(page, {
|
||||||
|
flags: ["enable-token-shadow"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await tokensTabPanel
|
||||||
|
.getByRole("button", { name: `Add Token: ${type}` })
|
||||||
|
.click();
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||||
|
name: "Name",
|
||||||
|
});
|
||||||
|
await nameField.fill(name);
|
||||||
|
|
||||||
|
const referenceToggle = tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||||
|
await referenceToggle.click();
|
||||||
|
|
||||||
|
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||||
|
name: "Reference",
|
||||||
|
});
|
||||||
|
await referenceField.fill(reference);
|
||||||
|
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
await submitButton.click();
|
||||||
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe("Remapping Tokens", () => {
|
||||||
test.describe("Box Shadow Token Remapping", () => {
|
test.describe("Box Shadow Token Remapping", () => {
|
||||||
test("User renames box shadow token with alias references", async ({
|
test("User renames box shadow token with alias references", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { tokensSidebar } = await setupTokensFile(page, {
|
||||||
tokensUpdateCreateModal,
|
flags: ["enable-token-shadow"],
|
||||||
tokensSidebar,
|
});
|
||||||
tokenContextMenuForToken,
|
|
||||||
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
|
||||||
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
|
||||||
|
|
||||||
// Create base shadow token
|
// Create base shadow token
|
||||||
await tokensTabPanel
|
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
|
||||||
.getByRole("button", { name: "Add Token: Shadow" })
|
|
||||||
.click();
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill("base-shadow");
|
|
||||||
|
|
||||||
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
|
||||||
name: "Color",
|
|
||||||
});
|
|
||||||
await colorField.fill("#000000");
|
|
||||||
|
|
||||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
// Create derived shadow token that references base-shadow
|
// Create derived shadow token that references base-shadow
|
||||||
await tokensTabPanel
|
await createCompositeDerivedToken(
|
||||||
.getByRole("button", { name: "Add Token: Shadow" })
|
page,
|
||||||
.click();
|
"Shadow",
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
"derived-shadow",
|
||||||
|
"{base-shadow}",
|
||||||
nameField = tokensUpdateCreateModal.getByRole("textbox", {
|
);
|
||||||
name: "Name",
|
|
||||||
});
|
|
||||||
await nameField.fill("derived-shadow");
|
|
||||||
|
|
||||||
const referenceToggle =
|
|
||||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
|
||||||
await referenceToggle.click();
|
|
||||||
|
|
||||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
|
||||||
name: "Reference",
|
|
||||||
});
|
|
||||||
await referenceField.fill("{base-shadow}");
|
|
||||||
|
|
||||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
// Rename base-shadow token
|
// Rename base-shadow token
|
||||||
const baseToken = tokensSidebar.getByRole("button", {
|
await renameToken(page, "base-shadow", "foundation-shadow");
|
||||||
name: "base-shadow",
|
|
||||||
});
|
|
||||||
await baseToken.click({ button: "right" });
|
|
||||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill("foundation-shadow");
|
|
||||||
|
|
||||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
|
|
||||||
// Check for remapping modal
|
// Check for remapping modal
|
||||||
const remappingModal = page.getByTestId("token-remapping-modal");
|
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||||
await expect(remappingModal).toContainText("1");
|
await expect(remappingModal).toContainText("base-shadow");
|
||||||
|
await expect(remappingModal).toContainText("foundation-shadow");
|
||||||
|
|
||||||
const confirmButton = remappingModal.getByRole("button", {
|
const confirmButton = remappingModal.getByRole("button", {
|
||||||
name: /remap/i,
|
name: "remap tokens",
|
||||||
});
|
});
|
||||||
await confirmButton.click();
|
await confirmButton.click();
|
||||||
|
|
||||||
@@ -116,51 +146,16 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
workspacePage,
|
workspacePage,
|
||||||
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
|
||||||
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
|
||||||
|
|
||||||
// Create base shadow token
|
// Create base shadow token
|
||||||
await tokensTabPanel
|
await createToken(page, "Shadow", "primary-shadow", "Color", "#000000");
|
||||||
.getByRole("button", { name: "Add Token: Shadow" })
|
|
||||||
.click();
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill("primary-shadow");
|
|
||||||
|
|
||||||
let colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
|
||||||
name: "Color",
|
|
||||||
});
|
|
||||||
await colorField.fill("#000000");
|
|
||||||
|
|
||||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
// Create derived shadow token that references base
|
// Create derived shadow token that references base
|
||||||
await tokensTabPanel
|
await createCompositeDerivedToken(
|
||||||
.getByRole("button", { name: "Add Token: Shadow" })
|
page,
|
||||||
.click();
|
"Shadow",
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
"card-shadow",
|
||||||
|
"{primary-shadow}",
|
||||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
);
|
||||||
await nameField.fill("card-shadow");
|
|
||||||
|
|
||||||
const referenceToggle =
|
|
||||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
|
||||||
await referenceToggle.click();
|
|
||||||
|
|
||||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
|
||||||
name: "Reference",
|
|
||||||
});
|
|
||||||
await referenceField.fill("{primary-shadow}");
|
|
||||||
|
|
||||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
// Apply the referenced token to a shape
|
// Apply the referenced token to a shape
|
||||||
await page.getByRole("tab", { name: "Layers" }).click();
|
await page.getByRole("tab", { name: "Layers" }).click();
|
||||||
@@ -183,16 +178,16 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
await tokenContextMenuForToken.getByText("Edit token").click();
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
await nameField.fill("main-shadow");
|
await nameField.fill("main-shadow");
|
||||||
|
|
||||||
// Update the color value
|
// Update the color value
|
||||||
colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||||
name: "Color",
|
name: "Color",
|
||||||
});
|
});
|
||||||
await colorField.fill("#FF0000");
|
await colorField.fill("#FF0000");
|
||||||
|
|
||||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
name: "Save",
|
name: "Save",
|
||||||
});
|
});
|
||||||
await submitButton.click();
|
await submitButton.click();
|
||||||
@@ -202,7 +197,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
const confirmButton = remappingModal.getByRole("button", {
|
const confirmButton = remappingModal.getByRole("button", {
|
||||||
name: /remap/i,
|
name: "remap tokens",
|
||||||
});
|
});
|
||||||
await confirmButton.click();
|
await confirmButton.click();
|
||||||
|
|
||||||
@@ -259,73 +254,25 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
|
||||||
// Create base typography token
|
// Create base typography token
|
||||||
await tokensTabPanel
|
await createToken(page, "Typography", "base-text", "Font size", "16");
|
||||||
.getByRole("button", { name: "Add Token: Typography" })
|
|
||||||
.click();
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill("base-text");
|
|
||||||
|
|
||||||
const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
|
|
||||||
name: "Font size",
|
|
||||||
});
|
|
||||||
await fontSizeField.fill("16");
|
|
||||||
|
|
||||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
// Create derived typography token
|
// Create derived typography token
|
||||||
await tokensTabPanel
|
await createCompositeDerivedToken(
|
||||||
.getByRole("button", { name: "Add Token: Typography" })
|
page,
|
||||||
.click();
|
"Typography",
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
"body-text",
|
||||||
|
"{base-text}",
|
||||||
nameField = tokensUpdateCreateModal.getByRole("textbox", {
|
);
|
||||||
name: "Name",
|
|
||||||
});
|
|
||||||
await nameField.fill("body-text");
|
|
||||||
|
|
||||||
const referenceToggle =
|
|
||||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
|
||||||
await referenceToggle.click();
|
|
||||||
|
|
||||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
|
||||||
name: "Reference",
|
|
||||||
});
|
|
||||||
await referenceField.fill("{base-text}");
|
|
||||||
|
|
||||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
// Rename base token
|
// Rename base token
|
||||||
const baseToken = tokensSidebar.getByRole("button", {
|
await renameToken(page, "base-text", "default-text");
|
||||||
name: "base-text",
|
|
||||||
});
|
|
||||||
await baseToken.click({ button: "right" });
|
|
||||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill("default-text");
|
|
||||||
|
|
||||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
|
|
||||||
// Check for remapping modal
|
// Check for remapping modal
|
||||||
const remappingModal = page.getByTestId("token-remapping-modal");
|
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
const confirmButton = remappingModal.getByRole("button", {
|
const confirmButton = remappingModal.getByRole("button", {
|
||||||
name: /remap/i,
|
name: "remap tokens",
|
||||||
});
|
});
|
||||||
await confirmButton.click();
|
await confirmButton.click();
|
||||||
|
|
||||||
@@ -351,24 +298,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
|
||||||
// Create base typography token
|
// Create base typography token
|
||||||
await tokensTabPanel
|
await createToken(page, "Typography", "body-style", "Font size", "16");
|
||||||
.getByRole("button", { name: "Add Token: Typography" })
|
|
||||||
.click();
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill("body-style");
|
|
||||||
|
|
||||||
let fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
|
|
||||||
name: "Font size",
|
|
||||||
});
|
|
||||||
await fontSizeField.fill("16");
|
|
||||||
|
|
||||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
// Create derived typography token
|
// Create derived typography token
|
||||||
await tokensTabPanel
|
await tokensTabPanel
|
||||||
@@ -376,7 +306,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
.click();
|
.click();
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
nameField = tokensUpdateCreateModal.getByRole("textbox", {
|
let nameField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||||
name: "Name",
|
name: "Name",
|
||||||
});
|
});
|
||||||
await nameField.fill("paragraph-style");
|
await nameField.fill("paragraph-style");
|
||||||
@@ -390,7 +320,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
});
|
});
|
||||||
await referenceField.fill("{body-style}");
|
await referenceField.fill("{body-style}");
|
||||||
|
|
||||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
name: "Save",
|
name: "Save",
|
||||||
});
|
});
|
||||||
await submitButton.click();
|
await submitButton.click();
|
||||||
@@ -421,7 +351,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
await nameField.fill("text-base");
|
await nameField.fill("text-base");
|
||||||
|
|
||||||
// Update the font size value
|
// Update the font size value
|
||||||
fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
|
const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||||
name: "Font size",
|
name: "Font size",
|
||||||
});
|
});
|
||||||
await fontSizeField.fill("18");
|
await fontSizeField.fill("18");
|
||||||
@@ -436,7 +366,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
const confirmButton = remappingModal.getByRole("button", {
|
const confirmButton = remappingModal.getByRole("button", {
|
||||||
name: /remap/i,
|
name: "remap tokens",
|
||||||
});
|
});
|
||||||
await confirmButton.click();
|
await confirmButton.click();
|
||||||
|
|
||||||
@@ -471,72 +401,29 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
test("User renames border radius token with alias references", async ({
|
test("User renames border radius token with alias references", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { tokensSidebar } = await setupTokensFile(page);
|
||||||
tokensUpdateCreateModal,
|
|
||||||
tokensSidebar,
|
|
||||||
tokenContextMenuForToken,
|
|
||||||
} = await setupTokensFile(page);
|
|
||||||
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
|
||||||
|
|
||||||
// Create base border radius token
|
// Create base border radius token
|
||||||
await tokensTabPanel
|
await createToken(page, "Border Radius", "base-radius", "Value", "4");
|
||||||
.getByRole("button", { name: "Add Token: Border Radius" })
|
|
||||||
.click();
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill("base-radius");
|
|
||||||
|
|
||||||
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
|
||||||
await valueField.fill("4");
|
|
||||||
|
|
||||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
// Create derived border radius token
|
// Create derived border radius token
|
||||||
await tokensTabPanel
|
await createToken(
|
||||||
.getByRole("button", { name: "Add Token: Border Radius" })
|
page,
|
||||||
.click();
|
"Border Radius",
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
"card-radius",
|
||||||
|
"Value",
|
||||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
"{base-radius}",
|
||||||
await nameField.fill("card-radius");
|
);
|
||||||
|
|
||||||
const valueField2 = tokensUpdateCreateModal.getByLabel("Value");
|
|
||||||
await valueField2.fill("{base-radius}");
|
|
||||||
|
|
||||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
// Rename base token
|
// Rename base token
|
||||||
const baseToken = tokensSidebar.getByRole("button", {
|
await renameToken(page, "base-radius", "primary-radius");
|
||||||
name: "base-radius",
|
|
||||||
});
|
|
||||||
await baseToken.click({ button: "right" });
|
|
||||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill("primary-radius");
|
|
||||||
|
|
||||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
|
|
||||||
// Check for remapping modal
|
// Check for remapping modal
|
||||||
const remappingModal = page.getByTestId("token-remapping-modal");
|
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
const confirmButton = remappingModal.getByRole("button", {
|
const confirmButton = remappingModal.getByRole("button", {
|
||||||
name: /remap/i,
|
name: "remap tokens",
|
||||||
});
|
});
|
||||||
await confirmButton.click();
|
await confirmButton.click();
|
||||||
|
|
||||||
@@ -558,43 +445,17 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
tokenContextMenuForToken,
|
tokenContextMenuForToken,
|
||||||
} = await setupTokensFile(page);
|
} = await setupTokensFile(page);
|
||||||
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
|
||||||
|
|
||||||
// Create base border radius token
|
// Create base border radius token
|
||||||
await tokensTabPanel
|
await createToken(page, "Border Radius", "radius-sm", "Value", "4");
|
||||||
.getByRole("button", { name: "Add Token: Border Radius" })
|
|
||||||
.click();
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
let nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill("radius-sm");
|
|
||||||
|
|
||||||
let valueField = tokensUpdateCreateModal.getByLabel("Value");
|
|
||||||
await valueField.fill("4");
|
|
||||||
|
|
||||||
let submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
// Create derived border radius token
|
// Create derived border radius token
|
||||||
await tokensTabPanel
|
await createToken(
|
||||||
.getByRole("button", { name: "Add Token: Border Radius" })
|
page,
|
||||||
.click();
|
"Border Radius",
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
"button-radius",
|
||||||
|
"Value",
|
||||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
"{radius-sm}",
|
||||||
await nameField.fill("button-radius");
|
);
|
||||||
|
|
||||||
const valueField2 = tokensUpdateCreateModal.getByLabel("Value");
|
|
||||||
await valueField2.fill("{radius-sm}");
|
|
||||||
|
|
||||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await submitButton.click();
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
// Rename and update value of base token
|
// Rename and update value of base token
|
||||||
const radiusToken = tokensSidebar.getByRole("button", {
|
const radiusToken = tokensSidebar.getByRole("button", {
|
||||||
@@ -604,14 +465,14 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
await tokenContextMenuForToken.getByText("Edit token").click();
|
await tokenContextMenuForToken.getByText("Edit token").click();
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
nameField = tokensUpdateCreateModal.getByLabel("Name");
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
await nameField.fill("radius-base");
|
await nameField.fill("radius-base");
|
||||||
|
|
||||||
// Update the value
|
// Update the value
|
||||||
valueField = tokensUpdateCreateModal.getByLabel("Value");
|
const valueField = tokensUpdateCreateModal.getByLabel("Value");
|
||||||
await valueField.fill("8");
|
await valueField.fill("8");
|
||||||
|
|
||||||
submitButton = tokensUpdateCreateModal.getByRole("button", {
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
name: "Save",
|
name: "Save",
|
||||||
});
|
});
|
||||||
await submitButton.click();
|
await submitButton.click();
|
||||||
@@ -621,7 +482,7 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
const confirmButton = remappingModal.getByRole("button", {
|
const confirmButton = remappingModal.getByRole("button", {
|
||||||
name: /remap/i,
|
name: "remap tokens",
|
||||||
});
|
});
|
||||||
await confirmButton.click();
|
await confirmButton.click();
|
||||||
|
|
||||||
@@ -648,4 +509,82 @@ test.describe("Tokens: Remapping Feature", () => {
|
|||||||
await expect(currentValue).toHaveValue("{radius-base}");
|
await expect(currentValue).toHaveValue("{radius-base}");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("Cancel remap", () => {
|
||||||
|
test("Only rename - breaks reference", async ({ page }) => {
|
||||||
|
const { tokensSidebar } = await setupTokensFile(page, {
|
||||||
|
flags: ["enable-token-shadow"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create base shadow token
|
||||||
|
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
|
||||||
|
|
||||||
|
// Create derived shadow token that references base-shadow
|
||||||
|
await createCompositeDerivedToken(
|
||||||
|
page,
|
||||||
|
"Shadow",
|
||||||
|
"derived-shadow",
|
||||||
|
"{base-shadow}",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rename base-shadow token
|
||||||
|
await renameToken(page, "base-shadow", "foundation-shadow");
|
||||||
|
|
||||||
|
// Check for remapping modal
|
||||||
|
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||||
|
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
|
const cancelButton = remappingModal.getByRole("button", {
|
||||||
|
name: "don't remap",
|
||||||
|
});
|
||||||
|
await cancelButton.click();
|
||||||
|
|
||||||
|
// Verify token was renamed
|
||||||
|
await expect(
|
||||||
|
tokensSidebar.getByRole("button", {
|
||||||
|
name: "foundation-shadow",
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
tokensSidebar.locator('[aria-label="Missing reference"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Cancel process - no changes applied", async ({ page }) => {
|
||||||
|
const { tokensSidebar } = await setupTokensFile(page, {
|
||||||
|
flags: ["enable-token-shadow"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create base shadow token
|
||||||
|
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
|
||||||
|
|
||||||
|
// Create derived shadow token that references base-shadow
|
||||||
|
await createCompositeDerivedToken(
|
||||||
|
page,
|
||||||
|
"Shadow",
|
||||||
|
"derived-shadow",
|
||||||
|
"{base-shadow}",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rename base-shadow token
|
||||||
|
await renameToken(page, "base-shadow", "foundation-shadow");
|
||||||
|
|
||||||
|
// Check for remapping modal
|
||||||
|
const remappingModal = page.getByTestId("token-remapping-modal");
|
||||||
|
await expect(remappingModal).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
|
const closeButton = remappingModal.getByRole("button", {
|
||||||
|
name: "close",
|
||||||
|
});
|
||||||
|
await closeButton.click();
|
||||||
|
|
||||||
|
// Verify original token name still exists
|
||||||
|
await expect(
|
||||||
|
tokensSidebar.getByRole("button", { name: "base-shadow" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
tokensSidebar.getByRole("button", { name: "derived-shadow" }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
[app.main.ui.forms :as fc]
|
[app.main.ui.forms :as fc]
|
||||||
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
|
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
|
||||||
[app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]]
|
[app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]]
|
||||||
[app.main.ui.workspace.tokens.remapping-modal :as remapping-modal]
|
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.forms :as fm]
|
[app.util.forms :as fm]
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
@@ -182,9 +181,33 @@
|
|||||||
(when (or (k/enter? e) (k/space? e))
|
(when (or (k/enter? e) (k/space? e))
|
||||||
(on-cancel e))))
|
(on-cancel e))))
|
||||||
|
|
||||||
|
on-remap-token
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps token)
|
||||||
|
(fn [valid-token name old-name description]
|
||||||
|
(st/emit!
|
||||||
|
(dwtl/update-token (:id token)
|
||||||
|
{:name name
|
||||||
|
:value (:value valid-token)
|
||||||
|
:description description})
|
||||||
|
(remap/remap-tokens old-name name)
|
||||||
|
(dwtp/propagate-workspace-tokens)
|
||||||
|
(modal/hide!))))
|
||||||
|
|
||||||
|
on-rename-token
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps token)
|
||||||
|
(fn [valid-token name description]
|
||||||
|
(st/emit!
|
||||||
|
(dwtl/update-token (:id token)
|
||||||
|
{:name name
|
||||||
|
:value (:value valid-token)
|
||||||
|
:description description})
|
||||||
|
(modal/hide!))))
|
||||||
|
|
||||||
on-submit
|
on-submit
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps validate-token token tokens token-type value-subfield type active-tab)
|
(mf/deps validate-token token tokens token-type value-subfield type active-tab on-remap-token on-rename-token is-create)
|
||||||
(fn [form _event]
|
(fn [form _event]
|
||||||
(let [name (get-in @form [:clean-data :name])
|
(let [name (get-in @form [:clean-data :name])
|
||||||
path (str (d/name token-type) "." name)
|
path (str (d/name token-type) "." name)
|
||||||
@@ -202,22 +225,15 @@
|
|||||||
file-data (dh/lookup-file-data state)
|
file-data (dh/lookup-file-data state)
|
||||||
old-name (:name token)
|
old-name (:name token)
|
||||||
is-rename (and (= action "edit") (not= name old-name))
|
is-rename (and (= action "edit") (not= name old-name))
|
||||||
references-count (remap/count-token-references file-data old-name)]
|
references-count (remap/count-token-references file-data old-name)
|
||||||
|
on-remap #(on-remap-token valid-token name old-name description)
|
||||||
|
on-rename #(on-rename-token valid-token name description)]
|
||||||
(if (and is-rename (> references-count 0))
|
(if (and is-rename (> references-count 0))
|
||||||
(remapping-modal/show-remapping-modal
|
(st/emit! (modal/show :tokens/remapping-confirmation {:old-token-name old-name
|
||||||
{:old-token-name old-name
|
:new-token-name name
|
||||||
:new-token-name name
|
:references-count references-count
|
||||||
:references-count references-count
|
:on-remap on-remap
|
||||||
:on-confirm (fn []
|
:on-rename on-rename}))
|
||||||
(st/emit!
|
|
||||||
(dwtl/update-token (:id token)
|
|
||||||
{:name name
|
|
||||||
:value (:value valid-token)
|
|
||||||
:description description})
|
|
||||||
(remap/remap-tokens old-name name)
|
|
||||||
(dwtp/propagate-workspace-tokens)
|
|
||||||
(modal/hide!)))
|
|
||||||
:on-cancel #(modal/hide!)})
|
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(if is-create
|
(if is-create
|
||||||
(dwtl/create-token (ctob/make-token {:name name
|
(dwtl/create-token (ctob/make-token {:name name
|
||||||
|
|||||||
@@ -295,7 +295,8 @@
|
|||||||
errors?
|
errors?
|
||||||
[:> icon*
|
[:> icon*
|
||||||
{:icon-id i/broken-link
|
{:icon-id i/broken-link
|
||||||
:class (stl/css :token-pill-icon)}]
|
:class (stl/css :token-pill-icon)
|
||||||
|
:aria-label (tr "workspace.tokens.missing-reference")}]
|
||||||
|
|
||||||
color
|
color
|
||||||
[:> swatch* {:background color
|
[:> swatch* {:background color
|
||||||
|
|||||||
@@ -11,22 +11,15 @@
|
|||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||||
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||||
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
|
[app.main.ui.ds.foundations.typography :as t]
|
||||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||||
[app.util.dom :as dom]
|
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
|
[app.util.keyboard :as kbd]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(defn show-remapping-modal
|
|
||||||
"Show the token remapping confirmation modal"
|
|
||||||
[{:keys [old-token-name new-token-name references-count on-confirm on-cancel]}]
|
|
||||||
(let [props {:old-token-name old-token-name
|
|
||||||
:new-token-name new-token-name
|
|
||||||
:references-count references-count
|
|
||||||
:on-confirm on-confirm
|
|
||||||
:on-cancel on-cancel}]
|
|
||||||
(st/emit! (modal/show :tokens/remapping-confirmation props))))
|
|
||||||
|
|
||||||
(defn hide-remapping-modal
|
(defn hide-remapping-modal
|
||||||
"Hide the token remapping confirmation modal"
|
"Hide the token remapping confirmation modal"
|
||||||
[]
|
[]
|
||||||
@@ -34,73 +27,73 @@
|
|||||||
|
|
||||||
;; Remapping Modal Component
|
;; Remapping Modal Component
|
||||||
(mf/defc token-remapping-modal
|
(mf/defc token-remapping-modal
|
||||||
{::mf/wrap-props false
|
{::mf/register modal/components
|
||||||
::mf/register modal/components
|
|
||||||
::mf/register-as :tokens/remapping-confirmation}
|
::mf/register-as :tokens/remapping-confirmation}
|
||||||
[{:keys [old-token-name new-token-name references-count on-confirm on-cancel]}]
|
[{:keys [old-token-name new-token-name on-remap on-rename]}]
|
||||||
(let [remapping-in-progress* (mf/use-state false)
|
(let [remap-modal (get @st/state :remap-modal)
|
||||||
remapping-in-progress? (deref remapping-in-progress*)
|
|
||||||
|
|
||||||
;; Remap logic on confirm
|
;; Remap logic on confirm
|
||||||
on-confirm-remap
|
confirm-remap
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-confirm remapping-in-progress*)
|
(mf/deps on-remap remap-modal)
|
||||||
(fn [e]
|
(fn []
|
||||||
(dom/prevent-default e)
|
|
||||||
(dom/stop-propagation e)
|
|
||||||
(reset! remapping-in-progress* true)
|
|
||||||
;; Call shared remapping logic
|
;; Call shared remapping logic
|
||||||
(let [state @st/state
|
(let [old-token-name (:old-token-name remap-modal)
|
||||||
remap-modal (:remap-modal state)
|
|
||||||
old-token-name (:old-token-name remap-modal)
|
|
||||||
new-token-name (:new-token-name remap-modal)]
|
new-token-name (:new-token-name remap-modal)]
|
||||||
(st/emit! [:tokens/remap-tokens old-token-name new-token-name]))
|
(st/emit! [:tokens/remap-tokens old-token-name new-token-name]))
|
||||||
(when (fn? on-confirm)
|
(when (fn? on-remap)
|
||||||
(on-confirm))))
|
(on-remap))))
|
||||||
|
|
||||||
on-cancel-remap
|
rename-token
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-cancel)
|
(mf/deps on-rename)
|
||||||
(fn [e]
|
(fn []
|
||||||
(dom/prevent-default e)
|
(when (fn? on-rename)
|
||||||
(dom/stop-propagation e)
|
(on-rename))))
|
||||||
(modal/hide!)
|
|
||||||
(when (fn? on-cancel)
|
cancel-action
|
||||||
(on-cancel))))]
|
(mf/use-fn
|
||||||
|
(fn []
|
||||||
|
(hide-remapping-modal)))
|
||||||
|
|
||||||
|
;; Close modal on Escape key if not in progress
|
||||||
|
on-key-down
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps cancel-action)
|
||||||
|
(fn [event]
|
||||||
|
(when (kbd/enter? event)
|
||||||
|
(cancel-action))))]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-overlay)
|
||||||
|
:on-key-down on-key-down
|
||||||
|
:role "alertdialog"
|
||||||
|
:aria-modal "true"
|
||||||
|
:aria-labelledby "modal-title"}
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
|
||||||
[:div {:class (stl/css :modal-dialog)
|
[:div {:class (stl/css :modal-dialog)
|
||||||
:data-testid "token-remapping-modal"}
|
:data-testid "token-remapping-modal"}
|
||||||
|
[:> icon-button* {:on-click cancel-action
|
||||||
|
:class (stl/css :close-btn)
|
||||||
|
:icon i/close
|
||||||
|
:variant "action"
|
||||||
|
:aria-label (tr "labels.close")}]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-header)}
|
[:div {:class (stl/css :modal-header)}
|
||||||
[:> heading* {:level 2
|
[:> heading* {:level 2
|
||||||
:typography "headline-medium"
|
:id "modal-title"
|
||||||
|
:typography "headline-large"
|
||||||
:class (stl/css :modal-title)}
|
:class (stl/css :modal-title)}
|
||||||
(tr "workspace.tokens.remap-token-references")]]
|
(tr "workspace.tokens.remap-token-references-title" old-token-name new-token-name)]]
|
||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
[:> heading* {:level 3
|
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-effects")]
|
||||||
:typography "title-medium"
|
[:> text* {:as "p" :typography t/body-medium} (tr "workspace.tokens.remap-warning-time")]]
|
||||||
:class (stl/css :modal-msg)}
|
|
||||||
(tr "workspace.tokens.renaming-token-from-to" old-token-name new-token-name)]
|
|
||||||
[:div {:class (stl/css :modal-scd-msg)}
|
|
||||||
(if (> references-count 0)
|
|
||||||
(tr "workspace.tokens.references-found" references-count)
|
|
||||||
(tr "workspace.tokens.no-references-found"))]
|
|
||||||
(when remapping-in-progress?
|
|
||||||
[:> context-notification*
|
|
||||||
{:level :info
|
|
||||||
:appearance :ghost}
|
|
||||||
(tr "workspace.tokens.remapping-in-progress")])]
|
|
||||||
[:div {:class (stl/css :modal-footer)}
|
[:div {:class (stl/css :modal-footer)}
|
||||||
[:div {:class (stl/css :action-buttons)}
|
[:div {:class (stl/css :action-buttons)}
|
||||||
[:> button* {:on-click on-cancel-remap
|
[:> button* {:on-click rename-token
|
||||||
:type "button"
|
:type "button"
|
||||||
:variant "secondary"
|
:variant "secondary"}
|
||||||
:disabled remapping-in-progress?}
|
(tr "workspace.tokens.not-remap")]
|
||||||
(tr "labels.cancel")]
|
[:> button* {:on-click confirm-remap
|
||||||
[:> button* {:on-click on-confirm-remap
|
|
||||||
:type "button"
|
:type "button"
|
||||||
:variant "primary"
|
:variant "primary"}
|
||||||
:disabled remapping-in-progress?}
|
(tr "workspace.tokens.remap")]]]]]))
|
||||||
(if (> references-count 0)
|
|
||||||
(tr "workspace.tokens.remap-and-rename")
|
|
||||||
(tr "workspace.tokens.rename-only"))]]]]]))
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
@use "refactor/common-refactor.scss" as deprecated;
|
@use "refactor/common-refactor.scss" as deprecated;
|
||||||
|
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
|
--modal-title-foreground-color: var(--color-foreground-primary);
|
||||||
|
--modal-text-foreground-color: var(--color-foreground-secondary);
|
||||||
|
|
||||||
@extend .modal-overlay-base;
|
@extend .modal-overlay-base;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -17,16 +20,22 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
inset-block-start: 0;
|
inset-block-start: 0;
|
||||||
height: 100%;
|
block-size: 100%;
|
||||||
width: 100%;
|
inline-size: 100%;
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--overlay-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: $sz-6;
|
||||||
|
inset-inline-end: $sz-6;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-dialog {
|
.modal-dialog {
|
||||||
@extend .modal-container-base;
|
@extend .modal-container-base;
|
||||||
width: 100%;
|
inline-size: 100%;
|
||||||
max-width: 32rem;
|
max-inline-size: 32rem;
|
||||||
max-height: unset;
|
max-block-size: unset;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -45,11 +54,7 @@
|
|||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@include t.use-typography("body-large");
|
@include t.use-typography("body-large");
|
||||||
margin-block-end: var(--sp-xxl);
|
color: var(--modal-text-foreground-color);
|
||||||
padding: var(--sp-xxl) 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--sp-l);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
|
|||||||
@@ -8072,6 +8072,10 @@ msgid "workspace.tokens.missing-references"
|
|||||||
msgstr "Missing token references: "
|
msgstr "Missing token references: "
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
|
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
|
||||||
|
msgid "workspace.tokens.missing-reference"
|
||||||
|
msgstr "Missing reference"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:299
|
||||||
msgid "workspace.tokens.more-options"
|
msgid "workspace.tokens.more-options"
|
||||||
msgstr "Right click to see options"
|
msgstr "Right click to see options"
|
||||||
|
|
||||||
@@ -8162,36 +8166,29 @@ msgstr "Enter a token shadow alias"
|
|||||||
msgid "workspace.tokens.reference-error"
|
msgid "workspace.tokens.reference-error"
|
||||||
msgstr "Reference Errors: "
|
msgstr "Reference Errors: "
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
msgid "workspace.tokens.references-found"
|
msgid "workspace.tokens.remap-token-references-title"
|
||||||
msgstr "%s references found in your design"
|
msgstr "Remap all tokens that use `%s` to `%s`?"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
|
|
||||||
msgid "workspace.tokens.remap-and-rename"
|
|
||||||
msgstr "Remap & Rename"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
|
|
||||||
#, unused
|
|
||||||
msgid "workspace.tokens.remap-explanation"
|
|
||||||
msgstr ""
|
|
||||||
"All references to this token will be automatically updated to use the new "
|
|
||||||
"name."
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
msgid "workspace.tokens.remap-token-references"
|
msgid "workspace.tokens.remap-warning-effects"
|
||||||
msgstr "Remap Token References"
|
msgstr "This will change all layers and references that use the old token name."
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
|
msgid "workspace.tokens.remap-warning-time"
|
||||||
|
msgstr "This action could take a while."
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92
|
||||||
msgid "workspace.tokens.remapping-in-progress"
|
msgid "workspace.tokens.remapping-in-progress"
|
||||||
msgstr "Remapping token references..."
|
msgstr "Remapping token references..."
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:106
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:106
|
||||||
msgid "workspace.tokens.rename-only"
|
msgid "workspace.tokens.not-remap"
|
||||||
msgstr "Rename"
|
msgstr "Don't remap"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:83
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
|
||||||
msgid "workspace.tokens.renaming-token-from-to"
|
msgid "workspace.tokens.remap"
|
||||||
msgstr "Renaming token from '%s' to '%s'"
|
msgstr "Remap tokens"
|
||||||
|
|
||||||
#: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:271, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:445, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:169, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:304, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:225, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:332, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:432, src/app/main/ui/workspace/tokens/management/token_pill.cljs:121
|
#: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:271, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:445, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:169, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:304, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:225, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:332, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:432, src/app/main/ui/workspace/tokens/management/token_pill.cljs:121
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
|
|||||||
@@ -7994,6 +7994,10 @@ msgstr "Line height (multiplicador, px o %) o {alias}"
|
|||||||
msgid "workspace.tokens.missing-references"
|
msgid "workspace.tokens.missing-references"
|
||||||
msgstr "Referencias de tokens no encontradas: "
|
msgstr "Referencias de tokens no encontradas: "
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
|
||||||
|
msgid "workspace.tokens.missing-reference"
|
||||||
|
msgstr "Referencia no encontrada"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
|
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
|
||||||
msgid "workspace.tokens.more-options"
|
msgid "workspace.tokens.more-options"
|
||||||
msgstr "Click derecho para ver opciones"
|
msgstr "Click derecho para ver opciones"
|
||||||
@@ -8063,36 +8067,29 @@ msgstr "La referencia no es válida o no se encuentra en ningún set activo."
|
|||||||
msgid "workspace.tokens.reference-error"
|
msgid "workspace.tokens.reference-error"
|
||||||
msgstr "Errores en referencias: "
|
msgstr "Errores en referencias: "
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:86
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
msgid "workspace.tokens.references-found"
|
msgid "workspace.tokens.remap-token-references-title"
|
||||||
msgstr "%s referencias encontradas en tu diseño"
|
msgstr "¿Actualizar todas las referencias de `%s` a `%s`?"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
|
|
||||||
msgid "workspace.tokens.remap-and-rename"
|
|
||||||
msgstr "Actualizar referencias y renombrar"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs
|
|
||||||
#, unused
|
|
||||||
msgid "workspace.tokens.remap-explanation"
|
|
||||||
msgstr ""
|
|
||||||
"Todas las referencias a este token se actualizarán automáticamente para "
|
|
||||||
"usar el nuevo nombre."
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
msgid "workspace.tokens.remap-token-references"
|
msgid "workspace.tokens.remap-warning-effects"
|
||||||
msgstr "Actualizar referencias de token"
|
msgstr "Esta acción actualizará todas las capas y referencias que usen el token antiguo"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:78
|
||||||
|
msgid "workspace.tokens.remap-warning-time"
|
||||||
|
msgstr "Este proceso puede durar un poco"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:92
|
||||||
msgid "workspace.tokens.remapping-in-progress"
|
msgid "workspace.tokens.remapping-in-progress"
|
||||||
msgstr "Actualizando referencias de token..."
|
msgstr "Actualizando referencias de token..."
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:106
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:106
|
||||||
msgid "workspace.tokens.rename-only"
|
msgid "workspace.tokens.not-remap"
|
||||||
msgstr "Renombrar"
|
msgstr "No actualizar"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:83
|
#: src/app/main/ui/workspace/tokens/remapping_modal.cljs:105
|
||||||
msgid "workspace.tokens.renaming-token-from-to"
|
msgid "workspace.tokens.remap"
|
||||||
msgstr "Renombrando el token de '%s' a '%s'"
|
msgstr "Actualizar tokens"
|
||||||
|
|
||||||
#: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:271, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:445, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:169, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:304, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:225, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:332, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:432, src/app/main/ui/workspace/tokens/management/token_pill.cljs:121
|
#: src/app/main/data/workspace/tokens/warnings.cljs:15, src/app/main/data/workspace/tokens/warnings.cljs:19, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:56, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:84, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:103, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:271, src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:445, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:169, src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs:304, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:225, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:332, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:432, src/app/main/ui/workspace/tokens/management/token_pill.cljs:121
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
|
|||||||
Reference in New Issue
Block a user