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:
parent
8c81d48858
commit
cad7d75590
11 changed files with 197 additions and 85 deletions
|
@ -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]
|
||||
|
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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;
|
||||
|
|
33
frontend/playwright/ui/specs/dashboard-libraries.spec.js
Normal file
33
frontend/playwright/ui/specs/dashboard-libraries.spec.js
Normal 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();
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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))))
|
||||
|
||||
|
|
|
@ -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))))))))
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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)))))))
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue