diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 3d20987122..bc512c0b7f 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -552,7 +552,6 @@ and p.team_id = ? order by f.modified_at desc") - (defn- get-library-summary [cfg {:keys [id data] :as file}] (letfn [(assets-sample [assets limit] diff --git a/frontend/playwright/data/dashboard/get-team-shared-files-10142.json b/frontend/playwright/data/dashboard/get-team-shared-files-10142.json new file mode 100644 index 0000000000..4f974f3c9e --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-shared-files-10142.json @@ -0,0 +1,43 @@ +{ + "~#set": [ + { + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:name": "Lorem Ipsum", + "~:revn": 2, + "~:modified-at": "~m1739356261950", + "~:vern": 0, + "~:id": "~u69b52fcf-7de0-81cd-8005-b9b180a0bfb5", + "~:thumbnail-id": "~u55bb9e08-6eed-4a64-a94d-2bcce7006e79", + "~:is-shared": true, + "~:project-id": "~u1ad2931c-eb80-8098-8005-b86c1d9d26c2", + "~:created-at": "~m1739356217030", + "~:library-summary": { + "~:components": { + "~:count": 0, + "~:sample": [] + }, + "~:media": { + "~:count": 0, + "~:sample": [] + }, + "~:colors": { + "~:count": 1, + "~:sample": [ + { + "~:path": "", + "~:color": "#0087ff", + "~:name": "#0087ff", + "~:modified-at": "~m1739356244863", + "~:opacity": 1, + "~:id": "~u0449ccff-62fe-805c-8005-b9b194b094dd" + } + ] + }, + "~:typographies": { + "~:count": 0, + "~:sample": [] + } + } + } + ] +} diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js index 9dfde88439..f297218d3e 100644 --- a/frontend/playwright/ui/pages/DashboardPage.js +++ b/frontend/playwright/ui/pages/DashboardPage.js @@ -72,7 +72,7 @@ export class DashboardPage extends BaseWebSocketPage { this.draftsLink = this.sidebar.getByText("Drafts"); this.fontsLink = this.sidebar.getByText("Fonts"); - this.libsLink = this.sidebar.getByText("Libraries"); + this.librariesLink = this.sidebar.getByText("Libraries"); this.searchButton = page.getByRole("button", { name: "dashboard-search" }); this.searchInput = page.getByPlaceholder("Search…"); @@ -281,6 +281,13 @@ export class DashboardPage extends BaseWebSocketPage { await this.userProfileOption.click(); } + + async goToLibraries() { + await this.page.goto( + `#/dashboard/libraries?team-id=${DashboardPage.anyTeamId}`, + ); + await expect(this.mainHeading).toHaveText("Libraries"); + } } export default DashboardPage; diff --git a/frontend/playwright/ui/specs/dashboard-libraries.spec.js b/frontend/playwright/ui/specs/dashboard-libraries.spec.js new file mode 100644 index 0000000000..d30f6e82c0 --- /dev/null +++ b/frontend/playwright/ui/specs/dashboard-libraries.spec.js @@ -0,0 +1,33 @@ +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("BUG 10421 - Fix libraries context menu", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.mockRPC( + "get-team-shared-files?team-id=*", + "dashboard/get-team-shared-files-10142.json", + ); + + await dashboardPage.mockRPC( + "get-all-projects", + "dashboard/get-all-projects.json", + ); + + await dashboardPage.goToLibraries(); + + const libraryItem = page.getByTitle(/Lorem Ipsum/); + + await expect(libraryItem).toBeVisible(); + await libraryItem.getByRole("button", { name: "Options" }).click(); + + await expect(page.getByText("Rename")).toBeVisible(); +}); diff --git a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js index 44bfb9abb0..50ed19787f 100644 --- a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js +++ b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js @@ -47,7 +47,7 @@ test("User goes to an empty libraries page", async ({ page }) => { await dashboardPage.setupLibrariesEmpty(); await dashboardPage.goToDashboard(); - await dashboardPage.libsLink.click(); + await dashboardPage.librariesLink.click(); await expect(dashboardPage.mainHeading).toHaveText("Libraries"); await expect(dashboardPage.page).toHaveScreenshot(); @@ -100,7 +100,7 @@ test("User goes to a full library page", async ({ page }) => { await dashboardPage.setupDashboardFull(); await dashboardPage.goToDashboard(); - await dashboardPage.libsLink.click(); + await dashboardPage.librariesLink.click(); await expect(dashboardPage.mainHeading).toHaveText("Libraries"); await expect(dashboardPage.page).toHaveScreenshot(); diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index ddd2206b59..d42f307251 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -187,8 +187,8 @@ (ptk/reify ::show-file-menu-with-position ptk/UpdateEvent (update [_ state] - (update state :dashboard-local - assoc :menu-open true + (update state :dashboard-local assoc + :menu-open true :menu-pos pos :file-id file-id)))) diff --git a/frontend/src/app/main/data/project.cljs b/frontend/src/app/main/data/project.cljs index 74a927d8d0..81db7856b4 100644 --- a/frontend/src/app/main/data/project.cljs +++ b/frontend/src/app/main/data/project.cljs @@ -55,7 +55,6 @@ (dissoc state :current-project-id) state))))) - (defn- files-fetched [project-id files] (ptk/reify ::files-fetched @@ -67,14 +66,14 @@ (assoc project :count (count files)))))))) (defn fetch-files - [project-id] - (assert (uuid? project-id) "expected valid uuid for `project-id`") - (ptk/reify ::fetch-files - ptk/WatchEvent - (watch [_ _ _] - (->> (rp/cmd! :get-project-files {:project-id project-id}) - (rx/map (partial files-fetched project-id)))))) - + ([] (fetch-files nil)) + ([project-id] + (ptk/reify ::fetch-files + ptk/WatchEvent + (watch [_ state _] + (when-let [project-id (or project-id (:current-project-id state))] + (->> (rp/cmd! :get-project-files {:project-id project-id}) + (rx/map (partial files-fetched project-id)))))))) diff --git a/frontend/src/app/main/data/team.cljs b/frontend/src/app/main/data/team.cljs index 5ff7cf097d..50af94a444 100644 --- a/frontend/src/app/main/data/team.cljs +++ b/frontend/src/app/main/data/team.cljs @@ -227,26 +227,6 @@ (->> (rp/cmd! :get-webhooks {:team-id team-id}) (rx/map (partial webhooks-fetched team-id))))))) -(defn- shared-files-fetched - [files] - (ptk/reify ::shared-files-fetched - ptk/UpdateEvent - (update [_ state] - (let [files (d/index-by :id files)] - (assoc state :shared-files files))))) - -(defn fetch-shared-files - "Event mainly used for fetch a list of shared libraries for a team, - this list does not includes the content of the library per se. It - is used mainly for show available libraries and a summary of it." - [] - (ptk/reify ::fetch-shared-files - ptk/WatchEvent - (watch [_ state _] - (let [team-id (:current-team-id state)] - (->> (rp/cmd! :get-team-shared-files {:team-id team-id}) - (rx/map shared-files-fetched)))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Modification ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -567,6 +547,25 @@ (rx/of (fetch-webhooks))))) (rx/catch on-error)))))) - +(defn- shared-files-fetched + [files] + (ptk/reify ::shared-files-fetched + ptk/UpdateEvent + (update [_ state] + (let [files (d/index-by :id files)] + (update state :shared-files merge files))))) + +(defn fetch-shared-files + "Event mainly used for fetch a list of shared libraries for a team, + this list does not includes the content of the library per se. It + is used mainly for show available libraries and a summary of it." + ([] (fetch-shared-files nil)) + ([team-id] + (ptk/reify ::fetch-shared-files + ptk/WatchEvent + (watch [_ state _] + (when-let [team-id (or team-id (:current-team-id state))] + (->> (rp/cmd! :get-team-shared-files {:team-id team-id}) + (rx/map shared-files-fetched))))))) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 5d8ed5a0ef..bc59636340 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -316,19 +316,25 @@ (fn [event] (dom/stop-propagation event) (dom/prevent-default event) + (when-not selected? (when-not (kbd/shift? event) (st/emit! (dd/clear-selected-files))) - (st/emit! (dd/toggle-file-select file))) + (do + (st/emit! (dd/toggle-file-select file)))) + + (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)] - (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)] (st/emit! (dd/show-file-menu-with-position file-id position))))) on-context-menu @@ -401,50 +407,53 @@ [:h3 (:name file)]) [:& grid-item-metadata {:modified-at (:modified-at file)}]] - (when-not is-library-view - [:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))} - [:div - {:class (stl/css :project-th-icon :menu) - :tab-index "0" - :ref menu-ref - :id (str file-id "-action-menu") - :on-click on-menu-click - :on-key-down (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (on-menu-click event)))} - menu-icon - (when (and selected? file-menu-open?) + [:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))} + [:div + {:class (stl/css :project-th-icon :menu) + :tab-index "0" + :role "button" + :aria-label (tr "dashboard.options") + :ref menu-ref + :id (str file-id "-action-menu") + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/stop-propagation event) + (on-menu-click event)))} + menu-icon + (when (and selected? file-menu-open?) ;; When the menu is open we disable events in the dashboard. We need to force pointer events ;; so the menu can be handled - [:div {:style {:pointer-events "all"}} - [:> file-menu* {:files (vals selected-files) - :left (+ 24 (:x (:menu-pos dashboard-local))) - :top (:y (:menu-pos dashboard-local)) - :can-edit can-edit - :navigate true - :on-edit on-edit - :on-menu-close on-menu-close - :origin origin - :parent-id (dm/str file-id "-action-menu")}]])]])]]])) + [:div {:style {:pointer-events "all"}} + [:> file-menu* {:files (vals selected-files) + :left (+ 24 (:x (:menu-pos dashboard-local))) + :top (:y (:menu-pos dashboard-local)) + :can-edit can-edit + :navigate true + :on-edit on-edit + :on-menu-close on-menu-close + :origin origin + :parent-id (dm/str file-id "-action-menu")}]])]]]]])) (mf/defc grid {::mf/props :obj} [{:keys [files project origin limit create-fn can-edit selected-files]}] (let [dragging? (mf/use-state false) - project-id (:id project) + project-id (get project :id) + team-id (get project :team-id) + node-ref (mf/use-var nil) on-finish-import (mf/use-fn + (mf/deps project-id team-id) (fn [] (st/emit! (dpj/fetch-files project-id) - (dtm/fetch-shared-files) + (dtm/fetch-shared-files team-id) (dd/clear-selected-files)))) - - - import-files (use-import-file project-id on-finish-import) + import-files + (use-import-file project-id on-finish-import) on-drag-enter (mf/use-fn diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index 1ba3e856f1..dd9464c60c 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -15,23 +15,38 @@ [app.main.ui.hooks :as hooks] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [okulary.core :as l] [rumext.v2 :as mf])) +(def ^:private ref:selected-files + (l/derived (fn [state] + (let [selected (get state :selected-files) + files (get state :shared-files)] + (refs/extract-selected-files files selected))) + st/state)) + (mf/defc libraries-page* {::mf/props :obj} [{:keys [team default-project]}] (let [files (mf/deref refs/shared-files) - files - (mf/with-memo [files] - (->> (vals files) - (sort-by :modified-at) - (reverse))) + team-id + (get team :id) can-edit (-> team :permissions :can-edit) + files + (mf/with-memo [files team-id] + (->> (vals files) + (filter #(= team-id (:team-id %))) + (sort-by :modified-at) + (reverse))) + + selected-files + (mf/deref ref:selected-files) + [rowref limit] (hooks/use-dynamic-grid-item-width 350)] @@ -41,16 +56,19 @@ (:name team))] (dom/set-html-title (tr "title.dashboard.shared-libraries" tname)))) - (mf/with-effect [team] - (st/emit! (dtm/fetch-shared-files) + (mf/with-effect [team-id] + (st/emit! (dtm/fetch-shared-files team-id) (dd/clear-selected-files))) [:* [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} [:div#dashboard-libraries-title {:class (stl/css :dashboard-title)} [:h1 (tr "dashboard.libraries-title")]]] - [:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared) :ref rowref} + + [:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared) + :ref rowref} [:& grid {:files files + :selected-files selected-files :project default-project :origin :libraries :limit limit diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 5a07a1a256..963385602f 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -142,6 +142,8 @@ (let [id (:id library) importing? (deref importing) + team-id (mf/use-ctx ctx/current-team-id) + on-error (mf/use-fn (fn [_] @@ -150,11 +152,13 @@ on-success (mf/use-fn + (mf/deps team-id) (fn [_] - (st/emit! (dtm/fetch-shared-files)))) + (st/emit! (dtm/fetch-shared-files team-id)))) import-library (mf/use-fn + (mf/deps on-success on-error) (fn [_] (reset! importing id) (st/emit! (dd/clone-template @@ -565,6 +569,7 @@ file (deref refs/file) file-id (:id file) + team-id (:team-id file) shared? (:is-shared file) linked-libraries @@ -611,8 +616,8 @@ :id "updates" :content updates-tab}]] - (mf/with-effect [] - (st/emit! (dtm/fetch-shared-files))) + (mf/with-effect [team-id] + (st/emit! (dtm/fetch-shared-files team-id))) [:div {:class (stl/css :modal-overlay) :on-click close-dialog-outside