0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-13 07:21:40 -05:00

🐛 Fix libraries context menu (#5854)

*  Add integration test for Bug #10421

* 🐛 Fix dashboard library item menu

*  Fixup integration test
This commit is contained in:
Belén Albeza 2025-02-14 14:34:54 +01:00 committed by GitHub
parent 8c81d48858
commit cad7d75590
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 197 additions and 85 deletions

View file

@ -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]

View file

@ -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": []
}
}
}
]
}

View file

@ -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;

View file

@ -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();
});

View file

@ -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();

View file

@ -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))))

View file

@ -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))))))))

View file

@ -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)))))))

View file

@ -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

View file

@ -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

View file

@ -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