0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-27 15:11:26 -05:00

Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Alejandro Alonso 2024-06-13 11:04:58 +02:00
commit 7e87362a39
48 changed files with 995 additions and 145 deletions

View file

@ -26,9 +26,13 @@
- Allow library colors as recent colors [Taiga #7640](https://tree.taiga.io/project/penpot/issue/7640)
- Missing scroll in viewmode comments [Taiga #7427](https://tree.taiga.io/project/penpot/issue/7427)
- Comments in View mode should mimic the positioning behavior of the Workspace [Taiga #7346](https://tree.taiga.io/project/penpot/issue/7346)
- Misaligned input on comments [Taiga #7461](https://tree.taiga.io/project/penpot/issue/7461)
### :bug: Bugs fixed
- Fix selection rectangle appears on scroll [Taiga #7525](https://tree.taiga.io/project/penpot/issue/7525)
- Fix layer tree not expanding to the bottom edge [Taiga #7466](https://tree.taiga.io/project/penpot/issue/7466)
- Fix guides move when board is moved by inputs [Taiga #8010](https://tree.taiga.io/project/penpot/issue/8010)
- Fix clickable area of Penptot logo in the viewer [Taiga #7988](https://tree.taiga.io/project/penpot/issue/7988)
- Fix constraints dropdown when selecting multiple shapes [Taiga #7686](https://tree.taiga.io/project/penpot/issue/7686)
- Layout and scrollign fixes for the bottom palette [Taiga #7559](https://tree.taiga.io/project/penpot/issue/7559)
@ -43,7 +47,12 @@
- Fix "Attribute overrides in copies are not exported in zip file" [Taiga #8072](https://tree.taiga.io/project/penpot/issue/8072)
- Fix group not automatically selected in the Layers panel after creation [Taiga #8078](https://tree.taiga.io/project/penpot/issue/8078)
- Fix export boards loses opacity [Taiga #7592](https://tree.taiga.io/project/penpot/issue/7592)
- Fix change color on imported svg also changes the stroke alignment[Taiga #7673](https://github.com/penpot/penpot/pull/7673)
- Fix show in view mode and interactions workflow [Taiga #4711](https://github.com/penpot/penpot/pull/4711)
- Fix internal error when I set up a stroke for some objects without and with stroke [Taiga #7558](https://tree.taiga.io/project/penpot/issue/7558)
- Toolbar keeps toggling on and off on spacebar press [Taiga #7654](https://github.com/penpot/penpot/pull/7654)
- Fix toolbar keeps hiding when click outside workspace [Taiga #7776](https://tree.taiga.io/project/penpot/issue/7776)
- Fix open overlay relative to a frame [Taiga #7563](https://tree.taiga.io/project/penpot/issue/7563)
## 2.0.3

View file

@ -2,13 +2,19 @@
We want to thank to the amazing people that help us! Thank you! You're the best!
Feel free you make a PR updating this file if you miss you in the
list.
## Security
* Husnain Iqbal (CEO OF ALPHA INFERNO PVT LTD)
* [Shiraz Ali Khan](https://www.linkedin.com/in/shiraz-ali-khan-1ba508180/)
* Vaibhav Shukla
* Hassan Ahmed (Alias Xen Lee)
* Michal Biesiada (@mbiesiad)
## Internationalization
* [00ff88](https://hosted.weblate.org/user/00ff88)
* [AhmadHB](https://hosted.weblate.org/user/AhmadHB)
* [Aimee](https://hosted.weblate.org/user/Aimee)
@ -90,6 +96,7 @@ We want to thank to the amazing people that help us! Thank you! You're the best!
* [zcraber](https://hosted.weblate.org/user/zcraber)
## Libraries & templates
* systxema
* plumilla
* victor crespo

View file

@ -87,7 +87,10 @@
:ldap-attrs-fullname "cn"
;; a server prop key where initial project is stored.
:initial-project-skey "initial-project"})
:initial-project-skey "initial-project"
;; time to avoid email sending after profile modification
:email-verify-threshold "15m"})
(s/def ::default-rpc-rlimit ::us/vector-of-strings)
(s/def ::rpc-rlimit-config ::fs/path)
@ -213,6 +216,7 @@
(s/def ::telemetry-uri ::us/string)
(s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string)
(s/def ::email-verify-threshold ::dt/duration)
(s/def ::config
(s/keys :opt-un [::secret-key
@ -334,7 +338,8 @@
::telemetry-uri
::telemetry-referer
::telemetry-with-taiga
::tenant]))
::tenant
::email-verify-threshold]))
(def default-flags
[:enable-backend-api-doc

View file

@ -38,13 +38,11 @@
(def schema:token
[::sm/word-string {:max 6000}])
(def ^:private default-verify-threshold
(dt/duration "15m"))
(defn- elapsed-verify-threshold?
[profile]
(let [elapsed (dt/diff (:modified-at profile) (dt/now))]
(pos? (compare elapsed default-verify-threshold))))
(let [elapsed (dt/diff (:modified-at profile) (dt/now))
verify-threshold (cf/get :email-verify-threshold)]
(pos? (compare elapsed verify-threshold))))
;; ---- COMMAND: login with password
@ -130,12 +128,21 @@
;; ---- COMMAND: Logout
(def ^:private schema:logout
[:map {:title "logoug"}
[:profile-id {:optional true} ::sm/uuid]])
(sv/defmethod ::logout
"Clears the authentication cookie and logout the current session."
{::rpc/auth false
::doc/added "1.15"}
[cfg _]
(rph/with-transform {} (session/delete-fn cfg)))
::doc/changes [["2.1" "Now requires profile-id passed in the body"]]
::doc/added "1.0"
::sm/params schema:logout}
[cfg params]
(if (= (:profile-id params)
(::rpc/profile-id params))
(rph/with-transform {} (session/delete-fn cfg))
{}))
;; ---- COMMAND: Recover Profile

View file

@ -184,10 +184,7 @@
(ctk/instance-head? child))
(let [slot (guess-swap-slot component-child component-container)]
(l/dbg :hint "child" :id (:id child) :name (:name child) :slot slot)
(ctn/update-shape container (:id child)
#(update % :touched
cfh/set-touched-group
(ctk/build-swap-slot-group slot))))
(ctn/update-shape container (:id child) #(ctk/set-swap-slot % slot)))
container)]
(recur (process-copy-head container child)
(rest children)

View file

@ -481,7 +481,7 @@
(let [slot (:swap-slot args)]
(when (some? slot)
(log/debug :hint (str " -> set swap-slot to " slot))
(update shape :touched cfh/set-touched-group (ctk/build-swap-slot-group slot)))))]
(ctk/set-swap-slot shape slot))))]
(log/dbg :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)

View file

@ -284,9 +284,17 @@
(let [children (cfh/get-children-with-self (:objects container) shape-id)
skip-near (fn [changes shape]
(let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})]
(if (some? (:shape-ref ref-shape))
(pcb/update-shapes changes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape)))
changes)))]
(cond-> changes
(some? (:shape-ref ref-shape))
(pcb/update-shapes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape)))
;; When advancing level, if the referenced shape has a swap slot, it must be
;; copied to the current shape, because the shape-ref now will not be pointing
;; to a near main (except for first level subcopies).
(and (some? (ctk/get-swap-slot ref-shape))
(nil? (ctk/get-swap-slot shape))
(not= (:id shape) shape-id))
(pcb/update-shapes [(:id shape)] #(ctk/set-swap-slot % (ctk/get-swap-slot ref-shape))))))]
(reduce skip-near changes children)))
(defn prepare-restore-component
@ -1194,7 +1202,7 @@
:shapes all-parents}))
changes' (reduce del-obj-change changes' new-shapes)]
(if (and (cfh/touched-group? parent-shape :shapes-group) omit-touched?)
(if (and (ctk/touched-group? parent-shape :shapes-group) omit-touched?)
changes
changes')))
@ -1349,7 +1357,7 @@
changes'
ids)]
(if (and (cfh/touched-group? parent :shapes-group) omit-touched?)
(if (and (ctk/touched-group? parent :shapes-group) omit-touched?)
changes
changes')))
@ -1385,7 +1393,7 @@
:ignore-touched true
:syncing true})))]
(if (and (cfh/touched-group? parent :shapes-group) omit-touched?)
(if (and (ctk/touched-group? parent :shapes-group) omit-touched?)
changes
changes')))
@ -1846,12 +1854,11 @@
;; if the shape isn't inside a main component, it shouldn't have a swap slot
(and (nil? (ctk/get-swap-slot new-shape))
inside-comp?)
(update :touched cfh/set-touched-group (-> (ctf/find-swap-slot shape
page
{:id (:id file)
:data file}
libraries)
(ctk/build-swap-slot-group))))]
(ctk/set-swap-slot (ctf/find-swap-slot shape
page
{:id (:id file)
:data file}
libraries)))]
[new-shape (-> changes
;; Restore the properties

View file

@ -183,6 +183,15 @@
(and (= shape-id (:main-instance-id component))
(= page-id (:main-instance-page component))))
(defn set-touched-group
[touched group]
(when group
(conj (or touched #{}) group)))
(defn touched-group?
[shape group]
((or (:touched shape) #{}) group))
(defn build-swap-slot-group
"Convert a swap-slot into a :touched group"
[swap-slot]
@ -204,6 +213,13 @@
(when group
(group->swap-slot group))))
(defn set-swap-slot
"Add a touched group with a form :swap-slot-<uuid>."
[shape swap-slot]
(cond-> shape
(some? swap-slot)
(update :touched set-touched-group (build-swap-slot-group swap-slot))))
(defn match-swap-slot?
[shape-main shape-inst]
(let [slot-main (get-swap-slot shape-main)

View file

@ -0,0 +1,343 @@
{
"~:features":{
"~#set":[
"layout/grid",
"styles/v2",
"fdata/shape-data-type"
]
},
"~:permissions":{
"~:type":"~:membership",
"~:is-owner":true,
"~:is-admin":true,
"~:can-edit":true,
"~:can-read":true,
"~:is-logged":true
},
"~:has-media-trimmed":false,
"~:comment-thread-seqn":0,
"~:name":"New File 12",
"~:revn":2,
"~:modified-at":"~m1718012938567",
"~:id":"~u1795a568-0df0-8095-8004-7ba741f56be2",
"~:is-shared":false,
"~:version":48,
"~:project-id":"~u4dc640b0-5cbf-11ec-a7c5-91e9eb4f238d",
"~:created-at":"~m1718012912598",
"~:data":{
"~:pages":[
"~u1795a568-0df0-8095-8004-7ba741f56be3"
],
"~:pages-index":{
"~u1795a568-0df0-8095-8004-7ba741f56be3":{
"~:options":{
},
"~:objects":{
"~u00000000-0000-0000-0000-000000000000":{
"~#shape":{
"~:y":0,
"~:hide-fill-on-export":false,
"~:transform":{
"~#matrix":{
"~:a":1.0,
"~:b":0.0,
"~:c":0.0,
"~:d":1.0,
"~:e":0.0,
"~:f":0.0
}
},
"~:rotation":0,
"~:name":"Root Frame",
"~:width":0.01,
"~:type":"~:frame",
"~:points":[
{
"~#point":{
"~:x":0,
"~:y":0
}
},
{
"~#point":{
"~:x":0.01,
"~:y":0
}
},
{
"~#point":{
"~:x":0.01,
"~:y":0.01
}
},
{
"~#point":{
"~:x":0,
"~:y":0.01
}
}
],
"~:proportion-lock":false,
"~:transform-inverse":{
"~#matrix":{
"~:a":1.0,
"~:b":0.0,
"~:c":0.0,
"~:d":1.0,
"~:e":0.0,
"~:f":0.0
}
},
"~:id":"~u00000000-0000-0000-0000-000000000000",
"~:parent-id":"~u00000000-0000-0000-0000-000000000000",
"~:frame-id":"~u00000000-0000-0000-0000-000000000000",
"~:strokes":[
],
"~:x":0,
"~:proportion":1.0,
"~:selrect":{
"~#rect":{
"~:x":0,
"~:y":0,
"~:width":0.01,
"~:height":0.01,
"~:x1":0,
"~:y1":0,
"~:x2":0.01,
"~:y2":0.01
}
},
"~:fills":[
{
"~:fill-color":"#FFFFFF",
"~:fill-opacity":1
}
],
"~:flip-x":null,
"~:height":0.01,
"~:flip-y":null,
"~:shapes":[
"~u2ace9ce8-8e01-8086-8004-7ba745d4305a",
"~u2ace9ce8-8e01-8086-8004-7ba748566e02"
]
}
},
"~u2ace9ce8-8e01-8086-8004-7ba745d4305a":{
"~#shape":{
"~:y":221,
"~:rx":0,
"~:transform":{
"~#matrix":{
"~:a":1.0,
"~:b":0.0,
"~:c":0.0,
"~:d":1.0,
"~:e":0.0,
"~:f":0.0
}
},
"~:rotation":0,
"~:grow-type":"~:fixed",
"~:hide-in-viewer":false,
"~:name":"Rectangle",
"~:width":105,
"~:type":"~:rect",
"~:points":[
{
"~#point":{
"~:x":165,
"~:y":221
}
},
{
"~#point":{
"~:x":270,
"~:y":221
}
},
{
"~#point":{
"~:x":270,
"~:y":316
}
},
{
"~#point":{
"~:x":165,
"~:y":316
}
}
],
"~:proportion-lock":false,
"~:transform-inverse":{
"~#matrix":{
"~:a":1.0,
"~:b":0.0,
"~:c":0.0,
"~:d":1.0,
"~:e":0.0,
"~:f":0.0
}
},
"~:id":"~u2ace9ce8-8e01-8086-8004-7ba745d4305a",
"~:parent-id":"~u00000000-0000-0000-0000-000000000000",
"~:frame-id":"~u00000000-0000-0000-0000-000000000000",
"~:strokes":[
],
"~:x":165,
"~:proportion":1,
"~:selrect":{
"~#rect":{
"~:x":165,
"~:y":221,
"~:width":105,
"~:height":95,
"~:x1":165,
"~:y1":221,
"~:x2":270,
"~:y2":316
}
},
"~:fills":[
{
"~:fill-color":"#B1B2B5",
"~:fill-opacity":1
}
],
"~:flip-x":null,
"~:ry":0,
"~:height":95,
"~:flip-y":null
}
},
"~u2ace9ce8-8e01-8086-8004-7ba748566e02":{
"~#shape":{
"~:y":228,
"~:transform":{
"~#matrix":{
"~:a":1.0,
"~:b":0.0,
"~:c":0.0,
"~:d":1.0,
"~:e":0.0,
"~:f":0.0
}
},
"~:rotation":0,
"~:grow-type":"~:fixed",
"~:hide-in-viewer":false,
"~:name":"Ellipse",
"~:width":85,
"~:type":"~:circle",
"~:points":[
{
"~#point":{
"~:x":344,
"~:y":228
}
},
{
"~#point":{
"~:x":429,
"~:y":228
}
},
{
"~#point":{
"~:x":429,
"~:y":308
}
},
{
"~#point":{
"~:x":344,
"~:y":308
}
}
],
"~:proportion-lock":false,
"~:transform-inverse":{
"~#matrix":{
"~:a":1.0,
"~:b":0.0,
"~:c":0.0,
"~:d":1.0,
"~:e":0.0,
"~:f":0.0
}
},
"~:blur":{
"~:id":"~u2ace9ce8-8e01-8086-8004-7ba757cdd271",
"~:type":"~:layer-blur",
"~:value":4,
"~:hidden":false
},
"~:id":"~u2ace9ce8-8e01-8086-8004-7ba748566e02",
"~:parent-id":"~u00000000-0000-0000-0000-000000000000",
"~:frame-id":"~u00000000-0000-0000-0000-000000000000",
"~:strokes":[
{
"~:stroke-alignment":"~:inner",
"~:stroke-style":"~:solid",
"~:stroke-color":"#000000",
"~:stroke-opacity":1,
"~:stroke-width":1
}
],
"~:x":344,
"~:proportion":1,
"~:shadow":[
{
"~:color":{
"~:color":"#000000",
"~:opacity":0.2
},
"~:spread":0,
"~:offset-y":4,
"~:style":"~:drop-shadow",
"~:blur":4,
"~:hidden":false,
"~:id":"~u2ace9ce8-8e01-8086-8004-7ba756ddebd5",
"~:offset-x":4
}
],
"~:selrect":{
"~#rect":{
"~:x":344,
"~:y":228,
"~:width":85,
"~:height":80,
"~:x1":344,
"~:y1":228,
"~:x2":429,
"~:y2":308
}
},
"~:fills":[
{
"~:fill-color":"#1247e7",
"~:fill-opacity":1
}
],
"~:flip-x":null,
"~:height":80,
"~:flip-y":null
}
}
},
"~:id":"~u1795a568-0df0-8095-8004-7ba741f56be3",
"~:name":"Page 1"
}
},
"~:id":"~u1795a568-0df0-8095-8004-7ba741f56be2",
"~:recent-colors":[
{
"~:color":"#1247e7",
"~:opacity":1
}
]
}
}

View file

@ -4,11 +4,6 @@ export class DashboardPage extends BaseWebSocketPage {
static async init(page) {
await BaseWebSocketPage.initWebSockets(page);
await BaseWebSocketPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
await BaseWebSocketPage.mockRPC(page, "get-teams", "logged-in-user/get-teams-default.json");
await BaseWebSocketPage.mockRPC(
page,

View file

@ -0,0 +1,45 @@
import { BaseWebSocketPage } from "./BaseWebSocketPage";
export class OnboardingPage extends BaseWebSocketPage {
constructor(page) {
super(page);
this.submitButton = page.getByRole("Button",{ name: "Next" })
}
async fillOnboardingInputsStep1() {
await this.page.getByText('Personal').click();
await this.page.getByText('Select option').click();
await this.page.getByText('Testing before self-hosting').click();
await this.submitButton.click();
}
async fillOnboardingInputsStep2() {
await this.page.getByText('Figma').click();
await this.submitButton.click();
}
async fillOnboardingInputsStep3() {
await this.page.getByText('Select option').first().click();
await this.page.getByText('Product Managment').click();
await this.page.getByText('Select option').first().click();
await this.page.getByText('Director').click();
await this.page.getByText('Select option').click();
await this.page.getByText('11-30').click();
await this.submitButton.click();
}
async fillOnboardingInputsStep4() {
await this.page.getByText('Other').click();
await this.page.getByPlaceholder('Other (specify)').fill("Another");
await this.submitButton.click();
}
async fillOnboardingInputsStep5() {
await this.page.getByText('Event').click();
}
}
export default OnboardingPage;

View file

@ -43,13 +43,15 @@ export class WorkspacePage extends BaseWebSocketPage {
this.presentUserListItems = page.getByTestId("active-users-list").getByAltText("Princesa Leia");
this.viewport = page.getByTestId("viewport");
this.rootShape = page.locator(`[id="shape-00000000-0000-0000-0000-000000000000"]`);
this.toolbarOptions = page.getByTestId("toolbar-options");
this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" });
this.toggleToolbarButton = page.getByRole("button", { name: "Toggle toolbar" });
this.colorpicker = page.getByTestId("colorpicker");
this.layers = page.getByTestId("layers");
this.layers = page.getByTestId("layer-tree");
this.palette = page.getByTestId("palette");
this.assets = page.getByTestId("assets");
this.libraries = page.getByTestId("libraries");
this.closeLibraries = page.getByTestId("close-libraries");
this.sidebar = page.getByTestId("left-sidebar");
this.selectionRect = page.getByTestId("workspace-selection-rect");
this.horizontalScrollbar = page.getByTestId("horizontal-scrollbar");
this.librariesModal = page.getByTestId("libraries-modal");
}
@ -102,6 +104,19 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.mouse.up();
}
async panOnViewportAt(x, y, width, height) {
await this.page.waitForTimeout(100);
await this.viewport.hover({ position: { x, y } });
await this.page.mouse.down({ button: "middle" });
await this.viewport.hover({ position: { x: x + width, y: y + height } });
await this.page.mouse.up({ button: "middle" });
}
async togglePages() {
const pagesToggle = this.page.getByText("Pages");
await pagesToggle.click();
}
async moveSelectionToShape(name) {
await this.page.locator('rect.viewport-selrect').hover();
await this.page.mouse.down();
@ -120,15 +135,21 @@ export class WorkspacePage extends BaseWebSocketPage {
}
async expectSelectedLayer(name) {
await expect(this.layers.getByTestId("layer-row").filter({ has: this.page.getByText(name) })).toHaveClass(/selected/);
await expect(this.layers.getByTestId("layer-row").filter({ has: this.page.getByText(name) })).toHaveClass(
/selected/,
);
}
async expectHiddenToolbarOptions() {
await expect(this.toolbarOptions).toHaveCSS("opacity", "0");
}
async clickAssets(clickOptions = {}) {
await this.assets.click(clickOptions);
await this.sidebar.getByText("Assets").click(clickOptions);
}
async clickLibraries(clickOptions = {}) {
await this.libraries.click(clickOptions);
async openLibrariesModal(clickOptions = {}) {
await this.sidebar.getByText("Libraries").click(clickOptions);
}
async clickLibrary(name, clickOptions = {}) {
@ -136,11 +157,15 @@ export class WorkspacePage extends BaseWebSocketPage {
.getByTestId("library-item")
.filter({ hasText: name })
.getByRole("button")
.click(clickOptions);
.click(clickOptions);
}
async clickCloseLibraries(clickOptions = {}) {
await this.closeLibraries.click(clickOptions);
async closeLibrariesModal(clickOptions = {}) {
await this.librariesModal.getByRole("button", { name: "Close" }).click(clickOptions);
}
async clickColorPalette(clickOptions = {}) {
await this.palette.getByRole("button", { name: "Color Palette (Alt+P)" }).click(clickOptions);
}
async clickColorPalette(clickOptions = {}) {

View file

@ -3,6 +3,11 @@ 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("Dashboad page has title ", async ({ page }) => {

View file

@ -7,6 +7,8 @@ test.beforeEach(async ({ page }) => {
const multipleConstraintsFileId = `03bff843-920f-81a1-8004-756365e1eb6a`;
const multipleConstraintsPageId = `03bff843-920f-81a1-8004-756365e1eb6b`;
const multipleAttributesFileId = `1795a568-0df0-8095-8004-7ba741f56be2`;
const multipleAttributesPageId = `1795a568-0df0-8095-8004-7ba741f56be3`;
const setupFileWithMultipeConstraints = async (workspace) => {
await workspace.setupEmptyFile();
@ -21,6 +23,15 @@ const setupFileWithMultipeConstraints = async (workspace) => {
);
};
const setupFileWithMultipeAttributes = async (workspace) => {
await workspace.setupEmptyFile();
await workspace.mockRPC(/get\-file\?/, "design/get-file-multiple-attributes.json");
await workspace.mockRPC(
"get-file-object-thumbnails?file-id=*",
"design/get-file-object-thumbnails-multiple-attributes.json",
);
};
test.describe("Constraints", () => {
test("Constraint dropdown shows 'Mixed' when multiple layers are selected with different constraints", async ({
page,
@ -45,6 +56,43 @@ test.describe("Constraints", () => {
});
});
test.describe("Multiple shapes attributes", () => {
test("User selects multiple shapes with sames fills, strokes, shadows and blur", async ({ page }) => {
const workspace = new WorkspacePage(page);
await setupFileWithMultipeConstraints(workspace);
await workspace.goToWorkspace({
fileId: multipleConstraintsFileId,
pageId: multipleConstraintsPageId,
});
await workspace.clickToggableLayer("Board");
await workspace.clickLeafLayer("Ellipse");
await workspace.clickLeafLayer("Rectangle", { modifiers: ["Shift"] });
await expect(workspace.page.getByTestId("add-fill")).toBeVisible();
await expect(workspace.page.getByTestId("add-stroke")).toBeVisible();
await expect(workspace.page.getByTestId("add-shadow")).toBeVisible();
await expect(workspace.page.getByTestId("add-blur")).toBeVisible();
});
test("User selects multiple shapes with different fills, strokes, shadows and blur", async ({ page }) => {
const workspace = new WorkspacePage(page);
await setupFileWithMultipeAttributes(workspace);
await workspace.goToWorkspace({
fileId: multipleAttributesFileId,
pageId: multipleAttributesPageId,
});
await workspace.clickLeafLayer("Ellipse");
await workspace.clickLeafLayer("Rectangle", { modifiers: ["Shift"] });
await expect(workspace.page.getByTestId("add-fill")).toBeHidden();
await expect(workspace.page.getByTestId("add-stroke")).toBeHidden();
await expect(workspace.page.getByTestId("add-shadow")).toBeHidden();
await expect(workspace.page.getByTestId("add-blur")).toBeHidden();
});
});
test("BUG 7760 - Layout losing properties when changing parents", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();

View file

@ -0,0 +1,32 @@
import { test, expect } from "@playwright/test";
import DashboardPage from "../pages/DashboardPage";
import OnboardingPage from "../pages/OnboardingPage"
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(page, "get-profile", "logged-in-user/get-profile-logged-in.json");
});
test("User can complete the onboarding", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
const onboardingPage = new OnboardingPage(page);
await dashboardPage.goToWorkspace();
await expect(page.getByRole("heading", { name: "Help us get to know you" })).toBeVisible();
await onboardingPage.fillOnboardingInputsStep1();
await expect(page.getByRole("heading", { name: "Which one of these tools do" })).toBeVisible();
await onboardingPage.fillOnboardingInputsStep2();
await expect(page.getByRole("heading", { name: "Tell us about your job" })).toBeVisible();
await onboardingPage.fillOnboardingInputsStep3();
await expect(page.getByRole("heading", { name: "Where would you like to get" })).toBeVisible();
await onboardingPage.fillOnboardingInputsStep4();
await expect(page.getByRole("heading", { name: "How did you hear about Penpot?" })).toBeVisible();
await onboardingPage.fillOnboardingInputsStep5();
await expect(page.getByRole("button", { name: "Start" })).toBeEnabled();
});

View file

@ -0,0 +1,56 @@
import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../pages/WorkspacePage";
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
});
test.describe("Layers tab", () => {
test("BUG 7466 - Layers tab height extends to the bottom when 'Pages' is collapsed", async ({ page }) => {
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.goToWorkspace();
const { height: heightExpanded } = await workspace.layers.boundingBox();
await workspace.togglePages();
const { height: heightCollapsed } = await workspace.layers.boundingBox();
expect(heightExpanded > heightCollapsed);
});
});
test.describe("Assets tab", () => {
test("User adds a library and its automatically selected in the color palette", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC("link-file-to-library", "workspace/link-file-to-library.json");
await workspacePage.mockRPC("unlink-file-from-library", "workspace/unlink-file-from-library.json");
await workspacePage.mockRPC(
"get-team-shared-files?team-id=*",
"workspace/get-team-shared-libraries-non-empty.json",
);
await workspacePage.goToWorkspace();
// Add Testing library 1
await workspacePage.clickColorPalette();
await workspacePage.clickAssets();
// Now the get-file call should return a library
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json");
await workspacePage.openLibrariesModal();
await workspacePage.clickLibrary("Testing library 1");
await workspacePage.closeLibrariesModal();
await expect(workspacePage.palette.getByRole("button", { name: "test-color-187cd5" })).toBeVisible();
// Remove Testing library 1
await workspacePage.openLibrariesModal();
await workspacePage.clickLibrary("Testing library 1");
await workspacePage.closeLibrariesModal();
await expect(
workspacePage.palette.getByText("There are no color styles in your library yet"),
).toBeVisible();
});
});

View file

@ -39,6 +39,60 @@ test("User draws a rect", async ({ page }) => {
await expect(shape).toHaveAttribute("height", "100");
});
test("User makes a group", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json");
await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-create-rect.json");
await workspacePage.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
await workspacePage.clickLeafLayer("Rectangle");
await workspacePage.page.keyboard.press("ControlOrMeta+g");
await workspacePage.expectSelectedLayer("Group");
});
test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.toggleToolbarButton.click();
await workspacePage.page.keyboard.press("Backspace");
await workspacePage.page.keyboard.press("Enter");
await workspacePage.expectHiddenToolbarOptions();
});
test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json");
await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-create-rect.json");
await workspacePage.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
// Move created rect to a corner, in orther to get scrollbars
await workspacePage.panOnViewportAt(128, 128, 300, 300);
// Check scrollbars appear
const horizontalScrollbar = workspacePage.horizontalScrollbar;
await expect(horizontalScrollbar).toBeVisible();
// Grab scrollbar and move
const {x, y} = await horizontalScrollbar.boundingBox();
await page.waitForTimeout(100);
await workspacePage.viewport.hover({ position: { x: x, y: y + 5 } });
await page.mouse.down();
await workspacePage.viewport.hover({ position: { x: x - 130, y: y - 95 } });
await expect(workspacePage.selectionRect).not.toBeInViewport();
});
test("User adds a library and its automatically selected in the color palette", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
@ -53,31 +107,16 @@ test("User adds a library and its automatically selected in the color palette",
await workspacePage.clickAssets();
// Now the get-file call should return a library
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json");
await workspacePage.clickLibraries();
await workspacePage.openLibrariesModal();
await workspacePage.clickLibrary("Testing library 1")
await workspacePage.clickCloseLibraries();
await workspacePage.closeLibrariesModal();
await expect(workspacePage.palette.getByRole("button", { name: "test-color-187cd5" })).toBeVisible();
// Remove Testing library 1
await workspacePage.clickLibraries();
await workspacePage.openLibrariesModal();
await workspacePage.clickLibrary("Testing library 1")
await workspacePage.clickCloseLibraries();
await workspacePage.closeLibrariesModal();
await expect(workspacePage.palette.getByText('There are no color styles in your library yet')).toBeVisible();
});
test("User makes a group", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json");
await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-create-rect.json");
await workspacePage.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375"
});
await workspacePage.clickLeafLayer("Rectangle");
await workspacePage.page.keyboard.press("ControlOrMeta+g");
await workspacePage.expectSelectedLayer("Group");
});

View file

@ -328,11 +328,15 @@
(-data [_] {})
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :logout)
(rx/delay-at-least 300)
(rx/catch (constantly (rx/of 1)))
(rx/map #(logged-out params)))))))
(watch [_ state _]
(let [profile-id (:profile-id state)]
(->> (rx/interval 500)
(rx/take 1)
(rx/mapcat (fn [_]
(->> (rp/cmd! :logout {:profile-id profile-id})
(rx/delay-at-least 300)
(rx/catch (constantly (rx/of 1))))))
(rx/map #(logged-out params))))))))
;; --- Update Profile

View file

@ -248,7 +248,7 @@
(assoc :stroke-style :solid)
(not (contains? new-attrs :stroke-alignment))
(assoc :stroke-alignment :inner)
(assoc :stroke-alignment :center)
:always
(d/without-nils))]

View file

@ -198,7 +198,8 @@
(dws/select-shapes (d/ordered-set (:id group))))
(ptk/data-event :layout/update {:ids parents}))))))))
(def group-selected
(defn group-selected
[]
(ptk/reify ::group-selected
ptk/WatchEvent
(watch [_ state _]
@ -258,7 +259,8 @@
(when change-selection?
(dws/select-shapes child-ids))))))))
(def ungroup-selected
(defn ungroup-selected
[]
(ptk/reify ::ungroup-selected
ptk/WatchEvent
(watch [_ state _]

View file

@ -79,20 +79,21 @@
(rx/from (->> guides (mapv #(remove-guide %))))))))
(defmethod ptk/resolve ::move-frame-guides
[_ ids]
[_ args]
(dm/assert!
"expected a coll of uuids"
(every? uuid? ids))
(every? uuid? (:ids args)))
(ptk/reify ::move-frame-guides
ptk/WatchEvent
(watch [_ state _]
(let [objects (wsh/lookup-page-objects state)
(let [ids (:ids args)
object-modifiers (:modifiers args)
objects (wsh/lookup-page-objects state)
is-frame? (fn [id] (= :frame (get-in objects [id :type])))
frame-ids? (into #{} (filter is-frame?) ids)
object-modifiers (get state :workspace-modifiers)
build-move-event
(fn [guide]
(let [frame (get objects (:frame-id guide))

View file

@ -497,7 +497,7 @@
(if undo-transation?
(rx/of (dwu/start-undo-transaction undo-id))
(rx/empty))
(rx/of (ptk/event ::dwg/move-frame-guides ids-with-children)
(rx/of (ptk/event ::dwg/move-frame-guides {:ids ids-with-children :modifiers object-modifiers})
(ptk/event ::dwcm/move-frame-comment-threads ids-with-children)
(dwsh/update-shapes
ids

View file

@ -22,6 +22,7 @@
(or (= type ::common/finish-path)
(= type :app.main.data.workspace.path.shortcuts/esc-pressed)
(= type :app.main.data.workspace.common/clear-edition-mode)
(= type :app.main.data.workspace.edition/clear-edition-mode)
(= type :app.main.data.workspace/finalize-page)
(= event :interrupt) ;; ESC
(and ^boolean (mse/mouse-event? event)

View file

@ -119,12 +119,12 @@
:group {:tooltip (ds/meta "G")
:command (ds/c-mod "g")
:subsections [:modify-layers]
:fn #(emit-when-no-readonly dw/group-selected)}
:fn #(emit-when-no-readonly (dw/group-selected))}
:ungroup {:tooltip (ds/shift "G")
:command "shift+g"
:subsections [:modify-layers]
:fn #(emit-when-no-readonly dw/ungroup-selected)}
:fn #(emit-when-no-readonly (dw/ungroup-selected))}
:mask {:tooltip (ds/meta "M")
:command (ds/c-mod "m")

View file

@ -20,6 +20,7 @@
[app.main.ui.icons :as i]
[app.util.i18n :refer [tr tr-html]]
[app.util.router :as rt]
[app.util.storage :as sto]
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
[rumext.v2 :as mf]))
@ -163,11 +164,7 @@
;; --- PAGE: register validation
(defn- handle-register-error
[_form _data]
(st/emit! (msg/error (tr "errors.generic"))))
(defn- handle-register-success
(defn- on-register-success
[data]
(cond
(some? (:invitation-token data))
@ -178,7 +175,9 @@
(st/emit! (du/login-from-register))
:else
(st/emit! (rt/nav :auth-register-success {} {:email (:email data)}))))
(do
(swap! sto/storage assoc ::email (:email data))
(st/emit! (rt/nav :auth-register-success)))))
(s/def ::accept-terms-and-privacy (s/and ::us/boolean true?))
(s/def ::accept-newsletter-subscription ::us/boolean)
@ -192,31 +191,63 @@
:opt-un [::accept-terms-and-privacy
::accept-newsletter-subscription])))
(mf/defc terms-and-privacy
{::mf/props :obj
::mf/private true}
[]
(let [terms-label
(mf/html
[:& tr-html
{:tag-name "div"
:label "auth.terms-and-privacy-agreement"
:params [cf/terms-of-service-uri cf/privacy-policy-uri]}])]
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
[:& fm/input {:name :accept-terms-and-privacy
:class (stl/css :checkbox-terms-and-privacy)
:type "checkbox"
:default-checked false
:label terms-label}]]))
(mf/defc register-validate-form
{::mf/props :obj}
[{:keys [params on-success-callback]}]
(let [form (fm/use-form :spec ::register-validate-form
:validators [(fm/validate-not-empty :fullname (tr "auth.name.not-all-space"))
(fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))]
(let [validators (mf/with-memo []
[(fm/validate-not-empty :fullname (tr "auth.name.not-all-space"))
(fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))])
form (fm/use-form :spec ::register-validate-form
:validators validators
:initial params)
submitted? (mf/use-state false)
on-success (fn [p]
(if (nil? on-success-callback)
(handle-register-success p)
(on-success-callback (:email p))))
on-success
(mf/use-fn
(mf/deps on-success-callback)
(fn [params]
(if (nil? on-success-callback)
(on-register-success params)
(on-success-callback (:email params)))))
on-error
(mf/use-fn
(fn [_cause]
(st/emit! (msg/error (tr "errors.generic")))))
on-submit
(mf/use-fn
(fn [form _event]
(fn [form _]
(reset! submitted? true)
(let [params (:clean-data @form)]
(->> (rp/cmd! :register-profile params)
(rx/finalize #(reset! submitted? false))
(rx/subs! on-success
(partial handle-register-error form))))))]
(rx/subs! on-success on-error)))))]
[:& fm/form {:on-submit on-submit :form form
[:& fm/form {:on-submit on-submit
:form form
:class (stl/css :register-validate-form)}
[:div {:class (stl/css :fields-row)}
[:& fm/input {:name :fullname
:label (tr "auth.fullname")
@ -225,18 +256,7 @@
:class (stl/css :form-field)}]]
(when (contains? cf/flags :terms-and-privacy-checkbox)
(let [terms-label
(mf/html
[:& tr-html
{:tag-name "div"
:label "auth.terms-and-privacy-agreement"
:params [cf/terms-of-service-uri cf/privacy-policy-uri]}])]
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
[:& fm/input {:name :accept-terms-and-privacy
:class (stl/css :checkbox-terms-and-privacy)
:type "checkbox"
:default-checked false
:label terms-label}]]))
[:& terms-and-privacy])
[:> fm/submit-button*
{:label (tr "auth.register-submit")
@ -245,6 +265,7 @@
(mf/defc register-validate-page
{::mf/props :obj}
[{:keys [params]}]
[:div {:class (stl/css :auth-form-wrapper)}
[:h1 {:class (stl/css :logo-container)}
@ -263,13 +284,15 @@
(tr "labels.go-back")]]]])
(mf/defc register-success-page
[{:keys [params]}]
[:div {:class (stl/css :auth-form-wrapper :register-success)}
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
[:div {:class (stl/css :auth-title-wrapper)}
[:h2 {:class (stl/css :auth-title)}
(tr "auth.check-mail")]
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]]
[:div {:class (stl/css :notification-text-email)} (:email params "")]
[:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]])
{::mf/props :obj}
[]
(let [email (::email @sto/storage)]
[:div {:class (stl/css :auth-form-wrapper :register-success)}
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
[:div {:class (stl/css :auth-title-wrapper)}
[:h2 {:class (stl/css :auth-title)}
(tr "auth.check-mail")]
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]]
[:div {:class (stl/css :notification-text-email)} email]
[:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]]))

View file

@ -142,11 +142,10 @@
// thread-content
.thread-content {
position: absolute;
overflow-y: scroll;
scrollbar-gutter: stable;
overflow-y: auto;
width: $s-284;
padding: $s-12;
padding-inline-end: 0;
padding-inline-end: $s-8;
pointer-events: auto;
user-select: text;

View file

@ -44,9 +44,17 @@
(or (empty? overlays-ids) (nil? shape) (cfh/root? shape)) base-frame
:else (find-relative-to-base-frame (cfh/get-parent objects (:id shape)) objects overlays-ids base-frame)))
(defn- ignore-frame-shape
[shape objects manual?]
(let [shape (cond-> shape ;; When the the interaction is not manual and its origin is a frame,
;; we need to ignore it on all the find-frame calculations
(and (:frame-id shape) (not manual?))
(assoc :type :rect))
objects (assoc objects (:id shape) shape)]
[shape objects]))
(defn- activate-interaction
[interaction shape base-frame frame-offset objects overlays]
(case (:action-type interaction)
:navigate
(when-let [frame-id (:destination interaction)]
@ -58,9 +66,11 @@
(dv/go-to-frame frame-id (:animation interaction)))))
:open-overlay
(let [dest-frame-id (:destination interaction)
(let [manual? (= :manual (:overlay-pos-type interaction))
[shape objects] (ignore-frame-shape shape objects manual?)
dest-frame-id (:destination interaction)
dest-frame (get objects dest-frame-id)
relative-to-id (if (= :manual (:overlay-pos-type interaction))
relative-to-id (if manual?
(if (= (:type shape) :frame) ;; manual interactions are always from "self"
(:frame-id shape)
(:id shape))
@ -88,7 +98,9 @@
fixed-base?))))
:toggle-overlay
(let [dest-frame-id (:destination interaction)
(let [manual? (= :manual (:overlay-pos-type interaction))
[shape objects] (ignore-frame-shape shape objects manual?)
dest-frame-id (:destination interaction)
dest-frame (get objects dest-frame-id)
relative-to-id (if (= :manual (:overlay-pos-type interaction))
(if (= (:type shape) :frame) ;; manual interactions are always from "self"
@ -146,7 +158,9 @@
(st/emit! (dv/close-overlay frame-id)))
:toggle-overlay
(let [dest-frame-id (:destination interaction)
(let [manual? (= :manual (:overlay-pos-type interaction))
[shape objects] (ignore-frame-shape shape objects manual?)
dest-frame-id (:destination interaction)
dest-frame (get objects dest-frame-id)
relative-to-id (if (= :manual (:overlay-pos-type interaction))
(if (= (:type shape) :frame) ;; manual interactions are always from "self"
@ -178,7 +192,9 @@
:close-overlay
(let [dest-frame-id (:destination interaction)
(let [manual? (= :manual (:overlay-pos-type interaction))
[shape objects] (ignore-frame-shape shape objects manual?)
dest-frame-id (:destination interaction)
dest-frame (get objects dest-frame-id)
relative-to-id (if (= :manual (:overlay-pos-type interaction))
(if (= (:type shape) :frame) ;; manual interactions are always from "self"

View file

@ -243,8 +243,8 @@
is-group? (and single? has-group?)
is-bool? (and single? has-bool?)
do-create-group #(st/emit! dw/group-selected)
do-remove-group #(st/emit! dw/ungroup-selected)
do-create-group #(st/emit! (dw/group-selected))
do-remove-group #(st/emit! (dw/ungroup-selected))
do-mask-group #(st/emit! (dw/mask-group))
do-unmask-group #(st/emit! (dw/unmask-group))
do-create-artboard-from-selection

View file

@ -519,6 +519,7 @@
[:div {:class (stl/css :modal-dialog)}
[:button {:class (stl/css :close-btn)
:on-click close-dialog
:aria-label (tr "labels.close")
:data-testid "close-libraries"}
close-icon]
[:div {:class (stl/css :modal-title)}

View file

@ -38,8 +38,7 @@
(let [options-mode (mf/deref refs/options-mode-global)
mode-inspect? (= options-mode :inspect)
project (mf/deref refs/workspace-project)
show-pages? (mf/use-state true)
toggle-pages (mf/use-callback #(reset! show-pages? not))
section (cond (or mode-inspect? (contains? layout :layers)) :layers
(contains? layout :assets) :assets)
@ -50,9 +49,12 @@
{on-pointer-down :on-pointer-down on-lost-pointer-capture :on-lost-pointer-capture on-pointer-move :on-pointer-move parent-ref :parent-ref size :size}
(use-resize-hook :left-sidebar 275 275 500 :x false :left)
{on-pointer-down-pages :on-pointer-down on-lost-pointer-capture-pages :on-lost-pointer-capture on-pointer-move-pages :on-pointer-move size-pages :size}
{on-pointer-down-pages :on-pointer-down on-lost-pointer-capture-pages :on-lost-pointer-capture on-pointer-move-pages :on-pointer-move size-pages-opened :size}
(use-resize-hook :sitemap 200 38 400 :y false nil)
show-pages? (mf/use-state true)
toggle-pages (mf/use-callback #(reset! show-pages? not))
size-pages (mf/use-memo (mf/deps show-pages? size-pages-opened) (fn [] (if @show-pages? size-pages-opened 32)))
handle-collapse
(mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
@ -63,6 +65,7 @@
[:& (mf/provider muc/sidebar) {:value :left}
[:aside {:ref parent-ref
:id "left-sidebar-aside"
:data-testid "left-sidebar"
:data-size (str size)
:class (stl/css-case :left-settings-bar true
:global/two-row (<= size 300)

View file

@ -84,10 +84,8 @@ $width-settings-bar-max: $s-500;
.resize-area-horiz {
position: absolute;
// top: calc($s-88 + var(--height, 200px));
left: 0;
width: 100%;
// height: $s-8;
border-bottom: $s-2 solid var(--resize-area-border-color);
cursor: ns-resize;
}

View file

@ -510,7 +510,7 @@
(mf/use-fn
#(st/emit! (dw/toggle-focus-mode)))]
[:div#layers {:class (stl/css :layers) :data-testid "layers"}
[:div#layers {:class (stl/css :layers) :data-testid "layer-tree"}
(if (d/not-empty? focus)
[:div {:class (stl/css :tool-window-bar)}
[:button {:class (stl/css :focus-title)

View file

@ -87,6 +87,7 @@
:class (stl/css-case :title-spacing-blur (not has-value?))}
(when-not has-value?
[:button {:class (stl/css :add-blur)
:data-testid "add-blur"
:on-click handle-add} i/add])]]
(when (and open? has-value?)
[:div {:class (stl/css :element-set-content)}

View file

@ -86,6 +86,7 @@
(mf/deps adjust-textarea-size creating?)
(fn [event]
(dom/stop-propagation event)
(rerender-fn)
(when-let [textarea (mf/ref-val textarea-ref)]
(dom/set-value! textarea annotation)
(reset! editing* false)
@ -98,6 +99,7 @@
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(rerender-fn)
(when ^boolean main-instance?
(when-let [textarea (mf/ref-val textarea-ref)]
(reset! editing* true)
@ -109,6 +111,7 @@
(mf/deps creating?)
(fn [event]
(dom/stop-propagation event)
(rerender-fn)
(when-let [textarea (mf/ref-val textarea-ref)]
(let [text (dom/get-value textarea)]
(when-not (str/blank? text)
@ -124,6 +127,7 @@
(fn [event]
(dom/stop-propagation event)
(let [on-accept (fn []
(rerender-fn)
(st/emit!
;; (ptk/data-event {::ev/name "delete-component-annotation"})
(when creating?

View file

@ -146,6 +146,7 @@
(when (and (not disable-remove?) (not (= :multiple fills)))
[:button {:class (stl/css :add-fill)
:data-testid "add-fill"
:on-click on-add} i/add])]]
(when open?

View file

@ -298,6 +298,7 @@
(when-not (= :multiple shadows)
[:button {:class (stl/css :add-shadow)
:data-testid "add-shadow"
:on-click on-add-shadow} i/add])]]
(when open?

View file

@ -169,9 +169,10 @@
:on-collapsed toggle-content
:title label
:class (stl/css-case :title-spacing-stroke (not has-strokes?))}
[:button {:class (stl/css :add-stroke)
:on-click on-add-stroke} i/add]]]
(when (not (= :multiple strokes))
[:button {:class (stl/css :add-stroke)
:data-testid "add-stroke"
:on-click on-add-stroke} i/add])]]
(when open?
[:div {:class (stl/css-case :element-content true
:empty-content (not has-strokes?))}

View file

@ -205,7 +205,6 @@
(fn [event]
(st/emit! (dw/create-page {:file-id file-id :project-id project-id}))
(-> event dom/get-current-target dom/blur!)))
size (if show-pages? size 32)
read-only? (mf/use-ctx ctx/workspace-read-only?)]
[:div {:class (stl/css :sitemap)

View file

@ -115,13 +115,16 @@
toggle-toolbar
(mf/use-fn
#(st/emit! (dwc/toggle-toolbar-visibility)))]
(fn [event]
(dom/blur! (dom/get-target event))
(st/emit! (dwc/toggle-toolbar-visibility))))]
(when-not ^boolean read-only?
[:aside {:class (stl/css-case :main-toolbar true
:main-toolbar-no-rulers (not rulers?)
:main-toolbar-hidden hide-toolbar?)}
[:ul {:class (stl/css :main-toolbar-options)}
[:ul {:class (stl/css :main-toolbar-options)
:data-testid "toolbar-options"}
[:li
[:button
{:title (tr "workspace.toolbar.move" (sc/get-tooltip :move))
@ -197,7 +200,9 @@
:on-click toggle-debug-panel}
i/bug]])]]
[:button {:class (stl/css :toolbar-handler)
[:button {:title (tr "workspace.toolbar.toggle-toolbar")
:aria-label (tr "workspace.toolbar.toggle-toolbar")
:class (stl/css :toolbar-handler)
:on-click toggle-toolbar}
[:div {:class (stl/css :toolbar-handler-btn)}]]])))

View file

@ -636,8 +636,8 @@
:objects base-objects
:modifiers modifiers
:shape frame
:view-only true}]))
:view-only true}]))]
[:g.scrollbar-wrapper {:clipPath "url(#clip-handlers)"}
[:& scroll-bars/viewport-scrollbars
{:objects base-objects
:zoom zoom

View file

@ -196,7 +196,8 @@
[:*
(when show-v-scroll?
[:g.v-scroll {:fill clr/black}
[:g.v-scroll {:fill clr/black
:data-testid "vertical-scrollbar"}
[:rect {:on-pointer-move #(on-pointer-move % :y)
:on-pointer-down #(on-pointer-down % :y)
:on-pointer-up on-pointer-up
@ -210,7 +211,8 @@
:style {:stroke "white"
:stroke-width (/ 0.15 zoom)}}]])
(when show-h-scroll?
[:g.h-scroll {:fill clr/black}
[:g.h-scroll {:fill clr/black
:data-testid "horizontal-scrollbar"}
[:rect {:on-pointer-move #(on-pointer-move % :x)
:on-pointer-down #(on-pointer-down % :x)
:on-pointer-up on-pointer-up

View file

@ -67,6 +67,7 @@
[:rect.selection-rect
{:x (:x data)
:y (:y data)
:data-testid "workspace-selection-rect"
:width (:width data)
:height (:height data)
:style {;; Primary with 0.1 opacity

View file

@ -9,6 +9,7 @@
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.ids-map :as cthi]
[app.common.test-helpers.shapes :as cths]
[app.main.data.workspace.colors :as dc]
[app.main.data.workspace.shapes :as dwsh]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths]))
@ -46,3 +47,36 @@
(t/is (= (count fills') 1))
(t/is (= (:fill-color fill') "#fabada"))
(t/is (= (:fill-opacity fill') 1))))))))
(t/deftest test-update-stroke
;; Old shapes without stroke-alignment are rendered as if it is centered
(t/async
done
(let [;; ==== Setup
store
(ths/setup-store
(-> (cthf/sample-file :file1 :page-label :page1)
(cths/add-sample-shape :shape1 :strokes [{:stroke-color "#000000"
:stroke-opacity 1
:stroke-width 2}])))
;; ==== Action
events
[(dc/change-stroke #{(cthi/id :shape1)} {:color "#FABADA"} 0)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
shape1' (get-in new-state [:workspace-data
:pages-index
(cthi/id :page1)
:objects
(cthi/id :shape1)])
stroke' (-> (:strokes shape1')
first)]
;; ==== Check
(println stroke')
(t/is (some? shape1'))
(t/is (= (:stroke-alignment stroke') :center))))))))

View file

@ -0,0 +1,57 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns frontend-tests.logic.frame-guides-test
(:require
[app.common.test-helpers.compositions :as ctho]
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.shapes :as cths]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.guides :as-alias dwg]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.pages :as thp]
[frontend-tests.helpers.state :as ths]))
(t/use-fixtures :each
{:before thp/reset-idmap!})
(t/deftest test-remove-swap-slot-copy-paste-blue1-to-root
(t/async
done
(let [;; ==== Setup
file (-> (cthf/sample-file :file1)
(ctho/add-frame :frame1))
store (ths/setup-store file)
frame1 (cths/get-shape file :frame1)
guide {:axis :x
:frame-id (:id frame1)
:id (uuid/next)
:position 0}
;; ==== Action
events
[(dw/update-guides guide)
(dw/update-position (:id frame1) {:x 100})]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
guide' (-> page'
:options
:guides
(vals)
(first))]
;; ==== Check
;; guide has moved
(t/is (= (:position guide') 100))))))))

View file

@ -0,0 +1,51 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns frontend-tests.logic.groups-test
(:require
[app.common.data :as d]
[app.common.test-helpers.compositions :as ctho]
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.shapes :as cths]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.groups :as dwgr]
[app.main.data.workspace.selection :as dws]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.pages :as thp]
[frontend-tests.helpers.state :as ths]))
(t/use-fixtures :each
{:before thp/reset-idmap!})
(t/deftest test-create-group
(t/async
done
(let [;; ==== Setup
file (-> (cthf/sample-file :file1)
(cths/add-sample-shape :test-shape))
store (ths/setup-store file)
test-shape (cths/get-shape file :test-shape)
;; ==== Action
events
[(dws/select-shapes (d/ordered-set (:id test-shape)))
(dwgr/group-selected)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
group-id (->> (:objects page')
vals
(filter #(= :group (:type %)))
first
:id)]
;; ==== Check
;; Group has been created and is selected
(t/is (= (get-in new-state [:workspace-local :selected]) #{group-id}))))))))

View file

@ -5109,6 +5109,10 @@ msgstr "Text (%s)"
msgid "workspace.toolbar.text-palette"
msgstr "Typographies (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.toggle-toolbar"
msgstr "Toggle toolbar"
msgid "workspace.top-bar.read-only.done"
msgstr "Done"

View file

@ -5271,6 +5271,10 @@ msgstr "Texto (%s)"
msgid "workspace.toolbar.text-palette"
msgstr "Tipografías (%s)"
#: src/app/main/ui/workspace/left_toolbar.cljs
msgid "workspace.toolbar.toggle-toolbar"
msgstr "Alternar barra de herramientas"
msgid "workspace.top-bar.read-only.done"
msgstr "Hecho"