mirror of
https://github.com/penpot/penpot.git
synced 2026-01-06 05:18:52 -05:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02611029fb | ||
|
|
14e4e6d6ea | ||
|
|
9170c70f2a | ||
|
|
83d8bf37a6 | ||
|
|
1fb21d537c | ||
|
|
ac80e9a1ac | ||
|
|
dbbb8e76ab | ||
|
|
916f055aec | ||
|
|
6d8c183160 | ||
|
|
9d2f484aa3 | ||
|
|
2dc0cfdee3 | ||
|
|
a25abd0ca4 | ||
|
|
3a9119cf29 | ||
|
|
c236e0765b | ||
|
|
f8fad95fef | ||
|
|
97ae295cb9 | ||
|
|
bd888dcde2 | ||
|
|
784274f8ae | ||
|
|
eda6c6a4c3 | ||
|
|
7d7594818c | ||
|
|
7cc8f67e24 | ||
|
|
87fc3bbb8e | ||
|
|
bbb2cc972f | ||
|
|
6a07e6ae01 | ||
|
|
87dfd2b3c8 | ||
|
|
b0bfb8006d | ||
|
|
d46274abf2 | ||
|
|
23f7889cff | ||
|
|
534659cdc6 | ||
|
|
1e68d4ec87 | ||
|
|
1779fd3e8b | ||
|
|
3c496ddd9d | ||
|
|
47bc9d8ef1 | ||
|
|
a3a5fe056d | ||
|
|
fbb3271c81 | ||
|
|
ecc93d9246 | ||
|
|
302672f5b0 | ||
|
|
4f16ea2d2d | ||
|
|
b7a0b7d629 | ||
|
|
bd6f1bef10 | ||
|
|
c4941bb102 | ||
|
|
b8a606a35f | ||
|
|
370eebeb64 | ||
|
|
35bcb082a0 | ||
|
|
dd220e228e | ||
|
|
7b63aa4a4f | ||
|
|
33a07346dd | ||
|
|
abd77559ab | ||
|
|
28878caca9 | ||
|
|
74f3379b5d | ||
|
|
379770343a | ||
|
|
6327286328 | ||
|
|
3a2677a91a | ||
|
|
fcd232aa35 |
@@ -111,7 +111,7 @@ jobs:
|
||||
yarn run build:app:assets
|
||||
clojure -M:dev:shadow-cljs release main
|
||||
yarn playwright install --with-deps chromium
|
||||
yarn e2e:test
|
||||
yarn test:e2e
|
||||
|
||||
- run:
|
||||
name: "backend tests"
|
||||
|
||||
22
CHANGES.md
22
CHANGES.md
@@ -13,6 +13,7 @@
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- All our plugins beta testers :heart:.
|
||||
- Fix problem when translating multiple path points by @eeropic [#4459](https://github.com/penpot/penpot/issues/4459)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
@@ -27,6 +28,25 @@
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with constraints buttons [Taiga #8465](https://tree.taiga.io/project/penpot/issue/8465)
|
||||
- Fix problem with go back button on error page [Taiga #8887](https://tree.taiga.io/project/penpot/issue/8887)
|
||||
- Fix problem with shadows in text for Safari [Taiga #8770](https://tree.taiga.io/project/penpot/issue/8770)
|
||||
- Fix a regression with feedback form subject and content limits [Taiga #8908](https://tree.taiga.io/project/penpot/issue/8908)
|
||||
- Fix problem with stroke and filter ordering in frames [Github #5058](https://github.com/penpot/penpot/issues/5058)
|
||||
- Fix problem with hover layers when hidden/blocked [Github #5074](https://github.com/penpot/penpot/issues/5074)
|
||||
- Fix problem with precision on boolean calculation [Taiga #8482](https://tree.taiga.io/project/penpot/issue/8482)
|
||||
- Fix problem when translating multiple path points [Github #4459](https://github.com/penpot/penpot/issues/4459)
|
||||
- Fix problem on importing (and exporting) files with flows [Taiga #8914](https://tree.taiga.io/project/penpot/issue/8914)
|
||||
- Fix Internal Error page: "go to your penpot" wrong design [Taiga #8922](https://tree.taiga.io/project/penpot/issue/8922)
|
||||
- Fix problem updating layout when toggle visibility in component copy [Github #5143](https://github.com/penpot/penpot/issues/5143)
|
||||
- Fix "Done" button on toolbar on inspect mode should go to design mode [Taiga #8933](https://tree.taiga.io/project/penpot/issue/8933)
|
||||
- Fix problem with shortcuts in text editor [Github #5078](https://github.com/penpot/penpot/issues/5078)
|
||||
- Fix problems with show in viewer and interactions [Github #4868](https://github.com/penpot/penpot/issues/4868)
|
||||
- Add visual feedback when moving an element into a board [Github #3210](https://github.com/penpot/penpot/issues/3210)
|
||||
- Fix percent calculation on grid layout tracks [Github #4688](https://github.com/penpot/penpot/issues/4688)
|
||||
- Fix problem with caps and inner shadows [Github #4517](https://github.com/penpot/penpot/issues/4517)
|
||||
- Fix problem with horizontal/vertical lines and shadows [Github #4516](https://github.com/penpot/penpot/issues/4516)
|
||||
|
||||
## 2.2.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
@@ -171,7 +191,7 @@ time being.
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
### :heart: Communityq contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
|
||||
@@ -315,15 +315,13 @@
|
||||
(l/dbg :hint "sendmail"
|
||||
:id (:id params)
|
||||
:to (:to params)
|
||||
:subject (str/trim (:subject params))
|
||||
:body (str/join "," (map :type (:body params))))
|
||||
:subject (str/trim (:subject params)))
|
||||
|
||||
(.sendMessage ^Transport transport
|
||||
^MimeMessage message
|
||||
(.getAllRecipients message))))))
|
||||
|
||||
(when (or (contains? cf/flags :log-emails)
|
||||
(not (contains? cf/flags :smtp)))
|
||||
(when (contains? cf/flags :log-emails)
|
||||
(send-to-logger! cfg params))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
(l/dbg :hint "run webhook"
|
||||
:event-name (:name event)
|
||||
:webhook-id (:id whook)
|
||||
:webhook-id (str (:id whook))
|
||||
:webhook-uri (:uri whook)
|
||||
:webhook-mtype (:mtype whook))
|
||||
|
||||
|
||||
@@ -328,8 +328,10 @@
|
||||
:page-name page-name
|
||||
:position position
|
||||
:content content
|
||||
:frame-id frame-id}]
|
||||
(db/tx-run! cfg create-comment-thread params))))
|
||||
:frame-id frame-id}
|
||||
thread (db/tx-run! cfg create-comment-thread params)]
|
||||
|
||||
(vary-meta thread assoc ::audit/props thread))))
|
||||
|
||||
(defn- create-comment-thread
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
(def ^:private schema:send-user-feedback
|
||||
[:map {:title "send-user-feedback"}
|
||||
[:subject [:string {:max 250}]]
|
||||
[:content [:string {:max 250}]]])
|
||||
[:subject [:string {:max 400}]]
|
||||
[:content [:string {:max 2500}]]])
|
||||
|
||||
(sv/defmethod ::send-user-feedback
|
||||
{::doc/added "1.18"
|
||||
|
||||
@@ -45,37 +45,38 @@
|
||||
(sv/defmethod ::create-temp-file
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::sm/params schema:create-temp-file}
|
||||
[cfg {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team (teams/get-team conn :profile-id profile-id :project-id project-id)
|
||||
::sm/params schema:create-temp-file
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team (teams/get-team conn :profile-id profile-id :project-id project-id)
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
input-features
|
||||
(:features params #{})
|
||||
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
input-features (:features params #{})
|
||||
;; If the imported project doesn't contain v2 we need to remove it
|
||||
team-features
|
||||
(cond-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(not (contains? input-features "components/v2"))
|
||||
(disj "components/v2"))
|
||||
|
||||
;; If the imported project doesn't contain v2 we need to remove it
|
||||
team-features
|
||||
(cond-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(not (contains? input-features "components/v2"))
|
||||
(disj "components/v2"))
|
||||
;; We also include all no migration features declared by
|
||||
;; client; that enables the ability to enable a runtime
|
||||
;; feature on frontend and make it permanent on file
|
||||
features
|
||||
(-> input-features
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union team-features))
|
||||
|
||||
params
|
||||
(-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :deleted-at (dt/in-future {:days 1}))
|
||||
(assoc :features features))]
|
||||
|
||||
;; We also include all no migration features declared by
|
||||
;; client; that enables the ability to enable a runtime
|
||||
;; feature on frontend and make it permanent on file
|
||||
features (-> input-features
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union team-features))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :deleted-at (dt/in-future {:days 1}))
|
||||
(assoc :features features))]
|
||||
|
||||
(files.create/create-file cfg params)))))
|
||||
(files.create/create-file cfg params)))
|
||||
|
||||
;; --- MUTATION COMMAND: update-temp-file
|
||||
|
||||
|
||||
@@ -780,6 +780,7 @@
|
||||
|
||||
(def ^:private schema:create-invitation
|
||||
[:map {:title "params:create-invitation"}
|
||||
[::rpc/profile-id ::sm/uuid]
|
||||
[:team
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
@@ -936,7 +937,7 @@
|
||||
(map :email))
|
||||
|
||||
(defn- create-team-invitations
|
||||
[{:keys [::db/conn] :as cfg} profile team role emails]
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
|
||||
(let [join-requests (into #{} xf:map-email
|
||||
(get-valid-requests-email conn (:id team)))
|
||||
team-members (into #{} xf:map-email
|
||||
@@ -950,11 +951,7 @@
|
||||
;; We don't send invitations to
|
||||
;; join-requested members
|
||||
(remove join-requests)
|
||||
(map (fn [email]
|
||||
{:email email
|
||||
:team team
|
||||
:profile profile
|
||||
:role role}))
|
||||
(map (fn [email] (assoc params :email email)))
|
||||
(keep (partial create-invitation cfg)))
|
||||
emails)]
|
||||
|
||||
@@ -980,7 +977,7 @@
|
||||
join the team."
|
||||
{::doc/added "1.17"
|
||||
::sm/params schema:create-team-invitations}
|
||||
[cfg {:keys [::rpc/profile-id team-id emails role] :as params}]
|
||||
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
|
||||
(let [perms (get-permissions cfg profile-id team-id)
|
||||
profile (db/get-by-id cfg :profile profile-id)
|
||||
emails (into #{} (map profile/clean-email) emails)]
|
||||
@@ -1006,7 +1003,16 @@
|
||||
(check-profile-muted cfg profile)
|
||||
|
||||
(let [team (db/get-by-id cfg :team team-id)
|
||||
invitations (db/tx-run! cfg create-team-invitations profile team role emails)]
|
||||
;; NOTE: Is important pass RPC method params down to the
|
||||
;; `create-team-invitations` because it uses the implicit
|
||||
;; RPC properties from params for fill necessary data on
|
||||
;; emiting an entry to the audit-log
|
||||
invitations (db/tx-run! cfg create-team-invitations
|
||||
(-> params
|
||||
(assoc :profile profile)
|
||||
(assoc :team team)
|
||||
(assoc :emails emails)))]
|
||||
|
||||
(with-meta {:total (count invitations)
|
||||
:invitations invitations}
|
||||
{::audit/props {:invitations (count invitations)}}))))
|
||||
@@ -1057,17 +1063,16 @@
|
||||
(audit/submit! cfg event))
|
||||
|
||||
;; Create invitations for all provided emails.
|
||||
(let [profile (db/get-by-id conn :profile profile-id)]
|
||||
(->> emails
|
||||
(map (fn [email]
|
||||
(-> params
|
||||
(assoc :team team)
|
||||
(assoc :profile profile)
|
||||
(assoc :email email)
|
||||
(assoc :role role))))
|
||||
(run! (partial create-invitation cfg))))
|
||||
(let [profile (db/get-by-id conn :profile profile-id)
|
||||
params (-> params
|
||||
(assoc :team team)
|
||||
(assoc :profile profile)
|
||||
(assoc :role role))
|
||||
invitations (->> emails
|
||||
(map (fn [email] (assoc params :email email)))
|
||||
(map (partial create-invitation cfg)))]
|
||||
|
||||
(vary-meta team assoc ::audit/props {:invitations (count emails)})))
|
||||
(vary-meta team assoc ::audit/props {:invitations (count invitations)}))))
|
||||
|
||||
;; --- Query: get-team-invitation-token
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]))
|
||||
|
||||
(defn shape-stroke-margin
|
||||
@@ -60,6 +61,7 @@
|
||||
filter-y (mth/min y (+ y offset-y (- spread) (- blur) -5))
|
||||
filter-w (+ w (mth/abs offset-x) (* spread 2) (* blur 2) 10)
|
||||
filter-h (+ h (mth/abs offset-y) (* spread 2) (* blur 2) 10)]
|
||||
|
||||
(grc/make-rect filter-x filter-y filter-w filter-h)))
|
||||
|
||||
(defn get-rect-filter-bounds
|
||||
@@ -96,12 +98,15 @@
|
||||
([shape ignore-margin?]
|
||||
(let [strokes (:strokes shape)
|
||||
|
||||
open-path? (and ^boolean (cfh/path-shape? shape)
|
||||
^boolean (gsh/open-path? shape))
|
||||
|
||||
stroke-width
|
||||
(->> strokes
|
||||
(map #(case (get % :stroke-alignment :center)
|
||||
:center (/ (:stroke-width % 0) 2)
|
||||
:outer (:stroke-width % 0)
|
||||
0))
|
||||
(if open-path? (:stroke-width % 0) 0)))
|
||||
(reduce d/max 0))
|
||||
|
||||
stroke-margin
|
||||
|
||||
@@ -852,8 +852,10 @@
|
||||
|
||||
(defn ray-overlaps?
|
||||
[ray-point {selrect :selrect}]
|
||||
(and (>= (:y ray-point) (:y1 selrect))
|
||||
(<= (:y ray-point) (:y2 selrect))))
|
||||
(and (or (> (:y ray-point) (:y1 selrect))
|
||||
(mth/almost-zero? (- (:y ray-point) (:y1 selrect))))
|
||||
(or (< (:y ray-point) (:y2 selrect))
|
||||
(mth/almost-zero? (- (:y ray-point) (:y2 selrect))))))
|
||||
|
||||
(defn content->geom-data
|
||||
[content]
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
;; All parents of any deleted shape must be resized.
|
||||
(into res (cfh/get-parent-ids objects id)))
|
||||
(d/ordered-set)
|
||||
ids-to-delete)
|
||||
(concat ids-to-delete ids-to-hide))
|
||||
|
||||
all-children
|
||||
(->> ids-to-delete ;; Children of deleted shapes must be also deleted.
|
||||
@@ -408,17 +408,12 @@
|
||||
;; Resize parent containers that need to
|
||||
(pcb/resize-parents parents))))
|
||||
|
||||
|
||||
|
||||
|
||||
(defn change-show-in-viewer [shape hide?]
|
||||
(cond-> (assoc shape :hide-in-viewer hide?)
|
||||
;; When a frame is no longer shown in view mode, it cannot have interactions
|
||||
hide?
|
||||
(dissoc :interactions)))
|
||||
(assoc shape :hide-in-viewer hide?))
|
||||
|
||||
(defn add-new-interaction [shape interaction]
|
||||
(-> shape
|
||||
(update :interactions ctsi/add-interaction interaction)
|
||||
;; When a interaction is created, the frame must be shown in view mode
|
||||
(dissoc :hide-in-viewer)))
|
||||
(update :interactions ctsi/add-interaction interaction)))
|
||||
|
||||
(defn show-in-viewer [shape]
|
||||
(dissoc shape :hide-in-viewer))
|
||||
|
||||
@@ -1,75 +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 common-tests.logic.hide-in-viewer-test
|
||||
(:require
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
|
||||
(t/deftest test-remove-show-in-view-mode-delete-interactions
|
||||
(let [;; ==== Setup
|
||||
|
||||
file (-> (thf/sample-file :file1)
|
||||
(tho/add-frame :frame-dest)
|
||||
(tho/add-frame :frame-origin)
|
||||
(ths/add-interaction :frame-origin :frame-dest))
|
||||
|
||||
frame-origin (ths/get-shape file :frame-origin)
|
||||
|
||||
page (thf/current-page file)
|
||||
|
||||
|
||||
;; ==== Action
|
||||
changes (-> (pcb/empty-changes nil (:id page))
|
||||
(pcb/with-objects (:objects page))
|
||||
(pcb/update-shapes [(:id frame-origin)] #(cls/change-show-in-viewer % true)))
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
;; ==== Get
|
||||
frame-origin' (ths/get-shape file' :frame-origin)]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (some? (:interactions frame-origin)))
|
||||
(t/is (nil? (:interactions frame-origin')))))
|
||||
|
||||
|
||||
|
||||
(t/deftest test-add-new-interaction-updates-show-in-view-mode
|
||||
(let [;; ==== Setup
|
||||
|
||||
file (-> (thf/sample-file :file1)
|
||||
(tho/add-frame :frame-dest :hide-in-viewer true)
|
||||
(tho/add-frame :frame-origin :hide-in-viewer true))
|
||||
frame-dest (ths/get-shape file :frame-dest)
|
||||
frame-origin (ths/get-shape file :frame-origin)
|
||||
|
||||
page (thf/current-page file)
|
||||
|
||||
;; ==== Action
|
||||
new-interaction (-> ctsi/default-interaction
|
||||
(ctsi/set-destination (:id frame-dest))
|
||||
(assoc :position-relative-to (:id frame-dest)))
|
||||
|
||||
changes (-> (pcb/empty-changes nil (:id page))
|
||||
(pcb/with-objects (:objects page))
|
||||
(pcb/update-shapes [(:id frame-origin)] #(cls/add-new-interaction % new-interaction)))
|
||||
file' (thf/apply-changes file changes)
|
||||
|
||||
;; ==== Get
|
||||
frame-origin' (ths/get-shape file' :frame-origin)]
|
||||
|
||||
;; ==== Check
|
||||
(t/is (true? (:hide-in-viewer frame-origin)))
|
||||
(t/is (nil? (:hide-in-viewer frame-origin')))))
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PATH=/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
|
||||
export JAVA_OPTS="-Xmx1000m -Xms50m"
|
||||
export JAVA_OPTS=${JAVA_OPTS:-"-Xmx1000m -Xms200m"};
|
||||
|
||||
alias l='ls --color -GFlh'
|
||||
alias rm='rm -r'
|
||||
|
||||
@@ -10,7 +10,7 @@ rm -rf target
|
||||
export NODE_ENV=production;
|
||||
|
||||
# Build the application
|
||||
clojure -J-Xms100M -J-Xmx1000M -J-XX:+UseSerialGC -M:dev:shadow-cljs release main;
|
||||
clojure -M:dev:shadow-cljs release main;
|
||||
|
||||
# Remove source
|
||||
rm -rf target/app;
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
:output-wrapper false}
|
||||
|
||||
:release
|
||||
{:closure-defines {goog.debug.LOGGING_ENABLED true}
|
||||
:compiler-options
|
||||
{:compiler-options
|
||||
{:fn-invoke-direct true
|
||||
:source-map true
|
||||
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :simple]
|
||||
|
||||
@@ -1 +1 @@
|
||||
v14.15.0
|
||||
v20.11.1
|
||||
|
||||
@@ -20,10 +20,9 @@
|
||||
"build:app:assets": "node ./scripts/build-app-assets.js",
|
||||
"build:storybook": "yarn run build:storybook:assets && yarn run build:storybook:cljs && storybook build",
|
||||
"build:storybook:assets": "node ./scripts/build-storybook-assets.js",
|
||||
"build:storybook:cljs": "clojure -M:dev:shadow-cljs release storybook",
|
||||
"build:storybook:cljs": "clojure -M:dev:shadow-cljs compile storybook",
|
||||
"build:renderer": "yarn run wasm-pack build ./renderer --target web --out-dir ../resources/public/js/renderer --release",
|
||||
"e2e:server": "node ./scripts/e2e-server.js",
|
||||
"e2e:test": "playwright test --project default",
|
||||
"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 -w",
|
||||
@@ -35,6 +34,7 @@
|
||||
"test:compile": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'",
|
||||
"test:run": "node target/tests.cjs",
|
||||
"test:watch": "clojure -M:dev:shadow-cljs watch test",
|
||||
"test:e2e": "playwright test --project default",
|
||||
"translations": "node ./scripts/translations.js",
|
||||
"watch": "yarn run watch:app:assets",
|
||||
"watch:app:assets": "node ./scripts/watch.js",
|
||||
|
||||
@@ -56,7 +56,7 @@ export default defineConfig({
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
timeout: 2 * 60 * 1000,
|
||||
command: "yarn e2e:server",
|
||||
command: "yarn run e2e:server",
|
||||
url: "http://localhost:3000",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
|
||||
@@ -168,7 +168,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
async moveSelectionToShape(name) {
|
||||
await this.page.locator("rect.viewport-selrect").hover();
|
||||
await this.page.mouse.down();
|
||||
await this.viewport.getByTestId(name).first().hover({ force: true });
|
||||
await this.viewport.getByText(name).first().hover({ force: true });
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
|
||||
BIN
frontend/resources/images/features/2.3-img-slide-1.gif
Normal file
BIN
frontend/resources/images/features/2.3-img-slide-1.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
frontend/resources/images/features/2.3-img-slide-2.gif
Normal file
BIN
frontend/resources/images/features/2.3-img-slide-2.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 711 KiB |
BIN
frontend/resources/images/features/2.3-slide-0.png
Normal file
BIN
frontend/resources/images/features/2.3-slide-0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@
|
||||
<script defer src="{{& polyfills}}"></script>
|
||||
{{/manifest}}
|
||||
|
||||
<script type="module" src="{{pluginRuntimeUri}}/index.js"></script>
|
||||
<script type="module" src="{{& pluginRuntimeUri}}"></script>
|
||||
|
||||
<script>
|
||||
window.penpotTranslations = JSON.parse({{& translations}});
|
||||
|
||||
@@ -181,14 +181,16 @@ export async function watch(baseDir, predicate, callback) {
|
||||
}
|
||||
|
||||
async function readShadowManifest() {
|
||||
const ts = Date.now();
|
||||
try {
|
||||
const manifestPath = "resources/public/js/manifest.json";
|
||||
let content = await fs.readFile(manifestPath, { encoding: "utf8" });
|
||||
content = JSON.parse(content);
|
||||
|
||||
const index = {
|
||||
config: "js/config.js?ts=" + Date.now(),
|
||||
polyfills: "js/polyfills.js?ts=" + Date.now(),
|
||||
ts: ts,
|
||||
config: "js/config.js?ts=" + ts,
|
||||
polyfills: "js/polyfills.js?ts=" + ts,
|
||||
};
|
||||
|
||||
for (let item of content) {
|
||||
@@ -198,12 +200,13 @@ async function readShadowManifest() {
|
||||
return index;
|
||||
} catch (cause) {
|
||||
return {
|
||||
config: "js/config.js",
|
||||
polyfills: "js/polyfills.js",
|
||||
main: "js/main.js",
|
||||
shared: "js/shared.js",
|
||||
worker: "js/worker.js",
|
||||
rasterizer: "js/rasterizer.js",
|
||||
ts: ts,
|
||||
config: "js/config.js?ts=" + ts,
|
||||
polyfills: "js/polyfills.js?ts=" + ts,
|
||||
main: "js/main.js?ts=" + ts,
|
||||
shared: "js/shared.js?ts=" + ts,
|
||||
worker: "js/worker.js?ts=" + ts,
|
||||
rasterizer: "js/rasterizer.js?ts=" + ts,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -409,8 +412,8 @@ async function generateTemplates() {
|
||||
|
||||
const pluginRuntimeUri =
|
||||
process.env.PENPOT_PLUGIN_DEV === "true"
|
||||
? "http://localhost:4200"
|
||||
: "./plugins-runtime";
|
||||
? "http://localhost:4200/index.js?ts=" + manifest.ts
|
||||
: "plugins-runtime/index.js?ts=" + manifest.ts;
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/index.mustache",
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
(def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot-docs-plugins.pages.dev/plugins/getting-started/#examples"))
|
||||
(def plugins-whitelist (into #{} (obj/get global "penpotPluginsWhitelist" [])))
|
||||
|
||||
(defn- normalize-uri
|
||||
[uri-str]
|
||||
|
||||
@@ -6,14 +6,42 @@
|
||||
|
||||
(ns app.main.data.plugins
|
||||
(:require
|
||||
[app.plugins.register :as pr]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.register :as preg]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.http :as http]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn open-plugin!
|
||||
(defn fetch-manifest
|
||||
[plugin-url]
|
||||
(->> (http/send! {:method :get
|
||||
:uri plugin-url
|
||||
:omit-default-headers true
|
||||
:response-type :json})
|
||||
(rx/map :body)
|
||||
(rx/map #(preg/parse-manifest plugin-url %))))
|
||||
|
||||
(defn save-current-plugin
|
||||
[id]
|
||||
(ptk/reify ::save-current-plugin
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :open-plugins] (fnil conj #{}) id))))
|
||||
|
||||
(defn remove-current-plugin
|
||||
[id]
|
||||
(ptk/reify ::remove-current-plugin
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
|
||||
|
||||
(defn- load-plugin!
|
||||
[{:keys [plugin-id name description host code icon permissions]}]
|
||||
(try
|
||||
(st/emit! (save-current-plugin plugin-id))
|
||||
(.ɵloadPlugin
|
||||
^js ug/global
|
||||
#js {:pluginId plugin-id
|
||||
@@ -22,10 +50,60 @@
|
||||
:host host
|
||||
:code code
|
||||
:icon icon
|
||||
:permissions (apply array permissions)})
|
||||
:permissions (apply array permissions)}
|
||||
(fn []
|
||||
(st/emit! (remove-current-plugin plugin-id))))
|
||||
|
||||
(catch :default e
|
||||
(st/emit! (remove-current-plugin plugin-id))
|
||||
(.error js/console "Error" e))))
|
||||
|
||||
(defn open-plugin!
|
||||
[{:keys [url] :as manifest}]
|
||||
(if url
|
||||
;; If the saved manifest has a URL we fetch the manifest to check
|
||||
;; for updates
|
||||
(->> (fetch-manifest url)
|
||||
(rx/subs!
|
||||
(fn [new-manifest]
|
||||
(let [new-manifest (merge new-manifest (select-keys manifest [:plugin-id]))]
|
||||
(cond
|
||||
(not= (:permissions new-manifest) (:permissions manifest))
|
||||
(modal/show!
|
||||
:plugin-permissions-update
|
||||
{:plugin new-manifest
|
||||
:on-accept
|
||||
#(do
|
||||
(preg/install-plugin! new-manifest)
|
||||
(load-plugin! new-manifest))})
|
||||
|
||||
(not= new-manifest manifest)
|
||||
(do (preg/install-plugin! new-manifest)
|
||||
(load-plugin! manifest))
|
||||
:else
|
||||
(load-plugin! manifest))))
|
||||
(fn []
|
||||
;; Error fetching the manifest we'll load the plugin with the
|
||||
;; old manifest
|
||||
(load-plugin! manifest))))
|
||||
(load-plugin! manifest)))
|
||||
|
||||
(defn close-plugin!
|
||||
[{:keys [plugin-id]}]
|
||||
(try
|
||||
(.ɵunloadPlugin ^js ug/global plugin-id)
|
||||
(catch :default e
|
||||
(.error js/console "Error" e))))
|
||||
|
||||
(defn close-current-plugin
|
||||
[]
|
||||
(ptk/reify ::close-current-plugin
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [ids (dm/get-in state [:workspace-local :open-plugins])]
|
||||
(doseq [id ids]
|
||||
(close-plugin! (preg/get-plugin id)))))))
|
||||
|
||||
(defn delay-open-plugin
|
||||
[plugin]
|
||||
(ptk/reify ::delay-open-plugin
|
||||
@@ -39,5 +117,5 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when-let [pid (::open-plugin state)]
|
||||
(open-plugin! (pr/get-plugin pid))
|
||||
(open-plugin! (preg/get-plugin pid))
|
||||
(rx/of #(dissoc % ::open-plugin))))))
|
||||
|
||||
@@ -24,6 +24,10 @@ target.stopCallback = function (e, element, combo) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('composedPath' in e && typeof e.composedPath === 'function') {
|
||||
// For open shadow trees, update `element` so that the following check works.
|
||||
const initialEventTarget = e.composedPath()[0];
|
||||
|
||||
@@ -147,11 +147,15 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (or page-id (:current-page-id state))]
|
||||
(rx/of (dwsh/update-shapes
|
||||
[shape-id]
|
||||
(fn [shape]
|
||||
(cls/add-new-interaction shape interaction))
|
||||
{:page-id page-id}))))))
|
||||
(rx/of (dwsh/update-shapes [shape-id]
|
||||
(fn [shape]
|
||||
(cls/add-new-interaction shape interaction))
|
||||
{:page-id page-id})
|
||||
|
||||
(when (:destination interaction)
|
||||
(dwsh/update-shapes [(:destination interaction)]
|
||||
cls/show-in-viewer
|
||||
{:page-id page-id})))))))
|
||||
|
||||
(defn add-new-interaction
|
||||
([shape] (add-new-interaction shape nil))
|
||||
@@ -167,15 +171,20 @@
|
||||
flows (get page :objects)
|
||||
flow (ctp/get-frame-flow flows (:id frame))]
|
||||
(rx/concat
|
||||
(rx/of (dwsh/update-shapes [(:id shape)]
|
||||
(fn [shape]
|
||||
(let [new-interaction (-> ctsi/default-interaction
|
||||
(ctsi/set-destination destination)
|
||||
(assoc :position-relative-to (:id shape)))]
|
||||
(cls/add-new-interaction shape new-interaction)))))
|
||||
(when (and (not (connected-frame? objects (:id frame)))
|
||||
(nil? flow))
|
||||
(rx/of (add-flow (:id frame))))))))))
|
||||
(rx/of (dwsh/update-shapes
|
||||
[(:id shape)]
|
||||
(fn [shape]
|
||||
(let [new-interaction (-> ctsi/default-interaction
|
||||
(ctsi/set-destination destination)
|
||||
(assoc :position-relative-to (:id shape)))]
|
||||
(cls/add-new-interaction shape new-interaction))))
|
||||
|
||||
(when destination
|
||||
(dwsh/update-shapes [destination] cls/show-in-viewer))
|
||||
|
||||
(when (and (not (connected-frame? objects (:id frame)))
|
||||
(nil? flow))
|
||||
(add-flow (:id frame))))))))))
|
||||
|
||||
(defn remove-interaction
|
||||
([shape index]
|
||||
@@ -186,8 +195,7 @@
|
||||
(watch [_ _ _]
|
||||
(rx/of (dwsh/update-shapes [(:id shape)]
|
||||
(fn [shape]
|
||||
(update shape :interactions
|
||||
ctsi/remove-interaction index))
|
||||
(update shape :interactions ctsi/remove-interaction index))
|
||||
{:page-id page-id}))))))
|
||||
(defn update-interaction
|
||||
([shape index update-fn]
|
||||
@@ -196,11 +204,16 @@
|
||||
(ptk/reify ::update-interaction
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (dwsh/update-shapes [(:id shape)]
|
||||
(fn [shape]
|
||||
(update shape :interactions
|
||||
ctsi/update-interaction index update-fn))
|
||||
options))))))
|
||||
(let [interactions (ctsi/update-interaction (:interactions shape) index update-fn)
|
||||
interaction (nth interactions index)]
|
||||
(rx/of (dwsh/update-shapes
|
||||
[(:id shape)]
|
||||
(fn [shape]
|
||||
(assoc shape :interactions interactions))
|
||||
options)
|
||||
|
||||
(when (some? (:destination interaction))
|
||||
(dwsh/update-shapes [(:destination interaction)] cls/show-in-viewer options))))))))
|
||||
|
||||
(defn remove-all-interactions-nav-to
|
||||
"Remove all interactions that navigate to the given frame."
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
|
||||
selected-points (dm/get-in state [:workspace-local :edit-path id :selected-points] #{})
|
||||
|
||||
start-position (apply min #(gpt/distance start-position %) selected-points)
|
||||
start-position (apply min-key #(gpt/distance start-position %) selected-points)
|
||||
|
||||
content (st/get-path state :content)
|
||||
points (upg/content->points content)]
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
|
||||
(ns app.main.data.workspace.shortcuts
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.exports :as de]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.plugins :as dpl]
|
||||
[app.main.data.preview :as dp]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.users :as du]
|
||||
@@ -28,6 +30,7 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks.resize :as r]
|
||||
[app.util.dom :as dom]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -44,6 +47,17 @@
|
||||
(when-not (deref refs/workspace-read-only?)
|
||||
(run! st/emit! events)))
|
||||
|
||||
(def esc-pressed
|
||||
(ptk/reify ::esc-pressed
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(rx/of
|
||||
:interrupt
|
||||
(let [selection (dm/get-in state [:workspace-local :selected])]
|
||||
(if (empty? selection)
|
||||
(dpl/close-current-plugin)
|
||||
(dw/deselect-all true)))))))
|
||||
|
||||
;; Shortcuts format https://github.com/ccampbell/mousetrap
|
||||
|
||||
(def base-shortcuts
|
||||
@@ -111,7 +125,7 @@
|
||||
:escape {:tooltip (ds/esc)
|
||||
:command "escape"
|
||||
:subsections [:edit]
|
||||
:fn #(st/emit! :interrupt (dw/deselect-all true))}
|
||||
:fn #(st/emit! esc-pressed)}
|
||||
|
||||
|
||||
;; MODIFY LAYERS
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
:dashboard-team-webhooks
|
||||
:dashboard-team-settings)
|
||||
[:?
|
||||
#_[:& app.main.ui.releases/release-notes-modal {:version "1.19"}]
|
||||
#_[:& app.main.ui.releases/release-notes-modal {:version "2.3"}]
|
||||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||
#_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal]
|
||||
|
||||
@@ -483,7 +483,8 @@
|
||||
|
||||
;; Empty values means "submit" the form (whent some items have been added
|
||||
(when (and (kbd/enter? event) (str/empty? @value) (not-empty @items))
|
||||
(on-submit form))
|
||||
(when (fn? on-submit)
|
||||
(on-submit form event)))
|
||||
|
||||
;; If we have a string in the input we add it only if valid
|
||||
(when (and (valid-item-fn val) (not (str/empty? @value)))
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
[app.main.ui.workspace.plugins]
|
||||
[app.plugins.register :as preg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
@@ -202,19 +201,14 @@
|
||||
(mf/with-layout-effect
|
||||
[plugin-url team-id project-id]
|
||||
(when plugin-url
|
||||
(->> (http/send! {:method :get
|
||||
:uri plugin-url
|
||||
:omit-default-headers true
|
||||
:response-type :json})
|
||||
(rx/map :body)
|
||||
(->> (dp/fetch-manifest plugin-url)
|
||||
(rx/subs!
|
||||
(fn [body]
|
||||
(if-let [plugin (preg/parse-manifest plugin-url body)]
|
||||
(fn [plugin]
|
||||
(if plugin
|
||||
(do
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "install-plugin" :name (:name plugin) :url plugin-url}))
|
||||
(open-permissions-dialog plugin))
|
||||
(st/emit! (notif/error "Cannot parser the plugin manifest"))))
|
||||
|
||||
(fn [_]
|
||||
(st/emit! (notif/error "The plugin URL is incorrect")))))))))
|
||||
|
||||
|
||||
@@ -195,19 +195,17 @@
|
||||
(fn [_event]
|
||||
(swap! collapsed* not)))
|
||||
|
||||
update-can-move
|
||||
(fn [scroll-left scroll-available client-width]
|
||||
(reset! can-move {:left (> scroll-left 0)
|
||||
:right (> scroll-available client-width)}))
|
||||
|
||||
on-scroll
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(let [scroll (dom/get-target-scroll e)
|
||||
scroll-left (:scroll-left scroll)
|
||||
(let [scroll (dom/get-target-scroll e)
|
||||
scroll-left (:scroll-left scroll)
|
||||
scroll-available (- (:scroll-width scroll) scroll-left)
|
||||
client-rect (dom/get-client-size (dom/get-target e))]
|
||||
(update-can-move scroll-left scroll-available (unchecked-get client-rect "width")))))
|
||||
client-rect (dom/get-client-size (dom/get-target e))
|
||||
client-width (unchecked-get client-rect "width")]
|
||||
|
||||
(reset! can-move {:left (> scroll-left 0)
|
||||
:right (> scroll-available client-width)}))))
|
||||
|
||||
on-move-left
|
||||
(mf/use-fn #(move-left))
|
||||
@@ -231,7 +229,7 @@
|
||||
(let [content (mf/ref-val content-ref)]
|
||||
(when (and (some? content) (some? templates))
|
||||
(dom/scroll-to content #js {:behavior "instant" :left 0 :top 0})
|
||||
(.dispatchEvent content (js/Event. "scroll")))))
|
||||
(dom/dispatch-event content (dom/event "scroll")))))
|
||||
|
||||
(mf/with-effect [profile collapsed]
|
||||
(swap! storage/global assoc ::collapsed collapsed)
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
[app.main.ui.releases.v2-0]
|
||||
[app.main.ui.releases.v2-1]
|
||||
[app.main.ui.releases.v2-2]
|
||||
[app.main.ui.releases.v2-3]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as tm]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
115
frontend/src/app/main/ui/releases/v2_3.cljs
Normal file
115
frontend/src/app/main/ui/releases/v2_3.cljs
Normal file
@@ -0,0 +1,115 @@
|
||||
;; 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.releases.v2-3
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.releases.common :as c]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; TODO: Review all copies and alt text
|
||||
(defmethod c/render-release-notes "2.3"
|
||||
[{:keys [slide klass next finish navigate version]}]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.3-slide-0.png"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "A graphic illustration with Penpot style"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"What’s new in Penpot?"]
|
||||
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"Penpot can now be extended by using Plugins!"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"The introduction of our brand new Plugin system allows you to access even richer ecosystem of capabilities."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"We are beyond excitement about how this will further involve the Penpot community in building the best design and prototyping platform."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Let’s dive in!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click next} "Continue"]]]]]]
|
||||
|
||||
0
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.3-img-slide-1.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Build Plugins to enhance your workflow"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Build Plugins and enhance your workflow"]]
|
||||
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Penpot Plugins encourage developers to easily customize and expand the platform using standard web technologies like JavaScript, CSS, and HTML."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Find everything you need in ouor full comprehensive documentation to start building your plugins now!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 2}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
|
||||
1
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.3-img-slide-2.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Plugins are safe and extremely easy to use"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Plugins are safe and extremely easy to use"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Penpot plugins are quite easy to install."]
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Be sure to keep an eye on our evolving " [:a {:href "https://penpot.app/penpothub" :target "_blank"} "Penpot Hub"] " to pick the ones that are best suited to enhance your workflow."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"This is just the beginning of a myriad of possibilities. Let’s build this community together <3."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 2}]
|
||||
|
||||
[:button {:on-click finish
|
||||
:class (stl/css :next-btn)} "Let's go"]]]]]])))
|
||||
|
||||
102
frontend/src/app/main/ui/releases/v2_3.scss
Normal file
102
frontend/src/app/main/ui/releases/v2_3.scss
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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
|
||||
|
||||
@import "refactor/common-refactor.scss";
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: grid;
|
||||
grid-template-columns: $s-324 1fr;
|
||||
height: $s-500;
|
||||
width: $s-888;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--modal-background-color);
|
||||
border: $s-2 solid var(--modal-border-color);
|
||||
}
|
||||
|
||||
.start-image {
|
||||
width: $s-324;
|
||||
border-radius: $br-8 0 0 $br-8;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: $s-40;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr $s-32;
|
||||
gap: $s-24;
|
||||
|
||||
a {
|
||||
color: var(--button-primary-background-color-rest);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: grid;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
@include flexCenter;
|
||||
@include headlineSmallTypography;
|
||||
height: $s-32;
|
||||
width: $s-96;
|
||||
background-color: var(--communication-tag-background-color);
|
||||
color: var(--communication-tag-foreground-color);
|
||||
border-radius: $br-8;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include headlineLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.features-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-16;
|
||||
width: $s-440;
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
@include bodyLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
@include bodyMediumTypography;
|
||||
margin: 0;
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
@include bodyMediumTypography;
|
||||
color: var(--modal-text-foreground-color);
|
||||
list-style: disc;
|
||||
display: grid;
|
||||
gap: $s-8;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-areas: "bullets button";
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
@extend .button-primary;
|
||||
width: $s-100;
|
||||
justify-self: flex-end;
|
||||
grid-area: button;
|
||||
}
|
||||
@@ -173,7 +173,7 @@
|
||||
(mf/defc export-flows
|
||||
[{:keys [flows]}]
|
||||
[:> "penpot:flows" #js {}
|
||||
(for [{:keys [id name starting-frame]} flows]
|
||||
(for [{:keys [id name starting-frame]} (vals flows)]
|
||||
[:> "penpot:flow" #js {:id id
|
||||
:name name
|
||||
:starting-frame starting-frame}])])
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.shapes.bounds :as gsb]
|
||||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -129,6 +130,34 @@
|
||||
[filters]
|
||||
(map #(assoc %1 :filter-in %2) filters (cons nil (map :id filters))))
|
||||
|
||||
(defn filter-coords
|
||||
[bounds selrect padding]
|
||||
(if (or (mth/close? 0.01 (:width selrect))
|
||||
(mth/close? 0.01 (:height selrect)))
|
||||
|
||||
;; We cannot use "objectBoundingbox" if the shape doesn't have width/heigth
|
||||
;; From the SVG spec (https://www.w3.org/TR/SVG11/coords.html#ObjectBoundingBox
|
||||
;; Keyword objectBoundingBox should not be used when the geometry of the applicable element
|
||||
;; has no width or no height, such as the case of a horizontal or vertical line, even when
|
||||
;; the line has actual thickness when viewed due to having a non-zero stroke width since
|
||||
;; stroke width is ignored for bounding box calculations. When the geometry of the
|
||||
;; applicable element has no width or height and objectBoundingBox is specified, then
|
||||
;; the given effect (e.g., a gradient or a filter) will be ignored.
|
||||
(let [filter-width (+ (:width bounds) (* 2 (:horizontal padding)))
|
||||
filter-height (+ (:height bounds) (* 2 (:vertical padding)))
|
||||
filter-x (- (:x bounds) #_(:x selrect) (:horizontal padding))
|
||||
filter-y (- (:y bounds) #_(:y selrect) (:vertical padding))
|
||||
filter-units "userSpaceOnUse"]
|
||||
[filter-x filter-y filter-width filter-height filter-units])
|
||||
|
||||
;; If the width/height is not zero we use objectBoundingBox as it's more stable
|
||||
(let [filter-width (/ (+ (:width bounds) (* 2 (:horizontal padding))) (:width selrect))
|
||||
filter-height (/ (+ (:height bounds) (* 2 (:vertical padding))) (:height selrect))
|
||||
filter-x (/ (- (:x bounds) (:x selrect) (:horizontal padding)) (:width selrect))
|
||||
filter-y (/ (- (:y bounds) (:y selrect) (:vertical padding)) (:height selrect))
|
||||
filter-units "objectBoundingBox"]
|
||||
[filter-x filter-y filter-width filter-height filter-units])))
|
||||
|
||||
(mf/defc filters
|
||||
[{:keys [filter-id shape]}]
|
||||
|
||||
@@ -136,17 +165,17 @@
|
||||
bounds (gsb/get-rect-filter-bounds (:selrect shape) filters (or (-> shape :blur :value) 0))
|
||||
padding (gsb/calculate-padding shape)
|
||||
selrect (:selrect shape)
|
||||
filter-x (/ (- (:x bounds) (:x selrect) (:horizontal padding)) (:width selrect))
|
||||
filter-y (/ (- (:y bounds) (:y selrect) (:vertical padding)) (:height selrect))
|
||||
filter-width (/ (+ (:width bounds) (* 2 (:horizontal padding))) (:width selrect))
|
||||
filter-height (/ (+ (:height bounds) (* 2 (:vertical padding))) (:height selrect))]
|
||||
|
||||
[filter-x filter-y filter-width filter-height filter-units]
|
||||
(filter-coords bounds selrect padding)]
|
||||
|
||||
(when (> (count filters) 2)
|
||||
[:filter {:id filter-id
|
||||
:x filter-x
|
||||
:y filter-y
|
||||
:width filter-width
|
||||
:height filter-height
|
||||
:filterUnits "objectBoundingBox"
|
||||
:filterUnits filter-units
|
||||
:color-interpolation-filters "sRGB"}
|
||||
(for [[index entry] (d/enumerate filters)]
|
||||
[:& filter-entry {:key (dm/str filter-id "-" index)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.attrs :as attrs]
|
||||
[app.main.ui.shapes.custom-stroke :refer [shape-fills shape-strokes]]
|
||||
[app.main.ui.shapes.filters :as filters]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.object :as obj]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -65,6 +66,11 @@
|
||||
|
||||
render-id (mf/use-ctx muc/render-id)
|
||||
|
||||
filter-id-blur (dm/fmt "filter-blur-%" render-id)
|
||||
filter-id-shadows (dm/fmt "filter-shadow-%" render-id)
|
||||
filter-str-blur (filters/filter-str filter-id-blur shape)
|
||||
filter-str-shadows (filters/filter-str filter-id-shadows shape)
|
||||
|
||||
x (dm/get-prop shape :x)
|
||||
y (dm/get-prop shape :y)
|
||||
w (dm/get-prop shape :width)
|
||||
@@ -86,29 +92,37 @@
|
||||
:className "frame-background"})))
|
||||
path? (some? (.-d props))]
|
||||
|
||||
[:*
|
||||
[:g {:clip-path (when-not ^boolean show-content?
|
||||
(frame-clip-url shape render-id))
|
||||
;; A frame sets back normal fill behavior (default
|
||||
;; transparent). It may have been changed to default black
|
||||
;; if a shape coming from an imported SVG file is
|
||||
;; rendered. See main.ui.shapes.attrs/add-style-attrs.
|
||||
:fill "none"
|
||||
:opacity opacity}
|
||||
;; We need to separate blur from shadows because the blur is applied to the strokes
|
||||
;; while the shadows have to be placed *under* the stroke (for example, the inner shadows)
|
||||
;; and the shadows needs to be applied only to the content (without the stroke)
|
||||
[:g {:filter filter-str-blur}
|
||||
[:defs
|
||||
[:& filters/filters {:shape (dissoc shape :blur) :filter-id filter-id-shadows}]
|
||||
[:& filters/filters {:shape (assoc shape :shadow []) :filter-id filter-id-blur}]]
|
||||
|
||||
[:& shape-fills {:shape shape}
|
||||
(if ^boolean path?
|
||||
[:> :path props]
|
||||
[:> :rect props])]
|
||||
;; This need to be separated in two layers so the clip doesn't affect the shadow filters
|
||||
;; otherwise the shadow will be clipped and not visible
|
||||
[:g {:filter filter-str-shadows}
|
||||
[:g {:clip-path (when-not ^boolean show-content? (frame-clip-url shape render-id))
|
||||
;; A frame sets back normal fill behavior (default
|
||||
;; transparent). It may have been changed to default black
|
||||
;; if a shape coming from an imported SVG file is
|
||||
;; rendered. See main.ui.shapes.attrs/add-style-attrs.
|
||||
:fill "none"
|
||||
:opacity opacity}
|
||||
|
||||
children]
|
||||
[:& shape-fills {:shape shape}
|
||||
(if ^boolean path?
|
||||
[:> :path props]
|
||||
[:> :rect props])]
|
||||
|
||||
children]]
|
||||
|
||||
[:& shape-strokes {:shape shape}
|
||||
(if ^boolean path?
|
||||
[:> :path props]
|
||||
[:> :rect props])]]))
|
||||
|
||||
|
||||
(mf/defc frame-thumbnail-image
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
(let [shape (unchecked-get props "shape")
|
||||
children (unchecked-get props "children")
|
||||
pointer-events (unchecked-get props "pointer-events")
|
||||
disable-shadows? (unchecked-get props "disable-shadows?")
|
||||
shape-id (dm/get-prop shape :id)
|
||||
|
||||
preview-blend-mode-ref
|
||||
@@ -67,7 +66,6 @@
|
||||
|
||||
type (dm/get-prop shape :type)
|
||||
render-id (h/use-render-id)
|
||||
filter-id (dm/str "filter-" render-id)
|
||||
styles (-> (obj/create)
|
||||
(obj/set! "pointerEvents" pointer-events)
|
||||
(cond-> (not (cfh/frame-shape? shape))
|
||||
@@ -82,32 +80,30 @@
|
||||
shape-without-blur (dissoc shape :blur)
|
||||
shape-without-shadows (assoc shape :shadow [])
|
||||
|
||||
filter-id (dm/str "filter-" render-id)
|
||||
filter-str
|
||||
(when (and (or (cfh/group-shape? shape)
|
||||
(cfh/frame-shape? shape)
|
||||
(cfh/svg-raw-shape? shape))
|
||||
(not disable-shadows?))
|
||||
(when (or (cfh/group-shape? shape)
|
||||
(cfh/svg-raw-shape? shape))
|
||||
(filters/filter-str filter-id shape))
|
||||
|
||||
wrapper-props
|
||||
(-> (obj/clone props)
|
||||
(obj/unset! "shape")
|
||||
(obj/unset! "children")
|
||||
(obj/unset! "disable-shadows?")
|
||||
(obj/set! "ref" ref)
|
||||
(obj/set! "id" (dm/fmt "shape-%" shape-id))
|
||||
(obj/set! "data-testid" (:name shape))
|
||||
|
||||
;; TODO: This is added for backward compatibility.
|
||||
(cond-> (and (cfh/text-shape? shape) (empty? (:position-data shape)))
|
||||
(-> (obj/set! "x" (:x shape))
|
||||
(obj/set! "y" (:y shape))
|
||||
(obj/set! "width" (:width shape))
|
||||
(obj/set! "height" (:height shape))))
|
||||
(obj/set! "style" styles))
|
||||
|
||||
wrapper-props
|
||||
(cond-> wrapper-props
|
||||
;; NOTE: This is added for backward compatibility
|
||||
(and (cfh/text-shape? shape)
|
||||
(empty? (:position-data shape)))
|
||||
(-> (obj/set! "x" (:x shape))
|
||||
(obj/set! "y" (:y shape))
|
||||
(obj/set! "width" (:width shape))
|
||||
(obj/set! "height" (:height shape)))
|
||||
|
||||
(= :group type)
|
||||
(-> (attrs/add-fill-props! shape render-id)
|
||||
(attrs/add-border-props! shape))
|
||||
@@ -115,11 +111,13 @@
|
||||
(some? filter-str)
|
||||
(obj/set! "filter" filter-str))
|
||||
|
||||
svg-group? (and (contains? shape :svg-attrs) (= :group type))
|
||||
svg-group?
|
||||
(and (contains? shape :svg-attrs) (= :group type))
|
||||
|
||||
children (cond-> children
|
||||
svg-group?
|
||||
(propagate-wrapper-styles wrapper-props))]
|
||||
children
|
||||
(cond-> children
|
||||
svg-group?
|
||||
(propagate-wrapper-styles wrapper-props))]
|
||||
|
||||
[:& (mf/provider muc/render-id) {:value render-id}
|
||||
[:> :g wrapper-props
|
||||
@@ -128,9 +126,14 @@
|
||||
|
||||
[:defs
|
||||
[:& defs/svg-defs {:shape shape :render-id render-id}]
|
||||
[:& filters/filters {:shape shape :filter-id filter-id}]
|
||||
[:& filters/filters {:shape shape-without-blur :filter-id (dm/fmt "filter-shadow-%" render-id)}]
|
||||
[:& filters/filters {:shape shape-without-shadows :filter-id (dm/fmt "filter-blur-%" render-id)}]
|
||||
|
||||
;; The filters for frames should be setup inside the container.
|
||||
(when-not (cfh/frame-shape? shape)
|
||||
[:*
|
||||
[:& filters/filters {:shape shape :filter-id filter-id}]
|
||||
[:& filters/filters {:shape shape-without-blur :filter-id (dm/fmt "filter-shadow-%" render-id)}]
|
||||
[:& filters/filters {:shape shape-without-shadows :filter-id (dm/fmt "filter-blur-%" render-id)}]])
|
||||
|
||||
[:& frame/frame-clip-def {:shape shape :render-id render-id}]
|
||||
|
||||
;; Text fills need to be defined afterwards because they are specified per text-block
|
||||
|
||||
@@ -98,7 +98,11 @@
|
||||
(obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))}
|
||||
(cond-> browser-props
|
||||
(obj/merge! browser-props)))
|
||||
shape (assoc shape :fills (:fills data))
|
||||
shape (-> shape
|
||||
(assoc :fills (:fills data))
|
||||
;; The text elements have the shadow and blur already applied in the
|
||||
;; group parent.
|
||||
(dissoc :shadow :blur))
|
||||
|
||||
;; Need to create new render-id per text-block
|
||||
render-id (dm/str render-id "-" index)]
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]]
|
||||
[app.main.ui.auth.register :as register]
|
||||
[app.main.ui.dashboard.sidebar :refer [sidebar]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
|
||||
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.viewer.header :as viewer.header]
|
||||
[app.util.dom :as dom]
|
||||
@@ -42,11 +44,10 @@
|
||||
[:section {:class (stl/css :exception-layout)}
|
||||
[:button
|
||||
{:class (stl/css :exception-header)
|
||||
:on-click rt/nav-root}
|
||||
i/logo-icon
|
||||
:on-click on-nav-root}
|
||||
[:> raw-svg* {:id "penpot-logo-icon" :class (stl/css :penpot-logo)}]
|
||||
(when profile-id
|
||||
(str "< "
|
||||
(tr "not-found.no-permission.go-dashboard")))]
|
||||
[:div {:class (stl/css :go-back-wrapper)} [:> icon* {:id "arrow" :class (stl/css :back-arrow)}] [:span (tr "not-found.no-permission.go-dashboard")]])]
|
||||
[:div {:class (stl/css :deco-before)} i/logo-error-screen]
|
||||
(when-not profile-id
|
||||
[:button {:class (stl/css :login-header)
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
width: 25%;
|
||||
padding-bottom: $s-28;
|
||||
|
||||
&:first-child {
|
||||
text-align: right;
|
||||
@@ -82,12 +83,25 @@
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--color-foreground-primary);
|
||||
width: $s-48;
|
||||
height: auto;
|
||||
}
|
||||
.penpot-logo {
|
||||
fill: var(--color-foreground-primary);
|
||||
width: $s-48;
|
||||
height: $s-48;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.go-back-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: $s-8;
|
||||
margin-left: $s-12;
|
||||
font-size: $fs-14;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.config :as cfg]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.plugins :as dp]
|
||||
@@ -20,7 +20,6 @@
|
||||
[app.plugins.register :as preg]
|
||||
[app.util.avatars :as avatars]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
@@ -96,15 +95,11 @@
|
||||
(mf/deps plugins-state plugin-url)
|
||||
(fn []
|
||||
(reset! fetching-manifest? true)
|
||||
(->> (http/send! {:method :get
|
||||
:uri plugin-url
|
||||
:omit-default-headers true
|
||||
:response-type :json})
|
||||
(rx/map :body)
|
||||
(->> (dp/fetch-manifest plugin-url)
|
||||
(rx/subs!
|
||||
(fn [body]
|
||||
(fn [plugin]
|
||||
(reset! fetching-manifest? false)
|
||||
(if-let [plugin (preg/parse-manifest plugin-url body)]
|
||||
(if plugin
|
||||
(do
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "install-plugin" :name (:name plugin) :url plugin-url}))
|
||||
(modal/show!
|
||||
@@ -118,7 +113,8 @@
|
||||
(reset! plugin-url* ""))
|
||||
;; Cannot get the manifest
|
||||
(reset! input-status* :error-manifest)))
|
||||
(fn [_]
|
||||
(fn [err]
|
||||
(.error js/console err)
|
||||
(reset! fetching-manifest? false)
|
||||
(reset! input-status* :error-url))))))
|
||||
|
||||
@@ -141,6 +137,7 @@
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "remove-plugin"
|
||||
:name (:name plugin)
|
||||
:host (:host plugin)}))
|
||||
(dp/close-plugin! plugin)
|
||||
(preg/remove-plugin! plugin)
|
||||
(reset! plugins-state* (preg/plugins-list)))))]
|
||||
|
||||
@@ -168,10 +165,11 @@
|
||||
[:div {:class (stl/css-case :info true :error error?)}
|
||||
(tr "workspace.plugins.error.manifest")])
|
||||
|
||||
[:> i18n/tr-html*
|
||||
{:class (stl/css :discover)
|
||||
:on-click #(st/emit! (ptk/event ::ev/event {::ev/name "open-plugins-list"}))
|
||||
:content (tr "workspace.plugins.discover" cf/plugins-list-uri)}]
|
||||
(when-not (empty? plugins-state)
|
||||
[:> i18n/tr-html*
|
||||
{:class (stl/css :discover)
|
||||
:on-click #(st/emit! (ptk/event ::ev/event {::ev/name "open-plugins-list"}))
|
||||
:content (tr "workspace.plugins.discover" cfg/plugins-list-uri)}])
|
||||
|
||||
[:hr]
|
||||
|
||||
@@ -180,7 +178,7 @@
|
||||
[:div {:class (stl/css :plugins-empty-logo)} i/puzzle]
|
||||
[:div {:class (stl/css :plugins-empty-text)} (tr "workspace.plugins.empty-plugins")]
|
||||
[:a {:class (stl/css :plugins-link)
|
||||
:href cf/plugins-list-uri
|
||||
:href cfg/plugins-list-uri
|
||||
:target "_blank"
|
||||
:on-click #(st/emit! (ptk/event ::ev/event {::ev/name "open-plugins-list"}))}
|
||||
(tr "workspace.plugins.plugin-list-link") i/external-link]]
|
||||
@@ -197,6 +195,62 @@
|
||||
:on-open-plugin handle-open-plugin
|
||||
:on-remove-plugin handle-remove-plugin}])]])]]]))
|
||||
|
||||
(mf/defc plugins-permission-list
|
||||
[{:keys [permissions]}]
|
||||
[:div {:class (stl/css :permissions-list)}
|
||||
(cond
|
||||
(contains? permissions "content:write")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-1
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.content-write")]]
|
||||
|
||||
(contains? permissions "content:read")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-1
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.content-read")]])
|
||||
|
||||
(cond
|
||||
(contains? permissions "user:read")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-2
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.user-read")]])
|
||||
|
||||
(cond
|
||||
(contains? permissions "library:write")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-3
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.library-write")]]
|
||||
|
||||
(contains? permissions "library:read")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-3
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.library-read")]])
|
||||
|
||||
(cond
|
||||
(contains? permissions "comment:write")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-1
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.comment-write")]]
|
||||
|
||||
(contains? permissions "comment:read")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-1
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.comment-read")]])
|
||||
|
||||
(cond
|
||||
(contains? permissions "allow:downloads")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-1
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.allow-download")]])])
|
||||
|
||||
(mf/defc plugins-permissions-dialog
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :plugin-permissions}
|
||||
@@ -231,62 +285,65 @@
|
||||
[:div {:class (stl/css :modal-title)} (tr "workspace.plugins.permissions.title" (str/upper (:name plugin)))]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :permissions-list)}
|
||||
(cond
|
||||
(contains? permissions "content:write")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-1
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.content-write")]]
|
||||
[:& plugins-permission-list {:permissions permissions}]
|
||||
|
||||
(contains? permissions "content:read")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-1
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.content-read")]])
|
||||
(when-not (contains? cfg/plugins-whitelist host)
|
||||
[:div {:class (stl/css :permissions-disclaimer)}
|
||||
(tr "workspace.plugins.permissions.disclaimer")])]
|
||||
|
||||
(cond
|
||||
(contains? permissions "user:read")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-2
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.user-read")]])
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:input
|
||||
{:class (stl/css :cancel-button :button-expand)
|
||||
:type "button"
|
||||
:value (tr "ds.confirm-cancel")
|
||||
:on-click handle-close-dialog}]
|
||||
|
||||
(cond
|
||||
(contains? permissions "library:write")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-3
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.library-write")]]
|
||||
[:input
|
||||
{:class (stl/css :primary-button :button-expand)
|
||||
:type "button"
|
||||
:value (tr "ds.confirm-allow")
|
||||
:on-click handle-accept-dialog}]]]]]))
|
||||
|
||||
(contains? permissions "library:read")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-3
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.library-read")]])
|
||||
|
||||
(cond
|
||||
(contains? permissions "comment:write")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-1
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.comment-write")]]
|
||||
(mf/defc plugins-permissions-updated-dialog
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :plugin-permissions-update}
|
||||
[{:keys [plugin on-accept on-close]}]
|
||||
|
||||
(contains? permissions "comment:read")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-1
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.comment-read")]])
|
||||
(let [{:keys [host permissions]} plugin
|
||||
permissions (set permissions)
|
||||
|
||||
(cond
|
||||
(contains? permissions "allow:downloads")
|
||||
[:div {:class (stl/css :permissions-list-entry)}
|
||||
i/oauth-1
|
||||
[:p {:class (stl/css :permissions-list-text)}
|
||||
(tr "workspace.plugins.permissions.allow-download")]])]
|
||||
handle-accept-dialog
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "allow-plugin-permissions"
|
||||
:host host
|
||||
:permissions (->> permissions (str/join ", "))})
|
||||
(modal/hide))
|
||||
(when on-accept (on-accept))))
|
||||
|
||||
[:div {:class (stl/css :permissions-disclaimer)}
|
||||
(tr "workspace.plugins.permissions.disclaimer")]]
|
||||
handle-close-dialog
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "reject-plugin-permissions"
|
||||
:host host
|
||||
:permissions (->> permissions (str/join ", "))})
|
||||
(modal/hide))
|
||||
(when on-close (on-close))))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog :plugin-permissions)}
|
||||
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon]
|
||||
[:div {:class (stl/css :modal-title)}
|
||||
(tr "workspace.plugins.permissions-update.title" (str/upper (:name plugin)))]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-paragraph)}
|
||||
(tr "workspace.plugins.permissions-update.warning")]
|
||||
[:& plugins-permission-list {:permissions permissions}]]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
|
||||
@@ -76,6 +76,11 @@
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.modal-paragraph {
|
||||
font-size: $fs-14;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
@extend .button-primary;
|
||||
@include headlineSmallTypography;
|
||||
@@ -267,7 +272,7 @@ div.input-error {
|
||||
@include bodySmallTypography;
|
||||
padding: $s-16;
|
||||
background: var(--color-background-quaternary);
|
||||
color: var(--color-foreground-quaternary);
|
||||
color: var(--color-foreground-primary);
|
||||
border-radius: $br-4;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.shapes.bounds :as gsb]
|
||||
[app.common.math :as mth]
|
||||
[app.common.thumbnails :as thc]
|
||||
@@ -45,7 +44,7 @@
|
||||
(refs/children-objects shape-id))
|
||||
childs (mf/deref childs-ref)]
|
||||
|
||||
[:& shape-container {:shape shape :ref ref :disable-shadows? (cfh/is-direct-child-of-root? shape)}
|
||||
[:& shape-container {:shape shape :ref ref}
|
||||
[:& frame-shape {:shape shape :childs childs}]
|
||||
(when *assert*
|
||||
[:& wsd/shape-debug {:shape shape}])]))))
|
||||
@@ -187,7 +186,7 @@
|
||||
|
||||
(fdm/use-dynamic-modifiers objects (mf/ref-val content-ref) modifiers)
|
||||
|
||||
[:& shape-container {:shape shape :disable-shadows? thumbnail?}
|
||||
[:& shape-container {:shape shape}
|
||||
[:g.frame-container
|
||||
{:id (dm/str "frame-container-" frame-id)
|
||||
:key "frame-container"
|
||||
|
||||
@@ -230,6 +230,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.layer-row:hover .element-actions.selected & {
|
||||
opacity: $op-10;
|
||||
}
|
||||
|
||||
.layer-row.highlight &,
|
||||
.layer-row:hover & {
|
||||
display: flex;
|
||||
|
||||
@@ -138,6 +138,8 @@
|
||||
first-selected-shape (first selected-shapes)
|
||||
shape-parent-frame (cfh/get-frame objects (:frame-id first-selected-shape))
|
||||
|
||||
options-mode (mf/deref refs/options-mode-global)
|
||||
|
||||
on-change-tab
|
||||
(fn [options-mode]
|
||||
(let [options-mode (keyword options-mode)]
|
||||
@@ -187,6 +189,7 @@
|
||||
[:> tab-switcher* {:tabs tabs
|
||||
:default-selected "info"
|
||||
:on-change-tab on-change-tab
|
||||
:selected (name options-mode)
|
||||
:class (stl/css :options-tab-switcher)}]]))
|
||||
|
||||
;; TODO: this need optimizations, selected-objects and
|
||||
|
||||
@@ -36,32 +36,37 @@
|
||||
.constraints-bottom {
|
||||
@include flexCenter;
|
||||
grid-area: top;
|
||||
.constraint-btn,
|
||||
.constraint-btn-special,
|
||||
.constraint-btn-rotated {
|
||||
@include buttonStyle;
|
||||
@include flexCenter;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.resalted-area {
|
||||
width: $s-32;
|
||||
height: $s-3;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--button-constraint-background-color-rest);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
&.active .resalted-area {
|
||||
outline: $s-4 solid var(--button-constraint-border-color-hover);
|
||||
background-color: var(--button-constraint-background-color-hover);
|
||||
}
|
||||
&:hover .resalted-area,
|
||||
&:focus .resalted-area {
|
||||
outline: $s-4 solid var(--button-constraint-border-color-hover);
|
||||
background-color: var(--button-constraint-background-color-hover);
|
||||
}
|
||||
}
|
||||
.constraint-btn,
|
||||
.constraint-btn-special,
|
||||
.constraint-btn-rotated {
|
||||
@include buttonStyle;
|
||||
@include flexCenter;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
--resalted-area-background-color: var(--button-constraint-background-color-rest);
|
||||
--resalted-area-border-color: none;
|
||||
&.active {
|
||||
--resalted-area-border-color: var(--button-constraint-border-color-hover);
|
||||
--resalted-area-background-color: var(--button-constraint-background-color-hover);
|
||||
}
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
--resalted-area-border-color: var(--button-constraint-border-color-hover);
|
||||
--resalted-area-background-color: var(--button-constraint-background-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.resalted-area {
|
||||
width: $s-32;
|
||||
height: $s-3;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--resalted-area-background-color);
|
||||
outline: $s-4 solid var(--resalted-area-border-color);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.constraints-left {
|
||||
grid-area: left;
|
||||
.constraint-btn-rotated {
|
||||
@@ -73,6 +78,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.constraints-center {
|
||||
grid-area: center;
|
||||
position: relative;
|
||||
@@ -113,7 +119,7 @@
|
||||
grid-area: bottom;
|
||||
}
|
||||
|
||||
.contraints-selects {
|
||||
.constraints-selects {
|
||||
@include flexColumn;
|
||||
}
|
||||
|
||||
|
||||
@@ -677,7 +677,7 @@
|
||||
[{:keys [type value]}]
|
||||
(case type
|
||||
:auto "auto"
|
||||
:percent (fmt/format-percent value)
|
||||
:percent (fmt/format-percent (/ value 100))
|
||||
:flex (fmt/format-frs value)
|
||||
:fixed (fmt/format-pixels value)
|
||||
value))
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
.grid-layout-menu {
|
||||
@include flexColumn;
|
||||
gap: $s-8;
|
||||
overflow: hidden;
|
||||
|
||||
.row {
|
||||
@include flexRow;
|
||||
|
||||
@@ -397,7 +397,7 @@
|
||||
(->> @hover-ids
|
||||
(filter #(cfh/frame-shape? (get base-objects %)))
|
||||
(remove selected)
|
||||
(first))
|
||||
(last))
|
||||
outlined-frame (get objects outlined-frame-id)]
|
||||
[:*
|
||||
[:& outline/shape-outlines
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
(rx/filter some?))))))
|
||||
|
||||
over-shapes-stream
|
||||
(mf/with-memo [move-stream mod-str]
|
||||
(mf/with-memo [query-point move-stream mod-str]
|
||||
(->> (rx/merge
|
||||
;; This stream works to "refresh" the outlines when the control is pressed
|
||||
;; but the mouse has not been moved from its position.
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
manifest
|
||||
(d/without-nils
|
||||
{:plugin-id plugin-id
|
||||
:url plugin-url
|
||||
:name name
|
||||
:description desc
|
||||
:host origin
|
||||
|
||||
@@ -720,6 +720,19 @@
|
||||
[filename blob]
|
||||
(trigger-download-uri filename (.-type ^js blob) (wapi/create-uri blob)))
|
||||
|
||||
(defn event
|
||||
"Create an instance of DOM Event"
|
||||
([^string type]
|
||||
(js/Event. type))
|
||||
([^string type options]
|
||||
(js/Event. type options)))
|
||||
|
||||
(defn dispatch-event
|
||||
[target event]
|
||||
(when (some? target)
|
||||
(.dispatchEvent ^js target event)))
|
||||
|
||||
|
||||
(defn save-as
|
||||
[uri filename mtype description]
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.transit :as t]
|
||||
[app.util.functions :as fns]
|
||||
[app.util.globals :as g]
|
||||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.util :as ou]))
|
||||
|
||||
@@ -157,3 +158,11 @@
|
||||
(defonce user (create-storage local-storage-backend "penpot-user"))
|
||||
(defonce storage (create-storage local-storage-backend "penpot"))
|
||||
(defonce session (create-storage session-storage-backend "penpot"))
|
||||
|
||||
(defonce before-unload
|
||||
(letfn [(on-before-unload [_]
|
||||
(binding [*sync* true]
|
||||
(swap! global assoc ::last-refresh (dt/now))
|
||||
(swap! user assoc ::last-refresh (dt/now))))]
|
||||
(.addEventListener g/window "beforeunload" on-before-unload)
|
||||
on-before-unload))
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
(ns app.util.time
|
||||
(:require
|
||||
["./time_impl.js" :as impl]
|
||||
["./time_impl" :as impl]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.time :as common-time]
|
||||
[app.util.object :as obj]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fmt1 from "date-fns/format";
|
||||
import fmt2 from "date-fns/formatDistanceToNowStrict";
|
||||
import {format as dfnFormat} from "date-fns/format";
|
||||
import {formatDistanceToNowStrict as dfnFormatDistance} from "date-fns/formatDistanceToNowStrict";
|
||||
|
||||
import {arSA} from "date-fns/locale/ar-SA";
|
||||
import {ca} from "date-fns/locale/ca";
|
||||
@@ -67,5 +67,5 @@ export const locales = {
|
||||
"ja_jp": ja,
|
||||
};
|
||||
|
||||
export const format = fmt1.format;
|
||||
export const format_distance_to_now = fmt2.formatDistanceToNowStrict;
|
||||
export const format = dfnFormat;
|
||||
export const format_distance_to_now = dfnFormatDistance;
|
||||
|
||||
@@ -458,8 +458,10 @@
|
||||
page-data (-> (parser/parse-page-data content)
|
||||
(assoc :name page-name)
|
||||
(assoc :id (resolve page-id)))
|
||||
|
||||
flows (->> (get page-data :flows)
|
||||
(update-vals #(update % :starting-frame resolve))
|
||||
(map #(update % :starting-frame resolve))
|
||||
(d/index-by :id)
|
||||
(not-empty))
|
||||
|
||||
guides (-> (get page-data :guides)
|
||||
@@ -815,9 +817,12 @@
|
||||
:errors (:errors file)
|
||||
:file-id (:file-id data)})))))))
|
||||
(rx/catch (fn [cause]
|
||||
(log/error :hint (ex-message cause)
|
||||
:file-id (:file-id data)
|
||||
:cause cause)
|
||||
(let [data (ex-data cause)]
|
||||
(log/error :hint (ex-message cause)
|
||||
:file-id (:file-id data))
|
||||
(when-let [explain (:explain data)]
|
||||
(js/console.log explain)))
|
||||
|
||||
(rx/of {:status :import-error
|
||||
:file-id (:file-id data)
|
||||
:error (ex-message cause)
|
||||
|
||||
@@ -5610,6 +5610,12 @@ msgstr "'%s' PLUGIN WANTS ACCESS TO:"
|
||||
msgid "workspace.plugins.permissions.user-read"
|
||||
msgstr "Read the profile information of the current user."
|
||||
|
||||
msgid "workspace.plugins.permissions-update.title"
|
||||
msgstr "UPDATE THIS PLUGIN"
|
||||
|
||||
msgid "workspace.plugins.permissions-update.warning"
|
||||
msgstr "The plugin has been modified since you last opened it. It now also wants to access:"
|
||||
|
||||
msgid "workspace.plugins.try-out.title"
|
||||
msgstr "'%s' PLUGIN IS INSTALLED FOR YOUR USER!"
|
||||
|
||||
|
||||
@@ -5594,6 +5594,12 @@ msgstr "LA EXTENSIÓN '%s' SOLICITA PERMISO PARA ACCEDER:"
|
||||
msgid "workspace.plugins.permissions.user-read"
|
||||
msgstr "Leer la información del usuario actual."
|
||||
|
||||
msgid "workspace.plugins.permissions-update.title"
|
||||
msgstr "EXTENSIÓN ACTUALIZADA"
|
||||
|
||||
msgid "workspace.plugins.permissions-update.warning"
|
||||
msgstr "La extensión ha cambiado desde la última vez que la abriste. Ahora quiere acceder a:"
|
||||
|
||||
msgid "workspace.plugins.try-out.title"
|
||||
msgstr "¡LA EXTENSIÓN '%s' HA SIDO INSTALADA PARA TU USUARIO!"
|
||||
|
||||
|
||||
10
manage.sh
10
manage.sh
@@ -7,6 +7,9 @@ export DEVENV_PNAME="penpotdev";
|
||||
export CURRENT_USER_ID=$(id -u);
|
||||
export CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD);
|
||||
|
||||
# Set default java options
|
||||
export JAVA_OPTS=${JAVA_OPTS:-"-Xmx1000m -Xms50m"};
|
||||
|
||||
set -e
|
||||
|
||||
function print-current-version {
|
||||
@@ -95,10 +98,12 @@ function run-devenv-shell {
|
||||
if [[ ! $(docker ps -f "name=penpot-devenv-main" -q) ]]; then
|
||||
start-devenv
|
||||
fi
|
||||
docker exec -ti penpot-devenv-main sudo -EH -u penpot bash
|
||||
docker exec -ti \
|
||||
-e JAVA_OPTS="$JAVA_OPTS" \
|
||||
-e EXTERNAL_UID=$CURRENT_USER_ID \
|
||||
penpot-devenv-main sudo -EH -u penpot bash;
|
||||
}
|
||||
|
||||
|
||||
function build {
|
||||
echo ">> build start: $1"
|
||||
local version=$(print-current-version);
|
||||
@@ -111,6 +116,7 @@ function build {
|
||||
-e EXTERNAL_UID=$CURRENT_USER_ID \
|
||||
-e BUILD_STORYBOOK=$BUILD_STORYBOOK \
|
||||
-e SHADOWCLJS_EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS \
|
||||
-e JAVA_OPTS="$JAVA_OPTS" \
|
||||
-w /home/penpot/penpot/$1 \
|
||||
$DEVENV_IMGNAME:latest sudo -EH -u penpot ./scripts/build $version
|
||||
|
||||
|
||||
Reference in New Issue
Block a user