🐛 Fix several corner cases that causes race conditions on workspace and dashboard loading

* 🐛 Fix several race conditions on workspace and dashboard code

It also fixes a corner case that happens when penpot workspace
is loaded in a background tab on firefox.

* 🐛 Add missing team-id prop to several file returning endpoints
This commit is contained in:
Andrey Antukh 2025-03-14 09:55:41 +01:00 committed by GitHub
parent 61800d8945
commit 05d6d2fcd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 138 additions and 125 deletions

View file

@ -6,6 +6,14 @@
- Add support for WEBP format on shape export [Github #6053](https://github.com/penpot/penpot/pull/6053) and [Github #6074](https://github.com/penpot/penpot/pull/6074) - Add support for WEBP format on shape export [Github #6053](https://github.com/penpot/penpot/pull/6053) and [Github #6074](https://github.com/penpot/penpot/pull/6074)
### :bug: Bugs fixed
- Fix feature loading on workspace when opening a file in a background
tab [Taiga #10377](https://tree.taiga.io/project/penpot/issue/10377)
- Fix minor inconsistencies on RPC `get-file-libraries` and `get-file`
methods (add missing team-id prop)
## 2.5.3 ## 2.5.3
### :bug: Bugs fixed ### :bug: Bugs fixed

View file

@ -323,6 +323,7 @@
file (-> (get-file cfg id :project-id project-id) file (-> (get-file cfg id :project-id project-id)
(assoc :permissions perms) (assoc :permissions perms)
(assoc :team-id (:id team))
(check-version!))] (check-version!))]
(-> (cfeat/get-team-enabled-features cf/flags team) (-> (cfeat/get-team-enabled-features cf/flags team)
@ -613,6 +614,7 @@
SELECT l.id, SELECT l.id,
l.features, l.features,
l.project_id, l.project_id,
p.team_id,
l.created_at, l.created_at,
l.modified_at, l.modified_at,
l.deleted_at, l.deleted_at,
@ -622,6 +624,7 @@
l.synced_at, l.synced_at,
l.is_shared l.is_shared
FROM libs AS l FROM libs AS l
INNER JOIN project AS p ON (p.id = l.project_id)
WHERE l.deleted_at IS NULL OR l.deleted_at > now();") WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
(defn get-file-libraries (defn get-file-libraries

View file

@ -38,7 +38,9 @@
(declare process-message) (declare process-message)
(defn initialize (defn initialize
[] [team-id]
(assert (uuid? team-id) "expected uuid instance for `team-id`")
(ptk/reify ::initialize (ptk/reify ::initialize
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
@ -46,8 +48,8 @@
profile-id (:profile-id state)] profile-id (:profile-id state)]
(->> (rx/merge (->> (rx/merge
(rx/of (fetch-projects) (rx/of (fetch-projects team-id)
(df/fetch-fonts)) (df/fetch-fonts team-id))
(->> stream (->> stream
(rx/filter (ptk/type? ::dws/message)) (rx/filter (ptk/type? ::dws/message))
(rx/map deref) (rx/map deref)
@ -60,8 +62,8 @@
(rx/take-until stopper)))))) (rx/take-until stopper))))))
(defn finalize (defn finalize
[] [team-id]
(ptk/data-event ::finalize {})) (ptk/data-event ::finalize {:team-id team-id}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Fetching (context aware: current team) ;; Data Fetching (context aware: current team)
@ -69,7 +71,7 @@
;; --- EVENT: fetch-projects ;; --- EVENT: fetch-projects
(defn projects-fetched (defn- projects-fetched
[projects] [projects]
(ptk/reify ::projects-fetched (ptk/reify ::projects-fetched
ptk/UpdateEvent ptk/UpdateEvent
@ -80,13 +82,12 @@
projects)))) projects))))
(defn fetch-projects (defn fetch-projects
[] [team-id]
(ptk/reify ::fetch-projects (ptk/reify ::fetch-projects
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ _ _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-projects {:team-id team-id}) (->> (rp/cmd! :get-projects {:team-id team-id})
(rx/map projects-fetched)))))) (rx/map projects-fetched)))))
;; --- EVENT: search ;; --- EVENT: search
@ -115,7 +116,7 @@
;; --- EVENT: recent-files ;; --- EVENT: recent-files
(defn recent-files-fetched (defn- recent-files-fetched
[files] [files]
(ptk/reify ::recent-files-fetched (ptk/reify ::recent-files-fetched
ptk/UpdateEvent ptk/UpdateEvent
@ -126,13 +127,14 @@
(update :files d/merge files)))))) (update :files d/merge files))))))
(defn fetch-recent-files (defn fetch-recent-files
[] ([] (fetch-recent-files nil))
([team-id]
(ptk/reify ::fetch-recent-files (ptk/reify ::fetch-recent-files
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [team-id (:current-team-id state)] (when-let [team-id (or team-id (:current-team-id state))]
(->> (rp/cmd! :get-team-recent-files {:team-id team-id}) (->> (rp/cmd! :get-team-recent-files {:team-id team-id})
(rx/map recent-files-fetched)))))) (rx/map recent-files-fetched)))))))
;; --- EVENT: fetch-template-files ;; --- EVENT: fetch-template-files

View file

@ -73,13 +73,12 @@
(fonts/register! :custom fonts)))))) (fonts/register! :custom fonts))))))
(defn fetch-fonts (defn fetch-fonts
[] [team-id]
(ptk/reify ::load-team-fonts (ptk/reify ::fetch-fonts
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ _ _]
(let [team-id (:current-team-id state)]
(->> (rp/cmd! :get-font-variants {:team-id team-id}) (->> (rp/cmd! :get-font-variants {:team-id team-id})
(rx/map fonts-fetched)))))) (rx/map fonts-fetched)))))
(defn process-upload (defn process-upload
"Given a seq of blobs and the team id, creates a ready-to-use fonts "Given a seq of blobs and the team id, creates a ready-to-use fonts

View file

@ -64,13 +64,14 @@
(update :profiles merge (d/index-by :id members)))))) (update :profiles merge (d/index-by :id members))))))
(defn fetch-members (defn fetch-members
[] ([] (fetch-members nil))
([team-id]
(ptk/reify ::fetch-members (ptk/reify ::fetch-members
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [team-id (:current-team-id state)] (when-let [team-id (or team-id (:current-team-id state))]
(->> (rp/cmd! :get-team-members {:team-id team-id}) (->> (rp/cmd! :get-team-members {:team-id team-id})
(rx/map (partial members-fetched team-id))))))) (rx/map (partial members-fetched team-id))))))))
(defn- invitations-fetched (defn- invitations-fetched
[team-id invitations] [team-id invitations]
@ -88,41 +89,20 @@
(->> (rp/cmd! :get-team-invitations {:team-id team-id}) (->> (rp/cmd! :get-team-invitations {:team-id team-id})
(rx/map (partial invitations-fetched team-id))))))) (rx/map (partial invitations-fetched team-id)))))))
(defn set-current-team
[{:keys [id permissions features] :as team}]
(ptk/reify ::set-current-team
ptk/UpdateEvent
(update [_ state]
(-> state
;; FIXME: redundant operation, only necessary on workspace
;; until workspace initialization is refactored
(update-in [:teams id] merge team)
(assoc :permissions permissions)
;; FIXME: this is a redundant operation that only needed by
;; workspace; ti will not be needed after workspace
;; bootstrap & urls refactor
(assoc :current-team-id id)))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (features/initialize (or features #{}))))
ptk/EffectEvent
(effect [_ _ _]
(swap! storage/global assoc ::current-team-id id))))
(defn- team-initialized (defn- team-initialized
[] [team-id]
(ptk/reify ::team-initialized (ptk/reify ::team-initialized
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [team-id (:current-team-id state) (let [teams (get state :teams)
teams (get state :teams)
team (get teams team-id)] team (get teams team-id)]
(if (not team) (if (not team)
(rx/throw (ex/error :type :authentication)) (rx/throw (ex/error :type :authentication))
(rx/of (set-current-team team) (let [permissions (get team :permissions)
(fetch-members))))))) features (get team :features)]
(rx/of #(assoc % :permissions permissions)
(features/initialize (or features #{}))
(fetch-members team-id))))))))
(defn initialize-team (defn initialize-team
[team-id] [team-id]
@ -138,8 +118,7 @@
(rx/of (fetch-teams)) (rx/of (fetch-teams))
(->> stream (->> stream
(rx/filter (ptk/type? ::teams-fetched)) (rx/filter (ptk/type? ::teams-fetched))
(rx/observe-on :async) (rx/map (partial team-initialized team-id))))
(rx/map team-initialized)))
(rx/take-until stopper)))))) (rx/take-until stopper))))))
(defn finalize-team (defn finalize-team
@ -169,7 +148,7 @@
params (assoc params :team-id team-id)] params (assoc params :team-id team-id)]
(->> (rp/cmd! :update-team-member-role params) (->> (rp/cmd! :update-team-member-role params)
(rx/mapcat (fn [_] (rx/mapcat (fn [_]
(rx/of (fetch-members) (rx/of (fetch-members team-id)
(fetch-teams) (fetch-teams)
(ptk/data-event ::ev/event (ptk/data-event ::ev/event
{::ev/name "update-team-member-role" {::ev/name "update-team-member-role"
@ -187,7 +166,7 @@
params (assoc params :team-id team-id)] params (assoc params :team-id team-id)]
(->> (rp/cmd! :delete-team-member params) (->> (rp/cmd! :delete-team-member params)
(rx/mapcat (fn [_] (rx/mapcat (fn [_]
(rx/of (fetch-members) (rx/of (fetch-members team-id)
(fetch-teams) (fetch-teams)
(ptk/data-event ::ev/event (ptk/data-event ::ev/event
{::ev/name "delete-team-member" {::ev/name "delete-team-member"

View file

@ -291,7 +291,8 @@
(watch [_ state stream] (watch [_ state stream]
(let [features (features/get-team-enabled-features state) (let [features (features/get-team-enabled-features state)
render-wasm? (contains? features "render-wasm/v1") render-wasm? (contains? features "render-wasm/v1")
stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)] stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
team-id (:current-team-id state)]
(->> (rx/concat (->> (rx/concat
;; Firstly load wasm module if it is enabled and fonts ;; Firstly load wasm module if it is enabled and fonts
@ -305,7 +306,7 @@
(rx/filter (ptk/type? ::df/fonts-loaded)) (rx/filter (ptk/type? ::df/fonts-loaded))
(rx/take 1) (rx/take 1)
(rx/ignore)) (rx/ignore))
(rx/of (df/fetch-fonts))) (rx/of (df/fetch-fonts team-id)))
;; Then fetch file and thumbnails ;; Then fetch file and thumbnails
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features}) (->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
@ -335,7 +336,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(log/debug :hint "initialize-workspace" :file-id file-id) (log/debug :hint "initialize-workspace" :file-id (dm/str file-id))
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream) (let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
rparams (rt/get-params state)] rparams (rt/get-params state)]

View file

@ -210,17 +210,20 @@
(swap! storage/session dissoc :plugin-url)))))) (swap! storage/session dissoc :plugin-url))))))
(defn use-templates-import (defn use-templates-import
[can-edit? template-url default-project-id] [can-edit? template-url project]
(mf/with-layout-effect (let [project-id (get project :id)
[can-edit? template-url default-project-id] team-id (get project :team-id)]
(when (and (some? template-url) (some? default-project-id)) (mf/with-layout-effect [can-edit? template-url project-id team-id]
(when (and (some? template-url)
(some? project-id)
(some? team-id))
(if can-edit? (if can-edit?
(let [valid-url? (and (str/ends-with? template-url ".penpot") (let [valid-url? (and (str/ends-with? template-url ".penpot")
(str/starts-with? template-url cf/templates-uri)) (str/starts-with? template-url cf/templates-uri))
template-name (when valid-url? (subs template-url (count cf/templates-uri))) template-name (when valid-url? (subs template-url (count cf/templates-uri)))
on-import #(st/emit! (dpj/fetch-files default-project-id) on-import #(st/emit! (dpj/fetch-files project-id)
(dd/fetch-recent-files) (dd/fetch-recent-files team-id)
(dd/fetch-projects) (dd/fetch-projects team-id)
(dd/clear-selected-files) (dd/clear-selected-files)
(ptk/event ::ev/event {::ev/name "install-template-from-link-finished" (ptk/event ::ev/event {::ev/name "install-template-from-link-finished"
:name template-name :name template-name
@ -238,14 +241,14 @@
(st/emit! (notif/error (tr "dashboard.import.error"))) (st/emit! (notif/error (tr "dashboard.import.error")))
(st/emit! (modal/show (st/emit! (modal/show
{:type :import {:type :import
:project-id default-project-id :project-id project-id
:entries [{:name template-name :uri (wapi/create-uri (:body result))}] :entries [{:name template-name :uri (wapi/create-uri (:body result))}]
:on-finish-import on-import}))))))) :on-finish-import on-import})))))))
(st/emit! (notif/error (tr "dashboard.import.bad-url"))))) (st/emit! (notif/error (tr "dashboard.import.bad-url")))))
(st/emit! (notif/error (tr "dashboard.import.no-perms")))) (st/emit! (notif/error (tr "dashboard.import.no-perms"))))
(binding [storage/*sync* true] (binding [storage/*sync* true]
(swap! storage/session dissoc :template-url))))) (swap! storage/session dissoc :template-url))))))
(mf/defc dashboard* (mf/defc dashboard*
{::mf/props :obj} {::mf/props :obj}
@ -270,10 +273,10 @@
(hooks/use-shortcuts ::dashboard sc/shortcuts) (hooks/use-shortcuts ::dashboard sc/shortcuts)
(mf/with-effect [] (mf/with-effect [team-id]
(st/emit! (dd/initialize)) (st/emit! (dd/initialize team-id))
(fn [] (fn []
(st/emit! (dd/finalize)))) (st/emit! (dd/finalize team-id))))
(mf/with-effect [] (mf/with-effect []
(let [key (events/listen goog/global "keydown" (let [key (events/listen goog/global "keydown"
@ -285,7 +288,7 @@
(events/unlistenByKey key)))) (events/unlistenByKey key))))
(use-plugin-register plugin-url team-id (:id default-project)) (use-plugin-register plugin-url team-id (:id default-project))
(use-templates-import can-edit? template-url (:id default-project)) (use-templates-import can-edit? template-url default-project)
[:& (mf/provider ctx/current-project-id) {:value project-id} [:& (mf/provider ctx/current-project-id) {:value project-id}
[:> modal-container*] [:> modal-container*]

View file

@ -133,7 +133,7 @@
(st/emit! (dcm/go-to-dashboard-files (st/emit! (dcm/go-to-dashboard-files
{:project-id project-id {:project-id project-id
:team-id team-id})) :team-id team-id}))
(st/emit! (dd/fetch-recent-files) (st/emit! (dd/fetch-recent-files team-id)
(dd/clear-selected-files)))) (dd/clear-selected-files))))
on-move-accept on-move-accept

View file

@ -566,8 +566,9 @@
on-finish-import on-finish-import
(mf/use-fn (mf/use-fn
(mf/deps team-id)
(fn [] (fn []
(st/emit! (dd/fetch-recent-files) (st/emit! (dd/fetch-recent-files team-id)
(dd/clear-selected-files)))) (dd/clear-selected-files))))
import-files (use-import-file project-id on-finish-import) import-files (use-import-file project-id on-finish-import)
@ -608,9 +609,10 @@
on-drop-success on-drop-success
(mf/use-fn (mf/use-fn
(mf/deps team-id)
(fn [] (fn []
(st/emit! (ntf/success (tr "dashboard.success-move-file")) (st/emit! (ntf/success (tr "dashboard.success-move-file"))
(dd/fetch-recent-files) (dd/fetch-recent-files team-id)
(dd/clear-selected-files)))) (dd/clear-selected-files))))
on-drop on-drop

View file

@ -344,11 +344,13 @@
continue-template continue-template
(mf/use-fn (mf/use-fn
(mf/deps on-finish-import)
(fn [template] (fn [template]
(let [on-success (let [on-success
(fn [_event] (fn [_event]
(reset! status* :import-success) (reset! status* :import-success)
(st/emit! (dd/fetch-recent-files))) (when (fn? on-finish-import)
(on-finish-import)))
on-error on-error
(fn [cause] (fn [cause]
@ -479,8 +481,6 @@
[:> import-entry* {:entry (assoc template :status status) [:> import-entry* {:entry (assoc template :status status)
:can-be-deleted false}])] :can-be-deleted false}])]
;; (prn "import-dialog" status)
[:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)} [:div {:class (stl/css :action-buttons)}
(when (= :analyze status) (when (= :analyze status)

View file

@ -105,7 +105,8 @@
[{:keys [project is-first team files can-edit]}] [{:keys [project is-first team files can-edit]}]
(let [locale (mf/deref i18n/locale) (let [locale (mf/deref i18n/locale)
project-id (:id project) project-id (get project :id)
team-id (get team :id)
file-count (or (:count project) 0) file-count (or (:count project) 0)
is-draft? (:is-default project) is-draft? (:is-default project)
@ -191,11 +192,11 @@
on-import on-import
(mf/use-fn (mf/use-fn
(mf/deps project-id) (mf/deps project-id team-id)
(fn [] (fn []
(st/emit! (dpj/fetch-files project-id) (st/emit! (dpj/fetch-files project-id)
(dd/fetch-recent-files) (dd/fetch-recent-files team-id)
(dd/fetch-projects) (dd/fetch-projects team-id)
(dd/clear-selected-files)))) (dd/clear-selected-files))))
handle-create-click handle-create-click
@ -317,6 +318,8 @@
(sort-by :modified-at) (sort-by :modified-at)
(reverse))) (reverse)))
team-id (get team :id)
recent-map (mf/deref ref:recent-files) recent-map (mf/deref ref:recent-files)
permisions (:permissions team) permisions (:permissions team)
@ -327,7 +330,7 @@
show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true)) show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true))
show-team-hero? (deref show-team-hero*) show-team-hero? (deref show-team-hero*)
is-my-penpot (= (:default-team-id profile) (:id team)) is-my-penpot (= (:default-team-id profile) team-id)
is-defalt-team? (:is-default team) is-defalt-team? (:is-default team)
on-close on-close
@ -346,8 +349,8 @@
(:name team))] (:name team))]
(dom/set-html-title (tr "title.dashboard.projects" tname)))) (dom/set-html-title (tr "title.dashboard.projects" tname))))
(mf/with-effect [] (mf/with-effect [team-id]
(st/emit! (dd/fetch-recent-files) (st/emit! (dd/fetch-recent-files team-id)
(dd/clear-selected-files))) (dd/clear-selected-files)))
(when (seq projects) (when (seq projects)

View file

@ -37,6 +37,7 @@
[template team-id project-id default-project-id section] [template team-id project-id default-project-id section]
(letfn [(on-finish [] (letfn [(on-finish []
(st/emit! (st/emit!
(dd/fetch-recent-files team-id)
(ptk/event ::ev/event {::ev/name "import-template-finish" (ptk/event ::ev/event {::ev/name "import-template-finish"
::ev/origin "dashboard" ::ev/origin "dashboard"
:template (:name template) :template (:name template)

View file

@ -568,7 +568,6 @@
[{:keys [starting-tab file-id] :as props :or {starting-tab :libraries}}] [{:keys [starting-tab file-id] :as props :or {starting-tab :libraries}}]
(let [files (mf/deref refs/files) (let [files (mf/deref refs/files)
file (get files file-id) file (get files file-id)
team-id (:team-id file)
shared? (:is-shared file) shared? (:is-shared file)
linked-libraries linked-libraries
@ -616,8 +615,8 @@
:id "updates" :id "updates"
:content updates-tab}]] :content updates-tab}]]
(mf/with-effect [team-id] (mf/with-effect []
(st/emit! (dtm/fetch-shared-files team-id))) (st/emit! (dtm/fetch-shared-files)))
[:div {:class (stl/css :modal-overlay) [:div {:class (stl/css :modal-overlay)
:on-click close-dialog-outside :on-click close-dialog-outside

View file

@ -14,7 +14,6 @@
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.uri :as u] [app.common.uri :as u]
[app.main.data.fonts :as df] [app.main.data.fonts :as df]
[app.main.data.team :as dtm]
[app.main.features :as features] [app.main.features :as features]
[app.main.render :as render] [app.main.render :as render]
[app.main.repo :as repo] [app.main.repo :as repo]
@ -30,6 +29,20 @@
(log/setup! {:app :info}) (log/setup! {:app :info})
(defn set-current-team
[{:keys [id permissions features] :as team}]
(ptk/reify ::set-current-team
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc :permissions permissions)
(update :teams assoc id team)
(assoc :current-team-id id)))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (features/initialize (or features #{}))))))
(defn- fetch-team (defn- fetch-team
[& {:keys [file-id]}] [& {:keys [file-id]}]
(ptk/reify ::fetch-team (ptk/reify ::fetch-team
@ -37,7 +50,7 @@
(watch [_ _ _] (watch [_ _ _]
(->> (repo/cmd! :get-team {:file-id file-id}) (->> (repo/cmd! :get-team {:file-id file-id})
(rx/mapcat (fn [team] (rx/mapcat (fn [team]
(rx/of (dtm/set-current-team team) (rx/of (set-current-team team)
(ptk/data-event ::team-fetched team)))))))) (ptk/data-event ::team-fetched team))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;