mirror of
https://github.com/penpot/penpot.git
synced 2025-05-18 15:26:10 +02: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 = ?
|
and p.team_id = ?
|
||||||
order by f.modified_at desc")
|
order by f.modified_at desc")
|
||||||
|
|
||||||
|
|
||||||
(defn- get-library-summary
|
(defn- get-library-summary
|
||||||
[cfg {:keys [id data] :as file}]
|
[cfg {:keys [id data] :as file}]
|
||||||
(letfn [(assets-sample [assets limit]
|
(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.draftsLink = this.sidebar.getByText("Drafts");
|
||||||
this.fontsLink = this.sidebar.getByText("Fonts");
|
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.searchButton = page.getByRole("button", { name: "dashboard-search" });
|
||||||
this.searchInput = page.getByPlaceholder("Search…");
|
this.searchInput = page.getByPlaceholder("Search…");
|
||||||
|
@ -281,6 +281,13 @@ export class DashboardPage extends BaseWebSocketPage {
|
||||||
|
|
||||||
await this.userProfileOption.click();
|
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;
|
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.setupLibrariesEmpty();
|
||||||
|
|
||||||
await dashboardPage.goToDashboard();
|
await dashboardPage.goToDashboard();
|
||||||
await dashboardPage.libsLink.click();
|
await dashboardPage.librariesLink.click();
|
||||||
|
|
||||||
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
|
@ -100,7 +100,7 @@ test("User goes to a full library page", async ({ page }) => {
|
||||||
await dashboardPage.setupDashboardFull();
|
await dashboardPage.setupDashboardFull();
|
||||||
|
|
||||||
await dashboardPage.goToDashboard();
|
await dashboardPage.goToDashboard();
|
||||||
await dashboardPage.libsLink.click();
|
await dashboardPage.librariesLink.click();
|
||||||
|
|
||||||
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
await expect(dashboardPage.mainHeading).toHaveText("Libraries");
|
||||||
await expect(dashboardPage.page).toHaveScreenshot();
|
await expect(dashboardPage.page).toHaveScreenshot();
|
||||||
|
|
|
@ -187,8 +187,8 @@
|
||||||
(ptk/reify ::show-file-menu-with-position
|
(ptk/reify ::show-file-menu-with-position
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update state :dashboard-local
|
(update state :dashboard-local assoc
|
||||||
assoc :menu-open true
|
:menu-open true
|
||||||
:menu-pos pos
|
:menu-pos pos
|
||||||
:file-id file-id))))
|
:file-id file-id))))
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,6 @@
|
||||||
(dissoc state :current-project-id)
|
(dissoc state :current-project-id)
|
||||||
state)))))
|
state)))))
|
||||||
|
|
||||||
|
|
||||||
(defn- files-fetched
|
(defn- files-fetched
|
||||||
[project-id files]
|
[project-id files]
|
||||||
(ptk/reify ::files-fetched
|
(ptk/reify ::files-fetched
|
||||||
|
@ -67,14 +66,14 @@
|
||||||
(assoc project :count (count files))))))))
|
(assoc project :count (count files))))))))
|
||||||
|
|
||||||
(defn fetch-files
|
(defn fetch-files
|
||||||
[project-id]
|
([] (fetch-files nil))
|
||||||
(assert (uuid? project-id) "expected valid uuid for `project-id`")
|
([project-id]
|
||||||
(ptk/reify ::fetch-files
|
(ptk/reify ::fetch-files
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ state _]
|
||||||
|
(when-let [project-id (or project-id (:current-project-id state))]
|
||||||
(->> (rp/cmd! :get-project-files {:project-id project-id})
|
(->> (rp/cmd! :get-project-files {:project-id project-id})
|
||||||
(rx/map (partial files-fetched project-id))))))
|
(rx/map (partial files-fetched project-id))))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -227,26 +227,6 @@
|
||||||
(->> (rp/cmd! :get-webhooks {:team-id team-id})
|
(->> (rp/cmd! :get-webhooks {:team-id team-id})
|
||||||
(rx/map (partial webhooks-fetched 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
|
;; Data Modification
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -567,6 +547,25 @@
|
||||||
(rx/of (fetch-webhooks)))))
|
(rx/of (fetch-webhooks)))))
|
||||||
(rx/catch on-error))))))
|
(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]
|
(fn [event]
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
|
|
||||||
(when-not selected?
|
(when-not selected?
|
||||||
(when-not (kbd/shift? event)
|
(when-not (kbd/shift? event)
|
||||||
(st/emit! (dd/clear-selected-files)))
|
(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)
|
(let [client-position
|
||||||
position (if (and (nil? (:y client-position)) (nil? (:x 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)
|
(let [target-element (dom/get-target event)
|
||||||
points (dom/get-bounding-rect target-element)
|
points (dom/get-bounding-rect target-element)
|
||||||
y (:top points)
|
y (:top points)
|
||||||
x (:left points)]
|
x (:left points)]
|
||||||
(gpt/point x y))
|
(gpt/point x y))
|
||||||
client-position)]
|
client-position)]
|
||||||
|
|
||||||
(st/emit! (dd/show-file-menu-with-position file-id position)))))
|
(st/emit! (dd/show-file-menu-with-position file-id position)))))
|
||||||
|
|
||||||
on-context-menu
|
on-context-menu
|
||||||
|
@ -401,11 +407,12 @@
|
||||||
[:h3 (:name file)])
|
[:h3 (:name file)])
|
||||||
[:& grid-item-metadata {:modified-at (:modified-at 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-case :project-th-actions true :force-display (:menu-open dashboard-local))}
|
||||||
[:div
|
[:div
|
||||||
{:class (stl/css :project-th-icon :menu)
|
{:class (stl/css :project-th-icon :menu)
|
||||||
:tab-index "0"
|
:tab-index "0"
|
||||||
|
:role "button"
|
||||||
|
:aria-label (tr "dashboard.options")
|
||||||
:ref menu-ref
|
:ref menu-ref
|
||||||
:id (str file-id "-action-menu")
|
:id (str file-id "-action-menu")
|
||||||
:on-click on-menu-click
|
:on-click on-menu-click
|
||||||
|
@ -426,25 +433,27 @@
|
||||||
:on-edit on-edit
|
:on-edit on-edit
|
||||||
:on-menu-close on-menu-close
|
:on-menu-close on-menu-close
|
||||||
:origin origin
|
:origin origin
|
||||||
:parent-id (dm/str file-id "-action-menu")}]])]])]]]))
|
:parent-id (dm/str file-id "-action-menu")}]])]]]]]))
|
||||||
|
|
||||||
(mf/defc grid
|
(mf/defc grid
|
||||||
{::mf/props :obj}
|
{::mf/props :obj}
|
||||||
[{:keys [files project origin limit create-fn can-edit selected-files]}]
|
[{: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 (:id project)
|
project-id (get project :id)
|
||||||
|
team-id (get project :team-id)
|
||||||
|
|
||||||
node-ref (mf/use-var nil)
|
node-ref (mf/use-var nil)
|
||||||
|
|
||||||
on-finish-import
|
on-finish-import
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
(mf/deps project-id team-id)
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit! (dpj/fetch-files project-id)
|
(st/emit! (dpj/fetch-files project-id)
|
||||||
(dtm/fetch-shared-files)
|
(dtm/fetch-shared-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)
|
|
||||||
|
|
||||||
on-drag-enter
|
on-drag-enter
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
|
|
@ -15,23 +15,38 @@
|
||||||
[app.main.ui.hooks :as hooks]
|
[app.main.ui.hooks :as hooks]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
|
[okulary.core :as l]
|
||||||
[rumext.v2 :as mf]))
|
[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/defc libraries-page*
|
||||||
{::mf/props :obj}
|
{::mf/props :obj}
|
||||||
[{:keys [team default-project]}]
|
[{:keys [team default-project]}]
|
||||||
(let [files
|
(let [files
|
||||||
(mf/deref refs/shared-files)
|
(mf/deref refs/shared-files)
|
||||||
|
|
||||||
files
|
team-id
|
||||||
(mf/with-memo [files]
|
(get team :id)
|
||||||
(->> (vals files)
|
|
||||||
(sort-by :modified-at)
|
|
||||||
(reverse)))
|
|
||||||
|
|
||||||
can-edit
|
can-edit
|
||||||
(-> team :permissions :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]
|
[rowref limit]
|
||||||
(hooks/use-dynamic-grid-item-width 350)]
|
(hooks/use-dynamic-grid-item-width 350)]
|
||||||
|
|
||||||
|
@ -41,16 +56,19 @@
|
||||||
(:name team))]
|
(:name team))]
|
||||||
(dom/set-html-title (tr "title.dashboard.shared-libraries" tname))))
|
(dom/set-html-title (tr "title.dashboard.shared-libraries" tname))))
|
||||||
|
|
||||||
(mf/with-effect [team]
|
(mf/with-effect [team-id]
|
||||||
(st/emit! (dtm/fetch-shared-files)
|
(st/emit! (dtm/fetch-shared-files team-id)
|
||||||
(dd/clear-selected-files)))
|
(dd/clear-selected-files)))
|
||||||
|
|
||||||
[:*
|
[:*
|
||||||
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
|
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
|
||||||
[:div#dashboard-libraries-title {:class (stl/css :dashboard-title)}
|
[:div#dashboard-libraries-title {:class (stl/css :dashboard-title)}
|
||||||
[:h1 (tr "dashboard.libraries-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
|
[:& grid {:files files
|
||||||
|
:selected-files selected-files
|
||||||
:project default-project
|
:project default-project
|
||||||
:origin :libraries
|
:origin :libraries
|
||||||
:limit limit
|
:limit limit
|
||||||
|
|
|
@ -142,6 +142,8 @@
|
||||||
(let [id (:id library)
|
(let [id (:id library)
|
||||||
importing? (deref importing)
|
importing? (deref importing)
|
||||||
|
|
||||||
|
team-id (mf/use-ctx ctx/current-team-id)
|
||||||
|
|
||||||
on-error
|
on-error
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [_]
|
(fn [_]
|
||||||
|
@ -150,11 +152,13 @@
|
||||||
|
|
||||||
on-success
|
on-success
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
(mf/deps team-id)
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(st/emit! (dtm/fetch-shared-files))))
|
(st/emit! (dtm/fetch-shared-files team-id))))
|
||||||
|
|
||||||
import-library
|
import-library
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
(mf/deps on-success on-error)
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(reset! importing id)
|
(reset! importing id)
|
||||||
(st/emit! (dd/clone-template
|
(st/emit! (dd/clone-template
|
||||||
|
@ -565,6 +569,7 @@
|
||||||
file (deref refs/file)
|
file (deref refs/file)
|
||||||
|
|
||||||
file-id (:id file)
|
file-id (:id file)
|
||||||
|
team-id (:team-id file)
|
||||||
shared? (:is-shared file)
|
shared? (:is-shared file)
|
||||||
|
|
||||||
linked-libraries
|
linked-libraries
|
||||||
|
@ -611,8 +616,8 @@
|
||||||
:id "updates"
|
:id "updates"
|
||||||
:content updates-tab}]]
|
:content updates-tab}]]
|
||||||
|
|
||||||
(mf/with-effect []
|
(mf/with-effect [team-id]
|
||||||
(st/emit! (dtm/fetch-shared-files)))
|
(st/emit! (dtm/fetch-shared-files team-id)))
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-overlay)
|
[:div {:class (stl/css :modal-overlay)
|
||||||
:on-click close-dialog-outside
|
:on-click close-dialog-outside
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue