Compare commits

..

1 Commits

Author SHA1 Message Date
Eva Marco
91b1c210d1 wip 2025-12-11 09:47:11 +01:00
523 changed files with 3935 additions and 84230 deletions

View File

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

View File

@@ -11,7 +11,7 @@ jobs:
secrets: inherit secrets: inherit
with: with:
gh_ref: ${{ github.ref_name }} gh_ref: ${{ github.ref_name }}
build_wasm: "yes" build_wasm: "no"
build_storybook: "yes" build_storybook: "yes"
build-docker: build-docker:
@@ -33,7 +33,7 @@ jobs:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }} MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: | TEXT: |
🐳 *[PENPOT] Docker image available: ${{ github.ref_name }}* 🐳 *[PENPOT] Docker image available.*
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra @infra

View File

@@ -51,51 +51,6 @@ jobs:
run: | run: |
./scripts/test ./scripts/test
test-plugins:
name: Plugins Runtime Linter & Tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Setup Node
id: setup-node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
pnpm install;
- name: Run Lint
working-directory: ./plugins
run: pnpm run lint
- name: Run Format Check
working-directory: ./plugins
run: pnpm run format:check
- name: Run Test
working-directory: ./plugins
run: pnpm run test
- name: Build runtime
working-directory: ./plugins
run: pnpm run build
- name: Build plugins
working-directory: ./plugins
run: pnpm run build:plugins
- name: Build styles
working-directory: ./plugins
run: pnpm run build:styles-example
test-frontend: test-frontend:
name: "Frontend Tests" name: "Frontend Tests"
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@@ -112,8 +67,6 @@ jobs:
- name: Component Tests - name: Component Tests
working-directory: ./frontend working-directory: ./frontend
env:
VITEST_BROWSER_TIMEOUT: 120000
run: | run: |
./scripts/test-components ./scripts/test-components

2
.nvmrc
View File

@@ -1 +1 @@
v22.21.1 v22.19.0

View File

@@ -1,18 +1,5 @@
# CHANGELOG # CHANGELOG
## 2.14.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
### :bug: Bugs fixed
## 2.13.0 (Unreleased) ## 2.13.0 (Unreleased)
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
@@ -25,28 +12,16 @@
### :sparkles: New features & Enhancements ### :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) - 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 ### :bug: Bugs fixed
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565) - Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460) - Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339) - Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
- Fix problem with grid layout components and auto sizing [Github #7797](https://github.com/penpot/penpot/issues/7797)
## 2.12.1 ## 2.12.0 (Unreleased)
### :bug: Bugs fixed
- Fix setting a portion of text as bold or underline messes things up [Github #7980](https://github.com/penpot/penpot/issues/7980)
- Fix problem with style in fonts input [Taiga #12935](https://tree.taiga.io/project/penpot/issue/12935)
- Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917)
## 2.12.0
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
@@ -108,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) - 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) - 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 ### :sparkles: New features & Enhancements
@@ -140,9 +114,6 @@ example. It's still usable as before, we just removed the example.
- Fix unpublish library modal not scrolling a long file list [Taiga #12285](https://tree.taiga.io/project/penpot/issue/12285) - 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 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 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 ## 2.11.1

View File

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

View File

@@ -3,7 +3,7 @@
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Tokens%20starter%20kit.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Tokens%20starter%20kit.penpot"}
{:id "penpot-design-system" {:id "penpot-design-system"
:name "Penpot Design System | Pencil" :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" {:id "wireframing-kit"
:name "Wireframe library" :name "Wireframe library"
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"} :file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"}

View File

@@ -331,81 +331,6 @@
(set/difference cfeat/backend-only-features)) (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 (defn get-project
[cfg project-id] [cfg project-id]
(db/get cfg :project {:id project-id})) (db/get cfg :project {:id project-id}))

View File

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

View File

@@ -30,7 +30,7 @@
(defn- get-file-media-object (defn- get-file-media-object
[pool id] [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 (defn- serve-object-from-s3
[{:keys [::sto/storage] :as cfg} obj] [{:keys [::sto/storage] :as cfg} obj]

View File

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

View File

@@ -14,7 +14,6 @@
[app.common.spec :as us] [app.common.spec :as us]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.uri :as u] [app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http :as-alias http] [app.http :as-alias http]
@@ -93,11 +92,7 @@
(let [handler-name (:type path-params) (let [handler-name (:type path-params)
etag (yreq/get-header request "if-none-match") etag (yreq/get-header request "if-none-match")
profile-id (or (::session/profile-id request) profile-id (or (::session/profile-id request)
(::actoken/profile-id request) (::actoken/profile-id request))
(if (::http/auth-with-shared-key request)
uuid/zero
nil))
ip-addr (inet/parse-request request) ip-addr (inet/parse-request request)
data (-> params data (-> params

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,8 @@
java-http-clj/java-http-clj {:mvn/version "0.4.3"} java-http-clj/java-http-clj {:mvn/version "0.4.3"}
integrant/integrant {:mvn/version "1.0.0"} 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 funcool/promesa
{:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8" {:git/sha "46048fc0d4bf5466a2a4121f5d52aefa6337f2e8"
:git/url "https://github.com/funcool/promesa"} :git/url "https://github.com/funcool/promesa"}

View File

@@ -82,113 +82,6 @@
(declare create-svg-children) (declare create-svg-children)
(declare parse-svg-element) (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 (defn create-svg-shapes
([svg-data pos objects frame-id parent-id selected center?] ([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?)) (create-svg-shapes (uuid/next) svg-data pos objects frame-id parent-id selected center?))
@@ -219,9 +112,6 @@
(csvg/fix-percents) (csvg/fix-percents)
(csvg/extract-defs)) (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 ;; In penpot groups have the size of their children. To
;; respect the imported svg size and empty space let's create ;; respect the imported svg size and empty space let's create
;; a transparent shape as background to respect the imported ;; 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) (reduce (partial create-svg-children objects selected frame-id root-id svg-data)
[unames []] [unames []]
(d/enumerate (->> (:content svg-data) (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 [root-shape children])))
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])))
(defn create-raw-svg (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) (let [props (csvg/attrs->props attrs)
vbox (grc/make-rect offset-x offset-y width height)] vbox (grc/make-rect offset-x offset-y width height)]
(cts/setup-shape (cts/setup-shape
@@ -281,11 +160,10 @@
:y y :y y
:content data :content data
:svg-attrs props :svg-attrs props
:svg-viewbox vbox :svg-viewbox vbox})))
:svg-defs defs})))
(defn create-svg-root (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) (let [props (-> (dissoc attrs :viewBox :view-box :xmlns)
(d/without-keys csvg/inheritable-props) (d/without-keys csvg/inheritable-props)
(csvg/attrs->props))] (csvg/attrs->props))]
@@ -299,8 +177,7 @@
:height height :height height
:x (+ x offset-x) :x (+ x offset-x)
:y (+ y offset-y) :y (+ y offset-y)
:svg-attrs props :svg-attrs props})))
:svg-defs defs})))
(defn create-svg-children (defn create-svg-children
[objects selected frame-id parent-id svg-data [unames children] [_index svg-element]] [objects selected frame-id parent-id svg-data [unames children] [_index svg-element]]
@@ -321,7 +198,7 @@
(defn create-group (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)) (let [transform (csvg/parse-transform (:transform attrs))
attrs (-> attrs attrs (-> attrs
(d/without-keys csvg/inheritable-props) (d/without-keys csvg/inheritable-props)
@@ -337,8 +214,7 @@
:height height :height height
:svg-transform transform :svg-transform transform
:svg-attrs attrs :svg-attrs attrs
:svg-viewbox vbox :svg-viewbox vbox})))
:svg-defs defs})))
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}] (defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
(when (and (contains? attrs :d) (seq (:d attrs))) (when (and (contains? attrs :d) (seq (:d attrs)))
@@ -647,21 +523,6 @@
:else (dm/str tag))] :else (dm/str tag))]
(dm/str "svg-" suffix))) (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 (defn parse-svg-element
[frame-id svg-data {:keys [tag attrs hidden] :as element} unames] [frame-id svg-data {:keys [tag attrs hidden] :as element} unames]
@@ -673,11 +534,7 @@
(let [name (or (:id attrs) (tag->name tag)) (let [name (or (:id attrs) (tag->name tag))
att-refs (csvg/find-attr-references attrs) att-refs (csvg/find-attr-references attrs)
defs (get svg-data :defs) defs (get svg-data :defs)
valid-refs (filter-valid-def-references att-refs defs) references (csvg/find-def-references defs att-refs)
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)
href-id (or (:href attrs) (:xlink:href attrs) " ") href-id (or (:href attrs) (:xlink:href attrs) " ")
href-id (if (and (string? href-id) href-id (if (and (string? href-id)

View File

@@ -169,7 +169,6 @@
:enable-component-thumbnails :enable-component-thumbnails
:enable-render-wasm-dpr :enable-render-wasm-dpr
:enable-token-color :enable-token-color
:enable-token-shadow
:enable-inspect-styles :enable-inspect-styles
:enable-feature-fdata-objects-map]) :enable-feature-fdata-objects-map])

View File

@@ -546,19 +546,9 @@
filter-values))) filter-values)))
(defn extract-ids [val] (defn extract-ids [val]
;; Extract referenced ids from string values like "url(#myId)". (when (some? val)
;; 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)
(->> (re-seq xml-id-regex val) (->> (re-seq xml-id-regex val)
(mapv second)) (mapv second))))
(sequential? val)
(mapcat extract-ids val)
:else
[]))
(defn fix-dot-number (defn fix-dot-number
"Fixes decimal numbers starting in dot but without leading 0" "Fixes decimal numbers starting in dot but without leading 0"

View File

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

View File

@@ -269,8 +269,8 @@
"Remove flex children properties except the fit-content for flex layouts. These are properties "Remove flex children properties except the fit-content for flex layouts. These are properties
that we don't have to propagate to copies but will be respected when swapping components" that we don't have to propagate to copies but will be respected when swapping components"
[shape] [shape]
(let [layout-item-h-sizing (when (and (ctl/any-layout? shape) (ctl/auto-width? shape)) :auto) (let [layout-item-h-sizing (when (and (ctl/flex-layout? shape) (ctl/auto-width? shape)) :auto)
layout-item-v-sizing (when (and (ctl/any-layout? shape) (ctl/auto-height? shape)) :auto)] layout-item-v-sizing (when (and (ctl/flex-layout? shape) (ctl/auto-height? shape)) :auto)]
(-> shape (-> shape
(d/without-keys ctk/swap-keep-attrs) (d/without-keys ctk/swap-keep-attrs)
(cond-> (some? layout-item-h-sizing) (cond-> (some? layout-item-h-sizing)

View File

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

View File

@@ -1,29 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.types.project
(:require
[app.common.schema :as sm]
[app.common.time :as cm]))
(def schema:project
[:map {:title "Profile"}
[:id ::sm/uuid]
[:created-at {:optional true} ::cm/inst]
[:modified-at {:optional true} ::cm/inst]
[:name :string]
[:is-default {:optional true} ::sm/boolean]
[:is-pinned {:optional true} ::sm/boolean]
[:count {:optional true} ::sm/int]
[:total-count {:optional true} ::sm/int]
[:team-id ::sm/uuid]])
(def valid-project?
(sm/lazy-validator schema:project))
(def check-project
(sm/check-fn schema:project))

View File

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

View File

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

View File

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

View File

@@ -23,25 +23,30 @@ tmux -2 new-session -d -s penpot
tmux rename-window -t penpot:0 'frontend watch' tmux rename-window -t penpot:0 'frontend watch'
tmux select-window -t penpot:0 tmux select-window -t penpot:0
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
tmux send-keys -t penpot './scripts/watch app' enter tmux send-keys -t penpot 'yarn run watch' enter
tmux new-window -t penpot:1 -n 'frontend storybook' tmux new-window -t penpot:1 -n 'frontend shadow'
tmux select-window -t penpot:1 tmux select-window -t penpot:1
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
tmux send-keys -t penpot './scripts/watch storybook' enter tmux send-keys -t penpot 'yarn run watch:app' enter
tmux new-window -t penpot:2 -n 'exporter' tmux new-window -t penpot:2 -n 'frontend storybook'
tmux select-window -t penpot:2 tmux select-window -t penpot:2
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 send-keys -t penpot 'cd penpot/exporter' enter C-l 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 'rm -f target/app.js*' enter C-l
tmux send-keys -t penpot './scripts/watch' enter tmux send-keys -t penpot 'yarn run watch' enter
tmux split-window -v tmux split-window -v
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
tmux send-keys -t penpot './scripts/wait-and-start.sh' enter tmux send-keys -t penpot './scripts/wait-and-start.sh' enter
tmux new-window -t penpot:3 -n 'backend' tmux new-window -t penpot:4 -n 'backend'
tmux select-window -t penpot:3 tmux select-window -t penpot:4
tmux send-keys -t penpot 'cd penpot/backend' enter C-l tmux send-keys -t penpot 'cd penpot/backend' enter C-l
tmux send-keys -t penpot './scripts/start-dev' enter tmux send-keys -t penpot './scripts/start-dev' enter

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC", "author": "Kaleidos INC",
"private": true, "private": true,
"packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8", "packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/penpot/penpot" "url": "https://github.com/penpot/penpot"
@@ -16,12 +16,11 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"generic-pool": "^3.9.0", "generic-pool": "^3.9.0",
"inflation": "^2.1.0", "inflation": "^2.1.0",
"ioredis": "^5.8.2", "ioredis": "^5.8.1",
"playwright": "^1.57.0", "playwright": "^1.55.1",
"raw-body": "^3.0.2", "raw-body": "^3.0.1",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"svgo": "penpot/svgo#v3.1", "svgo": "penpot/svgo#v3.1",
"undici": "^7.16.0",
"xml-js": "^1.6.11", "xml-js": "^1.6.11",
"xregexp": "^5.1.2" "xregexp": "^5.1.2"
}, },
@@ -30,8 +29,8 @@
}, },
"scripts": { "scripts": {
"clear:shadow-cache": "rm -rf .shadow-cljs && rm -rf target", "clear:shadow-cache": "rm -rf .shadow-cljs && rm -rf target",
"watch:app": "yarn run clear:shadow-cache && clojure -M:dev:shadow-cljs watch main", "watch:app": "clojure -M:dev:shadow-cljs watch main",
"watch": "yarn run watch:app", "watch": "yarn run clear:shadow-cache && yarn run watch:app",
"build:app": "clojure -M:dev:shadow-cljs release main", "build:app": "clojure -M:dev:shadow-cljs release main",
"build": "yarn run clear:shadow-cache && yarn run build:app", "build": "yarn run clear:shadow-cache && yarn run build:app",
"fmt:clj:check": "cljfmt check --parallel=false src/", "fmt:clj:check": "cljfmt check --parallel=false src/",

View File

@@ -7,4 +7,5 @@ bb -i '(babashka.wait/wait-for-port "localhost" 9630)';
bb -i '(babashka.wait/wait-for-path "target/app.js")'; bb -i '(babashka.wait/wait-for-path "target/app.js")';
sleep 2; sleep 2;
export NODE_TLS_REJECT_UNAUTHORIZED=0
exec node target/app.js exec node target/app.js

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env bash
TARGET=${1:-app};
set -ex
exec yarn run watch:$TARGET

View File

@@ -107,12 +107,12 @@
:on-progress on-progress) :on-progress on-progress)
append (fn [{:keys [filename path] :as resource}] 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 proc (->> exports
(map (fn [export] (rd/render export append))) (map (fn [export] (rd/render export append)))
(p/all) (p/all)
(p/mcat (fn [_] (rsc/close-zip zip))) (p/fnly (fn [_] (.finalize zip)))
(p/fmap (constantly resource)) (p/fmap (constantly resource))
(p/mcat (partial rsc/upload-resource auth-token)) (p/mcat (partial rsc/upload-resource auth-token))
(p/fmap (fn [resource] (p/fmap (fn [resource]

View File

@@ -11,7 +11,6 @@
["node:fs" :as fs] ["node:fs" :as fs]
["node:fs/promises" :as fsp] ["node:fs/promises" :as fsp]
["node:path" :as path] ["node:path" :as path]
["undici" :as http]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.transit :as t] [app.common.transit :as t]
[app.common.uri :as u] [app.common.uri :as u]
@@ -54,40 +53,30 @@
(.pipe zip out) (.pipe zip out)
zip)) zip))
(defn add-to-zip (defn add-to-zip!
[zip path name] [zip path name]
(.file ^js zip path #js {:name name})) (.file ^js zip path #js {:name name}))
(defn close-zip (defn close-zip!
[zip] [zip]
(p/create (fn [resolve] (.finalize ^js zip))
(.on ^js zip "close" resolve)
(.finalize ^js zip))))
(defn upload-resource (defn upload-resource
[auth-token resource] [auth-token resource]
(->> (fsp/readFile (:path resource)) (->> (fsp/readFile (:path resource))
(p/fmap (fn [buffer] (p/fmap (fn [buffer]
(js/console.log buffer)
(new js/Blob #js [buffer] #js {:type (:mtype resource)}))) (new js/Blob #js [buffer] #js {:type (:mtype resource)})))
(p/mcat (fn [blob] (p/mcat (fn [blob]
(let [fdata (new http/FormData) (let [fdata (new js/FormData)
agent (new http/Agent #js {:connect #js {:rejectUnauthorized false}}) uri (-> (cf/get :public-uri)
headers #js {"X-Shared-Key" cf/management-key (u/ensure-path-slash)
"Authorization" (str "Bearer " auth-token)} (u/join "api/management/methods/upload-tempfile")
(str))]
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))]
(.append fdata "content" blob (:filename resource)) (.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] (p/mcat (fn [response]
(if (not= (.-status response) 200) (if (not= (.-status response) 200)
(ex/raise :type :internal (ex/raise :type :internal

View File

@@ -75,8 +75,7 @@
[path] [path]
(->> (.stat fs/promises path) (->> (.stat fs/promises path)
(p/fmap (fn [data] (p/fmap (fn [data]
{:path path {:created-at (inst-ms (.-ctime ^js data))
:created-at (inst-ms (.-ctime ^js data))
:size (.-size data)})) :size (.-size data)}))
(p/merr (fn [_cause] (p/merr (fn [_cause]
(p/resolved nil))))) (p/resolved nil)))))

View File

@@ -243,7 +243,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"bytes@npm:~3.1.2": "bytes@npm:3.1.2":
version: 3.1.2 version: 3.1.2
resolution: "bytes@npm:3.1.2" resolution: "bytes@npm:3.1.2"
checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e
@@ -442,7 +442,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"depd@npm:~2.0.0": "depd@npm:2.0.0, depd@npm:~2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "depd@npm:2.0.0" resolution: "depd@npm:2.0.0"
checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c
@@ -577,12 +577,11 @@ __metadata:
date-fns: "npm:^4.1.0" date-fns: "npm:^4.1.0"
generic-pool: "npm:^3.9.0" generic-pool: "npm:^3.9.0"
inflation: "npm:^2.1.0" inflation: "npm:^2.1.0"
ioredis: "npm:^5.8.2" ioredis: "npm:^5.8.1"
playwright: "npm:^1.57.0" playwright: "npm:^1.55.1"
raw-body: "npm:^3.0.2" raw-body: "npm:^3.0.1"
source-map-support: "npm:^0.5.21" source-map-support: "npm:^0.5.21"
svgo: "penpot/svgo#v3.1" svgo: "penpot/svgo#v3.1"
undici: "npm:^7.16.0"
ws: "npm:^8.18.3" ws: "npm:^8.18.3"
xml-js: "npm:^1.6.11" xml-js: "npm:^1.6.11"
xregexp: "npm:^5.1.2" xregexp: "npm:^5.1.2"
@@ -683,16 +682,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"http-errors@npm:~2.0.1": "http-errors@npm:2.0.0":
version: 2.0.1 version: 2.0.0
resolution: "http-errors@npm:2.0.1" resolution: "http-errors@npm:2.0.0"
dependencies: dependencies:
depd: "npm:~2.0.0" depd: "npm:2.0.0"
inherits: "npm:~2.0.4" inherits: "npm:2.0.4"
setprototypeof: "npm:~1.2.0" setprototypeof: "npm:1.2.0"
statuses: "npm:~2.0.2" statuses: "npm:2.0.1"
toidentifier: "npm:~1.0.1" toidentifier: "npm:1.0.1"
checksum: 10c0/fb38906cef4f5c83952d97661fe14dc156cb59fe54812a42cd448fa57b5c5dfcb38a40a916957737bd6b87aab257c0648d63eb5b6a9ca9f548e105b6072712d4 checksum: 10c0/fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19
languageName: node languageName: node
linkType: hard linkType: hard
@@ -716,6 +715,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "iconv-lite@npm:^0.6.2":
version: 0.6.3 version: 0.6.3
resolution: "iconv-lite@npm:0.6.3" resolution: "iconv-lite@npm:0.6.3"
@@ -725,15 +733,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "ieee754@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "ieee754@npm:1.2.1" resolution: "ieee754@npm:1.2.1"
@@ -755,16 +754,16 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 2.0.4
resolution: "inherits@npm:2.0.4" resolution: "inherits@npm:2.0.4"
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
languageName: node languageName: node
linkType: hard linkType: hard
"ioredis@npm:^5.8.2": "ioredis@npm:^5.8.1":
version: 5.8.2 version: 5.8.1
resolution: "ioredis@npm:5.8.2" resolution: "ioredis@npm:5.8.1"
dependencies: dependencies:
"@ioredis/commands": "npm:1.4.0" "@ioredis/commands": "npm:1.4.0"
cluster-key-slot: "npm:^1.1.0" cluster-key-slot: "npm:^1.1.0"
@@ -775,7 +774,7 @@ __metadata:
redis-errors: "npm:^1.2.0" redis-errors: "npm:^1.2.0"
redis-parser: "npm:^3.0.0" redis-parser: "npm:^3.0.0"
standard-as-callback: "npm:^2.1.0" standard-as-callback: "npm:^2.1.0"
checksum: 10c0/305e385f811d49908899e32c2de69616cd059f909afd9e0a53e54f596b1a5835ee3449bfc6a3c49afbc5a2fd27990059e316cc78f449c94024957bd34c826d88 checksum: 10c0/4ed66444017150da027bce940a24bf726994691e2a7b3aa11d52f8aeb37f258068cc171af4d9c61247acafc28eb086fa8a7c79420b8e8d2907d2f74f39584465
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1106,27 +1105,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"playwright-core@npm:1.57.0": "playwright-core@npm:1.55.1":
version: 1.57.0 version: 1.55.1
resolution: "playwright-core@npm:1.57.0" resolution: "playwright-core@npm:1.55.1"
bin: bin:
playwright-core: cli.js playwright-core: cli.js
checksum: 10c0/798e35d83bf48419a8c73de20bb94d68be5dde68de23f95d80a0ebe401e3b83e29e3e84aea7894d67fa6c79d2d3d40cc5bcde3e166f657ce50987aaa2421b6a9 checksum: 10c0/39837a8c1232ec27486eac8c3fcacc0b090acc64310f7f9004b06715370fc426f944e3610fe8c29f17cd3d68280ed72c75f660c02aa5b5cf0eb34bab0031308f
languageName: node languageName: node
linkType: hard linkType: hard
"playwright@npm:^1.57.0": "playwright@npm:^1.55.1":
version: 1.57.0 version: 1.55.1
resolution: "playwright@npm:1.57.0" resolution: "playwright@npm:1.55.1"
dependencies: dependencies:
fsevents: "npm:2.3.2" fsevents: "npm:2.3.2"
playwright-core: "npm:1.57.0" playwright-core: "npm:1.55.1"
dependenciesMeta: dependenciesMeta:
fsevents: fsevents:
optional: true optional: true
bin: bin:
playwright: cli.js playwright: cli.js
checksum: 10c0/ab03c99a67b835bdea9059f516ad3b6e42c21025f9adaa161a4ef6bc7ca716dcba476d287140bb240d06126eb23f889a8933b8f5f1f1a56b80659d92d1358899 checksum: 10c0/b84a97b0d764403df512f5bbb10c7343974e151a28202cc06f90883a13e8a45f4491a0597f0ae5fb03a026746cbc0d200f0f32195bfaa381aee5ca5770626771
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1161,15 +1160,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"raw-body@npm:^3.0.2": "raw-body@npm:^3.0.1":
version: 3.0.2 version: 3.0.1
resolution: "raw-body@npm:3.0.2" resolution: "raw-body@npm:3.0.1"
dependencies: dependencies:
bytes: "npm:~3.1.2" bytes: "npm:3.1.2"
http-errors: "npm:~2.0.1" http-errors: "npm:2.0.0"
iconv-lite: "npm:~0.7.0" iconv-lite: "npm:0.7.0"
unpipe: "npm:~1.0.0" unpipe: "npm:1.0.0"
checksum: 10c0/d266678d08e1e7abea62c0ce5864344e980fa81c64f6b481e9842c5beaed2cdcf975f658a3ccd67ad35fc919c1f6664ccc106067801850286a6cbe101de89f29 checksum: 10c0/892f4fbd21ecab7e2fed0f045f7af9e16df7e8050879639d4e482784a2f4640aaaa33d916a0e98013f23acb82e09c2e3c57f84ab97104449f728d22f65a7d79a
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1270,7 +1269,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"setprototypeof@npm:~1.2.0": "setprototypeof@npm:1.2.0":
version: 1.2.0 version: 1.2.0
resolution: "setprototypeof@npm:1.2.0" resolution: "setprototypeof@npm:1.2.0"
checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc
@@ -1368,10 +1367,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"statuses@npm:~2.0.2": "statuses@npm:2.0.1":
version: 2.0.2 version: 2.0.1
resolution: "statuses@npm:2.0.2" resolution: "statuses@npm:2.0.1"
checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1500,7 +1499,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"toidentifier@npm:~1.0.1": "toidentifier@npm:1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "toidentifier@npm:1.0.1" resolution: "toidentifier@npm:1.0.1"
checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1
@@ -1514,13 +1513,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "unique-filename@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "unique-filename@npm:4.0.0" resolution: "unique-filename@npm:4.0.0"
@@ -1539,7 +1531,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"unpipe@npm:~1.0.0": "unpipe@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "unpipe@npm:1.0.0" resolution: "unpipe@npm:1.0.0"
checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c

View File

@@ -1,5 +1,3 @@
import { defineConfig } from 'vite';
/** @type { import('@storybook/react-vite').StorybookConfig } */ /** @type { import('@storybook/react-vite').StorybookConfig } */
const config = { const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
@@ -7,38 +5,18 @@ const config = {
addons: [ addons: [
"@storybook/addon-themes", "@storybook/addon-themes",
"@storybook/addon-docs", "@storybook/addon-docs",
"@storybook/addon-vitest", "@storybook/addon-vitest"
], ],
core: {
builder: "@storybook/builder-vite",
options: {
viteConfigPath: "../vite.config.js",
},
},
framework: { framework: {
name: "@storybook/react-vite", name: "@storybook/react-vite",
options: { options: {},
// fastRefresh: false,
}
}, },
docs: {}, docs: {},
async viteFinal(config) {
return defineConfig({
...config,
plugins: [
...(config.plugins ?? []),
{
name: 'force-full-reload-always',
apply: 'serve',
enforce: 'post',
handleHotUpdate(ctx) {
ctx.server.ws.send({
type: 'full-reload',
path: '*',
});
// returning [] tells Vite: “no modules handled”
return [];
},
}
]
});
}
}; };
export default config; export default config;

View File

@@ -1,5 +1,6 @@
import { withThemeByClassName } from "@storybook/addon-themes"; import { withThemeByClassName } from "@storybook/addon-themes";
import Components from "@target/components"; import Components from "@target/components";
import translations from "@public/translation.en.js"; import translations from "@public/translation.en.js";
Components.setDefaultTranslations(translations); Components.setDefaultTranslations(translations);

View File

@@ -8,11 +8,6 @@
metosin/reitit-core {:mvn/version "0.9.1"} metosin/reitit-core {:mvn/version "0.9.1"}
funcool/okulary {:mvn/version "2022.04.11-16"} 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 funcool/potok2
{:git/tag "v2.2" {:git/tag "v2.2"
:git/sha "0f7e15a" :git/sha "0f7e15a"
@@ -50,7 +45,7 @@
{thheller/shadow-cljs {:mvn/version "3.2.2"} {thheller/shadow-cljs {:mvn/version "3.2.2"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"} com.bhauman/rebel-readline {:mvn/version "RELEASE"}
org.clojure/tools.namespace {: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"}}} cider/cider-nrepl {:mvn/version "0.57.0"}}}
:shadow-cljs :shadow-cljs

View File

@@ -32,8 +32,8 @@
"e2e:server": "node ./scripts/e2e-server.js", "e2e:server": "node ./scripts/e2e-server.js",
"fmt:clj": "cljfmt fix --parallel=true src/ test/", "fmt:clj": "cljfmt fix --parallel=true src/ test/",
"fmt:clj:check": "cljfmt check --parallel=false 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": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -w",
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js text-editor/**/*.js", "fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js",
"lint:clj": "clj-kondo --parallel --lint src/", "lint:clj": "clj-kondo --parallel --lint src/",
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss", "lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w", "lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
@@ -47,81 +47,89 @@
"watch:app:libs": "node ./scripts/build-libs.js --watch", "watch:app:libs": "node ./scripts/build-libs.js --watch",
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook", "watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
"clear:shadow-cache": "rm -rf .shadow-cljs", "clear:shadow-cache": "rm -rf .shadow-cljs",
"watch": "exit 0", "watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
"watch:app": "yarn run clear:shadow-cache && concurrently --kill-others-on-fail \"yarn run watch:app:assets\" \"yarn run watch:app:main\" \"yarn run watch:app:libs\"", "watch": "yarn run watch:app:assets",
"watch:storybook": "yarn run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\"" "watch:storybook": "yarn run build:storybook:assets && concurrently \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"",
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
}, },
"devDependencies": { "devDependencies": {
"@penpot/draft-js": "portal:./packages/draft-js", "@playwright/test": "1.52.0",
"@penpot/mousetrap": "portal:./packages/mousetrap", "@storybook/addon-docs": "10.0.4",
"@penpot/plugins-runtime": "1.3.2", "@storybook/addon-themes": "10.0.4",
"@penpot/svgo": "penpot/svgo#v3.2", "@storybook/addon-vitest": "10.0.4",
"@penpot/text-editor": "portal:./text-editor", "@storybook/react-vite": "10.0.4",
"@playwright/test": "1.57.0", "@types/node": "^22.15.21",
"@storybook/addon-docs": "10.1.11", "@vitest/browser": "3.2.4",
"@storybook/addon-themes": "10.1.11", "@vitest/coverage-v8": "3.2.4",
"@storybook/addon-vitest": "10.1.11",
"@storybook/react-vite": "10.1.11",
"@tokens-studio/sd-transforms": "1.2.11",
"@types/node": "^22.19.3",
"@vitest/browser": "4.0.16",
"@vitest/browser-playwright": "^4.0.16",
"@vitest/coverage-v8": "4.0.16",
"@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", "autoprefixer": "^10.4.21",
"compression": "^1.8.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"date-fns": "^4.1.0",
"esbuild": "^0.25.9", "esbuild": "^0.25.9",
"eventsource-parser": "^3.0.6",
"express": "^5.1.0", "express": "^5.1.0",
"fancy-log": "^2.0.0", "fancy-log": "^2.0.0",
"getopts": "^2.3.0", "getopts": "^2.3.0",
"gettext-parser": "^8.0.0", "gettext-parser": "^8.0.0",
"highlight.js": "^11.10.0", "gulp-concat": "^2.6.1",
"js-beautify": "^1.15.4", "gulp-gzip": "^1.4.2",
"jsdom": "^27.4.0", "gulp-mustache": "^5.0.0",
"lodash": "^4.17.21", "gulp-postcss": "^10.0.0",
"lodash.debounce": "^4.0.8", "gulp-rename": "^2.0.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-svg-sprite": "^2.0.3",
"jsdom": "^27.0.0",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"marked": "^15.0.12", "marked": "^15.0.12",
"mkdirp": "^3.0.1", "mkdirp": "^3.0.1",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"opentype.js": "^1.3.4",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
"playwright": "1.56.1", "playwright": "1.56.1",
"postcss": "^8.5.4", "postcss": "^8.5.4",
"postcss-clean": "^1.2.2", "postcss-clean": "^1.2.2",
"postcss-modules": "^6.0.1",
"prettier": "3.5.3", "prettier": "3.5.3",
"pretty-time": "^1.1.0", "pretty-time": "^1.1.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"rimraf": "^6.0.1",
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"storybook": "10.0.4",
"svg-sprite": "^2.0.4",
"typescript": "^5.9.2",
"vite": "^6.3.5",
"vitest": "^3.2.0",
"wasm-pack": "^0.13.1",
"watcher": "^2.3.1",
"workerpool": "^9.3.2"
},
"dependencies": {
"@penpot/draft-js": "portal:./vendor/draft-js",
"@penpot/hljs": "portal:./vendor/hljs",
"@penpot/mousetrap": "portal:./vendor/mousetrap",
"@penpot/plugins-runtime": "1.3.2",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "portal:./text-editor",
"@tokens-studio/sd-transforms": "1.2.11",
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
"compression": "^1.8.1",
"date-fns": "^4.1.0",
"eventsource-parser": "^3.0.6",
"js-beautify": "^1.15.4",
"lodash": "^4.17.21",
"lodash.debounce": "^4.0.8",
"opentype.js": "^1.3.4",
"postcss-modules": "^6.0.1",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
"react": "19.1.1", "react": "19.1.1",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-virtualized": "^9.22.6", "react-virtualized": "^9.22.6",
"rimraf": "^6.0.1",
"rxjs": "8.0.0-alpha.14", "rxjs": "8.0.0-alpha.14",
"sass": "^1.89.0",
"sass-embedded": "^1.89.0",
"sax": "^1.4.1", "sax": "^1.4.1",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"storybook": "10.1.11",
"style-dictionary": "5.0.0-rc.1", "style-dictionary": "5.0.0-rc.1",
"svg-sprite": "^2.0.4",
"tdigest": "^0.1.2", "tdigest": "^0.1.2",
"tinycolor2": "^1.6.0", "tinycolor2": "^1.6.0",
"typescript": "^5.9.2",
"ua-parser-js": "2.0.5", "ua-parser-js": "2.0.5",
"vite": "^7.3.0",
"vitest": "^4.0.16",
"wait-on": "^9.0.3",
"wasm-pack": "^0.13.1",
"watcher": "^2.3.1",
"workerpool": "^9.3.2",
"xregexp": "^5.1.2" "xregexp": "^5.1.2"
} }
} }

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
export class Clipboard { export class Clipboard {
static Permission = { static Permission = {
ONLY_READ: ["clipboard-read"], ONLY_READ: ['clipboard-read'],
ONLY_WRITE: ["clipboard-write"], ONLY_WRITE: ['clipboard-write'],
ALL: ["clipboard-read", "clipboard-write"], ALL: ['clipboard-read', 'clipboard-write']
}; }
static enable(context, permissions) { static enable(context, permissions) {
return context.grantPermissions(permissions); return context.grantPermissions(permissions)
} }
static writeText(page, text) { static writeText(page, text) {
@@ -18,8 +18,8 @@ export class Clipboard {
} }
constructor(page, context) { constructor(page, context) {
this.page = page; this.page = page
this.context = context; this.context = context
} }
enable(permissions) { enable(permissions) {

View File

@@ -1,16 +1,18 @@
export class Transit { export class Transit {
static parse(value) { static parse(value) {
if (typeof value !== "string") return value; if (typeof value !== 'string')
return value
if (value.startsWith("~")) return value.slice(2); if (value.startsWith('~'))
return value.slice(2)
return value; return value
} }
static get(object, ...path) { static get(object, ...path) {
let aux = object; let aux = object;
for (const name of path) { for (const name of path) {
if (typeof name !== "string") { if (typeof name !== 'string') {
if (!(name in aux)) { if (!(name in aux)) {
return undefined; return undefined;
} }

View File

@@ -9,7 +9,7 @@ export class BasePage {
*/ */
static async mockRPCs(page, paths, options) { static async mockRPCs(page, paths, options) {
for (const [path, jsonFilename] of Object.entries(paths)) { for (const [path, jsonFilename] of Object.entries(paths)) {
await this.mockRPC(page, path, jsonFilename, options); await this.mockRPC(page, path, jsonFilename, options)
} }
} }

View File

@@ -106,13 +106,6 @@ export class DashboardPage extends BaseWebSocketPage {
); );
} }
async setupDeletedFiles() {
await this.mockRPC(
"get-team-deleted-files?team-id=*",
"dashboard/get-team-deleted-files.json",
);
}
async setupDrafts() { async setupDrafts() {
await this.mockRPC( await this.mockRPC(
"get-project-files?project-id=*", "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("search-files", "dashboard/search-files.json");
await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.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() { async setupAccessTokensEmpty() {
@@ -300,13 +289,6 @@ export class DashboardPage extends BaseWebSocketPage {
await expect(this.mainHeading).toHaveText("Libraries"); 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() { async openProfileMenu() {
await this.userAccount.click(); await this.userAccount.click();
} }

View File

@@ -1,7 +1,7 @@
import { expect } from "@playwright/test"; import { expect } from "@playwright/test";
import { readFile } from "node:fs/promises"; import { readFile } from 'node:fs/promises';
import { BaseWebSocketPage } from "./BaseWebSocketPage"; import { BaseWebSocketPage } from "./BaseWebSocketPage";
import { Transit } from "../../helpers/Transit"; import { Transit } from '../../helpers/Transit';
export class WorkspacePage extends BaseWebSocketPage { export class WorkspacePage extends BaseWebSocketPage {
static TextEditor = class TextEditor { static TextEditor = class TextEditor {

View File

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

View File

@@ -51,7 +51,7 @@ test.skip("BUG 12164 - Crash when trying to fetch a missing font", async ({
pageId: "2b7f0188-51a1-8193-8006-e05bad87b74d", pageId: "2b7f0188-51a1-8193-8006-e05bad87b74d",
}); });
await workspacePage.page.waitForTimeout(1000); await workspacePage.page.waitForTimeout(1000)
await workspacePage.waitForFirstRender(); await workspacePage.waitForFirstRender();
await expect( await expect(

View File

@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { Clipboard } from "../../helpers/Clipboard"; import { Clipboard } from '../../helpers/Clipboard';
import { WorkspacePage } from "../pages/WorkspacePage"; import { WorkspacePage } from "../pages/WorkspacePage";
const timeToWait = 100; const timeToWait = 100;
@@ -11,14 +11,14 @@ test.beforeEach(async ({ page, context }) => {
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]); await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
}); });
test.afterEach(async ({ context }) => { test.afterEach(async ({ context}) => {
context.clearPermissions(); context.clearPermissions();
}); })
test("Create a new text shape", async ({ page }) => { test("Create a new text shape", async ({ page }) => {
const initialText = "Lorem ipsum"; const initialText = "Lorem ipsum";
const workspace = new WorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.goToWorkspace(); await workspace.goToWorkspace();
@@ -36,7 +36,10 @@ test("Create a new text shape from pasting text", async ({ page, context }) => {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json"); await workspace.mockRPC(
"update-file?id=*",
"text-editor/update-file.json",
);
await workspace.goToWorkspace(); await workspace.goToWorkspace();
await Clipboard.writeText(page, textToPaste); await Clipboard.writeText(page, textToPaste);
@@ -52,13 +55,10 @@ test("Create a new text shape from pasting text", async ({ page, context }) => {
await workspace.textEditor.stopEditing(); await workspace.textEditor.stopEditing();
}); });
test("Create a new text shape from pasting text using context menu", async ({ test("Create a new text shape from pasting text using context menu", async ({ page, context }) => {
page,
context,
}) => {
const textToPaste = "Lorem ipsum"; const textToPaste = "Lorem ipsum";
const workspace = new WorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.goToWorkspace(); await workspace.goToWorkspace();
@@ -72,13 +72,11 @@ test("Create a new text shape from pasting text using context menu", async ({
expect(textContent).toBe(textToPaste); expect(textContent).toBe(textToPaste);
await workspace.textEditor.stopEditing(); await workspace.textEditor.stopEditing();
}); })
test("Update an already created text shape by appending text", async ({ test("Update an already created text shape by appending text", async ({ page }) => {
page,
}) => {
const workspace = new WorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json"); await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
@@ -96,7 +94,7 @@ test("Update an already created text shape by prepending text", async ({
page, page,
}) => { }) => {
const workspace = new WorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json"); await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
@@ -114,7 +112,7 @@ test("Update an already created text shape by inserting text in between", async
page, page,
}) => { }) => {
const workspace = new WorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json"); await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
@@ -128,13 +126,10 @@ test("Update an already created text shape by inserting text in between", async
await workspace.textEditor.stopEditing(); await workspace.textEditor.stopEditing();
}); });
test("Update a new text shape appending text by pasting text", async ({ test("Update a new text shape appending text by pasting text", async ({ page, context }) => {
page,
context,
}) => {
const textToPaste = " dolor sit amet"; const textToPaste = " dolor sit amet";
const workspace = new WorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json"); await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
@@ -152,12 +147,11 @@ test("Update a new text shape appending text by pasting text", async ({
}); });
test("Update a new text shape prepending text by pasting text", async ({ test("Update a new text shape prepending text by pasting text", async ({
page, page, context
context,
}) => { }) => {
const textToPaste = "Dolor sit amet "; const textToPaste = "Dolor sit amet ";
const workspace = new WorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json"); await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
@@ -179,7 +173,7 @@ test("Update a new text shape replacing (starting) text with pasted text", async
}) => { }) => {
const textToPaste = "Dolor sit amet"; const textToPaste = "Dolor sit amet";
const workspace = new WorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json"); await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
@@ -203,7 +197,7 @@ test("Update a new text shape replacing (ending) text with pasted text", async (
}) => { }) => {
const textToPaste = "dolor sit amet"; const textToPaste = "dolor sit amet";
const workspace = new WorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json"); await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
@@ -227,7 +221,7 @@ test("Update a new text shape replacing (in between) text with pasted text", asy
}) => { }) => {
const textToPaste = "dolor sit amet"; const textToPaste = "dolor sit amet";
const workspace = new WorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json"); await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
@@ -250,11 +244,14 @@ test("Update text font size selecting a part of it (starting)", async ({
page, page,
}) => { }) => {
const workspace = new WorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json"); await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json"); await workspace.mockRPC(
"update-file?id=*",
"text-editor/update-file.json",
);
await workspace.goToWorkspace(); await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum"); await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing(); await workspace.textEditor.startEditing();
@@ -283,10 +280,7 @@ test.skip("Update text line height selecting a part of it (starting)", async ({
await workspace.textEditor.selectFromStart(5); await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeLineHeight(1.4); await workspace.textEditor.changeLineHeight(1.4);
const lineHeight = await workspace.textEditor.waitForParagraphStyle( const lineHeight = await workspace.textEditor.waitForParagraphStyle(1, 'line-height');
1,
"line-height",
);
expect(lineHeight).toBe("1.4"); expect(lineHeight).toBe("1.4");
const textContent = await workspace.textEditor.waitForTextSpanContent(); const textContent = await workspace.textEditor.waitForTextSpanContent();

View File

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

View File

@@ -74,7 +74,7 @@ export function isJsFile(path) {
export async function compileSass(worker, path, options) { export async function compileSass(worker, path, options) {
path = ph.resolve(path); path = ph.resolve(path);
// log.info("compile:", path); log.info("compile:", path);
return worker.exec("compileSass", [path, options]); return worker.exec("compileSass", [path, options]);
} }
@@ -187,7 +187,7 @@ async function readManifestFile(resource) {
return JSON.parse(content); return JSON.parse(content);
} }
async function generateManifest() { async function readShadowManifest() {
const index = { const index = {
app_main: "./js/main.js", app_main: "./js/main.js",
render_main: "./js/render.js", render_main: "./js/render.js",
@@ -197,7 +197,6 @@ async function generateManifest() {
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION, polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
libs: "./js/libs.js?version=" + CURRENT_VERSION, libs: "./js/libs.js?version=" + CURRENT_VERSION,
worker_main: "./js/worker/main.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({ importmap: JSON.stringify({
"imports": { "imports": {
@@ -277,7 +276,6 @@ export async function compileTranslations() {
"id", "id",
"ru", "ru",
"tr", "tr",
"hi",
"zh_CN", "zh_CN",
"zh_Hant", "zh_Hant",
"hr", "hr",
@@ -393,7 +391,7 @@ async function generateTemplates() {
const isDebug = process.env.NODE_ENV !== "production"; const isDebug = process.env.NODE_ENV !== "production";
await fs.mkdir("./resources/public/", { recursive: true }); await fs.mkdir("./resources/public/", { recursive: true });
const manifest = await generateManifest(); const manifest = await readShadowManifest();
let content; let content;
const iconsSprite = await fs.readFile( const iconsSprite = await fs.readFile(

View File

@@ -20,26 +20,25 @@ echo $PATH
set -ex set -ex
corepack enable; corepack enable;
corepack install; corepack install || exit 1;
yarn install || exit 1; yarn install || exit 1;
rm -rf target/dist;
rm -rf resources/public; rm -rf resources/public;
rm -rf target/dist;
mkdir -p resources/public; mkdir -p resources/public;
mkdir -p target/dist; mkdir -p target/dist;
pushd ../render-wasm;
./build
popd
yarn run build:app:main $EXTRA_PARAMS; yarn run build:app:main $EXTRA_PARAMS;
yarn run build:app:libs;
yarn run build:app:assets;
sed -i "s/\.\/render.js/.\/render.js?version=$CURRENT_VERSION/g" resources/public/js/worker/main*.js if [ "$INCLUDE_WASM" = "yes" ]; then
yarn run build:wasm || exit 1;
fi
rsync -avr resources/public/ target/dist/ yarn run build:app:libs || exit 1;
yarn run build:app:assets || exit 1;
rsync -avr resources/public/ target/dist/;
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
# build storybook # build storybook

View File

@@ -7,4 +7,7 @@ yarn install;
yarn run playwright install chromium --with-deps; yarn run playwright install chromium --with-deps;
yarn run build:storybook yarn run build:storybook
yarn run test:storybook
exec npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on tcp:6006 && yarn test:storybook"

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env bash
TARGET=${1:-app};
set -ex
exec yarn run watch:$TARGET

View File

@@ -92,7 +92,7 @@
{:main {:main
{:entries [app.worker] {:entries [app.worker]
:web-worker true :web-worker true
:prepend-js "importScripts('./render.js');" :prepend-js "importScripts('/js/worker/render.js');"
:depends-on #{}}} :depends-on #{}}}
:js-options :js-options
@@ -121,22 +121,24 @@
:storybook :storybook
{:target :esm {:target :esm
:output-dir "target/storybook/" :output-dir "target/storybook/"
:devtools {:enabled false :devtools {:enabled false}
:console-support false}
:js-options :js-options
{:js-provider :import {:js-provider :import
:entry-keys ["module" "browser" "main"] :entry-keys ["module" "browser" "main"]
:export-conditions ["module" "import", "browser" "require" "default"]} :export-conditions ["module" "import", "browser" "require" "default"]}
:modules :modules
{:components {:base
{:entries []}
:components
{:exports {default app.main.ui.ds/default {:exports {default app.main.ui.ds/default
helpers app.main.ui.ds.helpers/default} helpers app.main.ui.ds.helpers/default}
:prepend-js ";(globalThis.goog.provide = globalThis.goog.constructNamespace_);(globalThis.goog.require = globalThis.goog.module.get);" :prepend-js ";(globalThis.goog.provide = globalThis.goog.constructNamespace_);(globalThis.goog.require = globalThis.goog.module.get);"
:depends-on #{}}} :depends-on #{:base}}}
:compiler-options :compiler-options
{:output-feature-set :es-next {:output-feature-set :es2020
:output-wrapper false :output-wrapper false
:warnings {:fn-deprecated false}}} :warnings {:fn-deprecated false}}}

View File

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

View File

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

View File

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

View File

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

View File

@@ -98,7 +98,9 @@
(def context (def context
(atom (d/without-nils (collect-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 ;; --- EVENT TRANSLATION

View File

@@ -24,8 +24,6 @@
(def revn-data (atom {})) (def revn-data (atom {}))
(def queue-conj (fnil conj #queue [])) (def queue-conj (fnil conj #queue []))
(def force-persist? #(= % ::force-persist))
(defn- update-status (defn- update-status
[status] [status]
(ptk/reify ::update-status (ptk/reify ::update-status

View File

@@ -32,7 +32,7 @@
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf] [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.plugins :as dp]
[app.main.data.profile :as du] [app.main.data.profile :as du]
[app.main.data.project :as dpj] [app.main.data.project :as dpj]
@@ -67,7 +67,6 @@
[app.main.errors] [app.main.errors]
[app.main.features :as features] [app.main.features :as features]
[app.main.features.pointer-map :as fpmap] [app.main.features.pointer-map :as fpmap]
[app.main.refs :as refs]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.router :as rt] [app.main.router :as rt]
[app.render-wasm :as wasm] [app.render-wasm :as wasm]
@@ -270,12 +269,8 @@
(ptk/reify ::process-wasm-object (ptk/reify ::process-wasm-object
ptk/EffectEvent ptk/EffectEvent
(effect [_ state _] (effect [_ state _]
(let [objects (dsh/lookup-page-objects state) (let [objects (dsh/lookup-page-objects state)]
shape (get objects id)] (wasm.api/process-object (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))))))
(defn initialize-workspace (defn initialize-workspace
[team-id file-id] [team-id file-id]
@@ -384,61 +379,6 @@
(->> (rx/from added) (->> (rx/from added)
(rx/map process-wasm-object))))))) (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}]})))]
(when (d/not-empty? changes)
(dch/commit-changes
{:redo-changes changes :undo-changes []
:save-undo? false
:tags #{:position-data}})))))
(rx/take-until stoper-s))))
(->> stream (->> stream
(rx/filter dch/commit?) (rx/filter dch/commit?)
(rx/map deref) (rx/map deref)

View File

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

View File

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

View File

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

View File

@@ -30,9 +30,6 @@
(def profile (def profile
(l/derived (l/key :profile) st/state)) (l/derived (l/key :profile) st/state))
(def current-page-id
(l/derived (l/key :current-page-id) st/state))
(def team (def team
(l/derived (fn [state] (l/derived (fn [state]
(let [team-id (:current-team-id state) (let [team-id (:current-team-id state)
@@ -636,6 +633,3 @@
(def persistence-state (def persistence-state
(l/derived (comp :status :persistence) st/state)) (l/derived (comp :status :persistence) st/state))
(def progress
(l/derived :progress st/state))

View File

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

View File

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

View File

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

View File

@@ -1,103 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.components.progress
"Assets exportation common components."
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.types.color :as clr]
[app.main.data.common :as dcm]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as deprecated-icons]
[app.util.i18n :as i18n :refer [tr]]
[app.util.theme :as theme]
[rumext.v2 :as mf]))
(def ^:private neutral-icon
(deprecated-icons/icon-xref :msg-neutral (stl/css :icon)))
(def ^:private error-icon
(deprecated-icons/icon-xref :delete-text (stl/css :icon)))
(def ^:private close-icon
(deprecated-icons/icon-xref :close (stl/css :close-icon)))
(mf/defc progress-notification-widget*
[]
(let [state (mf/deref refs/progress)
profile (mf/deref refs/profile)
theme (get profile :theme theme/default)
default-theme? (= theme/default theme)
error? (:error state)
healthy? (:healthy state)
visible? (:visible state)
progress (:progress state)
hint (:hint state)
total (:total state)
pwidth
(if error?
280
(/ (* progress 280) total))
color
(cond
error? clr/new-danger
healthy? (if default-theme?
clr/new-primary
clr/new-primary-light)
(not healthy?) clr/new-warning)
background-clr
(if default-theme?
clr/background-quaternary
clr/background-quaternary-light)
toggle-detail-visibility
(mf/use-fn
(fn []
(st/emit! (dcm/toggle-progress-visibility))))]
[:*
(when visible?
[:div {:class (stl/css-case :progress-modal true
:has-error error?)}
(if error?
error-icon
neutral-icon)
[:div {:class (stl/css :title)}
[:div {:class (stl/css :title-text)} hint]
(if error?
[:button {:class (stl/css :retry-btn)
;; :on-click retry-last-operation
}
(tr "labels.retry")]
[:span {:class (stl/css :progress)}
(dm/str progress " / " total)])]
[:button {:class (stl/css :progress-close-button)
:on-click toggle-detail-visibility}
close-icon]
(when-not error?
[:svg {:class (stl/css :progress-bar)
:height 4
:width 280}
[:g
[:path {:d "M0 0 L280 0"
:stroke background-clr
:stroke-width 30}]
[:path {:d (dm/str "M0 0 L280 0")
:stroke color
:stroke-width 30
:fill "transparent"
:stroke-dasharray 280
:stroke-dashoffset (- 280 pwidth)
:style {:transition "stroke-dashoffset 1s ease-in-out"}}]]])])]))

View File

@@ -1,101 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
// PROGRESS WIDGET
.progress-widget {
@include deprecated.flexCenter;
width: deprecated.$s-28;
height: deprecated.$s-28;
}
// PROGRESS MODAL
.progress-modal {
--export-modal-bg-color: var(--alert-background-color-default);
--export-modal-fg-color: var(--alert-text-foreground-color-default);
--export-modal-icon-color: var(--alert-icon-foreground-color-default);
--export-modal-border-color: var(--alert-border-color-default);
position: absolute;
right: deprecated.$s-16;
top: deprecated.$s-48;
display: grid;
grid-template-columns: deprecated.$s-24 1fr deprecated.$s-24;
grid-template-areas:
"icon text close"
"bar bar bar";
gap: deprecated.$s-4 deprecated.$s-8;
padding-block-start: deprecated.$s-8;
background-color: var(--export-modal-bg-color);
border: deprecated.$s-1 solid var(--export-modal-border-color);
border-radius: deprecated.$br-8;
z-index: deprecated.$z-index-modal;
overflow: hidden;
}
.has-error {
--export-modal-bg-color: var(--alert-background-color-error);
--export-modal-fg-color: var(--alert-text-foreground-color-error);
--export-modal-icon-color: var(--alert-icon-foreground-color-error);
--export-modal-border-color: var(--alert-border-color-error);
grid-template-areas: "icon text close";
gap: deprecated.$s-8;
padding-block: deprecated.$s-8;
}
.icon {
@extend .button-icon;
grid-area: icon;
align-self: center;
margin-inline-start: deprecated.$s-8;
stroke: var(--export-modal-icon-color);
}
.title {
@include deprecated.bodyMediumTypography;
display: grid;
grid-template-columns: auto 1fr;
gap: deprecated.$s-8;
grid-area: text;
align-self: center;
padding: 0;
margin: 0;
color: var(--export-modal-fg-color);
}
.progress {
@include deprecated.bodyMediumTypography;
padding-left: deprecated.$s-8;
margin: 0;
align-self: center;
color: var(--modal-text-foreground-color);
}
.retry-btn {
@include deprecated.buttonStyle;
@include deprecated.bodySmallTypography;
display: inline;
text-align: left;
color: var(--modal-link-foreground-color);
margin: 0;
padding: 0;
}
.progress-close-button {
@include deprecated.buttonStyle;
padding: 0;
margin-inline-end: deprecated.$s-8;
}
.close-icon {
@extend .button-icon;
stroke: var(--export-modal-icon-color);
}
.progress-bar {
margin-top: 0;
grid-area: bar;
}

View File

@@ -60,7 +60,6 @@
current-id (get state :id) current-id (get state :id)
current-value (get state :current-value) current-value (get state :current-value)
current-label (get label-index current-value) current-label (get label-index current-value)
is-open? (get state :is-open?) is-open? (get state :is-open?)
node-ref (mf/use-ref nil) node-ref (mf/use-ref nil)

View File

@@ -19,9 +19,7 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.router :as rt] [app.main.router :as rt]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.progress :refer [progress-notification-widget*]]
[app.main.ui.context :as ctx] [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.files :refer [files-section*]]
[app.main.ui.dashboard.fonts :refer [fonts-page* font-providers-page*]] [app.main.ui.dashboard.fonts :refer [fonts-page* font-providers-page*]]
[app.main.ui.dashboard.import] [app.main.ui.dashboard.import]
@@ -75,13 +73,7 @@
show-templates? show-templates?
(and (contains? cf/flags :dashboard-templates-section) (and (contains? cf/flags :dashboard-templates-section)
(:can-edit permissions)) (:can-edit permissions))]
show-deleted? (:can-edit permissions)
section (if (and (not show-deleted?) (= section :dashboard-deleted))
:dashboard-recent
section)]
(mf/with-effect [] (mf/with-effect []
(let [key1 (events/listen js/window "resize" on-resize)] (let [key1 (events/listen js/window "resize" on-resize)]
@@ -92,9 +84,6 @@
[:div {:class (stl/css :dashboard-content) [:div {:class (stl/css :dashboard-content)
:on-click clear-selected-fn :on-click clear-selected-fn
:ref container} :ref container}
[:> progress-notification-widget*]
(case section (case section
:dashboard-recent :dashboard-recent
(when (seq projects) (when (seq projects)
@@ -151,11 +140,6 @@
:dashboard-settings :dashboard-settings
[:> team-settings-page* {:team team :profile profile}] [:> team-settings-page* {:team team :profile profile}]
:dashboard-deleted
[:> deleted-section* {:team team
:projects projects
:profile profile}]
nil)])) nil)]))
(def ref:dashboard-initialized (def ref:dashboard-initialized

View File

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

View File

@@ -1,139 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
@use "common/refactor/common-dashboard";
@use "../ds/typography.scss" as t;
@use "../ds/_borders.scss" as *;
@use "../ds/spacing.scss" as *;
@use "../ds/_sizes.scss" as *;
@use "../ds/z-index.scss" as *;
.dashboard-container {
flex: 1 0 0;
width: 100%;
margin-inline-end: var(--sp-l);
border-top: $b-1 solid var(--panel-border-color);
overflow-y: auto;
padding-block-end: var(--sp-xxxl);
}
.deleted-info-content {
display: flex;
justify-content: space-between;
padding: var(--sp-s) var(--sp-xxl) var(--sp-s) var(--sp-xxl);
}
.deleted-info {
display: block;
height: fit-content;
color: var(--color-foreground-secondary);
@include t.use-typography("body-large");
line-height: 0.8;
height: var(--sp-xl);
}
.info-text-highlight {
color: var(--color-accent-primary);
}
.deleted-options {
display: flex;
gap: 5px;
flex-shrink: 0;
}
.nav {
background: var(--color-background-default);
padding: var(--sp-xxl) var(--sp-xxl) var(--sp-s) var(--sp-xxl);
position: sticky;
top: 0;
// We need to use the the deprecated z-index so it won't clash with the dashboard
// onboarding modals
z-index: deprecated.$z-index-3;
}
.nav-inside {
border-bottom: $b-1 solid var(--panel-border-color);
display: flex;
gap: var(--sp-l);
justify-content: space-between;
}
.nav-option {
color: var(--color-foreground-secondary);
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border: $b-1 solid transparent;
cursor: pointer;
}
.selected {
color: var(--color-foreground-primary);
border-bottom: $b-1 solid var(--color-foreground-primary);
}
.project {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
width: 99%;
max-height: $sz-40;
padding: var(--sp-s) var(--sp-s) var(--sp-s) var(--sp-l);
margin-block-start: var(--sp-l);
border-radius: $br-4;
}
.project-name-wrapper {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
min-height: var(--sp-xxxl);
margin-inline-start: var(--sp-s);
}
.project-name {
@include t.use-typography("body-large");
width: fit-content;
margin-inline-end: var(--sp-m);
line-height: 0.8;
color: var(--title-foreground-color-hover);
height: var(--sp-l);
}
.project-actions {
display: flex;
opacity: var(--actions-opacity);
margin-inline-start: var(--sp-xxxl);
}
.add-file-btn,
.options-btn {
@extend .button-tertiary;
height: var(--sp-xxxl);
width: var(--sp-xxxl);
margin: 0 var(--sp-s);
padding: var(--sp-s);
}
.info-wrapper {
display: flex;
align-items: center;
gap: var(--sp-s);
}
.add-icon,
.menu-icon {
@extend .button-icon;
stroke: var(--icon-foreground);
}

View File

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

View File

@@ -360,9 +360,7 @@
(st/emit! (modal/show options)))))] (st/emit! (modal/show options)))))]
[:div {:class (stl/css :font-item :table-row)} [:div {:class (stl/css :font-item :table-row)}
[:div {:class (stl/css-case :table-field true [:div {:class (stl/css :table-field :family)}
:family true
:is-edition edition?)}
(if ^boolean edition? (if ^boolean edition?
[:input {:type "text" [:input {:type "text"
:auto-focus true :auto-focus true

View File

@@ -126,9 +126,6 @@
@include twoLineTextEllipsis; @include twoLineTextEllipsis;
min-width: $sz-200; min-width: $sz-200;
width: $sz-200; width: $sz-200;
&.is-edition {
overflow: visible;
}
} }
> .filenames { > .filenames {

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,11 +27,11 @@
[app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.project-menu :refer [project-menu*]] [app.main.ui.dashboard.project-menu :refer [project-menu*]]
[app.main.ui.dashboard.subscription :refer [dashboard-cta* [app.main.ui.dashboard.subscription :refer [subscription-sidebar*
get-subscription-type
menu-team-icon* menu-team-icon*
dashboard-cta*
show-subscription-dashboard-banner? show-subscription-dashboard-banner?
subscription-sidebar*]] get-subscription-type]]
[app.main.ui.dashboard.team-form] [app.main.ui.dashboard.team-form]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.icons :as deprecated-icon] [app.main.ui.icons :as deprecated-icon]

View File

@@ -45,7 +45,7 @@
.element-list { .element-list {
@include t.use-typography("body-large"); @include t.use-typography("body-large");
color: var(--modal-text-foreground-color); color: var(--modal-text-foreground-color);
overflow-y: auto; overflow-y: scroll;
margin-block: 0; margin-block: 0;
} }

View File

@@ -32,13 +32,19 @@
min-width: var(--sp-l); min-width: var(--sp-l);
} }
// TODO: Review if we need other type of button, so we don't need important here
.invisible-button { .invisible-button {
position: absolute;
right: 4px;
top: 4px;
opacity: var(--opacity-button); opacity: var(--opacity-button);
background-color: var(--color-background-quaternary) !important;
&:hover { &:hover {
background-color: var(--color-background-quaternary);
--opacity-button: 1; --opacity-button: 1;
} }
&:focus { &:focus {
background-color: var(--color-background-quaternary);
--opacity-button: 1; --opacity-button: 1;
} }
} }

View File

@@ -80,7 +80,7 @@
[:div {:class (stl/css :pill-dot)}])]] [:div {:class (stl/css :pill-dot)}])]]
(when-not ^boolean disabled (when-not ^boolean disabled
[:> icon-button* {:variant "action" [:> icon-button* {:variant "ghost"
:class (stl/css :invisible-button) :class (stl/css :invisible-button)
:icon i/broken-link :icon i/broken-link
:ref token-detach-btn-ref :ref token-detach-btn-ref

View File

@@ -8,6 +8,7 @@
@use "ds/_sizes.scss" as *; @use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as t; @use "ds/typography.scss" as t;
@use "ds/colors.scss" as *; @use "ds/colors.scss" as *;
@use "ds/mixins.scss" as *;
.token-field { .token-field {
--token-field-bg-color: var(--color-background-tertiary); --token-field-bg-color: var(--color-background-tertiary);
@@ -16,9 +17,8 @@
--token-field-outline-color: none; --token-field-outline-color: none;
--token-field-height: var(--sp-xxxl); --token-field-height: var(--sp-xxxl);
--token-field-margin: unset; --token-field-margin: unset;
display: grid; display: grid;
grid-template-columns: 1fr auto; width: inherit;
column-gap: var(--sp-xs); column-gap: var(--sp-xs);
align-items: center; align-items: center;
position: relative; position: relative;
@@ -27,6 +27,7 @@
border-radius: $br-8; border-radius: $br-8;
padding: var(--sp-xs); padding: var(--sp-xs);
outline: $b-1 solid var(--token-field-outline-color); outline: $b-1 solid var(--token-field-outline-color);
position: relative;
&:hover { &:hover {
--token-field-bg-color: var(--color-background-quaternary); --token-field-bg-color: var(--color-background-quaternary);
@@ -39,7 +40,7 @@
} }
.with-icon { .with-icon {
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr;
} }
.token-field-disabled { .token-field-disabled {
@@ -57,6 +58,8 @@
--pill-bg-color: var(--color-background-tertiary); --pill-bg-color: var(--color-background-tertiary);
--pill-fg-color: var(--color-token-foreground); --pill-fg-color: var(--color-token-foreground);
@include t.use-typography("code-font"); @include t.use-typography("code-font");
@include textEllipsis;
display: block;
height: var(--sp-xxl); height: var(--sp-xxl);
width: fit-content; width: fit-content;
background: var(--pill-bg-color); background: var(--pill-bg-color);
@@ -65,6 +68,7 @@
color: var(--pill-fg-color); color: var(--pill-fg-color);
border-radius: $br-6; border-radius: $br-6;
padding-inline: $sz-6; padding-inline: $sz-6;
max-width: 100%;
&:hover { &:hover {
--pill-bg-color: var(--color-token-background); --pill-bg-color: var(--color-token-background);
--pill-fg-color: var(--color-foreground-primary); --pill-fg-color: var(--color-foreground-primary);
@@ -115,6 +119,9 @@
} }
.invisible-button { .invisible-button {
position: absolute;
right: 0;
top: 0;
opacity: var(--opacity-button); opacity: var(--opacity-button);
&:hover { &:hover {

View File

@@ -159,4 +159,6 @@ $arrow-side: 12px;
block-size: fit-content; block-size: fit-content;
inline-size: fit-content; inline-size: fit-content;
line-height: 0; line-height: 0;
display: grid;
max-width: 100%;
} }

View File

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

View File

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

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