Merge remote-tracking branch 'penpot/develop' into token-studio-develop

This commit is contained in:
Florian Schroedl
2024-05-16 18:43:05 +02:00
35 changed files with 4865 additions and 2407 deletions

View File

@@ -18,7 +18,7 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 4 : undefined,
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */

View File

@@ -2,17 +2,19 @@ export class MockWebSocketHelper extends EventTarget {
static #mocks = new Map();
static async init(page) {
await page.exposeFunction("MockWebSocket$$constructor", (url, protocols) => {
const webSocket = new MockWebSocketHelper(page, url, protocols);
this.#mocks = new Map();
await page.exposeFunction("onMockWebSocketConstructor", (url) => {
const webSocket = new MockWebSocketHelper(page, url);
this.#mocks.set(url, webSocket);
});
await page.exposeFunction("MockWebSocket$$spyMessage", (url, data) => {
await page.exposeFunction("onMockWebSocketSpyMessage", (url, data) => {
if (!this.#mocks.has(url)) {
throw new Error(`WebSocket with URL ${url} not found`);
}
this.#mocks.get(url).dispatchEvent(new MessageEvent("message", { data }));
});
await page.exposeFunction("MockWebSocket$$spyClose", (url, code, reason) => {
await page.exposeFunction("onMockWebSocketSpyClose", (url, code, reason) => {
if (!this.#mocks.has(url)) {
throw new Error(`WebSocket with URL ${url} not found`);
}
@@ -36,39 +38,52 @@ export class MockWebSocketHelper extends EventTarget {
#page = null;
#url;
#protocols;
constructor(page, url, protocols) {
super();
this.#page = page;
this.#url = url;
this.#protocols = protocols;
}
mockOpen(options) {
return this.#page.evaluate(({ url, options }) => {
if (typeof WebSocket.getByURL !== 'function') {
throw new Error('WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?')
}
WebSocket.getByURL(url).mockOpen(options);
}, { url: this.#url, options });
return this.#page.evaluate(
({ url, options }) => {
if (typeof WebSocket.getByURL !== "function") {
throw new Error(
"WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?",
);
}
WebSocket.getByURL(url).mockOpen(options);
},
{ url: this.#url, options },
);
}
mockMessage(data) {
return this.#page.evaluate(({ url, data }) => {
if (typeof WebSocket.getByURL !== 'function') {
throw new Error('WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?')
}
WebSocket.getByURL(url).mockMessage(data);
}, { url: this.#url, data });
return this.#page.evaluate(
({ url, data }) => {
if (typeof WebSocket.getByURL !== "function") {
throw new Error(
"WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?",
);
}
WebSocket.getByURL(url).mockMessage(data);
},
{ url: this.#url, data },
);
}
mockClose() {
return this.#page.evaluate(({ url }) => {
if (typeof WebSocket.getByURL !== 'function') {
throw new Error('WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?')
}
WebSocket.getByURL(url).mockClose();
}, { url: this.#url });
return this.#page.evaluate(
({ url }) => {
if (typeof WebSocket.getByURL !== "function") {
throw new Error(
"WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?",
);
}
WebSocket.getByURL(url).mockClose();
},
{ url: this.#url },
);
}
}

View File

@@ -46,14 +46,14 @@ window.WebSocket = class MockWebSocket extends EventTarget {
MockWebSocket.#mocks.set(this.#url, this);
if (typeof window["MockWebSocket$$constructor"] === "function") {
MockWebSocket$$constructor(this.#url, this.#protocols);
if (typeof window["onMockWebSocketConstructor"] === "function") {
onMockWebSocketConstructor(this.#url, this.#protocols);
}
if (typeof window["MockWebSocket$$spyMessage"] === "function") {
this.#spyMessage = MockWebSocket$$spyMessage;
if (typeof window["onMockWebSocketSpyMessage"] === "function") {
this.#spyMessage = onMockWebSocketSpyMessage;
}
if (typeof window["MockWebSocket$$spyClose"] === "function") {
this.#spyClose = MockWebSocket$$spyClose;
if (typeof window["onMockWebSocketSpyClose"] === "function") {
this.#spyClose = onMockWebSocketSpyClose;
}
}

View File

@@ -8,8 +8,8 @@ export class BaseWebSocketPage extends BasePage {
* @param {Page} page
* @returns
*/
static initWebSockets(page) {
return MockWebSocketHelper.init(page);
static async initWebSockets(page) {
await MockWebSocketHelper.init(page);
}
/**

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
});
test.skip("User loads worskpace with empty file", async ({ page }) => {
test("User loads worskpace with empty file", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile(page);
@@ -15,7 +15,7 @@ test.skip("User loads worskpace with empty file", async ({ page }) => {
await expect(workspacePage.pageName).toHaveText("Page 1");
});
test.skip("User receives presence notifications updates in the workspace", async ({ page }) => {
test("User receives presence notifications updates in the workspace", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
@@ -25,7 +25,7 @@ test.skip("User receives presence notifications updates in the workspace", async
await expect(page.getByTestId("active-users-list").getByAltText("Princesa Leia")).toHaveCount(2);
});
test.skip("User draws a rect", async ({ page }) => {
test("User draws a rect", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-create-rect.json");
@@ -35,6 +35,6 @@ test.skip("User draws a rect", async ({ page }) => {
await workspacePage.clickWithDragViewportAt(128, 128, 200, 100);
const shape = await workspacePage.rootShape.locator("rect");
expect(shape).toHaveAttribute("width", "200");
expect(shape).toHaveAttribute("height", "100");
await expect(shape).toHaveAttribute("width", "200");
await expect(shape).toHaveAttribute("height", "100");
});

View File

@@ -29,7 +29,7 @@
<script defer src="{{& polyfills}}"></script>
{{/manifest}}
<script type="module" src="{{pluginRuntimeUri}}/index.mjs"></script>
<script type="module" src="{{pluginRuntimeUri}}/index.js"></script>
<script>
window.penpotTranslations = JSON.parse({{& translations}});

View File

@@ -8,6 +8,7 @@
(:require
[app.main.data.events :as ev]
[app.main.data.exports :as de]
[app.main.data.modal :as modal]
[app.main.data.preview :as dp]
[app.main.data.shortcuts :as ds]
[app.main.data.users :as du]
@@ -23,6 +24,7 @@
[app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.undo :as dwu]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.hooks.resize :as r]
@@ -552,7 +554,15 @@
:command (ds/a-mod "m")
:subsections [:basics]
:fn #(st/emit! (with-meta (du/toggle-theme)
{::ev/origin "workspace:shortcut"}))}})
{::ev/origin "workspace:shortcut"}))}
;; PLUGINS
:plugins {:tooltip (ds/meta (ds/alt "P"))
:command (ds/c-mod "alt+p")
:subsections [:basics]
:fn #(when (features/active-feature? @st/state "plugins/runtime")
(st/emit! (modal/show :plugin-management {})))}})
(def debug-shortcuts
;; PREVIEW

View File

@@ -7,6 +7,7 @@
(ns app.main.ui.components.search-bar
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
@@ -23,7 +24,7 @@
icon (unchecked-get props "icon")
autofocus (unchecked-get props "auto-focus")
id (unchecked-get props "id")
input-class (unchecked-get props "class")
handle-change
(mf/use-fn
@@ -51,7 +52,7 @@
[:span {:class (stl/css-case :search-box true
:has-children (some? children))}
children
[:div {:class (stl/css :search-input-wrapper)}
[:div {:class (dm/str input-class " " (stl/css :search-input-wrapper))}
icon
[:input {:id id
:on-change handle-change

View File

@@ -26,6 +26,7 @@
[app.main.ui.workspace.libraries]
[app.main.ui.workspace.nudge]
[app.main.ui.workspace.palette :refer [palette]]
[app.main.ui.workspace.plugins]
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
[app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]]
[app.main.ui.workspace.sidebar.history :refer [history-toolbox]]

View File

@@ -7,6 +7,8 @@
(ns app.main.ui.workspace.main-menu
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -20,12 +22,14 @@
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.shortcuts :as sc]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
[app.main.ui.context :as ctx]
[app.main.ui.hooks.resize :as r]
[app.main.ui.icons :as i]
[app.main.ui.workspace.plugins :as uwp]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
@@ -60,6 +64,9 @@
nav-to-feedback
(mf/use-fn #(st/emit! (rt/nav-new-window* {:rname :settings-feedback})))
plugins?
(features/active-feature? @st/state "plugins/runtime")
show-shortcuts
(mf/use-fn
(mf/deps layout)
@@ -83,7 +90,8 @@
[:& dropdown-menu {:show true
:on-close on-close
:list-class (stl/css-case :sub-menu true
:help-info true)}
:help-info plugins?
:help-info-old (not plugins?))}
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click nav-to-helpc-center
:on-key-down (fn [event]
@@ -148,7 +156,6 @@
:id "file-menu-shortcuts"}
[:span {:class (stl/css :item-name)} (tr "label.shortcuts")]
[:span {:class (stl/css :shortcut)}
(for [sc (scd/split-sc (sc/get-tooltip :show-shortcuts))]
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
@@ -596,6 +603,41 @@
[:span {:class (stl/css :item-name)}
(tr "dashboard.export-frames")]])]))
(mf/defc plugins-menu
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[{:keys [open-plugins on-close]}]
(when (features/active-feature? @st/state "plugins/runtime")
(let [plugins (uwp/load-from-store)]
[:& dropdown-menu {:show true
:list-class (stl/css-case :sub-menu true :plugins true)
:on-close on-close}
[:> dropdown-menu-item* {:on-click open-plugins
:class (stl/css :submenu-item)
:on-key-down (fn [event]
(when (kbd/enter? event)
(open-plugins event)))
:data-test "open-plugins"
:id "file-menu-open-plugins"}
[:span {:class (stl/css :item-name)}
(tr "workspace.plugins.menu.plugins-manager")]
[:span {:class (stl/css :shortcut)}
(for [sc (scd/split-sc (sc/get-tooltip :plugins))]
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]
(when (d/not-empty? plugins)
[:div {:class (stl/css :separator)}])
(for [[idx {:keys [name url]}] (d/enumerate plugins)]
[:> dropdown-menu-item* {:key (dm/str "plugins-menu-" idx)
:on-click #(uwp/open-plugin! url)
:class (stl/css :submenu-item)
:on-key-down (fn [event]
(when (kbd/enter? event)
#(uwp/open-plugin! url)))}
[:span {:class (stl/css :item-name)} name]])])))
(mf/defc menu
{::mf/wrap-props false}
[{:keys [layout file profile]}]
@@ -644,12 +686,19 @@
(reset! show-menu* false)
(reset! sub-menu* nil))))
toggle-theme
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(st/emit! (du/toggle-theme))))]
(st/emit! (du/toggle-theme))))
open-plugins-manager
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(reset! show-menu* false)
(reset! sub-menu* nil)
(st/emit! (modal/show :plugin-management {}))))]
[:*
@@ -703,6 +752,19 @@
:id "file-menu-preferences"}
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.preferences")]
[:span {:class (stl/css :open-arrow)} i/arrow]]
(when (features/active-feature? @st/state "plugins/runtime")
[:> dropdown-menu-item* {:class (stl/css :menu-item)
:on-click on-menu-click
:on-key-down (fn [event]
(when (kbd/enter? event)
(on-menu-click event)))
:on-pointer-enter on-menu-click
:data-test "plugins"
:id "file-menu-plugins"}
[:span {:class (stl/css :item-name)} (tr "workspace.plugins.menu.title")]
[:span {:class (stl/css :open-arrow)} i/arrow]])
[:div {:class (stl/css :separator)}]
[:> dropdown-menu-item* {:class (stl/css-case :menu-item true)
:on-click on-menu-click
@@ -739,6 +801,11 @@
:toggle-theme toggle-theme
:on-close close-sub-menu}]
:plugins
[:& plugins-menu
{:open-plugins open-plugins-manager
:on-close close-sub-menu}]
:help-info
[:& help-info-menu
{:layout layout

View File

@@ -95,7 +95,15 @@
top: $s-148;
}
&.plugins {
top: $s-180;
}
&.help-info {
top: $s-196;
top: $s-232;
}
&.help-info-old {
top: $s-192;
}
}

View File

@@ -0,0 +1,180 @@
;; 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 app.main.ui.workspace.plugins
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.data.modal :as modal]
[app.main.ui.components.search-bar :refer [search-bar]]
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.icons :as i]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
(def ^:private close-icon
(i/icon-xref :close (stl/css :close-icon)))
(mf/defc plugin-entry
[{:keys [index _icon url name description on-open-plugin on-remove-plugin]}]
(let [handle-open-click
(mf/use-callback
(mf/deps index url on-open-plugin)
(fn []
(when on-open-plugin
(on-open-plugin index url))))
handle-delete-click
(mf/use-callback
(mf/deps index url on-remove-plugin)
(fn []
(when on-remove-plugin
(on-remove-plugin index url))))]
[:div {:class (stl/css :plugins-list-element)}
[:div {:class (stl/css :plugin-icon)} ""]
[:div {:class (stl/css :plugin-description)}
[:div {:class (stl/css :plugin-title)} name]
[:div {:class (stl/css :plugin-summary)} description]]
[:button {:class (stl/css :open-button)
:on-click handle-open-click} (tr "workspace.plugins.button-open")]
[:button {:class (stl/css :trash-button)
:on-click handle-delete-click} i/delete]]))
(defn load-from-store
[]
(let [ls (.-localStorage js/window)
plugins-val (.getItem ls "plugins")]
(when plugins-val
(let [plugins-js (.parse js/JSON plugins-val)]
(js->clj plugins-js {:keywordize-keys true})))))
(defn save-to-store
[plugins]
(let [ls (.-localStorage js/window)
plugins-js (clj->js plugins)
plugins-val (.stringify js/JSON plugins-js)]
(.setItem ls "plugins" plugins-val)))
(defn open-plugin!
[url]
(.ɵloadPlugin js/window #js {:manifest url}))
(mf/defc plugin-management-dialog
{::mf/register modal/components
::mf/register-as :plugin-management}
[]
(let [plugins-state* (mf/use-state [])
plugins-state @plugins-state*
plugin-url* (mf/use-state "")
plugin-url @plugin-url*
input-status* (mf/use-state nil) ;; :error-url :error-manifest :success
input-status @input-status*
error? (contains? #{:error-url :error-manifest} input-status)
handle-close-dialog
(mf/use-callback
(fn []
(modal/hide!)))
handle-url-input
(mf/use-callback
(fn [value]
(reset! input-status* nil)
(reset! plugin-url* value)))
handle-install-click
(mf/use-callback
(mf/deps plugins-state plugin-url)
(fn []
(->> (http/send! {:method :get
:uri plugin-url
:response-type :json})
(rx/map :body)
(rx/subs!
(fn [body]
(let [name (obj/get body "name")
new-state (conj plugins-state {:name name :url plugin-url})]
(reset! input-status* :success)
(reset! plugin-url* "")
(reset! plugins-state* new-state)
(save-to-store new-state)))
(fn [_]
(reset! input-status* :error-url))))))
handle-open-plugin
(mf/use-callback
(fn [_ url]
(open-plugin! url)
(modal/hide!)))
handle-remove-plugin
(mf/use-callback
(mf/deps plugins-state)
(fn [rm-idx _]
(let [new-state
(into []
(keep-indexed (fn [idx item]
(when (not= idx rm-idx) item)))
plugins-state)]
(reset! plugins-state* new-state)
(save-to-store new-state))))]
(mf/use-effect
(fn []
(reset! plugins-state* (d/nilv (load-from-store) []))))
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog)}
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon]
[:div {:class (stl/css :modal-title)} (tr "workspace.plugins.title")]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :top-bar)}
[:& search-bar {:on-change handle-url-input
:value plugin-url
:placeholder (tr "workspace.plugins.search-placeholder")
:class (stl/css-case :input-error error?)}]
[:button {:class (stl/css :primary-button)
:disabled (empty? plugin-url)
:on-click handle-install-click} (tr "workspace.plugins.install")]]
(when error?
[:div {:class (stl/css-case :info true :error error?)}
(tr "workspace.plugins.error.url")])
[:hr]
[:& title-bar {:collapsable false
:title (tr "workspace.plugins.installed-plugins")}]
(if (empty? plugins-state)
[:div {:class (stl/css :plugins-empty)}
[:div {:class (stl/css :plugins-empty-logo)} i/logo-icon]
[:div {:class (stl/css :plugins-empty-text)} (tr "workspace.plugins.empty-plugins")]]
[:div {:class (stl/css :plugins-list)}
(for [[idx {:keys [name url]}] (d/enumerate plugins-state)]
[:& plugin-entry {:key (dm/str "plugin-" idx)
:name name
:url url
:index idx
:icon nil
:description "Nullam ullamcorper ligula ac felis commodo pulvinar."
:on-open-plugin handle-open-plugin
:on-remove-plugin handle-remove-plugin}])])]]]))

View File

@@ -0,0 +1,167 @@
// 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
@import "refactor/common-refactor.scss";
.modal-overlay {
@extend .modal-overlay-base;
}
.modal-dialog {
@extend .modal-container-base;
display: grid;
grid-template-rows: auto 1fr;
height: $s-472;
max-height: $s-472;
width: $s-472;
max-width: $s-472;
}
.close-btn {
@extend .modal-close-btn-base;
}
.close-icon {
@extend .button-icon;
stroke: var(--icon-foreground);
}
.modal-title {
@include headlineMediumTypography;
margin-block-end: $s-16;
color: var(--modal-title-foreground-color);
}
.modal-content {
display: flex;
flex-direction: column;
height: $s-380;
}
.primary-button {
@extend .button-primary;
@include headlineSmallTypography;
padding: $s-0 $s-16;
}
.search-icon {
@include flexCenter;
width: $s-20;
padding: 0 0 0 $s-8;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
}
.top-bar {
display: flex;
gap: $s-8;
}
.open-button {
@extend .button-secondary;
width: $s-68;
min-width: $s-68;
height: $s-32;
text-transform: uppercase;
}
.trash-button {
@extend .button-tertiary;
width: $s-32;
height: $s-32;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
.plugins-list {
padding-top: $s-20;
overflow-x: hidden;
overflow-y: scroll;
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.plugins-list-element {
display: flex;
gap: $s-12;
}
.plugin-icon {
min-width: $s-32;
min-height: $s-32;
width: $s-32;
height: $s-32;
background: #b1b2b5;
}
.plugin-description {
display: flex;
flex-direction: column;
gap: $s-8;
}
.plugin-title {
@include bodyMediumTypography;
color: #ffffff;
}
.plugin-summary {
@include bodySmallTypography;
color: #8f9da3;
}
.plugins-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
margin-top: 3rem;
}
.plugins-empty-logo {
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background: #212426;
svg {
width: 16px;
height: 16px;
fill: #8f9da3;
}
}
.plugins-empty-text {
@include bodySmallTypography;
color: white;
}
div.input-error {
border: 1px solid var(--input-border-color-error);
}
.info {
@include bodySmallTypography;
margin-top: $s-4;
&.error {
color: var(--input-border-color-error);
}
&.success {
color: var(--input-border-color-success);
}
}

View File

@@ -11,6 +11,7 @@
[app.common.files.changes-builder :as cb]
[app.common.geom.point :as gpt]
[app.common.record :as cr]
[app.common.text :as txt]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as ch]
@@ -19,6 +20,7 @@
[app.main.store :as st]
[app.plugins.events :as events]
[app.plugins.file :as file]
[app.plugins.library :as library]
[app.plugins.page :as page]
[app.plugins.shape :as shape]
[app.plugins.utils :as utils]
@@ -129,13 +131,29 @@
[_]
(create-shape :rect))
(createText
[_ text]
(let [page-id (:current-page-id @st/state)
page (dm/get-in @st/state [:workspace-data :pages-index page-id])
shape (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width})
(txt/change-text text)
(assoc :position-data nil))
changes
(-> (cb/empty-changes)
(cb/with-page page)
(cb/with-objects (:objects page))
(cb/add-object shape))]
(st/emit! (ch/commit-changes changes))
(shape/data->shape-proxy shape)))
(createShapeFromSvg
[_ svg-string]
(let [id (uuid/next)
page-id (:current-page-id @st/state)]
(st/emit! (dwm/create-svg-shape id "svg" svg-string (gpt/point 0 0)))
(shape/data->shape-proxy
(dm/get-in @st/state [:workspace-data :pages-index page-id :objects id])))))
(when (some? svg-string)
(let [id (uuid/next)
page-id (:current-page-id @st/state)]
(st/emit! (dwm/create-svg-shape id "svg" svg-string (gpt/point 0 0)))
(shape/data->shape-proxy
(dm/get-in @st/state [:workspace-data :pages-index page-id :objects id]))))))
(defn create-context
[]
@@ -144,4 +162,5 @@
{:name "root" :get #(.getRoot ^js %)}
{:name "currentPage" :get #(.getPage ^js %)}
{:name "selection" :get #(.getSelectedShapes ^js %)}
{:name "viewport" :get #(.getViewport ^js %)}))
{:name "viewport" :get #(.getViewport ^js %)}
{:name "library" :get (fn [_] (library/create-library-subcontext))}))

View File

@@ -34,7 +34,7 @@
(st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value}))))
(addRowAtIndex
[self type value index]
[self index type value]
(let [id (get-data self :id)
type (keyword type)]
(st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value} index))))
@@ -46,7 +46,7 @@
(st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value}))))
(addColumnAtIndex
[self type value index]
[self index type value]
(let [id (get-data self :id)
type (keyword type)]
(st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value} index))))
@@ -170,4 +170,37 @@
(fn [self value]
(let [id (get-data self :id)]
(when (us/safe-int? value)
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}})))))})))
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}})))))}
{:name "topPadding"
:get #(:p1 (get-state % :layout-padding))
:set
(fn [self value]
(let [id (get-data self :id)]
(when (us/safe-int? value)
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}})))))}
{:name "rightPadding"
:get #(:p2 (get-state % :layout-padding))
:set
(fn [self value]
(let [id (get-data self :id)]
(when (us/safe-int? value)
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}})))))}
{:name "bottomPadding"
:get #(:p3 (get-state % :layout-padding))
:set
(fn [self value]
(let [id (get-data self :id)]
(when (us/safe-int? value)
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}})))))}
{:name "leftPadding"
:get #(:p4 (get-state % :layout-padding))
:set
(fn [self value]
(let [id (get-data self :id)]
(when (us/safe-int? value)
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}})))))})))

View File

@@ -0,0 +1,96 @@
;; 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 app.plugins.library
"RPC for plugins runtime."
(:require
[app.common.data.macros :as dm]
[app.common.record :as cr]
[app.main.store :as st]
[app.plugins.utils :as utils :refer [get-data]]
[app.util.object :as obj]))
(defn get-library-info
([self attr]
(let [lib-id (get-data self :id)
current-file-id (:current-file-id @st/state)]
(if (= lib-id current-file-id)
(dm/get-in @st/state [:workspace-file attr])
(dm/get-in @st/state [:workspace-libraries lib-id attr]))))
([self attr mapfn]
(-> (get-library-info self attr)
(mapfn))))
(defn get-library-data
([self attr]
(let [lib-id (get-data self :id)
current-file-id (:current-file-id @st/state)]
(if (= lib-id current-file-id)
(dm/get-in @st/state [:workspace-data attr])
(dm/get-in @st/state [:workspace-libraries lib-id :data attr]))))
([self attr mapfn]
(-> (get-library-data self attr)
(mapfn))))
(defn- array-to-js
[value]
(.freeze
js/Object
(apply array (->> value (map utils/to-js)))))
(deftype Library [_data]
Object)
(defn create-library
[data]
(cr/add-properties!
(Library. data)
{:name "_data"
:enumerable false}
{:name "id"
:get (fn [self]
(str (:id (obj/get self "_data"))))}
{:name "name"
:get (fn [self]
(get-library-info self :name))}
{:name "colors"
:get (fn [self]
(array-to-js (get-library-data self :colors vals)))}
{:name "typographies"
:get (fn [self]
(array-to-js (get-library-data self :typographies vals)))}
{:name "components"
:get (fn [self]
(array-to-js (get-library-data self :components vals)))}))
(deftype PenpotLibrarySubcontext []
Object
(find
[_ _name])
(find [_]))
(defn create-library-subcontext
[]
(cr/add-properties!
(PenpotLibrarySubcontext.)
{:name "local" :get
(fn [_]
(let [file (get @st/state :workspace-file)
data (get @st/state :workspace-data)]
(create-library (assoc file :data data))))}
{:name "connected" :get
(fn [_]
(let [libraries (get @st/state :workspace-libraries)]
(apply array (->> libraries vals (map create-library)))))}))

View File

@@ -7,10 +7,13 @@
(ns app.plugins.shape
"RPC for plugins runtime."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.record :as crc]
[app.common.spec :as us]
[app.common.text :as txt]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.main.data.workspace :as udw]
[app.main.data.workspace.changes :as dwc]
@@ -24,17 +27,11 @@
(declare data->shape-proxy)
(defn- make-fills
[fills]
(defn- array-to-js
[value]
(.freeze
js/Object
(apply array (->> fills (map utils/to-js)))))
(defn- make-strokes
[strokes]
(.freeze
js/Object
(apply array (->> strokes (map utils/to-js)))))
(apply array (->> value (map utils/to-js)))))
(defn- locate-shape
[shape-id]
@@ -86,7 +83,8 @@
(addGridLayout [self]
(let [id (get-data self :id)]
(st/emit! (dwsl/create-layout-from-id id :grid :from-frame? true :calculate-params? false)))))
(st/emit! (dwsl/create-layout-from-id id :grid :from-frame? true :calculate-params? false))
(grid/grid-layout-proxy (obj/get self "_data")))))
(crc/define-properties!
ShapeProxy
@@ -107,6 +105,118 @@
{:name "type"
:get (get-data-fn :type name)}
{:name "name"
:get #(get-state % :name)
:set (fn [self value]
(let [id (get-data self :id)]
(st/emit! (dwc/update-shapes [id] #(assoc % :name value)))))}
{:name "blocked"
:get #(get-state % :blocked boolean)
:set (fn [self value]
(let [id (get-data self :id)]
(st/emit! (dwc/update-shapes [id] #(assoc % :blocked value)))))}
{:name "hidden"
:get #(get-state % :hidden boolean)
:set (fn [self value]
(let [id (get-data self :id)]
(st/emit! (dwc/update-shapes [id] #(assoc % :hidden value)))))}
{:name "proportionLock"
:get #(get-state % :proportion-lock boolean)
:set (fn [self value]
(let [id (get-data self :id)]
(st/emit! (dwc/update-shapes [id] #(assoc % :proportion-lock value)))))}
{:name "constraintsHorizontal"
:get #(get-state % :constraints-h d/name)
:set (fn [self value]
(let [id (get-data self :id)
value (keyword value)]
(when (contains? cts/horizontal-constraint-types value)
(st/emit! (dwc/update-shapes [id] #(assoc % :constraints-h value))))))}
{:name "constraintsVertical"
:get #(get-state % :constraints-v d/name)
:set (fn [self value]
(let [id (get-data self :id)
value (keyword value)]
(when (contains? cts/vertical-constraint-types value)
(st/emit! (dwc/update-shapes [id] #(assoc % :constraints-v value))))))}
{:name "borderRadius"
:get #(get-state % :rx)
:set (fn [self value]
(let [id (get-data self :id)]
(when (us/safe-int? value)
(st/emit! (dwc/update-shapes [id] #(assoc % :rx value :ry value))))))}
{:name "borderRadiusTopLeft"
:get #(get-state % :r1)
:set (fn [self value]
(let [id (get-data self :id)]
(when (us/safe-int? value)
(st/emit! (dwc/update-shapes [id] #(assoc % :r1 value))))))}
{:name "borderRadiusTopRight"
:get #(get-state % :r2)
:set (fn [self value]
(let [id (get-data self :id)]
(when (us/safe-int? value)
(st/emit! (dwc/update-shapes [id] #(assoc % :r2 value))))))}
{:name "borderRadiusBottomRight"
:get #(get-state % :r3)
:set (fn [self value]
(let [id (get-data self :id)]
(when (us/safe-int? value)
(st/emit! (dwc/update-shapes [id] #(assoc % :r3 value))))))}
{:name "borderRadiusBottomLeft"
:get #(get-state % :r4)
:set (fn [self value]
(let [id (get-data self :id)]
(when (us/safe-int? value)
(st/emit! (dwc/update-shapes [id] #(assoc % :r4 value))))))}
{:name "opacity"
:get #(get-state % :opacity)
:set (fn [self value]
(let [id (get-data self :id)]
(when (and (us/safe-number? value) (>= value 0) (<= value 1))
(st/emit! (dwc/update-shapes [id] #(assoc % :opacity value))))))}
{:name "blendMode"
:get #(get-state % :blend-mode d/name)
:set (fn [self value]
(let [id (get-data self :id)
value (keyword value)]
(when (contains? cts/blend-modes value)
(st/emit! (dwc/update-shapes [id] #(assoc % :blend-mode value))))))}
{:name "shadows"
:get #(get-state % :shadow array-to-js)
:set (fn [self value]
(let [id (get-data self :id)
value (mapv #(utils/from-js %) value)]
(st/emit! (dwc/update-shapes [id] #(assoc % :shadows value)))))}
{:name "blur"
:get #(get-state % :blur utils/to-js)
:set (fn [self value]
(let [id (get-data self :id)
value (utils/from-js value)]
(st/emit! (dwc/update-shapes [id] #(assoc % :blur value)))))}
{:name "exports"
:get #(get-state % :exports array-to-js)
:set (fn [self value]
(let [id (get-data self :id)
value (mapv #(utils/from-js %) value)]
(st/emit! (dwc/update-shapes [id] #(assoc % :exports value)))))}
;; Geometry properties
{:name "x"
:get #(get-state % :x)
:set
@@ -183,21 +293,22 @@
{:name "height"
:get #(get-state % :height)}
{:name "name"
:get #(get-state % :name)
:set (fn [self value]
(let [id (get-data self :id)]
(st/emit! (dwc/update-shapes [id] #(assoc % :name value)))))}
{:name "flipX"
:get #(get-state % :flip-x)}
{:name "flipY"
:get #(get-state % :flip-y)}
;; Strokes and fills
{:name "fills"
:get #(get-state % :fills make-fills)
:get #(get-state % :fills array-to-js)
:set (fn [self value]
(let [id (get-data self :id)
value (mapv #(utils/from-js %) value)]
(st/emit! (dwc/update-shapes [id] #(assoc % :fills value)))))}
{:name "strokes"
:get #(get-state % :strokes make-strokes)
:get #(get-state % :strokes array-to-js)
:set (fn [self value]
(let [id (get-data self :id)
value (mapv #(utils/from-js %) value)]
@@ -220,8 +331,16 @@
(fn [self]
(let [layout (get-state self :layout)]
(when (= :grid layout)
(grid/grid-layout-proxy data))))})
(grid/grid-layout-proxy data))))}
{:name "guides"
:get #(get-state % :grids array-to-js)
:set (fn [self value]
(let [id (get-data self :id)
value (mapv #(utils/from-js %) value)]
(st/emit! (dwc/update-shapes [id] #(assoc % :grids value)))))})
;; TODO: Flex properties
#_(crc/add-properties!
{:name "flex"
:get
@@ -235,9 +354,18 @@
(obj/unset! "addFlexLayout")))
(cond-> (cfh/text-shape? data)
(crc/add-properties!
{:name "characters"
:get #(get-state % :content txt/content->text)
:set (fn [self value]
(let [id (get-data self :id)]
(st/emit! (dwc/update-shapes [id] #(txt/change-text % value)))))}))))
(-> (crc/add-properties!
{:name "characters"
:get #(get-state % :content txt/content->text)
:set (fn [self value]
(let [id (get-data self :id)]
(st/emit! (dwc/update-shapes [id] #(txt/change-text % value)))))})
(crc/add-properties!
{:name "growType"
:get #(get-state % :grow-type d/name)
:set (fn [self value]
(let [id (get-data self :id)
value (keyword value)]
(when (contains? #{:auto-width :auto-height :fixed} value)
(st/emit! (dwc/update-shapes [id] #(assoc % :grow-type value))))))})))))

View File

@@ -40,8 +40,6 @@
fills' (:fills shape1')
fill' (first fills')]
(cthf/dump-shape shape1')
;; ==== Check
(t/is (some? shape1'))
(t/is (= (count fills') 1))

View File

@@ -6,13 +6,19 @@
(ns frontend-tests.helpers.pages
(:require
[app.common.data :as d]
[app.common.files.changes :as cp]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.logic.libraries :as cll]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.main.data.workspace.groups :as dwg]
[app.main.data.workspace.layout :as layout]
@@ -186,3 +192,82 @@
:components (:components data)}})
(update :workspace-data
assoc :components {} :pages [] :pages-index {}))))
(defn simulate-copy-shape
[selected objects libraries page file features version]
(letfn [(sort-selected [data]
(let [;; Narrow the objects map so it contains only relevant data for
;; selected and its parents
objects (cfh/selected-subtree objects selected)
selected (->> (ctst/sort-z-index objects selected)
(reverse)
(into (d/ordered-set)))]
(assoc data :selected selected)))
;; Prepare the shape object.
(prepare-object [objects parent-frame-id obj]
(maybe-translate obj objects parent-frame-id))
;; Collects all the items together and split images into a
;; separated data structure for a more easy paste process.
(collect-data [result {:keys [id ::images] :as item}]
(cond-> result
:always
(update :objects assoc id (dissoc item ::images))
(some? images)
(update :images into images)))
(maybe-translate [shape objects parent-frame-id]
(if (= parent-frame-id uuid/zero)
shape
(let [frame (get objects parent-frame-id)]
(gsh/translate-to-frame shape frame))))
;; When copying an instance that is nested inside another one, we need to
;; advance the shape refs to one or more levels of remote mains.
(advance-copies [data]
(let [heads (mapcat #(ctn/get-child-heads (:objects data) %) selected)]
(update data :objects
#(reduce (partial advance-copy file libraries page)
%
heads))))
(advance-copy [file libraries page objects shape]
(if (and (ctk/instance-head? shape) (not (ctk/main-instance? shape)))
(let [level-delta (ctn/get-nesting-level-delta (:objects page) shape uuid/zero)]
(if (pos? level-delta)
(reduce (partial advance-shape file libraries page level-delta)
objects
(cfh/get-children-with-self objects (:id shape)))
objects))
objects))
(advance-shape [file libraries page level-delta objects shape]
(let [new-shape-ref (ctf/advance-shape-ref file page libraries shape level-delta {:include-deleted? true})]
(cond-> objects
(and (some? new-shape-ref) (not= new-shape-ref (:shape-ref shape)))
(assoc-in [(:id shape) :shape-ref] new-shape-ref))))]
(let [file-id (:id file)
frame-id (cfh/common-parent-frame objects selected)
initial {:type :copied-shapes
:features features
:version version
:file-id file-id
:selected selected
:objects {}
:images #{}
:in-viewport false}
shapes (->> (cfh/selected-with-children objects selected)
(keep (d/getf objects)))]
(->> shapes
(map (partial prepare-object objects frame-id))
(reduce collect-data initial)
sort-selected
advance-copies))))

View File

@@ -54,3 +54,8 @@
(doall (for [event events]
(ptk/emit! store event)))
(ptk/emit! store :the/end)))
(defn get-file-from-store
[store]
(-> (:workspace-file store)
(assoc :data (:workspace-data store))))

View File

@@ -0,0 +1,815 @@
;; 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.comp-remove-swap-slots-test
(:require
[app.common.test-helpers.components :as cthc]
[app.common.test-helpers.compositions :as ctho]
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.ids-map :as cthi]
[app.common.test-helpers.shapes :as cths]
[app.common.types.component :as ctk]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.selection :as dws]
[cljs.test :as t :include-macros true]
[cuerdas.core :as str]
[frontend-tests.helpers.pages :as thp]
[frontend-tests.helpers.state :as ths]))
;; Related .penpot file: common/test/cases/remove-swap-slots.penpot
(defn- setup-file
[]
;; {:frame-red} [:name Frame1] # [Component :red]
;; {:frame-blue} [:name Frame1] # [Component :blue]
;; {:frame-green} [:name Frame1] # [Component :green]
;; :red-copy-green [:name Frame1] @--> :frame-red
;; {:frame-b1} [:name Frame1] # [Component :b1]
;; :blue1 [:name Frame1, :swap-slot-label :red-copy] @--> :frame-blue
;; :frame-yellow [:name Frame1]
;; :green-copy [:name Frame1] @--> :frame-green
;; :blue-copy-in-green-copy [:name Frame1, :swap-slot-label :red-copy-green] @--> :frame-blue
;; {:frame-b2} [:name Frame1] # [Component :b2]
(-> (cthf/sample-file :file1)
(ctho/add-frame :frame-red)
(cthc/make-component :red :frame-red)
(ctho/add-frame :frame-blue :name "frame-blue")
(cthc/make-component :blue :frame-blue)
(ctho/add-frame :frame-green :name "frame-green")
(cthc/make-component :green :frame-green)
(cthc/instantiate-component :red :red-copy-green :parent-label :frame-green)
(ctho/add-frame :frame-b1)
(cthc/make-component :b1 :frame-b1)
(ctho/add-frame :frame-yellow :parent-label :frame-b1 :name "frame-yellow")
(cthc/instantiate-component :red :red-copy :parent-label :frame-b1)
(cthc/component-swap :red-copy :blue :blue1)
(cthc/instantiate-component :green :green-copy :parent-label :frame-b1 :children-labels [:red-copy-in-green-copy])
(cthc/component-swap :red-copy-in-green-copy :blue :blue-copy-in-green-copy)
(ctho/add-frame :frame-b2)
(cthc/make-component :b2 :frame-b2)))
(defn- setup-file-blue1-in-yellow
[]
;; {:frame-red} [:name Frame1] # [Component :red]
;; {:frame-blue} [:name Frame1] # [Component :blue]
;; {:frame-green} [:name Frame1] # [Component :green]
;; :red-copy-green [:name Frame1] @--> :frame-red
;; {:frame-b1} [:name Frame1] # [Component :b1]
;; :frame-yellow [:name Frame1]
;; :blue1 [:name Frame1, :swap-slot-label :red-copy] @--> :frame-blue
;; :green-copy [:name Frame1] @--> :frame-green
;; :blue-copy-in-green-copy [:name Frame1, :swap-slot-label :red-copy-green] @--> :frame-blue
;; {:frame-b2} [:name Frame1] # [Component :b2]
(-> (cthf/sample-file :file1)
(ctho/add-frame :frame-red)
(cthc/make-component :red :frame-red)
(ctho/add-frame :frame-blue :name "frame-blue")
(cthc/make-component :blue :frame-blue)
(ctho/add-frame :frame-green)
(cthc/make-component :green :frame-green)
(cthc/instantiate-component :red :red-copy-green :parent-label :frame-green)
(ctho/add-frame :frame-b1)
(cthc/make-component :b1 :frame-b1)
(ctho/add-frame :frame-yellow :parent-label :frame-b1 :name "frame-yellow")
(cthc/instantiate-component :red :red-copy :parent-label :frame-yellow)
(cthc/component-swap :red-copy :blue :blue1)
(cthc/instantiate-component :green :green-copy :parent-label :frame-b1 :children-labels [:red-copy-in-green-copy])
(cthc/component-swap :red-copy-in-green-copy :blue :blue-copy-in-green-copy)
(ctho/add-frame :frame-b2)
(cthc/make-component :b2 :frame-b2)))
(defn- find-copied-shape
[original-shape page parent-id]
;; copied shape has the same name, is in the specified parent, and doesn't have a label
(->> (vals (:objects page))
(filter #(and (= (:name %) (:name original-shape))
(= (:parent-id %) parent-id)
(str/starts-with? (cthi/label (:id %)) "<no-label")))
first))
(t/deftest test-remove-swap-slot-copy-paste-blue1-to-root
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
blue1 (cths/get-shape file :blue1)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape uuid/zero)
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
blue1' (cths/get-shape file' :blue1)
copied-blue1' (find-copied-shape blue1' page' uuid/zero)]
;; ==== Check
;; blue1 has swap-id
(t/is (some? (ctk/get-swap-slot blue1')))
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-copy-paste-blue1-to-b1
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
b1 (cths/get-shape file :frame-b1)
blue1 (cths/get-shape file :blue1)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id b1))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
b1' (cths/get-shape file' :frame-b1)
blue1' (cths/get-shape file' :blue1)
copied-blue1' (find-copied-shape blue1' page' (:id b1'))]
;; ==== Check
;; blue1 has swap-id
(t/is (some? (ctk/get-swap-slot blue1')))
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-copy-paste-blue1-to-yellow
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
yellow (cths/get-shape file :frame-yellow)
blue1 (cths/get-shape file :blue1)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id yellow))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
yellow' (cths/get-shape file' :frame-yellow)
blue1' (cths/get-shape file' :blue1)
copied-blue1' (find-copied-shape blue1' page' (:id yellow'))]
;; ==== Check
;; blue1 has swap-id
(t/is (some? (ctk/get-swap-slot blue1')))
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-copy-paste-blue1-to-b2
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
b2 (cths/get-shape file :frame-b2)
blue1 (cths/get-shape file :blue1)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id b2))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
b2' (cths/get-shape file' :frame-b2)
blue1' (cths/get-shape file' :blue1)
copied-blue1' (find-copied-shape blue1' page' (:id b2'))]
;; ==== Check
;; blue1 has swap-id
(t/is (some? (ctk/get-swap-slot blue1')))
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-cut-paste-blue1-to-root
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
blue1 (cths/get-shape file :blue1)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id blue1))
(dw/delete-selected)
(dws/select-shape uuid/zero)
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
copied-blue1' (find-copied-shape blue1 page' uuid/zero)]
;; ==== Check
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-cut-paste-blue1-to-yellow
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
yellow (cths/get-shape file :frame-yellow)
blue1 (cths/get-shape file :blue1)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id blue1))
(dw/delete-selected)
(dws/select-shape (:id yellow))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
yellow' (cths/get-shape file' :frame-yellow)
copied-blue1' (find-copied-shape blue1 page' (:id yellow'))]
;; ==== Check
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-cut-paste-blue1-to-b2
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
b2 (cths/get-shape file :frame-b2)
blue1 (cths/get-shape file :blue1)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id blue1)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id blue1))
(dw/delete-selected)
(dws/select-shape (:id b2))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
b2' (cths/get-shape file' :frame-b2)
copied-blue1' (find-copied-shape blue1 page' (:id b2'))]
;; ==== Check
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-copy-paste-yellow-to-root
(t/async
done
(let [;; ==== Setup
file (setup-file-blue1-in-yellow)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
yellow (cths/get-shape file :frame-yellow)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape uuid/zero)
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
copied-yellow' (find-copied-shape yellow page' uuid/zero)
blue1' (cths/get-shape file' :blue1)
copied-blue1' (find-copied-shape blue1' page' (:id copied-yellow'))]
;; ==== Check
;; blue1 has swap-id
(t/is (some? (ctk/get-swap-slot blue1')))
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-copy-paste-yellow-to-b1
(t/async
done
(let [;; ==== Setup
file (setup-file-blue1-in-yellow)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
yellow (cths/get-shape file :frame-yellow)
b1 (cths/get-shape file :frame-b1)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id b1))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
b1' (cths/get-shape file' :frame-b1)
copied-yellow' (find-copied-shape yellow page' (:id b1'))
blue1' (cths/get-shape file' :blue1)
copied-blue1' (find-copied-shape blue1' page' (:id copied-yellow'))]
;; ==== Check
;; blue1 has swap-id
(t/is (some? (ctk/get-swap-slot blue1')))
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-copy-paste-yellow-to-b2
(t/async
done
(let [;; ==== Setup
file (setup-file-blue1-in-yellow)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
yellow (cths/get-shape file :frame-yellow)
b2 (cths/get-shape file :frame-b2)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id b2))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
b2' (cths/get-shape file' :frame-b2)
copied-yellow' (find-copied-shape yellow page' (:id b2'))
blue1' (cths/get-shape file' :blue1)
copied-blue1' (find-copied-shape blue1' page' (:id copied-yellow'))]
;; ==== Check
;; blue1 has swap-id
(t/is (some? (ctk/get-swap-slot blue1')))
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-cut-paste-yellow-to-root
(t/async
done
(let [;; ==== Setup
file (setup-file-blue1-in-yellow)
store (ths/setup-store file)
blue1 (cths/get-shape file :blue1)
;; ==== Action
page (cthf/current-page file)
yellow (cths/get-shape file :frame-yellow)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id yellow))
(dw/delete-selected)
(dws/select-shape uuid/zero)
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
copied-yellow' (find-copied-shape yellow page' uuid/zero)
copied-blue1' (find-copied-shape blue1 page' (:id copied-yellow'))]
;; ==== Check
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-cut-paste-yellow-to-b1
(t/async
done
(let [;; ==== Setup
file (setup-file-blue1-in-yellow)
store (ths/setup-store file)
blue1 (cths/get-shape file :blue1)
;; ==== Action
page (cthf/current-page file)
yellow (cths/get-shape file :frame-yellow)
b1 (cths/get-shape file :frame-b1)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id yellow))
(dw/delete-selected)
(dws/select-shape (:id b1))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
b1' (cths/get-shape file' :frame-b1)
copied-yellow' (find-copied-shape yellow page' (:id b1'))
copied-blue1' (find-copied-shape blue1 page' (:id copied-yellow'))]
;; ==== Check
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-remove-swap-slot-cut-paste-yellow-to-b2
(t/async
done
(let [;; ==== Setup
file (setup-file-blue1-in-yellow)
store (ths/setup-store file)
blue1 (cths/get-shape file :blue1)
;; ==== Action
page (cthf/current-page file)
yellow (cths/get-shape file :frame-yellow)
b2 (cths/get-shape file :frame-b2)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id yellow)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id yellow))
(dw/delete-selected)
(dws/select-shape (:id b2))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
b2' (cths/get-shape file' :frame-b2)
copied-yellow' (find-copied-shape yellow page' (:id b2'))
copied-blue1' (find-copied-shape blue1 page' (:id copied-yellow'))]
;; ==== Check
;; copied-blue1 has not swap-id
(t/is (some? copied-blue1'))
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-keep-swap-slot-copy-paste-green-copy-to-root
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
green (cths/get-shape file :green-copy)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape uuid/zero)
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
blue2' (cths/get-shape file' :blue-copy-in-green-copy)
copied-green' (find-copied-shape green page' uuid/zero)
copied-blue2' (find-copied-shape blue2' page' (:id copied-green'))]
;; ==== Check
;; blue2 has swap-id
(t/is (some? (ctk/get-swap-slot blue2')))
;; copied-blue2 also has swap-id
(t/is (some? copied-blue2'))
(t/is (some? (ctk/get-swap-slot copied-blue2')))))))))
(t/deftest test-keep-swap-slot-copy-paste-green-copy-to-b1
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
b1 (cths/get-shape file :frame-b1)
green (cths/get-shape file :green-copy)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id b1))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
b1' (cths/get-shape file' :frame-b1)
blue2' (cths/get-shape file' :blue-copy-in-green-copy)
copied-green' (find-copied-shape green page' (:id b1'))
copied-blue2' (find-copied-shape blue2' page' (:id copied-green'))]
;; ==== Check
;; blue1 has swap-id
(t/is (some? (ctk/get-swap-slot blue2')))
;; copied-blue1 also has swap-id
(t/is (some? copied-blue2'))
(t/is (some? (ctk/get-swap-slot copied-blue2')))))))))
(t/deftest test-keep-swap-slot-copy-paste-green-copy-to-b2
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
b2 (cths/get-shape file :frame-b2)
green (cths/get-shape file :green-copy)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id b2))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
b2' (cths/get-shape file' :frame-b2)
blue2' (cths/get-shape file' :blue-copy-in-green-copy)
copied-green' (find-copied-shape green page' (:id b2'))
copied-blue2' (find-copied-shape blue2' page' (:id copied-green'))]
;; ==== Check
;; blue2 has swap-id
(t/is (some? (ctk/get-swap-slot blue2')))
;; copied-blue1 also has swap-id
(t/is (some? copied-blue2'))
(t/is (some? (ctk/get-swap-slot copied-blue2')))))))))
(t/deftest test-keep-swap-slot-cut-paste-green-copy-to-root
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
green (cths/get-shape file :green-copy)
blue2 (cths/get-shape file :blue-copy-in-green-copy)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id green))
(dw/delete-selected)
(dws/select-shape uuid/zero)
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
copied-green' (find-copied-shape green page' uuid/zero)
copied-blue1' (find-copied-shape blue2 page' (:id copied-green'))]
;; ==== Check
;; copied-blue1 has swap-id
(t/is (some? copied-blue1'))
(t/is (some? (ctk/get-swap-slot copied-blue1')))))))))
(t/deftest test-keep-swap-slot-cut-paste-green-copy-to-b1
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
b1 (cths/get-shape file :frame-b1)
green (cths/get-shape file :green-copy)
blue2 (cths/get-shape file :blue-copy-in-green-copy)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id green))
(dw/delete-selected)
(dws/select-shape (:id b1))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
b1' (cths/get-shape file' :frame-b1)
copied-green' (find-copied-shape green page' (:id b1'))
copied-blue2' (find-copied-shape blue2 page' (:id copied-green'))]
;; ==== Check
;; copied-blue1 has swap-id
(t/is (some? copied-blue2'))
(t/is (some? (ctk/get-swap-slot copied-blue2')))))))))
(t/deftest test-keep-swap-slot-cut-paste-green-copy-to-b2
(t/async
done
(let [;; ==== Setup
file (setup-file)
store (ths/setup-store file)
;; ==== Action
page (cthf/current-page file)
b2 (cths/get-shape file :frame-b2)
blue2 (cths/get-shape file :blue-copy-in-green-copy)
green (cths/get-shape file :green-copy)
features #{"components/v2"}
version 46
pdata (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id file) file} page file features version)
events
[(dws/select-shape (:id green))
(dw/delete-selected)
(dws/select-shape (:id b2))
(dw/paste-shapes pdata)]]
(ths/run-store
store done events
(fn [new-state]
(let [;; ==== Get
file' (ths/get-file-from-store new-state)
page' (cthf/current-page file')
b2' (cths/get-shape file' :frame-b2)
copied-green' (find-copied-shape green page' (:id b2'))
copied-blue2' (find-copied-shape blue2 page' (:id copied-green'))]
;; ==== Check
;; copied-blue1 has swap-id
(t/is (some? copied-blue2'))
(t/is (some? (ctk/get-swap-slot copied-blue2')))))))))

View File

@@ -5175,3 +5175,33 @@ msgstr "Update"
msgid "workspace.viewport.click-to-close-path"
msgstr "Click to close the path"
msgid "workspace.plugins.title"
msgstr "Plugins"
msgid "workspace.plugins.search-placeholder"
msgstr "Write a plugin URL"
msgid "workspace.plugins.install"
msgstr "Install"
msgid "workspace.plugins.installed-plugins"
msgstr "Installed plugins"
msgid "workspace.plugins.empty-plugins"
msgstr "No plugins installed yet"
msgid "workspace.plugins.button-open"
msgstr "Open"
msgid "workspace.plugins.error.url"
msgstr "The plugin doesn't exist or the URL is not correct."
msgid "workspace.plugins.success"
msgstr "Plugin correctly loaded."
msgid "workspace.plugins.menu.title"
msgstr "Plugins"
msgid "workspace.plugins.menu.plugins-manager"
msgstr "Plugins manager"

View File

@@ -5301,3 +5301,34 @@ msgstr "Actualizar"
msgid "workspace.viewport.click-to-close-path"
msgstr "Pulsar para cerrar la ruta"
msgid "workspace.plugins.title"
msgstr "Extensiones"
msgid "workspace.plugins.search-placeholder"
msgstr "Intruduzca URL de la extensión"
msgid "workspace.plugins.install"
msgstr "Instalar"
msgid "workspace.plugins.installed-plugins"
msgstr "Extensiones instaladas"
msgid "workspace.plugins.empty-plugins"
msgstr "No se encuentran extensiones"
msgid "workspace.plugins.button-open"
msgstr "Abrir"
msgid "workspace.plugins.error.url"
msgstr "La extensión no existe o la url no es correcta."
msgid "workspace.plugins.success"
msgstr "Extensión cargada correctamente."
msgid "workspace.plugins.menu.title"
msgstr "Extensiones"
msgid "workspace.plugins.menu.plugins-manager"
msgstr "Gestor de extensiones"