Compare commits

..

11 Commits

Author SHA1 Message Date
Yamila Moreno
bca41e5dc4 🔧 Fix nginx entrypoint 2025-12-22 09:07:33 +01:00
Marina López
1c706cffb3 Add create org link 2025-12-22 09:07:33 +01:00
Yamila Moreno
361d9305ac 🔧 Add control-center to nginx 2025-12-22 09:07:33 +01:00
Pablo Alba
1911f98389 ♻️ Cleanup unused imports 2025-12-22 09:07:33 +01:00
Juanfran
af670370a2 ♻️ Change Nitrate organization-id schema to text 2025-12-22 09:07:33 +01:00
Pablo Alba
0baf755b19 Move nitrate url to an env variable 2025-12-22 09:07:33 +01:00
Pablo Alba
eb6cb11834 Add photoUrl to profile on nitrate authenticate 2025-12-22 09:07:33 +01:00
Pablo Alba
aaa89436b5 Add retry and validation to nitrate module 2025-12-22 09:07:33 +01:00
Pablo Alba
e52c64f676 Add nitrate to tmux devenv 2025-12-22 09:07:33 +01:00
Pablo Alba
7ab3c826bb 🐛 Fix nitrate get-teams returns deleted teams 2025-12-22 09:07:33 +01:00
Pablo Alba
9552741936 🎉 Integration with nitrate platform 2025-12-22 09:07:32 +01:00
145 changed files with 3246 additions and 5593 deletions

View File

@@ -1,14 +0,0 @@
name: _STAGING RENDER
on:
schedule:
- cron: '36 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "staging-render"
build_wasm: "yes"
build_storybook: "yes"

View File

@@ -33,7 +33,7 @@ jobs:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
🐳 *[PENPOT] Docker image available: {{ github.ref_name }}*
🐳 *[PENPOT] Docker image available.*
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

2
.gitignore vendored
View File

@@ -20,6 +20,7 @@
.rebel_readline_history
.repl
.shadow-cljs
.pnpm-store/
/*.jpg
/*.md
/*.png
@@ -71,6 +72,7 @@
/library/target/
/library/*.zip
/external
/penpot-nitrate
clj-profiler/
node_modules

View File

@@ -12,16 +12,13 @@
### :sparkles: New features & Enhancements
- Add new Box Shadow Tokens [Taiga #10201](https://tree.taiga.io/project/penpot/us/10201)
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
- Add deleted files to dashboard [Taiga #8149](https://tree.taiga.io/project/penpot/us/8149)
### :bug: Bugs fixed
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
- Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917)
## 2.12.0 (Unreleased)
@@ -86,7 +83,6 @@ example. It's still usable as before, we just removed the example.
- Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome)
- Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887)
- Enable Hindi translations on the application
### :sparkles: New features & Enhancements
@@ -120,7 +116,6 @@ example. It's still usable as before, we just removed the example.
- Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841)
- Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492)
- Fix horizontal scroll on layer panel [Taiga #12843](https://tree.taiga.io/project/penpot/issue/12843)
- Fix unicode handling on email template abbreviation filter [Github #7966](https://github.com/penpot/penpot/pull/7966)
## 2.11.1

View File

@@ -240,4 +240,4 @@
</div>
</body>
</html>
</html>

View File

@@ -36,7 +36,8 @@ export PENPOT_FLAGS="\
enable-file-validation \
enable-file-schema-validation \
enable-redis-cache \
enable-subscriptions";
enable-subscriptions \
enable-nitrate";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"
@@ -55,6 +56,8 @@ export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
export PENPOT_NITRATE_BACKEND_URI=http://localhost:3000
export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Djdk.attach.allowAttachSelf \

View File

@@ -331,81 +331,6 @@
(set/difference cfeat/backend-only-features))
#{}))))
(defn check-file-exists
[cfg id & {:keys [include-deleted?]
:or {include-deleted? false}
:as options}]
(db/get-with-sql cfg [sql:get-minimal-file id]
{:db/remove-deleted (not include-deleted?)}))
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
inner join file as f on (f.id = fpr.file_id)
where fpr.file_id = ?
and fpr.profile_id = ?
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?")
(defn- get-file-permissions*
[conn profile-id file-id]
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])))
(defn get-file-permissions
([conn profile-id file-id]
(let [rows (get-file-permissions* conn profile-id file-id)
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true
:is-logged (some? profile-id)})))
([conn profile-id file-id share-id]
(let [perms (get-file-permissions conn profile-id file-id)
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
(dissoc :flags)
(update :pages db/decode-pgarray #{}))]
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
;; will probably need to tweak this function in order to expose
;; this flags to the frontend.
(cond
(some? perms) perms
(some? ldata) {:type :share-link
:can-read true
:pages (:pages ldata)
:is-logged (some? profile-id)
:who-comment (:who-comment ldata)
:who-inspect (:who-inspect ldata)}))))
(defn get-project
[cfg project-id]
(db/get cfg :project {:id project-id}))

View File

@@ -821,10 +821,9 @@
entries (keep (match-storage-entry-fn) entries)]
(doseq [{:keys [id entry]} entries]
(let [object (-> (read-entry input entry)
(decode-storage-object)
(update :bucket d/nilv sto/default-bucket)
(validate-storage-object))
(let [object (->> (read-entry input entry)
(decode-storage-object)
(validate-storage-object))
ext (cmedia/mtype->extension (:content-type object))
path (str "objects/" id ext)

View File

@@ -225,6 +225,8 @@
[:netty-io-threads {:optional true} ::sm/int]
[:executor-threads {:optional true} ::sm/int]
[:nitrate-backend-uri {:optional true} ::sm/uri]
;; DEPRECATED
[:assets-storage-backend {:optional true} :keyword]
[:storage-assets-fs-directory {:optional true} :string]

View File

@@ -30,7 +30,7 @@
(defn- get-file-media-object
[pool id]
(db/get pool :file-media-object {:id id} {::db/remove-deleted false}))
(db/get pool :file-media-object {:id id}))
(defn- serve-object-from-s3
[{:keys [::sto/storage] :as cfg} obj]

View File

@@ -309,7 +309,7 @@
(fn [request]
(let [key (yreq/get-header request "x-shared-key")]
(if (= key shared-key)
(handler (assoc request ::http/auth-with-shared-key true))
(handler request)
{::yres/status 403}))))
(fn [_ _]
{::yres/status 403})))

View File

@@ -323,6 +323,7 @@
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::rds/pool (ig/ref ::rds/pool)
:app.nitrate/client (ig/ref :app.nitrate/client)
::wrk/executor (ig/ref ::wrk/netty-executor)
::session/manager (ig/ref ::session/manager)
::ldap/provider (ig/ref ::ldap/provider)
@@ -339,6 +340,9 @@
::email/blacklist (ig/ref ::email/blacklist)
::email/whitelist (ig/ref ::email/whitelist)}
:app.nitrate/client
{::http.client/client (ig/ref ::http.client/client)}
:app.rpc/management-methods
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
@@ -348,6 +352,7 @@
::sto/storage (ig/ref ::sto/storage)
::mtx/metrics (ig/ref ::mtx/metrics)
::mbus/msgbus (ig/ref ::mbus/msgbus)
:app.nitrate/client (ig/ref :app.nitrate/client)
::rds/client (ig/ref ::rds/client)
::setup/props (ig/ref ::setup/props)}

123
backend/src/app/nitrate.clj Normal file
View File

@@ -0,0 +1,123 @@
;; 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
"Module that make calls to the external nitrate aplication"
(:require
[app.common.logging :as l]
[app.common.schema :as sm]
[app.config :as cf]
[app.http.client :as http]
[app.rpc :as-alias rpc]
[app.setup :as-alias setup]
[app.util.json :as json]
[clojure.core :as c]
[integrant.core :as ig]))
(def baseuri (cf/get :nitrate-backend-uri))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- coercer
[schema & {:as opts}]
(let [decode-fn (sm/decoder schema sm/json-transformer)
check-fn (sm/check-fn schema opts)]
(fn [data]
(-> data decode-fn check-fn))))
(defn- request-builder
[cfg method uri management-key profile-id]
(fn []
(http/req! cfg {:method method
:headers {"content-type" "application/json"
"accept" "application/json"
"x-shared-key" management-key
"x-profile-id" (str profile-id)}
:uri uri
:version :http1.1})))
(defn- with-retries
[handler max-retries]
(fn []
(loop [attempt 1]
(let [result (try
(handler)
(catch Exception e
(if (< attempt max-retries)
::retry
(do
;; TODO Error handling
(l/error :hint "request fail after multiple retries" :cause e)
nil))))]
(if (= result ::retry)
(recur (inc attempt))
result)))))
(defn- with-validate [handler uri schema]
(fn []
(let [coercer-http (coercer schema
:type :validation
:hint (str "invalid data received calling " uri))]
(try
(coercer-http (-> (handler) :body json/decode))
(catch Exception e
;; TODO Error handling
(l/error :hint "error validating json response" :cause e)
nil)))))
(defn- request-to-nitrate
[{:keys [::management-key] :as cfg} method uri schema {:keys [::rpc/profile-id] :as params}]
(let [full-http-call (-> (request-builder cfg method uri management-key profile-id)
(with-retries 3)
(with-validate uri schema))]
(full-http-call)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn call
[cfg method params]
(when (contains? cf/flags :nitrate)
(let [client (get cfg ::client)
method (get client method)]
(method params))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:organization
[:map
[:id ::sm/text]
[:name ::sm/text]])
(defn- get-team-org
[cfg {:keys [team-id] :as params}]
(request-to-nitrate cfg :get (str baseuri "/api/teams/" (str team-id)) schema:organization params))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INITIALIZATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/init-key ::client
[_ {:keys [::setup/props] :as cfg}]
(if (contains? cf/flags :nitrate)
(let [management-key (or (cf/get :management-api-key)
(get props :management-key))
cfg (assoc cfg ::management-key management-key)]
{:get-team-org (partial get-team-org cfg)})
{}))
(defmethod ig/halt-key! ::client
[_ {:keys []}]
(do :stuff))

View File

@@ -14,7 +14,6 @@
[app.common.spec :as us]
[app.common.time :as ct]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http :as-alias http]
@@ -93,11 +92,7 @@
(let [handler-name (:type path-params)
etag (yreq/get-header request "if-none-match")
profile-id (or (::session/profile-id request)
(::actoken/profile-id request)
(if (::http/auth-with-shared-key request)
uuid/zero
nil))
(::actoken/profile-id request))
ip-addr (inet/parse-request request)
data (-> params
@@ -301,6 +296,7 @@
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
(->> (sv/scan-ns
'app.rpc.management.subscription
'app.rpc.management.nitrate
'app.rpc.management.exporter)
(map (partial process-method cfg "management" wrap-management))
(into {}))))

View File

@@ -307,8 +307,7 @@
:content-type (:mtype input)})]
(:id sobject))
(catch Throwable cause
(l/wrn :hint "unable to import profile picture"
:uri uri
(l/err :hint "unable to import profile picture"
:cause cause)
nil)))

View File

@@ -79,14 +79,85 @@
;; --- FILE PERMISSIONS
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
inner join file as f on (f.id = fpr.file_id)
where fpr.file_id = ?
and fpr.profile_id = ?
and f.deleted_at is null
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
and f.deleted_at is null
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?
and f.deleted_at is null")
(defn get-file-permissions
[conn profile-id file-id]
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])))
(defn get-permissions
([conn profile-id file-id]
(let [rows (get-file-permissions conn profile-id file-id)
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true
:is-logged (some? profile-id)})))
([conn profile-id file-id share-id]
(let [perms (get-permissions conn profile-id file-id)
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
(dissoc :flags)
(update :pages db/decode-pgarray #{}))]
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
;; will probably need to tweak this function in order to expose
;; this flags to the frontend.
(cond
(some? perms) perms
(some? ldata) {:type :share-link
:can-read true
:pages (:pages ldata)
:is-logged (some? profile-id)
:who-comment (:who-comment ldata)
:who-inspect (:who-inspect ldata)}))))
(def has-edit-permissions?
(perms/make-edition-predicate-fn bfc/get-file-permissions))
(perms/make-edition-predicate-fn get-permissions))
(def has-read-permissions?
(perms/make-read-predicate-fn bfc/get-file-permissions))
(perms/make-read-predicate-fn get-permissions))
(def has-comment-permissions?
(perms/make-comment-predicate-fn bfc/get-file-permissions))
(perms/make-comment-predicate-fn get-permissions))
(def check-edition-permissions!
(perms/make-check-fn has-edit-permissions?))
@@ -99,7 +170,7 @@
(defn check-comment-permissions!
[conn profile-id file-id share-id]
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
(let [perms (get-permissions conn profile-id file-id share-id)
can-read (has-read-permissions? perms)
can-comment (has-comment-permissions? perms)]
(when-not (or can-read can-comment)
@@ -151,7 +222,7 @@
(defn- get-minimal-file-with-perms
[cfg {:keys [:id ::rpc/profile-id]}]
(let [mfile (get-minimal-file cfg id)
perms (bfc/get-file-permissions cfg profile-id id)]
perms (get-permissions cfg profile-id id)]
(assoc mfile :permissions perms)))
(defn get-file-etag
@@ -177,7 +248,7 @@
;; will be already prefetched and we just reuse them instead
;; of making an additional database queries.
(let [perms (or (:permissions (::cond/object params))
(bfc/get-file-permissions conn profile-id id))]
(get-permissions conn profile-id id))]
(check-read-permissions! perms)
(let [team (teams/get-team conn
@@ -240,7 +311,7 @@
::sm/result schema:file-fragment}
[cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}]
(db/run! cfg (fn [cfg]
(let [perms (bfc/get-file-permissions cfg profile-id file-id share-id)]
(let [perms (get-permissions cfg profile-id file-id share-id)]
(check-read-permissions! perms)
(-> (get-file-fragment cfg file-id fragment-id)
(rph/with-http-cache long-cache-duration))))))
@@ -385,7 +456,8 @@
:code :params-validation
:hint "page-id is required when object-id is provided"))
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
(let [perms (get-permissions conn profile-id file-id share-id)
file (bfc/get-file cfg file-id :read-only? true)
proj (db/get conn :project {:id (:project-id file)})
@@ -616,10 +688,11 @@
"Get libraries used by the specified file."
{::doc/added "1.17"
::sm/params schema:get-file-libraries}
[cfg {:keys [::rpc/profile-id file-id]}]
(bfc/check-file-exists cfg file-id)
(check-read-permissions! cfg profile-id file-id)
(bfc/get-file-libraries cfg file-id))
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
(bfc/get-file-libraries conn file-id)))
;; --- COMMAND QUERY: Files that use this File library
@@ -704,6 +777,7 @@
f.created_at,
f.modified_at,
f.name,
f.is_shared,
f.deleted_at AS will_be_deleted_at,
ft.media_id AS thumbnail_id,
row_number() OVER w AS row_num,
@@ -711,7 +785,8 @@
FROM file AS f
INNER JOIN project AS p ON (p.id = f.project_id)
LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id
AND ft.revn = f.revn)
AND ft.revn = f.revn
AND ft.deleted_at is null)
WHERE p.team_id = ?
AND (p.deleted_at > ?::timestamptz OR
f.deleted_at > ?::timestamptz)
@@ -813,7 +888,7 @@
AND (f.deleted_at IS NULL OR f.deleted_at > now())
ORDER BY f.created_at ASC;")
(defn- absorb-library-by-file
(defn- absorb-library-by-file!
[cfg ldata file-id]
(assert (db/connection-map? cfg)
@@ -837,7 +912,7 @@
:modified-at (ct/now)
:has-media-trimmed false}))))
(defn- absorb-library*
(defn- absorb-library
"Find all files using a shared library, and absorb all library assets
into the file local libraries"
[cfg {:keys [id data] :as library}]
@@ -852,10 +927,10 @@
:library-id (str id)
:files (str/join "," (map str ids)))
(run! (partial absorb-library-by-file cfg data) ids)
(run! (partial absorb-library-by-file! cfg data) ids)
library))
(defn absorb-library
(defn absorb-library!
[{:keys [::db/conn] :as cfg} id]
(let [file (-> (bfc/get-file cfg id
:realize? true
@@ -872,7 +947,7 @@
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-file-features! (:features file)))
(absorb-library* cfg file)))
(absorb-library cfg file)))
(defn- set-file-shared
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
@@ -885,14 +960,14 @@
;; file, we need to perform more complex operation,
;; so in this case we retrieve the complete file and
;; perform all required validations.
(let [file (-> (absorb-library cfg id)
(let [file (-> (absorb-library! cfg id)
(assoc :is-shared false))]
(db/delete! conn :file-library-rel {:library-file-id id})
(db/update! conn :file
{:is-shared false
:modified-at (ct/now)}
{:id id})
file)
(select-keys file [:id :name :is-shared]))
(and (false? (:is-shared file))
(true? (:is-shared params)))
@@ -939,11 +1014,6 @@
{:id file-id}
{::db/return-keys [:id :name :is-shared :deleted-at
:project-id :created-at :modified-at]})]
;; Remove all possible relations for that file
(db/delete! conn :file-library-rel
{:library-file-id file-id})
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
@@ -1094,53 +1164,47 @@
;; --- MUTATION COMMAND: delete-files-immediatelly
(def ^:private sql:get-delete-team-files-candidates
"SELECT f.id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
JOIN team AS t ON (t.id = p.team_id)
WHERE t.deleted_at IS NULL
AND t.id = ?
AND f.id = ANY(?::uuid[])")
(def ^:private sql:delete-team-files
"UPDATE file AS uf SET deleted_at = ?::timestamptz
FROM (
SELECT f.id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
JOIN team AS t ON (t.id = p.team_id)
WHERE t.deleted_at IS NULL
AND t.id = ?
AND f.id = ANY(?::uuid[])
) AS subquery
WHERE uf.id = subquery.id
RETURNING uf.id, uf.deleted_at;")
(def ^:private schema:permanently-delete-team-files
[:map {:title "permanently-delete-team-files"}
[:team-id ::sm/uuid]
[:ids [::sm/set ::sm/uuid]]])
(defn- permanently-delete-team-files
[{:keys [::db/conn]} {:keys [::rpc/request-at team-id ids]}]
(let [ids (into #{}
d/xf:map-id
(db/exec! conn [sql:get-delete-team-files-candidates team-id
(db/create-array conn "uuid" ids)]))]
(reduce (fn [acc id]
(events/tap :progress {:file-id id :index (inc (count acc)) :total (count ids)})
(db/update! conn :file
{:deleted-at request-at}
{:id id}
{::db/return-keys false})
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at request-at
:id id}})
(conj acc id))
#{}
ids)))
(sv/defmethod ::permanently-delete-team-files
"Mark the specified files to be deleted immediatelly on the
specified team. The team-id on params will be used to filter and
check writable permissons on team."
{::doc/added "2.13"
::sm/params schema:permanently-delete-team-files}
{::doc/added "2.12"
::sm/params schema:permanently-delete-team-files
::db/transaction true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(teams/check-edition-permissions! pool profile-id team-id)
(sse/response #(db/tx-run! cfg permanently-delete-team-files params)))
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at team-id ids]}]
(teams/check-edition-permissions! conn profile-id team-id)
(reduce (fn [acc {:keys [id deleted-at]}]
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at deleted-at
:id id}})
(conj acc id))
#{}
(db/plan conn [sql:delete-team-files request-at team-id
(db/create-array conn "uuid" ids)])))
;; --- MUTATION COMMAND: restore-files-immediatelly
@@ -1204,7 +1268,7 @@
{:keys [files projects]}
(reduce (fn [result {:keys [id project-id]}]
(let [index (-> result :files count)]
(events/tap :progress {:file-id id :index (inc index) :total total-files})
(events/tap :progress {:file-id id :index index :total total-files})
(restore-file conn id)
(-> result
@@ -1227,7 +1291,7 @@
(sv/defmethod ::restore-deleted-team-files
"Removes the deletion mark from the specified files (and respective
projects) on the specified team."
{::doc/added "2.13"
{::doc/added "2.12"
::sse/stream? true
::sm/params schema:restore-deleted-team-files}
[cfg params]

View File

@@ -199,13 +199,15 @@
[cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}]
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-read-permissions! conn profile-id file-id)
(let [team (teams/get-team conn
:profile-id profile-id
:file-id file-id)
file (bfc/get-file cfg file-id
:include-deleted? true
:realize? true
:read-only? true)
strip-frames-with-thumbnails
(or (nil? strip-frames-with-thumbnails) ;; if not present, default to true
(true? strip-frames-with-thumbnails))]
@@ -331,16 +333,12 @@
;; --- MUTATION COMMAND: create-file-thumbnail
(defn- create-file-thumbnail
[{:keys [::db/conn ::sto/storage] :as cfg} {:keys [file-id revn props media] :as params}]
(defn- create-file-thumbnail!
[{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props media] :as params}]
(media/validate-media-type! media)
(media/validate-media-size! media)
(let [file (bfc/get-file cfg file-id
:include-deleted? true
:load-data? false)
props (db/tjson (or props {}))
(let [props (db/tjson (or props {}))
path (:path media)
mtype (:mtype media)
hash (sto/calculate-hash path)
@@ -369,7 +367,7 @@
(db/update! conn :file-thumbnail
{:media-id (:id media)
:deleted-at (:deleted-at file)
:deleted-at nil
:updated-at tnow
:props props}
{:file-id file-id
@@ -380,7 +378,6 @@
:revn revn
:created-at tnow
:updated-at tnow
:deleted-at (:deleted-at file)
:props props
:media-id (:id media)}))
@@ -405,8 +402,6 @@
::rtry/when rtry/conflict-exception?
::sm/params schema:create-file-thumbnail}
;; FIXME: do not run the thumbnail upload inside a transaction
[cfg {:keys [::rpc/profile-id file-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
;; TODO For now we check read permissions instead of write,
@@ -414,6 +409,6 @@
;; review this approach on the future.
(files/check-read-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(let [media (create-file-thumbnail cfg params)]
(let [media (create-file-thumbnail! cfg params)]
{:uri (files/resolve-public-uri (:id media))
:id (:id media)})))))

View File

@@ -6,7 +6,6 @@
(ns app.rpc.commands.fonts
(:require
[app.binfile.common :as bfc]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.schema :as sm]
@@ -67,7 +66,7 @@
(uuid? file-id)
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})
perms (bfc/get-file-permissions conn profile-id file-id share-id)]
perms (files/get-permissions conn profile-id file-id share-id)]
(files/check-read-permissions! perms)
(db/query conn :team-font-variant
{:team-id (:team-id project)

View File

@@ -23,6 +23,7 @@
[app.main :as-alias main]
[app.media :as media]
[app.msgbus :as mbus]
[app.nitrate :as nitrate]
[app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
@@ -172,6 +173,12 @@
(map decode-row)
(map process-permissions)))
(defn- add-org-to-team
[cfg team params]
(let [params (assoc (or params {}) :team-id (:id team))
org (nitrate/call cfg :get-team-org params)]
(assoc team :organization-id (:id org) :organization-name (:name org))))
(defn get-teams
[conn profile-id]
(let [profile (profile/get-profile conn profile-id)
@@ -190,7 +197,9 @@
::sm/params schema:get-teams}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(dm/with-open [conn (db/open pool)]
(get-teams conn profile-id)))
(cond->> (get-teams conn profile-id)
(contains? cf/flags :nitrate)
(map #(add-org-to-team cfg % params)))))
(def ^:private sql:get-owned-teams
"SELECT t.id, t.name,

View File

@@ -13,6 +13,7 @@
[app.config :as cf]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc]
@@ -120,7 +121,7 @@
[system {:keys [::rpc/profile-id file-id share-id] :as params}]
(db/run! system
(fn [{:keys [::db/conn] :as system}]
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
(let [perms (files/get-permissions conn profile-id file-id share-id)
params (-> params
(assoc ::perms perms)
(assoc :profile-id profile-id))]

View File

@@ -0,0 +1,112 @@
;; 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.nitrate
"Internal Nitrate HTTP API.
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
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as doc]
[app.util.services :as sv]))
;; ---- API: authenticate
(def ^:private schema:profile
[:map
[:id ::sm/uuid]
[:name :string]
[:email :string]
[:photo-url :string]])
(sv/defmethod ::authenticate
"Authenticate an user
@api GET /authenticate
@returns
200 OK: Returns the authenticated user."
{::doc/added "2.12"
::sm/result schema:profile}
[cfg {:keys [::rpc/profile-id] :as params}]
(let [profile (profile/get-profile cfg profile-id)]
{:id (get profile :id)
:name (get profile :fullname)
:email (get profile :email)
:photo-url (files/resolve-public-uri (get profile :photo-id))}))
;; ---- API: get-teams
(def ^:private sql:get-teams
"SELECT t.*
FROM team AS t
JOIN team_profile_rel AS tpr ON t.id = tpr.team_id
WHERE tpr.profile_id = ?
AND tpr.is_owner = 't'
AND t.is_default = 'f'
AND t.deleted_at is null;")
(def ^:private schema:team
[:map
[:id ::sm/uuid]
[:name :string]])
(def ^:private schema:get-teams-result
[:vector schema:team])
(sv/defmethod ::get-teams
"List teams for which current user is owner.
@api GET /get-teams
@returns
200 OK: Returns the list of teams for the user."
{::doc/added "2.12"
::sm/result schema:get-teams-result}
[cfg {:keys [::rpc/profile-id]}]
(when (contains? cf/flags :nitrate)
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
(->> (db/exec! cfg [sql:get-teams current-user-id])
(map #(select-keys % [:id :name]))))))
;; ---- API: notify-team-change
(def ^:private schema:notify-team-change
[:map
[:id ::sm/uuid]
[:organization-id ::sm/text]])
(sv/defmethod ::notify-team-change
"Notify to Penpot a team change from nitrate
@api POST /notify-team-change
@returns
200 OK"
{::doc/added "2.12"
::sm/params schema:notify-team-change
::rpc/auth false}
[cfg {:keys [id organization-id organization-name]}]
(when (contains? cf/flags :nitrate)
(let [msgbus (::mbus/msgbus cfg)]
(mbus/pub! msgbus
;;TODO There is a bug on dashboard with teams notifications.
;;For now we send it to uuid/zero instead of team-id
:topic uuid/zero
:message {:type :team-org-change
:team-id id
:organization-id organization-id
:organization-name organization-name}))))

View File

@@ -104,29 +104,28 @@
(def ^:private schema:limit
[:and
[:map
[::name :keyword]
[::name :any]
[::strategy schema:strategy]
[::key :string]
[::opts :string]
[::capacity {:optional true} ::sm/int]
[::rate {:optional true} ::sm/int]
[::interval {:optional true} ::ct/duration]
[::params {:optional true} [::sm/vec :any]]
[::permits {:optional true} ::sm/int]
[::unit {:optional true} [:enum :days :hours :minutes :seconds :weeks]]]
[:fn (fn [attrs]
(let [contains-fn (partial contains? attrs)]
(or (every? contains-fn [::capacity ::rate ::interval])
(every? contains-fn [::permits ::unit]))))]])
[::opts :string]]
[:or
[:map
[::capacity ::sm/int]
[::rate ::sm/int]
[::internal ::ct/duration]
[::params [::sm/vec :any]]]
[:map
[::nreq ::sm/int]
[::unit [:enum :days :hours :minutes :seconds :weeks]]]]])
(def ^:private schema:limits
[:map-of :keyword [::sm/vec schema:limit]])
(def ^:private valid-limit-tuple?
(sm/validator schema:limit-tuple))
(sm/lazy-validator schema:limit-tuple))
(def ^:private valid-rlimit-instance?
(sm/validator ::rpc/rlimit))
(sm/lazy-validator ::rpc/rlimit))
(defmethod parse-limit :window
[[name strategy opts :as vlimit]]
@@ -135,16 +134,16 @@
(merge
{::name name
::strategy strategy}
(if-let [[_ permits unit] (re-find window-opts-re opts)]
(let [permits (parse-long permits)]
{::permits permits
(if-let [[_ nreq unit] (re-find window-opts-re opts)]
(let [nreq (parse-long nreq)]
{::nreq nreq
::unit (case unit
"d" :days
"h" :hours
"m" :minutes
"s" :seconds
"w" :weeks)
::key (str "penpot.rlimit." (cf/get :tenant) ".window." (d/name name))
::key (str "ratelimit.window." (d/name name))
::opts opts})
(ex/raise :type :validation
:code :invalid-window-limit-opts
@@ -165,15 +164,15 @@
::interval interval
::opts opts
::params [(->seconds interval) rate capacity]
::key (str "penpot.rlimit." (cf/get :tenant) ".bucket." (d/name name))})
::key (str "ratelimit.bucket." (d/name name))})
(ex/raise :type :validation
:code :invalid-bucket-limit-opts
:hint (str/ffmt "looks like '%' does not have a valid format" opts))))
(defmethod process-limit :bucket
[rconn profile-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
[rconn user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
(let [script (-> bucket-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." profile-id)])
(assoc ::rscript/keys [(str key "." service "." user-id)])
(assoc ::rscript/vals (conj params (->seconds now))))
result (rds/eval rconn script)
allowed? (boolean (nth result 0))
@@ -193,18 +192,18 @@
(assoc ::lresult/remaining remaining))))
(defmethod process-limit :window
[rconn profile-id now {:keys [::permits ::unit ::key ::service] :as limit}]
[rconn user-id now {:keys [::nreq ::unit ::key ::service] :as limit}]
(let [ts (ct/truncate now unit)
ttl (ct/diff now (ct/plus ts {unit 1}))
script (-> window-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." profile-id "." (ct/format-inst ts))])
(assoc ::rscript/vals [permits (->seconds ttl)]))
(assoc ::rscript/keys [(str key "." service "." user-id "." (ct/format-inst ts))])
(assoc ::rscript/vals [nreq (->seconds ttl)]))
result (rds/eval rconn script)
allowed? (boolean (nth result 0))
remaining (nth result 1)]
(l/trace :hint "limit processed"
:service service
:name (name (::name limit))
:limit (name (::name limit))
:strategy (name (::strategy limit))
:opts (::opts limit)
:allowed allowed?
@@ -215,8 +214,8 @@
(assoc ::lresult/reset (ct/plus ts {unit 1})))))
(defn- process-limits
[rconn profile-id limits now]
(let [results (into [] (map (partial process-limit rconn profile-id now)) limits)
[rconn user-id limits now]
(let [results (into [] (map (partial process-limit rconn user-id now)) limits)
remaining (->> results
(d/index-by ::name ::lresult/remaining)
(uri/map->query-string))
@@ -228,7 +227,7 @@
(when rejected
(l/warn :hint "rejected rate limit"
:profile-id (str profile-id)
:user-id (str user-id)
:limit-service (-> rejected ::service name)
:limit-name (-> rejected ::name name)
:limit-strategy (-> rejected ::strategy name)))
@@ -372,9 +371,12 @@
(defn- on-refresh-error
[_ cause]
(when-not (instance? java.util.concurrent.RejectedExecutionException cause)
(l/warn :hint "unexpected exception on loading config"
:cause cause
::l/sync? true)))
(if-let [explain (-> cause ex-data ex/explain)]
(l/warn ::l/raw (str "unable to refresh config, invalid format:\n" explain)
::l/sync? true)
(l/warn :hint "unexpected exception on loading config"
:cause cause
::l/sync? true))))
(defn- get-config-path
[]

View File

@@ -25,9 +25,9 @@ local allowed = filled >= requested
local newTokens = filled
if allowed then
newTokens = filled - requested
redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp)
end
redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp)
redis.call("expire", tokensKey, ttl)
return { allowed, newTokens }

View File

@@ -35,9 +35,6 @@
:assets-s3 :s3
nil)))
(def default-bucket
"file-media-object")
(def valid-buckets
#{"file-media-object"
"team-font-variant"

View File

@@ -25,7 +25,7 @@
[app.common.time :as ct]
[app.config :as cf]
[app.db :as db]
[app.storage :as sto]
[app.storage :as-alias sto]
[app.storage.impl :as impl]
[integrant.core :as ig]))
@@ -130,7 +130,7 @@
[{:keys [metadata]}]
(or (some-> metadata :bucket)
(some-> metadata :reference d/name)
sto/default-bucket))
"file-media-object"))
(defn- process-objects!
[conn has-refs? bucket objects]

View File

@@ -45,8 +45,7 @@
:deleted-at (ct/format-inst deleted-at))
(db/update! conn :file
{:deleted-at deleted-at
:is-shared false}
{:deleted-at deleted-at}
{:id id}
{::db/return-keys false})
@@ -54,7 +53,7 @@
(not *team-deletion*))
;; NOTE: we don't prevent file deletion on absorb operation failure
(try
(db/tx-run! cfg files/absorb-library id)
(db/tx-run! cfg files/absorb-library! id)
(catch Throwable cause
(l/warn :hint "error on absorbing library"
:file-id id

View File

@@ -7,18 +7,10 @@
(ns app.util.template
(:require
[app.common.exceptions :as ex]
[cuerdas.core :as str]
[selmer.filters :as sf]
[selmer.parser :as sp]))
;; (sp/cache-off!)
(sf/add-filter! :abbreviate
(fn [s n]
(let [n (parse-long n)]
(str/abbreviate s n))))
(defn render
[path context]
(try

View File

@@ -137,34 +137,33 @@ RETURNING task.id, task.queue")
::wait)))
(run-batch []
(try
(let [rconn (rds/connect cfg)]
(try
(-> cfg
(assoc ::rds/conn rconn)
(db/tx-run! run-batch'))
(finally
(.close ^AutoCloseable rconn))))
(let [rconn (rds/connect cfg)]
(try
(-> cfg
(assoc ::rds/conn rconn)
(db/tx-run! run-batch'))
(catch InterruptedException cause
(throw cause))
(catch InterruptedException cause
(throw cause))
(catch Exception cause
(cond
(rds/exception? cause)
(do
(l/wrn :hint "redis exception (will retry in an instant)" :cause cause)
(px/sleep timeout))
(catch Exception cause
(cond
(rds/exception? cause)
(do
(l/wrn :hint "redis exception (will retry in an instant)" :cause cause)
(px/sleep timeout))
(db/sql-exception? cause)
(do
(l/wrn :hint "database exception (will retry in an instant)" :cause cause)
(px/sleep timeout))
(db/sql-exception? cause)
(do
(l/wrn :hint "database exception (will retry in an instant)" :cause cause)
(px/sleep timeout))
:else
(do
(l/err :hint "unhandled exception (will retry in an instant)" :cause cause)
(px/sleep timeout))))
:else
(do
(l/err :hint "unhandled exception (will retry in an instant)" :cause cause)
(px/sleep timeout))))))
(finally
(.close ^AutoCloseable rconn)))))
(dispatcher []
(l/inf :hint "started")
@@ -177,7 +176,7 @@ RETURNING task.id, task.queue")
(catch InterruptedException _
(l/trc :hint "interrupted"))
(catch Throwable cause
(l/err :hint "unexpected exception" :cause cause))
(l/err :hint " unexpected exception" :cause cause))
(finally
(l/inf :hint "terminated"))))]

View File

@@ -595,8 +595,8 @@
(px/exec! :virtual #(rcp/write-body-to-stream body nil output))
(into []
(map (fn [{:keys [event data]}]
(d/vec2 (keyword event)
(tr/decode-str data))))
[(keyword event)
(tr/decode-str data)]))
(parse-sse (slurp' input)))
(finally
(.close input)))))

View File

@@ -1921,11 +1921,7 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (fn? result))
(let [[ev1 ev2 :as events] (th/consume-sse result)]
(t/is (= 2 (count events)))
(t/is (= (:ids data) (val ev2)))))
(t/is (= (:ids data) result)))
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
(t/is (= (:deleted-at row) now)))))))

View File

@@ -29,7 +29,8 @@
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
integrant/integrant {:mvn/version "1.0.0"}
funcool/cuerdas {:mvn/version "2026.415"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2025.06.16-414"}
funcool/promesa
{:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8"
:git/url "https://github.com/funcool/promesa"}

View File

@@ -145,7 +145,10 @@
;; A temporal flag, enables backend code use more extensivelly
;; redis for caching data
:redis-cache})
:redis-cache
;; Activates the nitrate module
:nitrate})
(def all-flags
(set/union email login varia))
@@ -169,7 +172,6 @@
:enable-component-thumbnails
:enable-render-wasm-dpr
:enable-token-color
:enable-token-shadow
:enable-inspect-styles
:enable-feature-fdata-objects-map])

View File

@@ -340,7 +340,7 @@
(dfn-diff t2 t1)))
#?(:cljs
(defn set-default-locale
(defn set-default-locale!
[locale]
(when-let [locale (unchecked-get locales locale)]
(dfn-set-default-options #js {:locale locale}))))

View File

@@ -362,24 +362,24 @@
component (ctkl/get-component component-file (:component-id top-instance) true)
remote-shape (get-ref-shape component-file component shape)
component-container (get-component-container component-file component)
[remote-shape component-container component-file]
[remote-shape component-container]
(if (some? remote-shape)
[remote-shape component-container component-file]
[remote-shape component-container]
;; If not found, try the case of this being a fostered or swapped children
(let [head-instance (ctn/get-head-shape (:objects container) shape)
component-file (get-in libraries [(:component-file head-instance) :data])
head-component (ctkl/get-component component-file (:component-id head-instance) true)
remote-shape' (get-ref-shape component-file head-component shape)
component-container' (get-component-container component-file head-component)]
[remote-shape' component-container' component-file]))]
(let [head-instance (ctn/get-head-shape (:objects container) shape)
component-file (get-in libraries [(:component-file head-instance) :data])
head-component (ctkl/get-component component-file (:component-id head-instance) true)
remote-shape' (get-ref-shape component-file head-component shape)
component-container (get-component-container component-file component)]
[remote-shape' component-container]))]
(if (nil? remote-shape)
nil
(if (nil? (:shape-ref remote-shape))
(cond-> remote-shape
(and remote-shape with-context?)
(with-meta {:file {:id (:id component-file)
:data component-file}
(with-meta {:file {:id (:id file-data)
:data file-data}
:container component-container}))
(find-remote-shape component-container libraries remote-shape :with-context? with-context?)))))

View File

@@ -1,29 +0,0 @@
;; 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.common.types.project
(:require
[app.common.schema :as sm]
[app.common.time :as cm]))
(def schema:project
[:map {:title "Profile"}
[:id ::sm/uuid]
[:created-at {:optional true} ::cm/inst]
[:modified-at {:optional true} ::cm/inst]
[:name :string]
[:is-default {:optional true} ::sm/boolean]
[:is-pinned {:optional true} ::sm/boolean]
[:count {:optional true} ::sm/int]
[:total-count {:optional true} ::sm/int]
[:team-id ::sm/uuid]])
(def valid-project?
(sm/lazy-validator schema:project))
(def check-project
(sm/check-fn schema:project))

View File

@@ -59,7 +59,6 @@
:dimensions "dimension"
:font-family "fontFamilies"
:font-size "fontSizes"
:font-weight "fontWeights"
:letter-spacing "letterSpacing"
:number "number"
:opacity "opacity"
@@ -71,6 +70,7 @@
:stroke-width "borderWidth"
:text-case "textCase"
:text-decoration "textDecoration"
:font-weight "fontWeights"
:typography "typography"})
(def dtcg-token-type->token-type

View File

@@ -1410,8 +1410,8 @@ Will return a value that matches this schema:
;; NOTE: we can't assign statically at eval time the value of a
;; function that is declared but not defined; so we need to pass
;; an anonymous function and delegate the resolution to runtime
{:encode/json #(some-> % export-dtcg-json)
:decode/json #(some-> % read-multi-set-dtcg)
{:encode/json #(export-dtcg-json %)
:decode/json #(read-multi-set-dtcg %)
;; FIXME: add better, more reallistic generator
:gen/gen (->> (sg/small-int)
(sg/fmap (fn [_]
@@ -1545,7 +1545,7 @@ Will return a value that matches this schema:
(and (not (contains? decoded-json "$metadata"))
(not (contains? decoded-json "$themes"))))
(defn convert-dtcg-font-family
(defn- convert-dtcg-font-family
"Convert font-family token value from DTCG format to internal format.
- If value is a string, split it into a collection of font families
- If value is already an array, keep it as is
@@ -1556,7 +1556,7 @@ Will return a value that matches this schema:
(sequential? value) value
:else value))
(defn convert-dtcg-typography-composite
(defn- convert-dtcg-typography-composite
"Convert typography token value keys from DTCG format to internal format."
[value]
(if (map? value)
@@ -1568,7 +1568,7 @@ Will return a value that matches this schema:
;; Reference value
value))
(defn convert-dtcg-shadow-composite
(defn- convert-dtcg-shadow-composite
"Convert shadow token value from DTCG format to internal format."
[value]
(let [process-shadow (fn [shadow]

View File

@@ -10,7 +10,3 @@ localhost:3449 {
http://localhost:3450 {
reverse_proxy localhost:4449
}
http://penpot-devenv-main:3450 {
reverse_proxy localhost:4449
}

View File

@@ -50,4 +50,9 @@ tmux select-window -t penpot:4
tmux send-keys -t penpot 'cd penpot/backend' enter C-l
tmux send-keys -t penpot './scripts/start-dev' enter
tmux new-window -t penpot:5 -n 'nitrate'
tmux select-window -t penpot:5
tmux send-keys -t penpot 'cd penpot/penpot-nitrate' enter C-l
tmux send-keys -t penpot 'pnpm dev --host' enter
tmux -2 attach-session -t penpot

View File

@@ -29,8 +29,9 @@ update_flags /var/www/app/js/config.js
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"

View File

@@ -139,6 +139,14 @@ http {
proxy_pass $PENPOT_BACKEND_URI/ws/notifications;
}
location /control-center {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $http_cf_connecting_ip;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass $PENPOT_NITRATE_URI$request_uri;
}
include /etc/nginx/overrides/server.d/*.conf;
location / {

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8",
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
@@ -16,9 +16,9 @@
"date-fns": "^4.1.0",
"generic-pool": "^3.9.0",
"inflation": "^2.1.0",
"ioredis": "^5.8.2",
"playwright": "^1.57.0",
"raw-body": "^3.0.2",
"ioredis": "^5.8.1",
"playwright": "^1.55.1",
"raw-body": "^3.0.1",
"source-map-support": "^0.5.21",
"svgo": "penpot/svgo#v3.1",
"undici": "^7.16.0",

View File

@@ -243,7 +243,7 @@ __metadata:
languageName: node
linkType: hard
"bytes@npm:~3.1.2":
"bytes@npm:3.1.2":
version: 3.1.2
resolution: "bytes@npm:3.1.2"
checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e
@@ -442,7 +442,7 @@ __metadata:
languageName: node
linkType: hard
"depd@npm:~2.0.0":
"depd@npm:2.0.0, depd@npm:~2.0.0":
version: 2.0.0
resolution: "depd@npm:2.0.0"
checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c
@@ -577,9 +577,9 @@ __metadata:
date-fns: "npm:^4.1.0"
generic-pool: "npm:^3.9.0"
inflation: "npm:^2.1.0"
ioredis: "npm:^5.8.2"
playwright: "npm:^1.57.0"
raw-body: "npm:^3.0.2"
ioredis: "npm:^5.8.1"
playwright: "npm:^1.55.1"
raw-body: "npm:^3.0.1"
source-map-support: "npm:^0.5.21"
svgo: "penpot/svgo#v3.1"
undici: "npm:^7.16.0"
@@ -683,16 +683,16 @@ __metadata:
languageName: node
linkType: hard
"http-errors@npm:~2.0.1":
version: 2.0.1
resolution: "http-errors@npm:2.0.1"
"http-errors@npm:2.0.0":
version: 2.0.0
resolution: "http-errors@npm:2.0.0"
dependencies:
depd: "npm:~2.0.0"
inherits: "npm:~2.0.4"
setprototypeof: "npm:~1.2.0"
statuses: "npm:~2.0.2"
toidentifier: "npm:~1.0.1"
checksum: 10c0/fb38906cef4f5c83952d97661fe14dc156cb59fe54812a42cd448fa57b5c5dfcb38a40a916957737bd6b87aab257c0648d63eb5b6a9ca9f548e105b6072712d4
depd: "npm:2.0.0"
inherits: "npm:2.0.4"
setprototypeof: "npm:1.2.0"
statuses: "npm:2.0.1"
toidentifier: "npm:1.0.1"
checksum: 10c0/fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19
languageName: node
linkType: hard
@@ -716,6 +716,15 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:0.7.0":
version: 0.7.0
resolution: "iconv-lite@npm:0.7.0"
dependencies:
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
checksum: 10c0/2382400469071c55b6746c531eed5fa4d033e5db6690b7331fb2a5f59a30d7a9782932e92253db26df33c1cf46fa200a3fbe524a2a7c62037c762283f188ec2f
languageName: node
linkType: hard
"iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
@@ -725,15 +734,6 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:~0.7.0":
version: 0.7.0
resolution: "iconv-lite@npm:0.7.0"
dependencies:
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
checksum: 10c0/2382400469071c55b6746c531eed5fa4d033e5db6690b7331fb2a5f59a30d7a9782932e92253db26df33c1cf46fa200a3fbe524a2a7c62037c762283f188ec2f
languageName: node
linkType: hard
"ieee754@npm:^1.2.1":
version: 1.2.1
resolution: "ieee754@npm:1.2.1"
@@ -755,16 +755,16 @@ __metadata:
languageName: node
linkType: hard
"inherits@npm:~2.0.3, inherits@npm:~2.0.4":
"inherits@npm:2.0.4, inherits@npm:~2.0.3":
version: 2.0.4
resolution: "inherits@npm:2.0.4"
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
languageName: node
linkType: hard
"ioredis@npm:^5.8.2":
version: 5.8.2
resolution: "ioredis@npm:5.8.2"
"ioredis@npm:^5.8.1":
version: 5.8.1
resolution: "ioredis@npm:5.8.1"
dependencies:
"@ioredis/commands": "npm:1.4.0"
cluster-key-slot: "npm:^1.1.0"
@@ -775,7 +775,7 @@ __metadata:
redis-errors: "npm:^1.2.0"
redis-parser: "npm:^3.0.0"
standard-as-callback: "npm:^2.1.0"
checksum: 10c0/305e385f811d49908899e32c2de69616cd059f909afd9e0a53e54f596b1a5835ee3449bfc6a3c49afbc5a2fd27990059e316cc78f449c94024957bd34c826d88
checksum: 10c0/4ed66444017150da027bce940a24bf726994691e2a7b3aa11d52f8aeb37f258068cc171af4d9c61247acafc28eb086fa8a7c79420b8e8d2907d2f74f39584465
languageName: node
linkType: hard
@@ -1106,27 +1106,27 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.57.0":
version: 1.57.0
resolution: "playwright-core@npm:1.57.0"
"playwright-core@npm:1.55.1":
version: 1.55.1
resolution: "playwright-core@npm:1.55.1"
bin:
playwright-core: cli.js
checksum: 10c0/798e35d83bf48419a8c73de20bb94d68be5dde68de23f95d80a0ebe401e3b83e29e3e84aea7894d67fa6c79d2d3d40cc5bcde3e166f657ce50987aaa2421b6a9
checksum: 10c0/39837a8c1232ec27486eac8c3fcacc0b090acc64310f7f9004b06715370fc426f944e3610fe8c29f17cd3d68280ed72c75f660c02aa5b5cf0eb34bab0031308f
languageName: node
linkType: hard
"playwright@npm:^1.57.0":
version: 1.57.0
resolution: "playwright@npm:1.57.0"
"playwright@npm:^1.55.1":
version: 1.55.1
resolution: "playwright@npm:1.55.1"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.57.0"
playwright-core: "npm:1.55.1"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 10c0/ab03c99a67b835bdea9059f516ad3b6e42c21025f9adaa161a4ef6bc7ca716dcba476d287140bb240d06126eb23f889a8933b8f5f1f1a56b80659d92d1358899
checksum: 10c0/b84a97b0d764403df512f5bbb10c7343974e151a28202cc06f90883a13e8a45f4491a0597f0ae5fb03a026746cbc0d200f0f32195bfaa381aee5ca5770626771
languageName: node
linkType: hard
@@ -1161,15 +1161,15 @@ __metadata:
languageName: node
linkType: hard
"raw-body@npm:^3.0.2":
version: 3.0.2
resolution: "raw-body@npm:3.0.2"
"raw-body@npm:^3.0.1":
version: 3.0.1
resolution: "raw-body@npm:3.0.1"
dependencies:
bytes: "npm:~3.1.2"
http-errors: "npm:~2.0.1"
iconv-lite: "npm:~0.7.0"
unpipe: "npm:~1.0.0"
checksum: 10c0/d266678d08e1e7abea62c0ce5864344e980fa81c64f6b481e9842c5beaed2cdcf975f658a3ccd67ad35fc919c1f6664ccc106067801850286a6cbe101de89f29
bytes: "npm:3.1.2"
http-errors: "npm:2.0.0"
iconv-lite: "npm:0.7.0"
unpipe: "npm:1.0.0"
checksum: 10c0/892f4fbd21ecab7e2fed0f045f7af9e16df7e8050879639d4e482784a2f4640aaaa33d916a0e98013f23acb82e09c2e3c57f84ab97104449f728d22f65a7d79a
languageName: node
linkType: hard
@@ -1270,7 +1270,7 @@ __metadata:
languageName: node
linkType: hard
"setprototypeof@npm:~1.2.0":
"setprototypeof@npm:1.2.0":
version: 1.2.0
resolution: "setprototypeof@npm:1.2.0"
checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc
@@ -1368,10 +1368,10 @@ __metadata:
languageName: node
linkType: hard
"statuses@npm:~2.0.2":
version: 2.0.2
resolution: "statuses@npm:2.0.2"
checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f
"statuses@npm:2.0.1":
version: 2.0.1
resolution: "statuses@npm:2.0.1"
checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0
languageName: node
linkType: hard
@@ -1500,7 +1500,7 @@ __metadata:
languageName: node
linkType: hard
"toidentifier@npm:~1.0.1":
"toidentifier@npm:1.0.1":
version: 1.0.1
resolution: "toidentifier@npm:1.0.1"
checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1
@@ -1539,7 +1539,7 @@ __metadata:
languageName: node
linkType: hard
"unpipe@npm:~1.0.0":
"unpipe@npm:1.0.0":
version: 1.0.0
resolution: "unpipe@npm:1.0.0"
checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c

View File

@@ -8,11 +8,6 @@
metosin/reitit-core {:mvn/version "0.9.1"}
funcool/okulary {:mvn/version "2022.04.11-16"}
funcool/tubax
{:git/tag "v2025.11.28"
:git/sha "2d9a986"
:git/url "https://github.com/funcool/tubax.git"}
funcool/potok2
{:git/tag "v2.2"
:git/sha "0f7e15a"
@@ -50,7 +45,7 @@
{thheller/shadow-cljs {:mvn/version "3.2.2"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "0.4.6"}
criterium/criterium {:mvn/version "RELEASE"}
cider/cider-nrepl {:mvn/version "0.57.0"}}}
:shadow-cljs

View File

@@ -53,74 +53,83 @@
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
},
"devDependencies": {
"@penpot/draft-js": "portal:./packages/draft-js",
"@penpot/mousetrap": "portal:./packages/mousetrap",
"@penpot/plugins-runtime": "1.3.2",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "portal:./text-editor",
"@playwright/test": "1.52.0",
"@storybook/addon-docs": "10.0.4",
"@storybook/addon-themes": "10.0.4",
"@storybook/addon-vitest": "10.0.4",
"@storybook/react-vite": "10.0.4",
"@tokens-studio/sd-transforms": "1.2.11",
"@types/node": "^22.15.21",
"@vitest/browser": "3.2.4",
"@vitest/coverage-v8": "3.2.4",
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
"autoprefixer": "^10.4.21",
"compression": "^1.8.1",
"concurrently": "^9.2.1",
"date-fns": "^4.1.0",
"esbuild": "^0.25.9",
"eventsource-parser": "^3.0.6",
"express": "^5.1.0",
"fancy-log": "^2.0.0",
"getopts": "^2.3.0",
"gettext-parser": "^8.0.0",
"highlight.js": "^11.10.0",
"js-beautify": "^1.15.4",
"gulp-concat": "^2.6.1",
"gulp-gzip": "^1.4.2",
"gulp-mustache": "^5.0.0",
"gulp-postcss": "^10.0.0",
"gulp-rename": "^2.0.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-svg-sprite": "^2.0.3",
"jsdom": "^27.0.0",
"lodash": "^4.17.21",
"lodash.debounce": "^4.0.8",
"map-stream": "0.0.7",
"marked": "^15.0.12",
"mkdirp": "^3.0.1",
"mustache": "^4.2.0",
"nodemon": "^3.1.10",
"npm-run-all": "^4.1.5",
"opentype.js": "^1.3.4",
"p-limit": "^6.2.0",
"playwright": "1.56.1",
"postcss": "^8.5.4",
"postcss-clean": "^1.2.2",
"postcss-modules": "^6.0.1",
"prettier": "3.5.3",
"pretty-time": "^1.1.0",
"prop-types": "^15.8.1",
"rimraf": "^6.0.1",
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"storybook": "10.0.4",
"svg-sprite": "^2.0.4",
"typescript": "^5.9.2",
"vite": "^6.3.5",
"vitest": "^3.2.0",
"wasm-pack": "^0.13.1",
"watcher": "^2.3.1",
"workerpool": "^9.3.2"
},
"dependencies": {
"@penpot/draft-js": "portal:./vendor/draft-js",
"@penpot/hljs": "portal:./vendor/hljs",
"@penpot/mousetrap": "portal:./vendor/mousetrap",
"@penpot/plugins-runtime": "1.3.2",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "portal:./text-editor",
"@tokens-studio/sd-transforms": "1.2.11",
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
"compression": "^1.8.1",
"date-fns": "^4.1.0",
"eventsource-parser": "^3.0.6",
"js-beautify": "^1.15.4",
"lodash": "^4.17.21",
"lodash.debounce": "^4.0.8",
"opentype.js": "^1.3.4",
"postcss-modules": "^6.0.1",
"randomcolor": "^0.6.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-error-boundary": "^6.0.0",
"react-virtualized": "^9.22.6",
"rimraf": "^6.0.1",
"rxjs": "8.0.0-alpha.14",
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"sax": "^1.4.1",
"source-map-support": "^0.5.21",
"storybook": "10.0.4",
"style-dictionary": "5.0.0-rc.1",
"svg-sprite": "^2.0.4",
"tdigest": "^0.1.2",
"tinycolor2": "^1.6.0",
"typescript": "^5.9.2",
"ua-parser-js": "2.0.5",
"vite": "^6.3.5",
"vitest": "^3.2.0",
"wasm-pack": "^0.13.1",
"watcher": "^2.3.1",
"workerpool": "^9.3.2",
"xregexp": "^5.1.2"
}
}

View File

@@ -1 +0,0 @@
[]

View File

@@ -1,47 +0,0 @@
[
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41234",
"~:revn": 1,
"~:vern": 1,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1705307400000",
"~:modified-at": "~m1732111500000",
"~:deleted-at": "~m1732111500000",
"~:name": "Deleted Design File 1",
"~:is-shared": false,
"~:will-be-deleted-at": "~m1732716300000",
"~:thumbnail-id": null,
"~:row-num": 1,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
},
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41235",
"~:revn": 2,
"~:vern": 2,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1704875700000",
"~:modified-at": "~m1732025400000",
"~:deleted-at": "~m1732025400000",
"~:name": "Deleted Design File 2",
"~:is-shared": true,
"~:will-be-deleted-at": "~m1732630200000",
"~:thumbnail-id": null,
"~:row-num": 2,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
},
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41236",
"~:revn": 3,
"~:vern": 3,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920c",
"~:created-at": "~m1706792400000",
"~:modified-at": "~m1731939600000",
"~:deleted-at": "~m1731939600000",
"~:name": "Old Project Design",
"~:is-shared": false,
"~:will-be-deleted-at": "~m1732544400000",
"~:thumbnail-id": null,
"~:row-num": 3,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
}
]

View File

@@ -106,13 +106,6 @@ export class DashboardPage extends BaseWebSocketPage {
);
}
async setupDeletedFiles() {
await this.mockRPC(
"get-team-deleted-files?team-id=*",
"dashboard/get-team-deleted-files.json",
);
}
async setupDrafts() {
await this.mockRPC(
"get-project-files?project-id=*",
@@ -167,10 +160,6 @@ export class DashboardPage extends BaseWebSocketPage {
});
await this.mockRPC("search-files", "dashboard/search-files.json");
await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json");
await this.mockRPC(
"get-team-deleted-files?team-id=*",
"dashboard/get-team-deleted-files.json",
);
}
async setupAccessTokensEmpty() {
@@ -300,13 +289,6 @@ export class DashboardPage extends BaseWebSocketPage {
await expect(this.mainHeading).toHaveText("Libraries");
}
async goToDeleted() {
await this.page.goto(
`#/dashboard/deleted?team-id=${DashboardPage.anyTeamId}`,
);
await expect(this.mainHeading).toHaveText("Projects");
}
async openProfileMenu() {
await this.userAccount.click();
}

View File

@@ -1,31 +0,0 @@
import { test, expect } from "@playwright/test";
import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test.describe("Dashboard Deleted Page", () => {
test("User can navigate to deleted page", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
// Setup mock for deleted files API
await dashboardPage.setupDeletedFiles();
// Navigate directly to deleted page
await dashboardPage.goToDeleted();
// Check for the restore all and clear trash buttons
await expect(
page.getByRole("button", { name: "Restore All" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Clear trash" }),
).toBeVisible();
});
});

View File

@@ -24,8 +24,6 @@
<link rel="icon" href="images/favicon.png" />
<script type="importmap">{{& manifest.importmap }}</script>
<script type="module">
globalThis.penpotVersion = "{{& version}}";
globalThis.penpotBuildDate = "{{& build_date}}";
@@ -35,6 +33,7 @@
{{# manifest}}
<script src="{{& config}}"></script>
<script src="{{& polyfills}}"></script>
<script type="importmap">{{& importmap }}</script>
{{/manifest}}
<!--cookie-consent-->
@@ -50,9 +49,7 @@
<script type="module" src="{{& libs}}"></script>
<script type="module">
import { init } from "{{& app_main}}";
import defaultTranslations from "{{& default_translations}}";
init({defaultTranslations});
init();
</script>
{{/manifest}}
</body>

View File

@@ -187,7 +187,7 @@ async function readManifestFile(resource) {
return JSON.parse(content);
}
async function generateManifest() {
async function readShadowManifest() {
const index = {
app_main: "./js/main.js",
render_main: "./js/render.js",
@@ -197,7 +197,6 @@ async function generateManifest() {
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
libs: "./js/libs.js?version=" + CURRENT_VERSION,
worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION,
default_translations: "./js/translation.en.js?version=" + CURRENT_VERSION,
importmap: JSON.stringify({
"imports": {
@@ -277,7 +276,6 @@ export async function compileTranslations() {
"id",
"ru",
"tr",
"hi",
"zh_CN",
"zh_Hant",
"hr",
@@ -393,7 +391,7 @@ async function generateTemplates() {
const isDebug = process.env.NODE_ENV !== "production";
await fs.mkdir("./resources/public/", { recursive: true });
const manifest = await generateManifest();
const manifest = await readShadowManifest();
let content;
const iconsSprite = await fs.readFile(

View File

@@ -90,10 +90,7 @@
(rx/map #(ws/initialize)))))))
(defn ^:export init
[options]
(some-> (unchecked-get options "defaultTranslations")
(i18n/set-default-translations))
[]
(mw/init!)
(i18n/init)
(cur/init-styles)

View File

@@ -302,9 +302,3 @@
:height 720}])
(def max-input-length 255)
(def ^:const default-slow-progress-threshold
"A constant value that represents a threshold in milliseconds when a
normal progress becomes tagged as slow if no event received in the
specified amount of time"
1000)

View File

@@ -10,7 +10,6 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.team :as ctt]
[app.main.data.helpers :as dsh]
[app.main.data.modal :as modal]
@@ -230,91 +229,6 @@
;; Delay so the navigation can finish
(rx/delay 250))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PROGRESS EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def noop-fn
(constantly nil))
(def ^:private schema:progress-params
[:map {:title "Progress"}
[:key {:optional true} ::sm/text]
[:index {:optional true} ::sm/int]
[:total ::sm/int]
[:hints
[:map-of :keyword fn?]]
[:slow-progress-threshold {:optional true} ::sm/int]])
(def ^:private check-progress-params
(sm/check-fn schema:progress-params))
(defn initialize-progress
[& {:keys [key index total hints slow-progress-threshold] :as params}]
(assert (check-progress-params params))
(ptk/reify ::initialize-progress
ptk/UpdateEvent
(update [_ state]
(update state :progress
(fn [_]
(let [hint ((:normal hints noop-fn) params)]
{:threshold (or slow-progress-threshold 5000)
:key key
:last-update (ct/now)
:healthy true
:visible true
:hints hints
:progress (d/nilv index 0)
:total total
:hint hint}))))))
(defn update-progress
[{:keys [index total] :as params}]
(assert (check-progress-params params))
(ptk/reify ::update-progress
ptk/UpdateEvent
(update [_ state]
(update state :progress
(fn [state]
(let [last-update (get state :last-update)
hints (get state :hints)
threshold (get state :slow-progress-threshold)
time-diff (ct/diff-ms last-update (ct/now))
healthy? (< time-diff threshold)
hint (if healthy?
((:normal hints noop-fn) params)
((:slow hints noop-fn) params))]
(-> state
(assoc :progress index)
(assoc :total total)
(assoc :last-update (ct/now))
(assoc :healthy healthy?)
(assoc :hint hint))))))))
(defn toggle-progress-visibility
[]
(ptk/reify ::toggle-progress-visibility
ptk/UpdateEvent
(update [_ state]
(update state :progress
(fn [state]
(update state :visible not))))))
(defn clear-progress
[]
(ptk/reify ::clear-progress
ptk/UpdateEvent
(update [_ state]
(dissoc state :progress))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; NAVEGATION EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -472,21 +386,3 @@
(rx/of ::dps/force-persist
(rt/nav :viewer params options))))))
(defn go-to-dashboard-deleted
[& {:keys [team-id] :as options}]
(ptk/reify ::go-to-dashboard-deleted
ptk/WatchEvent
(watch [_ state _]
(let [profile (get state :profile)
team-id (cond
(= :default team-id)
(:default-team-id profile)
(uuid? team-id)
team-id
:else
(:current-team-id state))
params {:team-id team-id}]
(rx/of (modal/hide)
(rt/nav :dashboard-deleted params options))))))

View File

@@ -13,18 +13,14 @@
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.project :refer [valid-project?]]
[app.common.uuid :as uuid]
[app.main.constants :as mconst]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.fonts :as df]
[app.main.data.helpers :as dsh]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.websocket :as dws]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.i18n :as i18n :refer [tr]]
[app.util.sse :as sse]
[beicon.v2.core :as rx]
@@ -80,8 +76,7 @@
ptk/UpdateEvent
(update [_ state]
(reduce (fn [state {:keys [id] :as project}]
;; Replace completely instead of merge to ensure deleted-at is removed
(assoc-in state [:projects id] project))
(update-in state [:projects id] merge project))
state
projects))))
@@ -157,34 +152,6 @@
(->> (rp/cmd! :get-builtin-templates)
(rx/map builtin-templates-fetched)))))
;; --- EVENT: deleted-files
(defn- deleted-files-fetched
[files]
(ptk/reify ::deleted-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [now (ct/now)
filtered-files (filterv (fn [file]
(let [will-be-deleted-at (:will-be-deleted-at file)]
(or (nil? will-be-deleted-at)
(ct/is-after? will-be-deleted-at now))))
files)
files (d/index-by :id filtered-files)]
(-> state
(assoc :deleted-files files)
(update :files d/merge files))))))
(defn fetch-deleted-files
([] (fetch-deleted-files nil))
([team-id]
(ptk/reify ::fetch-deleted-files
ptk/WatchEvent
(watch [_ state _]
(when-let [team-id (or team-id (:current-team-id state))]
(->> (rp/cmd! :get-team-deleted-files {:team-id team-id})
(rx/map deleted-files-fetched)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Selection
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -493,7 +460,6 @@
(-> state
(d/update-in-when [:files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:recent-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:deleted-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-when :dashboard-search-result update-search-files))))))
;; --- EVENT: create-file
@@ -683,258 +649,22 @@
(rx/of (dcm/change-team-role params)
(modal/hide)))))
(defn handle-change-team-org
[{:keys [team-id organization-id organization-name] :as message}]
(ptk/reify ::handle-change-team-org
ptk/UpdateEvent
(update [_ state]
(if (contains? (:teams state) team-id)
(-> state
(assoc-in [:teams team-id :organization-id] organization-id)
(assoc-in [:teams team-id :organization-name] organization-name))
state))))
(defn- process-message
[{:keys [type] :as msg}]
(case type
:notification (dcm/handle-notification msg)
:team-role-change (handle-change-team-role msg)
:team-membership-change (dcm/team-membership-change msg)
:team-org-change (handle-change-team-org msg)
nil))
;; --- Delete files immediately
(defn- delete-files
[{:keys [team-id ids on-success on-error]}]
(assert (uuid? team-id))
(assert (set? ids))
(assert (every? uuid? ids))
(assert (fn? on-success))
(assert (fn? on-error))
(ptk/reify ::delete-files
ptk/WatchEvent
(watch [_ _ _]
(let [progress-hint #(tr "dashboard.progress-notification.deleting-files")
slow-hint #(tr "dashboard.progress-notification.slow-delete")
stream (->> (rp/cmd! ::sse/permanently-delete-team-files {:team-id team-id :ids ids})
(rx/share))]
(rx/merge
(rx/of (dcm/initialize-progress
{:slow-progress-threshold
mconst/default-slow-progress-threshold
:total (count ids)
:hints {:progress progress-hint
:slow slow-hint}}))
(->> stream
(rx/filter sse/progress?)
(rx/mapcat (fn [event]
(if-let [payload (sse/get-payload event)]
(let [{:keys [index total]} payload]
(if (and index total)
(rx/of (dcm/update-progress {:index index :total total}))
(rx/empty)))
(rx/empty))))
(rx/catch rx/empty))
(->> stream
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/merge-map (fn [_]
(rx/concat
(rx/of (dcm/clear-progress)
(fetch-projects team-id)
(fetch-deleted-files team-id)
(fetch-projects team-id))
(on-success))))
(rx/catch (fn [error]
(rx/concat
(rx/of (dcm/clear-progress))
(on-error error))))))))))
(defn delete-files-immediately
[{:keys [team-id ids] :as params}]
(assert (uuid? team-id))
(assert (set? ids))
(assert (every? uuid? ids))
(ptk/reify ::delete-files-immediately
ptk/WatchEvent
(watch [_ state _]
(let [deleted-files
(get state :deleted-files)
on-success
(fn []
(if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/success (tr "dashboard.delete-success-notification" fname))))
(rx/of (ntf/success (tr "dashboard.delete-files-success-notification" (count ids))))))
on-error
#(rx/of (ntf/error (tr "dashboard.errors.error-on-delete-files")))]
(rx/of (ev/event
{::ev/name "delete-files"
::ev/origin "dashboard:trash"
:team-id team-id
:num-files (count ids)})
(delete-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
(defn delete-project-immediately
[{:keys [team-id id name] :as project}]
(assert (valid-project? project))
(ptk/reify ::delete-project-immediately
ptk/WatchEvent
(watch [_ state _]
(let [ids
(reduce-kv (fn [acc file-id file]
(if (= (:project-id file) id)
(conj acc file-id)
acc))
#{}
(get state :deleted-files))
on-success
#(rx/of (ntf/success (tr "dashboard.delete-success-notification" name)))
on-error
#(rx/of (ntf/error (tr "dashboard.errors.error-on-delete-project" name)))]
(rx/of (ev/event
{::ev/name "delete-files"
::ev/origin "dashboard:trash"
:team-id team-id
:project-id id
:num-files (count ids)})
(delete-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
;; --- Restore deleted files immediately
(defn- restore-files
[{:keys [team-id ids on-success on-error]}]
(assert (uuid? team-id))
(assert (set? ids))
(assert (every? uuid? ids))
(assert (fn? on-success))
(assert (fn? on-error))
(ptk/reify ::restore-files
ptk/WatchEvent
(watch [_ _ _]
(let [progress-hint #(tr "dashboard.progress-notification.restoring-files")
slow-hint #(tr "dashboard.progress-notification.slow-restore")]
(rx/merge
(rx/of (dcm/initialize-progress
{:slow-progress-threshold
mconst/default-slow-progress-threshold
:total (count ids)
:hints {:progress progress-hint
:slow slow-hint}}))
(let [stream (->> (rp/cmd! ::sse/restore-deleted-team-files {:team-id team-id :ids ids})
(rx/share))]
(rx/merge
(->> stream
(rx/filter sse/progress?)
(rx/mapcat (fn [event]
(if-let [payload (sse/get-payload event)]
(let [{:keys [index total]} payload]
(if (and index total)
(rx/of (dcm/update-progress {:index index :total total}))
(rx/empty)))
(rx/empty))))
(rx/catch rx/empty))
(->> stream
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/mapcat (fn [_]
(rx/concat
(rx/of (dcm/clear-progress)
;; (ntf/success (tr "dashboard.restore-success-notification"))
(fetch-projects team-id)
(fetch-deleted-files team-id)
(fetch-projects team-id))
(on-success))))
(rx/catch (fn [error]
(rx/concat
(rx/of (dcm/clear-progress))
(on-error error))))))))))))
(defn restore-files-immediately
[{:keys [team-id ids]}]
(assert (uuid? team-id))
(assert (set? ids))
(ptk/reify ::restore-files-immediately
ptk/WatchEvent
(watch [_ state _]
(let [deleted-files
(get state :deleted-files)
on-success
(fn []
(if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/success (tr "dashboard.restore-success-notification" fname))))
(rx/of (ntf/success (tr "dashboard.restore-files-success-notification" (count ids))))))
on-error
(fn [_cause]
(if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/error (tr "dashboard.errors.error-on-restore-file" fname))))
(rx/of (ntf/error (tr "dashboard.errors.error-on-restore-files")))))]
(rx/of (ev/event
{::ev/name "restore-files"
::ev/origin "dashboard:trash"
:team-id team-id
:num-files (count ids)})
(restore-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
(defn restore-project-immediately
[{:keys [team-id id name] :as project}]
(assert (valid-project? project))
(ptk/reify ::restore-project-immediately
ptk/WatchEvent
(watch [_ state _]
(let [ids
(reduce-kv (fn [acc file-id file]
(if (= (:project-id file) id)
(conj acc file-id)
acc))
#{}
(get state :deleted-files))
on-success
#(st/emit! (ntf/success (tr "dashboard.restore-success-notification" name)))
on-error
#(st/emit! (ntf/error (tr "dashboard.errors.error-on-restoring-project" name)))]
(rx/of (ev/event
{::ev/name "restore-files"
::ev/origin "dashboard:trash"
:team-id team-id
:project-id id
:num-files (count ids)})
(restore-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))

View File

@@ -98,7 +98,9 @@
(def context
(atom (d/without-nils (collect-context))))
(add-watch i18n/locale "events" #(swap! context assoc :locale %4))
(add-watch i18n/state "events"
(fn [_ _ _ v]
(swap! context assoc :locale (get v :locale))))
;; --- EVENT TRANSLATION

View File

@@ -270,12 +270,8 @@
(ptk/reify ::process-wasm-object
ptk/EffectEvent
(effect [_ state _]
(let [objects (dsh/lookup-page-objects state)
shape (get objects id)]
;; Only process objects that exist in the current page
;; This prevents errors when processing changes from other pages
(when shape
(wasm.api/process-object shape))))))
(let [objects (dsh/lookup-page-objects state)]
(wasm.api/process-object (get objects id))))))
(defn initialize-workspace
[team-id file-id]

View File

@@ -14,7 +14,7 @@
[app.common.types.fills :as types.fills]
[app.common.types.library :as ctl]
[app.common.types.shape :as shp]
[app.common.types.shape.shadow :as types.shadow]
[app.common.types.shape.shadow :refer [check-shadow]]
[app.common.types.text :as txt]
[app.main.broadcast :as mbc]
[app.main.data.helpers :as dsh]
@@ -406,30 +406,30 @@
(defn change-shadow
[ids attrs index]
(letfn [(update-shadow [shape]
(let [;; If we try to set a gradient to a shadow (for
;; example using the color selection from
;; multiple shapes) let's use the first stop
;; color
attrs (cond-> attrs
(:gradient attrs)
(-> (dm/get-in [:gradient :stops 0])
(select-keys types.shadow/color-attrs)))
(ptk/reify ::change-shadow
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes
ids
(fn [shape]
(let [;; If we try to set a gradient to a shadow (for
;; example using the color selection from
;; multiple shapes) let's use the first stop
;; color
attrs (cond-> attrs
(:gradient attrs)
(dm/get-in [:gradient :stops 0]))
attrs' (-> (dm/get-in shape [:shadow index :color])
(merge attrs)
(d/without-nils))]
(assoc-in shape [:shadow index :color] attrs')))]
(ptk/reify ::change-shadow
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes ids update-shadow))))))
attrs' (-> (dm/get-in shape [:shadow index :color])
(merge attrs)
(d/without-nils))]
(assoc-in shape [:shadow index :color] attrs'))))))))
(defn add-shadow
[ids shadow]
(assert
(types.shadow/check-shadow shadow)
(check-shadow shadow)
"expected a valid shadow struct")
(assert
@@ -1146,16 +1146,16 @@
(defn- shadow->color-attr
"Given a stroke map enriched with :shape-id, :index, and optionally
:has-token-applied / :token-name, returns a color attribute map.
If :has-token-applied is true, adds token metadata to :attrs:
{:has-token-applied true
:token-name <token-name>}
Args:
- stroke: map with stroke info, including :shape-id and :index
- file-id: current file UUID
- libraries: map of shared color libraries
Returns:
A map like:
{:attrs {...color data...}
@@ -1260,12 +1260,12 @@
will include extra attributes in its :attrs map:
{:has-token-applied true
:token-name <token-name>}
Args:
- shapes: vector of shape maps
- file-id: current file UUID
- libraries: map of shared color libraries
Returns:
A vector of color attribute maps with metadata for each shape."
[shapes file-id libraries]

View File

@@ -554,7 +554,7 @@
(when (features/active-feature? state "text-editor/v2")
(let [instance (:workspace-editor state)
styles (some-> (editor.v2/getCurrentStyle instance)
(styles/get-styles-from-style-declaration :removed-mixed true)
(styles/get-styles-from-style-declaration)
((comp update-node-fn migrate-node))
(styles/attrs->styles))]
(editor.v2/applyStylesToSelection instance styles)))))))

View File

@@ -238,12 +238,12 @@
:always
(ctm/resize scalev resize-origin shape-transform shape-transform-inverse)
(and (or (ctl/any-layout-immediate-child? objects shape) (ctl/any-layout? shape))
(and (ctl/any-layout-immediate-child? objects shape)
(not= (:layout-item-h-sizing shape) :fix)
^boolean change-width?)
(ctm/change-property :layout-item-h-sizing :fix)
(and (or (ctl/any-layout-immediate-child? objects shape) (ctl/any-layout? shape))
(and (ctl/any-layout-immediate-child? objects shape)
(not= (:layout-item-v-sizing shape) :fix)
^boolean change-height?)
(ctm/change-property :layout-item-v-sizing :fix)

View File

@@ -636,6 +636,3 @@
(def persistence-state
(l/derived (comp :status :persistence) st/state))
(def progress
(l/derived :progress st/state))

View File

@@ -87,12 +87,6 @@
{:stream? true
:form-data? true}
::sse/permanently-delete-team-files
{:stream? true}
::sse/restore-deleted-team-files
{:stream? true}
:export-binfile {:response-type :blob}
:retrieve-list-of-builtin-templates {:query-params :all}})

View File

@@ -224,8 +224,7 @@
:dashboard-members
:dashboard-invitations
:dashboard-webhooks
:dashboard-settings
:dashboard-deleted)
:dashboard-settings)
(let [params (get params :query)
team-id (some-> params :team-id uuid/parse*)
project-id (some-> params :project-id uuid/parse*)

View File

@@ -50,8 +50,7 @@
touched? (and (contains? (:data @form) input-name)
(get-in @form [:touched input-name]))
error (or (get-in @form [:errors input-name])
(get-in @form [:extra-errors input-name]))
error (get-in @form [:errors input-name])
value (get-in @form [:data input-name] "")

View File

@@ -1,103 +0,0 @@
;; 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.components.progress
"Assets exportation common components."
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.types.color :as clr]
[app.main.data.common :as dcm]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as deprecated-icons]
[app.util.i18n :as i18n :refer [tr]]
[app.util.theme :as theme]
[rumext.v2 :as mf]))
(def ^:private neutral-icon
(deprecated-icons/icon-xref :msg-neutral (stl/css :icon)))
(def ^:private error-icon
(deprecated-icons/icon-xref :delete-text (stl/css :icon)))
(def ^:private close-icon
(deprecated-icons/icon-xref :close (stl/css :close-icon)))
(mf/defc progress-notification-widget*
[]
(let [state (mf/deref refs/progress)
profile (mf/deref refs/profile)
theme (get profile :theme theme/default)
default-theme? (= theme/default theme)
error? (:error state)
healthy? (:healthy state)
visible? (:visible state)
progress (:progress state)
hint (:hint state)
total (:total state)
pwidth
(if error?
280
(/ (* progress 280) total))
color
(cond
error? clr/new-danger
healthy? (if default-theme?
clr/new-primary
clr/new-primary-light)
(not healthy?) clr/new-warning)
background-clr
(if default-theme?
clr/background-quaternary
clr/background-quaternary-light)
toggle-detail-visibility
(mf/use-fn
(fn []
(st/emit! (dcm/toggle-progress-visibility))))]
[:*
(when visible?
[:div {:class (stl/css-case :progress-modal true
:has-error error?)}
(if error?
error-icon
neutral-icon)
[:div {:class (stl/css :title)}
[:div {:class (stl/css :title-text)} hint]
(if error?
[:button {:class (stl/css :retry-btn)
;; :on-click retry-last-operation
}
(tr "labels.retry")]
[:span {:class (stl/css :progress)}
(dm/str progress " / " total)])]
[:button {:class (stl/css :progress-close-button)
:on-click toggle-detail-visibility}
close-icon]
(when-not error?
[:svg {:class (stl/css :progress-bar)
:height 4
:width 280}
[:g
[:path {:d "M0 0 L280 0"
:stroke background-clr
:stroke-width 30}]
[:path {:d (dm/str "M0 0 L280 0")
:stroke color
:stroke-width 30
:fill "transparent"
:stroke-dasharray 280
:stroke-dashoffset (- 280 pwidth)
:style {:transition "stroke-dashoffset 1s ease-in-out"}}]]])])]))

View File

@@ -1,101 +0,0 @@
// 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 "refactor/common-refactor.scss" as deprecated;
// PROGRESS WIDGET
.progress-widget {
@include deprecated.flexCenter;
width: deprecated.$s-28;
height: deprecated.$s-28;
}
// PROGRESS MODAL
.progress-modal {
--export-modal-bg-color: var(--alert-background-color-default);
--export-modal-fg-color: var(--alert-text-foreground-color-default);
--export-modal-icon-color: var(--alert-icon-foreground-color-default);
--export-modal-border-color: var(--alert-border-color-default);
position: absolute;
right: deprecated.$s-16;
top: deprecated.$s-48;
display: grid;
grid-template-columns: deprecated.$s-24 1fr deprecated.$s-24;
grid-template-areas:
"icon text close"
"bar bar bar";
gap: deprecated.$s-4 deprecated.$s-8;
padding-block-start: deprecated.$s-8;
background-color: var(--export-modal-bg-color);
border: deprecated.$s-1 solid var(--export-modal-border-color);
border-radius: deprecated.$br-8;
z-index: deprecated.$z-index-modal;
overflow: hidden;
}
.has-error {
--export-modal-bg-color: var(--alert-background-color-error);
--export-modal-fg-color: var(--alert-text-foreground-color-error);
--export-modal-icon-color: var(--alert-icon-foreground-color-error);
--export-modal-border-color: var(--alert-border-color-error);
grid-template-areas: "icon text close";
gap: deprecated.$s-8;
padding-block: deprecated.$s-8;
}
.icon {
@extend .button-icon;
grid-area: icon;
align-self: center;
margin-inline-start: deprecated.$s-8;
stroke: var(--export-modal-icon-color);
}
.title {
@include deprecated.bodyMediumTypography;
display: grid;
grid-template-columns: auto 1fr;
gap: deprecated.$s-8;
grid-area: text;
align-self: center;
padding: 0;
margin: 0;
color: var(--export-modal-fg-color);
}
.progress {
@include deprecated.bodyMediumTypography;
padding-left: deprecated.$s-8;
margin: 0;
align-self: center;
color: var(--modal-text-foreground-color);
}
.retry-btn {
@include deprecated.buttonStyle;
@include deprecated.bodySmallTypography;
display: inline;
text-align: left;
color: var(--modal-link-foreground-color);
margin: 0;
padding: 0;
}
.progress-close-button {
@include deprecated.buttonStyle;
padding: 0;
margin-inline-end: deprecated.$s-8;
}
.close-icon {
@extend .button-icon;
stroke: var(--export-modal-icon-color);
}
.progress-bar {
margin-top: 0;
grid-area: bar;
}

View File

@@ -19,9 +19,7 @@
[app.main.refs :as refs]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.progress :refer [progress-notification-widget*]]
[app.main.ui.context :as ctx]
[app.main.ui.dashboard.deleted :refer [deleted-section*]]
[app.main.ui.dashboard.files :refer [files-section*]]
[app.main.ui.dashboard.fonts :refer [fonts-page* font-providers-page*]]
[app.main.ui.dashboard.import]
@@ -86,9 +84,6 @@
[:div {:class (stl/css :dashboard-content)
:on-click clear-selected-fn
:ref container}
[:> progress-notification-widget*]
(case section
:dashboard-recent
(when (seq projects)
@@ -145,11 +140,6 @@
:dashboard-settings
[:> team-settings-page* {:team team :profile profile}]
:dashboard-deleted
[:> deleted-section* {:team team
:projects projects
:profile profile}]
nil)]))
(def ref:dashboard-initialized

View File

@@ -1,308 +0,0 @@
;; 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.dashboard.deleted
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.dashboard.grid :refer [grid*]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ^:private ref:deleted-files
(l/derived :deleted-files st/state))
(def ^:private menu-icon
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
(mf/defc header*
{::mf/props :obj
::mf/private true}
[]
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
[:div#dashboard-deleted-title {:class (stl/css :dashboard-title)}
[:h1 (tr "dashboard.projects-title")]]])
(mf/defc deleted-project-menu*
[{:keys [project show on-close top left]}]
(let [top (d/nilv top 0)
left (d/nilv left 0)
on-restore-project
(mf/use-fn
(mf/deps project)
(fn []
(let [on-accept #(st/emit! (dd/restore-project-immediately project))]
(st/emit! (modal/show {:type :confirm
:title (tr "dashboard.restore-project-confirmation.title")
:message (tr "dashboard.restore-project-confirmation.description" (:name project))
:accept-style :primary
:accept-label (tr "labels.continue")
:on-accept on-accept})))))
on-delete-project
(mf/use-fn
(mf/deps project)
(fn []
(let [accept-fn #(st/emit! (dd/delete-project-immediately project))]
(st/emit! (modal/show {:type :confirm
:title (tr "dashboard.delete-forever-confirmation.title")
:message (tr "dashboard.delete-project-forever-confirmation.description" (:name project))
:accept-label (tr "dashboard.delete-forever-confirmation.title")
:on-accept accept-fn})))))
options
(mf/with-memo [on-restore-project on-delete-project]
[{:name (tr "dashboard.restore-project-button")
:id "project-restore"
:handler on-restore-project}
{:name (tr "dashboard.delete-project-button")
:id "project-delete"
:handler on-delete-project}])]
[:> context-menu*
{:on-close on-close
:show show
:fixed (or (not= top 0) (not= left 0))
:min-width true
:top top
:left left
:options options}]))
(mf/defc deleted-project-item*
{::mf/private true}
[{:keys [project files]}]
(let [project-files (filterv #(= (:project-id %) (:id project)) files)
empty? (empty? project-files)
selected-files (mf/deref refs/selected-files)
dstate (mf/deref refs/dashboard-local)
edit-id (:project-for-edit dstate)
local (mf/use-state
#(do {:menu-open false
:menu-pos nil
:edition (= (:id project) edit-id)}))
[rowref limit] (hooks/use-dynamic-grid-item-width)
on-menu-click
(mf/use-fn
(fn [event]
(dom/prevent-default event)
(let [client-position (dom/get-client-position event)
position (if (and (nil? (:y client-position)) (nil? (:x client-position)))
(let [target-element (dom/get-target event)
points (dom/get-bounding-rect target-element)
y (:top points)
x (:left points)]
(gpt/point x y))
client-position)]
(swap! local assoc
:menu-open true
:menu-pos position))))
on-menu-close
(mf/use-fn #(swap! local assoc :menu-open false))
handle-menu-click
(mf/use-callback
(mf/deps on-menu-click)
(fn [event]
(when (kbd/enter? event)
(dom/stop-propagation event)
(on-menu-click event))))]
[:article {:class (stl/css-case :dashboard-project-row true)}
[:header {:class (stl/css :project)}
[:div {:class (stl/css :project-name-wrapper)}
[:h2 {:class (stl/css :project-name)
:title (:name project)}
(:name project)]
(when (:deleted-at project)
[:div {:class (stl/css :info-wrapper)}
[:div {:class (stl/css-case :project-actions true)}
[:button {:class (stl/css :options-btn)
:on-click on-menu-click
:title (tr "dashboard.options")
:aria-label (tr "dashboard.options")
:data-testid "project-options"
:on-key-down handle-menu-click}
menu-icon]]
(when (:menu-open @local)
[:> deleted-project-menu*
{:project project
:show (:menu-open @local)
:left (+ 24 (:x (:menu-pos @local)))
:top (:y (:menu-pos @local))
:on-close on-menu-close}])])]]
[:div {:class (stl/css :grid-container) :ref rowref}
(if ^boolean empty?
[:> empty-placeholder* {:title (tr "dashboard.empty-placeholder-files-title")
:class (stl/css :placeholder-placement)
:type 1
:subtitle (tr "dashboard.empty-placeholder-files-subtitle")}]
[:> grid*
{:project project
:files project-files
:origin :deleted
:can-edit false
:can-restore true
:limit limit
:selected-files selected-files}])]]))
(mf/defc menu*
[{:keys [team-id section]}]
(let [on-recent-click
(mf/use-fn
(mf/deps team-id)
(fn []
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id))))
on-deleted-click
(mf/use-fn
(mf/deps team-id)
(fn []
(st/emit! (dcm/go-to-dashboard-deleted :team-id team-id))))]
[:div {:class (stl/css :nav)}
[:div {:class [(stl/css :nav-option)
(stl/css-case :selected (= section :dashboard-recent))]
:data-testid "recent-tab"
:on-click on-recent-click}
(tr "labels.recent")]
[:div {:class [(stl/css :nav-option)
(stl/css-case :selected (= section :dashboard-deleted))]
:variant "ghost"
:type "button"
:data-testid "deleted-tab"
:on-click on-deleted-click}
(tr "labels.deleted")]]))
(mf/defc deleted-section*
[{:keys [team projects]}]
(let [deleted-map
(mf/deref ref:deleted-files)
projects
(mf/with-memo [projects deleted-map]
(->> projects
(filter (fn [project]
(or (:deleted-at project)
(when deleted-map
(some #(= (:id project) (:project-id %))
(vals deleted-map))))))
(filter (fn [project]
(when deleted-map
(some #(= (:id project) (:project-id %))
(vals deleted-map)))))
(sort-by :modified-at)
(reverse)))
team-id
(get team :id)
;; Calculate deletion days based on team subscription
deletion-days
(let [subscription (get team :subscription)
sub-type (get subscription :type)
sub-status (get subscription :status)
canceled? (contains? #{"canceled" "unpaid"} sub-status)]
(cond
(and (= "unlimited" sub-type) (not canceled?)) 30
(and (= "enterprise" sub-type) (not canceled?)) 90
:else 7))
on-delete-all
(mf/use-fn
(mf/deps team-id deleted-map)
(fn []
(when-let [ids (not-empty (into #{} (map key) deleted-map))]
(let [on-accept #(st/emit! (dd/delete-files-immediately
{:team-id team-id
:ids ids}))]
(st/emit! (modal/show {:type :confirm
:title (tr "dashboard.delete-forever-confirmation.title")
:message (tr "dashboard.delete-all-forever-confirmation.description" (count ids))
:accept-label (tr "dashboard.delete-forever-confirmation.title")
:on-accept on-accept}))))))
on-restore-all
(mf/use-fn
(mf/deps team-id deleted-map)
(fn []
(when-let [ids (not-empty (into #{} (map key) deleted-map))]
(let [on-accept #(st/emit! (dd/restore-files-immediately {:team-id team-id :ids ids}))]
(st/emit! (modal/show {:type :confirm
:title (tr "dashboard.restore-all-confirmation.title")
:message (tr "dashboard.restore-all-confirmation.description" (count ids))
:accept-label (tr "labels.continue")
:accept-style :primary
:on-accept on-accept}))))))]
(mf/with-effect [team-id]
(st/emit! (dd/fetch-projects team-id)
(dd/fetch-deleted-files team-id)
(dd/clear-selected-files)))
[:*
[:> header* {:team team}]
[:section {:class (stl/css :dashboard-container :no-bg)}
[:*
[:div {:class (stl/css :no-bg)}
[:> menu* {:team-id team-id :section :dashboard-deleted}]
[:div {:class (stl/css :deleted-info-content)}
[:p {:class (stl/css :deleted-info)}
(tr "dashboard.trash-info-text-part1")
[:span {:class (stl/css :info-text-highlight)}
(tr "dashboard.trash-info-text-part2" deletion-days)]
(tr "dashboard.trash-info-text-part3")
[:br]
(tr "dashboard.trash-info-text-part4")]
[:div {:class (stl/css :deleted-options)}
[:> button* {:variant "ghost"
:type "button"
:on-click on-restore-all}
(tr "dashboard.restore-all-deleted-button")]
[:> button* {:variant "destructive"
:type "button"
:icon "delete"
:on-click on-delete-all}
(tr "dashboard.clear-trash-button")]]]
(when (seq projects)
(for [{:keys [id] :as project} projects]
(let [files (when deleted-map
(->> (vals deleted-map)
(filterv #(= id (:project-id %)))
(sort-by :modified-at #(compare %2 %1))))]
[:> deleted-project-item* {:project project
:files files
:key id}])))]]]]))

View File

@@ -1,137 +0,0 @@
// 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 "common/refactor/common-dashboard";
@use "../ds/typography.scss" as t;
@use "../ds/_borders.scss" as *;
@use "../ds/spacing.scss" as *;
@use "../ds/_sizes.scss" as *;
@use "../ds/z-index.scss" as *;
.dashboard-container {
flex: 1 0 0;
width: 100%;
margin-inline-end: var(--sp-l);
border-top: $b-1 solid var(--panel-border-color);
overflow-y: auto;
padding-block-end: var(--sp-xxxl);
}
.deleted-info-content {
display: flex;
justify-content: space-between;
padding: var(--sp-s) var(--sp-xxl) var(--sp-s) var(--sp-xxl);
}
.deleted-info {
display: block;
height: fit-content;
color: var(--color-foreground-secondary);
@include t.use-typography("body-large");
line-height: 0.8;
height: var(--sp-xl);
}
.info-text-highlight {
color: var(--color-accent-primary);
}
.deleted-options {
display: flex;
gap: 5px;
flex-shrink: 0;
}
.nav {
display: flex;
gap: var(--sp-l);
justify-content: space-between;
border-bottom: $b-1 solid var(--panel-border-color);
//padding-inline-start: var(--sp-l);
background: var(--color-background-default);
position: sticky;
top: 0;
z-index: var(--z-index-panels);
/* margin: 0 1.5rem; */
/* margin-top: 1rem; */
margin: var(--sp-xxl) var(--sp-xxl) var(--sp-xxl) var(--sp-xxl);
}
.nav-option {
color: var(--color-foreground-secondary);
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border: $b-1 solid transparent;
cursor: pointer;
}
.selected {
color: var(--color-foreground-primary);
border-bottom: $b-1 solid var(--color-foreground-primary);
}
.project {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
width: 99%;
max-height: $sz-40;
padding: var(--sp-s) var(--sp-s) var(--sp-s) var(--sp-l);
margin-block-start: var(--sp-l);
border-radius: $br-4;
}
.project-name-wrapper {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
min-height: var(--sp-xxxl);
margin-inline-start: var(--sp-s);
}
.project-name {
@include t.use-typography("body-large");
width: fit-content;
margin-inline-end: var(--sp-m);
line-height: 0.8;
color: var(--title-foreground-color-hover);
height: var(--sp-l);
}
.project-actions {
display: flex;
opacity: var(--actions-opacity);
margin-inline-start: var(--sp-xxxl);
}
.add-file-btn,
.options-btn {
@extend .button-tertiary;
height: var(--sp-xxxl);
width: var(--sp-xxxl);
margin: 0 var(--sp-s);
padding: var(--sp-s);
}
.info-wrapper {
display: flex;
align-items: center;
gap: var(--sp-s);
}
.add-icon,
.menu-icon {
@extend .button-icon;
stroke: var(--icon-foreground);
}

View File

@@ -55,7 +55,7 @@
projects))
(mf/defc file-menu*
[{:keys [files on-edit on-close top left navigate origin parent-id can-edit can-restore]}]
[{:keys [files on-edit on-close top left navigate origin parent-id can-edit]}]
(assert (seq files) "missing `files` prop")
(assert (fn? on-edit) "missing `on-edit` prop")
@@ -187,39 +187,7 @@
on-export-binary-files
(fn []
(st/emit! (-> (fexp/open-export-dialog files)
(with-meta {::ev/origin "dashboard"}))))
restore-fn
(fn [_]
(st/emit! (dd/restore-files-immediately
(with-meta {:team-id (:id current-team)
:ids #{(:id file)}}
{:on-success #(st/emit! (ntf/success (tr "dashboard.restore-success-notification" (:name file)))
(dd/fetch-projects (:id current-team))
(dd/fetch-deleted-files (:id current-team)))
:on-error #(st/emit! (ntf/error (tr "dashboard.errors.error-on-restore-file" (:name file))))}))))
on-restore-immediately
(fn []
(st/emit!
(modal/show {:type :confirm
:title (tr "dashboard-restore-file-confirmation.title")
:message (tr "dashboard-restore-file-confirmation.description" (:name file))
:accept-label (tr "labels.continue")
:accept-style :primary
:on-accept restore-fn})))
on-delete-immediately
(fn []
(let [accept-fn #(st/emit! (dd/delete-files-immediately
{:team-id (:id current-team)
:ids #{(:id file)}}))]
(st/emit!
(modal/show {:type :confirm
:title (tr "dashboard.delete-forever-confirmation.title")
:message (tr "dashboard.delete-file-forever-confirmation.description" (:name file))
:accept-label (tr "dashboard.delete-forever-confirmation.title")
:on-accept accept-fn}))))]
(with-meta {::ev/origin "dashboard"}))))]
(mf/with-effect []
(->> (rp/cmd! :get-all-projects)
@@ -259,85 +227,76 @@
(:id sub-project))})})}]))
options
(if can-restore
[(when can-restore
{:name (tr "dashboard.restore-file-button")
:id "restore-file"
:handler on-restore-immediately})
(when can-restore
{:name (tr "dashboard.delete-file-button")
:id "delete-file"
:handler on-delete-immediately})]
(if multi?
[(when can-edit
{:name (tr "dashboard.duplicate-multi" file-count)
:id "duplicate-multi"
:handler on-duplicate})
(if multi?
[(when can-edit
{:name (tr "dashboard.duplicate-multi" file-count)
:id "duplicate-multi"
:handler on-duplicate})
(when (and (or (seq current-projects) (seq other-teams)) can-edit)
{:name (tr "dashboard.move-to-multi" file-count)
:id "file-move-multi"
:options sub-options})
(when (and (or (seq current-projects) (seq other-teams)) can-edit)
{:name (tr "dashboard.move-to-multi" file-count)
:id "file-move-multi"
:options sub-options})
{:name (tr "dashboard.export-binary-multi" file-count)
:id "file-binary-export-multi"
:handler on-export-binary-files}
{:name (tr "dashboard.export-binary-multi" file-count)
:id "file-binary-export-multi"
:handler on-export-binary-files}
(when (and (:is-shared file) can-edit)
{:name (tr "labels.unpublish-multi-files" file-count)
:id "file-unpublish-multi"
:handler on-del-shared})
(when (and (:is-shared file) can-edit)
{:name (tr "labels.unpublish-multi-files" file-count)
:id "file-unpublish-multi"
:handler on-del-shared})
(when (and (not is-lib-page?) can-edit)
{:name :separator}
{:name (tr "labels.delete-multi-files" file-count)
:id "file-delete-multi"
:handler on-delete})]
(when (and (not is-lib-page?) can-edit)
{:name :separator}
{:name (tr "labels.delete-multi-files" file-count)
:id "file-delete-multi"
:handler on-delete})]
[{:name (tr "dashboard.open-in-new-tab")
:id "file-open-new-tab"
:handler on-new-tab}
(when (and (not is-search-page?) can-edit)
{:name (tr "labels.rename")
:id "file-rename"
:handler on-edit})
[{:name (tr "dashboard.open-in-new-tab")
:id "file-open-new-tab"
:handler on-new-tab}
(when (and (not is-search-page?) can-edit)
{:name (tr "labels.rename")
:id "file-rename"
:handler on-edit})
(when (and (not is-search-page?) can-edit)
{:name (tr "dashboard.duplicate")
:id "file-duplicate"
:handler on-duplicate})
(when (and (not is-search-page?) can-edit)
{:name (tr "dashboard.duplicate")
:id "file-duplicate"
:handler on-duplicate})
(when (and (not is-lib-page?)
(not is-search-page?)
(or (seq current-projects) (seq other-teams))
can-edit)
{:name (tr "dashboard.move-to")
:id "file-move-to"
:options sub-options})
(when (and (not is-lib-page?)
(not is-search-page?)
(or (seq current-projects) (seq other-teams))
can-edit)
{:name (tr "dashboard.move-to")
:id "file-move-to"
:options sub-options})
(when (and (not is-search-page?)
can-edit)
(if (:is-shared file)
{:name (tr "dashboard.unpublish-shared")
:id "file-del-shared"
:handler on-del-shared}
{:name (tr "dashboard.add-shared")
:id "file-add-shared"
:handler on-add-shared}))
(when (and (not is-search-page?)
can-edit)
(if (:is-shared file)
{:name (tr "dashboard.unpublish-shared")
:id "file-del-shared"
:handler on-del-shared}
{:name (tr "dashboard.add-shared")
:id "file-add-shared"
:handler on-add-shared}))
{:name :separator}
{:name :separator}
{:name (tr "dashboard.download-binary-file")
:id "download-binary-file"
:handler on-export-binary-files}
{:name (tr "dashboard.download-binary-file")
:id "download-binary-file"
:handler on-export-binary-files}
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name :separator})
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name :separator})
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name (tr "labels.delete")
:id "file-delete"
:handler on-delete})]))]
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name (tr "labels.delete")
:id "file-delete"
:handler on-delete})])]
[:> context-menu*
{:on-close on-close

View File

@@ -86,7 +86,7 @@
(mf/defc grid-item-thumbnail*
{::mf/props :obj
::mf/private true}
[{:keys [can-edit file can-restore]}]
[{:keys [can-edit file]}]
(let [file-id (get file :id)
revn (get file :revn)
thumbnail-id (get file :thumbnail-id)
@@ -109,8 +109,7 @@
:message (ex-message cause)))))]
(partial rx/dispose! subscription))))
[:div {:class (stl/css-case :grid-item-th true
:deleted-item can-restore)
[:div {:class (stl/css :grid-item-th)
:style {:background-color bg-color}
:ref container}
(when visible?
@@ -132,15 +131,13 @@
(mf/defc grid-item-library*
{::mf/props :obj}
[{:keys [file can-restore]}]
[{:keys [file]}]
(mf/with-effect [file]
(when file
(let [font-ids (map :font-id (get-in file [:library-summary :typographies :sample] []))]
(run! fonts/ensure-loaded! font-ids))))
[:div {:class (stl/css-case :grid-item-th true
:library true
:deleted-item can-restore)}
[:div {:class (stl/css :grid-item-th :library)}
(if (nil? file)
[:> loader* {:class (stl/css :grid-loader)
:overlay true
@@ -240,13 +237,10 @@
;; --- Grid Item
(mf/defc grid-item-metadata*
[{:keys [file]}]
(let [time (ct/timeago (or (:will-be-deleted-at file)
(:modified-at file)))]
[:span {:class (stl/css :date)
:title (tr "dashboard.deleted.will-be-deleted-at" time)}
time]))
(mf/defc grid-item-metadata
[{:keys [modified-at]}]
(let [time (ct/timeago modified-at)]
[:span {:class (stl/css :date)} time]))
(defn create-counter-element
[_element file-count]
@@ -256,7 +250,7 @@
counter-el))
(mf/defc grid-item*
[{:keys [file origin can-edit selected-files can-restore]}]
[{:keys [file origin can-edit selected-files]}]
(let [file-id (get file :id)
state (mf/deref refs/dashboard-local)
@@ -295,13 +289,12 @@
on-navigate
(mf/use-fn
(mf/deps file-id can-restore)
(mf/deps file-id)
(fn [event]
(when-not can-restore
(let [menu-icon (mf/ref-val menu-ref)
target (dom/get-target event)]
(when-not (dom/child? target menu-icon)
(st/emit! (dcm/go-to-workspace :file-id file-id)))))))
(let [menu-icon (mf/ref-val menu-ref)
target (dom/get-target event)]
(when-not (dom/child? target menu-icon)
(st/emit! (dcm/go-to-workspace :file-id file-id))))))
on-drag-start
(mf/use-fn
@@ -419,8 +412,8 @@
[:div {:class (stl/css :overlay)}]
(if ^boolean is-library-view?
[:> grid-item-library* {:file file :can-restore can-restore}]
[:> grid-item-thumbnail* {:file file :can-edit can-edit :can-restore can-restore}])
[:> grid-item-library* {:file file}]
[:> grid-item-thumbnail* {:file file :can-edit can-edit}])
(when (and (:is-shared file) (not is-library-view?))
[:div {:class (stl/css :item-badge)} deprecated-icon/library])
@@ -432,7 +425,7 @@
:on-end edit
:max-length 250}]
[:h3 (:name file)])
[:> grid-item-metadata* {:file file}]]
[:& grid-item-metadata {:modified-at (:modified-at file)}]]
[:div {:class (stl/css-case :project-th-actions true :force-display menu-open?)}
[:div
@@ -458,12 +451,11 @@
:on-edit on-edit
:on-close on-menu-close
:origin origin
:parent-id (dm/str file-id "-action-menu")
:can-restore can-restore}]])]]]]]))
:parent-id (dm/str file-id "-action-menu")}]])]]]]]))
(mf/defc grid*
{::mf/props :obj}
[{:keys [files project origin limit create-fn can-edit selected-files can-restore]}]
[{:keys [files project origin limit create-fn can-edit selected-files]}]
(let [dragging? (mf/use-state false)
project-id (get project :id)
team-id (get project :team-id)
@@ -543,8 +535,7 @@
:key (dm/str (:id item))
:origin origin
:selected-files selected-files
:can-edit can-edit
:can-restore can-restore}])])
:can-edit can-edit}])])
:else
[:> empty-grid-placeholder*
@@ -557,7 +548,7 @@
:on-finish-import on-finish-import}])]))
(mf/defc line-grid-row
[{:keys [files selected-files dragging? limit can-edit can-restore] :as props}]
[{:keys [files selected-files dragging? limit can-edit] :as props}]
(let [elements limit
limit (if dragging? (dec limit) limit)]
[:ul {:class (stl/css :grid-row :no-wrap)
@@ -572,11 +563,10 @@
:file item
:selected-files selected-files
:can-edit can-edit
:key (dm/str (:id item))
:can-restore can-restore}])]))
:key (dm/str (:id item))}])]))
(mf/defc line-grid
[{:keys [project team files limit create-fn can-edit can-restore] :as props}]
[{:keys [project team files limit create-fn can-edit] :as props}]
(let [dragging? (mf/use-state false)
project-id (:id project)
team-id (:id team)
@@ -674,8 +664,7 @@
:selected-files selected-files
:dragging? @dragging?
:can-edit can-edit
:limit limit
:can-restore can-restore}]
:limit limit}]
:else
[:> empty-grid-placeholder*

View File

@@ -375,7 +375,3 @@ $thumbnail-default-height: deprecated.$s-168; // Default width
.grid-loader {
--icon-width: calc(var(--th-width, #{$thumbnail-default-width}) * 0.25);
}
.deleted-item {
opacity: 0.5;
}

View File

@@ -17,7 +17,6 @@
[app.main.data.project :as dpj]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.deleted :as deleted]
[app.main.ui.dashboard.grid :refer [line-grid]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.pin-button :refer [pin-button*]]
@@ -316,28 +315,28 @@
{::mf/props :obj}
[{:keys [team projects profile]}]
(let [team-id (get team :id)
recent-map (mf/deref ref:recent-files)
permisions (:permissions team)
can-edit (:can-edit permisions)
can-invite (or (:is-owner permisions)
(:is-admin permisions))
show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true))
show-team-hero? (deref show-team-hero*)
my-penpot? (= (:default-team-id profile) team-id)
default-team? (:is-default team)
projects
(let [projects
(mf/with-memo [projects]
(->> projects
(remove :deleted-at)
(sort-by :modified-at)
(reverse)))
team-id (get team :id)
recent-map (mf/deref ref:recent-files)
permisions (:permissions team)
can-edit (:can-edit permisions)
can-invite (or (:is-owner permisions)
(:is-admin permisions))
show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true))
show-team-hero? (deref show-team-hero*)
is-my-penpot (= (:default-team-id profile) team-id)
is-defalt-team? (:is-default team)
on-close
(mf/use-fn
(fn []
@@ -367,19 +366,16 @@
[:*
(when (and show-team-hero?
can-invite
(not default-team?))
(not is-defalt-team?))
[:> team-hero* {:team team :on-close on-close}])
[:div {:class (stl/css-case :dashboard-container true
:no-bg true
:dashboard-projects true
:with-team-hero (and (not my-penpot?)
(not default-team?)
:with-team-hero (and (not is-my-penpot)
(not is-defalt-team?)
show-team-hero?
can-invite))}
[:> deleted/menu* {:team-id team-id :section :dashboard-recent}]
(for [{:keys [id] :as project} projects]
;; FIXME: refactor this, looks inneficient
(let [files (when recent-map

View File

@@ -4,21 +4,16 @@
//
// Copyright (c) KALEIDOS INC
@use "common/refactor/common-refactor.scss" as deprecated;
@use "refactor/common-refactor.scss" as deprecated;
@use "common/refactor/common-dashboard";
@use "../ds/typography.scss" as t;
@use "../ds/_borders.scss" as *;
@use "../ds/spacing.scss" as *;
@use "../ds/_sizes.scss" as *;
@use "../ds/z-index.scss" as *;
.dashboard-container {
flex: 1 0 0;
width: 100%;
margin-inline-end: var(--sp-l);
border-top: $b-1 solid var(--panel-border-color);
margin-right: deprecated.$s-16;
border-top: deprecated.$s-1 solid var(--panel-border-color);
overflow-y: auto;
padding-bottom: var(--sp-xxxl);
padding-bottom: deprecated.$s-32;
}
.dashboard-projects {
@@ -32,16 +27,16 @@
.dashboard-shared {
width: calc(100vw - deprecated.$s-320);
margin-inline-end: deprecated.$s-52;
margin-right: deprecated.$s-52;
}
.search {
margin-block-start: var(--sp-m);
margin-top: deprecated.$s-12;
}
.dashboard-project-row {
--actions-opacity: 0;
margin-block-end: var(--sp-xxl);
margin-bottom: deprecated.$s-24;
position: relative;
&:hover,
@@ -65,12 +60,12 @@
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
gap: deprecated.$s-8;
width: 99%;
max-height: $sz-40;
padding: var(--sp-s) var(--sp-s) var(--sp-s) var(--sp-l);
margin-block-start: var(--sp-l);
border-radius: $br-4;
max-height: deprecated.$s-40;
padding: deprecated.$s-8 deprecated.$s-8 deprecated.$s-8 deprecated.$s-16;
margin-top: deprecated.$s-16;
border-radius: deprecated.$br-4;
}
.project-name-wrapper {
@@ -78,29 +73,30 @@
align-items: center;
justify-content: flex-start;
width: 100%;
min-height: var(--sp-xxxl);
margin-inline-start: var(--sp-s);
min-height: deprecated.$s-32;
margin-left: deprecated.$s-8;
}
.project-name {
@include t.use-typography("body-large");
@include deprecated.bodyLargeTypography;
@include deprecated.textEllipsis;
width: fit-content;
margin-inline-end: var(--sp-m);
margin-right: deprecated.$s-12;
line-height: 0.8;
color: var(--title-foreground-color-hover);
cursor: pointer;
height: var(--sp-l);
height: deprecated.$s-16;
}
.info-wrapper {
display: flex;
align-items: center;
gap: var(--sp-s);
gap: deprecated.$s-8;
}
.info,
.recent-files-row-title-info {
@include t.use-typography("body-medium");
@include deprecated.bodyMediumTypography;
color: var(--title-foreground-color);
@media (max-width: 760px) {
display: none;
@@ -110,16 +106,16 @@
.project-actions {
display: flex;
opacity: var(--actions-opacity);
margin-inline-start: var(--sp-xxxl);
margin-left: deprecated.$s-32;
}
.add-file-btn,
.options-btn {
@extend .button-tertiary;
height: var(--sp-xxxl);
width: var(--sp-xxxl);
margin: 0 var(--sp-s);
padding: var(--sp-s);
height: deprecated.$s-32;
width: deprecated.$s-32;
margin: 0 deprecated.$s-8;
padding: deprecated.$s-8;
}
.add-icon,
@@ -130,24 +126,24 @@
.grid-container {
width: 100%;
padding: 0 var(--sp-xs);
padding: 0 deprecated.$s-4;
}
.placeholder-placement {
margin: var(--sp-l) var(--sp-xxxl);
margin: deprecated.$s-16 deprecated.$s-32;
}
.show-more {
--show-more-color: var(--button-secondary-foreground-color-rest);
@include deprecated.buttonStyle;
@include t.use-typography("body-medium");
@include deprecated.bodyMediumTypography;
position: absolute;
top: var(--sp-s);
top: deprecated.$s-8;
right: deprecated.$s-52;
display: flex;
align-items: center;
justify-content: space-between;
column-gap: var(--sp-m);
column-gap: deprecated.$s-12;
color: var(--show-more-color);
&:hover {
@@ -156,8 +152,8 @@
}
.show-more-icon {
height: var(--sp-l);
width: var(--sp-l);
height: deprecated.$s-16;
width: deprecated.$s-16;
fill: none;
stroke: var(--show-more-color);
}
@@ -168,13 +164,13 @@
border-radius: deprecated.$br-8;
border: none;
display: flex;
margin: var(--sp-l);
padding: var(--sp-s);
margin: deprecated.$s-16;
padding: deprecated.$s-8;
position: relative;
img {
border-radius: $br-4;
height: var(--sp-xl) 0;
border-radius: deprecated.$br-4;
height: deprecated.$s-200;
width: auto;
@media (max-width: 1200px) {
@@ -189,18 +185,18 @@
flex-direction: column;
align-items: flex-start;
flex-grow: 1;
padding: var(--sp-xl) var(--sp-xl);
padding: deprecated.$s-20 deprecated.$s-20;
}
.title {
font-size: $sz-24;
font-size: deprecated.$fs-24;
color: var(--color-foreground-primary);
font-weight: deprecated.$fw400;
}
.info {
flex: 1;
font-size: $sz-16;
font-size: deprecated.$fs-16;
span {
color: var(--color-foreground-secondary);
display: block;
@@ -208,15 +204,15 @@
a {
color: var(--color-accent-primary);
}
padding: var(--sp-s) 0;
padding: deprecated.$s-8 0;
}
.close {
--close-icon-foreground-color: var(--icon-foreground);
position: absolute;
top: var(--sp-xl);
right: var(--sp-xxl);
width: var(--sp-xxl);
top: deprecated.$s-20;
right: deprecated.$s-24;
width: deprecated.$s-24;
background-color: transparent;
border: none;
cursor: pointer;
@@ -231,7 +227,7 @@
}
.invite {
height: var(--sp-xxxl);
height: deprecated.$s-32;
width: deprecated.$s-180;
}
@@ -239,8 +235,8 @@
display: flex;
align-items: center;
justify-content: center;
width: var(--sp-xl) 0;
height: var(--sp-xl) 0;
width: deprecated.$s-200;
height: deprecated.$s-200;
overflow: hidden;
border-radius: deprecated.$br-4;
@media (max-width: 1200px) {

View File

@@ -27,11 +27,11 @@
[app.main.ui.dashboard.comments :refer [comments-icon* comments-section]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.project-menu :refer [project-menu*]]
[app.main.ui.dashboard.subscription :refer [dashboard-cta*
get-subscription-type
[app.main.ui.dashboard.subscription :refer [subscription-sidebar*
menu-team-icon*
dashboard-cta*
show-subscription-dashboard-banner?
subscription-sidebar*]]
get-subscription-type]]
[app.main.ui.dashboard.team-form]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.icons :as deprecated-icon]
@@ -280,8 +280,8 @@
(mf/defc teams-selector-dropdown*
{::mf/private true}
[{:keys [team profile teams] :rest props}]
(let [on-create-click
[{:keys [team profile teams show-default-team allow-create-teams allow-create-org] :rest props}]
(let [on-create-team-click
(mf/use-fn #(st/emit! (modal/show :team-form {})))
on-team-click
@@ -290,18 +290,25 @@
(let [team-id (-> (dom/get-current-target event)
(dom/get-data "value")
(uuid/parse))]
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))]
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))
on-create-org-click
(mf/use-fn
(fn []
;; TODO update when org creation route is ready
(dom/open-new-window "localhost:3000/org/create")))]
[:> dropdown-menu* props
[:> dropdown-menu-item* {:on-click on-team-click
:data-value (:default-team-id profile)
:class (stl/css :team-dropdown-item)}
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]
(when show-default-team
[:> dropdown-menu-item* {:on-click on-team-click
:data-value (:default-team-id profile)
:class (stl/css :team-dropdown-item)}
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")]
(when (= (:default-team-id profile) (:id team))
tick-icon)]
[:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")]
(when (= (:default-team-id profile) (:id team))
tick-icon)])
(for [team-item (remove :is-default (vals teams))]
[:> dropdown-menu-item* {:on-click on-team-click
@@ -322,11 +329,19 @@
(when (= (:id team-item) (:id team))
tick-icon)])
[:hr {:role "separator" :class (stl/css :team-separator)}]
[:> dropdown-menu-item* {:on-click on-create-click
:class (stl/css :team-dropdown-item :action)}
[:span {:class (stl/css :icon-wrapper)} add-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]]]))
(when allow-create-teams
[:hr {:role "separator" :class (stl/css :team-separator)}]
[:> dropdown-menu-item* {:on-click on-create-team-click
:class (stl/css :team-dropdown-item :action)}
[:span {:class (stl/css :icon-wrapper)} add-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]])
(when allow-create-org
[:hr {:role "separator" :class (stl/css :team-separator)}]
[:> dropdown-menu-item* {:on-click on-create-org-click
:class (stl/css :team-dropdown-item :action)}
[:span {:class (stl/css :icon-wrapper)} add-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-org")]])]))
(mf/defc team-options-dropdown*
{::mf/private true}
@@ -476,9 +491,80 @@
:data-testid "delete-team"}
(tr "dashboard.delete-team")])]))
(mf/defc sidebar-org-switch*
[{:keys [team profile]}]
(let [teams (->> (mf/deref refs/teams)
vals
(group-by :organization-id)
(map (fn [[_group entries]] (first entries)))
vec
(d/index-by :id))
teams (update-vals teams
(fn [t]
(assoc t :name (str "ORG: " (:organization-name t)))))
team (assoc team :name (str "ORG: " (:organization-name team)))
show-teams-menu*
(mf/use-state false)
show-teams-menu?
(deref show-teams-menu*)
on-show-teams-click
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(swap! show-teams-menu* not)))
on-show-teams-keydown
(mf/use-fn
(fn [event]
(when (or (kbd/space? event)
(kbd/enter? event))
(dom/prevent-default event)
(dom/stop-propagation event)
(some-> (dom/get-current-target event)
(dom/click!)))))
close-teams-menu
(mf/use-fn #(reset! show-teams-menu* false))]
[:div {:class (stl/css :sidebar-team-switch)}
[:div {:class (stl/css :switch-content)}
[:button {:class (stl/css :current-team)
:on-click on-show-teams-click
:on-key-down on-show-teams-keydown}
[:div {:class (stl/css :team-name)}
[:img {:src (cf/resolve-team-photo-url team)
:class (stl/css :team-picture)
:alt (:name team)}]
[:span {:class (stl/css :team-text) :title (:name team)} (:name team)]]
arrow-icon]]
;; Teams Dropdown
[:> teams-selector-dropdown* {:show show-teams-menu?
:on-close close-teams-menu
:id "organizations-list"
:class (stl/css :dropdown :teams-dropdown)
:team team
:profile profile
:teams teams
:show-default-team false
:allow-create-teams false
:allow-create-org true}]]))
(mf/defc sidebar-team-switch*
[{:keys [team profile]}]
(let [teams (mf/deref refs/teams)
(let [nitrate? (contains? cf/flags :nitrate)
org-id (when nitrate? (:organization-id team))
teams (cond->> (mf/deref refs/teams)
nitrate?
(filter #(= (-> % val :organization-id) org-id)))
subscription
(get team :subscription)
@@ -586,7 +672,10 @@
:class (stl/css :dropdown :teams-dropdown)
:team team
:profile profile
:teams teams}]
:teams teams
:show-default-team true
:allow-create-teams true
:allow-create-org false}]
[:> team-options-dropdown* {:show show-team-options-menu?
:on-close close-team-options-menu
@@ -703,6 +792,8 @@
[:*
[:div {:class (stl/css-case :sidebar-content true)
:ref container}
(when (contains? cf/flags :nitrate)
[:> sidebar-org-switch* {:team team :profile profile}])
[:> sidebar-team-switch* {:team team :profile profile}]
[:> sidebar-search* {:search-term search-term

View File

@@ -205,10 +205,7 @@
:cmd :export-frames
:origin origin}]))
;; FIXME: deprecated, should be refactored in two components and use
;; the generic progress reporter
(mf/defc progress-widget
(mf/defc export-progress-widget
{::mf/wrap [mf/memo]}
[]
(let [state (mf/deref refs/export)
@@ -220,11 +217,11 @@
detail-visible? (:detail-visible state)
widget-visible? (:widget-visible state)
progress (:progress state)
items (:exports state)
total (or (:total state) (count items))
exports (:exports state)
total (count exports)
complete? (= progress total)
circ (* 2 Math/PI 12)
pct (if (zero? total) circ (- circ (* circ (/ progress total))))
pct (- circ (* circ (/ progress total)))
pwidth
(if error?
@@ -246,20 +243,16 @@
title
(cond
error? (tr "workspace.options.exporting-object-error")
complete? (tr "workspace.options.exporting-complete")
healthy? (tr "workspace.options.exporting-object")
(not healthy?) (tr "workspace.options.exporting-object-slow"))
error? (tr "workspace.options.exporting-object-error")
complete? (tr "workspace.options.exporting-complete")
healthy? (tr "workspace.options.exporting-object")
(not healthy?) (tr "workspace.options.exporting-object-slow"))
retry-last-operation
(mf/use-fn
(fn []
(st/emit! (de/retry-last-export))))
retry-last-export
(mf/use-fn #(st/emit! (de/retry-last-export)))
toggle-detail-visibility
(mf/use-fn
(fn []
(st/emit! (de/toggle-detail-visibililty))))]
(mf/use-fn #(st/emit! (de/toggle-detail-visibililty)))]
[:*
(when widget-visible?
@@ -290,11 +283,11 @@
error-icon
neutral-icon)
[:div {:class (stl/css :export-progress-title)}
[:div {:class (stl/css :title-text)} title]
[:p {:class (stl/css :export-progress-title)}
title
(if error?
[:button {:class (stl/css :retry-btn)
:on-click retry-last-operation}
:on-click retry-last-export}
(tr "workspace.options.retry")]
[:span {:class (stl/css :progress)}

View File

@@ -64,8 +64,7 @@
["/fonts" :dashboard-fonts]
["/fonts/providers" :dashboard-font-providers]
["/libraries" :dashboard-libraries]
["/files" :dashboard-files]
["/deleted" :dashboard-deleted]]
["/files" :dashboard-files]]
["/dashboard/team/:team-id"
["/members" :dashboard-legacy-team-members]

View File

@@ -18,18 +18,16 @@
(defn- on-error
[form error]
(let [data (ex-data error)]
(case (:code data)
:old-password-not-match
(swap! form assoc-in [:extra-errors :password-old]
{:message (tr "errors.wrong-old-password")})
(case (:code (ex-data error))
:old-password-not-match
(swap! form assoc-in [:errors :password-old]
{:message (tr "errors.wrong-old-password")})
:email-as-password
(swap! form assoc-in [:errors :password-1]
{:message (tr "errors.email-as-password")})
:email-as-password
(swap! form assoc-in [:extra-errors :password-1]
{:message (tr "errors.email-as-password")})
(let [msg (tr "generic.error")]
(st/emit! (ntf/error msg))))))
(let [msg (tr "generic.error")]
(st/emit! (ntf/error msg)))))
(defn- on-success
[form]

View File

@@ -14,7 +14,7 @@
[app.main.data.viewer.shortcuts :as sc]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.exports.assets :refer [progress-widget]]
[app.main.ui.exports.assets :refer [export-progress-widget]]
[app.main.ui.formats :as fmt]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.viewer.comments :refer [comments-menu]]
@@ -167,7 +167,7 @@
(open-share-dialog)))
[:div {:class (stl/css :options-zone)}
[:& progress-widget]
[:& export-progress-widget]
(case section
:interactions [:*

View File

@@ -22,7 +22,7 @@
[app.main.ui.dashboard.team]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.exports.assets :refer [progress-widget]]
[app.main.ui.exports.assets :refer [export-progress-widget]]
[app.main.ui.formats :as fmt]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.presence :refer [active-sessions]]
@@ -200,7 +200,7 @@
[:div {:class (stl/css :users-section)}
[:& active-sessions]]
[:& progress-widget]
[:& export-progress-widget]
[:div {:class (stl/css :separator)}]

View File

@@ -65,35 +65,34 @@
on-pointer-down
(fn [event]
(when (dom/left-mouse? event)
(dom/stop-propagation event)
(dom/prevent-default event)
(dom/stop-propagation event)
(dom/prevent-default event)
;; FIXME: revisit this, using meta here breaks equality checks
(when (and is-new (some? (meta position)))
(st/emit! (drp/create-node-at-position (meta position))))
;; FIXME: revisit this, using meta here breaks equality checks
(when (and is-new (some? (meta position)))
(st/emit! (drp/create-node-at-position (meta position))))
(let [is-shift (kbd/shift? event)
is-mod (kbd/mod? event)]
(cond
is-last
(st/emit! (drp/reset-last-handler))
(let [is-shift (kbd/shift? event)
is-mod (kbd/mod? event)]
(cond
is-last
(st/emit! (drp/reset-last-handler))
(and is-move is-mod (not is-curve))
(st/emit! (drp/make-curve position))
(and is-move is-mod (not is-curve))
(st/emit! (drp/make-curve position))
(and is-move is-mod is-curve)
(st/emit! (drp/make-corner position))
(and is-move is-mod is-curve)
(st/emit! (drp/make-corner position))
is-move
;; If we're dragging a selected item we don't change the selection
(st/emit! (drp/start-move-path-point position is-shift))
is-move
;; If we're dragging a selected item we don't change the selection
(st/emit! (drp/start-move-path-point position is-shift))
(and is-draw is-start-path)
(st/emit! (drp/start-path-from-point position))
(and is-draw is-start-path)
(st/emit! (drp/start-path-from-point position))
(and is-draw (not is-start-path))
(st/emit! (drp/close-path-drag-start position))))))]
(and is-draw (not is-start-path))
(st/emit! (drp/close-path-drag-start position)))))]
[:g.path-point
[:circle.path-point

View File

@@ -6,7 +6,7 @@
(ns app.main.ui.workspace.shapes.text.editor
(:require
["@penpot/draft-js" :as draft]
["draft-js" :as draft]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]

View File

@@ -271,10 +271,8 @@
:font-variant-id new-variant-id
:font-weight (:weight variant)
:font-style (:style variant)}))
;; NOTE: the select component we are using does not fire on-blur event
;; so we need to call on-blur manually
(when (some? on-blur)
(on-blur)))))
(dom/blur! (dom/get-target new-variant-id)))))
on-font-select
(mf/use-fn
@@ -308,7 +306,7 @@
:title (tr "inspect.attributes.typography.font-family")
:on-click #(reset! open-selector? true)}
(cond
(or (= :multiple font-id) (= "mixed" font-id))
(= :multiple font-id)
"--"
(some? font)

View File

@@ -19,5 +19,5 @@
}
.threads {
position: absolute;
position: fixed;
}

View File

@@ -54,7 +54,6 @@
[app.plugins.ruler-guides :as rg]
[app.plugins.text :as text]
[app.plugins.utils :as u]
[app.util.http :as http]
[app.util.object :as obj]
[beicon.v2.core :as rx]
[cuerdas.core :as str]))
@@ -1196,12 +1195,7 @@
(js/Promise.
(fn [resolve reject]
(->> (rp/cmd! :export payload)
(rx/mapcat (fn [{:keys [uri]}]
(->> (http/send! {:method :get
:uri uri
:response-type :blob
:omit-default-headers true})
(rx/map :body))))
(rx/mapcat #(rp/cmd! :export {:cmd :get-resource :wait true :id (:id %) :blob? true}))
(rx/mapcat #(.arrayBuffer %))
(rx/map #(js/Uint8Array. %))
(rx/subs! resolve reject))))))))

View File

@@ -194,12 +194,7 @@
:addToken
(fn [type-str name value]
(let [type (cto/dtcg-token-type->token-type type-str)
value (case type
:font-family (ctob/convert-dtcg-font-family (js->clj value))
:typography (ctob/convert-dtcg-typography-composite (js->clj value))
:shadow (ctob/convert-dtcg-shadow-composite (js->clj value))
(js->clj value))]
(let [type (cto/dtcg-token-type->token-type type-str)]
(cond
(nil? type)
(u/display-not-valid :addTokenType type-str)

View File

@@ -18,7 +18,6 @@
[app.common.types.path :as path]
[app.common.types.path.impl :as path.impl]
[app.common.types.shape.layout :as ctl]
[app.common.types.text :as txt]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.refs :as refs]
@@ -965,8 +964,8 @@
(set-shape-children children)
(set-shape-corners corners)
(set-shape-blur blur)
(when (= type :group)
(set-masked (boolean masked)))
(when (and (= type :group) masked)
(set-masked masked))
(when (= type :bool)
(set-shape-bool-type bool-type))
(when (and (some? content)
@@ -1412,23 +1411,20 @@
(get span))
text (subs (:text element) start-pos end-pos)]
(d/patch-object
txt/default-text-attrs
(d/without-nils
{:x x
:y (+ y height)
:width width
:height height
:direction (dr/translate-direction direction)
:font-family (get element :font-family)
:font-size (get element :font-size)
:font-weight (get element :font-weight)
:text-transform (get element :text-transform)
:text-decoration (get element :text-decoration)
:letter-spacing (get element :letter-spacing)
:font-style (get element :font-style)
:fills (get element :fills)
:text text}))))))]
{:x x
:y (+ y height)
:width width
:height height
:direction (dr/translate-direction direction)
:font-family (get element :font-family)
:font-size (get element :font-size)
:font-weight (get element :font-weight)
:text-transform (get element :text-transform)
:text-decoration (get element :text-decoration)
:letter-spacing (get element :letter-spacing)
:font-style (get element :font-style)
:fills (get element :fills)
:text text}))))]
(mem/free)
result)))

View File

@@ -233,8 +233,8 @@
(api/set-shape-shadows (:shadow shape)))
:masked-group
(when (cfh/group-shape? shape)
(api/set-masked (boolean (:masked-group shape))))
(when (cfh/mask-shape? shape)
(api/set-masked (:masked-group shape)))
:content
(cond

View File

@@ -6,11 +6,11 @@
(ns app.util.code-highlight
(:require
["highlight.js" :as hljs]
["@penpot/hljs" :as hljs]
[app.util.dom :as dom]))
(defn highlight!
{:lazy-loadable true}
[node]
(dom/set-data! node "highlighted" nil)
(.highlightElement hljs/default node))
(hljs/highlightElement node))

View File

@@ -135,18 +135,8 @@
(not extra-errors)
valid?)))))
(defn- make-initial-state
[initial-data]
(let [initial (if (fn? initial-data) (initial-data) initial-data)
initial (d/nilv initial {})]
{:initial initial
:data initial
:errors {}
:touched {}}))
(defn- create-form-mutator
[internal-state rerender-fn wrap-update-fn opts]
[internal-state rerender-fn wrap-update-fn initial opts]
(reify
IDeref
(-deref [_]
@@ -155,10 +145,7 @@
IReset
(-reset! [_ new-value]
(if (nil? new-value)
(let [initial (-> (mf/ref-val internal-state)
(get :initial)
(make-initial-state))]
(mf/set-ref-val! internal-state initial))
(mf/set-ref-val! internal-state (if (fn? initial) (initial) initial))
(mf/set-ref-val! internal-state new-value))
(rerender-fn))
@@ -189,20 +176,26 @@
initial
(mf/with-memo [initial]
(make-initial-state initial))
{:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}})
internal-state
(mf/use-ref initial)
(mf/use-ref nil)
form-mutator
(mf/with-memo [schema validators]
(mf/with-memo [initial schema validators]
(let [mutator (create-form-mutator internal-state rerender-fn wrap-update-schema-fn
initial
(select-keys opts [:schema :validators]))]
(swap! mutator identity)
mutator))]
(mf/with-effect [initial]
(mf/set-ref-val! internal-state initial))
;; Initialize internal state once
(mf/with-effect []
(mf/with-layout-effect []
(mf/set-ref-val! internal-state initial))
(mf/with-effect [initial]

View File

@@ -50,7 +50,6 @@
{:label "Føroyskt mál (community)" :value "fo"}
{:label "Korean (community)" :value "ko"}
{:label "עִבְרִית (community)" :value "he"}
{:label "आधुनिक मानक हिन्दी (community)" :value "hi"}
{:label "عربي/عربى (community)" :value "ar"}
{:label "فارسی (community)" :value "fa"}
{:label "日本語 (Community)" :value "ja_jp"}
@@ -70,6 +69,7 @@
(-> (.-language globals/navigator)
(parse-locale))))
;; Set initial translation loading state as globaly stored variable;
;; this facilitates hot reloading
(when-not (exists? (unchecked-get globals/global "penpotTranslations"))
@@ -93,8 +93,14 @@
(def ^:dynamic *current-locale*
(get-current))
(defonce locale
(l/atom *current-locale*))
(defonce state
(l/atom {:render 0 :locale *current-locale*}))
(defn- assign-current-locale
[state locale]
(-> state
(update :render inc)
(assoc :locale locale)))
(defn- get-translations
"Get globaly stored mutable object with all loaded translations"
@@ -108,10 +114,6 @@
(unchecked-set translations locale data)
nil))
(defn set-default-translations
[data]
(set-translations cf/default-language data))
(defn- load
[locale]
(let [path (str "./translation." locale ".js?version=" (:full cf/version))]
@@ -120,14 +122,15 @@
(p/fnly (fn [data cause]
(if cause
(js/console.error "unexpected error on fetching locale" cause)
(set-translations locale data)))))))
(do
(set! *current-locale* locale)
(set-translations locale data)
(swap! state assign-current-locale locale))))))))
(defn init
"Initialize the i18n module"
[]
(load *current-locale*)
(when-not (= *current-locale* cf/default-language)
(load cf/default-language)))
(load *current-locale*))
(defn set-locale
[lname]
@@ -142,10 +145,7 @@
(recur (rest locales)))
cf/default-language))))]
(->> (load lname)
(p/fnly (fn [_r _c]
(set! *current-locale* lname)
(reset! locale lname))))))
(load lname)))
(deftype C [val]
IDeref
@@ -206,7 +206,9 @@
:className class
:on-click on-click}]))
(add-watch locale "common.time"
(add-watch state "common.time"
(fn [_ _ pv cv]
(when (not= pv cv)
(ct/set-default-locale cv))))
(let [pv (get pv :locale)
cv (get cv :locale)]
(when (not= pv cv)
(ct/set-default-locale! cv)))))

View File

@@ -46,10 +46,6 @@
[event]
(= "end" (get-type event)))
(defn progress?
[event]
(= "progress" (get-type event)))
(defn event?
[event]
(= "event" (get-type event)))

View File

@@ -187,23 +187,19 @@
style-value (normalize-style-value style-name v)]
(assoc acc style-name style-value)))) {} style-defaults)))
(def mixed-values #{:mixed :multiple "mixed" "multiple"})
(defn get-styles-from-style-declaration
"Returns a ClojureScript object compatible with text nodes"
[style-declaration & {:keys [removed-mixed] :or {removed-mixed false}}]
[style-declaration]
(reduce
(fn [acc k]
(if (contains? mapping k)
(let [style-name (get-style-name-as-css-variable k)
[_ style-decode] (get mapping k)
style-value (.getPropertyValue style-declaration style-name)]
(when (or (not removed-mixed) (not (contains? mixed-values style-value)))
(assoc acc k (style-decode style-value))))
(assoc acc k (style-decode style-value)))
(let [style-name (get-style-name k)
style-value (normalize-attr-value k (.getPropertyValue style-declaration style-name))]
(when (or (not removed-mixed) (not (contains? mixed-values style-value)))
(assoc acc k style-value))))) {} txt/text-style-attrs))
(assoc acc k style-value)))) {} txt/text-style-attrs))
(defn get-styles-from-event
"Returns a ClojureScript object compatible with text nodes"

View File

@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2025-12-22 15:34+0000\n"
"Last-Translator: Shuaib Zahda <shuaib.zahda@gmail.com>\n"
"PO-Revision-Date: 2025-11-22 10:51+0000\n"
"Last-Translator: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>\n"
"Language-Team: Arabic <https://hosted.weblate.org/projects/penpot/frontend/"
"ar/>\n"
"Language: ar\n"
@@ -10,7 +10,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"X-Generator: Weblate 5.15.1\n"
"X-Generator: Weblate 5.15-dev\n"
#: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:159, src/app/main/ui/viewer/login.cljs:100
msgid "auth.already-have-account"
@@ -402,10 +402,8 @@ msgstr ""
"أصولهم*؟"
#: src/app/main/ui/exports/files.cljs:164
#, fuzzy
msgid "dashboard.export.options.all.message"
msgstr ""
"سيتم ادراج الملفات التي لها مكتبات مشتركة في التصدير، مع الحفاظ على روابطهم."
msgstr "سيتم ادراج الملفات التي لها مكتبات مشتركة في التصدير، مع الحفاظ على روابطهم."
#: src/app/main/ui/exports/files.cljs:165
msgid "dashboard.export.options.all.title"

View File

@@ -1575,6 +1575,10 @@ msgstr ""
"Pokud se chcete dozvědět více o inspektorovi designu, navštivte centrum "
"nápovědy společnosti Penpot"
#: src/app/main/ui/inspect/right_sidebar.cljs:240
msgid "inspect.empty.more-info"
msgstr "Více informací o inspektorovi"
#: src/app/main/ui/inspect/right_sidebar.cljs:232
msgid "inspect.empty.select"
msgstr ""

View File

@@ -1,19 +1,19 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2025-12-22 15:34+0000\n"
"Last-Translator: Anonymous <noreply@weblate.org>\n"
"Language-Team: German <https://hosted.weblate.org/projects/penpot/frontend/"
"de/>\n"
"PO-Revision-Date: 2025-11-03 20:51+0000\n"
"Last-Translator: Stas Haas <stas@girafic.de>\n"
"Language-Team: German "
"<https://hosted.weblate.org/projects/penpot/frontend/de/>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.15.1\n"
"X-Generator: Weblate 5.14.1-dev\n"
#: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:159, src/app/main/ui/viewer/login.cljs:100
msgid "auth.already-have-account"
msgstr "Sie haben bereits ein Konto?"
msgstr "Sie haben schon ein Konto?"
#: src/app/main/ui/auth/recovery_request.cljs:113, src/app/main/ui/auth/register.cljs:238
msgid "auth.check-mail"
@@ -584,11 +584,10 @@ msgstr ""
"machen?"
#: src/app/main/ui/exports/files.cljs:164
#, fuzzy
msgid "dashboard.export.options.all.message"
msgstr ""
"Dateien mit geteilten Bibliotheken werden exportiert, und ihre Verknüpfungen "
"bleiben erhalten."
"Dateien mit geteilten Bibliotheken werden exportiert, und ihre "
"Verknüpfungen bleiben erhalten."
#: src/app/main/ui/exports/files.cljs:165
msgid "dashboard.export.options.all.title"
@@ -1800,6 +1799,10 @@ msgstr ""
"Für weitere Informationen zum Thema \"Auswerten von "
"Design-Spezifikationen\", besuchen Sie bitte das Penpot-Hilfezentrum"
#: src/app/main/ui/inspect/right_sidebar.cljs:240
msgid "inspect.empty.more-info"
msgstr "Weitere Informationen zur Inspektion"
#: src/app/main/ui/inspect/right_sidebar.cljs:232
msgid "inspect.empty.select"
msgstr ""
@@ -1807,7 +1810,6 @@ msgstr ""
"Eigenschaften und Code zu überprüfen"
#: src/app/main/ui/inspect/right_sidebar.cljs:166
#, fuzzy
msgid "inspect.layer-info"
msgstr "Info zur Ebene"
@@ -2628,7 +2630,7 @@ msgstr "Eigene Schriftarten hochladen"
#: src/app/main/ui/dashboard/fonts.cljs:252
msgid "labels.uploading"
msgstr "Lädt hoch…"
msgstr "Hochladen…"
#: src/app/main/ui/inspect/right_sidebar.cljs:65, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1028
msgid "labels.variant"
@@ -3905,7 +3907,7 @@ msgstr "Kopieren"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:96
msgid "shortcuts.copy-link"
msgstr "Link kopieren"
msgstr "Link in die Zwischenablage kopieren"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:106
#, unused
@@ -6703,7 +6705,7 @@ msgstr "Als CSS kopieren (verschachtelte Ebenen)"
#: src/app/main/ui/workspace/context_menu.cljs:202
msgid "workspace.shape.menu.copy-link"
msgstr "Link kopieren"
msgstr "Link in die Zwischenablage kopieren"
#: src/app/main/ui/workspace/context_menu.cljs:215
msgid "workspace.shape.menu.copy-paste-as"
@@ -7502,662 +7504,3 @@ msgstr "Automatisch gespeicherte Versionen werden für %s Tage aufbewahrt."
#, unused
msgid "workspace.viewport.click-to-close-path"
msgstr "Klicken Sie, um den Pfad zu schließen"
#: src/app/main/ui/dashboard/team.cljs:765
msgid "dashboard.invitation-modal.delete"
msgstr "Diese Einladungen werden gelöscht:"
#: src/app/main/ui/dashboard/team.cljs:766
msgid "dashboard.invitation-modal.resend"
msgstr "Diese Einladungen werden erneut gesendet:"
#: src/app/main/ui/dashboard/team.cljs:933
msgid "team.invitations-selected"
msgid_plural "team.invitations-selected"
msgstr[0] "Eine Einladung ausgewählt"
msgstr[1] "%s Einladungen ausgewählt"
#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:121
msgid "workspace.tokens.themes-description"
msgstr ""
"Hier können Sie ihre Themes verwalten, ein- oder ausschalten und aktive Sets "
"auswählen."
#: src/app/main/ui/workspace/tokens/management.cljs:143
msgid "workspace.tokens.inactive-set"
msgstr "Inaktiv"
#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:174
msgid "workspace.tokens.no-active-sets"
msgstr "Keine Sets aktiviert"
#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:173
msgid "workspace.tokens.num-active-sets"
msgstr "%s aktivierte Sets"
#: src/app/main/ui/workspace/colorpicker/color_tokens.cljs:35
msgid "color-token.empty-state"
msgstr ""
"Keine Farb-Tokens vorhanden. Aktivieren sie Sets und/oder Themes oder fügen "
"Sie neue Tokens hinzu."
#: src/app/main/ui/ds/controls/numeric_input.cljs:99
msgid "ds.inputs.numeric-input.no-applicable-tokens"
msgstr ""
"Es sind keine passenden Tokens in aktivierten Sets oder Themes vorhanden."
#: src/app/main/ui/ds/controls/utilities/token_field.cljs:41, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:98, src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:105
msgid "ds.inputs.token-field.no-active-token-option"
msgstr "Dieser Token ist nicht Teil eines aktiven Sets oder ungültig."
#: src/app/main/errors.cljs:307
msgid "errors.deprecated"
msgstr ""
"Tut uns leid! Diese Datei beinhaltet veraltete Penpot-Assets und kann "
"deshalb nicht geöffnet werden."
#: src/app/main/errors.cljs:310
msgid "errors.deprecated.contact.after"
msgstr "damit wir Ihnen helfen können."
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:52, src/app/main/ui/workspace/tokens/management/create/form.cljs:80
msgid "errors.field-max-length"
msgstr "Darf maximal %s Zeichen enthalten."
#: src/app/main/ui/settings/feedback.cljs:143
msgid "feedback.other-ways-contact"
msgstr "Weitere Möglichkeiten uns zu kontaktieren"
#: src/app/main/ui/settings/feedback.cljs:122
msgid "feedback.description-placeholder"
msgstr "Bitte beschreiben Sie ihr Feedback"
#: src/app/main/ui/settings/feedback.cljs:126
msgid "feedback.penpot.link"
msgstr ""
"Wenn sich Ihr Feedback auf eine Datei oder Projekt bezieht, können Sie einen "
"Link hinzufügen:"
#: src/app/main/ui/settings/feedback.cljs:101
msgid "feedback.title-contact-us"
msgstr "Kontakt"
#: src/app/main/ui/settings/feedback.cljs:110, src/app/main/ui/settings/feedback.cljs:111
msgid "feedback.type"
msgstr "Art"
#: src/app/main/ui/settings/feedback.cljs:115
msgid "feedback.type.doubt"
msgstr "Bedenken"
#: src/app/main/ui/settings/feedback.cljs:113
msgid "feedback.type.idea"
msgstr "Idee"
#: src/app/main/ui/settings/feedback.cljs:114
msgid "feedback.type.issue"
msgstr "Problem"
#: src/app/main/ui/inspect/styles/rows/color_properties_row.cljs:120
msgid "inspect.attributes.image.preview"
msgstr "Bildvorschau"
#: src/app/main/ui/inspect/right_sidebar.cljs:170
msgid "inspect.color-space-label"
msgstr "Farbraum auswählen"
#: src/app/main/ui/inspect/styles/style_box.cljs:68
msgid "inspect.tabs.styles.copy-shorthand"
msgstr "CSS-Stile kopieren"
#: src/app/main/ui/inspect/styles/style_box.cljs:60, src/app/main/ui/workspace/colorpicker/color_tokens.cljs:179
msgid "inspect.tabs.styles.toggle-style"
msgstr "%s-Schaltfläche ein-/ausblenden"
#: src/app/main/ui/inspect/styles/style_box.cljs:21
msgid "inspect.tabs.styles.token-panel"
msgstr "Token Sets und Themes"
#: src/app/main/ui/inspect/styles/rows/color_properties_row.cljs:102, src/app/main/ui/inspect/styles/rows/properties_row.cljs:60
msgid "inspect.tabs.styles.token-resolved-value"
msgstr "Festgelegter Wert:"
#: src/app/main/ui/inspect/styles/style_box.cljs:26
msgid "labels.blur"
msgstr "Weichzeichnen"
#: src/app/main/ui/dashboard/sidebar.cljs:1031
msgid "labels.community-contributions"
msgstr "Community & Beiträge"
#: src/app/main/ui/static.cljs:406
msgid "labels.contact-support"
msgstr "Support kontaktieren"
#: src/app/main/ui/settings/sidebar.cljs:136
msgid "labels.contact-us"
msgstr "Kontakt"
#: src/app/main/ui/static.cljs:68
msgid "labels.copyright-period"
msgstr "Kaleidos © 2019-heute"
#: src/app/main/ui/settings/feedback.cljs:134, src/app/main/ui/static.cljs:400
msgid "labels.download"
msgstr "%s herunterladen"
#: src/app/main/ui/inspect/styles/style_box.cljs:23
msgid "labels.fill"
msgstr "Fläche"
#: src/app/main/ui/dashboard/sidebar.cljs:1020
msgid "labels.help-learning"
msgstr "Hilfe & Lernen"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:667
msgid "labels.reference"
msgstr "Referenz"
#: src/app/main/ui/dashboard/team.cljs:788
msgid "labels.resend"
msgstr "Erneut senden"
#: src/app/main/ui/dashboard/sidebar.cljs:873
msgid "labels.version-notes"
msgstr "Hinweise zu Version %s"
#: src/app/main/ui/inspect/styles/style_box.cljs:32
msgid "labels.visibility"
msgstr "Sichtbarkeit"
#: src/app/main/ui/static.cljs:397
msgid "labels.internal-error.desc-message-second"
msgstr "Probieren Sie es erneut oder kontaktieren Sie unseren Support."
#: src/app/main/ui/ds/product/loader.cljs:26
msgid "loader.tips.04.title"
msgstr ""
#: src/app/main/ui/dashboard/team.cljs:825
msgid "notifications.invitation-deleted"
msgstr "Einladung erfolgreich widerrufen"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:97
msgid "shortcuts.create-component-variant"
msgstr "Komponente / Variante erstellen"
#: src/app/main/ui/workspace/sidebar/assets/groups.cljs:81
msgid "workspace.assets.component-group-options"
msgstr "Optionen für diese Gruppe"
#: src/app/main/ui/workspace/colorpicker.cljs:427, src/app/main/ui/workspace/colorpicker.cljs:439
msgid "workspace.colorpicker.color-tokens"
msgstr "Farb-Tokens"
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:499
msgid "workspace.component.swap.loop-error"
msgstr "Komponenten können nicht in sich selbst verschachtelt werden."
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:498
msgid "workspace.component.switch.loop-error-multi"
msgstr ""
"Einige Elemente konnten nicht ausgetauscht werden. Komponenten können nicht "
"in sich selbst verschachtelt werden."
#: src/app/main/ui/workspace/libraries.cljs:107, src/app/main/ui/workspace/libraries.cljs:133
msgid "workspace.libraries.colors"
msgid_plural "workspace.libraries.colors"
msgstr[0] "1 Farbe"
msgstr[1] "%s Farben"
#: src/app/main/ui/workspace/libraries.cljs:101, src/app/main/ui/workspace/libraries.cljs:125
msgid "workspace.libraries.components"
msgid_plural "workspace.libraries.components"
msgstr[0] "1 Komponente"
msgstr[1] "%s Komponenten"
#: src/app/main/ui/workspace/libraries.cljs:104, src/app/main/ui/workspace/libraries.cljs:129
msgid "workspace.libraries.graphics"
msgid_plural "workspace.libraries.graphics"
msgstr[0] "%s Grafik"
msgstr[1] "%s Grafiken"
#: src/app/main/ui/workspace/libraries.cljs:110, src/app/main/ui/workspace/libraries.cljs:137
msgid "workspace.libraries.typography"
msgid_plural "workspace.libraries.typography"
msgstr[0] "1 Textstil"
msgstr[1] "%s Textstile"
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1055
msgid "workspace.options.component.unlinked"
msgstr "Getrennt"
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1330
msgid "workspace.options.component.variant.duplicated.group.locate"
msgstr "Doppelte Varianten anzeigen"
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1327
msgid "workspace.options.component.variant.duplicated.group.title"
msgstr "Einige Varianten haben identische Eigenschaften und Werte"
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:268
msgid "workspace.options.component.variant.duplicated.single.all"
msgstr ""
"Diese Varianten haben identische Eigenschaften und Werte. Geben Sie jeder "
"Variante einen eindeutigen Wert."
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:265
msgid "workspace.options.component.variant.duplicated.single.one"
msgstr ""
"Diese Variante hat die selben Eigenschaften order Werte wie eine andere "
"Variante. Geben Sie jeder Variante einen eindeutigen Wert."
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:271
msgid "workspace.options.component.variant.duplicated.single.some"
msgstr ""
"Einige dieser Varianten haben identische Eigenschaften und Werte. Geben Sie "
"jeder Variante einen eindeutigen Wert."
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:550
msgid "workspace.options.component.variant.malformed.copy"
msgstr ""
"Diese Komponente hat Varianten mit ungültigen Namen. Stellen Sie sicher, das "
"jede Variante korrekt benannt ist."
#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:424
msgid "workspace.options.component.variant.malformed.structure.title"
msgstr "Versuchen Sie diesem Schema zu folgen:"
#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:54
msgid "workspace.options.component.variants-help-modal.intro"
msgstr ""
"Um Änderungen beizubehalten wenn Sie Varianten wechseln, verbindet Penpot "
"Ebenen die:"
#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:91
msgid "workspace.options.component.variants-help-modal.outro"
msgstr ""
"Jede Änderung (z.B. Ebenen umbenennen oder gruppieren) trennt die "
"Verbindung. Änderungen können rückgängig gemacht werden, um die Verbindung "
"wiederherzustellen."
#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:67
msgid "workspace.options.component.variants-help-modal.rule1"
msgstr "Den gleichen Namen haben."
#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:76
msgid "workspace.options.component.variants-help-modal.rule2"
msgstr "Vom gleichen Typ sind."
#: src/app/main/ui/workspace/sidebar/options/menus/variants_help_modal.cljs:77
msgid "workspace.options.component.variants-help-modal.rule2.detail"
msgstr ""
"Rechtecke, Ellipsen, Pfade und boolesche Operationen gelten als der gleiche "
"Typ."
#: src/app/main/data/workspace/tokens/library_edit.cljs:209, src/app/main/data/workspace/tokens/library_edit.cljs:452
msgid "workspace.tokens.duplicate-suffix"
msgstr "Kopie"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:581
msgid "workspace.tokens.edit-token"
msgstr "%s Token bearbeiten"
#: src/app/main/data/workspace/tokens/errors.cljs:41
msgid "workspace.tokens.empty-input"
msgstr "Der Token-Wert darf nicht leer sein"
#: src/app/main/data/workspace/tokens/errors.cljs:15
msgid "workspace.tokens.error-parse"
msgstr "Fehler beim Importieren. JSON konnte nicht verarbeitet werden."
#: src/app/main/ui/workspace/tokens/export/modal.cljs:49
msgid "workspace.tokens.export"
msgstr "Export"
#: src/app/main/ui/workspace/tokens/export/modal.cljs:125
msgid "workspace.tokens.export-tokens"
msgstr "Tokens Exportieren"
#: src/app/main/ui/workspace/tokens/export/modal.cljs:118
msgid "workspace.tokens.export.multiple-files"
msgstr "Mehrere Dateien"
#: src/app/main/ui/workspace/tokens/export/modal.cljs:38
msgid "workspace.tokens.export.no-tokens-themes-sets"
msgstr "Es sind keine Tokens, Themes oder Sets zum Exportieren vorhanden."
#: src/app/main/ui/workspace/tokens/export/modal.cljs:35
msgid "workspace.tokens.export.preview"
msgstr "Vorschau:"
#: src/app/main/ui/workspace/tokens/export/modal.cljs:116
msgid "workspace.tokens.export.single-file"
msgstr "Einzeln"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1339
msgid "workspace.tokens.font-size-value-enter"
msgstr "Schriftgröße oder {alias}"
#: src/app/main/data/workspace/tokens/application.cljs:323
msgid "workspace.tokens.font-variant-not-found"
msgstr ""
"Fehler beim Festlegen der Schriftstärke/des Schriftstils. Dieser Schriftstil "
"ist in der aktuellen Schriftart nicht vorhanden"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1328, src/app/main/ui/workspace/tokens/management/create/form.cljs:1343
msgid "workspace.tokens.font-weight-value-enter"
msgstr "Schriftstärke (300, Fett Kursiv) oder {alias}"
#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:225
msgid "workspace.tokens.gaps"
msgstr "Zwischenräume"
#: src/app/main/ui/workspace/tokens/style_dictionary.cljs
#, unused
msgid "workspace.tokens.generic-error"
msgstr "Fehler: "
#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:129
msgid "workspace.tokens.group-name"
msgstr "Gruppenname"
#: src/app/main/ui/workspace/tokens/import/modal.cljs:233
msgid "workspace.tokens.import-button-prefix"
msgstr "%s importieren"
#: src/app/main/data/workspace/tokens/errors.cljs:32, src/app/main/data/workspace/tokens/errors.cljs:37
msgid "workspace.tokens.import-error"
msgstr "Fehler beim Import:"
#: src/app/main/ui/workspace/tokens/import/modal.cljs:273
msgid "workspace.tokens.import-menu-folder-option"
msgstr "Ordner"
#: src/app/main/ui/workspace/tokens/import/modal.cljs:272
msgid "workspace.tokens.import-menu-json-option"
msgstr "Einzelne JSON-Datei"
#: src/app/main/ui/workspace/tokens/import/modal.cljs:271
msgid "workspace.tokens.import-menu-zip-option"
msgstr "ZIP-Datei"
#: src/app/main/ui/workspace/tokens/import/modal.cljs:241
msgid "workspace.tokens.import-multiple-files"
msgstr ""
"Bei mehreren Dateien werden die Datei- oder Pfadnamen zum Benennen der Sets "
"verwendet."
#: src/app/main/ui/workspace/tokens/import/modal.cljs:240
msgid "workspace.tokens.import-single-file"
msgstr ""
"Bei einer einzelnen JSON-Datei sollten die Keys der ersten Ebene die Namen "
"der Token-Sets sein."
#: src/app/main/ui/workspace/tokens/import/modal.cljs:237
msgid "workspace.tokens.import-tokens"
msgstr "Token importieren"
#: src/app/main/ui/workspace/tokens/sidebar.cljs:414, src/app/main/ui/workspace/tokens/sidebar.cljs:415
#, unused
msgid "workspace.tokens.import-tooltip"
msgstr ""
"Beim Importieren einer JSON-Datei werden alle bestehenden Tokens, Sets und "
"Themes überschrieben"
#: src/app/main/ui/workspace/tokens/import/modal.cljs:247
msgid "workspace.tokens.import-warning"
msgstr ""
"Beim Import werden alle bestehenden Tokens, Sets und Themes überschrieben."
#: src/app/main/ui/workspace/tokens/management.cljs:134
msgid "workspace.tokens.inactive-set-description"
msgstr ""
"Dieses Set ist nicht aktiv. Ändern Sie das Theme oder aktivieren Sie dieses "
"Set, um Änderungen im Anzeigebereich sehen zu können"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:741
msgid "workspace.tokens.individual-tokens"
msgstr "Einzelne Token verwenden"
#: src/app/main/data/workspace/tokens/errors.cljs:49
msgid "workspace.tokens.invalid-color"
msgstr "Ungültiger Farbwert: %s"
#: src/app/main/data/workspace/tokens/errors.cljs:89
msgid "workspace.tokens.invalid-font-weight-token-value"
msgstr ""
"Ungültiger Wert für die Schriftstärke: Verwenden Sie numerische Werte (100"
"950) oder Standardbezeichnungen (dünn, leicht, normal, fett usw.), optional "
"gefolgt von „kursiv”"
#: src/app/main/data/workspace/tokens/errors.cljs:23
msgid "workspace.tokens.invalid-json"
msgstr "Fehler beim Importieren: Ihre JSON-Datei enthält ungültige Token-Daten."
#: src/app/main/data/workspace/tokens/errors.cljs:27
msgid "workspace.tokens.invalid-json-token-name"
msgstr "Fehler beim Importieren: Ihre JSON-Datei enthält ungültige Token-Namen."
#: src/app/main/data/workspace/tokens/errors.cljs:28
msgid "workspace.tokens.invalid-json-token-name-detail"
msgstr ""
"„%s“ ist kein gültiger Namen für ein Token.\n"
"Token-Namen dürfen nur Buchstaben und Ziffern enthalten, die durch Punkte "
"getrennt sind und dürfen nicht mit einem Dollarzeichen beginnen."
#: src/app/main/data/workspace/tokens/errors.cljs:101
msgid "workspace.tokens.invalid-shadow-type-token-value"
msgstr ""
"Ungültiger Schattentyp: Es werden nur „innerShadow” oder „dropShadow” "
"akzeptiert"
#: src/app/main/data/workspace/tokens/errors.cljs:81
msgid "workspace.tokens.invalid-text-case-token-value"
msgstr ""
"Ungültiger Token-Wert: Es werden nur „none“, „Uppercase“, „Lowercase“ oder „"
"Capitalize“ akzeptiert"
#: src/app/main/data/workspace/tokens/errors.cljs:85
msgid "workspace.tokens.invalid-text-decoration-token-value"
msgstr ""
"Ungültiger Token-Wert: Es werden nur „none“, „underline“ oder „strike-through"
"“ akzeptiert"
#: src/app/main/data/workspace/tokens/errors.cljs:93
msgid "workspace.tokens.invalid-token-value-typography"
msgstr ""
"Ungültiger Wert: Der Wert muss auf ein zusammengesetztes Typografie-Token "
"verweisen."
#: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77
msgid "workspace.tokens.invalid-value"
msgstr "Ungültiger Token-Wert: %s"
#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:229
msgid "workspace.tokens.label.group-placeholder"
msgstr "Gruppe hinzufügen (z. B. „Modus“)"
#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:236
msgid "workspace.tokens.label.theme-placeholder"
msgstr "Theme hinzufügen (z. B. „Hell“)"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1351
msgid "workspace.tokens.letter-spacing-value-enter-composite"
msgstr "Zeichenabstand oder {alias}"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1347
msgid "workspace.tokens.line-height-value-enter"
msgstr "Zeilenabstand (Multiplikator, px, %) oder {alias}"
#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:217
msgid "workspace.tokens.margins"
msgstr "Abstände"
#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:253
msgid "workspace.tokens.max-size"
msgstr "Maximale Größe"
#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:247
msgid "workspace.tokens.min-size"
msgstr "Mindestgröße"
#: src/app/main/data/workspace/tokens/errors.cljs:57
msgid "workspace.tokens.missing-references"
msgstr "Fehlende Token-Referenzen: "
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
msgid "workspace.tokens.more-options"
msgstr "Rechtsklick, um Optionen anzuzeigen"
#: src/app/main/data/workspace/tokens/errors.cljs:19
msgid "workspace.tokens.no-token-files-found"
msgstr "In dieser Datei wurden keine Tokens, Sets oder Themen gefunden."
#: src/app/main/data/workspace/tokens/errors.cljs:53
msgid "workspace.tokens.number-too-large"
msgstr "Ungültiger Tokenwert. Der ermittelte Wert ist zu hoch: %s"
#: src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/warnings.cljs:15
msgid "workspace.tokens.opacity-range"
msgstr ""
"Die Deckkraft muss zwischen 0 und 100 % oder zwischen 0 und 1 liegen (z. B. "
"50 % oder 0,5)."
#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:201
msgid "workspace.tokens.paddings"
msgstr "Innenabstände"
#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:277
msgid "workspace.tokens.radius"
msgstr "Radius"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:775
msgid "workspace.tokens.reference-composite"
msgstr "Geben Sie einen Typografie-Alias für diesen Token ein"
#: src/app/main/ui/workspace/tokens/style_dictionary.cljs
#, unused
msgid "workspace.tokens.reference-error"
msgstr "Referenzfehler: "
#: src/app/main/data/workspace/tokens/errors.cljs:45, src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:55
msgid "workspace.tokens.self-reference"
msgstr "Der Token referenziert sich selbst"
#: src/app/main/ui/workspace/tokens/sets/lists.cljs:60
msgid "workspace.tokens.set-edit-placeholder"
msgstr "Namen eingeben (für Gruppen „/“ verwenden)"
#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:168
msgid "workspace.tokens.sets-hint"
msgstr "Theme bearbeiten und Sets verwalten"
#: src/app/main/ui/workspace/tokens/settings/menu.cljs:91
msgid "workspace.tokens.setting-description"
msgstr ""
"Hier können Sie die grundlegende Schriftgröße festlegen, die als Wert für "
"1rem verwendet wird:"
#: src/app/main/ui/workspace/tokens/settings/menu.cljs:84
msgid "workspace.tokens.settings"
msgstr "Token-Einstellungen"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1084
msgid "workspace.tokens.shadow-add-shadow"
msgstr "Schatten hinzufügen"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:981, src/app/main/ui/workspace/tokens/management/create/form.cljs:982
msgid "workspace.tokens.shadow-blur"
msgstr "Weichzeichnen"
#: src/app/main/data/workspace/tokens/errors.cljs:105
msgid "workspace.tokens.shadow-blur-range"
msgstr "Wert muss größer oder gleich 0 sein."
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:987, src/app/main/ui/workspace/tokens/management/create/form.cljs:988
msgid "workspace.tokens.shadow-color"
msgstr "Farbe"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:990, src/app/main/ui/workspace/tokens/management/create/form.cljs:991
msgid "workspace.tokens.shadow-inset"
msgstr "Innerer Schatten"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1091
msgid "workspace.tokens.shadow-remove-shadow"
msgstr "Schatten entfernen"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1215
msgid "workspace.tokens.shadow-title"
msgstr "Schatten"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:975, src/app/main/ui/workspace/tokens/management/create/form.cljs:976
msgid "workspace.tokens.shadow-x"
msgstr "X"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:978, src/app/main/ui/workspace/tokens/management/create/form.cljs:979
msgid "workspace.tokens.shadow-y"
msgstr "Y"
#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:241
msgid "workspace.tokens.size"
msgstr "Größe"
#: src/app/main/data/workspace/tokens/errors.cljs:77, src/app/main/data/workspace/tokens/warnings.cljs:19
msgid "workspace.tokens.stroke-width-range"
msgstr "Die Rahmenbreite muss größer oder gleich 0 sein."
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1322, src/app/main/ui/workspace/tokens/management/create/form.cljs:1359
msgid "workspace.tokens.text-decoration-value-enter"
msgstr "none | underline | strike-through oder {alias}"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1316, src/app/main/ui/workspace/tokens/management/create/form.cljs:1355
msgid "workspace.tokens.text-case-value-enter"
msgstr "none | uppercase | lowercase | capitalize oder {alias}"
#: src/app/main/ui/workspace/tokens/themes/create_modal.cljs:52
msgid "workspace.tokens.theme-name-already-exists"
msgstr "Ein Theme mit diesem Namen existiert bereits"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:1283, src/app/main/ui/workspace/tokens/management/create/form.cljs:1335
msgid "workspace.tokens.token-font-family-value-enter"
msgstr "Schriftfamilie oder eine durch Kommas (,) getrennte Liste von Schriften"
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:44, src/app/main/ui/workspace/tokens/management/create/form.cljs:70
msgid "workspace.tokens.token-name-duplication-validation-error"
msgstr "Unter diesem Speicherort existiert bereits ein Token: %s"
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:42, src/app/main/ui/workspace/tokens/management/create/form.cljs:68
msgid "workspace.tokens.token-name-length-validation-error"
msgstr "Der Name muss mindestens 1 Zeichen lang sein"
#: src/app/main/ui/workspace/tokens/style_dictionary.cljs:259
#, unused
msgid "workspace.tokens.token-not-resolved"
msgstr "Das Token mit dem Namen „%s” konnte nicht gefunden werden"
#: src/app/main/ui/workspace/tokens/management/create/border_radius.cljs:185, src/app/main/ui/workspace/tokens/management/create/form.cljs:605
msgid "workspace.tokens.token-value-enter"
msgstr "Geben Sie einen Wert oder einen Alias mittels {alias} ein"
#: src/app/main/data/workspace/tokens/import_export.cljs:49
msgid "workspace.tokens.unknown-token-type-section"
msgstr "„%s“ wird nicht als Datentyp unterstützt (%s)\n"
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:745
msgid "workspace.tokens.use-reference"
msgstr "Referenz verwenden"
#: src/app/main/data/workspace/tokens/errors.cljs:65
msgid "workspace.tokens.value-with-units"
msgstr "Ungültiger Wert: Einheiten sind hier nicht zulässig."
#: src/app/main/ui/workspace/top_toolbar.cljs:129
msgid "workspace.toolbar.frame-first-time"
msgstr ""
"Zeichenfläche erstellen. Zum Festlegen der Größe anklicken und ziehen. (%s)"
#, unused
msgid "workspace.versions.locked-by-other"
msgstr ""
"Diese Version ist durch %s gesperrt und kann gerade nicht bearbeitet werden"

View File

@@ -421,6 +421,9 @@ msgstr "(copy)"
msgid "dashboard.create-new-team"
msgstr "Create new team"
msgid "dashboard.create-new-org"
msgstr "Create new org"
#: src/app/main/ui/workspace/main_menu.cljs:664
msgid "dashboard.create-version-menu"
msgstr "Pin this version"
@@ -8421,114 +8424,3 @@ msgstr "Autosaved versions will be kept for %s days."
#, unused
msgid "workspace.viewport.click-to-close-path"
msgstr "Click to close the path"
msgid "dashboard.deleted.will-be-deleted-at"
msgstr "Will be deleted %s"
msgid "labels.recent"
msgstr "Recent"
msgid "labels.deleted"
msgstr "Deleted"
msgid "dashboard.restore-all-deleted-button"
msgstr "Restore All"
msgid "dashboard.clear-trash-button"
msgstr "Clear trash"
msgid "dashboard.restore-file-button"
msgstr "Restore file"
msgid "dashboard.delete-file-button"
msgstr "Delete file"
msgid "dashboard.restore-project-button"
msgstr "Restore project"
msgid "dashboard.delete-project-button"
msgstr "Delete project"
msgid "dashboard.trash-info-text-part1"
msgstr "Deleted files will remain in the trash for"
msgid "dashboard.trash-info-text-part2"
msgstr " %s days. "
msgid "dashboard.trash-info-text-part3"
msgstr "After that, they will be permanently deleted."
msgid "dashboard.trash-info-text-part4"
msgstr "If you change your mind, you can restore them or delete them permanently from each file's menu."
msgid "dashboard.restore-all-confirmation.title"
msgstr "Restore all projects and files"
msgid "dashboard.restore-all-confirmation.description"
msgstr "You're going to restore all your projects and files. This may take a while."
msgid "dashboard-restore-file-confirmation.title"
msgstr "Restore file"
msgid "dashboard-restore-file-confirmation.description"
msgstr "You're going to restore %s."
msgid "dashboard.restore-project-confirmation.title"
msgstr "Restore Project"
msgid "dashboard.restore-project-confirmation.description"
msgstr "You're going to restore %s project and all the files contained in it."
msgid "dashboard.delete-forever-confirmation.title"
msgstr "Delete forever"
msgid "dashboard.delete-all-forever-confirmation.description"
msgstr "Are you sure you want to delete forever all your deleted projects and files? This is a non reversible action."
msgid "dashboard.delete-file-forever-confirmation.description"
msgstr "Are you sure you want to delete forever %s? This is a non reversible action."
msgid "dashboard.delete-project-forever-confirmation.description"
msgstr "Are you sure you want to delete forever %s project? You're going to delete it forever an all of the files contained in it. This is a non reversible action."
msgid "dashboard.restore-files-success-notification"
msgstr "%s files have been successfully restored."
msgid "dashboard.restore-success-notification"
msgstr "%s has been successfully restored."
msgid "dashboard.delete-files-success-notification"
msgstr "%s files have been successfully deleted."
msgid "dashboard.delete-success-notification"
msgstr "%s has been successfully deleted."
msgid "dashboard.errors.error-on-restore-files"
msgstr "There was an error while restoring the files."
msgid "dashboard.errors.error-on-restore-file"
msgstr "There was an error while restoring the file %s."
msgid "dashboard.errors.error-on-restoring-project"
msgstr "There was an error while restoring the project %s and its files."
msgid "dashboard.errors.error-on-delete-file"
msgstr "There was an error while deleting the file %s."
msgid "dashboard.errors.error-on-delete-files"
msgstr "There was an error while deleting the files."
msgid "dashboard.errors.error-on-delete-project"
msgstr "There was an error while deleting the project %s."
msgid "dashboard.progress-notification.restoring-files"
msgstr "Restoring files…"
msgid "dashboard.progress-notification.deleting-files"
msgstr "Deleting files…"
msgid "dashboard.progress-notification.slow-restore"
msgstr "Restore unexpectedly slow"
msgid "dashboard.progress-notification.slow-delete"
msgstr "Deletion unexpectedly slow"

View File

@@ -1,15 +1,15 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2025-12-22 15:34+0000\n"
"Last-Translator: Anonymous <noreply@weblate.org>\n"
"Language-Team: Spanish <https://hosted.weblate.org/projects/penpot/frontend/"
"es/>\n"
"PO-Revision-Date: 2025-10-07 16:35+0000\n"
"Last-Translator: Deleted User <noreply+94857@weblate.org>\n"
"Language-Team: Spanish "
"<https://hosted.weblate.org/projects/penpot/frontend/es/>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.15.1\n"
"X-Generator: Weblate 5.14-dev\n"
#: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:159, src/app/main/ui/viewer/login.cljs:100
msgid "auth.already-have-account"
@@ -430,6 +430,9 @@ msgstr "(copia)"
msgid "dashboard.create-new-team"
msgstr "Crear nuevo equipo"
msgid "dashboard.create-new-org"
msgstr "Crear nueva organización"
#: src/app/main/ui/workspace/main_menu.cljs:664
msgid "dashboard.create-version-menu"
msgstr "Guardar esta versión"
@@ -1843,7 +1846,6 @@ msgid "inspect.empty.select"
msgstr "Elige una forma, tablero o grupo para inspeccionar sus propiedades y código"
#: src/app/main/ui/inspect/right_sidebar.cljs:166
#, fuzzy
msgid "inspect.layer-info"
msgstr "Info. de capa"
@@ -8277,111 +8279,3 @@ msgstr "Los autoguardados duran %s días."
#, unused
msgid "workspace.viewport.click-to-close-path"
msgstr "Pulsar para cerrar la ruta"
msgid "labels.recent"
msgstr "Recientes"
msgid "labels.deleted"
msgstr "Eliminados"
msgid "dashboard.restore-all-deleted-button"
msgstr "Restaurar todo"
msgid "dashboard.clear-trash-button"
msgstr "Vaciar papelera"
msgid "dashboard.restore-file-button"
msgstr "Restaurar archivo"
msgid "dashboard.delete-file-button"
msgstr "Eliminar archivo"
msgid "dashboard.restore-project-button"
msgstr "Restaurar proyecto"
msgid "dashboard.delete-project-button"
msgstr "Eliminar proyecto"
msgid "dashboard.trash-info-text-part1"
msgstr "Los archivos eliminados permanecerán en la papelera durante"
msgid "dashboard.trash-info-text-part2"
msgstr " %s días. "
msgid "dashboard.trash-info-text-part3"
msgstr "Después de eso, serán eliminados permanentemente."
msgid "dashboard.trash-info-text-part4"
msgstr "Si cambias de opinión, puedes restaurarlos o eliminarlos permanentemente desde el menú de cada archivo."
msgid "dashboard.deleted.delete-forever"
msgstr "Eliminar para siempre"
msgid "dashboard.restore-all-confirmation.title"
msgstr "Restaurar todos los proyectos y archivos"
msgid "dashboard.restore-all-confirmation.description"
msgstr "Vas a restaurar todos tus proyectos y archivos. Esto puede tardar un poco."
msgid "dashboard-restore-file-confirmation.title"
msgstr "Restaurar archivo"
msgid "dashboard-restore-file-confirmation.description"
msgstr "Vas a restaurar %s."
msgid "dashboard.restore-project-confirmation.title"
msgstr "Restaurar proyecto"
msgid "dashboard.restore-project-confirmation.description"
msgstr "Vas a restaurar el proyecto %s y todos los archivos que contiene."
msgid "dashboard.delete-forever-confirmation.title"
msgstr "Eliminar para siempre"
msgid "dashboard.delete-all-forever-confirmation.description"
msgstr "¿Estás seguro de que quieres eliminar para siempre todos tus proyectos y archivos eliminados? Esta es una acción irreversible."
msgid "dashboard.delete-file-forever-confirmation.description"
msgstr "¿Estás seguro de que quieres eliminar para siempre %s? Esta es una acción irreversible."
msgid "dashboard.delete-project-forever-confirmation.description"
msgstr "¿Estás seguro de que quieres eliminar para siempre el proyecto %s? Vas a eliminarlo para siempre junto con todos los archivos que contiene. Esta es una acción irreversible."
msgid "dashboard.restore-files-success-notification"
msgstr "%s ficheros han sido restaurado correctamente."
msgid "dashboard.restore-success-notification"
msgstr "%s ha sido restaurado correctamente."
msgid "dashboard.delete-files-success-notification"
msgstr "%s ficheros han sido eliminados correctamente."
msgid "dashboard.delete-success-notification"
msgstr "%s ha sido eliminado correctamente."
msgid "dashboard.errors.error-on-restore-files"
msgstr "Hubo un error al restaurar los archivos."
msgid "dashboard.errors.error-on-restore-file"
msgstr "Hubo un error al restaurar el archivo %s."
msgid "dashboard.errors.error-on-restoring-files"
msgstr "Hubo un error al restaurar archivos."
msgid "dashboard.errors.error-on-delete-files"
msgstr "Hubo un error al eliminar archivos."
msgid "dashboard.errors.error-on-delete-project"
msgstr "Hubo un error al eliminar el proyecto %s"
msgid "dashboard.progress-notification.restoring-files"
msgstr "Restaurando archivos…"
msgid "dashboard.progress-notification.deleting-files"
msgstr "Eliminando archivos…"
msgid "dashboard.progress-notification.slow-restore"
msgstr "Restauración inesperadamente lenta"
msgid "dashboard.progress-notification.slow-delete"
msgstr "Eliminación inesperadamente lenta"

View File

@@ -1,15 +1,15 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2025-12-22 15:34+0000\n"
"Last-Translator: Yessenia Villarte Vaca <yesseniavillarte@gmail.com>\n"
"Language-Team: Spanish (Latin America) <https://hosted.weblate.org/projects/"
"penpot/frontend/es_419/>\n"
"PO-Revision-Date: 2024-06-17 08:07+0000\n"
"Last-Translator: Anonymous <noreply@weblate.org>\n"
"Language-Team: Spanish (Latin America) "
"<https://hosted.weblate.org/projects/penpot/frontend/es_419/>\n"
"Language: es_419\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.15.1\n"
"X-Generator: Weblate 5.6-dev\n"
#: src/app/main/ui/auth/register.cljs:215, src/app/main/ui/static.cljs:159, src/app/main/ui/viewer/login.cljs:100
msgid "auth.already-have-account"
@@ -454,7 +454,6 @@ msgstr ""
"¿Qué quiere hacer con sus activos*?"
#: src/app/main/ui/exports/files.cljs:164
#, fuzzy
msgid "dashboard.export.options.all.message"
msgstr ""
"Los archivos con bibliotecas compartidas se incluirán en la exportación, "

View File

@@ -1188,6 +1188,10 @@ msgstr ""
"Diseinua ikuskatzeari buruz gehiago jakin nahi baduzu zoaz Penpoten "
"laguntza zentrora"
#: src/app/main/ui/inspect/right_sidebar.cljs:240
msgid "inspect.empty.more-info"
msgstr "Informazio gehiago ikuskatzeari buruz"
#: src/app/main/ui/inspect/right_sidebar.cljs:232
msgid "inspect.empty.select"
msgstr ""

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