mirror of
https://github.com/penpot/penpot.git
synced 2025-12-24 06:58:34 -05:00
Compare commits
1 Commits
develop
...
juanfran-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a08c2ca46e |
2
.github/workflows/build-tag.yml
vendored
2
.github/workflows/build-tag.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: ${{ github.ref_name }}
|
||||
build_wasm: "yes"
|
||||
build_wasm: "no"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
|
||||
### :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
|
||||
|
||||
@@ -85,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
|
||||
|
||||
@@ -114,12 +111,6 @@ example. It's still usable as before, we just removed the example.
|
||||
- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721)
|
||||
- Fix problem with plugins content attribute [Plugins #209](https://github.com/penpot/penpot-plugins/issues/209)
|
||||
- Fix U and E icon displayed in project list [Taiga #12806](https://tree.taiga.io/project/penpot/issue/12806)
|
||||
- Fix unpublish library modal not scrolling a long file list [Taiga #12285](https://tree.taiga.io/project/penpot/issue/12285)
|
||||
- Fix incorrect interaction betwen hower and scroll on assets sidebar [Taiga #12389](https://tree.taiga.io/project/penpot/issue/12389)
|
||||
- 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
|
||||
|
||||
|
||||
@@ -240,4 +240,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -3,7 +3,7 @@
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Tokens%20starter%20kit.penpot"}
|
||||
{:id "penpot-design-system"
|
||||
:name "Penpot Design System | Pencil"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Pencil-Penpot-Design-System.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/penpot-app.penpot"}
|
||||
{:id "wireframing-kit"
|
||||
:name "Wireframe library"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"}
|
||||
|
||||
@@ -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}))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -106,17 +106,17 @@
|
||||
(let [content-part (MimeBodyPart.)
|
||||
alternative-mpart (MimeMultipart. "alternative")]
|
||||
|
||||
(when-let [content (get body "text/plain")]
|
||||
(let [text-part (MimeBodyPart.)]
|
||||
(.setText text-part ^String content ^String charset)
|
||||
(.addBodyPart alternative-mpart text-part)))
|
||||
|
||||
(when-let [content (get body "text/html")]
|
||||
(let [html-part (MimeBodyPart.)]
|
||||
(.setContent html-part ^String content
|
||||
(str "text/html; charset=" charset))
|
||||
(.addBodyPart alternative-mpart html-part)))
|
||||
|
||||
(when-let [content (get body "text/plain")]
|
||||
(let [text-part (MimeBodyPart.)]
|
||||
(.setText text-part ^String content ^String charset)
|
||||
(.addBodyPart alternative-mpart text-part)))
|
||||
|
||||
(.setContent content-part alternative-mpart)
|
||||
(.addBodyPart mixed-mpart content-part))
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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})))
|
||||
|
||||
@@ -79,6 +79,18 @@
|
||||
(remove #(contains? reserved-props (key %))))
|
||||
props))
|
||||
|
||||
(defn event-from-rpc-params
|
||||
"Create a base event skeleton with pre-filled some important
|
||||
data that can be extracted from RPC params object"
|
||||
[params]
|
||||
(let [context {:external-session-id (::rpc/external-session-id params)
|
||||
:external-event-origin (::rpc/external-event-origin params)
|
||||
:triggered-by (::rpc/handler-name params)}]
|
||||
{::type "action"
|
||||
::profile-id (::rpc/profile-id params)
|
||||
::ip-addr (::rpc/ip-addr params)
|
||||
::context (d/without-nils context)}))
|
||||
|
||||
(defn get-external-session-id
|
||||
[request]
|
||||
(when-let [session-id (yreq/get-header request "x-external-session-id")]
|
||||
@@ -87,24 +99,13 @@
|
||||
(str/blank? session-id))
|
||||
session-id)))
|
||||
|
||||
(defn- get-client-event-origin
|
||||
(defn- get-external-event-origin
|
||||
[request]
|
||||
(when-let [origin (yreq/get-header request "x-event-origin")]
|
||||
(when-not (or (= origin "null")
|
||||
(when-not (or (> (count origin) 256)
|
||||
(= origin "null")
|
||||
(str/blank? origin))
|
||||
(str/prune origin 200))))
|
||||
|
||||
(defn get-client-user-agent
|
||||
[request]
|
||||
(when-let [user-agent (yreq/get-header request "user-agent")]
|
||||
(str/prune user-agent 500)))
|
||||
|
||||
(defn- get-client-version
|
||||
[request]
|
||||
(when-let [origin (yreq/get-header request "x-frontend-version")]
|
||||
(when-not (or (= origin "null")
|
||||
(str/blank? origin))
|
||||
(str/prune origin 100))))
|
||||
origin)))
|
||||
|
||||
;; --- SPECS
|
||||
|
||||
@@ -133,33 +134,6 @@
|
||||
(def ^:private check-event
|
||||
(sm/check-fn schema:event))
|
||||
|
||||
(defn- prepare-context-from-request
|
||||
[request]
|
||||
(let [client-event-origin (get-client-event-origin request)
|
||||
client-version (get-client-version request)
|
||||
client-user-agent (get-client-user-agent request)
|
||||
session-id (get-external-session-id request)
|
||||
token-id (::actoken/id request)]
|
||||
(d/without-nils
|
||||
{:external-session-id session-id
|
||||
:access-token-id (some-> token-id str)
|
||||
:client-event-origin client-event-origin
|
||||
:client-user-agent client-user-agent
|
||||
:client-version client-version
|
||||
:version (:full cf/version)})))
|
||||
|
||||
(defn event-from-rpc-params
|
||||
"Create a base event skeleton with pre-filled some important
|
||||
data that can be extracted from RPC params object"
|
||||
[params]
|
||||
(let [context (some-> params meta ::http/request prepare-context-from-request)
|
||||
event {::type "action"
|
||||
::profile-id (or (::rpc/profile-id params) uuid/zero)
|
||||
::ip-addr (::rpc/ip-addr params)}]
|
||||
(cond-> event
|
||||
(some? context)
|
||||
(assoc ::context context))))
|
||||
|
||||
(defn prepare-event
|
||||
[cfg mdata params result]
|
||||
(let [resultm (meta result)
|
||||
@@ -174,10 +148,18 @@
|
||||
(merge (::props resultm))
|
||||
(dissoc :profile-id)
|
||||
(dissoc :type)))
|
||||
|
||||
(clean-props))
|
||||
|
||||
context (merge (::context resultm)
|
||||
(prepare-context-from-request request))
|
||||
token-id (::actoken/id request)
|
||||
context (-> (::context resultm)
|
||||
(assoc :external-session-id
|
||||
(get-external-session-id request))
|
||||
(assoc :external-event-origin
|
||||
(get-external-event-origin request))
|
||||
(assoc :access-token-id (some-> token-id str))
|
||||
(d/without-nils))
|
||||
|
||||
ip-addr (inet/parse-request request)]
|
||||
|
||||
{::type (or (::type resultm)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -712,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)
|
||||
|
||||
@@ -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)})))))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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
|
||||
[]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -35,9 +35,6 @@
|
||||
:assets-s3 :s3
|
||||
nil)))
|
||||
|
||||
(def default-bucket
|
||||
"file-media-object")
|
||||
|
||||
(def valid-buckets
|
||||
#{"file-media-object"
|
||||
"team-font-variant"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))))]
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -82,113 +82,6 @@
|
||||
(declare create-svg-children)
|
||||
(declare parse-svg-element)
|
||||
|
||||
(defn- process-gradient-stops
|
||||
"Processes gradient stops to extract stop-color and stop-opacity from style attributes
|
||||
and convert them to direct attributes. This ensures stops with style='stop-color:#...;stop-opacity:1'
|
||||
are properly converted to stop-color and stop-opacity attributes."
|
||||
[stops]
|
||||
(mapv (fn [stop]
|
||||
(let [stop-attrs (:attrs stop)
|
||||
stop-style (get stop-attrs :style)
|
||||
;; Parse style if it's a string using csvg/parse-style utility
|
||||
parsed-style (when (and (string? stop-style) (seq stop-style))
|
||||
(csvg/parse-style stop-style))
|
||||
;; Extract stop-color and stop-opacity from style
|
||||
style-stop-color (when parsed-style (:stop-color parsed-style))
|
||||
style-stop-opacity (when parsed-style (:stop-opacity parsed-style))
|
||||
;; Merge: use direct attributes first, then style values as fallback
|
||||
final-attrs (cond-> stop-attrs
|
||||
(and style-stop-color (not (contains? stop-attrs :stop-color)))
|
||||
(assoc :stop-color style-stop-color)
|
||||
|
||||
(and style-stop-opacity (not (contains? stop-attrs :stop-opacity)))
|
||||
(assoc :stop-opacity style-stop-opacity)
|
||||
|
||||
;; Remove style attribute if we've extracted its values
|
||||
(or style-stop-color style-stop-opacity)
|
||||
(dissoc :style))]
|
||||
(assoc stop :attrs final-attrs)))
|
||||
stops))
|
||||
|
||||
(defn- resolve-gradient-href
|
||||
"Resolves xlink:href references in gradients by merging the referenced gradient's
|
||||
stops and attributes with the referencing gradient. This ensures gradients that
|
||||
reference other gradients (like linearGradient3550 referencing linearGradient3536)
|
||||
inherit the stops from the base gradient.
|
||||
|
||||
According to SVG spec, when a gradient has xlink:href:
|
||||
- It inherits all attributes from the referenced gradient
|
||||
- It inherits all stops from the referenced gradient
|
||||
- The referencing gradient's attributes override the base ones
|
||||
- If the referencing gradient has stops, they replace the base stops
|
||||
|
||||
Returns the defs map with all gradient href references resolved."
|
||||
[defs]
|
||||
(letfn [(resolve-gradient [gradient-id gradient-node defs visited]
|
||||
(if (contains? visited gradient-id)
|
||||
(do
|
||||
#?(:cljs (js/console.warn "[resolve-gradient] Circular reference detected for" gradient-id)
|
||||
:clj nil)
|
||||
gradient-node) ;; Avoid circular references
|
||||
(let [attrs (:attrs gradient-node)
|
||||
href-id (or (:href attrs) (:xlink:href attrs))
|
||||
href-id (when (and (string? href-id) (pos? (count href-id)))
|
||||
(subs href-id 1)) ;; Remove leading #
|
||||
|
||||
base-gradient (when (and href-id (contains? defs href-id))
|
||||
(get defs href-id))
|
||||
|
||||
resolved-base (when base-gradient (resolve-gradient href-id base-gradient defs (conj visited gradient-id)))]
|
||||
|
||||
(if resolved-base
|
||||
;; Merge: base gradient attributes + referencing gradient attributes
|
||||
;; Use referencing gradient's stops if present, otherwise use base stops
|
||||
(let [base-attrs (:attrs resolved-base)
|
||||
ref-attrs (:attrs gradient-node)
|
||||
|
||||
;; Start with base attributes (without id), then merge with ref attributes
|
||||
;; This ensures ref attributes override base ones
|
||||
base-attrs-clean (dissoc base-attrs :id)
|
||||
ref-attrs-clean (dissoc ref-attrs :href :xlink:href :id)
|
||||
|
||||
;; Special handling for gradientTransform: if both have it, combine them
|
||||
base-transform (get base-attrs :gradientTransform)
|
||||
ref-transform (get ref-attrs :gradientTransform)
|
||||
combined-transform (cond
|
||||
(and base-transform ref-transform)
|
||||
(str base-transform " " ref-transform) ;; Apply base first, then ref
|
||||
:else (or ref-transform base-transform))
|
||||
|
||||
;; Merge attributes: base first, then ref (ref overrides)
|
||||
merged-attrs (-> (d/deep-merge base-attrs-clean ref-attrs-clean)
|
||||
(cond-> combined-transform
|
||||
(assoc :gradientTransform combined-transform)))
|
||||
|
||||
;; If referencing gradient has content (stops), use it; otherwise use base content
|
||||
final-content (if (seq (:content gradient-node))
|
||||
(:content gradient-node)
|
||||
(:content resolved-base))
|
||||
|
||||
;; Process stops to extract stop-color and stop-opacity from style attributes
|
||||
processed-content (process-gradient-stops final-content)
|
||||
|
||||
result {:tag (:tag gradient-node)
|
||||
:attrs (assoc merged-attrs :id gradient-id)
|
||||
:content processed-content}]
|
||||
result)
|
||||
;; Process stops even for gradients without references to extract style attributes
|
||||
(let [processed-content (process-gradient-stops (:content gradient-node))]
|
||||
(assoc gradient-node :content processed-content))))))]
|
||||
(let [gradient-tags #{:linearGradient :radialGradient}
|
||||
result (reduce-kv
|
||||
(fn [acc id node]
|
||||
(if (contains? gradient-tags (:tag node))
|
||||
(assoc acc id (resolve-gradient id node defs #{}))
|
||||
(assoc acc id node)))
|
||||
{}
|
||||
defs)]
|
||||
result)))
|
||||
|
||||
(defn create-svg-shapes
|
||||
([svg-data pos objects frame-id parent-id selected center?]
|
||||
(create-svg-shapes (uuid/next) svg-data pos objects frame-id parent-id selected center?))
|
||||
@@ -219,9 +112,6 @@
|
||||
(csvg/fix-percents)
|
||||
(csvg/extract-defs))
|
||||
|
||||
;; Resolve gradient href references in all defs before processing shapes
|
||||
def-nodes (resolve-gradient-href def-nodes)
|
||||
|
||||
;; In penpot groups have the size of their children. To
|
||||
;; respect the imported svg size and empty space let's create
|
||||
;; a transparent shape as background to respect the imported
|
||||
@@ -252,23 +142,12 @@
|
||||
(reduce (partial create-svg-children objects selected frame-id root-id svg-data)
|
||||
[unames []]
|
||||
(d/enumerate (->> (:content svg-data)
|
||||
(mapv #(csvg/inherit-attributes root-attrs %)))))
|
||||
(mapv #(csvg/inherit-attributes root-attrs %)))))]
|
||||
|
||||
;; Collect all defs from children and merge into root shape
|
||||
all-defs-from-children (reduce (fn [acc child]
|
||||
(if-let [child-defs (:svg-defs child)]
|
||||
(merge acc child-defs)
|
||||
acc))
|
||||
{}
|
||||
children)
|
||||
|
||||
;; Merge defs from svg-data and children into root shape
|
||||
root-shape-with-defs (assoc root-shape :svg-defs (merge def-nodes all-defs-from-children))]
|
||||
|
||||
[root-shape-with-defs children])))
|
||||
[root-shape children])))
|
||||
|
||||
(defn create-raw-svg
|
||||
[name frame-id {:keys [x y width height offset-x offset-y defs] :as svg-data} {:keys [attrs] :as data}]
|
||||
[name frame-id {:keys [x y width height offset-x offset-y]} {:keys [attrs] :as data}]
|
||||
(let [props (csvg/attrs->props attrs)
|
||||
vbox (grc/make-rect offset-x offset-y width height)]
|
||||
(cts/setup-shape
|
||||
@@ -281,11 +160,10 @@
|
||||
:y y
|
||||
:content data
|
||||
:svg-attrs props
|
||||
:svg-viewbox vbox
|
||||
:svg-defs defs})))
|
||||
:svg-viewbox vbox})))
|
||||
|
||||
(defn create-svg-root
|
||||
[id frame-id parent-id {:keys [name x y width height offset-x offset-y attrs defs] :as svg-data}]
|
||||
[id frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}]
|
||||
(let [props (-> (dissoc attrs :viewBox :view-box :xmlns)
|
||||
(d/without-keys csvg/inheritable-props)
|
||||
(csvg/attrs->props))]
|
||||
@@ -299,8 +177,7 @@
|
||||
:height height
|
||||
:x (+ x offset-x)
|
||||
:y (+ y offset-y)
|
||||
:svg-attrs props
|
||||
:svg-defs defs})))
|
||||
:svg-attrs props})))
|
||||
|
||||
(defn create-svg-children
|
||||
[objects selected frame-id parent-id svg-data [unames children] [_index svg-element]]
|
||||
@@ -321,7 +198,7 @@
|
||||
|
||||
|
||||
(defn create-group
|
||||
[name frame-id {:keys [x y width height offset-x offset-y defs] :as svg-data} {:keys [attrs]}]
|
||||
[name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}]
|
||||
(let [transform (csvg/parse-transform (:transform attrs))
|
||||
attrs (-> attrs
|
||||
(d/without-keys csvg/inheritable-props)
|
||||
@@ -337,8 +214,7 @@
|
||||
:height height
|
||||
:svg-transform transform
|
||||
:svg-attrs attrs
|
||||
:svg-viewbox vbox
|
||||
:svg-defs defs})))
|
||||
:svg-viewbox vbox})))
|
||||
|
||||
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
|
||||
(when (and (contains? attrs :d) (seq (:d attrs)))
|
||||
@@ -647,21 +523,6 @@
|
||||
:else (dm/str tag))]
|
||||
(dm/str "svg-" suffix)))
|
||||
|
||||
(defn- filter-valid-def-references
|
||||
"Filters out false positive references that are not valid def IDs.
|
||||
Filters out:
|
||||
- Colors in style attributes (hex colors like #f9dd67)
|
||||
- Style fragments that contain CSS keywords (like stop-opacity)
|
||||
- References that don't exist in defs"
|
||||
[ref-ids defs]
|
||||
(let [is-style-fragment? (fn [ref-id]
|
||||
(or (clr/hex-color-string? (str "#" ref-id))
|
||||
(str/includes? ref-id ";") ;; Contains CSS separator
|
||||
(str/includes? ref-id "stop-opacity") ;; CSS keyword
|
||||
(str/includes? ref-id "stop-color")))] ;; CSS keyword
|
||||
(->> ref-ids
|
||||
(remove is-style-fragment?) ;; Filter style fragments and hex colors
|
||||
(filter #(contains? defs %))))) ;; Only existing defs
|
||||
|
||||
(defn parse-svg-element
|
||||
[frame-id svg-data {:keys [tag attrs hidden] :as element} unames]
|
||||
@@ -673,11 +534,7 @@
|
||||
(let [name (or (:id attrs) (tag->name tag))
|
||||
att-refs (csvg/find-attr-references attrs)
|
||||
defs (get svg-data :defs)
|
||||
valid-refs (filter-valid-def-references att-refs defs)
|
||||
all-refs (csvg/find-def-references defs valid-refs)
|
||||
;; Filter the final result to ensure all references are valid defs
|
||||
;; This prevents false positives from style attributes in gradient stops
|
||||
references (filter-valid-def-references all-refs defs)
|
||||
references (csvg/find-def-references defs att-refs)
|
||||
|
||||
href-id (or (:href attrs) (:xlink:href attrs) " ")
|
||||
href-id (if (and (string? href-id)
|
||||
|
||||
@@ -169,7 +169,6 @@
|
||||
:enable-component-thumbnails
|
||||
:enable-render-wasm-dpr
|
||||
:enable-token-color
|
||||
:enable-token-shadow
|
||||
:enable-inspect-styles
|
||||
:enable-feature-fdata-objects-map])
|
||||
|
||||
|
||||
@@ -12,11 +12,8 @@
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.common :as gco]
|
||||
[app.common.logging :as log]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.logic.variant-properties :as clvp]
|
||||
@@ -28,7 +25,6 @@
|
||||
[app.common.types.library :as ctl]
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.path.segment :as segment]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
@@ -1878,44 +1874,6 @@
|
||||
roperations'
|
||||
uoperations')))))))
|
||||
|
||||
(defn- set-path-new-values
|
||||
[current-shape prev-shape transform]
|
||||
(let [new-content (segment/transform-content
|
||||
(:content current-shape)
|
||||
(gmt/transform-in (gpt/point 0 0) transform))
|
||||
new-points (-> (segment/content->selrect new-content)
|
||||
(grc/rect->points))
|
||||
points-center (gco/points->center new-points)
|
||||
new-selrect (gsh/calculate-selrect new-points points-center)
|
||||
shape (assoc current-shape
|
||||
:content new-content
|
||||
:points new-points
|
||||
:selrect new-selrect)
|
||||
|
||||
prev-center (segment/content-center (:content prev-shape))
|
||||
delta (gpt/subtract points-center (first new-points))
|
||||
new-pos (gpt/subtract prev-center delta)]
|
||||
(gsh/absolute-move shape new-pos)))
|
||||
|
||||
(defn- switch-path-change-value
|
||||
[prev-shape ;; The shape before the switch
|
||||
current-shape ;; The shape after the switch (a clean copy)
|
||||
ref-shape ;; The referenced shape on the main component
|
||||
;; before the switch
|
||||
attr]
|
||||
(let [old-width (-> ref-shape :selrect :width)
|
||||
new-width (-> prev-shape :selrect :width)
|
||||
|
||||
old-height (-> ref-shape :selrect :height)
|
||||
new-height (-> prev-shape :selrect :height)
|
||||
|
||||
transform (-> (gpt/point (/ new-width old-width)
|
||||
(/ new-height old-height))
|
||||
(gmt/scale-matrix))
|
||||
|
||||
shape (set-path-new-values current-shape prev-shape transform)]
|
||||
(get shape attr)))
|
||||
|
||||
|
||||
(defn- switch-text-change-value
|
||||
[prev-content ;; The :content of the text before the switch
|
||||
@@ -2067,10 +2025,6 @@
|
||||
(= :content attr)
|
||||
(touched attr-group))
|
||||
|
||||
path-change?
|
||||
(and (= :path (:type current-shape))
|
||||
(contains? #{:points :selrect :content} attr))
|
||||
|
||||
;; position-data is a special case because can be affected by :geometry-group and :content-group
|
||||
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
|
||||
;; so it's calculated again
|
||||
@@ -2099,12 +2053,6 @@
|
||||
(:content origin-ref-shape)
|
||||
touched)
|
||||
|
||||
path-change?
|
||||
(switch-path-change-value previous-shape
|
||||
current-shape
|
||||
origin-ref-shape
|
||||
attr)
|
||||
|
||||
:else
|
||||
(get previous-shape attr)))
|
||||
|
||||
|
||||
@@ -132,94 +132,3 @@ Some naming conventions:
|
||||
(if-let [last-period (str/last-index-of s ".")]
|
||||
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
|
||||
[s ""]))
|
||||
|
||||
;; Tree building functions --------------------------------------------------
|
||||
|
||||
"Build tree structure from flat list of paths"
|
||||
|
||||
"`build-tree-root` is the main function to build the tree."
|
||||
|
||||
"Receives a list of segments with 'name' properties representing paths,
|
||||
and a separator string."
|
||||
"E.g segments = [{... :name 'one/two/three'} {... :name 'one/two/four'} {... :name 'one/five'}]"
|
||||
|
||||
"Transforms into a tree structure like:
|
||||
[{:name 'one'
|
||||
:path 'one'
|
||||
:depth 0
|
||||
:leaf nil
|
||||
:children-fn (fn [] [{:name 'two'
|
||||
:path 'one.two'
|
||||
:depth 1
|
||||
:leaf nil
|
||||
:children-fn (fn [] [{... :name 'three'} {... :name 'four'}])}
|
||||
{:name 'five'
|
||||
:path 'one.five'
|
||||
:depth 1
|
||||
:leaf {... :name 'five'}
|
||||
...}])}]"
|
||||
|
||||
(defn- sort-by-children
|
||||
"Sorts segments so that those with children come first."
|
||||
[segments separator]
|
||||
(sort-by (fn [segment]
|
||||
(let [path (split-path (:name segment) :separator separator)
|
||||
path-length (count path)]
|
||||
(if (= path-length 1)
|
||||
1
|
||||
0)))
|
||||
segments))
|
||||
|
||||
(defn- group-by-first-segment
|
||||
"Groups segments by their first path segment and update segment name."
|
||||
[segments separator]
|
||||
(reduce (fn [acc segment]
|
||||
(let [[first-segment & remaining-segments] (split-path (:name segment) :separator separator)
|
||||
rest-path (when (seq remaining-segments) (join-path remaining-segments :separator separator :with-spaces? false))]
|
||||
(update acc first-segment (fnil conj [])
|
||||
(if rest-path
|
||||
(assoc segment :name rest-path)
|
||||
segment))))
|
||||
{}
|
||||
segments))
|
||||
|
||||
(defn- sort-and-group-segments
|
||||
"Sorts elements and groups them by their first path segment."
|
||||
[segments separator]
|
||||
(let [sorted (sort-by-children segments separator)
|
||||
grouped (group-by-first-segment sorted separator)]
|
||||
grouped))
|
||||
|
||||
(defn- build-tree-node
|
||||
"Builds a single tree node with lazy children."
|
||||
[segment-name remaining-segments separator parent-path depth]
|
||||
(let [current-path (if parent-path
|
||||
(str parent-path "." segment-name)
|
||||
segment-name)
|
||||
|
||||
is-leaf? (and (seq remaining-segments)
|
||||
(every? (fn [segment]
|
||||
(let [remaining-segment-name (first (split-path (:name segment) :separator separator))]
|
||||
(= segment-name remaining-segment-name)))
|
||||
remaining-segments))
|
||||
|
||||
leaf-segment (when is-leaf? (first remaining-segments))
|
||||
node {:name segment-name
|
||||
:path current-path
|
||||
:depth depth
|
||||
:leaf leaf-segment
|
||||
:children-fn (when-not is-leaf?
|
||||
(fn []
|
||||
(let [grouped-elements (sort-and-group-segments remaining-segments separator)]
|
||||
(mapv (fn [[child-segment-name remaining-child-segments]]
|
||||
(build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth)))
|
||||
grouped-elements))))}]
|
||||
node))
|
||||
|
||||
(defn build-tree-root
|
||||
"Builds the root level of the tree."
|
||||
[segments separator]
|
||||
(let [grouped-elements (sort-and-group-segments segments separator)]
|
||||
(mapv (fn [[segment-name remaining-segments]]
|
||||
(build-tree-node segment-name remaining-segments separator nil 0))
|
||||
grouped-elements)))
|
||||
|
||||
@@ -284,22 +284,9 @@
|
||||
(defn check-fn
|
||||
"Create a predefined check function"
|
||||
[s & {:keys [hint type code]}]
|
||||
(let [s #?(:clj
|
||||
(schema s)
|
||||
:cljs
|
||||
(try
|
||||
(schema s)
|
||||
(catch :default cause
|
||||
(let [data (ex-data cause)]
|
||||
(if (= :malli.core/invalid-schema (:type data))
|
||||
(throw (ex-info
|
||||
(str "Invalid schema\n"
|
||||
(pp/pprint-str (:data data)))
|
||||
{}))
|
||||
(throw cause))))))
|
||||
|
||||
validator* (delay (m/validator s))
|
||||
explainer* (delay (m/explainer s))
|
||||
(let [s (delay (schema s))
|
||||
validator* (delay (m/validator @s))
|
||||
explainer* (delay (m/explainer @s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
|
||||
@@ -546,19 +546,9 @@
|
||||
filter-values)))
|
||||
|
||||
(defn extract-ids [val]
|
||||
;; Extract referenced ids from string values like "url(#myId)".
|
||||
;; Non-string values (maps, numbers, nil, etc.) return an empty seq
|
||||
;; to avoid re-seq type errors when attributes carry nested structures.
|
||||
(cond
|
||||
(string? val)
|
||||
(when (some? val)
|
||||
(->> (re-seq xml-id-regex val)
|
||||
(mapv second))
|
||||
|
||||
(sequential? val)
|
||||
(mapcat extract-ids val)
|
||||
|
||||
:else
|
||||
[]))
|
||||
(mapv second))))
|
||||
|
||||
(defn fix-dot-number
|
||||
"Fixes decimal numbers starting in dot but without leading 0"
|
||||
|
||||
@@ -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}))))
|
||||
|
||||
@@ -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?)))))
|
||||
|
||||
|
||||
@@ -234,15 +234,16 @@
|
||||
"Calculate the boolean content from shape and objects. Returns a
|
||||
packed PathData instance"
|
||||
[shape objects]
|
||||
(let [content (calc-bool-content* shape objects)]
|
||||
(let [content (if (fn? wasm:calc-bool-content)
|
||||
(wasm:calc-bool-content (get shape :bool-type)
|
||||
(get shape :shapes))
|
||||
(calc-bool-content* shape objects))]
|
||||
(impl/path-data content)))
|
||||
|
||||
(defn update-bool-shape
|
||||
"Calculates the selrect+points for the boolean shape"
|
||||
[shape objects]
|
||||
(let [content (if (fn? wasm:calc-bool-content)
|
||||
(wasm:calc-bool-content shape objects)
|
||||
(calc-bool-content shape objects))
|
||||
(let [content (calc-bool-content shape objects)
|
||||
shape (assoc shape :content content)]
|
||||
(update-geometry shape)))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -10,7 +10,3 @@ localhost:3449 {
|
||||
http://localhost:3450 {
|
||||
reverse_proxy localhost:4449
|
||||
}
|
||||
|
||||
http://penpot-devenv-main:3450 {
|
||||
reverse_proxy localhost:4449
|
||||
}
|
||||
|
||||
@@ -223,19 +223,15 @@ http {
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~* \.(jpg|png|svg|ttf|woff|woff2)$ {
|
||||
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
|
||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~* \.(js|css|wasm)$ {
|
||||
add_header Cache-Control "no-store" always;
|
||||
}
|
||||
|
||||
location ~ ^/[^/]+/(.*)$ {
|
||||
return 301 " /404";
|
||||
}
|
||||
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,18 @@ tmux select-window -t penpot:1
|
||||
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
||||
tmux send-keys -t penpot 'yarn run watch:app' enter
|
||||
|
||||
tmux new-window -t penpot:2 -n 'frontend storybook'
|
||||
tmux new-window -t penpot:2 -n 'frontend ts'
|
||||
tmux select-window -t penpot:2
|
||||
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
||||
tmux send-keys -t penpot 'yarn watch:ts' enter
|
||||
|
||||
tmux new-window -t penpot:3 -n 'frontend storybook'
|
||||
tmux select-window -t penpot:3
|
||||
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
||||
tmux send-keys -t penpot 'yarn run watch:storybook' enter
|
||||
|
||||
tmux new-window -t penpot:3 -n 'exporter'
|
||||
tmux select-window -t penpot:3
|
||||
tmux new-window -t penpot:4 -n 'exporter'
|
||||
tmux select-window -t penpot:4
|
||||
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
|
||||
tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l
|
||||
tmux send-keys -t penpot 'yarn run watch' enter
|
||||
@@ -45,8 +50,8 @@ tmux split-window -v
|
||||
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
|
||||
tmux send-keys -t penpot './scripts/wait-and-start.sh' enter
|
||||
|
||||
tmux new-window -t penpot:4 -n 'backend'
|
||||
tmux select-window -t penpot:4
|
||||
tmux new-window -t penpot:5 -n 'backend'
|
||||
tmux select-window -t penpot:5
|
||||
tmux send-keys -t penpot 'cd penpot/backend' enter C-l
|
||||
tmux send-keys -t penpot './scripts/start-dev' enter
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ RUN set -ex; \
|
||||
mkdir -p /etc/nginx/overrides/main.d/; \
|
||||
mkdir -p /etc/nginx/overrides/http.d/; \
|
||||
mkdir -p /etc/nginx/overrides/server.d/; \
|
||||
mkdir -p /etc/nginx/overrides/assets.d/; \
|
||||
mkdir -p /etc/nginx/overrides/location.d/;
|
||||
|
||||
ARG BUNDLE_PATH="./bundle-frontend/"
|
||||
|
||||
@@ -110,8 +110,6 @@ http {
|
||||
recursive_error_pages on;
|
||||
proxy_intercept_errors on;
|
||||
error_page 301 302 307 = @handle_redirect;
|
||||
|
||||
include /etc/nginx/overrides/assets.d/*.conf;
|
||||
}
|
||||
|
||||
location /internal/assets {
|
||||
|
||||
@@ -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,12 +16,11 @@
|
||||
"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",
|
||||
"xml-js": "^1.6.11",
|
||||
"xregexp": "^5.1.2"
|
||||
},
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
(def browser-pool-factory
|
||||
(letfn [(create []
|
||||
(p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]}
|
||||
(p/let [opts #js {:args #js ["--font-render-hinting=none"]}
|
||||
browser (.launch pw/chromium opts)
|
||||
id (swap! pool-browser-id inc)]
|
||||
(l/info :origin "factory" :action "create" :browser-id id)
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
(p/fmap (fn [resource]
|
||||
(assoc exchange :response/body resource)))
|
||||
(p/merr (fn [cause]
|
||||
(l/error :hint "unexpected error on single export"
|
||||
(l/error :hint "unexpected error on export multiple"
|
||||
:cause cause)
|
||||
(p/rejected cause))))))
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
(redis/pub! topic data))))
|
||||
|
||||
on-error (fn [cause]
|
||||
(l/error :hint "unexpected error on multiple export" :cause cause)
|
||||
(l/error :hint "unexpected error on multiple exportation" :cause cause)
|
||||
(if wait
|
||||
(p/rejected cause)
|
||||
(redis/pub! topic {:type :export-update
|
||||
@@ -107,12 +107,12 @@
|
||||
:on-progress on-progress)
|
||||
|
||||
append (fn [{:keys [filename path] :as resource}]
|
||||
(rsc/add-to-zip zip path (str/replace filename sanitize-file-regex "_")))
|
||||
(rsc/add-to-zip! zip path (str/replace filename sanitize-file-regex "_")))
|
||||
|
||||
proc (->> exports
|
||||
(map (fn [export] (rd/render export append)))
|
||||
(p/all)
|
||||
(p/mcat (fn [_] (rsc/close-zip zip)))
|
||||
(p/fnly (fn [_] (.finalize zip)))
|
||||
(p/fmap (constantly resource))
|
||||
(p/mcat (partial rsc/upload-resource auth-token))
|
||||
(p/fmap (fn [resource]
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
["node:fs" :as fs]
|
||||
["node:fs/promises" :as fsp]
|
||||
["node:path" :as path]
|
||||
["undici" :as http]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uri :as u]
|
||||
@@ -54,40 +53,30 @@
|
||||
(.pipe zip out)
|
||||
zip))
|
||||
|
||||
(defn add-to-zip
|
||||
(defn add-to-zip!
|
||||
[zip path name]
|
||||
(.file ^js zip path #js {:name name}))
|
||||
|
||||
(defn close-zip
|
||||
(defn close-zip!
|
||||
[zip]
|
||||
(p/create (fn [resolve]
|
||||
(.on ^js zip "close" resolve)
|
||||
(.finalize ^js zip))))
|
||||
(.finalize ^js zip))
|
||||
|
||||
(defn upload-resource
|
||||
[auth-token resource]
|
||||
(->> (fsp/readFile (:path resource))
|
||||
(p/fmap (fn [buffer]
|
||||
(js/console.log buffer)
|
||||
(new js/Blob #js [buffer] #js {:type (:mtype resource)})))
|
||||
(p/mcat (fn [blob]
|
||||
(let [fdata (new http/FormData)
|
||||
agent (new http/Agent #js {:connect #js {:rejectUnauthorized false}})
|
||||
headers #js {"X-Shared-Key" cf/management-key
|
||||
"Authorization" (str "Bearer " auth-token)}
|
||||
|
||||
request #js {:headers headers
|
||||
:method "POST"
|
||||
:body fdata
|
||||
:dispatcher agent}
|
||||
uri (-> (cf/get :public-uri)
|
||||
(u/ensure-path-slash)
|
||||
(u/join "api/management/methods/upload-tempfile")
|
||||
(str))]
|
||||
|
||||
(let [fdata (new js/FormData)
|
||||
uri (-> (cf/get :public-uri)
|
||||
(u/ensure-path-slash)
|
||||
(u/join "api/management/methods/upload-tempfile")
|
||||
(str))]
|
||||
(.append fdata "content" blob (:filename resource))
|
||||
(http/fetch uri request))))
|
||||
|
||||
(js/fetch uri #js {:headers #js {"X-Shared-Key" cf/management-key
|
||||
"Authorization" (str "Bearer " auth-token)}
|
||||
:method "POST"
|
||||
:body fdata}))))
|
||||
(p/mcat (fn [response]
|
||||
(if (not= (.-status response) 200)
|
||||
(ex/raise :type :internal
|
||||
|
||||
@@ -75,8 +75,7 @@
|
||||
[path]
|
||||
(->> (.stat fs/promises path)
|
||||
(p/fmap (fn [data]
|
||||
{:path path
|
||||
:created-at (inst-ms (.-ctime ^js data))
|
||||
{:created-at (inst-ms (.-ctime ^js data))
|
||||
:size (.-size data)}))
|
||||
(p/merr (fn [_cause]
|
||||
(p/resolved nil)))))
|
||||
|
||||
@@ -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,12 +577,11 @@ __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"
|
||||
ws: "npm:^8.18.3"
|
||||
xml-js: "npm:^1.6.11"
|
||||
xregexp: "npm:^5.1.2"
|
||||
@@ -683,16 +682,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 +715,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 +733,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 +754,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 +774,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 +1105,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 +1160,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 +1269,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 +1367,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 +1499,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
|
||||
@@ -1514,13 +1513,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici@npm:^7.16.0":
|
||||
version: 7.16.0
|
||||
resolution: "undici@npm:7.16.0"
|
||||
checksum: 10c0/efd867792e9f233facf9efa0a087e2d9c3e4415c0b234061b9b40307ca4fa01d945fee4d43c7b564e1b80e0d519bcc682f9f6e0de13c717146c00a80e2f1fb0f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unique-filename@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "unique-filename@npm:4.0.0"
|
||||
@@ -1539,7 +1531,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
|
||||
|
||||
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@@ -11,4 +11,4 @@ node_modules/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/**/visual-specs/**/*.png
|
||||
|
||||
/ts/dist/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)", "../ts/src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
staticDirs: ["../resources/public"],
|
||||
addons: [
|
||||
"@storybook/addon-themes",
|
||||
|
||||
@@ -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,13 +45,10 @@
|
||||
{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
|
||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]
|
||||
:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
|
||||
"-Dpenpot.wasm.profile-marks=true"
|
||||
"-XX:+UnlockExperimentalVMOptions"
|
||||
"-XX:CompileCommand=blackhole,criterium.blackhole.Blackhole::consume"]}
|
||||
:jvm-opts ["--sun-misc-unsafe-memory-access=allow" "-Dpenpot.wasm.profile-marks=true"]}
|
||||
}}
|
||||
|
||||
23
frontend/eslint.config.js
Normal file
23
frontend/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['ts/dist']),
|
||||
{
|
||||
files: ['ts/src/**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -17,7 +17,8 @@
|
||||
"@zip.js/zip.js@npm:^2.7.44": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"playwright": "1.52.0",
|
||||
"playwright-core": "1.52.0"
|
||||
"playwright-core": "1.52.0",
|
||||
"globals": "^16.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build:app:assets": "node ./scripts/build-app-assets.js",
|
||||
@@ -32,8 +33,9 @@
|
||||
"e2e:server": "node ./scripts/e2e-server.js",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
|
||||
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js -w",
|
||||
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js text-editor/**/*.js",
|
||||
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c ts/src/**/*.ts -w",
|
||||
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c ts/src/**/*.ts -c --check",
|
||||
"lint:ts": "eslint ts/",
|
||||
"lint:clj": "clj-kondo --parallel --lint src/",
|
||||
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
|
||||
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
||||
@@ -50,77 +52,95 @@
|
||||
"watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
|
||||
"watch": "yarn run watch:app:assets",
|
||||
"watch:storybook": "yarn run build:storybook:assets && concurrently \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"",
|
||||
"watch:ts": "vite build --watch",
|
||||
"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",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@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",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"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",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"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.1",
|
||||
"@penpot/text-editor": "portal:./text-editor",
|
||||
"@penpot/ts": "portal:./ts",
|
||||
"@tokens-studio/sd-transforms": "1.2.11",
|
||||
"@vitejs/plugin-react": "4.2.0",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "New File 1",
|
||||
"~:revn": 11,
|
||||
"~:modified-at": "~m1713873823633",
|
||||
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:is-shared": false,
|
||||
"~:version": 46,
|
||||
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
|
||||
"~:created-at": "~m1713536343369",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u66697432-c33d-8055-8006-2c62cc084cad"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u66697432-c33d-8055-8006-2c62cc084cad": {
|
||||
"~#penpot/pointer": [
|
||||
"~ude58c8f6-c5c2-8196-8004-3df9e2e52d88",
|
||||
{
|
||||
"~:created-at": "~m1713873823636"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:options": {
|
||||
"~:components-v2": true
|
||||
},
|
||||
"~:recent-colors": [
|
||||
{
|
||||
"~:color": "#0000ff",
|
||||
"~:opacity": 1,
|
||||
"~:id": null,
|
||||
"~:file-id": null,
|
||||
"~:image": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"fdata/path-data",
|
||||
"plugins/runtime",
|
||||
"design-tokens/v1",
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type",
|
||||
"text-editor/v2"
|
||||
]
|
||||
},
|
||||
"~:team-id": "~u9e6e22b2-db76-81d6-8006-75d7cdbb8bad",
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "Bug 11552",
|
||||
"~:revn": 3,
|
||||
"~:modified-at": "~m1753957736516",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
|
||||
"~:is-shared": false,
|
||||
"~:migrations": {
|
||||
"~#ordered-set": [
|
||||
"legacy-2",
|
||||
"legacy-3",
|
||||
"legacy-5",
|
||||
"legacy-6",
|
||||
"legacy-7",
|
||||
"legacy-8",
|
||||
"legacy-9",
|
||||
"legacy-10",
|
||||
"legacy-11",
|
||||
"legacy-12",
|
||||
"legacy-13",
|
||||
"legacy-14",
|
||||
"legacy-16",
|
||||
"legacy-17",
|
||||
"legacy-18",
|
||||
"legacy-19",
|
||||
"legacy-25",
|
||||
"legacy-26",
|
||||
"legacy-27",
|
||||
"legacy-28",
|
||||
"legacy-29",
|
||||
"legacy-31",
|
||||
"legacy-32",
|
||||
"legacy-33",
|
||||
"legacy-34",
|
||||
"legacy-36",
|
||||
"legacy-37",
|
||||
"legacy-38",
|
||||
"legacy-39",
|
||||
"legacy-40",
|
||||
"legacy-41",
|
||||
"legacy-42",
|
||||
"legacy-43",
|
||||
"legacy-44",
|
||||
"legacy-45",
|
||||
"legacy-46",
|
||||
"legacy-47",
|
||||
"legacy-48",
|
||||
"legacy-49",
|
||||
"legacy-50",
|
||||
"legacy-51",
|
||||
"legacy-52",
|
||||
"legacy-53",
|
||||
"legacy-54",
|
||||
"legacy-55",
|
||||
"legacy-56",
|
||||
"legacy-57",
|
||||
"legacy-59",
|
||||
"legacy-62",
|
||||
"legacy-65",
|
||||
"legacy-66",
|
||||
"legacy-67",
|
||||
"0001-remove-tokens-from-groups",
|
||||
"0002-normalize-bool-content-v2",
|
||||
"0002-clean-shape-interactions",
|
||||
"0003-fix-root-shape",
|
||||
"0003-convert-path-content-v2",
|
||||
"0004-clean-shadow-color",
|
||||
"0005-deprecate-image-type",
|
||||
"0006-fix-old-texts-fills",
|
||||
"0007-clear-invalid-strokes-and-fills-v2",
|
||||
"0008-fix-library-colors-v4",
|
||||
"0009-clean-library-colors",
|
||||
"0009-add-partial-text-touched-flags"
|
||||
]
|
||||
},
|
||||
"~:version": 67,
|
||||
"~:project-id": "~u9e6e22b2-db76-81d6-8006-75d7cdc30669",
|
||||
"~:created-at": "~m1753957644225",
|
||||
"~:data": {
|
||||
"~:pages": ["~u238a17e0-75ff-8075-8006-934586ea2231"],
|
||||
"~:pages-index": {
|
||||
"~u238a17e0-75ff-8075-8006-934586ea2231": {
|
||||
"~:objects": {
|
||||
"~u00000000-0000-0000-0000-000000000000": {
|
||||
"~#shape": {
|
||||
"~:y": 0,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:name": "Root Frame",
|
||||
"~:width": 0.01,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.01
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 0,
|
||||
"~:proportion": 1.0,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 0,
|
||||
"~:y": 0,
|
||||
"~:width": 0.01,
|
||||
"~:height": 0.01,
|
||||
"~:x1": 0,
|
||||
"~:y1": 0,
|
||||
"~:x2": 0.01,
|
||||
"~:y2": 0.01
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#FFFFFF",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 0.01,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": ["~ucc6f0580-449c-8019-8006-9345db077fa0"]
|
||||
}
|
||||
},
|
||||
"~ucc6f0580-449c-8019-8006-9345db077fa0": {
|
||||
"~#shape": {
|
||||
"~:y": 438,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:auto-width",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "1s4am1jl24s",
|
||||
"~:children": [
|
||||
{
|
||||
"~:type": "paragraph-set",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "13p0zwl2yhc",
|
||||
"~:font-size": "14",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "Lorem ipsum"
|
||||
}
|
||||
],
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "20hf3kmyoub",
|
||||
"~:font-size": "14",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:type": "paragraph",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Lorem ipsum",
|
||||
"~:width": 77,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 404,
|
||||
"~:y": 438
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 481,
|
||||
"~:y": 438
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 481,
|
||||
"~:y": 455
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 404,
|
||||
"~:y": 455
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:id": "~ucc6f0580-449c-8019-8006-9345db077fa0",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:x": 404,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 404,
|
||||
"~:y": 438,
|
||||
"~:width": 77,
|
||||
"~:height": 17,
|
||||
"~:x1": 404,
|
||||
"~:y1": 438,
|
||||
"~:x2": 481,
|
||||
"~:y2": 455
|
||||
}
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 17,
|
||||
"~:flip-y": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2231",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
w
|
||||
{
|
||||
"~:revn": 2,
|
||||
"~:lagged": []
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"~:revn": 2,
|
||||
"~:lagged": []
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
[
|
||||
{
|
||||
"~:id": "~u088df3d4-d383-80f6-8004-527e50ea4f1f",
|
||||
"~:revn": 21,
|
||||
"~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:session-id": "~u1dc6d4fa-7bd3-803a-8004-527dd9df2c62",
|
||||
"~:changes": []
|
||||
}
|
||||
]
|
||||
@@ -1,36 +0,0 @@
|
||||
export class Clipboard {
|
||||
static Permission = {
|
||||
ONLY_READ: ["clipboard-read"],
|
||||
ONLY_WRITE: ["clipboard-write"],
|
||||
ALL: ["clipboard-read", "clipboard-write"],
|
||||
};
|
||||
|
||||
static enable(context, permissions) {
|
||||
return context.grantPermissions(permissions);
|
||||
}
|
||||
|
||||
static writeText(page, text) {
|
||||
return page.evaluate((text) => navigator.clipboard.writeText(text), text);
|
||||
}
|
||||
|
||||
static readText(page) {
|
||||
return page.evaluate(() => navigator.clipboard.readText());
|
||||
}
|
||||
|
||||
constructor(page, context) {
|
||||
this.page = page;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
enable(permissions) {
|
||||
return Clipboard.enable(this.context, permissions);
|
||||
}
|
||||
|
||||
writeText(text) {
|
||||
return Clipboard.writeText(this.page, text);
|
||||
}
|
||||
|
||||
readText() {
|
||||
return Clipboard.readText(this.page);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
export class Transit {
|
||||
static parse(value) {
|
||||
if (typeof value !== "string") return value;
|
||||
|
||||
if (value.startsWith("~")) return value.slice(2);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
static get(object, ...path) {
|
||||
let aux = object;
|
||||
for (const name of path) {
|
||||
if (typeof name !== "string") {
|
||||
if (!(name in aux)) {
|
||||
return undefined;
|
||||
}
|
||||
aux = aux[name];
|
||||
} else {
|
||||
const transitName = `~:${name}`;
|
||||
if (!(transitName in aux)) {
|
||||
return undefined;
|
||||
}
|
||||
aux = aux[transitName];
|
||||
}
|
||||
}
|
||||
return this.parse(aux);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,4 @@
|
||||
export class BasePage {
|
||||
/**
|
||||
* Mocks multiple RPC calls in a single call.
|
||||
*
|
||||
* @param {Page} page
|
||||
* @param {object<string, string>} paths
|
||||
* @param {*} options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async mockRPCs(page, paths, options) {
|
||||
for (const [path, jsonFilename] of Object.entries(paths)) {
|
||||
await this.mockRPC(page, path, jsonFilename, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks an RPC call using a file.
|
||||
*
|
||||
* @param {Page} page
|
||||
* @param {string} path
|
||||
* @param {string} jsonFilename
|
||||
* @param {*} options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async mockRPC(page, path, jsonFilename, options) {
|
||||
if (!page) {
|
||||
throw new TypeError("Invalid page argument. Must be a Playwright page.");
|
||||
@@ -116,10 +93,6 @@ export class BasePage {
|
||||
return this.#page;
|
||||
}
|
||||
|
||||
async mockRPCs(paths, options) {
|
||||
return BasePage.mockRPCs(this.page, paths, options);
|
||||
}
|
||||
|
||||
async mockRPC(path, jsonFilename, options) {
|
||||
return BasePage.mockRPC(this.page, path, jsonFilename, options);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,146 +1,7 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { BaseWebSocketPage } from "./BaseWebSocketPage";
|
||||
import { Transit } from "../../helpers/Transit";
|
||||
|
||||
export class WorkspacePage extends BaseWebSocketPage {
|
||||
static TextEditor = class TextEditor {
|
||||
constructor(workspacePage) {
|
||||
this.workspacePage = workspacePage;
|
||||
|
||||
// locators.
|
||||
this.fontSize = this.workspacePage.rightSidebar.getByRole("textbox", {
|
||||
name: "Font Size",
|
||||
});
|
||||
this.lineHeight = this.workspacePage.rightSidebar.getByRole("textbox", {
|
||||
name: "Line Height",
|
||||
});
|
||||
this.letterSpacing = this.workspacePage.rightSidebar.getByRole(
|
||||
"textbox",
|
||||
{
|
||||
name: "Letter Spacing",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
get page() {
|
||||
return this.workspacePage.page;
|
||||
}
|
||||
|
||||
async waitForStyle(locator, styleName) {
|
||||
return locator.evaluate(
|
||||
(element, styleName) => element.style.getPropertyValue(styleName),
|
||||
styleName,
|
||||
);
|
||||
}
|
||||
|
||||
async waitForEditor() {
|
||||
return this.page.waitForSelector('[data-itype="editor"]');
|
||||
}
|
||||
|
||||
async waitForRoot() {
|
||||
return this.page.waitForSelector('[data-itype="root"]');
|
||||
}
|
||||
|
||||
async waitForParagraph(nth) {
|
||||
if (!nth) {
|
||||
return this.page.waitForSelector('[data-itype="paragraph"]');
|
||||
}
|
||||
return this.page.waitForSelector(
|
||||
`[data-itype="paragraph"]:nth-child(${nth})`,
|
||||
);
|
||||
}
|
||||
|
||||
async waitForParagraphStyle(nth, styleName) {
|
||||
const paragraph = await this.waitForParagraph(nth);
|
||||
return this.waitForStyle(paragraph, styleName);
|
||||
}
|
||||
|
||||
async waitForTextSpan(nth = 0) {
|
||||
if (!nth) {
|
||||
return this.page.waitForSelector('[data-itype="inline"]');
|
||||
}
|
||||
return this.page.waitForSelector(
|
||||
`[data-itype="inline"]:nth-child(${nth})`,
|
||||
);
|
||||
}
|
||||
|
||||
async waitForTextSpanContent(nth = 0) {
|
||||
const textSpan = await this.waitForTextSpan(nth);
|
||||
const textContent = await textSpan.textContent();
|
||||
return textContent;
|
||||
}
|
||||
|
||||
async waitForTextSpanStyle(nth, styleName) {
|
||||
const textSpan = await this.waitForTextSpan(nth);
|
||||
return this.waitForStyle(textSpan, styleName);
|
||||
}
|
||||
|
||||
async startEditing() {
|
||||
await this.page.keyboard.press("Enter");
|
||||
return this.waitForEditor();
|
||||
}
|
||||
|
||||
stopEditing() {
|
||||
return this.page.keyboard.press("Escape");
|
||||
}
|
||||
|
||||
async moveToLeft(amount = 0) {
|
||||
for (let i = 0; i < amount; i++) {
|
||||
await this.page.keyboard.press("ArrowLeft");
|
||||
}
|
||||
}
|
||||
|
||||
async moveToRight(amount = 0) {
|
||||
for (let i = 0; i < amount; i++) {
|
||||
await this.page.keyboard.press("ArrowRight");
|
||||
}
|
||||
}
|
||||
|
||||
async moveFromStart(offset = 0) {
|
||||
await this.page.keyboard.press("ArrowLeft");
|
||||
await this.moveToRight(offset);
|
||||
}
|
||||
|
||||
async moveFromEnd(offset = 0) {
|
||||
await this.page.keyboard.press("ArrowRight");
|
||||
await this.moveToLeft(offset);
|
||||
}
|
||||
|
||||
async selectFromStart(length, offset = 0) {
|
||||
await this.moveFromStart(offset);
|
||||
await this.page.keyboard.down("Shift");
|
||||
await this.moveToRight(length);
|
||||
await this.page.keyboard.up("Shift");
|
||||
}
|
||||
|
||||
async selectFromEnd(length, offset = 0) {
|
||||
await this.moveFromEnd(offset);
|
||||
await this.page.keyboard.down("Shift");
|
||||
await this.moveToLeft(length);
|
||||
await this.page.keyboard.up("Shift");
|
||||
}
|
||||
|
||||
async changeNumericInput(locator, newValue) {
|
||||
await expect(locator).toBeVisible();
|
||||
await locator.focus();
|
||||
await locator.fill(`${newValue}`);
|
||||
await locator.blur();
|
||||
}
|
||||
|
||||
changeFontSize(newValue) {
|
||||
return this.changeNumericInput(this.fontSize, newValue);
|
||||
}
|
||||
|
||||
changeLineHeight(newValue) {
|
||||
return this.changeNumericInput(this.lineHeight, newValue);
|
||||
}
|
||||
|
||||
changeLetterSpacing(newValue) {
|
||||
return this.changeNumericInput(this.letterSpacing, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be called on `test.beforeEach`.
|
||||
*
|
||||
@@ -150,21 +11,50 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
static async init(page) {
|
||||
await BaseWebSocketPage.initWebSockets(page);
|
||||
|
||||
await BaseWebSocketPage.mockRPCs(page, {
|
||||
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
||||
"get-team-users?file-id=*":
|
||||
"logged-in-user/get-team-users-single-user.json",
|
||||
"get-comment-threads?file-id=*":
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
"get-project?id=*": "workspace/get-project-default.json",
|
||||
"get-team?id=*": "workspace/get-team-default.json",
|
||||
"get-teams": "get-teams.json",
|
||||
"get-team-members?team-id=*":
|
||||
"logged-in-user/get-team-members-your-penpot.json",
|
||||
"get-profiles-for-file-comments?file-id=*":
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
"update-profile-props": "workspace/update-profile-empty.json",
|
||||
});
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"logged-in-user/get-profile-logged-in.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-team-users?file-id=*",
|
||||
"logged-in-user/get-team-users-single-user.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-comment-threads?file-id=*",
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-project?id=*",
|
||||
"workspace/get-project-default.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-team?id=*",
|
||||
"workspace/get-team-default.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams.json");
|
||||
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-team-members?team-id=*",
|
||||
"logged-in-user/get-team-members-your-penpot.json",
|
||||
);
|
||||
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-profiles-for-file-comments?file-id=*",
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
);
|
||||
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"update-profile-props",
|
||||
"workspace/update-profile-empty.json",
|
||||
);
|
||||
}
|
||||
|
||||
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
|
||||
@@ -172,20 +62,9 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
|
||||
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
|
||||
|
||||
/**
|
||||
* WebSocket mock
|
||||
*
|
||||
* @type {MockWebSocketHelper}
|
||||
*/
|
||||
#ws = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {Page} page
|
||||
* @param {} [options]
|
||||
*/
|
||||
constructor(page, options) {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
this.pageName = page.getByTestId("page-name");
|
||||
|
||||
@@ -233,14 +112,11 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
"tokens-context-menu-for-set",
|
||||
);
|
||||
this.contextMenuForShape = page.getByTestId("context-menu");
|
||||
if (options?.textEditor) {
|
||||
this.textEditor = new WorkspacePage.TextEditor(this);
|
||||
}
|
||||
}
|
||||
|
||||
async goToWorkspace({
|
||||
fileId = this.fileId ?? WorkspacePage.anyFileId,
|
||||
pageId = this.pageId ?? WorkspacePage.anyPageId,
|
||||
fileId = WorkspacePage.anyFileId,
|
||||
pageId = WorkspacePage.anyPageId,
|
||||
} = {}) {
|
||||
await this.page.goto(
|
||||
`/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
|
||||
@@ -265,59 +141,48 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
}
|
||||
|
||||
async setupEmptyFile() {
|
||||
await this.mockRPCs({
|
||||
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
||||
"get-team-users?file-id=*":
|
||||
"logged-in-user/get-team-users-single-user.json ",
|
||||
"get-comment-threads?file-id=*":
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
"get-project?id=*": "workspace/get-project-default.json",
|
||||
"get-team?id=*": "workspace/get-team-default.json",
|
||||
"get-profiles-for-file-comments?file-id=*":
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
"get-file-object-thumbnails?file-id=*":
|
||||
"workspace/get-file-object-thumbnails-blank.json",
|
||||
"get-font-variants?team-id=*": "workspace/get-font-variants-empty.json",
|
||||
"get-file-fragment?file-id=*": "workspace/get-file-fragment-blank.json",
|
||||
"get-file-libraries?file-id=*": "workspace/get-file-libraries-empty.json",
|
||||
});
|
||||
|
||||
if (this.textEditor) {
|
||||
await this.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
}
|
||||
|
||||
// by default we mock the blank file.
|
||||
await this.mockGetFile("workspace/get-file-blank.json");
|
||||
await this.mockRPC(
|
||||
"get-profile",
|
||||
"logged-in-user/get-profile-logged-in.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-team-users?file-id=*",
|
||||
"logged-in-user/get-team-users-single-user.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-comment-threads?file-id=*",
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-project?id=*",
|
||||
"workspace/get-project-default.json",
|
||||
);
|
||||
await this.mockRPC("get-team?id=*", "workspace/get-team-default.json");
|
||||
await this.mockRPC(
|
||||
"get-profiles-for-file-comments?file-id=*",
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
);
|
||||
await this.mockRPC(/get\-file\?/, "workspace/get-file-blank.json");
|
||||
await this.mockRPC(
|
||||
"get-file-object-thumbnails?file-id=*",
|
||||
"workspace/get-file-object-thumbnails-blank.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-font-variants?team-id=*",
|
||||
"workspace/get-font-variants-empty.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-file-fragment?file-id=*",
|
||||
"workspace/get-file-fragment-blank.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-file-libraries?file-id=*",
|
||||
"workspace/get-file-libraries-empty.json",
|
||||
);
|
||||
}
|
||||
|
||||
async mockGetFile(jsonFilename, options) {
|
||||
const page = this.page;
|
||||
const jsonPath = `playwright/data/${jsonFilename}`;
|
||||
const body = await readFile(jsonPath, "utf-8");
|
||||
const payload = JSON.parse(body);
|
||||
|
||||
const fileId = Transit.get(payload, "id");
|
||||
const pageId = Transit.get(payload, "data", "pages", 0);
|
||||
const teamId = Transit.get(payload, "team-id");
|
||||
|
||||
this.fileId = fileId ?? this.anyFileId;
|
||||
this.pageId = pageId ?? this.anyPageId;
|
||||
this.teamId = teamId ?? this.anyTeamId;
|
||||
|
||||
const path = /get\-file\?/;
|
||||
const url = typeof path === "string" ? `**/api/main/methods/${path}` : path;
|
||||
const interceptConfig = {
|
||||
status: 200,
|
||||
contentType: "application/transit+json",
|
||||
...options,
|
||||
};
|
||||
return page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
...interceptConfig,
|
||||
body,
|
||||
}),
|
||||
);
|
||||
// await this.mockRPC(/get\-file\?/, jsonFile);
|
||||
async mockGetFile(jsonFile) {
|
||||
await this.mockRPC(/get\-file\?/, jsonFile);
|
||||
}
|
||||
|
||||
async mockGetAsset(regex, asset) {
|
||||
@@ -325,15 +190,22 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
}
|
||||
|
||||
async setupFileWithComments() {
|
||||
await this.mockRPCs({
|
||||
"get-comment-threads?file-id=*":
|
||||
"workspace/get-comment-threads-unread.json",
|
||||
"get-file-fragment?file-id=*&fragment-id=*":
|
||||
"viewer/get-file-fragment-single-board.json",
|
||||
"get-comments?thread-id=*": "workspace/get-thread-comments.json",
|
||||
"update-comment-thread-status":
|
||||
"workspace/update-comment-thread-status.json",
|
||||
});
|
||||
await this.mockRPC(
|
||||
"get-comment-threads?file-id=*",
|
||||
"workspace/get-comment-threads-unread.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-file-fragment?file-id=*&fragment-id=*",
|
||||
"viewer/get-file-fragment-single-board.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-comments?thread-id=*",
|
||||
"workspace/get-thread-comments.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"update-comment-thread-status",
|
||||
"workspace/update-comment-thread-status.json",
|
||||
);
|
||||
}
|
||||
|
||||
async clickWithDragViewportAt(x, y, width, height) {
|
||||
@@ -351,67 +223,6 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks and moves from the coordinates x1,y1 to x2,y2
|
||||
*
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {number} x2
|
||||
* @param {number} y2
|
||||
*/
|
||||
async clickAndMove(x1, y1, x2, y2) {
|
||||
await this.page.waitForTimeout(100);
|
||||
await this.viewport.hover({ position: { x: x1, y: y1 } });
|
||||
await this.page.mouse.down();
|
||||
await this.viewport.hover({ position: { x: x2, y: y2 } });
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Text Shape in the specified coordinates
|
||||
* with an initial text.
|
||||
*
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {number} x2
|
||||
* @param {number} y2
|
||||
* @param {string} initialText
|
||||
* @param {*} [options]
|
||||
*/
|
||||
async createTextShape(x1, y1, x2, y2, initialText, options) {
|
||||
const timeToWait = options?.timeToWait ?? 100;
|
||||
await this.page.keyboard.press("T");
|
||||
await this.page.waitForTimeout(timeToWait);
|
||||
await this.clickAndMove(x1, y1, x2, y2);
|
||||
await this.page.waitForTimeout(timeToWait);
|
||||
if (initialText) {
|
||||
await this.page.keyboard.type(initialText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the selected element into the clipboard.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async copy() {
|
||||
return this.page.keyboard.press("Control+C");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastes something from the clipboard.
|
||||
*
|
||||
* @param {"keyboard"|"context-menu"} [kind="keyboard"]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async paste(kind = "keyboard") {
|
||||
if (kind === "context-menu") {
|
||||
await this.viewport.click({ button: "right" });
|
||||
return this.page.getByText("PasteCtrlV").click();
|
||||
}
|
||||
return this.page.keyboard.press("Control+V");
|
||||
}
|
||||
|
||||
async panOnViewportAt(x, y, width, height) {
|
||||
await this.page.waitForTimeout(100);
|
||||
await this.viewport.hover({ position: { x, y } });
|
||||
@@ -439,15 +250,10 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async doubleClickLeafLayer(name, clickOptions = {}) {
|
||||
await this.clickLeafLayer(name, clickOptions);
|
||||
await this.clickLeafLayer(name, clickOptions);
|
||||
}
|
||||
|
||||
async clickToggableLayer(name, clickOptions = {}) {
|
||||
const layer = this.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: name });
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: name });
|
||||
const button = layer.getByRole("button");
|
||||
|
||||
await button.waitFor();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
@@ -360,7 +360,7 @@ test("Renders a file with texts with paragraphs and breaking lines", async ({
|
||||
id: "a5f238bd-dd8a-8164-8007-1bc3481eaf05",
|
||||
pageId: "a5f238bd-dd8a-8164-8007-1bc3481eaf06",
|
||||
});
|
||||
await workspace.waitForFirstRenderWithoutUI();
|
||||
await workspace.waitForFirstRender();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -18,10 +18,6 @@ const setupFile = async (workspacePage) => {
|
||||
fileId: "7b2da435-6186-815a-8007-0daa95d2f26d",
|
||||
pageId: "ce79274b-11ab-8088-8007-0487ad43f789",
|
||||
});
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-empty.json",
|
||||
);
|
||||
};
|
||||
|
||||
const shapeToLayerName = {
|
||||
|
||||
@@ -51,7 +51,7 @@ test.skip("BUG 12164 - Crash when trying to fetch a missing font", async ({
|
||||
pageId: "2b7f0188-51a1-8193-8006-e05bad87b74d",
|
||||
});
|
||||
|
||||
await workspacePage.page.waitForTimeout(1000);
|
||||
await workspacePage.page.waitForTimeout(1000)
|
||||
await workspacePage.waitForFirstRender();
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -1,323 +1,12 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { Clipboard } from "../../helpers/Clipboard";
|
||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||
|
||||
const timeToWait = 100;
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
await Clipboard.enable(context, Clipboard.Permission.ONLY_WRITE);
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WorkspacePage.init(page);
|
||||
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ context }) => {
|
||||
context.clearPermissions();
|
||||
});
|
||||
|
||||
test("Create a new text shape", async ({ page }) => {
|
||||
const initialText = "Lorem ipsum";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.createTextShape(190, 150, 300, 200, initialText);
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe(initialText);
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Create a new text shape from pasting text", async ({ page, context }) => {
|
||||
const textToPaste = "Lorem ipsum";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickAt(190, 150);
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
await page.waitForTimeout(timeToWait);
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe(textToPaste);
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Create a new text shape from pasting text using context menu", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const textToPaste = "Lorem ipsum";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickAt(190, 150);
|
||||
await workspace.paste("context-menu");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe(textToPaste);
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update an already created text shape by appending text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromEnd(0);
|
||||
await page.keyboard.type(" dolor sit amet");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem ipsum dolor sit amet");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update an already created text shape by prepending text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromStart(0);
|
||||
await page.keyboard.type("Dolor sit amet ");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update an already created text shape by inserting text in between", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromStart(5);
|
||||
await page.keyboard.type(" dolor sit amet");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem dolor sit amet ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape appending text by pasting text", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const textToPaste = " dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromEnd();
|
||||
await workspace.paste("keyboard");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem ipsum dolor sit amet");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape prepending text by pasting text", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const textToPaste = "Dolor sit amet ";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromStart();
|
||||
await workspace.paste("keyboard");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape replacing (starting) text with pasted text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const textToPaste = "Dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Dolor sit amet ipsum");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape replacing (ending) text with pasted text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const textToPaste = "dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromEnd(5);
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem dolor sit amet");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape replacing (in between) text with pasted text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const textToPaste = "dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5, 3);
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lordolor sit ametsum");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update text font size selecting a part of it (starting)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
await workspace.textEditor.changeFontSize(36);
|
||||
|
||||
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
|
||||
expect(textContent1).toBe("Lorem");
|
||||
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
|
||||
expect(textContent2).toBe(" ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test.skip("Update text line height selecting a part of it (starting)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
await workspace.textEditor.changeLineHeight(1.4);
|
||||
|
||||
const lineHeight = await workspace.textEditor.waitForParagraphStyle(
|
||||
1,
|
||||
"line-height",
|
||||
);
|
||||
expect(lineHeight).toBe("1.4");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem ipsum");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test.skip("Update text letter spacing selecting a part of it (starting)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
await workspace.textEditor.changeLetterSpacing(10);
|
||||
|
||||
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
|
||||
expect(textContent1).toBe("Lorem");
|
||||
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
|
||||
expect(textContent2).toBe(" ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||
test.skip("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||
const workspace = new WorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-11552.json");
|
||||
@@ -325,16 +14,21 @@ test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||
"update-file?id=*",
|
||||
"text-editor/update-file-11552.json",
|
||||
);
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.doubleClickLeafLayer("Lorem ipsum");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
fileId: "238a17e0-75ff-8075-8006-934586ea2230",
|
||||
pageId: "238a17e0-75ff-8075-8006-934586ea2231",
|
||||
});
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
|
||||
const fontSizeInput = workspace.rightSidebar.getByRole("textbox", {
|
||||
name: "Font Size",
|
||||
});
|
||||
await expect(fontSizeInput).toBeVisible();
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
await page.keyboard.press("ArrowRight");
|
||||
await workspace.page.keyboard.press("Enter");
|
||||
await workspace.page.keyboard.press("ArrowRight");
|
||||
|
||||
await fontSizeInput.fill("36");
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -746,6 +746,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
@include flexCenter;
|
||||
height: $s-48;
|
||||
width: $s-48;
|
||||
border-radius: $br-circle;
|
||||
background-color: var(--empty-message-background-color);
|
||||
svg {
|
||||
@extend .button-icon;
|
||||
height: $s-28;
|
||||
width: $s-28;
|
||||
stroke: var(--empty-message-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.attr-title {
|
||||
div {
|
||||
margin-left: 0;
|
||||
|
||||
@@ -18,14 +18,13 @@
|
||||
<meta name="twitter:creator" content="@penpotapp">
|
||||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
||||
<link id="theme" href="css/main.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
<link href="css/ts-style.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
{{#isDebug}}
|
||||
<link href="css/debug.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
{{/isDebug}}
|
||||
|
||||
<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 +34,7 @@
|
||||
{{# manifest}}
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
<script type="importmap">{{& importmap }}</script>
|
||||
{{/manifest}}
|
||||
|
||||
<!--cookie-consent-->
|
||||
@@ -50,9 +50,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>
|
||||
|
||||
@@ -181,13 +181,13 @@ export async function watch(baseDir, predicate, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
async function readManifestFile(resource) {
|
||||
const manifestPath = "resources/public/" + resource;
|
||||
async function readManifestFile() {
|
||||
const manifestPath = "resources/public/js/manifest.json";
|
||||
let content = await fs.readFile(manifestPath, { encoding: "utf8" });
|
||||
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(
|
||||
|
||||
@@ -20,26 +20,27 @@ echo $PATH
|
||||
set -ex
|
||||
|
||||
corepack enable;
|
||||
corepack install;
|
||||
corepack install || exit 1;
|
||||
yarn install || exit 1;
|
||||
|
||||
rm -rf target/dist;
|
||||
rm -rf resources/public;
|
||||
rm -rf target/dist;
|
||||
|
||||
mkdir -p resources/public;
|
||||
mkdir -p target/dist;
|
||||
|
||||
pushd ../render-wasm;
|
||||
./build
|
||||
popd
|
||||
yarn run build:app:main $EXTRA_PARAMS || exit 1
|
||||
|
||||
yarn run build:app:main $EXTRA_PARAMS;
|
||||
yarn run build:app:libs;
|
||||
yarn run build:app:assets;
|
||||
if [ "$INCLUDE_WASM" = "yes" ]; then
|
||||
yarn run build:wasm || exit 1;
|
||||
fi
|
||||
|
||||
sed -i "s/\.\/render.js/.\/render.js?version=$CURRENT_VERSION/g" resources/public/js/worker/main*.js
|
||||
yarn run build:app:libs || exit 1;
|
||||
yarn run build:app:assets || exit 1;
|
||||
|
||||
rsync -avr resources/public/ target/dist/
|
||||
sed -i "s/render-wasm.js/render-wasm.js?version=$CURRENT_VERSION/g" ./resources/public/js/worker/main.js;
|
||||
|
||||
rsync -avr resources/public/ target/dist/;
|
||||
|
||||
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
|
||||
# build storybook
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
:source-map-detail-level :all}}}
|
||||
|
||||
:worker
|
||||
{:target :browser
|
||||
{:target :esm
|
||||
:output-dir "resources/public/js/worker/"
|
||||
:asset-path "/js/worker"
|
||||
:devtools {:browser-inject :main
|
||||
@@ -92,7 +92,6 @@
|
||||
{:main
|
||||
{:entries [app.worker]
|
||||
:web-worker true
|
||||
:prepend-js "importScripts('./render.js');"
|
||||
:depends-on #{}}}
|
||||
|
||||
:js-options
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
public-uri))
|
||||
|
||||
(def worker-uri
|
||||
(obj/get global "penpotWorkerURI" "/js/worker/main.js"))
|
||||
(obj/get global "penpotWorkerURI" "/js/worker.js"))
|
||||
|
||||
(defn external-feature-flag
|
||||
[flag value]
|
||||
@@ -188,11 +188,6 @@
|
||||
(true? thumbnail?) (u/join (dm/str id "/thumbnail"))
|
||||
(false? thumbnail?) (u/join (dm/str id)))))))
|
||||
|
||||
(defn resolve-href
|
||||
[resource]
|
||||
(let [version (get version :full)
|
||||
href (-> public-uri
|
||||
(u/ensure-path-slash)
|
||||
(u/join resource)
|
||||
(get :path))]
|
||||
(str href "?version=" version)))
|
||||
(defn resolve-static-asset
|
||||
[path]
|
||||
(u/join public-uri path))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.main
|
||||
(:require
|
||||
["@penpot/ts" :refer [setTranslation]]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as log]
|
||||
[app.common.types.objects-map]
|
||||
@@ -38,6 +39,8 @@
|
||||
(log/setup! {:app :info})
|
||||
(log/set-level! :debug)
|
||||
|
||||
(setTranslation i18n/tr)
|
||||
|
||||
(when (= :browser cf/target)
|
||||
(log/inf :version (:full cf/version)
|
||||
:asserts *assert*
|
||||
@@ -90,10 +93,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)
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
(map :page-id))
|
||||
|
||||
(defn- apply-changes-localy
|
||||
[{:keys [file-id redo-changes ignore-wasm?] :as commit} pending]
|
||||
[{:keys [file-id redo-changes] :as commit} pending]
|
||||
(ptk/reify ::apply-changes-localy
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -103,7 +103,7 @@
|
||||
pids (into #{} xf:map-page-id redo-changes)]
|
||||
(reduce #(ctst/update-object-indices %1 %2) fdata pids)))]
|
||||
|
||||
(if (and (not ignore-wasm?) (features/active-feature? state "render-wasm/v1"))
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
;; Update the wasm model
|
||||
(let [shape-changes (volatile! {})
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
(defn commit
|
||||
"Create a commit event instance"
|
||||
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
|
||||
file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?]}]
|
||||
file-id file-revn file-vern undo-group tags stack-undo? source]}]
|
||||
|
||||
(assert (cpc/check-changes redo-changes)
|
||||
"expect valid vector of changes for redo-changes")
|
||||
@@ -147,8 +147,7 @@
|
||||
:save-undo? save-undo?
|
||||
:undo-group undo-group
|
||||
:tags tags
|
||||
:stack-undo? stack-undo?
|
||||
:ignore-wasm? ignore-wasm?}]
|
||||
:stack-undo? stack-undo?}]
|
||||
|
||||
(ptk/reify ::commit
|
||||
cljs.core/IDeref
|
||||
|
||||
@@ -386,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))))))
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
[app.main.data.modal :as modal]
|
||||
[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]
|
||||
@@ -77,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))))
|
||||
|
||||
@@ -154,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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -490,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
|
||||
@@ -687,156 +656,3 @@
|
||||
:team-role-change (handle-change-team-role msg)
|
||||
:team-membership-change (dcm/team-membership-change msg)
|
||||
nil))
|
||||
|
||||
|
||||
;; --- Delete files immediately
|
||||
|
||||
(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
|
||||
ev/Event
|
||||
(-data [_]
|
||||
{:team-id team-id
|
||||
:num-files (count ids)})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)]
|
||||
(->> (rp/cmd! :permanently-delete-team-files {:team-id team-id :ids ids})
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- Restore deleted files immediately
|
||||
|
||||
(defn- initialize-restore-status
|
||||
[files]
|
||||
(ptk/reify ::init-restore-status
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [restore-state {:in-progress true
|
||||
:healthy? true
|
||||
:error false
|
||||
:progress 0
|
||||
:widget-visible true
|
||||
:detail-visible true
|
||||
:files files
|
||||
:last-update (ct/now)
|
||||
:cmd :restore-files}]
|
||||
(assoc state :restore restore-state)))))
|
||||
|
||||
(defn- update-restore-status
|
||||
[{:keys [index total] :as data}]
|
||||
(ptk/reify ::upd-restore-status
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [time-diff (ct/diff-ms (get-in state [:restore :last-update]) (ct/now))
|
||||
healthy? (< time-diff 6000)]
|
||||
(update state :restore assoc
|
||||
:progress index
|
||||
:total total
|
||||
:last-update (ct/now)
|
||||
:healthy? healthy?)))))
|
||||
|
||||
(defn- complete-restore-status
|
||||
[]
|
||||
(ptk/reify ::comp-restore-status
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [total (get-in state [:restore :total])]
|
||||
(update state :restore assoc
|
||||
:in-progress false
|
||||
:progress total ; Ensure progress equals total on completion
|
||||
:last-update (ct/now))))))
|
||||
|
||||
(defn- error-restore-status
|
||||
[error]
|
||||
(ptk/reify ::err-restore-status
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :restore assoc
|
||||
:in-progress false
|
||||
:error error
|
||||
:last-update (ct/now)
|
||||
:healthy? false))))
|
||||
|
||||
(defn toggle-restore-detail-visibility
|
||||
[]
|
||||
(ptk/reify ::toggle-restore-detail
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:restore :detail-visible] not))))
|
||||
|
||||
(defn retry-last-restore
|
||||
[]
|
||||
(ptk/reify ::retry-restore
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
;; Reset restore state for retry - actual retry will be handled by UI
|
||||
(if (get state :restore)
|
||||
(update state :restore assoc :error false :in-progress false)
|
||||
state))))
|
||||
|
||||
(defn clear-restore-state
|
||||
[]
|
||||
(ptk/reify ::clear-restore
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(dissoc state :restore))))
|
||||
|
||||
(defn- projects-restored
|
||||
[team-id]
|
||||
(ptk/reify ::projects-restored
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
;; Refetch projects to get the updated state without deleted-at
|
||||
(rx/of (fetch-projects team-id)))))
|
||||
|
||||
(defn restore-files-immediately
|
||||
[{:keys [team-id ids] :as params}]
|
||||
(dm/assert! (uuid? team-id))
|
||||
(dm/assert! (set? ids))
|
||||
(dm/assert! (every? uuid? ids))
|
||||
|
||||
(ptk/reify ::restore-files-immediately
|
||||
ev/Event
|
||||
(-data [_]
|
||||
{:team-id team-id
|
||||
:num-files (count ids)})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
files (mapv #(hash-map :id %) ids)]
|
||||
|
||||
(rx/merge
|
||||
(rx/of (initialize-restore-status files))
|
||||
|
||||
(->> (rp/cmd! ::sse/restore-deleted-team-files {:team-id team-id :ids ids})
|
||||
(rx/tap (fn [event]
|
||||
(let [payload (sse/get-payload event)
|
||||
type (sse/get-type event)]
|
||||
(when (and payload (= type "progress"))
|
||||
(let [{:keys [index total]} payload]
|
||||
(when (and index total)
|
||||
;; Dispatch progress update
|
||||
(st/emit! (update-restore-status {:index index :total total}))))))))
|
||||
(rx/filter sse/end-of-stream?)
|
||||
(rx/map sse/get-payload)
|
||||
(rx/tap on-success)
|
||||
(rx/mapcat (fn [_]
|
||||
(rx/of (complete-restore-status)
|
||||
(projects-restored team-id))))
|
||||
(rx/catch (fn [error]
|
||||
(rx/concat
|
||||
(rx/of (error-restore-status (ex-message error)))
|
||||
(on-error error)))))
|
||||
|
||||
(rx/of (ptk/data-event ::restore-start {:total (count ids)})))))))
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
[]
|
||||
(let [uagent (new ua/UAParser)]
|
||||
(merge
|
||||
{:version (:full cf/version)
|
||||
{:app-version (:full cf/version)
|
||||
:locale i18n/*current-locale*}
|
||||
(let [browser (.getBrowser uagent)]
|
||||
{:browser (obj/get browser "name")
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
(def revn-data (atom {}))
|
||||
(def queue-conj (fnil conj #queue []))
|
||||
|
||||
(def force-persist? #(= % ::force-persist))
|
||||
|
||||
(defn- update-status
|
||||
[status]
|
||||
(ptk/reify ::update-status
|
||||
|
||||
@@ -261,19 +261,14 @@
|
||||
|
||||
(defn- parse-sd-token-font-family-value
|
||||
[value]
|
||||
(let [value (-> (js->clj value) (flatten))
|
||||
valid-font-family (or (string? value) (every? string? value))
|
||||
missing-references (seq (some cto/find-token-value-references value))]
|
||||
(let [missing-references (seq (some cto/find-token-value-references value))]
|
||||
(cond
|
||||
(not valid-font-family)
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-font-family value)]}
|
||||
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
|
||||
:else
|
||||
{:value value})))
|
||||
{:value (-> (js->clj value) (flatten))})))
|
||||
|
||||
(defn parse-atomic-typography-value [token-type token-value]
|
||||
(case token-type
|
||||
|
||||
@@ -351,31 +351,19 @@
|
||||
(on-success))))
|
||||
(rx/catch on-error))))))
|
||||
|
||||
|
||||
(def ^:private schema:create-invitation
|
||||
[:and
|
||||
[:map
|
||||
[:emails {:optional true} [::sm/set ::sm/email]]
|
||||
[:invitations {:optional true}
|
||||
[:vector
|
||||
[:map
|
||||
[:email ::sm/email]
|
||||
[:role [::sm/one-of ctt/valid-roles]]]]]
|
||||
[:team-id ::sm/uuid]
|
||||
[:resend? {:optional true} ::sm/boolean]]
|
||||
[:fn (fn [attrs]
|
||||
(or (contains? attrs :emails)
|
||||
(contains? attrs :invitations)))]])
|
||||
|
||||
(def ^:private check-create-invitations-params
|
||||
(sm/check-fn schema:create-invitation))
|
||||
|
||||
(defn create-invitations
|
||||
"Unified function to create invitations. Supports two parameter formats:
|
||||
1. {:emails #{...} :role :admin :team-id uuid} - single role for all emails
|
||||
2. {:invitations [{:email ... :role ...}] :team-id uuid} - individual roles per email"
|
||||
[{:keys [emails role team-id invitations resend?] :as params}]
|
||||
(check-create-invitations-params params)
|
||||
|
||||
(assert (uuid? team-id))
|
||||
;; Validate input format - must have either emails+role OR invitations
|
||||
(assert (or (and emails role (sm/check-set-of-emails emails) (keyword? role))
|
||||
(and invitations
|
||||
(sm/check-set-of-emails (map :email invitations))
|
||||
(every? #(contains? ctt/valid-roles (:role %)) invitations)))
|
||||
"Must provide either emails+role or invitations with individual roles")
|
||||
|
||||
(ptk/reify ::create-invitations
|
||||
ev/Event
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.persistence :as dps]
|
||||
[app.main.data.persistence :as-alias dps]
|
||||
[app.main.data.plugins :as dp]
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.data.project :as dpj]
|
||||
@@ -67,7 +67,6 @@
|
||||
[app.main.errors]
|
||||
[app.main.features :as features]
|
||||
[app.main.features.pointer-map :as fpmap]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.render-wasm :as wasm]
|
||||
@@ -270,12 +269,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]
|
||||
@@ -384,59 +379,6 @@
|
||||
(->> (rx/from added)
|
||||
(rx/map process-wasm-object)))))))
|
||||
|
||||
(when render-wasm?
|
||||
(let [local-commits-s
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/filter #(and (= :local (:source %))
|
||||
(not (contains? (:tags %) :position-data))))
|
||||
(rx/filter (complement empty?)))
|
||||
|
||||
notifier-s
|
||||
(rx/merge
|
||||
(->> local-commits-s (rx/debounce 1000))
|
||||
(->> stream (rx/filter dps/force-persist?)))
|
||||
|
||||
objects-s
|
||||
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
||||
|
||||
current-page-id-s
|
||||
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
|
||||
|
||||
(->> local-commits-s
|
||||
(rx/buffer-until notifier-s)
|
||||
(rx/with-latest-from objects-s)
|
||||
(rx/map
|
||||
(fn [[commits objects]]
|
||||
(->> commits
|
||||
(mapcat :redo-changes)
|
||||
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
|
||||
(filter #(cfh/text-shape? objects (:id %)))
|
||||
(map #(vector
|
||||
(:id %)
|
||||
(wasm.api/calculate-position-data (get objects (:id %))))))))
|
||||
|
||||
(rx/with-latest-from current-page-id-s)
|
||||
(rx/map
|
||||
(fn [[text-position-data page-id]]
|
||||
(let [changes
|
||||
(->> text-position-data
|
||||
(mapv (fn [[id position-data]]
|
||||
{:type :mod-obj
|
||||
:id id
|
||||
:page-id page-id
|
||||
:operations
|
||||
[{:type :set
|
||||
:attr :position-data
|
||||
:val position-data
|
||||
:ignore-touched true
|
||||
:ignore-geometry true}]})))]
|
||||
(dch/commit-changes
|
||||
{:redo-changes changes :undo-changes []
|
||||
:save-undo? false
|
||||
:tags #{:position-data}})))))))
|
||||
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -102,8 +102,7 @@
|
||||
{:origin it
|
||||
:redo-changes changes
|
||||
:undo-changes []
|
||||
:save-undo? false
|
||||
:ignore-wasm? true})))))))
|
||||
:save-undo? false})))))))
|
||||
|
||||
;; FIXME: would be nice to not execute this code twice per page in the
|
||||
;; same working session, maybe some local memoization can improve that
|
||||
@@ -120,5 +119,4 @@
|
||||
{:origin it
|
||||
:redo-changes changes
|
||||
:undo-changes []
|
||||
:save-undo? false
|
||||
:ignore-wasm? true})))))))
|
||||
:save-undo? false})))))))
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
|
||||
|
||||
ids
|
||||
(into (set (keys modif-tree)) xf:without-uuid-zero (keys transforms))
|
||||
(into [] xf:without-uuid-zero (keys transforms))
|
||||
|
||||
update-shape
|
||||
(fn [shape]
|
||||
|
||||
@@ -831,8 +831,7 @@
|
||||
(effect [_ state _]
|
||||
(when (features/active-feature? state "text-editor/v2")
|
||||
(let [instance (:workspace-editor state)
|
||||
attrs-to-override (some-> (editor.v2/getCurrentStyle instance)
|
||||
(styles/get-styles-from-style-declaration))
|
||||
attrs-to-override (some-> (editor.v2/getCurrentStyle instance) (styles/get-styles-from-style-declaration))
|
||||
overriden-attrs (merge attrs-to-override attrs)
|
||||
styles (styles/attrs->styles overriden-attrs)]
|
||||
(editor.v2/applyStylesToSelection instance styles))))))
|
||||
|
||||
@@ -88,10 +88,6 @@
|
||||
{:error/code :error.style-dictionary/invalid-token-value-font-weight
|
||||
:error/fn #(tr "workspace.tokens.invalid-font-weight-token-value" %)}
|
||||
|
||||
:error.style-dictionary/invalid-token-value-font-family
|
||||
{:error/code :error.style-dictionary/invalid-token-value-font-family
|
||||
:error/fn #(tr "workspace.tokens.invalid-font-family-token-value" %)}
|
||||
|
||||
:error.style-dictionary/invalid-token-value-typography
|
||||
{:error/code :error.style-dictionary/invalid-token-value-typography
|
||||
:error/fn #(tr "workspace.tokens.invalid-token-value-typography" %)}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
(log/set-level! :warn)
|
||||
|
||||
(def google-fonts
|
||||
(preload-gfonts "fonts/gfonts.2025.11.28.json"))
|
||||
(preload-gfonts "fonts/gfonts.2025.05.19.json"))
|
||||
|
||||
(def local-fonts
|
||||
[{:id "sourcesanspro"
|
||||
@@ -342,8 +342,8 @@
|
||||
(fn [result {:keys [font-id] :as node}]
|
||||
(let [current-font
|
||||
(if (some? font-id)
|
||||
(select-keys node [:font-id :font-variant-id :font-weight :font-style])
|
||||
(select-keys txt/default-typography [:font-id :font-variant-id :font-weight :font-style]))]
|
||||
(select-keys node [:font-id :font-variant-id])
|
||||
(select-keys txt/default-typography [:font-id :font-variant-id]))]
|
||||
(conj result current-font)))
|
||||
#{})))
|
||||
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
(def profile
|
||||
(l/derived (l/key :profile) st/state))
|
||||
|
||||
(def current-page-id
|
||||
(l/derived (l/key :current-page-id) st/state))
|
||||
|
||||
(def team
|
||||
(l/derived (fn [state]
|
||||
(let [team-id (:current-team-id state)
|
||||
@@ -375,9 +372,6 @@
|
||||
(def workspace-modifiers
|
||||
(l/derived :workspace-modifiers st/state))
|
||||
|
||||
(def workspace-wasm-modifiers
|
||||
(l/derived :workspace-wasm-modifiers st/state))
|
||||
|
||||
(def ^:private workspace-modifiers-with-objects
|
||||
(l/derived
|
||||
(fn [state]
|
||||
@@ -636,6 +630,3 @@
|
||||
|
||||
(def persistence-state
|
||||
(l/derived (comp :status :persistence) st/state))
|
||||
|
||||
(def restore
|
||||
(l/derived :restore st/state))
|
||||
|
||||
@@ -87,9 +87,6 @@
|
||||
{:stream? true
|
||||
:form-data? true}
|
||||
|
||||
::sse/restore-deleted-team-files
|
||||
{:stream? true}
|
||||
|
||||
:export-binfile {:response-type :blob}
|
||||
:retrieve-list-of-builtin-templates {:query-params :all}})
|
||||
|
||||
|
||||
@@ -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*)
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
current-id (get state :id)
|
||||
current-value (get state :current-value)
|
||||
current-label (get label-index current-value)
|
||||
|
||||
is-open? (get state :is-open?)
|
||||
|
||||
node-ref (mf/use-ref nil)
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
(def current-zoom (mf/create-context nil))
|
||||
|
||||
(def workspace-read-only? (mf/create-context nil))
|
||||
(def is-render? (mf/create-context false))
|
||||
(def is-component? (mf/create-context false))
|
||||
|
||||
(def sidebar
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[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]
|
||||
@@ -30,7 +29,6 @@
|
||||
[app.main.ui.dashboard.sidebar :refer [sidebar*]]
|
||||
[app.main.ui.dashboard.team :refer [team-settings-page* team-members-page* team-invitations-page* webhooks-page*]]
|
||||
[app.main.ui.dashboard.templates :refer [templates-section*]]
|
||||
[app.main.ui.exports.assets :refer [progress-widget]]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.modal :refer [modal-container*]]
|
||||
[app.main.ui.workspace.plugins]
|
||||
@@ -86,9 +84,6 @@
|
||||
[:div {:class (stl/css :dashboard-content)
|
||||
:on-click clear-selected-fn
|
||||
:ref container}
|
||||
|
||||
[:& progress-widget {:operation :restore}]
|
||||
|
||||
(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
|
||||
|
||||
@@ -1,331 +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.data.notifications :as ntf]
|
||||
[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 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 files team-id show on-close top left]}]
|
||||
(let [top (d/nilv top 0)
|
||||
left (d/nilv left 0)
|
||||
|
||||
file-ids
|
||||
(mf/with-memo [files]
|
||||
(into #{} d/xf:map-id files))
|
||||
|
||||
restore-fn
|
||||
(fn [_]
|
||||
(st/emit! (dd/restore-files-immediately
|
||||
(with-meta {:team-id team-id :ids file-ids}
|
||||
{:on-success #(st/emit! (ntf/success (tr "restore-modal.success-restore-immediately" (:name project)))
|
||||
(dd/fetch-projects team-id)
|
||||
(dd/fetch-deleted-files team-id))
|
||||
:on-error #(st/emit! (ntf/error (tr "restore-modal.error-restore-project" (:name project))))}))))
|
||||
|
||||
on-restore-project
|
||||
(fn []
|
||||
(st/emit!
|
||||
(modal/show {:type :confirm
|
||||
:title (tr "restore-modal.restore-project.title")
|
||||
:message (tr "restore-modal.restore-project.description" (:name project))
|
||||
:accept-style :primary
|
||||
:accept-label (tr "labels.continue")
|
||||
:on-accept restore-fn})))
|
||||
|
||||
delete-fn
|
||||
(fn [_]
|
||||
(st/emit! (ntf/success (tr "delete-forever-modal.success-delete-immediately" (:name project)))
|
||||
(dd/delete-files-immediately
|
||||
{:team-id team-id
|
||||
:ids file-ids})
|
||||
(dd/fetch-projects team-id)
|
||||
(dd/fetch-deleted-files team-id)))
|
||||
|
||||
on-delete-project
|
||||
(fn []
|
||||
(st/emit!
|
||||
(modal/show {:type :confirm
|
||||
:title (tr "delete-forever-modal.title")
|
||||
:message (tr "delete-forever-modal.delete-project.description" (:name project))
|
||||
:accept-label (tr "dashboard.deleted.delete-forever")
|
||||
:on-accept delete-fn})))
|
||||
options
|
||||
[{:name (tr "dashboard.deleted.restore-project")
|
||||
:id "project-restore"
|
||||
:handler on-restore-project}
|
||||
{:name (tr "dashboard.deleted.delete-project")
|
||||
: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/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [project team 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
|
||||
:files project-files
|
||||
:team-id (:id team)
|
||||
: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}])]]))
|
||||
|
||||
(def ^:private ref:deleted-files
|
||||
(l/derived :deleted-files st/state))
|
||||
|
||||
(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-clear
|
||||
(mf/use-fn
|
||||
(mf/deps team-id deleted-map)
|
||||
(fn []
|
||||
(when deleted-map
|
||||
(let [file-ids (into #{} (keys deleted-map))]
|
||||
(when (seq file-ids)
|
||||
(st/emit!
|
||||
(modal/show {:type :confirm
|
||||
:title (tr "delete-forever-modal.title")
|
||||
:message (tr "delete-forever-modal.delete-all.description" (count file-ids))
|
||||
:accept-label (tr "dashboard.deleted.delete-forever")
|
||||
:on-accept #(st/emit!
|
||||
(dd/delete-files-immediately
|
||||
{:team-id team-id
|
||||
:ids file-ids})
|
||||
(dd/fetch-projects team-id)
|
||||
(dd/fetch-deleted-files team-id))})))))))
|
||||
|
||||
restore-fn
|
||||
(fn [file-ids]
|
||||
(st/emit! (dd/restore-files-immediately
|
||||
(with-meta {:team-id team-id :ids file-ids}
|
||||
{:on-success #(st/emit! (dd/fetch-projects team-id)
|
||||
(dd/fetch-deleted-files team-id))
|
||||
:on-error #(st/emit! (ntf/error (tr "restore-modal.error-restore-files")))}))))
|
||||
|
||||
on-restore-all
|
||||
(mf/use-fn
|
||||
(mf/deps team-id deleted-map)
|
||||
(fn []
|
||||
(when deleted-map
|
||||
(let [file-ids (into #{} (keys deleted-map))]
|
||||
(when (seq file-ids)
|
||||
(st/emit!
|
||||
(modal/show {:type :confirm
|
||||
:title (tr "restore-modal.restore-all.title")
|
||||
:message (tr "restore-modal.restore-all.description" (count file-ids))
|
||||
:accept-label (tr "labels.continue")
|
||||
:accept-style :primary
|
||||
:on-accept #(restore-fn file-ids)})))))))
|
||||
|
||||
on-recent-click
|
||||
(mf/use-fn
|
||||
(mf/deps team-id)
|
||||
(fn []
|
||||
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id))))]
|
||||
|
||||
(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)}
|
||||
|
||||
[:div {:class (stl/css :nav-options)}
|
||||
[:> button* {:variant "ghost"
|
||||
:data-testid "recent-tab"
|
||||
:type "button"
|
||||
:on-click on-recent-click}
|
||||
(tr "dashboard.labels.recent")]
|
||||
[:div {:class (stl/css :selected)
|
||||
:data-testid "deleted-tab"}
|
||||
(tr "dashboard.labels.deleted")]]
|
||||
|
||||
[:div {:class (stl/css :deleted-content)}
|
||||
[:div {:class (stl/css :deleted-info)}
|
||||
[:div
|
||||
(tr "dashboard.deleted.info-text")
|
||||
[:span {:class (stl/css :info-text-highlight)}
|
||||
(tr "dashboard.deleted.info-days" deletion-days)]
|
||||
(tr "dashboard.deleted.info-text2")]
|
||||
[:div
|
||||
(tr "dashboard.deleted.restore-text")]]
|
||||
[:div {:class (stl/css :deleted-options)}
|
||||
[:> button* {:variant "ghost"
|
||||
:type "button"
|
||||
:on-click on-restore-all}
|
||||
(tr "dashboard.deleted.restore-all")]
|
||||
[:> button* {:variant "destructive"
|
||||
:type "button"
|
||||
:icon "delete"
|
||||
:on-click on-clear}
|
||||
(tr "dashboard.deleted.clear")]]]
|
||||
|
||||
(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
|
||||
:team team
|
||||
:files files
|
||||
:key id}])))]]]]))
|
||||
@@ -1,125 +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-content {
|
||||
display: flex;
|
||||
gap: var(--sp-l);
|
||||
justify-content: space-between;
|
||||
margin-inline-start: var(--sp-l);
|
||||
margin-block-start: var(--sp-xxl);
|
||||
}
|
||||
|
||||
.deleted-info {
|
||||
@include t.use-typography("body-medium");
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.info-text-highlight {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.deleted-options {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-options {
|
||||
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);
|
||||
}
|
||||
|
||||
.selected {
|
||||
@include t.use-typography("headline-small");
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-foreground-primary);
|
||||
border: $b-1 solid transparent;
|
||||
border-bottom: $b-1 solid var(--color-foreground-primary);
|
||||
padding: 0 var(--sp-m);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
@@ -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,46 +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 "restore-modal.success-restore-immediately" (:name file)))
|
||||
(dd/fetch-projects (:id current-team))
|
||||
(dd/fetch-deleted-files (:id current-team)))
|
||||
:on-error #(st/emit! (ntf/error (tr "restore-modal.error-restore-file" (:name file))))}))))
|
||||
|
||||
on-restore-immediately
|
||||
(fn []
|
||||
(st/emit!
|
||||
(modal/show {:type :confirm
|
||||
:title (tr "restore-modal.restore-file.title")
|
||||
:message (tr "restore-modal.restore-file.description" (:name file))
|
||||
:accept-label (tr "labels.continue")
|
||||
:accept-style :primary
|
||||
:on-accept restore-fn})))
|
||||
|
||||
|
||||
delete-fn
|
||||
(fn [_]
|
||||
(st/emit! (ntf/success (tr "delete-forever-modal.success-delete-immediately" (:name file)))
|
||||
(dd/delete-files-immediately
|
||||
{:team-id (:id current-team)
|
||||
:ids #{(:id file)}})
|
||||
(dd/fetch-projects (:id current-team))
|
||||
(dd/fetch-deleted-files (:id current-team))))
|
||||
|
||||
on-delete-immediately
|
||||
(fn []
|
||||
(st/emit!
|
||||
(modal/show {:type :confirm
|
||||
:title (tr "delete-forever-modal.title")
|
||||
:message (tr "delete-forever-modal.delete-file.description" (:name file))
|
||||
:accept-label (tr "delete-forever-modal.title")
|
||||
:on-accept delete-fn})))]
|
||||
(with-meta {::ev/origin "dashboard"}))))]
|
||||
|
||||
(mf/with-effect []
|
||||
(->> (rp/cmd! :get-all-projects)
|
||||
@@ -266,85 +227,76 @@
|
||||
(:id sub-project))})})}]))
|
||||
|
||||
options
|
||||
(if can-restore
|
||||
[(when can-restore
|
||||
{:name (tr "dashboard.restore-file")
|
||||
:id "restore-file"
|
||||
:handler on-restore-immediately})
|
||||
(when can-restore
|
||||
{:name (tr "dashboard.delete-file")
|
||||
: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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user