mirror of
https://github.com/penpot/penpot.git
synced 2026-07-25 23:48:15 +00:00
🎉 Add customizable shortcuts (#10237)
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
This commit is contained in:
parent
caacd482af
commit
5de06e8f77
@ -59,7 +59,9 @@
|
||||
[:release-notes-viewed {:optional true}
|
||||
[::sm/text {:max 100}]]
|
||||
[:notifications {:optional true} schema:props-notifications]
|
||||
[:workspace-visited {:optional true} ::sm/boolean]])
|
||||
[:workspace-visited {:optional true} ::sm/boolean]
|
||||
[:custom-shortcuts {:optional true}
|
||||
[:map-of {:gen/max 10} :keyword [:map-of :keyword :string]]]])
|
||||
|
||||
(def schema:profile
|
||||
[:map {:title "Profile"}
|
||||
|
||||
@ -175,7 +175,10 @@
|
||||
:mcp
|
||||
:background-blur
|
||||
:available-viewer-wasm
|
||||
:stroke-per-side})
|
||||
:stroke-path
|
||||
:stroke-per-side
|
||||
|
||||
:custom-shortcuts})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
{
|
||||
"~:v2-info-shown": true,
|
||||
"~:newsletter-updates": false,
|
||||
"~:onboarding-viewed": true,
|
||||
"~:onboarding-questions": {
|
||||
"~:role": "ux",
|
||||
"~:start-with": "wireframing",
|
||||
"~:expected-use": "personal",
|
||||
"~:experience-design-tool": "sketch"
|
||||
},
|
||||
"~:onboarding-questions-answered": true,
|
||||
"~:workspace-visited": true,
|
||||
"~:renderer": "~:wasm",
|
||||
"~:viewed-tutorial?": false,
|
||||
"~:viewed-walkthrough?": false,
|
||||
"~:release-notes-viewed": "0.0",
|
||||
"~:nudge": {
|
||||
"~:big": 10,
|
||||
"~:small": 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
{
|
||||
"~:v2-info-shown": true,
|
||||
"~:newsletter-updates": false,
|
||||
"~:onboarding-viewed": true,
|
||||
"~:onboarding-questions": {
|
||||
"~:role": "ux",
|
||||
"~:start-with": "wireframing",
|
||||
"~:expected-use": "personal",
|
||||
"~:experience-design-tool": "sketch"
|
||||
},
|
||||
"~:onboarding-questions-answered": true,
|
||||
"~:workspace-visited": true,
|
||||
"~:custom-shortcuts": {
|
||||
"~:workspace": {
|
||||
"~:align-bottom": "ctrl+y",
|
||||
"~:redo": ""
|
||||
}
|
||||
},
|
||||
"~:renderer": "~:wasm",
|
||||
"~:viewed-tutorial?": false,
|
||||
"~:viewed-walkthrough?": false,
|
||||
"~:release-notes-viewed": "0.0",
|
||||
"~:nudge": {
|
||||
"~:big": 10,
|
||||
"~:small": 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
{
|
||||
"~:v2-info-shown": true,
|
||||
"~:newsletter-updates": false,
|
||||
"~:onboarding-viewed": true,
|
||||
"~:onboarding-questions": {
|
||||
"~:role": "ux",
|
||||
"~:start-with": "wireframing",
|
||||
"~:expected-use": "personal",
|
||||
"~:experience-design-tool": "sketch"
|
||||
},
|
||||
"~:onboarding-questions-answered": true,
|
||||
"~:workspace-visited": true,
|
||||
"~:custom-shortcuts": {
|
||||
"~:workspace": {
|
||||
"~:align-bottom": "ctrl+y",
|
||||
"~:redo": ""
|
||||
}
|
||||
},
|
||||
"~:renderer": "~:wasm",
|
||||
"~:viewed-tutorial?": false,
|
||||
"~:viewed-walkthrough?": false,
|
||||
"~:release-notes-viewed": "0.0",
|
||||
"~:nudge": {
|
||||
"~:big": 10,
|
||||
"~:small": 1
|
||||
}
|
||||
}
|
||||
328
frontend/playwright/ui/pages/ShortcutsPage.js
Normal file
328
frontend/playwright/ui/pages/ShortcutsPage.js
Normal file
@ -0,0 +1,328 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { BaseWebSocketPage } from "./BaseWebSocketPage";
|
||||
|
||||
function decodeKeyword(value) {
|
||||
if (typeof value === "string" && value.startsWith("~:")) {
|
||||
return value.slice(2);
|
||||
}
|
||||
if (typeof value === "string" && value.startsWith("~$")) {
|
||||
return value.slice(2);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function decodeTransit(data) {
|
||||
if (Array.isArray(data)) {
|
||||
if (data[0] === "^ ") {
|
||||
const result = {};
|
||||
for (let i = 1; i < data.length; i += 2) {
|
||||
const key = decodeTransit(data[i]);
|
||||
const value = decodeTransit(data[i + 1]);
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return data.map(decodeTransit);
|
||||
}
|
||||
|
||||
if (data !== null && typeof data === "object") {
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
result[decodeKeyword(key)] = decodeTransit(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return decodeKeyword(data);
|
||||
}
|
||||
|
||||
function encodeKeyword(key) {
|
||||
return `~:${key}`;
|
||||
}
|
||||
|
||||
function encodeTransit(data) {
|
||||
if (data === null || typeof data === "boolean" || typeof data === "number") {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (typeof data === "string") {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(encodeTransit);
|
||||
}
|
||||
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
result[encodeKeyword(key)] = encodeTransit(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function decodeRequestBody(body) {
|
||||
try {
|
||||
return decodeTransit(JSON.parse(body));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class ShortcutsPage extends BaseWebSocketPage {
|
||||
static async init(page) {
|
||||
await super.init(page);
|
||||
|
||||
const profileText = await readFile(
|
||||
"playwright/data/logged-in-user/get-profile-logged-in.json",
|
||||
"utf-8",
|
||||
);
|
||||
const baseProfile = decodeTransit(JSON.parse(profileText));
|
||||
let customShortcuts = null;
|
||||
|
||||
await page.route("**/api/main/methods/get-profile", (route) => {
|
||||
const profile = JSON.parse(JSON.stringify(baseProfile));
|
||||
if (customShortcuts) {
|
||||
profile.props["custom-shortcuts"] = customShortcuts;
|
||||
}
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/transit+json",
|
||||
body: JSON.stringify(encodeTransit(profile)),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route(
|
||||
"**/api/main/methods/update-profile-props",
|
||||
async (route, request) => {
|
||||
const decoded = decodeRequestBody(request.postData() ?? "{}");
|
||||
if (decoded?.props && "custom-shortcuts" in decoded.props) {
|
||||
customShortcuts = decoded.props["custom-shortcuts"];
|
||||
}
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/transit+json",
|
||||
body: "{}",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await super.mockRPC(
|
||||
page,
|
||||
"get-teams",
|
||||
"logged-in-user/get-teams-default.json",
|
||||
);
|
||||
}
|
||||
|
||||
static async initWithShortcuts(page) {
|
||||
await super.init(page);
|
||||
|
||||
await super.mockRPCs(page, {
|
||||
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
||||
"get-teams": "logged-in-user/get-teams-default.json",
|
||||
"update-profile-props":
|
||||
"logged-in-user/update-profile-with-shortcuts.json",
|
||||
});
|
||||
}
|
||||
|
||||
static async initWithNoShortcuts(page) {
|
||||
await super.init(page);
|
||||
|
||||
await super.mockRPCs(page, {
|
||||
"get-profile": "logged-in-user/get-profile-no-shortcuts.json",
|
||||
"get-teams": "logged-in-user/get-teams-default.json",
|
||||
"update-profile-props":
|
||||
"logged-in-user/update-profile-with-shortcuts.json",
|
||||
});
|
||||
}
|
||||
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
this.shortcutsSection = page.getByRole("region", { name: /shortcuts/i });
|
||||
this.searchInput = page.getByRole("textbox", { name: /shortcuts/i });
|
||||
|
||||
this.allTab = page.getByRole("tab", { name: "All" });
|
||||
this.personalizedTab = page.getByRole("tab", { name: "Personalized" });
|
||||
this.disabledTab = page.getByRole("tab", { name: "Disabled" });
|
||||
|
||||
this.restoreAllButton = page.getByRole("button", {
|
||||
name: /restore all/i,
|
||||
});
|
||||
|
||||
this.importExportButton = page.getByRole("button", {
|
||||
name: /import\/export/i,
|
||||
});
|
||||
|
||||
this.fileInput = page.locator('input[type="file"]');
|
||||
}
|
||||
|
||||
async goToShortcuts() {
|
||||
await this.page.goto("#/settings/shortcuts");
|
||||
await expect(this.shortcutsSection).toBeVisible();
|
||||
}
|
||||
|
||||
async searchForShortcut(term) {
|
||||
await this.searchInput.fill(term);
|
||||
}
|
||||
|
||||
async clearSearch() {
|
||||
await this.searchInput.clear();
|
||||
}
|
||||
|
||||
async clickTab(tabName) {
|
||||
const tab = this.page.getByRole("tab", { name: tabName });
|
||||
await tab.click();
|
||||
}
|
||||
|
||||
async getShortcutRow(shortcutName) {
|
||||
return this.page.getByRole("listitem", {
|
||||
name: new RegExp(`^${shortcutName}$`, "i"),
|
||||
includeHidden: true,
|
||||
});
|
||||
}
|
||||
|
||||
async expandSubsection(subsectionName) {
|
||||
const subsectionButton = this.page.getByRole("button", {
|
||||
name: subsectionName,
|
||||
});
|
||||
await subsectionButton.click();
|
||||
}
|
||||
|
||||
async clickEditShortcut(shortcutName) {
|
||||
const button = this.page.getByRole("button", {
|
||||
name: new RegExp(`Edit ${shortcutName}`, "i"),
|
||||
});
|
||||
await button.click();
|
||||
const recordingArea = this.page.getByText("Press the key combination");
|
||||
await expect(recordingArea).toBeVisible();
|
||||
await recordingArea.focus();
|
||||
}
|
||||
|
||||
async pressKey(key) {
|
||||
await this.page.keyboard.press(key);
|
||||
}
|
||||
|
||||
async pressKeyCombo(keys) {
|
||||
for (const key of keys) {
|
||||
await this.page.keyboard.down(key);
|
||||
}
|
||||
for (const key of [...keys].reverse()) {
|
||||
await this.page.keyboard.up(key);
|
||||
}
|
||||
}
|
||||
|
||||
async saveShortcut() {
|
||||
const saveButton = this.page.getByRole("button", { name: /save/i });
|
||||
await saveButton.click();
|
||||
}
|
||||
|
||||
async cancelEdit() {
|
||||
const cancelButton = this.page.getByRole("button", { name: /cancel/i });
|
||||
await cancelButton.click();
|
||||
}
|
||||
|
||||
async resetShortcut(shortcutName) {
|
||||
const row = await this.getShortcutRow(shortcutName);
|
||||
const resetButton = row.getByRole("button", { name: /reset/i });
|
||||
await resetButton.click();
|
||||
}
|
||||
|
||||
async disableShortcut(shortcutName) {
|
||||
const row = await this.getShortcutRow(shortcutName);
|
||||
await row
|
||||
.getByRole("button", { name: new RegExp(`Edit ${shortcutName}`, "i") })
|
||||
.click();
|
||||
const disableButton = this.page.getByRole("button", { name: /disable/i });
|
||||
await disableButton.click();
|
||||
}
|
||||
|
||||
async expectShortcutCustomized(shortcutName) {
|
||||
const row = await this.getShortcutRow(shortcutName);
|
||||
await expect(row).toHaveAttribute("data-customized", "true");
|
||||
}
|
||||
|
||||
async expectShortcutNotCustomized(shortcutName) {
|
||||
const row = await this.getShortcutRow(shortcutName);
|
||||
await expect(row).not.toHaveAttribute("data-customized", "true");
|
||||
}
|
||||
|
||||
async expectShortcutHasConflict(shortcutName) {
|
||||
const row = await this.getShortcutRow(shortcutName);
|
||||
await expect(row).toHaveAttribute("data-conflict", "true");
|
||||
}
|
||||
|
||||
async expectShortcutVisible(shortcutName) {
|
||||
const row = await this.getShortcutRow(shortcutName);
|
||||
await expect(row).toBeVisible();
|
||||
}
|
||||
|
||||
async expectShortcutHidden(shortcutName) {
|
||||
const row = await this.getShortcutRow(shortcutName);
|
||||
await expect(row).not.toBeVisible();
|
||||
}
|
||||
|
||||
async restoreAllShortcuts() {
|
||||
await this.restoreAllButton.click();
|
||||
const confirmButton = this.page.getByRole("button", {
|
||||
name: "Restore",
|
||||
exact: true,
|
||||
});
|
||||
await confirmButton.click();
|
||||
}
|
||||
|
||||
async exportShortcuts() {
|
||||
await this.importExportButton.click();
|
||||
const exportButton = this.page.getByRole("menuitem", { name: /export/i });
|
||||
await exportButton.click();
|
||||
}
|
||||
|
||||
async importShortcuts(jsonData) {
|
||||
await this.importExportButton.click();
|
||||
const importButton = this.page.getByRole("menuitem", { name: /import$/i });
|
||||
await importButton.click();
|
||||
|
||||
const buffer = Buffer.from(JSON.stringify(jsonData), "utf-8");
|
||||
await this.fileInput.setInputFiles({
|
||||
name: "shortcuts.json",
|
||||
mimeType: "application/json",
|
||||
buffer,
|
||||
});
|
||||
}
|
||||
|
||||
async importShortcutsRaw(content) {
|
||||
await this.importExportButton.click();
|
||||
const importButton = this.page.getByRole("menuitem", { name: /import$/i });
|
||||
await importButton.click();
|
||||
|
||||
const data =
|
||||
typeof content === "string" ? content : JSON.stringify(content);
|
||||
const buffer = Buffer.from(data, "utf-8");
|
||||
await this.fileInput.setInputFiles({
|
||||
name: "shortcuts.json",
|
||||
mimeType: "application/json",
|
||||
buffer,
|
||||
});
|
||||
}
|
||||
|
||||
async getExportedJson() {
|
||||
const [download] = await Promise.all([
|
||||
this.page.waitForEvent("download"),
|
||||
this.exportShortcuts(),
|
||||
]);
|
||||
|
||||
expect(download.suggestedFilename()).toBe("penpot-shortcuts.json");
|
||||
|
||||
const path = await download.path();
|
||||
const content = await readFile(path, "utf-8");
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
async expectShortcutDisabled(shortcutName) {
|
||||
const row = await this.getShortcutRow(shortcutName);
|
||||
await expect(row).toHaveAttribute("data-customized", "true");
|
||||
await expect(row.locator("use[href*='broken-link']")).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export default ShortcutsPage;
|
||||
323
frontend/playwright/ui/specs/shortcuts.spec.js
Normal file
323
frontend/playwright/ui/specs/shortcuts.spec.js
Normal file
@ -0,0 +1,323 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import ShortcutsPage from "../pages/ShortcutsPage";
|
||||
|
||||
const customShortcutsFlag = "enable-custom-shortcuts";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await ShortcutsPage.init(page);
|
||||
await ShortcutsPage.mockConfigFlags(page, [customShortcutsFlag]);
|
||||
});
|
||||
|
||||
test.describe("Shortcuts Settings Page", () => {
|
||||
test("Shortcuts page loads correctly", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await expect(shortcutsPage.shortcutsSection).toBeVisible();
|
||||
await expect(shortcutsPage.searchInput).toBeVisible();
|
||||
});
|
||||
|
||||
test("Tabs are visible and clickable", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await expect(shortcutsPage.allTab).toBeVisible();
|
||||
await expect(shortcutsPage.personalizedTab).toBeVisible();
|
||||
await expect(shortcutsPage.disabledTab).toBeVisible();
|
||||
|
||||
await shortcutsPage.clickTab("Personalized");
|
||||
await expect(shortcutsPage.personalizedTab).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
const personalizedPlacehonder = page.getByText(/Head to All to start/i);
|
||||
await expect(personalizedPlacehonder).toBeVisible();
|
||||
|
||||
await shortcutsPage.clickTab("Disabled");
|
||||
await expect(shortcutsPage.disabledTab).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
const disabledPlaceholder = page.getByText(/There are not disabled/i);
|
||||
await expect(disabledPlaceholder).toBeVisible();
|
||||
|
||||
await shortcutsPage.clickTab("All");
|
||||
await expect(shortcutsPage.allTab).toHaveAttribute("aria-selected", "true");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Shortcut Customization", () => {
|
||||
test("User can edit a shortcut", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
// Expand subsection
|
||||
await shortcutsPage.expandSubsection("Alignment");
|
||||
|
||||
// Start edition of the shortcut
|
||||
await shortcutsPage.clickEditShortcut("Align bottom");
|
||||
|
||||
// Press a new key combination
|
||||
await shortcutsPage.pressKey("Control+y");
|
||||
|
||||
// Save the shortcut
|
||||
await shortcutsPage.saveShortcut();
|
||||
|
||||
// Verify the shortcut is now customized
|
||||
await shortcutsPage.expectShortcutCustomized("Align bottom");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Shortcut Import", () => {
|
||||
test("Import valid custom shortcuts", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.importShortcuts({
|
||||
workspace: { "align-bottom": "ctrl+y" },
|
||||
});
|
||||
|
||||
await shortcutsPage.expectShortcutCustomized("Align bottom");
|
||||
|
||||
await shortcutsPage.searchForShortcut("Align bottom");
|
||||
await shortcutsPage.clickTab("Personalized");
|
||||
await shortcutsPage.expectShortcutVisible("Align bottom");
|
||||
});
|
||||
|
||||
test("Import invalid JSON syntax", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.importShortcutsRaw("{invalid");
|
||||
|
||||
await expect(
|
||||
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Import valid JSON with invalid schema", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.importShortcuts({
|
||||
workspace: { "unknown-shortcut": "ctrl+y" },
|
||||
});
|
||||
|
||||
await expect(
|
||||
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Import JSON without workspace context accepted", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.importShortcuts({
|
||||
dashboard: { "toggle-theme": "alt+m" },
|
||||
});
|
||||
|
||||
await expect(
|
||||
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Import JSON with unsupported shortcut format", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.importShortcuts({
|
||||
workspace: { escape: 123 },
|
||||
});
|
||||
|
||||
await expect(
|
||||
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Import conflicting shortcuts", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.importShortcuts({
|
||||
workspace: {
|
||||
"align-bottom": "alt+a",
|
||||
},
|
||||
});
|
||||
|
||||
await shortcutsPage.expectShortcutCustomized("Align bottom");
|
||||
|
||||
await shortcutsPage.expandSubsection("Alignment");
|
||||
await shortcutsPage.expectShortcutDisabled("Align left");
|
||||
|
||||
const exported = await shortcutsPage.getExportedJson();
|
||||
expect(exported.workspace).toMatchObject({
|
||||
"align-bottom": "alt+a",
|
||||
"align-left": "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Shortcut Export", () => {
|
||||
test("Export customized shortcuts", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.expandSubsection("Alignment");
|
||||
await shortcutsPage.clickEditShortcut("Align bottom");
|
||||
await shortcutsPage.pressKey("Control+y");
|
||||
await shortcutsPage.saveShortcut();
|
||||
await shortcutsPage.expectShortcutCustomized("Align bottom");
|
||||
|
||||
const exported = await shortcutsPage.getExportedJson();
|
||||
expect(exported).toHaveProperty("workspace");
|
||||
expect(exported.workspace).toMatchObject({
|
||||
"align-bottom": "ctrl+y",
|
||||
});
|
||||
});
|
||||
|
||||
test("Export includes disabled shortcuts", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.importShortcuts({
|
||||
workspace: { "align-bottom": "" },
|
||||
});
|
||||
|
||||
await shortcutsPage.searchForShortcut("Align bottom");
|
||||
await shortcutsPage.expectShortcutDisabled("Align bottom");
|
||||
|
||||
const exported = await shortcutsPage.getExportedJson();
|
||||
expect(exported.workspace).toMatchObject({
|
||||
"align-bottom": "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Shortcut Conflict Detection", () => {
|
||||
test("Detects conflict and disables old shortcut on save", async ({
|
||||
page,
|
||||
}) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.expandSubsection("Alignment");
|
||||
await shortcutsPage.clickEditShortcut("Align bottom");
|
||||
await shortcutsPage.pressKey("Alt+a");
|
||||
|
||||
await expect(
|
||||
page.getByRole("alert").filter({ hasText: /Combination assigned to/i }),
|
||||
).toBeVisible();
|
||||
|
||||
await shortcutsPage.saveShortcut();
|
||||
await shortcutsPage.expectShortcutCustomized("Align bottom");
|
||||
await shortcutsPage.expectShortcutDisabled("Align left");
|
||||
|
||||
const exported = await shortcutsPage.getExportedJson();
|
||||
expect(exported.workspace).toMatchObject({
|
||||
"align-bottom": "alt+a",
|
||||
"align-left": "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Shortcut Reset", () => {
|
||||
test("Reset after import restores defaults", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.importShortcuts({
|
||||
workspace: { "align-bottom": "ctrl+y" },
|
||||
});
|
||||
await shortcutsPage.expectShortcutCustomized("Align bottom");
|
||||
|
||||
await shortcutsPage.restoreAllShortcuts();
|
||||
await shortcutsPage.expectShortcutNotCustomized("Align bottom");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Shortcut Persistence", () => {
|
||||
test("Custom shortcuts persist after reload", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.expandSubsection("Alignment");
|
||||
await shortcutsPage.clickEditShortcut("Align bottom");
|
||||
await shortcutsPage.pressKey("Control+y");
|
||||
await shortcutsPage.saveShortcut();
|
||||
await shortcutsPage.expectShortcutCustomized("Align bottom");
|
||||
|
||||
await page.reload();
|
||||
await shortcutsPage.goToShortcuts();
|
||||
await shortcutsPage.expectShortcutCustomized("Align bottom");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Cancel Shortcut Editing", () => {
|
||||
test("Cancel editing does not save changes", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.expandSubsection("Alignment");
|
||||
await shortcutsPage.clickEditShortcut("Align bottom");
|
||||
await shortcutsPage.pressKey("Control+y");
|
||||
await shortcutsPage.cancelEdit();
|
||||
|
||||
await shortcutsPage.expectShortcutNotCustomized("Align bottom");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Duplicate Shortcut Prevention", () => {
|
||||
test("Assigning same shortcut to two actions shows conflict and disables first", async ({
|
||||
page,
|
||||
}) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.expandSubsection("Alignment");
|
||||
|
||||
await shortcutsPage.clickEditShortcut("Align bottom");
|
||||
await shortcutsPage.pressKey("Control+y");
|
||||
await shortcutsPage.saveShortcut();
|
||||
await shortcutsPage.expectShortcutCustomized("Align bottom");
|
||||
|
||||
await shortcutsPage.clickEditShortcut("Align left");
|
||||
await shortcutsPage.pressKey("Control+y");
|
||||
|
||||
await expect(
|
||||
page.getByRole("alert").filter({ hasText: /Combination assigned to/i }),
|
||||
).toBeVisible();
|
||||
|
||||
await shortcutsPage.saveShortcut();
|
||||
await shortcutsPage.expectShortcutCustomized("Align left");
|
||||
|
||||
const exported = await shortcutsPage.getExportedJson();
|
||||
expect(exported.workspace).toMatchObject({
|
||||
"align-left": "ctrl+y",
|
||||
"align-bottom": "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Shortcut Round-Trip", () => {
|
||||
test("Export, reset, import restores configuration", async ({ page }) => {
|
||||
const shortcutsPage = new ShortcutsPage(page);
|
||||
await shortcutsPage.goToShortcuts();
|
||||
|
||||
await shortcutsPage.expandSubsection("Alignment");
|
||||
await shortcutsPage.clickEditShortcut("Align bottom");
|
||||
await shortcutsPage.pressKey("Control+y");
|
||||
await shortcutsPage.saveShortcut();
|
||||
await shortcutsPage.expectShortcutCustomized("Align bottom");
|
||||
|
||||
const exported = await shortcutsPage.getExportedJson();
|
||||
|
||||
await shortcutsPage.restoreAllShortcuts();
|
||||
await shortcutsPage.expectShortcutNotCustomized("Align bottom");
|
||||
|
||||
await shortcutsPage.importShortcuts(exported);
|
||||
await shortcutsPage.expectShortcutCustomized("Align bottom");
|
||||
|
||||
const reExported = await shortcutsPage.getExportedJson();
|
||||
expect(reExported).toEqual(exported);
|
||||
});
|
||||
});
|
||||
@ -17,31 +17,36 @@
|
||||
(def shortcuts
|
||||
{:toggle-theme {:tooltip (ds/alt "M")
|
||||
:command (ds/a-mod "m")
|
||||
:subsections [:general-dashboard]
|
||||
:section [:dashboard]
|
||||
:subsections [:generic]
|
||||
:fn #(st/emit! (with-meta (du/toggle-theme)
|
||||
{::ev/origin "dashboard:shortcuts"}))}})
|
||||
|
||||
(def shortcuts-sidebar-navigation
|
||||
{:go-to-drafts {:tooltip "G D"
|
||||
:command "g d"
|
||||
:section [:dashboard]
|
||||
:subsections [:navigation-dashboard]
|
||||
:fn #(st/emit! (dcm/go-to-dashboard-files :project-id :default))}
|
||||
|
||||
:go-to-libs {:tooltip "G L"
|
||||
:command "g l"
|
||||
:section [:dashboard]
|
||||
:subsections [:navigation-dashboard]
|
||||
:fn #(st/emit! (dcm/go-to-dashboard-libraries))}})
|
||||
|
||||
(def shortcut-search
|
||||
{:go-to-search {:tooltip (ds/meta "F")
|
||||
:command (ds/c-mod "f")
|
||||
:section [:dashboard]
|
||||
:subsections [:navigation-dashboard]
|
||||
:fn #(st/emit! (dcm/go-to-dashboard-search))}})
|
||||
|
||||
(def shortcut-create-new-project
|
||||
{:create-new-project {:tooltip "+"
|
||||
:command "+"
|
||||
:subsections [:general-dashboard]
|
||||
:section [:dashboard]
|
||||
:subsections [:generic]
|
||||
:fn #(st/emit! (dd/create-element))}})
|
||||
|
||||
;; Shortcuts combinations for files, drafts, libraries and fonts sections
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
;; 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.data.dashboard.shortcuts.customize
|
||||
(:require
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn set-custom-shortcut
|
||||
[shortcut-key new-command conflicting-key group-key]
|
||||
(ptk/reify ::set-custom-shortcut
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [current-customs (get-in state [:profile :props :custom-shortcuts] {})
|
||||
group-map (get current-customs group-key)
|
||||
group-map (if (map? group-map) group-map {})
|
||||
group-map (assoc group-map shortcut-key new-command)
|
||||
;; clear conflicting shortcut in the same group
|
||||
group-map (if conflicting-key
|
||||
(assoc group-map conflicting-key "")
|
||||
group-map)
|
||||
new-customs (assoc current-customs group-key group-map)]
|
||||
(rx/of
|
||||
(ev/event {::ev/name "set-custom-shortcut"})
|
||||
(du/update-profile-props {:custom-shortcuts new-customs})
|
||||
(ds/rebind-shortcuts))))))
|
||||
|
||||
|
||||
(defn reset-custom-shortcut
|
||||
[shortcut-key default-command group-key]
|
||||
|
||||
(ptk/reify ::reset-custom-shortcut
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [current-customs (get-in state [:profile :props :custom-shortcuts] {})
|
||||
group-map (get current-customs group-key)
|
||||
group-map (if (map? group-map) group-map {})
|
||||
|
||||
;; Find any action that is currently using the default command as its custom shortcut
|
||||
owner-key (when (and default-command (not= default-command ""))
|
||||
(some (fn [[k v]]
|
||||
(when (if (vector? default-command)
|
||||
(some #{v} default-command)
|
||||
(= v default-command))
|
||||
k))
|
||||
group-map))
|
||||
|
||||
;; Remove the shortcut being reset
|
||||
group-map (dissoc group-map shortcut-key)
|
||||
|
||||
;; Disable the owner that was using the default command
|
||||
group-map (if (and owner-key (not= owner-key shortcut-key))
|
||||
(assoc group-map owner-key "")
|
||||
group-map)
|
||||
|
||||
new-customs (if (empty? group-map)
|
||||
(dissoc current-customs group-key)
|
||||
(assoc current-customs group-key group-map))]
|
||||
(rx/of
|
||||
(ev/event {::ev/name "reset-custom-shortcut"})
|
||||
(du/update-profile-props {:custom-shortcuts new-customs})
|
||||
(ds/rebind-shortcuts))))))
|
||||
|
||||
(defn reset-all-custom-shortcuts
|
||||
[]
|
||||
(ptk/reify ::reset-all-custom-shortcuts
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (ev/event {::ev/name "reset-all-custom-shortcuts"})
|
||||
(du/update-profile-props {:custom-shortcuts {}})
|
||||
(ds/rebind-shortcuts)))))
|
||||
@ -121,6 +121,61 @@
|
||||
[sc]
|
||||
(str/split sc #"\+| "))))
|
||||
|
||||
(defn command->tooltip
|
||||
"Converts a Mousetrap command string (e.g. \"command+shift+z\") to
|
||||
a human-readable display string in the same format used by :tooltip
|
||||
fields (e.g. \"⌘⇧Z\" on macOS or \"Ctrl+Shift+Z\" on Windows).
|
||||
Returns nil for empty or unbound commands."
|
||||
[command]
|
||||
(when (and command (not= command ""))
|
||||
(let [is-macos? (cf/check-platform? :macos)
|
||||
parts (str/split command #"\+")
|
||||
display-part (fn [p]
|
||||
(case p
|
||||
"ctrl" (if is-macos? mac-control "Ctrl+")
|
||||
"command" (if is-macos? mac-command "Ctrl+")
|
||||
"alt" (if is-macos? mac-option "Alt+")
|
||||
"shift" (if is-macos? mac-shift "Shift+")
|
||||
"up" up-arrow
|
||||
"down" down-arrow
|
||||
"left" left-arrow
|
||||
"right" right-arrow
|
||||
"del" (if is-macos? mac-delete "Del")
|
||||
"backspace" (if is-macos? mac-delete "Backspace")
|
||||
"escape" (if is-macos? mac-esc "Escape")
|
||||
"enter" (if is-macos? mac-enter "Enter")
|
||||
"space" "Space"
|
||||
"tab" tab
|
||||
(str/upper p)))]
|
||||
(str/join "" (map display-part parts)))))
|
||||
|
||||
(defn apply-custom-overrides
|
||||
[shortcuts custom-overrides group-key]
|
||||
(let [group-overrides (get custom-overrides group-key)]
|
||||
(if (empty? group-overrides)
|
||||
shortcuts
|
||||
(reduce-kv
|
||||
(fn [acc sc-key new-command]
|
||||
(if (and (contains? acc sc-key)
|
||||
(not (:disabled (get acc sc-key))))
|
||||
(-> acc
|
||||
(assoc-in [sc-key :command] new-command)
|
||||
(update sc-key dissoc :show-command))
|
||||
acc))
|
||||
shortcuts
|
||||
group-overrides))))
|
||||
|
||||
(defn build-command-index
|
||||
[shortcuts]
|
||||
(reduce-kv
|
||||
(fn [acc sc-key sc-def]
|
||||
(let [commands (:command sc-def)]
|
||||
(if (vector? commands)
|
||||
(reduce #(assoc %1 %2 sc-key) acc commands)
|
||||
(assoc acc commands sc-key))))
|
||||
{}
|
||||
shortcuts))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Events
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@ -171,17 +226,34 @@
|
||||
(fnil conj (d/ordered-map)))
|
||||
|
||||
(defn push-shortcuts
|
||||
[key shortcuts]
|
||||
[key shortcuts group-key & {:keys [merge-shortcuts]}]
|
||||
(assert (keyword? key) "expected a keyword for `key`")
|
||||
(let [shortcuts (check-shortcuts shortcuts)]
|
||||
(ptk/reify ::push-shortcuts
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :shortcuts conj* [key shortcuts]))
|
||||
(let [custom-overrides (get-in state [:profile :props :custom-shortcuts])
|
||||
group-key (or group-key (:shortcuts-group-key state))
|
||||
merge-sc (cond
|
||||
(= merge-shortcuts :auto)
|
||||
(let [[_ sc] (last (:shortcuts state))]
|
||||
sc)
|
||||
|
||||
(map? merge-shortcuts)
|
||||
merge-shortcuts)
|
||||
|
||||
effective (if merge-sc
|
||||
(merge merge-sc shortcuts)
|
||||
shortcuts)
|
||||
effective (apply-custom-overrides effective custom-overrides group-key)]
|
||||
(-> state
|
||||
(update :shortcuts conj* [key effective])
|
||||
(assoc :shortcuts-group-key group-key))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(reset! shortcuts)))))
|
||||
(effect [_ state _]
|
||||
(let [[_ stored] (last (:shortcuts state))]
|
||||
(reset! stored))))))
|
||||
|
||||
(defn pop-shortcuts
|
||||
[key]
|
||||
@ -193,5 +265,19 @@
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [[_key shortcuts] (last (:shortcuts state))]
|
||||
(reset! shortcuts)))))
|
||||
(let [[_key shortcuts] (last (:shortcuts state))
|
||||
custom-overrides (get-in state [:profile :props :custom-shortcuts])
|
||||
group-key (:shortcuts-group-key state)
|
||||
effective (apply-custom-overrides shortcuts custom-overrides group-key)]
|
||||
(reset! effective)))))
|
||||
|
||||
(defn rebind-shortcuts
|
||||
[]
|
||||
(ptk/reify ::rebind-shortcuts
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [custom-overrides (get-in state [:profile :props :custom-shortcuts])
|
||||
group-key (:shortcuts-group-key state)
|
||||
merged (reduce merge (map val (:shortcuts state)))
|
||||
effective (apply-custom-overrides merged custom-overrides group-key)]
|
||||
(reset! effective)))))
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
(ns app.main.data.viewer.shortcuts
|
||||
(:require
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]))
|
||||
@ -15,61 +17,80 @@
|
||||
{:increase-zoom {:tooltip "+"
|
||||
:command "+"
|
||||
:subsections [:zoom-viewer]
|
||||
:section [:viewer]
|
||||
:fn #(st/emit! dv/increase-zoom)}
|
||||
|
||||
:toggle-theme {:tooltip (ds/alt "M")
|
||||
:command (ds/a-mod "m")
|
||||
:section [:viewer]
|
||||
:subsections [:generic]
|
||||
:fn #(st/emit! (with-meta (du/toggle-theme)
|
||||
{::ev/origin "viewer:shortcuts"}))}
|
||||
|
||||
:decrease-zoom {:tooltip "-"
|
||||
:command "-"
|
||||
:section [:viewer]
|
||||
:subsections [:zoom-viewer]
|
||||
:fn #(st/emit! dv/decrease-zoom)}
|
||||
|
||||
:select-all {:tooltip (ds/meta "A")
|
||||
:command (ds/c-mod "a")
|
||||
:subsections [:general-viewer]
|
||||
:section [:viewer]
|
||||
:subsections [:generic]
|
||||
:fn #(st/emit! (dv/select-all))}
|
||||
|
||||
:reset-zoom {:tooltip (ds/shift "0")
|
||||
:command "shift+0"
|
||||
:section [:viewer]
|
||||
:subsections [:zoom-viewer]
|
||||
:fn #(st/emit! dv/reset-zoom)}
|
||||
|
||||
:toggle-zoom-style {:tooltip "F"
|
||||
:command "f"
|
||||
:section [:viewer]
|
||||
:subsections [:zoom-viewer]
|
||||
:fn #(st/emit! dv/toggle-zoom-style)}
|
||||
|
||||
:toggle-fullscreen {:tooltip (ds/shift "F")
|
||||
:command ["shift+f" "alt+enter"]
|
||||
:section [:viewer]
|
||||
:subsections [:zoom-viewer]
|
||||
:fn #(st/emit! dv/toggle-fullscreen)}
|
||||
|
||||
:prev-frame {:tooltip ds/left-arrow
|
||||
:command ["left" "up" "shift+enter" "pageup" "shift+space"]
|
||||
:subsections [:general-viewer]
|
||||
:subsections [:generic]
|
||||
:section [:viewer]
|
||||
:fn #(st/emit! dv/select-prev-frame)}
|
||||
|
||||
:next-frame {:tooltip ds/right-arrow
|
||||
:command ["right" "down" "enter" "pagedown" "space"]
|
||||
:subsections [:general-viewer]
|
||||
:subsections [:generic]
|
||||
:section [:viewer]
|
||||
:fn #(st/emit! dv/select-next-frame)}
|
||||
|
||||
:open-inspect {:tooltip "G I"
|
||||
:command "g i"
|
||||
:subsections [:navigation-viewer]
|
||||
:section [:viewer]
|
||||
:fn #(st/emit! (dv/go-to-section :inspect))}
|
||||
|
||||
:open-comments {:tooltip "G C"
|
||||
:command "g c"
|
||||
:subsections [:navigation-viewer]
|
||||
:section [:viewer]
|
||||
:fn #(st/emit! (dv/go-to-section :comments))}
|
||||
|
||||
:open-interactions {:tooltip "G V"
|
||||
:command "g v"
|
||||
:subsections [:navigation-viewer]
|
||||
:section [:viewer]
|
||||
:fn #(st/emit! (dv/go-to-section :interactions))}
|
||||
|
||||
:open-workspace {:tooltip "G W"
|
||||
:command "g w"
|
||||
:subsections [:navigation-viewer]
|
||||
:section [:viewer]
|
||||
:fn #(st/emit! (dcm/go-to-workspace))}})
|
||||
|
||||
(defn get-tooltip [shortcut]
|
||||
|
||||
@ -68,4 +68,4 @@
|
||||
|
||||
(defn get-tooltip [shortcut]
|
||||
(assert (contains? shortcuts shortcut) (str shortcut))
|
||||
(get-in shortcuts [shortcut :tooltip]))
|
||||
(get-in shortcuts [shortcut :tooltip]))
|
||||
@ -69,32 +69,38 @@
|
||||
{;; EDIT
|
||||
:undo {:tooltip (ds/meta "Z")
|
||||
:command (ds/c-mod "z")
|
||||
:section [:workspace]
|
||||
:subsections [:edit]
|
||||
:fn #(emit-when-no-readonly dwu/undo)}
|
||||
|
||||
:redo {:tooltip (ds/meta "Y")
|
||||
:command [(ds/c-mod "shift+z") (ds/c-mod "y")]
|
||||
:section [:workspace]
|
||||
:subsections [:edit]
|
||||
:fn #(emit-when-no-readonly dwu/redo)}
|
||||
|
||||
:clear-undo {:tooltip (ds/alt "Q")
|
||||
:command "alt+q"
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly dwu/reinitialize-undo)}
|
||||
|
||||
:copy {:tooltip (ds/meta "C")
|
||||
:command (ds/c-mod "c")
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dw/copy-selected))}
|
||||
|
||||
:copy-link {:tooltip (ds/shift (ds/alt "C"))
|
||||
:command "shift+alt+c"
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dw/copy-link-to-clipboard))}
|
||||
|
||||
:cut {:tooltip (ds/meta "X")
|
||||
:command (ds/c-mod "x")
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly
|
||||
(dw/copy-selected)
|
||||
(dw/delete-selected))}
|
||||
@ -103,48 +109,57 @@
|
||||
:disabled true
|
||||
:command (ds/c-mod "v")
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn (constantly nil)}
|
||||
|
||||
:paste-replace {:tooltip (ds/meta (ds/shift "V"))
|
||||
:command (ds/c-mod "shift+v")
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/paste-from-clipboard {:replace? true}))}
|
||||
|
||||
:copy-props {:tooltip (ds/meta (ds/alt "c"))
|
||||
:command (ds/c-mod "alt+c")
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dw/copy-selected-props))}
|
||||
|
||||
:paste-props {:tooltip (ds/meta (ds/alt "v"))
|
||||
:command (ds/c-mod "alt+v")
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dw/paste-selected-props))}
|
||||
|
||||
:delete {:tooltip (ds/supr)
|
||||
:command ["del" "backspace"]
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/delete-selected))}
|
||||
|
||||
:duplicate {:tooltip (ds/meta "D")
|
||||
:command (ds/c-mod "d")
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dwv/duplicate-or-add-variant))}
|
||||
|
||||
:start-editing {:tooltip (ds/enter)
|
||||
:command "enter"
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/start-editing-selected))}
|
||||
|
||||
:start-measure {:tooltip (ds/alt "")
|
||||
:command ["alt" "."]
|
||||
:type "keydown"
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/toggle-distances-display true))}
|
||||
|
||||
:stop-measure {:tooltip (ds/alt "")
|
||||
:command ["alt" "."]
|
||||
:type "keyup"
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/toggle-distances-display false))}
|
||||
|
||||
:escape {:tooltip (ds/esc)
|
||||
@ -153,8 +168,10 @@
|
||||
:fn #(st/emit! :interrupt (dwdc/clear-drawing) (dw/deselect-all true))}
|
||||
|
||||
:find {:tooltip (ds/meta "F") :command (ds/c-mod "f") :subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dw/open-layers-search :find))}
|
||||
:find-and-replace {:tooltip (ds/meta "H") :command (ds/c-mod "h") :subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dw/open-layers-search :find-and-replace))}
|
||||
|
||||
;; MODIFY LAYERS
|
||||
@ -162,11 +179,13 @@
|
||||
:rename {:tooltip (ds/alt "N")
|
||||
:command "alt+n"
|
||||
:subsections [:edit]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/start-rename-selected))}
|
||||
|
||||
:group {:tooltip (ds/meta "G")
|
||||
:command (ds/c-mod "g")
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/group-selected))}
|
||||
|
||||
:ungroup {:tooltip (ds/shift "G")
|
||||
@ -177,99 +196,119 @@
|
||||
:mask {:tooltip (ds/meta "M")
|
||||
:command (ds/c-mod "m")
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/mask-group))}
|
||||
|
||||
:unmask {:tooltip (ds/meta-shift "M")
|
||||
:command (ds/c-mod "shift+m")
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/unmask-group))}
|
||||
|
||||
:create-component-variant {:tooltip (ds/meta "K")
|
||||
:command (ds/c-mod "k")
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dwv/add-component-or-variant))}
|
||||
|
||||
:detach-component {:tooltip (ds/meta-shift "K")
|
||||
:command (ds/c-mod "shift+k")
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly dwl/detach-selected-components)}
|
||||
|
||||
:flip-vertical {:tooltip (ds/shift "V")
|
||||
:command "shift+v"
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/flip-vertical-selected))}
|
||||
|
||||
:flip-horizontal {:tooltip (ds/shift "H")
|
||||
:command "shift+h"
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/flip-horizontal-selected))}
|
||||
:bring-forward {:tooltip (ds/meta ds/up-arrow)
|
||||
:command (ds/c-mod "up")
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/vertical-order-selected :up))}
|
||||
|
||||
:bring-backward {:tooltip (ds/meta ds/down-arrow)
|
||||
:command (ds/c-mod "down")
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/vertical-order-selected :down))}
|
||||
|
||||
:bring-front {:tooltip (ds/meta-shift ds/up-arrow)
|
||||
:command (ds/c-mod "shift+up")
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/vertical-order-selected :top))}
|
||||
|
||||
:bring-back {:tooltip (ds/meta-shift ds/down-arrow)
|
||||
:command (ds/c-mod "shift+down")
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/vertical-order-selected :bottom))}
|
||||
|
||||
:move-fast-up {:tooltip (ds/shift ds/up-arrow)
|
||||
:command ["shift+up" "shift+alt+up"]
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dwt/move-selected :up true))}
|
||||
|
||||
:move-fast-down {:tooltip (ds/shift ds/down-arrow)
|
||||
:command ["shift+down" "shift+alt+down"]
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dwt/move-selected :down true))}
|
||||
|
||||
:move-fast-right {:tooltip (ds/shift ds/right-arrow)
|
||||
:command ["shift+right" "shift+alt+right"]
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dwt/move-selected :right true))}
|
||||
|
||||
:move-fast-left {:tooltip (ds/shift ds/left-arrow)
|
||||
:command ["shift+left" "shift+alt+left"]
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dwt/move-selected :left true))}
|
||||
|
||||
:move-unit-up {:tooltip ds/up-arrow
|
||||
:command ["up" "alt+up"]
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dwt/move-selected :up false))}
|
||||
|
||||
:move-unit-down {:tooltip ds/down-arrow
|
||||
:command ["down" "alt+down"]
|
||||
:section [:workspace]
|
||||
:subsections [:modify-layers]
|
||||
:fn #(emit-when-no-readonly (dwt/move-selected :down false))}
|
||||
|
||||
:move-unit-left {:tooltip ds/right-arrow
|
||||
:command ["right" "alt+right"]
|
||||
:subsections [:modify-layers]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dwt/move-selected :right false))}
|
||||
|
||||
:move-unit-right {:tooltip ds/left-arrow
|
||||
:command ["left" "alt+left"]
|
||||
:section [:workspace]
|
||||
:subsections [:modify-layers]
|
||||
:fn #(emit-when-no-readonly (dwt/move-selected :left false))}
|
||||
|
||||
:artboard-selection {:tooltip (ds/meta (ds/alt "G"))
|
||||
:command (ds/c-mod "alt+g")
|
||||
:section [:workspace]
|
||||
:subsections [:modify-layers]
|
||||
:fn #(emit-when-no-readonly (dws/create-artboard-from-selection))}
|
||||
|
||||
:toggle-layout-flex {:tooltip (ds/shift "A")
|
||||
:command "shift+a"
|
||||
:section [:workspace]
|
||||
:subsections [:modify-layers]
|
||||
:fn #(emit-when-no-readonly
|
||||
(with-meta (dwsl/toggle-layout :flex)
|
||||
@ -277,6 +316,7 @@
|
||||
|
||||
:toggle-layout-grid {:tooltip (ds/meta-shift "A")
|
||||
:command (ds/c-mod "shift+a")
|
||||
:section [:workspace]
|
||||
:subsections [:modify-layers]
|
||||
:fn #(emit-when-no-readonly
|
||||
(with-meta (dwsl/toggle-layout :grid)
|
||||
@ -285,32 +325,38 @@
|
||||
|
||||
:draw-frame {:tooltip "B"
|
||||
:command ["b" "a"]
|
||||
:section [:workspace :basics]
|
||||
:subsections [:tools :basics]
|
||||
:fn #(emit-when-no-readonly (dwd/select-for-drawing :frame))}
|
||||
|
||||
:move {:tooltip "V"
|
||||
:command "v"
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly :interrupt)}
|
||||
|
||||
:draw-rect {:tooltip "R"
|
||||
:command "r"
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dwd/select-for-drawing :rect))}
|
||||
|
||||
:draw-ellipse {:tooltip "E"
|
||||
:command "e"
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dwd/select-for-drawing :circle))}
|
||||
|
||||
:draw-text {:tooltip "T"
|
||||
:command "t"
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly dwtxt/start-edit-if-selected
|
||||
(dwd/select-for-drawing :text))}
|
||||
|
||||
:draw-path {:tooltip "P"
|
||||
:command "p"
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dwd/select-for-drawing :path))}
|
||||
|
||||
@ -326,52 +372,62 @@
|
||||
|
||||
:draw-curve {:tooltip (ds/shift "C")
|
||||
:command "shift+c"
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dwd/select-for-drawing :curve))}
|
||||
|
||||
:add-comment {:tooltip "C"
|
||||
:command "c"
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(st/emit! (dwd/select-for-drawing :comments))}
|
||||
|
||||
:toggle-comments-visibility
|
||||
{:tooltip (ds/meta-shift "C")
|
||||
:command (ds/c-mod "shift+c")
|
||||
:section [:workspace]
|
||||
:subsections [:main-menu]
|
||||
:fn #(st/emit! (dwcm/toggle-comments-visibility {:origin "workspace-shortcuts"}))}
|
||||
|
||||
:insert-image {:tooltip (ds/shift "K")
|
||||
:command "shift+k"
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(-> "image-upload" dom/get-element dom/click)}
|
||||
|
||||
:toggle-visibility {:tooltip (ds/meta-shift "H")
|
||||
:command (ds/c-mod "shift+h")
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dw/toggle-visibility-selected))}
|
||||
|
||||
:toggle-lock {:tooltip (ds/meta-shift "L")
|
||||
:command (ds/c-mod "shift+l")
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dw/toggle-lock-selected))}
|
||||
|
||||
:toggle-lock-size {:tooltip (ds/shift "L")
|
||||
:command "shift+l"
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (dw/toggle-proportion-lock))}
|
||||
|
||||
:scale {:tooltip "K"
|
||||
:command "k"
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (toggle-layout-flag :scale-text))}
|
||||
|
||||
:open-color-picker {:tooltip "I"
|
||||
:command "i"
|
||||
:section [:workspace]
|
||||
:subsections [:tools]
|
||||
:fn #(emit-when-no-readonly (mdc/picker-for-selected-shape))}
|
||||
|
||||
:toggle-focus-mode {:command "f"
|
||||
:tooltip "F"
|
||||
:section [:workspace :basics]
|
||||
:subsections [:basics :tools]
|
||||
:fn #(st/emit! (dw/toggle-focus-mode))}
|
||||
|
||||
@ -379,41 +435,49 @@
|
||||
|
||||
:align-left {:tooltip (ds/alt "A")
|
||||
:command "alt+a"
|
||||
:section [:workspace]
|
||||
:subsections [:alignment]
|
||||
:fn #(emit-when-no-readonly (dw/align-objects :hleft))}
|
||||
|
||||
:align-right {:tooltip (ds/alt "D")
|
||||
:command "alt+d"
|
||||
:section [:workspace]
|
||||
:subsections [:alignment]
|
||||
:fn #(emit-when-no-readonly (dw/align-objects :hright))}
|
||||
|
||||
:align-top {:tooltip (ds/alt "W")
|
||||
:command "alt+w"
|
||||
:section [:workspace]
|
||||
:subsections [:alignment]
|
||||
:fn #(emit-when-no-readonly (dw/align-objects :vtop))}
|
||||
|
||||
:align-hcenter {:tooltip (ds/alt "H")
|
||||
:command "alt+h"
|
||||
:section [:workspace]
|
||||
:subsections [:alignment]
|
||||
:fn #(emit-when-no-readonly (dw/align-objects :hcenter))}
|
||||
|
||||
:align-vcenter {:tooltip (ds/alt "V")
|
||||
:command "alt+v"
|
||||
:section [:workspace]
|
||||
:subsections [:alignment]
|
||||
:fn #(emit-when-no-readonly (dw/align-objects :vcenter))}
|
||||
|
||||
:align-bottom {:tooltip (ds/alt "S")
|
||||
:command "alt+s"
|
||||
:section [:workspace]
|
||||
:subsections [:alignment]
|
||||
:fn #(emit-when-no-readonly (dw/align-objects :vbottom))}
|
||||
|
||||
:h-distribute {:tooltip (ds/meta-shift (ds/alt "H"))
|
||||
:command (ds/c-mod "shift+alt+h")
|
||||
:section [:workspace]
|
||||
:subsections [:alignment]
|
||||
:fn #(emit-when-no-readonly (dw/distribute-objects :horizontal))}
|
||||
|
||||
:v-distribute {:tooltip (ds/meta-shift (ds/alt "V"))
|
||||
:command (ds/c-mod "shift+alt+v")
|
||||
:section [:workspace]
|
||||
:subsections [:alignment]
|
||||
:fn #(emit-when-no-readonly (dw/distribute-objects :vertical))}
|
||||
|
||||
@ -421,11 +485,13 @@
|
||||
|
||||
:toggle-rulers {:tooltip (ds/meta-shift "R")
|
||||
:command (ds/c-mod "shift+r")
|
||||
:section [:workspace]
|
||||
:subsections [:main-menu]
|
||||
:fn #(st/emit! (toggle-layout-flag :rulers))}
|
||||
|
||||
:select-all {:tooltip (ds/meta "A")
|
||||
:command (ds/c-mod "a")
|
||||
:section [:workspace]
|
||||
:subsections [:main-menu]
|
||||
:fn #(st/emit! (dw/select-all))}
|
||||
|
||||
@ -434,37 +500,44 @@
|
||||
:command [(ds/c-mod "'") (ds/c-mod "219")]
|
||||
:show-command (ds/c-mod "'")
|
||||
:subsections [:main-menu]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (toggle-layout-flag :display-guides))}
|
||||
|
||||
:toggle-alignment {:tooltip (ds/meta "\\")
|
||||
:command (ds/c-mod "\\")
|
||||
:subsections [:main-menu]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (toggle-layout-flag :dynamic-alignment))}
|
||||
|
||||
:thumbnail-set {:tooltip (ds/shift "T")
|
||||
:command "shift+t"
|
||||
:subsections [:main-menu]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dw/toggle-file-thumbnail-selected))}
|
||||
|
||||
:show-pixel-grid {:tooltip (ds/shift ",")
|
||||
:command "shift+,"
|
||||
:subsections [:main-menu]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (toggle-layout-flag :show-pixel-grid))}
|
||||
|
||||
:snap-pixel-grid {:command ","
|
||||
:tooltip ","
|
||||
:subsections [:main-menu]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (toggle-layout-flag :snap-pixel-grid))}
|
||||
|
||||
:export-shapes {:tooltip (ds/meta-shift "E")
|
||||
:command (ds/c-mod "shift+e")
|
||||
:subsections [:basics :main-menu]
|
||||
:section [:workspace :basics]
|
||||
:fn #(st/emit!
|
||||
(de/show-workspace-export-dialog {:origin "workspace:shortcuts"}))}
|
||||
|
||||
:toggle-snap-ruler-guide {:tooltip (ds/meta-shift "G")
|
||||
:command (ds/c-mod "shift+g")
|
||||
:subsections [:main-menu]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (toggle-layout-flag :snap-ruler-guides))}
|
||||
|
||||
:toggle-snap-guides {:tooltip (ds/meta-shift "'")
|
||||
@ -472,11 +545,13 @@
|
||||
:command [(ds/c-mod "shift+'") (ds/c-mod "shift+219")]
|
||||
:show-command (ds/c-mod "shift+'")
|
||||
:subsections [:main-menu]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (toggle-layout-flag :snap-guides))}
|
||||
|
||||
:show-shortcuts {:tooltip "?"
|
||||
:command "?"
|
||||
:subsections [:main-menu]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (toggle-layout-flag :shortcuts))}
|
||||
|
||||
;; PANELS
|
||||
@ -484,22 +559,26 @@
|
||||
:toggle-layers {:tooltip (ds/alt "L")
|
||||
:command (ds/a-mod "l")
|
||||
:subsections [:panels]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dcm/go-to-workspace :layout :layers))}
|
||||
|
||||
:toggle-assets {:tooltip (ds/alt "I")
|
||||
:command (ds/a-mod "i")
|
||||
:subsections [:panels]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dcm/go-to-workspace :layout :assets))}
|
||||
|
||||
:toggle-history {:tooltip (ds/meta-alt "H")
|
||||
:command (ds/ca-mod "h")
|
||||
:subsections [:panels]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly
|
||||
(dw/toggle-layout-flag :document-history))}
|
||||
|
||||
:toggle-colorpalette {:tooltip (ds/alt "P")
|
||||
:command (ds/a-mod "p")
|
||||
:subsections [:panels]
|
||||
:section [:workspace]
|
||||
:fn #(do (r/set-resize-type! :bottom)
|
||||
(emit-when-no-readonly (dw/remove-layout-flag :hide-palettes)
|
||||
(dw/remove-layout-flag :textpalette)
|
||||
@ -508,6 +587,7 @@
|
||||
:toggle-textpalette {:tooltip (ds/alt "T")
|
||||
:command (ds/a-mod "t")
|
||||
:subsections [:panels]
|
||||
:section [:workspace]
|
||||
:fn #(do (r/set-resize-type! :bottom)
|
||||
(emit-when-no-readonly (dw/remove-layout-flag :hide-palettes)
|
||||
(dw/remove-layout-flag :colorpalette)
|
||||
@ -516,6 +596,7 @@
|
||||
:hide-ui {:tooltip "\\"
|
||||
:command "\\"
|
||||
:subsections [:panels :basics]
|
||||
:section [:workspace :basics]
|
||||
:fn #(st/emit! (toggle-layout-flag :hide-ui))}
|
||||
|
||||
;; ZOOM-WORKSPACE
|
||||
@ -523,36 +604,43 @@
|
||||
:increase-zoom {:tooltip "+"
|
||||
:command ["+" "="]
|
||||
:subsections [:zoom-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dw/increase-zoom))}
|
||||
|
||||
:decrease-zoom {:tooltip "-"
|
||||
:command ["-" "_"]
|
||||
:subsections [:zoom-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dw/decrease-zoom))}
|
||||
|
||||
:reset-zoom {:tooltip (ds/shift "0")
|
||||
:command ["shift+0" "shift+num0"]
|
||||
:subsections [:zoom-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! dw/reset-zoom)}
|
||||
|
||||
:fit-all {:tooltip (ds/shift "1")
|
||||
:command ["shift+1" "shift+num1"]
|
||||
:subsections [:zoom-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! dw/zoom-to-fit-all)}
|
||||
|
||||
:zoom-selected {:tooltip (ds/shift "2")
|
||||
:command ["shift+2" "shift+num2" "@" "\""]
|
||||
:subsections [:zoom-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! dw/zoom-to-selected-shape)}
|
||||
|
||||
:zoom-lense-increase {:tooltip "Z"
|
||||
:command "z"
|
||||
:subsections [:zoom-workspace]
|
||||
:section [:workspace]
|
||||
:fn identity}
|
||||
|
||||
:zoom-lense-decrease {:tooltip (ds/alt "Z")
|
||||
:command "alt+z"
|
||||
:subsections [:zoom-workspace]
|
||||
:section [:workspace]
|
||||
:fn identity}
|
||||
|
||||
;; NAVIGATION
|
||||
@ -561,36 +649,43 @@
|
||||
:open-viewer {:tooltip "G V"
|
||||
:command "g v"
|
||||
:subsections [:navigation-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dcm/go-to-viewer))}
|
||||
|
||||
:open-inspect {:tooltip "G I"
|
||||
:command "g i"
|
||||
:subsections [:navigation-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dcm/go-to-viewer :section :inspect))}
|
||||
|
||||
:open-comments {:tooltip "G C"
|
||||
:command "g c"
|
||||
:subsections [:navigation-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dcm/go-to-viewer :section :comments))}
|
||||
|
||||
:open-dashboard {:tooltip "G D"
|
||||
:command "g d"
|
||||
:subsections [:navigation-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dcm/go-to-dashboard-recent))}
|
||||
|
||||
:select-prev {:tooltip (ds/shift "tab")
|
||||
:command "shift+tab"
|
||||
:subsections [:navigation-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dw/select-prev-shape))}
|
||||
|
||||
:select-next {:tooltip ds/tab
|
||||
:command "tab"
|
||||
:subsections [:navigation-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(st/emit! (dw/select-next-shape))}
|
||||
|
||||
:select-parent-layer {:tooltip (ds/shift ds/enter)
|
||||
:command "shift+enter"
|
||||
:subsections [:navigation-workspace]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/select-parent-layer))}
|
||||
;; SHAPE
|
||||
|
||||
@ -598,27 +693,32 @@
|
||||
:bool-union {:tooltip (ds/meta (ds/alt "U"))
|
||||
:command (ds/c-mod "alt+u")
|
||||
:subsections [:shape]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/create-bool :union))}
|
||||
|
||||
:bool-difference {:tooltip (ds/meta (ds/alt "D"))
|
||||
:command (ds/c-mod "alt+d")
|
||||
:subsections [:shape]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/create-bool :difference))}
|
||||
|
||||
:bool-intersection {:tooltip (ds/meta (ds/alt "I"))
|
||||
:command (ds/c-mod "alt+i")
|
||||
:subsections [:shape]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/create-bool :intersection))}
|
||||
|
||||
:bool-exclude {:tooltip (ds/meta (ds/alt "E"))
|
||||
:command (ds/c-mod "alt+e")
|
||||
:subsections [:shape]
|
||||
:section [:workspace]
|
||||
:fn #(emit-when-no-readonly (dw/create-bool :exclude))}
|
||||
|
||||
;; THEME
|
||||
:toggle-theme {:tooltip (ds/alt "M")
|
||||
:command (ds/a-mod "m")
|
||||
:subsections [:basics]
|
||||
:subsections [:generic :basics]
|
||||
:section [:workspace :basics]
|
||||
:fn #(st/emit! (with-meta (du/toggle-theme)
|
||||
{::ev/origin "workspace:shortcut"}))}
|
||||
|
||||
@ -626,7 +726,8 @@
|
||||
;; PLUGINS
|
||||
:plugins {:tooltip (ds/meta (ds/alt "P"))
|
||||
:command (ds/c-mod "alt+p")
|
||||
:subsections [:basics]
|
||||
:subsections [:generic :basics]
|
||||
:section [:workspace :basics]
|
||||
:fn #(when (features/active-feature? @st/state "plugins/runtime")
|
||||
(st/emit!
|
||||
(ev/event {::ev/name "open-plugins-manager" ::ev/origin "workspace:shortcuts"})
|
||||
@ -644,6 +745,7 @@
|
||||
(map (fn [n] [(keyword (str "opacity-" n))
|
||||
{:tooltip (str n)
|
||||
:command [(str n) (str "num" n)]
|
||||
:section [:workspace]
|
||||
:subsections [:modify-layers]
|
||||
:fn #(emit-when-no-readonly (dwly/pressed-opacity n))}])))))
|
||||
|
||||
@ -652,6 +754,24 @@
|
||||
*assert*
|
||||
(merge debug-shortcuts)))
|
||||
|
||||
(defn get-tooltip [shortcut]
|
||||
(defn get-tooltip
|
||||
"Returns the tooltip string for a shortcut, using any custom binding
|
||||
from the user's profile props if one exists, falling back to the
|
||||
default :tooltip field."
|
||||
[shortcut]
|
||||
(assert (contains? shortcuts shortcut) (str shortcut))
|
||||
(get-in shortcuts [shortcut :tooltip]))
|
||||
(let [custom-shortcuts (deref refs/custom-shortcuts)
|
||||
custom-command (get-in custom-shortcuts [:workspace shortcut])]
|
||||
(if (and custom-command (not= custom-command ""))
|
||||
(ds/command->tooltip custom-command)
|
||||
(get-in shortcuts [shortcut :tooltip]))))
|
||||
|
||||
(defn get-effective-tooltip
|
||||
"Returns the tooltip string for a shortcut given an already-resolved
|
||||
custom-shortcuts map. Use this when you already have the custom
|
||||
shortcuts derefed for the current render cycle."
|
||||
[shortcut custom-shortcuts]
|
||||
(let [custom-command (get-in custom-shortcuts [:workspace shortcut])]
|
||||
(if (and custom-command (not= custom-command ""))
|
||||
(ds/command->tooltip custom-command)
|
||||
(get-in shortcuts [shortcut :tooltip]))))
|
||||
|
||||
@ -237,31 +237,37 @@
|
||||
{:underline {:tooltip (ds/meta "U")
|
||||
:command (ds/c-mod "u")
|
||||
:subsections [:text-editor]
|
||||
:section [:workspace]
|
||||
:fn #(update-attrs-when-no-readonly {:text-decoration "toggle-underline"})}
|
||||
|
||||
:line-through {:tooltip (ds/alt (ds/meta-shift "5"))
|
||||
:command "alt+shift+5"
|
||||
:subsections [:text-editor]
|
||||
:section [:workspace]
|
||||
:fn #(update-attrs-when-no-readonly {:text-decoration "toggle-line-through"})}
|
||||
|
||||
:font-size-inc {:tooltip (ds/meta-shift ">")
|
||||
:command (ds/c-mod "shift+.")
|
||||
:subsections [:text-editor]
|
||||
:section [:workspace]
|
||||
:fn #(update-attrs-when-no-readonly {:font-size-inc true})}
|
||||
|
||||
:font-size-dec {:tooltip (ds/meta-shift "<")
|
||||
:command (ds/c-mod "shift+,")
|
||||
:subsections [:text-editor]
|
||||
:section [:workspace]
|
||||
:fn #(update-attrs-when-no-readonly {:font-size-dec true})}
|
||||
|
||||
:bold {:tooltip (ds/meta "b")
|
||||
:command (ds/c-mod "b")
|
||||
:subsections [:text-editor]
|
||||
:section [:workspace]
|
||||
:fn #(update-attrs-when-no-readonly {:font-variant-id "toggle-bold"})}
|
||||
|
||||
:italic {:tooltip (ds/meta "i")
|
||||
:command (ds/c-mod "i")
|
||||
:subsections [:text-editor]
|
||||
:section [:workspace]
|
||||
:fn #(update-attrs-when-no-readonly {:font-variant-id "toggle-italic"})}})
|
||||
|
||||
|
||||
|
||||
@ -32,6 +32,9 @@
|
||||
(def profile
|
||||
(l/derived (l/key :profile) st/state))
|
||||
|
||||
(def custom-shortcuts
|
||||
(l/derived (fn [state] (get-in state [:profile :props :custom-shortcuts])) st/state))
|
||||
|
||||
(def current-page-id
|
||||
(l/derived (l/key :current-page-id) st/state))
|
||||
|
||||
|
||||
@ -203,7 +203,8 @@
|
||||
:settings-feedback
|
||||
:settings-subscription
|
||||
:settings-integrations
|
||||
:settings-notifications)
|
||||
:settings-notifications
|
||||
:settings-shortcuts)
|
||||
(let [params (get params :query)
|
||||
error-report-id (some-> params :error-report-id uuid/parse*)]
|
||||
[:? [:> settings-page*
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
display: flex;
|
||||
gap: deprecated.$s-2;
|
||||
height: deprecated.$s-32;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
border-radius: deprecated.$br-8;
|
||||
background-color: var(--search-bar-background-color);
|
||||
}
|
||||
|
||||
@ -33,6 +33,8 @@
|
||||
(def is-render? (mf/create-context false))
|
||||
(def is-component? (mf/create-context false))
|
||||
|
||||
(def shortcuts-ctx (mf/create-context nil))
|
||||
|
||||
(def sidebar
|
||||
"A context that intends to store the current sidebar position,
|
||||
useful for components that behaves distinctly if they are showed in
|
||||
|
||||
@ -305,7 +305,7 @@
|
||||
(filter :is-default)
|
||||
(first)))]
|
||||
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts-dashboard)
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts-dashboard :dashboard)
|
||||
|
||||
(mf/with-effect [team-id]
|
||||
(st/emit! (dd/initialize team-id))
|
||||
|
||||
@ -195,7 +195,7 @@
|
||||
(st/emit! (dpj/fetch-files project-id)
|
||||
(dd/clear-selected-files)))
|
||||
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries)
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries :dashboard)
|
||||
|
||||
[:*
|
||||
[:> header* {:team team
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
(st/emit! (dtm/fetch-shared-files team-id)
|
||||
(dd/clear-selected-files)))
|
||||
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries)
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries :dashboard)
|
||||
|
||||
[:*
|
||||
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
|
||||
|
||||
@ -369,7 +369,7 @@
|
||||
(st/emit! (dd/fetch-recent-files team-id)
|
||||
(dd/clear-selected-files)))
|
||||
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts-projects)
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts-projects :dashboard)
|
||||
|
||||
(when (seq projects)
|
||||
[:*
|
||||
|
||||
@ -42,4 +42,4 @@
|
||||
(on-ref node)))})]
|
||||
[:> element props
|
||||
(when icon [:> icon* {:icon-id icon :size "m"}])
|
||||
[:span {:class (stl/css :label-wrapper)} children]]))
|
||||
children]))
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
(def ^:private schema:empty-state
|
||||
[:map
|
||||
[:class {:optional true} :string]
|
||||
[:icon [:and :string [:fn #(contains? icon-list %)]]]
|
||||
[:icon {:optional true} [:and :string [:fn #(contains? icon-list %)]]]
|
||||
[:text :string]])
|
||||
|
||||
(mf/defc empty-state*
|
||||
@ -22,8 +22,9 @@
|
||||
[{:keys [class icon text] :rest props}]
|
||||
(let [props (mf/spread-props props {:class [class (stl/css :group)]})]
|
||||
[:> :div props
|
||||
[:div {:class (stl/css :icon-wrapper)}
|
||||
[:> icon* {:icon-id icon
|
||||
:size "l"
|
||||
:class (stl/css :icon)}]]
|
||||
(when icon
|
||||
[:div {:class (stl/css :icon-wrapper)}
|
||||
[:> icon* {:icon-id icon
|
||||
:size "l"
|
||||
:class (stl/css :icon)}]])
|
||||
[:div {:class (stl/css :text)} text]]))
|
||||
|
||||
@ -42,13 +42,14 @@
|
||||
state))
|
||||
|
||||
(defn use-shortcuts
|
||||
[key shortcuts]
|
||||
(mf/use-effect
|
||||
#js [(str key) shortcuts]
|
||||
(fn []
|
||||
(st/emit! (dsc/push-shortcuts key shortcuts))
|
||||
[key shortcuts group-key]
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)]
|
||||
(mf/use-effect
|
||||
#js [(str key) shortcuts custom-shortcuts]
|
||||
(fn []
|
||||
(st/emit! (dsc/pop-shortcuts key))))))
|
||||
(st/emit! (dsc/push-shortcuts key shortcuts group-key))
|
||||
(fn []
|
||||
(st/emit! (dsc/pop-shortcuts key)))))))
|
||||
|
||||
(defn- set-timer
|
||||
[state ms func]
|
||||
|
||||
@ -41,7 +41,8 @@
|
||||
["/options" :settings-options]
|
||||
["/subscriptions" :settings-subscription]
|
||||
["/integrations" :settings-integrations]
|
||||
["/notifications" :settings-notifications]]
|
||||
["/notifications" :settings-notifications]
|
||||
["/shortcuts" :settings-shortcuts]]
|
||||
|
||||
["/frame-preview" :frame-preview]
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
[app.main.ui.settings.options :refer [options-page*]]
|
||||
[app.main.ui.settings.password :refer [password-page*]]
|
||||
[app.main.ui.settings.profile :refer [profile-page*]]
|
||||
[app.main.ui.settings.shortcuts :refer [shortcuts-page*]]
|
||||
[app.main.ui.settings.sidebar :refer [sidebar*]]
|
||||
[app.main.ui.settings.subscription :refer [subscription-page*]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@ -38,7 +39,7 @@
|
||||
(let [section (get-in route [:data :name])
|
||||
profile (mf/deref refs/profile)]
|
||||
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts)
|
||||
(hooks/use-shortcuts ::dashboard sc/shortcuts :dashboard)
|
||||
|
||||
(mf/with-effect [profile]
|
||||
(when (nil? profile)
|
||||
@ -54,7 +55,7 @@
|
||||
|
||||
[:div {:class (stl/css :dashboard-content)}
|
||||
[:> header*]
|
||||
[:section {:class (stl/css :dashboard-container)}
|
||||
[:div {:class (stl/css :dashboard-container)}
|
||||
(case section
|
||||
:settings-profile
|
||||
[:> profile-page*]
|
||||
@ -77,7 +78,10 @@
|
||||
[:> integrations-page*]
|
||||
|
||||
:settings-notifications
|
||||
[:> notifications-page* {:profile profile}])]]]]))
|
||||
[:> notifications-page* {:profile profile}]
|
||||
|
||||
:settings-shortcuts
|
||||
[:> shortcuts-page* {:profile profile}])]]]]))
|
||||
|
||||
(mf/defc settings-page*
|
||||
{::mf/lazy-load true}
|
||||
|
||||
@ -98,7 +98,8 @@
|
||||
:form form}
|
||||
|
||||
;; --- Feedback section
|
||||
[:h2 {:class (stl/css :field-title :feedback-title)} (tr "feedback.title-contact-us")]
|
||||
[:h2 {:class (stl/css :field-title :feedback-title)
|
||||
:id "feedback-section-title"} (tr "feedback.title-contact-us")]
|
||||
[:p {:class (stl/css :field-text :feedback-title)} (tr "feedback.subtitle")]
|
||||
|
||||
[:div {:class (stl/css :fields-row)}
|
||||
@ -164,6 +165,7 @@
|
||||
(:content errors/last-report))
|
||||
props (mf/spread-props props {:error-report report})]
|
||||
|
||||
[:div {:class (stl/css :dashboard-settings)}
|
||||
[:section {:class (stl/css :dashboard-settings)
|
||||
:aria-labelledby "feedback-section-title"}
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:> feedback-form* props]]]))
|
||||
|
||||
@ -640,10 +640,12 @@
|
||||
(dom/set-html-title (tr "title.settings.integrations"))
|
||||
(st/emit! (du/fetch-access-tokens)))
|
||||
|
||||
[:div {:class (stl/css :integrations)}
|
||||
[:> heading* {:level 1
|
||||
[:section {:class (stl/css :integrations)
|
||||
:aria-labelledby "integrations-section-title"}
|
||||
[:> heading* {:level 2
|
||||
:typography t/title-large
|
||||
:class (stl/css :color-primary)}
|
||||
:class (stl/css :color-primary)
|
||||
:id "integrations-section-title"}
|
||||
(tr "integrations.title")]
|
||||
|
||||
(when ^boolean mcp-enabled?
|
||||
|
||||
@ -122,10 +122,15 @@
|
||||
(mf/use-effect
|
||||
#(dom/set-html-title (tr "title.settings.options")))
|
||||
|
||||
[:div {:class (stl/css :dashboard-settings)}
|
||||
[:section {:class (stl/css :dashboard-settings)
|
||||
:aria-labelledby "options-section-title"}
|
||||
[:*
|
||||
[:div {:class (stl/css :form-container) :data-testid "settings-form"}
|
||||
[:h2 (tr "labels.settings")]
|
||||
[:> heading* {:level 2
|
||||
:typography t/title-large
|
||||
:class (stl/css :color-primary)
|
||||
:id "options-section-title"}
|
||||
(tr "labels.settings")]
|
||||
[:> options-form*]]
|
||||
(when (contains? cf/flags :render-switch)
|
||||
[:> webgl-settings* {:renderer renderer}])]]))
|
||||
|
||||
@ -106,7 +106,8 @@
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "title.settings.password")))
|
||||
|
||||
[:section {:class (stl/css :dashboard-settings)}
|
||||
[:section {:class (stl/css :dashboard-settings)
|
||||
:aria-labelledby "password-section-title"}
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:h2 (tr "dashboard.password-change")]
|
||||
[:h2 {:id "password-section-title"} (tr "dashboard.password-change")]
|
||||
[:> password-form*]]])
|
||||
|
||||
@ -143,8 +143,9 @@
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "title.settings.profile")))
|
||||
|
||||
[:div {:class (stl/css :dashboard-settings)}
|
||||
[:section {:class (stl/css :dashboard-settings)
|
||||
:aria-labelledby "profile-section-title"}
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:h2 (tr "labels.profile")]
|
||||
[:h2 {:id "profile-section-title"} (tr "labels.profile")]
|
||||
[:> profile-photo-form*]
|
||||
[:> profile-form*]]])
|
||||
|
||||
166
frontend/src/app/main/ui/settings/restore_shortcuts_modal.cljs
Normal file
166
frontend/src/app/main/ui/settings/restore_shortcuts_modal.cljs
Normal file
@ -0,0 +1,166 @@
|
||||
(ns app.main.ui.settings.restore-shortcuts-modal
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.i18n :refer [tr]]
|
||||
[app.main.data.dashboard.shortcuts :as dsc]
|
||||
[app.main.data.dashboard.shortcuts.customize :as customize]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.viewer.shortcuts :as vsc]
|
||||
[app.main.data.workspace.path.shortcuts :as psc]
|
||||
[app.main.data.workspace.shortcuts :as wsc]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.shortcuts :as ss]
|
||||
[app.util.dom :as dom]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private workspace-shortcuts-raw
|
||||
(d/deep-merge psc/shortcuts
|
||||
wsc/shortcuts))
|
||||
|
||||
(def ^:private context->defaults
|
||||
{:workspace workspace-shortcuts-raw
|
||||
:dashboard dsc/shortcuts
|
||||
:viewer vsc/shortcuts})
|
||||
|
||||
(defn extract-shortcut-keys [shortcut-key custom-shortcuts context]
|
||||
(let [defaults (get context->defaults context)
|
||||
default-command (:command (get defaults shortcut-key))
|
||||
|
||||
default-managed-list (if (coll? default-command)
|
||||
default-command
|
||||
(conj () default-command))
|
||||
|
||||
default-chars-list (map ds/split-sc default-managed-list)
|
||||
|
||||
default-last-element (last default-chars-list)
|
||||
|
||||
default-short-char-list (if (= 1 (count default-chars-list))
|
||||
default-chars-list
|
||||
(drop-last default-chars-list))
|
||||
|
||||
ctx-customs (get custom-shortcuts context {})
|
||||
current-command (or (get ctx-customs shortcut-key) default-command)
|
||||
|
||||
current-managed-list (if (coll? current-command)
|
||||
current-command
|
||||
(conj () current-command))
|
||||
|
||||
current-chars-list (map ds/split-sc current-managed-list)
|
||||
|
||||
current-last-element (last current-chars-list)
|
||||
|
||||
current-short-char-list (if (= 1 (count current-chars-list))
|
||||
current-chars-list
|
||||
(drop-last current-chars-list))]
|
||||
[default-last-element default-short-char-list current-last-element current-command current-short-char-list]))
|
||||
|
||||
(mf/defc restore-all-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :restore-all-modal}
|
||||
[{:keys [custom-shortcuts]}]
|
||||
(let [handle-close-dialog (mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
handle-accept-dialog (mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (customize/reset-all-custom-shortcuts))
|
||||
(st/emit! (modal/hide))))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog)}
|
||||
[:> icon-button* {:class (stl/css :close-btn)
|
||||
:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click handle-close-dialog
|
||||
:tooltip-class (stl/css :close-btn-tooltip)
|
||||
:icon i/close}]
|
||||
[:div {:class (stl/css :modal-title)}
|
||||
(tr "restore-shortcuts.modal-title")]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-content-text)}
|
||||
(tr "restore-shortcuts.modal-text")]
|
||||
|
||||
[:table {:class (stl/css :shortcuts-table)}
|
||||
[:thead
|
||||
[:tr {:class (stl/css :shortcuts-list-header)}
|
||||
[:th {:class (stl/css :shortcut-header-name)}
|
||||
(tr "restore-shortcuts.acction")]
|
||||
[:th {:class (stl/css :shortcut-header-command)}
|
||||
(tr "labels.current")]
|
||||
[:th {:class (stl/css :shortcut-header-command)}
|
||||
(tr "labels.default")]]]
|
||||
[:tbody {:class (stl/css :shortcuts-list-body)}
|
||||
(for [context [:workspace :dashboard :viewer]
|
||||
shortcut-key (keys (get custom-shortcuts context {}))]
|
||||
(let [[default-last-element
|
||||
default-short-char-list
|
||||
current-last-element
|
||||
current-command
|
||||
current-short-char-list] (extract-shortcut-keys shortcut-key custom-shortcuts context)
|
||||
current-penultimate (last current-short-char-list)
|
||||
default-penultimate (last default-short-char-list)]
|
||||
[:tr {:key (dm/str (name context) "-" (name shortcut-key))
|
||||
:class (stl/css :shortcuts-list-item)}
|
||||
[:td {:class (stl/css :shortcut-name)}
|
||||
(ss/translation-keyname :sc shortcut-key)]
|
||||
[:td {:class (stl/css :shortcut-command)}
|
||||
(if (str/blank? current-command)
|
||||
[:span {:class (stl/css :shortcut-empty)} "-"]
|
||||
(for [chars current-short-char-list]
|
||||
[:* {:key (str/join chars)}
|
||||
(for [char chars]
|
||||
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
|
||||
:char char
|
||||
:class (stl/css :default-command)
|
||||
:command shortcut-key}])
|
||||
(when (not= chars current-penultimate) [:span {:class (stl/css :space)} ","])]))
|
||||
(when (not= current-last-element current-penultimate)
|
||||
[:*
|
||||
[:span {:class (stl/css :space)} (tr "shortcuts.or")]
|
||||
(for [char current-last-element]
|
||||
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
|
||||
:char char
|
||||
:class (stl/css :default-command)
|
||||
:command shortcut-key}])])]
|
||||
|
||||
[:td {:class (stl/css :shortcut-command)}
|
||||
(for [chars default-short-char-list]
|
||||
[:* {:key (str/join chars)}
|
||||
(for [char chars]
|
||||
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
|
||||
:class (stl/css :default-command)
|
||||
:char char
|
||||
:command shortcut-key}])
|
||||
(when (not= chars default-penultimate) [:span {:class (stl/css :space)} ","])])
|
||||
(when (not= default-last-element default-penultimate)
|
||||
[:*
|
||||
[:span {:class (stl/css :space)} (tr "shortcuts.or")]
|
||||
(for [char default-last-element]
|
||||
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
|
||||
:class (stl/css :default-command)
|
||||
:char char
|
||||
:command shortcut-key}])])]]))]]]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:> button* {:class (stl/css :cancel-button)
|
||||
:variant "secondary"
|
||||
:type "button"
|
||||
:on-click modal/hide!}
|
||||
(tr "labels.cancel")]
|
||||
[:> button* {:class (stl/css :cancel-button)
|
||||
:type "button"
|
||||
:variant "primary"
|
||||
:on-click handle-accept-dialog}
|
||||
(tr "restore-shortcuts.restore")]]]]]))
|
||||
147
frontend/src/app/main/ui/settings/restore_shortcuts_modal.scss
Normal file
147
frontend/src/app/main/ui/settings/restore_shortcuts_modal.scss
Normal file
@ -0,0 +1,147 @@
|
||||
// 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 Sucursal en España SL
|
||||
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/spacing.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.modal-overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: 0;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
z-index: var(--z-index-set);
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
position: relative;
|
||||
padding: var(--sp-xxxl);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-primary);
|
||||
border: $b-2 solid var(--color-background-quaternary);
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: var(--sp-xxxl);
|
||||
min-inline-size: $sz-364;
|
||||
min-block-size: $sz-192;
|
||||
max-block-size: $sz-520;
|
||||
inline-size: $sz-500;
|
||||
max-inline-size: $sz-712;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include t.use-typography("headline-medium");
|
||||
|
||||
color: var(--color-foreground-primary);
|
||||
block-size: px2rem(26);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
inset-block-start: px2rem(8);
|
||||
inset-inline-end: px2rem(6);
|
||||
block-size: $sz-32;
|
||||
inline-size: $sz-28;
|
||||
}
|
||||
|
||||
.close-btn-tooltip {
|
||||
position: absolute;
|
||||
inset-block-start: px2rem(8);
|
||||
inset-inline-end: px2rem(6);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
block-size: fit-content;
|
||||
}
|
||||
|
||||
.modal-content-text {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
justify-self: end;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.shortcuts-table {
|
||||
inline-size: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.shortcuts-list-item,
|
||||
.shortcuts-list-header {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
display: grid;
|
||||
grid-template-columns: 40% 30% 30%;
|
||||
}
|
||||
|
||||
.shortcuts-list-header {
|
||||
border-block-end: $b-1 solid var(--color-foreground-secondary);
|
||||
block-size: $sz-32;
|
||||
text-align: start;
|
||||
padding-block-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.shortcuts-list-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.shortcuts-list-item {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.shortcuts-list-item:first-child {
|
||||
padding-block-start: var(--sp-s);
|
||||
}
|
||||
|
||||
.shortcut-header-name,
|
||||
.shortcut-header-command {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.shortcut-command {
|
||||
display: flex;
|
||||
gap: var(--sp-s);
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
text-align: start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.default-command {
|
||||
background-color: var(--color-background-tertiary);
|
||||
color: var(--color-foreground-secondary);
|
||||
border-radius: $br-6;
|
||||
}
|
||||
|
||||
.shortcut-empty {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
479
frontend/src/app/main/ui/settings/shortcuts.cljs
Normal file
479
frontend/src/app/main/ui/settings/shortcuts.cljs
Normal file
@ -0,0 +1,479 @@
|
||||
(ns app.main.ui.settings.shortcuts
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.i18n :refer [tr]]
|
||||
[app.common.json :as json]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.data.dashboard.shortcuts :as dsc]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.viewer.shortcuts :as vsc]
|
||||
[app.main.data.workspace.path.shortcuts :as psc]
|
||||
[app.main.data.workspace.shortcuts :as wsc]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown-menu :refer [dropdown-menu*
|
||||
dropdown-menu-item*]]
|
||||
[app.main.ui.components.search-bar :refer [search-bar*]]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
|
||||
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
|
||||
[app.main.ui.settings.restore-shortcuts-modal]
|
||||
[app.main.ui.shortcuts :as ss]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.strings :refer [matches-search]]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[malli.error :as me]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc search-section*
|
||||
[{:keys [filter-term on-search-term-change on-search-clear-click on-restore-all show-restore-all? has-custom-shortcuts]}]
|
||||
[:div {:class (stl/css :shortcuts-search-section)}
|
||||
[:> search-bar* {:on-change on-search-term-change
|
||||
:on-clear on-search-clear-click
|
||||
:value filter-term
|
||||
:placeholder (tr "shortcuts.title")
|
||||
:icon-id i/search
|
||||
:auto-focus true}]
|
||||
(when (and show-restore-all? has-custom-shortcuts)
|
||||
[:> button*
|
||||
{:variant "secondary"
|
||||
:on-click on-restore-all
|
||||
:class (stl/css :restore-all-button)
|
||||
:icon i/reload}
|
||||
(tr "dashboard.restore-all-deleted-button")])])
|
||||
|
||||
(defn- filter-shortcuts-tree
|
||||
[tree shortcut-filter search-term]
|
||||
(let [sorted-comp (when (sorted? tree)
|
||||
#(compare (name %1) (name %2)))
|
||||
result (keep (fn [[k node]]
|
||||
(let [children (:children node)]
|
||||
(cond
|
||||
(map? children)
|
||||
(let [filtered-children
|
||||
(filter-shortcuts-tree children shortcut-filter search-term)]
|
||||
(when (seq filtered-children)
|
||||
[k (assoc node :children filtered-children)]))
|
||||
|
||||
(shortcut-filter k node search-term)
|
||||
[k node])))
|
||||
tree)]
|
||||
(if sorted-comp
|
||||
(into (sorted-map-by sorted-comp) result)
|
||||
(into {} result))))
|
||||
|
||||
(defn- collect-open-section-ids
|
||||
[tree]
|
||||
(letfn [(walk [nodes]
|
||||
(mapcat
|
||||
(fn [node]
|
||||
(let [children (:children node)]
|
||||
(concat
|
||||
(when (:children node)
|
||||
[(:id node)])
|
||||
(when (map? children)
|
||||
(walk (vals children))))))
|
||||
nodes))]
|
||||
(->> (walk (vals tree))
|
||||
(remove nil?)
|
||||
set)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Imported shortcuts JSON validation
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private known-shortcut-keys
|
||||
"Known shortcut keys per context, derived from the default shortcuts maps."
|
||||
{:workspace (set (keys wsc/shortcuts))
|
||||
:dashboard (set (keys dsc/shortcuts))
|
||||
:viewer (set (keys vsc/shortcuts))})
|
||||
|
||||
(def ^:private schema:imported-shortcuts
|
||||
"Malli schema for an imported custom-shortcuts payload.
|
||||
|
||||
The expected shape is a map of shortcut contexts to maps of shortcut-key -> command-string.
|
||||
Only keys that exist in the default shortcuts for each context are accepted."
|
||||
(letfn [(context-schema [ctx]
|
||||
(into [:map {:closed true}]
|
||||
(map (fn [k] [k {:optional true} :string]))
|
||||
(get known-shortcut-keys ctx)))]
|
||||
[:map {:closed true}
|
||||
[:workspace {:optional true} (context-schema :workspace)]
|
||||
[:dashboard {:optional true} (context-schema :dashboard)]
|
||||
[:viewer {:optional true} (context-schema :viewer)]]))
|
||||
|
||||
(defn- flatten-humanize
|
||||
"Turns Malli's humanized explain output into a flat list of {:path :message} maps."
|
||||
([human] (flatten-humanize [] human))
|
||||
([prefix human]
|
||||
(cond
|
||||
(or (string? human) (symbol? human) (keyword? human))
|
||||
[{:path (str/join "/" (map #(if (keyword? %) (name %) (str %)) prefix))
|
||||
:message (str human)}]
|
||||
|
||||
(map? human)
|
||||
(mapcat (fn [[k v]] (flatten-humanize (conj prefix k) v)) human)
|
||||
|
||||
(sequential? human)
|
||||
(mapcat #(flatten-humanize prefix %) human)
|
||||
|
||||
:else [])))
|
||||
|
||||
(defn validate-imported-shortcuts
|
||||
"Validates an imported custom-shortcuts JSON payload.
|
||||
|
||||
Returns {:valid? true} when the payload conforms to the expected shape, or
|
||||
{:valid? false :errors [{:path \"...\" :message \"...\"} ...]} otherwise.
|
||||
Never throws for validation errors."
|
||||
[data]
|
||||
(try
|
||||
(if-not (map? data)
|
||||
{:valid? false
|
||||
:errors [{:path ""
|
||||
:message "Expected a map of shortcut contexts"}]}
|
||||
(let [explain (sm/explain schema:imported-shortcuts data)]
|
||||
(if (nil? explain)
|
||||
{:valid? true}
|
||||
{:valid? false
|
||||
:errors (-> explain me/humanize flatten-humanize vec)})))
|
||||
(catch :default e
|
||||
{:valid? false
|
||||
:errors [{:path ""
|
||||
:message (str "Validation failed: " (.-message e))}]})))
|
||||
|
||||
(mf/defc shortcuts-list*
|
||||
[{:keys [shortcuts open-sections filter-term custom-shortcuts editable? manage-sections conflicts]}]
|
||||
(let [sections (mf/with-memo [shortcuts]
|
||||
(keep (fn [section]
|
||||
(when (seq (get-in section [1 :children]))
|
||||
section))
|
||||
shortcuts))]
|
||||
[:div {:class (stl/css :shortcuts-list)
|
||||
:aria-label (tr "shortcuts.title")}
|
||||
(for [section sections]
|
||||
(let [[section-key _] section]
|
||||
[:> ss/shortcut-section* {:key (name section-key)
|
||||
:section section
|
||||
:manage-sections manage-sections
|
||||
:open-sections open-sections
|
||||
:filter-term filter-term
|
||||
:editable? editable?
|
||||
:custom-shortcuts custom-shortcuts
|
||||
:conflicts conflicts}]))]))
|
||||
|
||||
(mf/defc shortcuts-tab-section*
|
||||
[{:keys [shortcut-filter show-restore-all? on-restore-all empty-str custom-shortcuts conflicts]}]
|
||||
(let [{:keys [all-shortcuts]} (mf/use-ctx ctx/shortcuts-ctx)
|
||||
open-sections* (mf/use-state [[:workspace]])
|
||||
open-sections (deref open-sections*)
|
||||
|
||||
filter-term* (mf/use-state "")
|
||||
filter-term (deref filter-term*)
|
||||
|
||||
section-has-content?
|
||||
(fn [section]
|
||||
(let [children (:children section)]
|
||||
(if (and (= (count children) 1) (contains? children :none))
|
||||
(seq (:children (:none children)))
|
||||
(seq children))))
|
||||
|
||||
all-shortcuts (into {} (filter (fn [[_ v]] (section-has-content? v)) all-shortcuts))
|
||||
filtered-shortcuts (filter-shortcuts-tree all-shortcuts shortcut-filter filter-term)
|
||||
|
||||
on-search-term-change
|
||||
(mf/use-fn
|
||||
(mf/deps all-shortcuts shortcut-filter)
|
||||
(fn [term]
|
||||
(reset! filter-term* term)
|
||||
(if (str/blank? term)
|
||||
(reset! open-sections* [[:workspace]])
|
||||
(let [filtered-tree (filter-shortcuts-tree all-shortcuts shortcut-filter term)
|
||||
open-ids (collect-open-section-ids filtered-tree)]
|
||||
(reset! open-sections* open-ids)))))
|
||||
|
||||
on-search-clear-click
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(reset! open-sections* [[:workspace]])
|
||||
(reset! filter-term* "")))
|
||||
|
||||
manage-sections
|
||||
(fn [item]
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [is-present? (some #(= % item) open-sections)
|
||||
new-value (if is-present?
|
||||
(filterv (fn [element] (not= element item)) open-sections)
|
||||
(conj open-sections item))]
|
||||
(reset! open-sections* new-value))))]
|
||||
|
||||
[:div {:class (stl/css :shortcuts-section)}
|
||||
[:> search-section* {:filter-term filter-term
|
||||
:on-search-term-change on-search-term-change
|
||||
:on-search-clear-click on-search-clear-click
|
||||
:on-restore-all on-restore-all
|
||||
:show-restore-all? show-restore-all?
|
||||
:has-custom-shortcuts (some #(seq (val %)) custom-shortcuts)}]
|
||||
(if (seq filtered-shortcuts)
|
||||
[:> shortcuts-list* {:shortcuts filtered-shortcuts
|
||||
:open-sections open-sections
|
||||
:filter-term filter-term*
|
||||
:custom-shortcuts custom-shortcuts
|
||||
:editable? true
|
||||
:manage-sections manage-sections
|
||||
:conflicts conflicts}]
|
||||
|
||||
[:> empty-state* {:text empty-str
|
||||
:class (stl/css :shortcuts-empty-state)}])]))
|
||||
|
||||
(mf/defc shortcuts-page*
|
||||
[{:keys [profile]}]
|
||||
(let [section* (mf/use-state :all)
|
||||
section (deref section*)
|
||||
|
||||
show-menu* (mf/use-state false)
|
||||
show-menu? (deref show-menu*)
|
||||
|
||||
open-menu
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show-menu* true)))
|
||||
|
||||
close-menu
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show-menu* false)))
|
||||
|
||||
input-ref (mf/use-ref nil)
|
||||
|
||||
tabs
|
||||
(mf/with-memo []
|
||||
[{:label (tr "labels.all")
|
||||
:id "all"}
|
||||
{:label (tr "shortcuts.personalized")
|
||||
:data-testid "personalized"
|
||||
:id "personalized"}
|
||||
{:label (tr "shortcuts.disabled")
|
||||
:data-testid "disabled"
|
||||
:id "disabled"}])
|
||||
|
||||
handle-change-tab
|
||||
(mf/use-fn
|
||||
(fn [new-section]
|
||||
(reset! section* (keyword new-section))))
|
||||
|
||||
custom-shortcuts (get-in profile [:props :custom-shortcuts])
|
||||
has-custom-shortcuts (some #(seq (val %)) custom-shortcuts)
|
||||
path-shortcuts-custom (ds/apply-custom-overrides
|
||||
psc/shortcuts
|
||||
custom-shortcuts
|
||||
:workspace)
|
||||
path-shortcuts-with-translation (->> path-shortcuts-custom
|
||||
(ss/add-translation :sc)
|
||||
(into {}))
|
||||
workspace-shortcuts-custom (ds/apply-custom-overrides
|
||||
wsc/shortcuts
|
||||
custom-shortcuts
|
||||
:workspace)
|
||||
workspace-shortcuts-with-translation (->> workspace-shortcuts-custom
|
||||
(ss/add-translation :sc)
|
||||
(into {}))
|
||||
dashboard-shortcuts-custom (ds/apply-custom-overrides
|
||||
dsc/shortcuts
|
||||
custom-shortcuts :dashboard)
|
||||
dashboard-shortcuts-with-translation (->> dashboard-shortcuts-custom
|
||||
(ss/add-translation :sc)
|
||||
(into {}))
|
||||
viewer-shortcuts-custom (ds/apply-custom-overrides
|
||||
vsc/shortcuts
|
||||
custom-shortcuts
|
||||
:viewer)
|
||||
|
||||
viewer-shortcuts-with-translation (->> viewer-shortcuts-custom
|
||||
(ss/add-translation :sc)
|
||||
(into {}))
|
||||
|
||||
all-shortcuts-raw (d/deep-merge
|
||||
workspace-shortcuts-custom
|
||||
path-shortcuts-custom
|
||||
dashboard-shortcuts-custom
|
||||
viewer-shortcuts-custom)
|
||||
|
||||
{:keys [all-shortcuts]}
|
||||
(ss/build-all-shortcuts-without-basics
|
||||
workspace-shortcuts-with-translation
|
||||
path-shortcuts-with-translation
|
||||
dashboard-shortcuts-with-translation
|
||||
viewer-shortcuts-with-translation
|
||||
:workspace-orig wsc/shortcuts
|
||||
:path-orig psc/shortcuts
|
||||
:dashboard-orig dsc/shortcuts
|
||||
:viewer-orig vsc/shortcuts)
|
||||
|
||||
shortcuts-ctx-value
|
||||
{:workspace-sc-trans (d/deep-merge workspace-shortcuts-with-translation path-shortcuts-with-translation)
|
||||
:dashboard-sc-trans dashboard-shortcuts-with-translation
|
||||
:viewer-sc-trans viewer-shortcuts-with-translation
|
||||
:all-shortcuts all-shortcuts
|
||||
:all-sc-raw all-shortcuts-raw}
|
||||
|
||||
on-restore-all
|
||||
(mf/use-fn
|
||||
(mf/deps custom-shortcuts)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (modal/show {:type :restore-all-modal
|
||||
:custom-shortcuts custom-shortcuts}))))
|
||||
|
||||
on-import-file
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(dom/click (mf/ref-val input-ref))))
|
||||
|
||||
shortcuts-json
|
||||
(mf/with-memo
|
||||
[custom-shortcuts]
|
||||
(some-> custom-shortcuts
|
||||
(json/encode :key-fn d/name :indent 2)))
|
||||
|
||||
on-export
|
||||
(mf/use-fn
|
||||
(mf/deps shortcuts-json has-custom-shortcuts)
|
||||
(fn []
|
||||
(when has-custom-shortcuts
|
||||
(->> (wapi/create-blob shortcuts-json "application/json")
|
||||
(dom/trigger-download "penpot-shortcuts.json")))))
|
||||
|
||||
on-file-selected
|
||||
(mf/use-fn
|
||||
(mf/deps all-shortcuts-raw)
|
||||
(fn [event]
|
||||
(let [file (-> (dom/get-target event)
|
||||
(dom/get-files)
|
||||
(first))]
|
||||
(->> (wapi/read-file-as-text file)
|
||||
(rx/subs!
|
||||
(fn [content]
|
||||
(try
|
||||
(let [shortcuts (js->clj (.parse js/JSON content)
|
||||
:keywordize-keys true)
|
||||
validation (validate-imported-shortcuts shortcuts)]
|
||||
(if (:valid? validation)
|
||||
(st/emit! (ss/import-custom-shortcuts shortcuts all-shortcuts-raw))
|
||||
(st/emit! (ntf/error (tr "errors.invalid-data")))))
|
||||
(catch :default _
|
||||
(st/emit! (ntf/error (tr "errors.invalid-data"))))))))
|
||||
|
||||
(-> (mf/ref-val input-ref)
|
||||
(dom/set-value! "")))))]
|
||||
|
||||
[:section {:class (stl/css :shortcuts-page)
|
||||
:aria-label (tr "shortcuts.page")}
|
||||
[:div {:class (stl/css :shortcuts-content)}
|
||||
[:> heading* {:level 2
|
||||
:typography t/title-large
|
||||
:class (stl/css :page-title)}
|
||||
(tr "label.shortcuts")]
|
||||
[:> text* {:class (stl/css :shortcuts-description)
|
||||
:as "span"
|
||||
:typography t/body-small}
|
||||
[:> icon* {:icon-id i/info
|
||||
:class (stl/css :shortcuts-description-icon)}]
|
||||
(tr "shortcuts.reload-hint")]
|
||||
|
||||
[:> (mf/provider ctx/shortcuts-ctx) {:value shortcuts-ctx-value}
|
||||
[:div {:class (stl/css :shortcuts-content)}
|
||||
[:> tab-switcher* {:tabs tabs
|
||||
:selected (name section)
|
||||
:on-change handle-change-tab
|
||||
:class (stl/css :shortcuts-switcher)}
|
||||
(case section
|
||||
:all
|
||||
[:> shortcuts-tab-section* {:shortcut-filter (fn [_ shortcut search-term]
|
||||
(or (str/blank? search-term)
|
||||
(matches-search (:translation shortcut) search-term)))
|
||||
:show-restore-all? true
|
||||
:empty-str (tr "shortcuts.no-shortcuts")
|
||||
:on-restore-all on-restore-all
|
||||
:custom-shortcuts custom-shortcuts}]
|
||||
|
||||
:personalized
|
||||
[:> shortcuts-tab-section* {:shortcut-filter (fn [shortcut-key shortcut search-term]
|
||||
(let [shortcut-group (first (:section shortcut))
|
||||
group-map (get custom-shortcuts shortcut-group)
|
||||
group-map (if (map? group-map) group-map {})
|
||||
customized? (and (contains? group-map shortcut-key)
|
||||
(not (str/blank? (get group-map shortcut-key))))]
|
||||
(and customized?
|
||||
(or (str/blank? search-term)
|
||||
(matches-search (:translation shortcut) search-term)))))
|
||||
:show-restore-all? true
|
||||
:empty-str (tr "shortcuts.no-personalized")
|
||||
:on-restore-all on-restore-all
|
||||
:custom-shortcuts custom-shortcuts}]
|
||||
|
||||
:disabled
|
||||
[:> shortcuts-tab-section* {:shortcut-filter (fn [shortcut-key shortcut search-term]
|
||||
(let [shortcut-group (first (:section shortcut))
|
||||
group-map (get custom-shortcuts shortcut-group)
|
||||
group-map (if (map? group-map) group-map {})
|
||||
in-group? (contains? group-map shortcut-key)
|
||||
blank? (str/blank? (get group-map shortcut-key))]
|
||||
(and in-group? blank?
|
||||
(or (str/blank? search-term)
|
||||
(matches-search (:translation shortcut) search-term)))))
|
||||
:show-restore-all? true
|
||||
:empty-str (tr "shortcuts.no-disabled")
|
||||
:on-restore-all on-restore-all
|
||||
:custom-shortcuts custom-shortcuts}])]]
|
||||
|
||||
[:div {:class (stl/css :shortcuts-page-footer)}
|
||||
[:div {:class (stl/css :shortcuts-info)}
|
||||
[:div {:class (stl/css :shortcuts-info-wrapper)}
|
||||
[:div {:class (stl/css :dot-wrapper)}
|
||||
[:div {:class (stl/css :shortcuts-customized-dot)}]]
|
||||
[:p {:class (stl/css :shortcuts-text)}
|
||||
(tr "shortcuts.personalized")]]
|
||||
|
||||
[:div {:class (stl/css :shortcuts-info-wrapper)}
|
||||
[:> icon* {:class (stl/css :shortcuts-not-assigned-icon)
|
||||
:icon-id i/broken-link}]
|
||||
[:p {:class (stl/css :shortcuts-text)}
|
||||
(tr "shortcuts.disabled")]]]
|
||||
|
||||
[:div {:class (stl/css :export-wrapper)}
|
||||
|
||||
[:> button* {:variant "secondary"
|
||||
:on-click open-menu
|
||||
:icon i/import-export}
|
||||
(tr "shortcuts.import-export")]
|
||||
[:> dropdown-menu* {:show show-menu?
|
||||
:on-close close-menu
|
||||
:id "tokens-menu"
|
||||
:class (stl/css :import-export-menu)}
|
||||
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
|
||||
:on-click on-import-file}
|
||||
[:div {:class (stl/css :import-menu-item)}
|
||||
[:div (tr "labels.import")]]]
|
||||
(when has-custom-shortcuts
|
||||
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
|
||||
:on-click on-export}
|
||||
(tr "labels.export")])]]
|
||||
|
||||
[:input
|
||||
{:type "file"
|
||||
:accept ".json,application/json"
|
||||
:ref input-ref
|
||||
:style {:display "none"}
|
||||
:on-change on-file-selected}]]]]]))
|
||||
324
frontend/src/app/main/ui/settings/shortcuts.scss
Normal file
324
frontend/src/app/main/ui/settings/shortcuts.scss
Normal file
@ -0,0 +1,324 @@
|
||||
// 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 Sucursal en España SL
|
||||
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/spacing.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/z-index.scss" as *;
|
||||
|
||||
.shortcuts-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-block-end: px2rem(32);
|
||||
}
|
||||
|
||||
.shortcuts-list {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
block-size: 100%;
|
||||
padding: var(--sp-m);
|
||||
overflow-y: auto;
|
||||
color: var(--color-foreground-secondary);
|
||||
min-block-size: px2rem(250);
|
||||
}
|
||||
|
||||
.shortcuts-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-inline-size: px2rem(590);
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: var(--color-foreground-primary);
|
||||
inline-size: 100%;
|
||||
margin-block-start: px2rem(80);
|
||||
}
|
||||
|
||||
.shortcuts-section {
|
||||
margin-block-end: var(--sp-lg);
|
||||
}
|
||||
|
||||
.shortcuts-search-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--sp-xs);
|
||||
padding-block-start: var(--sp-s);
|
||||
}
|
||||
|
||||
.restore-all-button {
|
||||
min-inline-size: px2rem(130);
|
||||
max-inline-size: px2rem(300);
|
||||
inline-size: fit-content;
|
||||
}
|
||||
|
||||
// RESTORE ALL MODAL
|
||||
|
||||
.modal-overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: 0;
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
z-index: var(--z-index-set);
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
position: relative;
|
||||
padding: var(--sp-xxxl);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-primary);
|
||||
border: $b-2 solid var(--color-background-quaternary);
|
||||
display: grid;
|
||||
grid-template-rows: 0 auto 1fr;
|
||||
gap: var(--sp-xxxl);
|
||||
min-inline-size: $sz-364;
|
||||
min-block-size: $sz-192;
|
||||
max-block-size: $sz-520;
|
||||
inline-size: $sz-500;
|
||||
max-inline-size: $sz-712;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include t.use-typography("headline-medium");
|
||||
|
||||
color: var(--color-foreground-primary);
|
||||
block-size: px2rem(26);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
inset-block-start: px2rem(8);
|
||||
inset-inline-end: px2rem(6);
|
||||
block-size: $sz-32;
|
||||
inline-size: $sz-28;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
block-size: fit-content;
|
||||
}
|
||||
|
||||
.modal-content-text {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
justify-self: end;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.shortcuts-table {
|
||||
inline-size: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.shortcuts-list-item,
|
||||
.shortcuts-list-header {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
display: grid;
|
||||
grid-template-columns: 40% 30% 30%;
|
||||
}
|
||||
|
||||
.shortcuts-list-header {
|
||||
border-block-end: $b-1 solid var(--color-foreground-secondary);
|
||||
block-size: $sz-32;
|
||||
text-align: start;
|
||||
padding-block-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.shortcuts-list-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.shortcuts-list-item {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.shortcuts-list-item:first-child {
|
||||
padding-block-start: var(--sp-s);
|
||||
}
|
||||
|
||||
.shortcut-header-name,
|
||||
.shortcut-header-command {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.shortcut-command {
|
||||
display: flex;
|
||||
gap: var(--sp-s);
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
text-align: start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.default-command {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
background-color: var(--color-background-tertiary);
|
||||
padding: px2rem(6) var(--sp-xs);
|
||||
}
|
||||
|
||||
.shortcuts-page-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-s);
|
||||
padding-block-start: var(--sp-m);
|
||||
}
|
||||
|
||||
.shortcuts-info {
|
||||
display: flex;
|
||||
gap: var(--sp-s);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shortcuts-page-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-s);
|
||||
padding-block-start: var(--sp-m);
|
||||
}
|
||||
|
||||
.shortcuts-info {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.shortcuts-customized-dot,
|
||||
.shortcuts-conflict-dot {
|
||||
inline-size: px2rem(4);
|
||||
block-size: px2rem(4);
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-accent-info);
|
||||
}
|
||||
|
||||
.shortcuts-conflict-dot {
|
||||
background-color: var(--color-accent-error);
|
||||
}
|
||||
|
||||
.shortcuts-info {
|
||||
display: flex;
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.shortcuts-info-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--sp-xxs);
|
||||
}
|
||||
|
||||
.dot-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
block-size: px2rem(16);
|
||||
inline-size: px2rem(16);
|
||||
}
|
||||
|
||||
.shortcuts-text {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.shortcuts-not-assigned-icon {
|
||||
margin-inline-end: var(--sp-xs);
|
||||
}
|
||||
|
||||
.export-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.import-export-menu {
|
||||
box-shadow: 0 0 px2rem(12) 0 var(--color-shadow-dark);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
position: absolute;
|
||||
padding: var(--sp-xs);
|
||||
border-radius: $br-8;
|
||||
z-index: var(--z-index-dropdown);
|
||||
color: var(--color-foreground-primary);
|
||||
background-color: var(--color-background-tertiary);
|
||||
border: $b-2 solid var(--color-background-quaternary);
|
||||
inset-block-start: calc(-1 * px2rem(6));
|
||||
inset-inline-end: 0;
|
||||
translate: 0 -100%;
|
||||
inline-size: px2rem(192);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.import-export-menu-item {
|
||||
@include t.use-typography("body-medium");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
block-size: $sz-28;
|
||||
inline-size: 100%;
|
||||
padding: px2rem(6);
|
||||
border-radius: $br-8;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-quaternary);
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.import-export-menu-item-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.shortcuts-empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
block-size: px2rem(250);
|
||||
}
|
||||
|
||||
.shortcuts-description {
|
||||
color: var(--color-foreground-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
padding: var(--sp-s) 0;
|
||||
margin-block-end: var(--sp-l);
|
||||
}
|
||||
@ -48,6 +48,9 @@
|
||||
(def ^:private go-settings-notifications
|
||||
#(st/emit! (rt/nav :settings-notifications)))
|
||||
|
||||
(def ^:private go-settings-shortcuts
|
||||
#(st/emit! (rt/nav :settings-shortcuts)))
|
||||
|
||||
(defn- show-release-notes
|
||||
[event]
|
||||
(let [version (:main cf/version)]
|
||||
@ -66,6 +69,7 @@
|
||||
subscription? (= section :settings-subscription)
|
||||
integrations? (= section :settings-integrations)
|
||||
notifications? (= section :settings-notifications)
|
||||
shortcuts? (= section :settings-shortcuts)
|
||||
team-id (or (dtm/get-last-team-id)
|
||||
(:default-team-id profile))
|
||||
|
||||
@ -83,7 +87,8 @@
|
||||
|
||||
[:hr {:class (stl/css :sidebar-separator)}]
|
||||
|
||||
[:div {:class (stl/css :sidebar-content-section)}
|
||||
[:nav {:class (stl/css :sidebar-content-section)
|
||||
:aria-label (tr "labels.settings")}
|
||||
[:ul {:class (stl/css :sidebar-nav-settings)}
|
||||
[:li {:class (stl/css-case :current profile?
|
||||
:settings-item true)
|
||||
@ -100,6 +105,12 @@
|
||||
:on-click go-settings-notifications}
|
||||
[:span {:class (stl/css :element-title)} (tr "labels.notifications")]]
|
||||
|
||||
(when (contains? cf/flags :custom-shortcuts)
|
||||
[:li {:class (stl/css-case :current shortcuts?
|
||||
:settings-item true)
|
||||
:on-click go-settings-shortcuts}
|
||||
[:span {:class (stl/css :element-title)} (tr "label.shortcuts")]])
|
||||
|
||||
[:li {:class (stl/css-case :current options?
|
||||
:settings-item true)
|
||||
:on-click go-settings-options
|
||||
@ -137,7 +148,7 @@
|
||||
(mf/defc sidebar*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [profile section]}]
|
||||
[:div {:class (stl/css :dashboard-sidebar :settings)}
|
||||
[:aside {:class (stl/css :dashboard-sidebar :settings)}
|
||||
[:> sidebar-content* {:profile profile
|
||||
:section section}]
|
||||
[:> profile-section* {:profile profile}]])
|
||||
|
||||
@ -565,10 +565,10 @@
|
||||
(tr "subscription.settings.enterprise")))}))
|
||||
(rt/nav :settings-subscription {} {::rt/replace true})))))
|
||||
|
||||
[:section {:class (stl/css :dashboard-section)}
|
||||
[:section {:class (stl/css :dashboard-section)
|
||||
:aria-labelledby "subscription-section-title"}
|
||||
[:div {:class (stl/css :dashboard-content)}
|
||||
[:h2 {:class (stl/css :title-section)} (tr "subscription.labels")]
|
||||
|
||||
[:h2 {:id "subscription-section-title" :class (stl/css :title-section)} (tr "subscription.labels")]
|
||||
|
||||
[:div {:class (stl/css :your-subscription)}
|
||||
[:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.section-plan")]
|
||||
|
||||
700
frontend/src/app/main/ui/shortcuts.cljs
Normal file
700
frontend/src/app/main/ui/shortcuts.cljs
Normal file
@ -0,0 +1,700 @@
|
||||
;; 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 Sucursal en España SL
|
||||
|
||||
(ns app.main.ui.shortcuts
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard.shortcuts.customize :as customize]
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.strings :refer [matches-search]]
|
||||
[cuerdas.core :as str]
|
||||
[goog.events :as events]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private modifier-keys
|
||||
#{"Control" "Shift" "Alt" "Meta"})
|
||||
|
||||
(def ^:private key-name-map
|
||||
{"ArrowUp" "up"
|
||||
"ArrowDown" "down"
|
||||
"ArrowLeft" "left"
|
||||
"ArrowRight" "right"
|
||||
"Escape" "escape"
|
||||
"Enter" "enter"
|
||||
"Backspace" "backspace"
|
||||
"Delete" "del"
|
||||
"Tab" "tab"
|
||||
" " "space"})
|
||||
|
||||
(defn- keyboard-event->mousetrap
|
||||
[^js event]
|
||||
(let [parts (cond-> []
|
||||
(and (.-ctrlKey event)
|
||||
(not (cf/check-platform? :macos)))
|
||||
(conj "ctrl")
|
||||
|
||||
(and (.-metaKey event)
|
||||
(cf/check-platform? :macos))
|
||||
(conj "command")
|
||||
|
||||
(.-altKey event)
|
||||
(conj "alt")
|
||||
|
||||
(.-shiftKey event)
|
||||
(conj "shift"))
|
||||
key (.-key event)
|
||||
key (if (contains? modifier-keys key)
|
||||
nil
|
||||
(or (get key-name-map key)
|
||||
(.toLowerCase key)))]
|
||||
(when key
|
||||
(str/join "+" (conj parts key)))))
|
||||
|
||||
(defn- keyboard-event->display-parts
|
||||
[^js event]
|
||||
(let [parts (cond-> []
|
||||
(and (.-ctrlKey event)
|
||||
(not (cf/check-platform? :macos)))
|
||||
(conj "ctrl")
|
||||
|
||||
(and (.-metaKey event)
|
||||
(cf/check-platform? :macos))
|
||||
(conj "command")
|
||||
|
||||
(.-altKey event)
|
||||
(conj "alt")
|
||||
|
||||
(.-shiftKey event)
|
||||
(conj "shift"))
|
||||
key (.-key event)]
|
||||
(if (contains? modifier-keys key)
|
||||
{:modifiers parts :finalized? false}
|
||||
{:modifiers parts
|
||||
:final-key (or (get key-name-map key) (.toLowerCase key))
|
||||
:finalized? true})))
|
||||
|
||||
(defn translation-keyname
|
||||
[type keyname]
|
||||
(let [translat-pre (case type
|
||||
:sc "shortcuts."
|
||||
:sec "shortcut-section."
|
||||
:sub-sec "shortcut-subsection.")]
|
||||
(tr (str translat-pre (d/name keyname)))))
|
||||
|
||||
(defn- find-conflict
|
||||
[new-command all-shortcuts current-key]
|
||||
(let [command-index (ds/build-command-index all-shortcuts)]
|
||||
(when-let [conflicting-key (get command-index new-command)]
|
||||
(when (not= conflicting-key current-key)
|
||||
{:key conflicting-key
|
||||
:name (translation-keyname :sc conflicting-key)}))))
|
||||
|
||||
(def ^:private import-contexts
|
||||
[:workspace :dashboard :viewer])
|
||||
|
||||
(defn- import-context-group
|
||||
"Imports a single context group from the payload, disabling any default
|
||||
shortcut whose command collides with a newly imported one, and any
|
||||
previously-imported entry in the same batch with a duplicate command."
|
||||
[group all-shortcuts]
|
||||
(reduce
|
||||
(fn [acc [command recorded-command]]
|
||||
(let [default-conflict (find-conflict recorded-command all-shortcuts command)
|
||||
acc-conflict (some (fn [[k v]]
|
||||
(when (and (not= k command) (= v recorded-command))
|
||||
k))
|
||||
acc)]
|
||||
(cond-> (assoc acc command recorded-command)
|
||||
(:key default-conflict)
|
||||
(assoc (:key default-conflict) "")
|
||||
acc-conflict
|
||||
(assoc acc-conflict ""))))
|
||||
{}
|
||||
group))
|
||||
|
||||
(defn import-custom-shortcuts
|
||||
[shortcuts all-shortcuts]
|
||||
(let [current-customs (or (get-in shortcuts [:profile :props :custom-shortcuts])
|
||||
(get-in @st/state [:profile :props :custom-shortcuts])
|
||||
{})
|
||||
|
||||
new-customs (reduce
|
||||
(fn [acc ctx]
|
||||
(if (contains? shortcuts ctx)
|
||||
(assoc acc ctx (import-context-group (get shortcuts ctx) all-shortcuts))
|
||||
acc))
|
||||
current-customs
|
||||
import-contexts)]
|
||||
|
||||
(du/update-profile-props {:custom-shortcuts new-customs})))
|
||||
|
||||
(defn add-translation
|
||||
[type item]
|
||||
(map (fn [[k v]] [k (assoc v :translation (translation-keyname type k))]) item))
|
||||
|
||||
(defn shortcuts->subsections
|
||||
[shortcuts]
|
||||
(let [subsections (into #{} (mapcat :subsections) (vals shortcuts))
|
||||
subsections (vec (sort-by name subsections))
|
||||
get-sc-by-subsection
|
||||
(fn [subsection [k v]]
|
||||
(when (some #(= subsection %) (:subsections v)) {k v}))
|
||||
reduce-sc
|
||||
(fn [acc subsection]
|
||||
(let [shortcuts-by-subsection (into {} (keep (partial get-sc-by-subsection subsection) shortcuts))]
|
||||
(assoc acc subsection {:children shortcuts-by-subsection})))]
|
||||
(reduce reduce-sc (sorted-map-by (fn [a b] (compare (name a) (name b)))) subsections)))
|
||||
|
||||
(defn build-all-shortcuts
|
||||
[workspace-sc dashboard-sc viewer-sc]
|
||||
(let [walk (fn walk [element parent-id]
|
||||
(if (nil? element)
|
||||
element
|
||||
(let [sorted-comp (when (sorted? element)
|
||||
#(compare (name %1) (name %2)))
|
||||
rec-fn (fn [[k item]]
|
||||
(let [item-id (if (nil? parent-id)
|
||||
[k]
|
||||
(conj parent-id k))]
|
||||
[k (assoc item :id item-id :children (walk (:children item) item-id))]))]
|
||||
(if sorted-comp
|
||||
(into (sorted-map-by sorted-comp) (map rec-fn element))
|
||||
(into {} (map rec-fn element))))))
|
||||
sorted-map-fn #(sorted-map-by (fn [a b] (compare (name a) (name b))))
|
||||
ws-subs (->> (shortcuts->subsections workspace-sc)
|
||||
(add-translation :sub-sec)
|
||||
(into (sorted-map-fn)))
|
||||
|
||||
db-subs (->> (shortcuts->subsections dashboard-sc)
|
||||
(add-translation :sub-sec)
|
||||
(into (sorted-map-fn)))
|
||||
vw-subs (->> (shortcuts->subsections viewer-sc)
|
||||
(add-translation :sub-sec)
|
||||
(into (sorted-map-fn)))
|
||||
basics (into {} (concat (:children (:basics ws-subs))
|
||||
(:children (:basics db-subs))
|
||||
(:children (:basics vw-subs))))
|
||||
ws-subs (dissoc ws-subs :basics)
|
||||
db-subs (dissoc db-subs :basics)
|
||||
vw-subs (dissoc vw-subs :basics)
|
||||
all {:basics {:children {:none {:children basics}}
|
||||
:translation (tr "shortcut-section.basics")}
|
||||
:workspace {:children ws-subs
|
||||
:translation (tr "shortcut-section.workspace")}
|
||||
:dashboard {:children db-subs
|
||||
:translation (tr "shortcut-section.dashboard")}
|
||||
:viewer {:children vw-subs
|
||||
:translation (tr "shortcut-section.viewer")}}
|
||||
all (walk all nil)
|
||||
all-sc-names (map #(translation-keyname :sc %)
|
||||
(concat (keys workspace-sc)
|
||||
(keys dashboard-sc)
|
||||
(keys viewer-sc)))
|
||||
all-sub-names (map #(translation-keyname :sub-sec %)
|
||||
(concat (keys ws-subs)
|
||||
(keys db-subs)
|
||||
(keys vw-subs)))
|
||||
all-section-names (map #(translation-keyname :sec %) (keys all))]
|
||||
{:all-shortcuts all
|
||||
:all-sc-names all-sc-names
|
||||
:all-sub-names all-sub-names
|
||||
:all-section-names all-section-names}))
|
||||
|
||||
(defn build-all-shortcuts-without-basics
|
||||
[workspace-sc path-sc dashboard-sc viewer-sc
|
||||
& {:keys [workspace-orig path-orig dashboard-orig viewer-orig]}]
|
||||
(let [original-commands (into {}
|
||||
(concat
|
||||
(map (fn [[k v]] [k (:command v)]) workspace-orig)
|
||||
(map (fn [[k v]] [k (:command v)]) path-orig)
|
||||
(map (fn [[k v]] [k (:command v)]) dashboard-orig)
|
||||
(map (fn [[k v]] [k (:command v)]) viewer-orig)))
|
||||
walk (fn walk [element parent-id]
|
||||
(if (nil? element)
|
||||
element
|
||||
(let [sorted-comp (when (sorted? element)
|
||||
#(compare (name %1) (name %2)))
|
||||
rec-fn (fn [[k item]]
|
||||
(let [item-id (if (nil? parent-id)
|
||||
[k]
|
||||
(conj parent-id k))
|
||||
item (if (:command item)
|
||||
(assoc item :original-command (get original-commands k (:command item)))
|
||||
item)]
|
||||
[k (assoc item :id item-id :children (walk (:children item) item-id))]))]
|
||||
(if sorted-comp
|
||||
(into (sorted-map-by sorted-comp) (map rec-fn element))
|
||||
(into {} (map rec-fn element))))))
|
||||
sorted-map-fn #(sorted-map-by (fn [a b] (compare (name a) (name b))))
|
||||
ws-subs (->> (shortcuts->subsections workspace-sc)
|
||||
(add-translation :sub-sec)
|
||||
(into (sorted-map-fn)))
|
||||
|
||||
pt-subs (->> (shortcuts->subsections path-sc)
|
||||
(add-translation :sub-sec)
|
||||
(into (sorted-map-fn)))
|
||||
db-subs (->> (shortcuts->subsections dashboard-sc)
|
||||
(add-translation :sub-sec)
|
||||
(into (sorted-map-fn)))
|
||||
vw-subs (->> (shortcuts->subsections viewer-sc)
|
||||
(add-translation :sub-sec)
|
||||
(into (sorted-map-fn)))
|
||||
ws-subs (dissoc ws-subs :basics)
|
||||
pt-subs (dissoc pt-subs :basics)
|
||||
db-subs (dissoc db-subs :basics)
|
||||
vw-subs (dissoc vw-subs :basics)
|
||||
ws-subs (merge ws-subs pt-subs)
|
||||
all {:workspace {:children ws-subs
|
||||
:translation (tr "shortcut-section.workspace")}
|
||||
:dashboard {:children db-subs
|
||||
:translation (tr "shortcut-section.dashboard")}
|
||||
:viewer {:children vw-subs
|
||||
:translation (tr "shortcut-section.viewer")}}
|
||||
all (walk all nil)
|
||||
all-sc-names (map #(translation-keyname :sc %)
|
||||
(concat (keys workspace-sc)
|
||||
(keys path-sc)
|
||||
(keys dashboard-sc)
|
||||
(keys viewer-sc)))
|
||||
all-sub-names (map #(translation-keyname :sub-sec %)
|
||||
(concat (keys ws-subs)
|
||||
(keys db-subs)
|
||||
(keys vw-subs)))
|
||||
all-section-names (map #(translation-keyname :sec %) (keys all))]
|
||||
{:all-shortcuts all
|
||||
:all-sc-names all-sc-names
|
||||
:all-sub-names all-sub-names
|
||||
:all-section-names all-section-names}))
|
||||
|
||||
(mf/defc converted-chars*
|
||||
[{:keys [char command class]}]
|
||||
(let [modified-keys {:up ds/up-arrow
|
||||
:down ds/down-arrow
|
||||
:left ds/left-arrow
|
||||
:right ds/right-arrow
|
||||
:plus "+"}
|
||||
macos-keys {:command "\u2318"
|
||||
:option "\u2325"
|
||||
:alt "\u2325"
|
||||
:delete "\u232B"
|
||||
:del "\u232B"
|
||||
:shift "\u21E7"
|
||||
:control "\u2303"
|
||||
:esc "\u238B"
|
||||
:enter "\u23CE"}
|
||||
is-macos? (cf/check-platform? :macos)
|
||||
char (if (contains? modified-keys (keyword char)) ((keyword char) modified-keys) char)
|
||||
char (if (and is-macos? (contains? macos-keys (keyword char))) ((keyword char) macos-keys) char)
|
||||
unique-key (str (d/name command) "-" char)]
|
||||
[:span {:class [class (stl/css :key)]
|
||||
:key unique-key} char]))
|
||||
|
||||
(mf/defc shortcuts-keys*
|
||||
[{:keys [content command is-customized has-conflict?]}]
|
||||
(let [managed-list (if (coll? content)
|
||||
content
|
||||
(conj () content))
|
||||
chars-list (map ds/split-sc managed-list)
|
||||
last-element (last chars-list)
|
||||
short-char-list (if (= 1 (count chars-list))
|
||||
chars-list
|
||||
(drop-last chars-list))
|
||||
penultimate (last short-char-list)]
|
||||
[:span {:class (stl/css :keys)}
|
||||
(for [chars short-char-list]
|
||||
[:* {:key (str/join chars)}
|
||||
(for [char chars]
|
||||
[:> converted-chars* {:key (dm/str char "-" (name command))
|
||||
:char char
|
||||
:command command
|
||||
:class (cond
|
||||
has-conflict? (stl/css :conflict-key)
|
||||
is-customized (stl/css :customized-key))}])
|
||||
|
||||
(when (not= chars penultimate) [:span {:class (stl/css :space)} ","])])
|
||||
(when (not= last-element penultimate)
|
||||
[:*
|
||||
[:span {:class (stl/css :space)} (tr "shortcuts.or")]
|
||||
(for [char last-element]
|
||||
[:> converted-chars* {:key (dm/str char "-" (name command))
|
||||
:char char
|
||||
:command command
|
||||
:class (cond
|
||||
has-conflict? (stl/css :conflict-key)
|
||||
is-customized (stl/css :customized-key))}])])]))
|
||||
|
||||
(mf/defc shortcut-row-editable*
|
||||
[{:keys [elements custom-shortcuts command-translate conflicts section-key]}]
|
||||
(let [{:keys [workspace-sc-trans dashboard-sc-trans viewer-sc-trans]} (mf/use-ctx ctx/shortcuts-ctx)
|
||||
section-shortcuts (case section-key
|
||||
(:workspace :path) workspace-sc-trans
|
||||
:dashboard dashboard-sc-trans
|
||||
:viewer viewer-sc-trans
|
||||
workspace-sc-trans)
|
||||
sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements))
|
||||
[command command-info] sc-by-translate
|
||||
content (or (:show-command command-info) (:command command-info))
|
||||
group-map (if (= section-key :basics)
|
||||
(merge (get custom-shortcuts :workspace)
|
||||
(get custom-shortcuts :dashboard)
|
||||
(get custom-shortcuts :viewer))
|
||||
(get custom-shortcuts section-key))
|
||||
group-map (if (map? group-map) group-map {})
|
||||
customized? (contains? group-map command)
|
||||
has-conflict? (contains? conflicts command)
|
||||
is-editing* (mf/use-state false)
|
||||
is-editing (deref is-editing*)
|
||||
|
||||
recording-ref (mf/use-ref nil)
|
||||
|
||||
display-parts* (mf/use-state nil)
|
||||
display-parts (deref display-parts*)
|
||||
|
||||
recorded-command* (mf/use-state nil)
|
||||
recorded-command (deref recorded-command*)
|
||||
|
||||
conflict* (mf/use-state nil)
|
||||
conflict (deref conflict*)
|
||||
|
||||
clean-editing-state
|
||||
(fn []
|
||||
(reset! is-editing* false)
|
||||
(reset! display-parts* nil)
|
||||
(reset! recorded-command* nil)
|
||||
(reset! conflict* nil))
|
||||
|
||||
effective-section-key (if (= section-key :basics) :workspace section-key)
|
||||
|
||||
on-reset-shortcut
|
||||
(mf/use-fn
|
||||
(mf/deps effective-section-key command-info)
|
||||
(fn [shortcut-key]
|
||||
(st/emit! (customize/reset-custom-shortcut shortcut-key (:original-command command-info) effective-section-key))
|
||||
(clean-editing-state)))
|
||||
|
||||
start-editing
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(reset! is-editing* true)
|
||||
(reset! display-parts* nil)
|
||||
(reset! recorded-command* nil)
|
||||
(reset! conflict* nil)))
|
||||
|
||||
stop-editing
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(clean-editing-state)))
|
||||
|
||||
disabled-shortcut
|
||||
(mf/use-fn
|
||||
(mf/deps command effective-section-key)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (customize/set-custom-shortcut
|
||||
command
|
||||
""
|
||||
nil
|
||||
effective-section-key))
|
||||
(clean-editing-state)))
|
||||
|
||||
save-fn
|
||||
(mf/use-fn
|
||||
(mf/deps recorded-command conflict command effective-section-key)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(when recorded-command
|
||||
(st/emit! (customize/set-custom-shortcut
|
||||
command
|
||||
recorded-command
|
||||
(:key conflict)
|
||||
effective-section-key)))
|
||||
(clean-editing-state)))
|
||||
|
||||
on-editable-container-blur
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(when-not (.contains (.-currentTarget e)
|
||||
(.-relatedTarget e))
|
||||
(clean-editing-state))))]
|
||||
|
||||
(mf/with-effect [is-editing]
|
||||
(when is-editing
|
||||
(some-> (mf/ref-val recording-ref) (dom/focus!))))
|
||||
|
||||
(mf/with-effect [is-editing]
|
||||
(when is-editing
|
||||
(letfn [(on-keydown [^js event]
|
||||
(.preventDefault event)
|
||||
(.stopPropagation event)
|
||||
(let [recorded-command (keyboard-event->mousetrap event)
|
||||
parts (keyboard-event->display-parts event)]
|
||||
(reset! display-parts* parts)
|
||||
(when recorded-command
|
||||
(reset! recorded-command* recorded-command)
|
||||
(reset! conflict* (find-conflict recorded-command section-shortcuts command)))))]
|
||||
(->> (events/listen (mf/ref-val recording-ref) "keydown" on-keydown)
|
||||
(partial events/unlistenByKey)))))
|
||||
|
||||
|
||||
[:li {:class (stl/css-case :shortcuts-name-editable true
|
||||
:shortcuts-editing is-editing
|
||||
:customized customized?)
|
||||
:data-customized (str customized?)
|
||||
:data-conflict (str has-conflict?)
|
||||
:aria-label command-translate
|
||||
:key command-translate}
|
||||
[:button {:on-click start-editing
|
||||
:disabled is-editing
|
||||
:aria-label (dm/str "Edit " command-translate)
|
||||
:class (stl/css-case :shortcut-button-editable true
|
||||
:shortcut-button-editing is-editing)}
|
||||
[:span {:class (stl/css :command-name)
|
||||
:id (dm/str command-translate "-label")}
|
||||
command-translate]
|
||||
[:div {:class (stl/css :shortcut-actions)
|
||||
:aria-labelledby (dm/str command-translate "-label")}
|
||||
(if (and customized? (str/blank? content))
|
||||
[:> icon* {:icon-id i/broken-link
|
||||
:size "s"
|
||||
:class (stl/css :shortcut-detach-icon)}]
|
||||
|
||||
(if has-conflict?
|
||||
(let [conflicting-key (get conflicts command)
|
||||
conflict-name (translation-keyname :sc conflicting-key)]
|
||||
[:> tooltip* {:content (tr "shortcuts.overwritten-by" conflict-name)
|
||||
:id (dm/str (name command) "-conflict-tooltip")
|
||||
:class (stl/css :option-text)}
|
||||
[:> shortcuts-keys* {:content content
|
||||
:command command
|
||||
:is-customized customized?
|
||||
:has-conflict? has-conflict?}]])
|
||||
[:> shortcuts-keys* {:content content
|
||||
:command command
|
||||
:is-customized customized?
|
||||
:has-conflict? has-conflict?}]))]]
|
||||
(when is-editing
|
||||
[:div {:class (stl/css :shortcut-editing)
|
||||
:ref recording-ref
|
||||
:tab-index 0
|
||||
:on-blur on-editable-container-blur}
|
||||
[:div {:class (stl/css :shortcut-recording-area)}
|
||||
(if (nil? display-parts)
|
||||
[:span {:class (stl/css :shortcut-placeholder)}
|
||||
(tr "shortcuts.key-combo")]
|
||||
[:span {:class (stl/css :shortcut-recorded-keys)}
|
||||
(for [mod (:modifiers display-parts)]
|
||||
[:span {:class (stl/css-case :key true
|
||||
:customized-key customized?) :key mod} mod])
|
||||
(when (:final-key display-parts)
|
||||
[:span {:class (stl/css-case :key true
|
||||
:customized-key customized?)} (:final-key display-parts)])
|
||||
(when-not (:finalized? display-parts)
|
||||
[:span {:class (stl/css :recording-ellipsis)} "..."])])]
|
||||
(when recorded-command
|
||||
(if conflict
|
||||
[:> context-notification* {:level :warning :class (stl/css :modal-error-msg)}
|
||||
(tr "shortcuts.edit-modal.conflict" (:name conflict))]
|
||||
[:> context-notification* {:level :success :class (stl/css :modal-success-msg)}
|
||||
(tr "shortcuts.edit-modal.success")]))
|
||||
[:div {:class (stl/css :edit-buttons)}
|
||||
[:div {:class (stl/css :confirmation-buttons)}
|
||||
[:> icon-button* {:variant "secondary"
|
||||
:aria-label (tr "labels.cancel")
|
||||
:on-click stop-editing
|
||||
:icon i/close
|
||||
:icon-size "m"}]
|
||||
[:> icon-button* {:variant "primary"
|
||||
:aria-label (tr "labels.save")
|
||||
:on-click save-fn
|
||||
:icon i/tick
|
||||
:icon-size "m"}]]
|
||||
[:div {:class (stl/css :confirmation-buttons)}
|
||||
[:> icon-button* {:variant "secondary"
|
||||
:aria-label (tr "shortcuts.disable")
|
||||
:on-click (fn [e]
|
||||
(dom/stop-propagation e)
|
||||
(disabled-shortcut command))
|
||||
:icon i/broken-link
|
||||
:icon-size "m"}]
|
||||
(when customized?
|
||||
[:> icon-button* {:variant "secondary"
|
||||
:aria-label (tr "shortcuts.reset")
|
||||
:on-click (fn [e]
|
||||
(dom/stop-propagation e)
|
||||
(on-reset-shortcut command))
|
||||
:icon i/reload
|
||||
:icon-size "m"}])]]])]))
|
||||
|
||||
(mf/defc shortcut-row*
|
||||
[{:keys [elements filter-term is-match-section is-match-subsection
|
||||
editable? custom-shortcuts section-key conflicts hidden subsection-name]}]
|
||||
(let [shortcut-translations (->> elements vals (map :translation) sort)
|
||||
match-shortcut? (some #(matches-search % filter-term) shortcut-translations)
|
||||
filtered (if (and (or is-match-section is-match-subsection) (not match-shortcut?))
|
||||
shortcut-translations
|
||||
(filter #(matches-search % filter-term) shortcut-translations))
|
||||
sorted-filtered (sort filtered)
|
||||
trigger-ref (mf/use-ref nil)]
|
||||
|
||||
[:ul {:class (stl/css :sub-menu)
|
||||
:id (dm/str (d/name subsection-name) "-subsection")
|
||||
:hidden hidden}
|
||||
(for [command-translate sorted-filtered]
|
||||
(let [sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements))
|
||||
[command command-info] sc-by-translate
|
||||
content (or (:show-command command-info) (:command command-info))
|
||||
group-map (if (= section-key :basics)
|
||||
(merge (get custom-shortcuts :workspace)
|
||||
(get custom-shortcuts :dashboard)
|
||||
(get custom-shortcuts :viewer))
|
||||
(get custom-shortcuts section-key))
|
||||
group-map (if (map? group-map) group-map {})
|
||||
customized? (contains? group-map command)
|
||||
has-conflict? (contains? conflicts command)]
|
||||
(if editable?
|
||||
[:> shortcut-row-editable* {:elements elements
|
||||
:custom-shortcuts custom-shortcuts
|
||||
:section-key section-key
|
||||
:key command-translate
|
||||
:command-translate command-translate
|
||||
:conflicts conflicts}]
|
||||
[:li {:class (stl/css-case :shortcuts-name true
|
||||
:customized customized?)
|
||||
:data-customized (str customized?)
|
||||
:data-conflict (str has-conflict?)
|
||||
:aria-label command-translate
|
||||
:key command-translate}
|
||||
[:span {:class (stl/css :command-name)
|
||||
:id (dm/str command-translate "-label")}
|
||||
command-translate]
|
||||
[:div {:class (stl/css :shortcut-actions)
|
||||
:aria-labelledby (dm/str command-translate "-label")}
|
||||
(if (and customized? (str/blank? content))
|
||||
[:> tooltip* {:content (tr "shortcuts.disabled-shortcut")
|
||||
:trigger-ref trigger-ref
|
||||
:id (dm/str command-translate "-tooltip")
|
||||
:class (stl/css :option-text)}
|
||||
[:> icon* {:icon-id i/broken-link
|
||||
:aria-labelledby (dm/str command-translate "-tooltip")
|
||||
:size "s"
|
||||
:ref trigger-ref
|
||||
:class (stl/css :shortcut-detach-icon)}]]
|
||||
(if has-conflict?
|
||||
(let [conflicting-key (get conflicts command)
|
||||
conflict-name (translation-keyname :sc conflicting-key)]
|
||||
[:> tooltip* {:content (tr "shortcuts.overwritten-by" conflict-name)
|
||||
:id (dm/str (name command) "-conflict-tooltip")
|
||||
:class (stl/css :option-text)}
|
||||
[:> shortcuts-keys* {:content content
|
||||
:command command
|
||||
:is-customized customized?
|
||||
:has-conflict? has-conflict?}]])
|
||||
[:> shortcuts-keys* {:content content
|
||||
:command command
|
||||
:is-customized customized?
|
||||
:has-conflict? has-conflict?}]))]])))]))
|
||||
|
||||
|
||||
(mf/defc section-title*
|
||||
[{:keys [name is-visible is-sub on-click]}]
|
||||
(let [handle-key-down
|
||||
(fn [event]
|
||||
(when (or (= "Enter" (.-key event))
|
||||
(= " " (.-key event)))
|
||||
(.preventDefault event)
|
||||
(on-click event)))]
|
||||
[:button {:class (if is-sub
|
||||
(stl/css :subsection-title)
|
||||
(stl/css :section-title))
|
||||
:on-click on-click
|
||||
:on-key-down handle-key-down
|
||||
:aria-expanded is-visible
|
||||
:aria-controls (dm/str (str/lower name) (if is-sub "-subsection" "-section"))}
|
||||
[:> icon* {:icon-id (if is-visible i/arrow-down i/arrow-right)
|
||||
:size "s"
|
||||
:aria-hidden true}]
|
||||
[:span {:class (if is-sub
|
||||
(stl/css :subsection-name)
|
||||
(stl/css :section-name))} name]]))
|
||||
|
||||
(mf/defc shortcut-subsection*
|
||||
[{:keys [subsections manage-sections filter-term is-match-section open-sections
|
||||
editable? custom-shortcuts section-key conflicts hidden]}]
|
||||
(if (= :none (first (keys subsections)))
|
||||
[:> shortcut-row* {:elements (:children (:none subsections))
|
||||
:filter-term filter-term
|
||||
:is-match-section is-match-section
|
||||
:is-match-subsection true
|
||||
:editable? editable?
|
||||
:hidden hidden
|
||||
:custom-shortcuts custom-shortcuts
|
||||
:section-key section-key
|
||||
:conflicts conflicts}]
|
||||
|
||||
[:ul {:class (stl/css :subsection-menu)
|
||||
:hidden hidden
|
||||
:id (dm/str (name section-key) "-section")}
|
||||
(let [sorted-subs (sort-by (fn [[_ v]] (:translation v)) subsections)]
|
||||
(for [[sub-name sub-info] sorted-subs]
|
||||
(let [visible? (some #(= % (:id sub-info)) open-sections)]
|
||||
[:li {:key (name sub-name)}
|
||||
[:h4
|
||||
[:> section-title* {:name (:translation sub-info)
|
||||
:is-visible visible?
|
||||
:is-sub true
|
||||
:on-click (manage-sections (:id sub-info))}]]
|
||||
|
||||
[:> shortcut-row* {:elements (:children sub-info)
|
||||
:subsection-name sub-name
|
||||
:filter-term filter-term
|
||||
:is-match-section is-match-section
|
||||
:is-match-subsection true
|
||||
:editable? editable?
|
||||
:hidden (not visible?)
|
||||
:custom-shortcuts custom-shortcuts
|
||||
:section-key section-key
|
||||
:conflicts conflicts}]])))]))
|
||||
|
||||
|
||||
|
||||
(mf/defc shortcut-section*
|
||||
[{:keys [section manage-sections open-sections filter-term
|
||||
editable? custom-shortcuts conflicts]}]
|
||||
(let [[section-key section-info] section
|
||||
section-id (:id section-info)
|
||||
section-translation (:translation section-info)
|
||||
subsections (:children section-info)
|
||||
visible? (some #(= % section-id) open-sections)]
|
||||
[:div {:class (stl/css :section)}
|
||||
[:h3 {:class (stl/css :section-header)}
|
||||
[:> section-title* {:name section-translation
|
||||
:is-visible visible?
|
||||
:is-sub false
|
||||
:on-click (manage-sections section-id)}]]
|
||||
|
||||
[:> shortcut-subsection* {:subsections subsections
|
||||
:open-sections open-sections
|
||||
:manage-sections manage-sections
|
||||
:is-match-section false
|
||||
:filter-term filter-term
|
||||
:editable? editable?
|
||||
:hidden (not visible?)
|
||||
:custom-shortcuts custom-shortcuts
|
||||
:section-key section-key
|
||||
:conflicts conflicts}]]))
|
||||
214
frontend/src/app/main/ui/shortcuts.scss
Normal file
214
frontend/src/app/main/ui/shortcuts.scss
Normal file
@ -0,0 +1,214 @@
|
||||
// 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 Sucursal en España SL
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
@use "ds/mixins.scss" as *;
|
||||
@use "ds/typography.scss" as t;
|
||||
|
||||
.section {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.subsection-title {
|
||||
@include t.use-typography("title-small");
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: var(--sp-s) 0;
|
||||
color: var(--color-foreground-secondary);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
.subsection-name,
|
||||
.section-name {
|
||||
padding-inline-start: var(--sp-xs);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-title {
|
||||
text-transform: none;
|
||||
padding-inline-start: var(--sp-m);
|
||||
}
|
||||
|
||||
.subsection-menu {
|
||||
margin-block-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
margin-block-end: var(--sp-s);
|
||||
}
|
||||
|
||||
.shortcuts-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
inline-size: 100%;
|
||||
min-block-size: $sz-32;
|
||||
padding: px2rem(6);
|
||||
margin-block-end: var(--sp-s);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
}
|
||||
|
||||
.command-name {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
margin-inline-start: var(--sp-xxs);
|
||||
color: var(--color-foreground-primary);
|
||||
flex: 1;
|
||||
min-inline-size: 0;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
// Editable rows
|
||||
|
||||
.shortcuts-name-editable {
|
||||
inline-size: 100%;
|
||||
min-block-size: $sz-32;
|
||||
padding: px2rem(6);
|
||||
margin-block-end: var(--sp-s);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-quaternary);
|
||||
}
|
||||
}
|
||||
|
||||
.shortcuts-editing {
|
||||
background-color: var(--color-background-quaternary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-s);
|
||||
|
||||
&:focus-within {
|
||||
border: $b-1 solid var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-button-editable {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.shortcut-button-editing {
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.shortcut-editing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-s);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.shortcut-recording-area {
|
||||
inline-size: 100%;
|
||||
padding: px2rem(24);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color-background-tertiary);
|
||||
border-radius: $br-8;
|
||||
border: $b-1 dashed var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.shortcut-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xxs);
|
||||
}
|
||||
|
||||
.edit-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: opacity 0.15s ease;
|
||||
justify-content: space-between;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.confirmation-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.keys {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--sp-xxs);
|
||||
}
|
||||
|
||||
.key {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-transform: capitalize;
|
||||
block-size: px2rem(20);
|
||||
padding: px2rem(2) px2rem(6);
|
||||
border-radius: $br-6;
|
||||
color: var(--color-foreground-primary);
|
||||
background-color: var(--color-background-primary);
|
||||
}
|
||||
|
||||
.customized-key {
|
||||
background-color: var(--color-background-info);
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.conflict-key {
|
||||
background-color: var(--color-background-error);
|
||||
color: var(--color-foreground-error);
|
||||
}
|
||||
|
||||
.space {
|
||||
margin: 0 var(--sp-xxs);
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.shortcut-placeholder {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.shortcut-recorded-keys {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--sp-xxs);
|
||||
}
|
||||
|
||||
.shortcut-detach-icon {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.recording-ellipsis {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
@ -406,7 +406,7 @@
|
||||
(when (not (dom/fullscreen?))
|
||||
(st/emit! (dv/exit-fullscreen)))))]
|
||||
|
||||
(hooks/use-shortcuts ::viewer sc/shortcuts)
|
||||
(hooks/use-shortcuts ::viewer sc/shortcuts :viewer)
|
||||
(when (nil? page)
|
||||
(ex/raise :type :not-found))
|
||||
|
||||
|
||||
@ -788,7 +788,7 @@
|
||||
(combine-groups-with-resolved color-tokens)))]
|
||||
|
||||
(mf/with-effect []
|
||||
(st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts)))
|
||||
(st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts :workspace)))
|
||||
(fn []
|
||||
(st/emit! (dsc/pop-shortcuts ::colorpicker))
|
||||
(when (and @dirty? @last-change on-close)
|
||||
|
||||
@ -56,9 +56,14 @@
|
||||
|
||||
(mf/defc menu-entry*
|
||||
{::mf/private true}
|
||||
[{:keys [title shortcut on-click on-pointer-enter on-pointer-leave
|
||||
[{:keys [title shortcut shortcut-key on-click on-pointer-enter on-pointer-leave
|
||||
on-unmount children is-selected icon disabled value]}]
|
||||
(let [submenu-ref (mf/use-ref nil)
|
||||
(let [cs (mf/deref refs/custom-shortcuts)
|
||||
customized? (and shortcut-key
|
||||
(let [c (get-in cs [:workspace shortcut-key])]
|
||||
(and c (not= c ""))))
|
||||
|
||||
submenu-ref (mf/use-ref nil)
|
||||
hovering? (mf/use-ref false)
|
||||
|
||||
on-click'
|
||||
@ -128,7 +133,8 @@
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [[idx sc] (d/enumerate (scd/split-sc shortcut))]
|
||||
[:span {:key (dm/str shortcut "-" idx)
|
||||
:class (stl/css :shortcut-key)} sc])])
|
||||
:class (stl/css-case :shortcut-key true
|
||||
:customized-key customized?)} sc])])
|
||||
|
||||
(when (> (count children) 1)
|
||||
[:span {:class (stl/css :submenu-icon)}
|
||||
@ -149,7 +155,10 @@
|
||||
(mf/defc context-menu-edit*
|
||||
{::mf/private true}
|
||||
[{:keys [shapes]}]
|
||||
(let [multiple? (> (count shapes) 1)
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
multiple? (> (count shapes) 1)
|
||||
|
||||
do-copy #(st/emit! (dw/copy-selected))
|
||||
do-copy-link #(st/emit! (dw/copy-link-to-clipboard))
|
||||
@ -207,19 +216,24 @@
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-id")
|
||||
:on-click do-copy-id}])
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy")
|
||||
:shortcut (sc/get-tooltip :copy)
|
||||
:shortcut (get-tt :copy)
|
||||
:shortcut-key :copy
|
||||
:on-click do-copy}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-link")
|
||||
:shortcut (sc/get-tooltip :copy-link)
|
||||
:shortcut (get-tt :copy-link)
|
||||
:shortcut-key :copy-link
|
||||
:on-click do-copy-link}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.cut")
|
||||
:shortcut (sc/get-tooltip :cut)
|
||||
:shortcut (get-tt :cut)
|
||||
:shortcut-key :cut
|
||||
:on-click do-cut}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.paste")
|
||||
:shortcut (sc/get-tooltip :paste)
|
||||
:shortcut (get-tt :paste)
|
||||
:shortcut-key :paste
|
||||
:on-click do-paste}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.duplicate")
|
||||
:shortcut (sc/get-tooltip :duplicate)
|
||||
:shortcut (get-tt :duplicate)
|
||||
:shortcut-key :duplicate
|
||||
:on-click do-duplicate}]
|
||||
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-paste-as")
|
||||
@ -242,11 +256,13 @@
|
||||
:on-click handle-copy-text}]
|
||||
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-props")
|
||||
:shortcut (sc/get-tooltip :copy-props)
|
||||
:shortcut (get-tt :copy-props)
|
||||
:shortcut-key :copy-props
|
||||
:disabled multiple?
|
||||
:on-click handle-copy-props}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.paste-props")
|
||||
:shortcut (sc/get-tooltip :paste-props)
|
||||
:shortcut (get-tt :paste-props)
|
||||
:shortcut-key :paste-props
|
||||
:disabled (and (cf/check-browser? :chrome) (not @enabled-paste-props*))
|
||||
:on-click handle-paste-props}]]
|
||||
|
||||
@ -255,7 +271,10 @@
|
||||
(mf/defc context-menu-layer-position*
|
||||
{::mf/private true}
|
||||
[{:keys [shapes]}]
|
||||
(let [do-bring-forward (mf/use-fn #(st/emit! (dw/vertical-order-selected :up)))
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
do-bring-forward (mf/use-fn #(st/emit! (dw/vertical-order-selected :up)))
|
||||
do-bring-to-front (mf/use-fn #(st/emit! (dw/vertical-order-selected :top)))
|
||||
do-send-backward (mf/use-fn #(st/emit! (dw/vertical-order-selected :down)))
|
||||
do-send-to-back (mf/use-fn #(st/emit! (dw/vertical-order-selected :bottom)))
|
||||
@ -284,16 +303,20 @@
|
||||
:on-unmount (on-unmount (:id object))
|
||||
:icon (usi/get-shape-icon object)}])])
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.forward")
|
||||
:shortcut (sc/get-tooltip :bring-forward)
|
||||
:shortcut (get-tt :bring-forward)
|
||||
:shortcut-key :bring-forward
|
||||
:on-click do-bring-forward}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.front")
|
||||
:shortcut (sc/get-tooltip :bring-front)
|
||||
:shortcut (get-tt :bring-front)
|
||||
:shortcut-key :bring-front
|
||||
:on-click do-bring-to-front}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.backward")
|
||||
:shortcut (sc/get-tooltip :bring-backward)
|
||||
:shortcut (get-tt :bring-backward)
|
||||
:shortcut-key :bring-backward
|
||||
:on-click do-send-backward}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.back")
|
||||
:shortcut (sc/get-tooltip :bring-back)
|
||||
:shortcut (get-tt :bring-back)
|
||||
:shortcut-key :bring-back
|
||||
:on-click do-send-to-back}]
|
||||
|
||||
[:> menu-separator* {}]]))
|
||||
@ -301,22 +324,30 @@
|
||||
(mf/defc context-menu-flip*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [do-flip-vertical #(st/emit! (dw/flip-vertical-selected))
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
do-flip-vertical #(st/emit! (dw/flip-vertical-selected))
|
||||
do-flip-horizontal #(st/emit! (dw/flip-horizontal-selected))]
|
||||
[:*
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.flip-vertical")
|
||||
:shortcut (sc/get-tooltip :flip-vertical)
|
||||
:shortcut (get-tt :flip-vertical)
|
||||
:shortcut-key :flip-vertical
|
||||
:on-click do-flip-vertical}]
|
||||
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.flip-horizontal")
|
||||
:shortcut (sc/get-tooltip :flip-horizontal)
|
||||
:shortcut (get-tt :flip-horizontal)
|
||||
:shortcut-key :flip-horizontal
|
||||
:on-click do-flip-horizontal}]
|
||||
[:> menu-separator* {}]]))
|
||||
|
||||
(mf/defc context-menu-thumbnail*
|
||||
{::mf/private true}
|
||||
[{:keys [shapes]}]
|
||||
(let [single? (= (count shapes) 1)
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
single? (= (count shapes) 1)
|
||||
has-frame? (some cfh/frame-shape? shapes)
|
||||
do-toggle-thumbnail #(st/emit! (dw/toggle-file-thumbnail-selected))]
|
||||
(when (and single? has-frame?)
|
||||
@ -325,25 +356,33 @@
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.thumbnail-remove")
|
||||
:on-click do-toggle-thumbnail}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.thumbnail-set")
|
||||
:shortcut (sc/get-tooltip :thumbnail-set)
|
||||
:shortcut (get-tt :thumbnail-set)
|
||||
:shortcut-key :thumbnail-set
|
||||
:on-click do-toggle-thumbnail}])
|
||||
[:> menu-separator* {}]])))
|
||||
|
||||
(mf/defc context-menu-rename*
|
||||
{::mf/private true}
|
||||
[{:keys [shapes]}]
|
||||
(let [do-rename #(st/emit! (dw/start-rename-selected))]
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
do-rename #(st/emit! (dw/start-rename-selected))]
|
||||
(when (= (count shapes) 1)
|
||||
[:*
|
||||
[:> menu-separator* {}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.rename")
|
||||
:shortcut (sc/get-tooltip :rename)
|
||||
:shortcut (get-tt :rename)
|
||||
:shortcut-key :rename
|
||||
:on-click do-rename}]])))
|
||||
|
||||
(mf/defc context-menu-group*
|
||||
{::mf/private true}
|
||||
[{:keys [shapes]}]
|
||||
(let [multiple? (> (count shapes) 1)
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
multiple? (> (count shapes) 1)
|
||||
single? (= (count shapes) 1)
|
||||
|
||||
objects (deref refs/workspace-page-objects)
|
||||
@ -371,44 +410,56 @@
|
||||
[:*
|
||||
(when (or has-bool? has-group? has-mask? has-frame?)
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.ungroup")
|
||||
:shortcut (sc/get-tooltip :ungroup)
|
||||
:shortcut (get-tt :ungroup)
|
||||
:shortcut-key :ungroup
|
||||
:on-click do-remove-group}])
|
||||
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.group")
|
||||
:shortcut (sc/get-tooltip :group)
|
||||
:shortcut (get-tt :group)
|
||||
:shortcut-key :group
|
||||
:on-click do-create-group}]
|
||||
|
||||
(when (or multiple? (and is-group? (not has-mask?)) is-bool?)
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.mask")
|
||||
:shortcut (sc/get-tooltip :mask)
|
||||
:shortcut (get-tt :mask)
|
||||
:shortcut-key :mask
|
||||
:on-click do-mask-group}])
|
||||
|
||||
(when has-mask?
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.unmask")
|
||||
:shortcut (sc/get-tooltip :unmask)
|
||||
:shortcut (get-tt :unmask)
|
||||
:shortcut-key :unmask
|
||||
:on-click do-unmask-group}])
|
||||
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.create-artboard-from-selection")
|
||||
:shortcut (sc/get-tooltip :artboard-selection)
|
||||
:shortcut (get-tt :artboard-selection)
|
||||
:shortcut-key :artboard-selection
|
||||
:on-click do-create-artboard-from-selection}]
|
||||
[:> menu-separator* {}]])]))
|
||||
|
||||
(mf/defc context-focus-mode-menu*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [focus (mf/deref refs/workspace-focus-selected)
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
focus (mf/deref refs/workspace-focus-selected)
|
||||
do-toggle-focus-mode #(st/emit! (dw/toggle-focus-mode))]
|
||||
|
||||
[:> menu-entry* {:title (if (empty? focus)
|
||||
(tr "workspace.focus.focus-on")
|
||||
(tr "workspace.focus.focus-off"))
|
||||
:shortcut (sc/get-tooltip :toggle-focus-mode)
|
||||
:shortcut (get-tt :toggle-focus-mode)
|
||||
:shortcut-key :toggle-focus-mode
|
||||
:on-click do-toggle-focus-mode}]))
|
||||
|
||||
(mf/defc context-menu-path*
|
||||
{::mf/private true}
|
||||
[{:keys [shapes objects disable-flatten disable-booleans]}]
|
||||
(let [multiple? (> (count shapes) 1)
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
multiple? (> (count shapes) 1)
|
||||
single? (= (count shapes) 1)
|
||||
|
||||
has-group? (->> shapes (d/seek cfh/group-shape?))
|
||||
@ -446,7 +497,8 @@
|
||||
[:*
|
||||
(when (and single? (not is-frame?))
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.edit")
|
||||
:shortcut (sc/get-tooltip :start-editing)
|
||||
:shortcut (get-tt :start-editing)
|
||||
:shortcut-key :start-editing
|
||||
:on-click do-start-editing}])
|
||||
|
||||
(when-not (or disable-flatten has-frame? has-path?)
|
||||
@ -463,16 +515,20 @@
|
||||
(or multiple? (and single? (or is-group? is-bool?))))
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.path")}
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.union")
|
||||
:shortcut (sc/get-tooltip :bool-union)
|
||||
:shortcut (get-tt :bool-union)
|
||||
:shortcut-key :bool-union
|
||||
:on-click (make-do-bool :union)}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.difference")
|
||||
:shortcut (sc/get-tooltip :bool-difference)
|
||||
:shortcut (get-tt :bool-difference)
|
||||
:shortcut-key :bool-difference
|
||||
:on-click (make-do-bool :difference)}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.intersection")
|
||||
:shortcut (sc/get-tooltip :bool-intersection)
|
||||
:shortcut (get-tt :bool-intersection)
|
||||
:shortcut-key :bool-intersection
|
||||
:on-click (make-do-bool :intersection)}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.exclude")
|
||||
:shortcut (sc/get-tooltip :bool-exclude)
|
||||
:shortcut (get-tt :bool-exclude)
|
||||
:shortcut-key :bool-exclude
|
||||
:on-click (make-do-bool :exclude)}]
|
||||
|
||||
(when (and single? is-bool? (not disable-flatten))
|
||||
@ -484,7 +540,10 @@
|
||||
(mf/defc context-menu-layer-options*
|
||||
{::mf/private true}
|
||||
[{:keys [shapes]}]
|
||||
(let [ids (mapv :id shapes)
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
ids (mapv :id shapes)
|
||||
do-show-shape #(st/emit! (dw/update-shape-flags ids {:hidden false}))
|
||||
do-hide-shape #(st/emit! (dw/update-shape-flags ids {:hidden true}))
|
||||
do-lock-shape #(st/emit! (dw/update-shape-flags ids {:blocked true}))
|
||||
@ -492,18 +551,22 @@
|
||||
[:*
|
||||
(if (every? :hidden shapes)
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.show")
|
||||
:shortcut (sc/get-tooltip :toggle-visibility)
|
||||
:shortcut (get-tt :toggle-visibility)
|
||||
:shortcut-key :toggle-visibility
|
||||
:on-click do-show-shape}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.hide")
|
||||
:shortcut (sc/get-tooltip :toggle-visibility)
|
||||
:shortcut (get-tt :toggle-visibility)
|
||||
:shortcut-key :toggle-visibility
|
||||
:on-click do-hide-shape}])
|
||||
|
||||
(if (every? :blocked shapes)
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.unlock")
|
||||
:shortcut (sc/get-tooltip :toggle-lock)
|
||||
:shortcut (get-tt :toggle-lock)
|
||||
:shortcut-key :toggle-lock
|
||||
:on-click do-unlock-shape}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.lock")
|
||||
:shortcut (sc/get-tooltip :toggle-lock)
|
||||
:shortcut (get-tt :toggle-lock)
|
||||
:shortcut-key :toggle-lock
|
||||
:on-click do-lock-shape}])]))
|
||||
|
||||
(mf/defc context-menu-prototype*
|
||||
@ -530,7 +593,10 @@
|
||||
(mf/defc context-menu-layout*
|
||||
{::mf/private true}
|
||||
[{:keys [shapes]}]
|
||||
(let [single? (= (count shapes) 1)
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
single? (= (count shapes) 1)
|
||||
objects (deref refs/workspace-page-objects)
|
||||
any-in-copy? (some true? (map #(ctn/has-any-copy-parent? objects %) shapes))
|
||||
|
||||
@ -565,28 +631,35 @@
|
||||
[:> menu-separator* {}]
|
||||
(if has-flex?
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.remove-flex")
|
||||
:shortcut (sc/get-tooltip :toggle-layout-flex)
|
||||
:shortcut (get-tt :toggle-layout-flex)
|
||||
:shortcut-key :toggle-layout-flex
|
||||
:on-click on-remove-layout}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.remove-grid")
|
||||
:shortcut (sc/get-tooltip :toggle-layout-grid)
|
||||
:shortcut (get-tt :toggle-layout-grid)
|
||||
:shortcut-key :toggle-layout-grid
|
||||
:on-click on-remove-layout}])]
|
||||
|
||||
(when (or single? (not any-is-variant?))
|
||||
[:div
|
||||
[:> menu-separator* {}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.add-flex")
|
||||
:shortcut (sc/get-tooltip :toggle-layout-flex)
|
||||
:shortcut (get-tt :toggle-layout-flex)
|
||||
:shortcut-key :toggle-layout-flex
|
||||
:value "flex"
|
||||
:on-click on-add-layout}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.add-grid")
|
||||
:shortcut (sc/get-tooltip :toggle-layout-grid)
|
||||
:shortcut (get-tt :toggle-layout-grid)
|
||||
:shortcut-key :toggle-layout-grid
|
||||
:value "grid"
|
||||
:on-click on-add-layout}]])))]))
|
||||
|
||||
(mf/defc context-menu-component*
|
||||
{:mf/private true}
|
||||
[{:keys [shapes]}]
|
||||
(let [single? (= (count shapes) 1)
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
single? (= (count shapes) 1)
|
||||
objects (deref refs/workspace-page-objects)
|
||||
can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) shapes))
|
||||
components-menu-entries (cmm/generate-components-menu-entries shapes)
|
||||
@ -608,7 +681,8 @@
|
||||
[:> menu-separator* {}]
|
||||
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.create-component")
|
||||
:shortcut (sc/get-tooltip :create-component-variant)
|
||||
:shortcut (get-tt :create-component-variant)
|
||||
:shortcut-key :create-component-variant
|
||||
:on-click do-add-component}]
|
||||
(when (not single?)
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.create-multiple-components")
|
||||
@ -618,17 +692,19 @@
|
||||
[:*
|
||||
[:> menu-separator*]
|
||||
(for [entry (filter some? components-menu-entries)]
|
||||
[:> menu-entry* {:key (:title entry)
|
||||
:title (:title entry)
|
||||
:shortcut (when (contains? entry :shortcut)
|
||||
(sc/get-tooltip (:shortcut entry)))
|
||||
:on-click (:action entry)}])])
|
||||
(let [sc-key (:shortcut entry)]
|
||||
[:> menu-entry* {:key (:title entry)
|
||||
:title (:title entry)
|
||||
:shortcut (when sc-key (get-tt sc-key))
|
||||
:shortcut-key sc-key
|
||||
:on-click (:action entry)}]))])
|
||||
|
||||
(when variant-container?
|
||||
[:*
|
||||
[:> menu-separator*]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.add-variant")
|
||||
:shortcut (sc/get-tooltip :create-component-variant)
|
||||
:shortcut (get-tt :create-component-variant)
|
||||
:shortcut-key :create-component-variant
|
||||
:on-click do-add-variant}]])
|
||||
|
||||
(when (and (not single?) all-main? (not any-variant?))
|
||||
@ -659,11 +735,15 @@
|
||||
(mf/defc context-menu-delete*
|
||||
{::mf/private true}
|
||||
[]
|
||||
(let [do-delete #(st/emit! (dw/delete-selected))]
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
do-delete #(st/emit! (dw/delete-selected))]
|
||||
[:*
|
||||
[:> menu-separator* {}]
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.delete")
|
||||
:shortcut (sc/get-tooltip :delete)
|
||||
:shortcut (get-tt :delete)
|
||||
:shortcut-key :delete
|
||||
:on-click do-delete}]]))
|
||||
|
||||
(mf/defc shape-context-menu*
|
||||
@ -741,7 +821,10 @@
|
||||
|
||||
(mf/defc viewport-context-menu*
|
||||
[]
|
||||
(let [focus (mf/deref refs/workspace-focus-selected)
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
focus (mf/deref refs/workspace-focus-selected)
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
do-paste #(st/emit! (dw/paste-from-clipboard))
|
||||
do-hide-ui #(st/emit! (-> (dw/toggle-layout-flag :hide-ui)
|
||||
@ -750,15 +833,18 @@
|
||||
[:*
|
||||
(when-not ^boolean read-only?
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.paste")
|
||||
:shortcut (sc/get-tooltip :paste)
|
||||
:shortcut (get-tt :paste)
|
||||
:shortcut-key :paste
|
||||
:on-click do-paste}])
|
||||
[:> menu-entry* {:title (tr "workspace.shape.menu.hide-ui")
|
||||
:shortcut (sc/get-tooltip :hide-ui)
|
||||
:shortcut (get-tt :hide-ui)
|
||||
:shortcut-key :hide-ui
|
||||
:on-click do-hide-ui}]
|
||||
|
||||
(when (d/not-empty? focus)
|
||||
[:> menu-entry* {:title (tr "workspace.focus.focus-off")
|
||||
:shortcut (sc/get-tooltip :toggle-focus-mode)
|
||||
:shortcut (get-tt :toggle-focus-mode)
|
||||
:shortcut-key :toggle-focus-mode
|
||||
:on-click do-toggle-focus-mode}])]))
|
||||
|
||||
(mf/defc grid-track-context-menu*
|
||||
|
||||
@ -21,8 +21,8 @@
|
||||
width: deprecated.$s-240;
|
||||
padding: deprecated.$s-4;
|
||||
border-radius: deprecated.$br-8;
|
||||
border: deprecated.$s-2 solid var(--panel-border-color);
|
||||
background-color: var(--menu-background-color);
|
||||
border: deprecated.$s-2 solid var(--color-background-quaternary);
|
||||
background-color: var(--color-background-tertiary);
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@ -36,6 +36,12 @@
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
--context-menu-item-bg-color: var(--color-background-tertiary);
|
||||
--context-menu-item-title-color: var(--color-foreground-primary);
|
||||
--context-menu-item-shortcut-color: var(--color-foreground-secondary);
|
||||
--context-menu-item-shortcut-bg-color: var(--color-background-primary);
|
||||
--context-menu-item-shortcut-border-color: transparent;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@ -44,54 +50,50 @@
|
||||
padding: deprecated.$s-6;
|
||||
border-radius: deprecated.$br-8;
|
||||
cursor: pointer;
|
||||
|
||||
.title {
|
||||
@include deprecated.body-small-typography;
|
||||
|
||||
color: var(--menu-foreground-color);
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
@include deprecated.flex-center;
|
||||
|
||||
gap: deprecated.$s-2;
|
||||
color: var(--menu-shortcut-foreground-color);
|
||||
|
||||
.shortcut-key {
|
||||
@include deprecated.body-small-typography;
|
||||
@include deprecated.flex-center;
|
||||
|
||||
height: deprecated.$s-20;
|
||||
padding: deprecated.$s-2 deprecated.$s-6;
|
||||
border-radius: deprecated.$br-6;
|
||||
background-color: var(--menu-shortcut-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-icon svg {
|
||||
@extend %button-icon-small;
|
||||
|
||||
stroke: var(--menu-foreground-color);
|
||||
}
|
||||
background-color: var(--context-menu-item-bg-color);
|
||||
border: 1px solid var(--context-menu-item-shortcut-border-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--menu-background-color-hover);
|
||||
|
||||
.title {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
color: var(--menu-shortcut-foreground-color-hover);
|
||||
}
|
||||
--context-menu-item-title-color: var(--menu-foreground-color-hover);
|
||||
--context-menu-item-bg-color: var(--menu-background-color-hover);
|
||||
--context-menu-item-shortcut-color: var(--menu-shortcut-foreground-color-hover);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: 1px solid var(--menu-border-color-focus);
|
||||
background-color: var(--menu-background-color-focus);
|
||||
--context-menu-item-bg-color: var(--menu-background-color-focus);
|
||||
--context-menu-item-shortcut-border-color: var(--menu-border-color-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@include deprecated.body-small-typography;
|
||||
|
||||
color: var(--context-menu-item-title-color);
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
@include deprecated.flex-center;
|
||||
|
||||
gap: deprecated.$s-2;
|
||||
color: var(--context-menu-item-shortcut-color);
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
@include deprecated.body-small-typography;
|
||||
@include deprecated.flex-center;
|
||||
|
||||
height: deprecated.$s-20;
|
||||
padding: deprecated.$s-2 deprecated.$s-6;
|
||||
border-radius: deprecated.$br-6;
|
||||
background-color: var(--context-menu-item-shortcut-bg-color);
|
||||
}
|
||||
|
||||
.submenu-icon svg {
|
||||
@extend %button-icon-small;
|
||||
|
||||
stroke: var(--menu-foreground-color);
|
||||
}
|
||||
|
||||
.icon-menu-item {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
@ -165,3 +167,8 @@
|
||||
border: deprecated.$s-2 solid var(--menu-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.customized-key {
|
||||
background-color: var(--color-background-info);
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
@ -51,11 +51,15 @@
|
||||
(mf/defc shortcuts*
|
||||
{::mf/private true}
|
||||
[{:keys [id]}]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip id))]
|
||||
[:span {:class (stl/css :shortcut-key)
|
||||
:key sc}
|
||||
sc])])
|
||||
(let [custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
customized? (let [c (get-in custom-shortcuts [:workspace id])]
|
||||
(and c (not= c "")))]
|
||||
[:span {:class (stl/css :shortcut)}
|
||||
(for [sc (scd/split-sc (sc/get-effective-tooltip id custom-shortcuts))]
|
||||
[:span {:class (stl/css-case :shortcut-key true
|
||||
:customized-key customized?)
|
||||
:key sc}
|
||||
sc])]))
|
||||
|
||||
(mf/defc help-info-menu*
|
||||
{::mf/private true
|
||||
@ -982,6 +986,19 @@
|
||||
::ev/origin "workspace:menu"})
|
||||
(modal/show :plugin-management {}))))
|
||||
|
||||
show-shortcuts
|
||||
(mf/use-fn
|
||||
(mf/deps layout)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(reset! show-menu* false)
|
||||
(reset! selected-sub-menu* nil)
|
||||
(when (contains? layout :collapse-left-sidebar)
|
||||
(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
|
||||
(st/emit!
|
||||
(-> (dw/toggle-layout-flag :shortcuts)
|
||||
(vary-meta assoc ::ev/origin "workspace-menu")))))
|
||||
|
||||
subscription (:subscription (:props profile))
|
||||
subscription-type (get-subscription-type subscription)]
|
||||
|
||||
@ -1148,6 +1165,7 @@
|
||||
:toggle-flag toggle-flag
|
||||
:toggle-theme toggle-theme
|
||||
:toggle-render toggle-render
|
||||
:show-shortcuts show-shortcuts
|
||||
:on-close close-sub-menu}]
|
||||
|
||||
:plugins
|
||||
|
||||
@ -182,3 +182,8 @@
|
||||
border-radius: $br-6;
|
||||
background-color: var(--menu-shortcut-background-color);
|
||||
}
|
||||
|
||||
.customized-key {
|
||||
background-color: var(--color-background-info);
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
@ -51,9 +51,12 @@
|
||||
|
||||
(mf/defc palette*
|
||||
[{:keys [layout on-change-size]}]
|
||||
(let [color-palette? (:colorpalette layout)
|
||||
text-palette? (:textpalette layout)
|
||||
hide-palettes? (:hide-palettes layout)
|
||||
(let [color-palette? (:colorpalette layout)
|
||||
text-palette? (:textpalette layout)
|
||||
hide-palettes? (:hide-palettes layout)
|
||||
|
||||
custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
container (mf/use-ref nil)
|
||||
@ -179,16 +182,16 @@
|
||||
[:ul {:class (dm/str size-classname " " (stl/css-case :palette-btn-list true
|
||||
:hidden-bts hide-palettes?))}
|
||||
[:li {:class (stl/css :palette-item)}
|
||||
[:button {:title (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette))
|
||||
:aria-label (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette))
|
||||
[:button {:title (tr "workspace.toolbar.color-palette" (get-tt :toggle-colorpalette))
|
||||
:aria-label (tr "workspace.toolbar.color-palette" (get-tt :toggle-colorpalette))
|
||||
:class (stl/css-case :palette-btn true
|
||||
:selected color-palette?)
|
||||
:on-click on-select-color-palette}
|
||||
deprecated-icon/drop-icon]]
|
||||
|
||||
[:li {:class (stl/css :palette-item)}
|
||||
[:button {:title (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette))
|
||||
:aria-label (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette))
|
||||
[:button {:title (tr "workspace.toolbar.text-palette" (get-tt :toggle-textpalette))
|
||||
:aria-label (tr "workspace.toolbar.text-palette" (get-tt :toggle-textpalette))
|
||||
:class (stl/css-case :palette-btn true
|
||||
:selected text-palette?)
|
||||
:on-click on-select-text-palette}
|
||||
|
||||
@ -40,8 +40,10 @@
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/wrap-props false}
|
||||
[{:keys [zoom on-increase on-decrease on-zoom-reset on-zoom-fit on-zoom-selected]}]
|
||||
(let [open* (mf/use-state false)
|
||||
open? (deref open*)
|
||||
(let [open* (mf/use-state false)
|
||||
open? (deref open*)
|
||||
custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
open-dropdown
|
||||
(mf/use-fn
|
||||
@ -97,14 +99,14 @@
|
||||
:on-click on-zoom-fit}
|
||||
(tr "workspace.header.zoom-fit-all")
|
||||
[:span {:class (stl/css :shortcuts)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :fit-all))]
|
||||
(for [sc (scd/split-sc (get-tt :fit-all))]
|
||||
[:span {:class (stl/css :shortcut-key)
|
||||
:key (str "zoom-fit-" sc)} sc])]]
|
||||
[:li {:class (stl/css :zoom-option)
|
||||
:on-click on-zoom-selected}
|
||||
(tr "workspace.header.zoom-selected")
|
||||
[:span {:class (stl/css :shortcuts)}
|
||||
(for [sc (scd/split-sc (sc/get-tooltip :zoom-selected))]
|
||||
(for [sc (scd/split-sc (get-tt :zoom-selected))]
|
||||
[:span {:class (stl/css :shortcut-key)
|
||||
:key (str "zoom-selected-" sc)} sc])]]]]]))
|
||||
|
||||
@ -118,6 +120,9 @@
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
selected-drawtool (mf/deref refs/selected-drawing-tool)
|
||||
|
||||
custom-shortcuts (mf/deref refs/custom-shortcuts)
|
||||
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
|
||||
|
||||
on-increase (mf/use-fn #(st/emit! (dw/increase-zoom nil)))
|
||||
on-decrease (mf/use-fn #(st/emit! (dw/decrease-zoom nil)))
|
||||
on-zoom-reset (mf/use-fn #(st/emit! dw/reset-zoom))
|
||||
@ -211,8 +216,8 @@
|
||||
:on-zoom-selected on-zoom-selected}]]
|
||||
|
||||
[:div {:class (stl/css :comments-section)}
|
||||
[:button {:title (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment))
|
||||
:aria-label (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment))
|
||||
[:button {:title (tr "workspace.toolbar.comments" (get-tt :add-comment))
|
||||
:aria-label (tr "workspace.toolbar.comments" (get-tt :add-comment))
|
||||
:class (stl/css-case :comments-btn true
|
||||
:selected (= selected-drawtool :comments))
|
||||
:on-click toggle-comments
|
||||
@ -239,7 +244,7 @@
|
||||
deprecated-icon/share])
|
||||
|
||||
[:a {:class (stl/css :viewer-btn)
|
||||
:title (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer))
|
||||
:title (tr "workspace.header.viewer" (get-tt :open-viewer))
|
||||
:on-click nav-to-viewer}
|
||||
deprecated-icon/play]]))
|
||||
|
||||
|
||||
@ -253,7 +253,7 @@
|
||||
(on-select @selected))
|
||||
|
||||
(mf/with-effect []
|
||||
(st/emit! (dsc/push-shortcuts :typography {}))
|
||||
(st/emit! (dsc/push-shortcuts :typography {} :workspace))
|
||||
(fn []
|
||||
(st/emit! (dsc/pop-shortcuts :typography))))
|
||||
|
||||
|
||||
@ -10,450 +10,80 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.dashboard.shortcuts]
|
||||
[app.main.data.dashboard.shortcuts :as dsc]
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.viewer.shortcuts]
|
||||
[app.main.data.viewer.shortcuts :as vsc]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.path.shortcuts]
|
||||
[app.main.data.workspace.shortcuts]
|
||||
[app.main.data.workspace.path.shortcuts :as psc]
|
||||
[app.main.data.workspace.shortcuts :as wsc]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.button-link :as bl]
|
||||
[app.main.ui.components.search-bar :refer [search-bar*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
|
||||
[app.main.ui.shortcuts :as ss]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.strings :refer [matches-search]]
|
||||
[clojure.set :as set]
|
||||
[clojure.string]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc converted-chars*
|
||||
[{:keys [char command]}]
|
||||
(let [modified-keys {:up ds/up-arrow
|
||||
:down ds/down-arrow
|
||||
:left ds/left-arrow
|
||||
:right ds/right-arrow
|
||||
:plus "+"}
|
||||
macos-keys {:command "\u2318"
|
||||
:option "\u2325"
|
||||
:alt "\u2325"
|
||||
:delete "\u232B"
|
||||
:del "\u232B"
|
||||
:shift "\u21E7"
|
||||
:control "\u2303"
|
||||
:esc "\u238B"
|
||||
:enter "\u23CE"}
|
||||
is-macos? (cf/check-platform? :macos)
|
||||
char (if (contains? modified-keys (keyword char)) ((keyword char) modified-keys) char)
|
||||
char (if (and is-macos? (contains? macos-keys (keyword char))) ((keyword char) macos-keys) char)
|
||||
unique-key (str (d/name command) "-" char)]
|
||||
[:span {:class (stl/css :key)
|
||||
:key unique-key} char]))
|
||||
|
||||
(defn translation-keyname
|
||||
[type keyname]
|
||||
;; Execution time translation strings:
|
||||
(comment
|
||||
(tr "shortcut-subsection.alignment")
|
||||
(tr "shortcut-subsection.edit")
|
||||
(tr "shortcut-subsection.general-dashboard")
|
||||
(tr "shortcut-subsection.general-viewer")
|
||||
(tr "shortcut-subsection.main-menu")
|
||||
(tr "shortcut-subsection.modify-layers")
|
||||
(tr "shortcut-subsection.navigation-dashboard")
|
||||
(tr "shortcut-subsection.navigation-viewer")
|
||||
(tr "shortcut-subsection.navigation-workspace")
|
||||
(tr "shortcut-subsection.panels")
|
||||
(tr "shortcut-subsection.path-editor")
|
||||
(tr "shortcut-subsection.shape")
|
||||
(tr "shortcut-subsection.text-editor")
|
||||
(tr "shortcut-subsection.tools")
|
||||
(tr "shortcut-subsection.zoom-viewer")
|
||||
(tr "shortcut-subsection.zoom-workspace")
|
||||
(tr "shortcuts.add-comment")
|
||||
(tr "shortcuts.add-node")
|
||||
(tr "shortcuts.align-bottom")
|
||||
(tr "shortcuts.align-center")
|
||||
(tr "shortcuts.align-hcenter")
|
||||
(tr "shortcuts.align-justify")
|
||||
(tr "shortcuts.align-left")
|
||||
(tr "shortcuts.align-right")
|
||||
(tr "shortcuts.align-top")
|
||||
(tr "shortcuts.align-vcenter")
|
||||
(tr "shortcuts.artboard-selection")
|
||||
(tr "shortcuts.bold")
|
||||
(tr "shortcuts.bool-difference")
|
||||
(tr "shortcuts.bool-exclude")
|
||||
(tr "shortcuts.bool-intersection")
|
||||
(tr "shortcuts.bool-union")
|
||||
(tr "shortcuts.bring-back")
|
||||
(tr "shortcuts.bring-backward")
|
||||
(tr "shortcuts.bring-forward")
|
||||
(tr "shortcuts.bring-front")
|
||||
(tr "shortcuts.clear-undo")
|
||||
(tr "shortcuts.copy")
|
||||
(tr "shortcuts.copy-link")
|
||||
(tr "shortcuts.create-component-variant")
|
||||
(tr "shortcuts.create-new-project")
|
||||
(tr "shortcuts.cut")
|
||||
(tr "shortcuts.decrease-zoom")
|
||||
(tr "shortcuts.delete")
|
||||
(tr "shortcuts.delete-node")
|
||||
(tr "shortcuts.detach-component")
|
||||
(tr "shortcuts.draw-curve")
|
||||
(tr "shortcuts.draw-ellipse")
|
||||
(tr "shortcuts.draw-frame")
|
||||
(tr "shortcuts.draw-nodes")
|
||||
(tr "shortcuts.draw-path")
|
||||
(tr "shortcuts.draw-rect")
|
||||
(tr "shortcuts.draw-text")
|
||||
(tr "shortcuts.duplicate")
|
||||
(tr "shortcuts.escape")
|
||||
(tr "shortcuts.export-shapes")
|
||||
(tr "shortcuts.find")
|
||||
(tr "shortcuts.find-and-replace")
|
||||
(tr "shortcuts.fit-all")
|
||||
(tr "shortcuts.flip-horizontal")
|
||||
(tr "shortcuts.flip-vertical")
|
||||
(tr "shortcuts.font-size-dec")
|
||||
(tr "shortcuts.font-size-inc")
|
||||
(tr "shortcuts.go-to-drafts")
|
||||
(tr "shortcuts.go-to-libs")
|
||||
(tr "shortcuts.go-to-search")
|
||||
(tr "shortcuts.group")
|
||||
(tr "shortcuts.h-distribute")
|
||||
(tr "shortcuts.hide-ui")
|
||||
(tr "shortcuts.increase-zoom")
|
||||
(tr "shortcuts.insert-image")
|
||||
(tr "shortcuts.italic")
|
||||
(tr "shortcuts.join-nodes")
|
||||
(tr "shortcuts.line-through")
|
||||
(tr "shortcuts.make-corner")
|
||||
(tr "shortcuts.make-curve")
|
||||
(tr "shortcuts.mask")
|
||||
(tr "shortcuts.merge-nodes")
|
||||
(tr "shortcuts.move")
|
||||
(tr "shortcuts.move-fast-down")
|
||||
(tr "shortcuts.move-fast-left")
|
||||
(tr "shortcuts.move-fast-right")
|
||||
(tr "shortcuts.move-fast-up")
|
||||
(tr "shortcuts.move-nodes")
|
||||
(tr "shortcuts.move-unit-down")
|
||||
(tr "shortcuts.move-unit-left")
|
||||
(tr "shortcuts.move-unit-right")
|
||||
(tr "shortcuts.move-unit-up")
|
||||
(tr "shortcuts.next-frame")
|
||||
(tr "shortcuts.opacity-0")
|
||||
(tr "shortcuts.opacity-1")
|
||||
(tr "shortcuts.opacity-2")
|
||||
(tr "shortcuts.opacity-3")
|
||||
(tr "shortcuts.opacity-4")
|
||||
(tr "shortcuts.opacity-5")
|
||||
(tr "shortcuts.opacity-6")
|
||||
(tr "shortcuts.opacity-7")
|
||||
(tr "shortcuts.opacity-8")
|
||||
(tr "shortcuts.opacity-9")
|
||||
(tr "shortcuts.open-color-picker")
|
||||
(tr "shortcuts.open-comments")
|
||||
(tr "shortcuts.open-dashboard")
|
||||
(tr "shortcuts.open-inspect")
|
||||
(tr "shortcuts.open-interactions")
|
||||
(tr "shortcuts.open-viewer")
|
||||
(tr "shortcuts.open-workspace")
|
||||
(tr "shortcuts.paste")
|
||||
(tr "shortcuts.paste-replace")
|
||||
(tr "shortcuts.prev-frame")
|
||||
(tr "shortcuts.redo")
|
||||
(tr "shortcuts.rename")
|
||||
(tr "shortcuts.reset-zoom")
|
||||
(tr "shortcuts.scale")
|
||||
(tr "shortcuts.search-placeholder")
|
||||
(tr "shortcuts.select-all")
|
||||
(tr "shortcuts.select-next")
|
||||
(tr "shortcuts.select-parent-layer")
|
||||
(tr "shortcuts.select-prev")
|
||||
(tr "shortcuts.separate-nodes")
|
||||
(tr "shortcuts.show-pixel-grid")
|
||||
(tr "shortcuts.show-shortcuts")
|
||||
(tr "shortcuts.snap-nodes")
|
||||
(tr "shortcuts.snap-pixel-grid")
|
||||
(tr "shortcuts.start-editing")
|
||||
(tr "shortcuts.start-measure")
|
||||
(tr "shortcuts.stop-measure")
|
||||
(tr "shortcuts.thumbnail-set")
|
||||
(tr "shortcuts.toggle-alignment")
|
||||
(tr "shortcuts.toggle-assets")
|
||||
(tr "shortcuts.toggle-colorpalette")
|
||||
(tr "shortcuts.toggle-focus-mode")
|
||||
(tr "shortcuts.toggle-fullscreen")
|
||||
(tr "shortcuts.toggle-guides")
|
||||
(tr "shortcuts.toggle-history")
|
||||
(tr "shortcuts.toggle-layers")
|
||||
(tr "shortcuts.toggle-layout-flex")
|
||||
(tr "shortcuts.toggle-layout-grid")
|
||||
(tr "shortcuts.toggle-lock")
|
||||
(tr "shortcuts.toggle-lock-size")
|
||||
(tr "shortcuts.toggle-rulers")
|
||||
(tr "shortcuts.toggle-snap-guides")
|
||||
(tr "shortcuts.toggle-snap-ruler-guide")
|
||||
(tr "shortcuts.toggle-textpalette")
|
||||
(tr "shortcuts.toggle-theme")
|
||||
(tr "shortcuts.toggle-visibility")
|
||||
(tr "shortcuts.toggle-zoom-style")
|
||||
(tr "shortcuts.underline")
|
||||
(tr "shortcuts.undo")
|
||||
(tr "shortcuts.ungroup")
|
||||
(tr "shortcuts.unmask")
|
||||
(tr "shortcuts.v-distribute")
|
||||
(tr "shortcuts.zoom-lense-decrease")
|
||||
(tr "shortcuts.zoom-lense-increase")
|
||||
(tr "shortcuts.zoom-selected"))
|
||||
|
||||
(let [translat-pre (case type
|
||||
:sc "shortcuts."
|
||||
:sec "shortcut-section."
|
||||
:sub-sec "shortcut-subsection.")]
|
||||
(tr (str translat-pre (d/name keyname)))))
|
||||
|
||||
(defn add-translation
|
||||
[type item]
|
||||
(map (fn [[k v]] [k (assoc v :translation (translation-keyname type k))]) item))
|
||||
|
||||
(defn shortcuts->subsections
|
||||
"A function to obtain the list of subsections and their
|
||||
associated shortcus from the general map of shortcuts"
|
||||
[shortcuts]
|
||||
(let [subsections (into #{} (mapcat :subsections) (vals shortcuts))
|
||||
get-sc-by-subsection
|
||||
(fn [subsection [k v]]
|
||||
(when (some #(= subsection %) (:subsections v)) {k v}))
|
||||
reduce-sc
|
||||
(fn [acc subsection]
|
||||
(let [shortcuts-by-subsection (into {} (keep (partial get-sc-by-subsection subsection) shortcuts))]
|
||||
(assoc acc subsection {:children shortcuts-by-subsection})))]
|
||||
(reduce reduce-sc {} subsections)))
|
||||
|
||||
(mf/defc shortcuts-keys*
|
||||
[{:keys [content command]}]
|
||||
(let [managed-list (if (coll? content)
|
||||
content
|
||||
(conj () content))
|
||||
chars-list (map ds/split-sc managed-list)
|
||||
last-element (last chars-list)
|
||||
short-char-list (if (= 1 (count chars-list))
|
||||
chars-list
|
||||
(drop-last chars-list))
|
||||
penultimate (last short-char-list)]
|
||||
[:span {:class (stl/css :keys)}
|
||||
(for [chars short-char-list]
|
||||
[:* {:key (str/join chars)}
|
||||
(for [char chars]
|
||||
[:> converted-chars* {:key (dm/str char "-" (name command))
|
||||
:char char
|
||||
:command command}])
|
||||
(when (not= chars penultimate) [:span {:class (stl/css :space)} ","])])
|
||||
(when (not= last-element penultimate)
|
||||
[:*
|
||||
[:span {:class (stl/css :space)} (tr "shortcuts.or")]
|
||||
(for [char last-element]
|
||||
[:> converted-chars* {:key (dm/str char "-" (name command))
|
||||
:char char
|
||||
:command command}])])]))
|
||||
|
||||
(mf/defc shortcut-row*
|
||||
[{:keys [elements filter-term is-match-section is-match-subsection]}]
|
||||
(let [shortcut-name (keys elements)
|
||||
shortcut-translations (map #(translation-keyname :sc %) shortcut-name)
|
||||
match-shortcut? (some #(matches-search % @filter-term) shortcut-translations)
|
||||
filtered (if (and (or is-match-section is-match-subsection) (not match-shortcut?))
|
||||
shortcut-translations
|
||||
(filter #(matches-search % @filter-term) shortcut-translations))
|
||||
sorted-filtered (sort filtered)]
|
||||
|
||||
[:ul {:class (stl/css :sub-menu)}
|
||||
(for [command-translate sorted-filtered]
|
||||
(let [sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements))
|
||||
[command comand-info] sc-by-translate
|
||||
content (or (:show-command comand-info) (:command comand-info))]
|
||||
[:li {:class (stl/css :shortcuts-name)
|
||||
:key command-translate}
|
||||
[:span {:class (stl/css :command-name)}
|
||||
command-translate]
|
||||
[:> shortcuts-keys* {:content content
|
||||
:command command}]]))]))
|
||||
|
||||
(mf/defc section-title*
|
||||
[{:keys [name is-visible is-sub]}]
|
||||
[:div {:class (if is-sub
|
||||
(stl/css :subsection-title)
|
||||
(stl/css :section-title))}
|
||||
[:> icon* {:icon-id (if is-visible i/arrow-down i/arrow-right)
|
||||
:size "s"}]
|
||||
[:span {:class (if is-sub
|
||||
(stl/css :subsection-name)
|
||||
(stl/css :section-name))} name]])
|
||||
|
||||
(mf/defc shortcut-subsection*
|
||||
[{:keys [subsections manage-sections filter-term is-match-section open-sections]}]
|
||||
(let [subsections-names (keys subsections)
|
||||
subsection-translations (if (= :none (first subsections-names))
|
||||
(map #(translation-keyname :sc %) subsections-names)
|
||||
(map #(translation-keyname :sub-sec %) subsections-names))
|
||||
sorted-translations (sort subsection-translations)]
|
||||
;; Basics section is treated different because it has no sub sections
|
||||
(if (= :none (first subsections-names))
|
||||
(let [basic-shortcuts (:none subsections)]
|
||||
[:> shortcut-row* {:elements (:children basic-shortcuts)
|
||||
:filter-term filter-term
|
||||
:is-match-section is-match-section
|
||||
:is-match-subsection true}])
|
||||
|
||||
[:ul {:class (stl/css :subsection-menu)}
|
||||
(for [sub-translated sorted-translations]
|
||||
(let [sub-by-translate (first (filter #(= (:translation (second %)) sub-translated) subsections))
|
||||
[sub-name sub-info] sub-by-translate
|
||||
visible? (some #(= % (:id sub-info)) @open-sections)
|
||||
match-subsection? (matches-search (translation-keyname :sub-sec sub-name) @filter-term)
|
||||
shortcut-names (map #(translation-keyname :sc %) (keys (:children sub-info)))
|
||||
match-shortcuts? (some #(matches-search % @filter-term) shortcut-names)]
|
||||
(when (or match-subsection? match-shortcuts? is-match-section)
|
||||
[:li {:key sub-translated
|
||||
:on-click (manage-sections (:id sub-info))}
|
||||
[:> section-title* {:name sub-translated
|
||||
:is-visible visible?
|
||||
:is-sub true}]
|
||||
|
||||
[:div {:style {:display (if visible? "initial" "none")}}
|
||||
[:> shortcut-row* {:elements (:children sub-info)
|
||||
:filter-term filter-term
|
||||
:is-match-section is-match-section
|
||||
:is-match-subsection match-subsection?}]]])))])))
|
||||
|
||||
(mf/defc shortcut-section*
|
||||
[{:keys [section manage-sections open-sections filter-term]}]
|
||||
(let [[section-key section-info] section
|
||||
section-id (:id section-info)
|
||||
section-translation (translation-keyname :sec section-key)
|
||||
match-section? (matches-search section-translation @filter-term)
|
||||
subsections (:children section-info)
|
||||
subs-names (keys subsections)
|
||||
subs-bodys (reduce #(conj %1 (:children (%2 subsections))) {} subs-names)
|
||||
sub-trans (map #(if (= "none" (d/name %))
|
||||
nil
|
||||
(translation-keyname :sub-sec %)) subs-names)
|
||||
match-subsection? (some #(matches-search % @filter-term) sub-trans)
|
||||
translations (map #(translation-keyname :sc %) (keys subs-bodys))
|
||||
match-shortcut? (some #(matches-search % @filter-term) translations)
|
||||
visible? (some #(= % section-id) @open-sections)]
|
||||
|
||||
(when (or match-section? match-subsection? match-shortcut?)
|
||||
[:div {:class (stl/css :section)
|
||||
:on-click (manage-sections section-id)}
|
||||
[:> section-title* {:name section-translation
|
||||
:is-visible visible?
|
||||
:is-sub false}]
|
||||
|
||||
[:div {:style {:display (if visible? "initial" "none")}}
|
||||
[:> shortcut-subsection* {:subsections subsections
|
||||
:open-sections open-sections
|
||||
:manage-sections manage-sections
|
||||
:is-match-section match-section?
|
||||
:filter-term filter-term}]]])))
|
||||
|
||||
(mf/defc shortcuts-container*
|
||||
[{:keys [class]}]
|
||||
(let [workspace-shortcuts app.main.data.workspace.shortcuts/shortcuts
|
||||
path-shortcuts app.main.data.workspace.path.shortcuts/shortcuts
|
||||
all-workspace-shortcuts (->> (d/deep-merge path-shortcuts workspace-shortcuts)
|
||||
(add-translation :sc)
|
||||
(let [profile (mf/deref refs/profile)
|
||||
custom-shortcuts (get-in profile [:props :custom-shortcuts])
|
||||
|
||||
path-shortcuts-custom (ds/apply-custom-overrides psc/shortcuts custom-shortcuts :path)
|
||||
|
||||
workspace-shortcuts-custom (ds/apply-custom-overrides wsc/shortcuts custom-shortcuts :workspace)
|
||||
|
||||
workspace-shortcuts (->> (d/deep-merge path-shortcuts-custom workspace-shortcuts-custom)
|
||||
(ss/add-translation :sc)
|
||||
(into {}))
|
||||
|
||||
dashboard-shortcuts (->> app.main.data.dashboard.shortcuts/shortcuts
|
||||
(add-translation :sc)
|
||||
|
||||
dashboard-shortcuts-custom (ds/apply-custom-overrides dsc/shortcuts custom-shortcuts :dashboard)
|
||||
dashboard-shortcuts (->> dashboard-shortcuts-custom
|
||||
(ss/add-translation :sc)
|
||||
(into {}))
|
||||
viewer-shortcuts (->> app.main.data.viewer.shortcuts/shortcuts
|
||||
(add-translation :sc)
|
||||
|
||||
viewer-shortcuts-custom (ds/apply-custom-overrides vsc/shortcuts custom-shortcuts :viewer)
|
||||
viewer-shortcuts (->> viewer-shortcuts-custom
|
||||
(ss/add-translation :sc)
|
||||
(into {}))
|
||||
open-sections (mf/use-state [[1]])
|
||||
filter-term (mf/use-state "")
|
||||
|
||||
open-sections* (mf/use-state [[:workspace]])
|
||||
open-sections (deref open-sections*)
|
||||
filter-term* (mf/use-state "")
|
||||
filter-term (deref filter-term*)
|
||||
|
||||
close-fn #(st/emit! (dw/toggle-layout-flag :shortcuts))
|
||||
|
||||
walk (fn walk [element parent-id]
|
||||
(if (nil? element)
|
||||
element
|
||||
(let [rec-fn (fn [index [k item]]
|
||||
(let [item-id (if (nil? parent-id)
|
||||
[index]
|
||||
(conj parent-id index))]
|
||||
[k (assoc item :id item-id :children (walk (:children item) item-id))]))]
|
||||
(into {} (map-indexed (partial rec-fn) element)))))
|
||||
{:keys [all-shortcuts all-sc-names all-sub-names all-section-names]}
|
||||
(ss/build-all-shortcuts workspace-shortcuts dashboard-shortcuts viewer-shortcuts)
|
||||
|
||||
workspace-sc-by-subsections (->> (shortcuts->subsections all-workspace-shortcuts)
|
||||
(add-translation :sub-sec)
|
||||
(into {}))
|
||||
dashboard-sc-by-subsections (->> (shortcuts->subsections dashboard-shortcuts)
|
||||
(add-translation :sub-sec)
|
||||
(into {}))
|
||||
viewer-sc-by-subsections (->> (shortcuts->subsections viewer-shortcuts)
|
||||
(add-translation :sub-sec)
|
||||
(into {}))
|
||||
basics-elements (into {} (concat (:children (:basics workspace-sc-by-subsections))
|
||||
(:children (:basics dashboard-sc-by-subsections))
|
||||
(:children (:basics viewer-sc-by-subsections))))
|
||||
|
||||
workspace-sc-by-subsections (dissoc workspace-sc-by-subsections :basics)
|
||||
dashboard-sc-by-subsections (dissoc dashboard-sc-by-subsections :basics)
|
||||
viewer-sc-by-subsections (dissoc viewer-sc-by-subsections :bassics)
|
||||
|
||||
all-shortcuts {:basics {:id [1]
|
||||
:children {:none {:children basics-elements}}
|
||||
:translation (tr "shortcut-section.basics")}
|
||||
:workspace {:id [2]
|
||||
:children workspace-sc-by-subsections
|
||||
:translation (tr "shortcut-section.workspace")}
|
||||
:dashboard {:id [3]
|
||||
:children dashboard-sc-by-subsections
|
||||
:translation (tr "shortcut-section.dashboard")}
|
||||
:viewer {:id [4]
|
||||
:children viewer-sc-by-subsections
|
||||
:translation (tr "shortcut-section.viewer")}}
|
||||
all-shortcuts (walk all-shortcuts nil)
|
||||
|
||||
all-sc-names (map #(translation-keyname :sc %) (concat
|
||||
(keys all-workspace-shortcuts)
|
||||
(keys dashboard-shortcuts)
|
||||
(keys viewer-shortcuts)))
|
||||
|
||||
all-sub-names (map #(translation-keyname :sub-sec %) (concat
|
||||
(keys workspace-sc-by-subsections)
|
||||
(keys dashboard-sc-by-subsections)
|
||||
(keys viewer-sc-by-subsections)))
|
||||
all-section-names (map #(translation-keyname :sec %) (keys all-shortcuts))
|
||||
all-item-names (concat all-sc-names all-sub-names all-section-names)
|
||||
match-any? (some #(matches-search % @filter-term) all-item-names)
|
||||
match-any? (some #(matches-search % filter-term) all-item-names)
|
||||
|
||||
manage-sections
|
||||
(fn [item]
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [is-present? (some #(= % item) @open-sections)
|
||||
(let [is-present? (some #(= % item) open-sections)
|
||||
new-value (if is-present?
|
||||
(filterv (fn [element] (not= element item)) @open-sections)
|
||||
(conj @open-sections item))]
|
||||
(reset! open-sections new-value))))
|
||||
(filterv (fn [element] (not= element item)) open-sections)
|
||||
(conj open-sections item))]
|
||||
(reset! open-sections* new-value))))
|
||||
|
||||
add-ids (fn [acc node]
|
||||
(let [id (:id node)
|
||||
addition (case (count id)
|
||||
1 id
|
||||
2 [[(first id)] id]
|
||||
3 [[(first id)] [(first id) (second id)]]
|
||||
"default" nil)]
|
||||
(if (= 1 (count addition))
|
||||
(conj acc addition)
|
||||
(into [] (concat acc addition)))))
|
||||
(let [id (:id node)
|
||||
parents (when (> (count id) 1)
|
||||
(mapv (fn [n] (vec (take n id)))
|
||||
(range 1 (count id))))]
|
||||
(into [] (concat acc parents))))
|
||||
|
||||
manage-section-on-search
|
||||
(fn [section term]
|
||||
@ -468,23 +98,28 @@
|
||||
manage-sections-on-search
|
||||
(fn [term]
|
||||
(if (= term "")
|
||||
(reset! open-sections [[1]])
|
||||
(reset! open-sections* [[:workspace]])
|
||||
(let [ids (set/union (manage-section-on-search :basics term)
|
||||
(manage-section-on-search :workspace term)
|
||||
(manage-section-on-search :dashboard term)
|
||||
(manage-section-on-search :viewer term))]
|
||||
(reset! open-sections ids))))
|
||||
(reset! open-sections* ids))))
|
||||
|
||||
on-search-term-change-2
|
||||
(mf/use-callback
|
||||
(fn [value]
|
||||
(manage-sections-on-search value)
|
||||
(reset! filter-term value)))
|
||||
(reset! filter-term* value)))
|
||||
|
||||
on-search-clear-click
|
||||
(mf/use-callback
|
||||
(fn [_]
|
||||
(reset! open-sections [[1]])
|
||||
(reset! filter-term "")))]
|
||||
(reset! open-sections* [[:workspace]])
|
||||
(reset! filter-term* "")))
|
||||
|
||||
go-to-edit-shortcuts
|
||||
(mf/use-fn
|
||||
#(st/emit! (rt/nav :settings-shortcuts {} {::rt/new-window true})))]
|
||||
|
||||
[:div {:class (dm/str class " " (stl/css :shortcuts))}
|
||||
[:> panel-title* {:class (stl/css :shortcuts-title)
|
||||
@ -494,7 +129,7 @@
|
||||
[:div {:class (stl/css :search-field)}
|
||||
[:> search-bar* {:on-change on-search-term-change-2
|
||||
:on-clear on-search-clear-click
|
||||
:value @filter-term
|
||||
:value filter-term
|
||||
:placeholder (tr "shortcuts.title")
|
||||
:icon-id i/search
|
||||
:auto-focus true}]]
|
||||
@ -502,9 +137,20 @@
|
||||
(if match-any?
|
||||
[:div {:class (stl/css :shortcuts-list)}
|
||||
(for [section all-shortcuts]
|
||||
[:> shortcut-section* {:key (->> section second :id first)
|
||||
:section section
|
||||
:manage-sections manage-sections
|
||||
:open-sections open-sections
|
||||
:filter-term filter-term}])]
|
||||
[:div {:class (stl/css :not-found)} (tr "shortcuts.not-found")])]))
|
||||
(let [[section-key _] section]
|
||||
[:> ss/shortcut-section* {:key (name section-key)
|
||||
:section section
|
||||
:manage-sections manage-sections
|
||||
:open-sections open-sections
|
||||
:filter-term filter-term
|
||||
:editable? false
|
||||
:custom-shortcuts custom-shortcuts}]))]
|
||||
[:div {:class (stl/css :not-found)} (tr "shortcuts.not-found")])
|
||||
(when (contains? cf/flags :custom-shortcuts)
|
||||
[:> bl/button-link* {:on-click go-to-edit-shortcuts
|
||||
:label (tr "shortcuts.edit-on-settings")
|
||||
:class (stl/css :edit-shortcuts-button)
|
||||
:icon (mf/html
|
||||
[:> icon* {:icon-id i/open-link
|
||||
:size "m"
|
||||
:aria-hidden true}])}])]))
|
||||
|
||||
@ -5,115 +5,63 @@
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/spacing.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.shortcuts {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
|
||||
// TODO: Fix this once we start implementing the DS.
|
||||
// We should not be doign these hardcoded calc's.
|
||||
height: calc(100vh - #{deprecated.$s-60});
|
||||
// We should not be doing these hardcoded calc's.
|
||||
block-size: calc(100vh - #{px2rem(60)});
|
||||
}
|
||||
|
||||
.search-field {
|
||||
margin: deprecated.$s-16 deprecated.$s-12 deprecated.$s-4 deprecated.$s-12;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
margin: var(--sp-l) var(--sp-m) var(--sp-xs) var(--sp-m);
|
||||
|
||||
// The search bar should fill available space; the reset-all button sits at the end
|
||||
& > :first-child {
|
||||
flex: 1;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcuts-title {
|
||||
margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 0;
|
||||
.shortcuts-list {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
block-size: 100%;
|
||||
padding: var(--sp-m);
|
||||
overflow-y: auto;
|
||||
color: var(--color-foreground-secondary);
|
||||
min-block-size: px2rem(250);
|
||||
}
|
||||
|
||||
.not-found {
|
||||
@include deprecated.body-small-typography;
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
color: var(--empty-message-foreground-color);
|
||||
margin: deprecated.$s-12;
|
||||
color: var(--color-foreground-secondary);
|
||||
margin: var(--sp-m);
|
||||
}
|
||||
|
||||
.shortcuts-list {
|
||||
.edit-shortcuts-button {
|
||||
@include t.use-typography("headline-small");
|
||||
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: deprecated.$s-12;
|
||||
overflow-y: auto;
|
||||
font-size: deprecated.$fs-12;
|
||||
color: var(--title-foreground-color);
|
||||
|
||||
.section-title,
|
||||
.subsection-title {
|
||||
@include deprecated.uppercase-title-typography;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: deprecated.$s-8 0;
|
||||
cursor: pointer;
|
||||
|
||||
.subsection-name,
|
||||
.section-name {
|
||||
padding-left: deprecated.$s-4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--title-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-title {
|
||||
text-transform: none;
|
||||
padding-left: deprecated.$s-12;
|
||||
}
|
||||
|
||||
.subsection-menu {
|
||||
margin-bottom: deprecated.$s-4;
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
margin-bottom: deprecated.$s-4;
|
||||
|
||||
.shortcuts-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
min-height: deprecated.$s-32;
|
||||
padding: deprecated.$s-6;
|
||||
margin-bottom: deprecated.$s-4;
|
||||
border-radius: deprecated.$br-8;
|
||||
background-color: var(--pill-background-color);
|
||||
|
||||
.command-name {
|
||||
@include deprecated.body-small-typography;
|
||||
|
||||
margin-left: deprecated.$s-2;
|
||||
color: var(--pill-foreground-color);
|
||||
}
|
||||
|
||||
.keys {
|
||||
@include deprecated.flex-center;
|
||||
|
||||
gap: deprecated.$s-2;
|
||||
color: var(--pill-foreground-color);
|
||||
|
||||
.key {
|
||||
@include deprecated.body-small-typography;
|
||||
@include deprecated.flex-center;
|
||||
|
||||
text-transform: capitalize;
|
||||
height: deprecated.$s-20;
|
||||
padding: deprecated.$s-2 deprecated.$s-6;
|
||||
border-radius: deprecated.$s-6;
|
||||
background-color: var(--menu-shortcut-background-color);
|
||||
}
|
||||
|
||||
.space {
|
||||
margin: 0 deprecated.$s-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@ -479,7 +479,7 @@
|
||||
|
||||
(defn setup-shortcuts
|
||||
[path-editing? drawing-path? text-editing? grid-editing?]
|
||||
(hooks/use-shortcuts ::workspace wsc/shortcuts)
|
||||
(hooks/use-shortcuts ::workspace wsc/shortcuts :workspace)
|
||||
|
||||
(mf/with-effect []
|
||||
(.addEventListener js/window "keydown" wsc/on-display-guides-keydown)
|
||||
@ -490,18 +490,20 @@
|
||||
(cond
|
||||
grid-editing?
|
||||
(do
|
||||
(st/emit! (dsc/push-shortcuts ::grid gsc/shortcuts))
|
||||
(st/emit! (dsc/push-shortcuts ::grid gsc/shortcuts :workspace
|
||||
:merge-shortcuts :auto))
|
||||
(fn []
|
||||
(st/emit! (dsc/pop-shortcuts ::grid))))
|
||||
|
||||
(or drawing-path? path-editing?)
|
||||
(do
|
||||
(st/emit! (dsc/push-shortcuts ::path psc/shortcuts))
|
||||
(st/emit! (dsc/push-shortcuts ::path psc/shortcuts :workspace
|
||||
:merge-shortcuts :auto))
|
||||
(fn []
|
||||
(st/emit! (dsc/pop-shortcuts ::path))))
|
||||
|
||||
text-editing?
|
||||
(do
|
||||
(st/emit! (dsc/push-shortcuts ::text tsc/shortcuts))
|
||||
(st/emit! (dsc/push-shortcuts ::text tsc/shortcuts :workspace))
|
||||
(fn []
|
||||
(st/emit! (dsc/pop-shortcuts ::text)))))))
|
||||
|
||||
@ -795,10 +795,8 @@
|
||||
(defn trigger-download
|
||||
[filename blob]
|
||||
(let [uri (wapi/create-uri blob)]
|
||||
(try
|
||||
(trigger-download-uri filename (.-type ^js blob) uri)
|
||||
(finally
|
||||
(wapi/revoke-uri uri)))))
|
||||
(trigger-download-uri filename (.-type ^js blob) uri)
|
||||
(js/setTimeout #(wapi/revoke-uri uri) 1000)))
|
||||
|
||||
(defn event
|
||||
"Create an instance of DOM Event"
|
||||
|
||||
@ -62,6 +62,7 @@
|
||||
[frontend-tests.ui.layout-container-multiple-test]
|
||||
[frontend-tests.ui.measures-menu-props-test]
|
||||
[frontend-tests.ui.settings-password-schema-test]
|
||||
[frontend-tests.ui.settings-shortcuts-test]
|
||||
[frontend-tests.util-clipboard-test]
|
||||
[frontend-tests.util-object-test]
|
||||
[frontend-tests.util-range-tree-test]
|
||||
@ -140,6 +141,7 @@
|
||||
'frontend-tests.ui.layout-container-multiple-test
|
||||
'frontend-tests.ui.measures-menu-props-test
|
||||
'frontend-tests.ui.settings-password-schema-test
|
||||
'frontend-tests.ui.settings-shortcuts-test
|
||||
'frontend-tests.util-clipboard-test
|
||||
'frontend-tests.util-object-test
|
||||
'frontend-tests.util-range-tree-test
|
||||
|
||||
180
frontend/test/frontend_tests/ui/settings_shortcuts_test.cljs
Normal file
180
frontend/test/frontend_tests/ui/settings_shortcuts_test.cljs
Normal file
@ -0,0 +1,180 @@
|
||||
(ns frontend-tests.ui.settings-shortcuts-test
|
||||
(:require
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.ui.settings.restore-shortcuts-modal :as restore-modal]
|
||||
[app.main.ui.settings.shortcuts :as sut]
|
||||
[app.main.ui.shortcuts :as ui-shortcuts]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-accepts-valid-workspace
|
||||
(t/is (= {:valid? true}
|
||||
(sut/validate-imported-shortcuts
|
||||
{:workspace {:escape "escape"
|
||||
:increase-zoom "+"
|
||||
:zoom-lense-decrease "alt+z"}}))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-accepts-optional-contexts
|
||||
(t/is (= {:valid? true}
|
||||
(sut/validate-imported-shortcuts
|
||||
{:workspace {:escape "escape"}
|
||||
:dashboard {:toggle-theme "alt+m"}
|
||||
:viewer {:next-frame "right"}}))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-accepts-empty-string-command
|
||||
(t/is (= {:valid? true}
|
||||
(sut/validate-imported-shortcuts
|
||||
{:workspace {:escape ""}}))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-rejects-nil
|
||||
(let [result (sut/validate-imported-shortcuts nil)]
|
||||
(t/is (= false (:valid? result)))
|
||||
(t/is (seq (:errors result)))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-rejects-non-map
|
||||
(t/is (= false (:valid? (sut/validate-imported-shortcuts "shortcuts"))))
|
||||
(t/is (= false (:valid? (sut/validate-imported-shortcuts 42))))
|
||||
(t/is (= false (:valid? (sut/validate-imported-shortcuts []))))
|
||||
(t/is (= false (:valid? (sut/validate-imported-shortcuts true)))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-accepts-missing-workspace
|
||||
(t/is (= {:valid? true}
|
||||
(sut/validate-imported-shortcuts {:dashboard {:toggle-theme "alt+m"}}))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-rejects-non-map-context
|
||||
(let [result (sut/validate-imported-shortcuts {:workspace "not-a-map"})]
|
||||
(t/is (= false (:valid? result)))
|
||||
(t/is (seq (:errors result)))
|
||||
(t/is (some #(= "workspace" (:path %)) (:errors result)))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-rejects-non-string-command
|
||||
(let [result (sut/validate-imported-shortcuts {:workspace {:escape 123}})]
|
||||
(t/is (= false (:valid? result)))
|
||||
(t/is (seq (:errors result)))
|
||||
(t/is (some #(= "workspace/escape" (:path %)) (:errors result)))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-rejects-vector-command
|
||||
(let [result (sut/validate-imported-shortcuts {:workspace {:escape ["ctrl+e"]}})]
|
||||
(t/is (= false (:valid? result)))
|
||||
(t/is (seq (:errors result)))
|
||||
(t/is (some #(= "workspace/escape" (:path %)) (:errors result)))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-rejects-unknown-top-level-key
|
||||
(let [result (sut/validate-imported-shortcuts {:workspace {:escape "escape"}
|
||||
:unknown-context {}})]
|
||||
(t/is (= false (:valid? result)))
|
||||
(t/is (seq (:errors result)))
|
||||
(t/is (some #(str/includes? (:path %) "unknown-context") (:errors result)))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-rejects-unknown-workspace-key
|
||||
(let [result (sut/validate-imported-shortcuts {:workspace {:not-a-real-shortcut "x"}})]
|
||||
(t/is (= false (:valid? result)))
|
||||
(t/is (seq (:errors result)))
|
||||
(t/is (some #(= "workspace/not-a-real-shortcut" (:path %)) (:errors result)))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-rejects-unknown-optional-context-key
|
||||
(let [result (sut/validate-imported-shortcuts {:viewer {:not-a-real-shortcut "x"}})]
|
||||
(t/is (= false (:valid? result)))
|
||||
(t/is (seq (:errors result)))
|
||||
(t/is (some #(= "viewer/not-a-real-shortcut" (:path %)) (:errors result)))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-never-throws
|
||||
(t/is (= false (:valid? (sut/validate-imported-shortcuts (js/Date.)))))
|
||||
(t/is (= false (:valid? (sut/validate-imported-shortcuts #{:workspace}))))
|
||||
(t/is (= false (:valid? (sut/validate-imported-shortcuts (fn []))))))
|
||||
|
||||
(t/deftest import-custom-shortcuts-updates-multiple-contexts
|
||||
(let [imported {:workspace {:add-comment "alt+c"}
|
||||
:viewer {:select-all "ctrl+alt+a"}}
|
||||
all-raw {:add-comment {:command "alt+c"}
|
||||
:select-all {:command "ctrl+a"}}]
|
||||
(with-redefs [du/update-profile-props identity]
|
||||
(t/is (= {:custom-shortcuts {:workspace {:add-comment "alt+c"}
|
||||
:viewer {:select-all "ctrl+alt+a"}}}
|
||||
(ui-shortcuts/import-custom-shortcuts imported all-raw))))))
|
||||
|
||||
(t/deftest import-custom-shortcuts-preserves-unimported-contexts
|
||||
(let [current {:workspace {:existing "ctrl+e"}
|
||||
:dashboard {:toggle-theme "alt+m"}
|
||||
:viewer {:select-all "ctrl+a"}}
|
||||
imported {:workspace {:add-comment "alt+c"}}
|
||||
all-raw {:add-comment {:command "alt+c"}
|
||||
:existing {:command "ctrl+e"}
|
||||
:toggle-theme {:command "alt+m"}
|
||||
:select-all {:command "ctrl+a"}}]
|
||||
(with-redefs [du/update-profile-props identity]
|
||||
(t/is (= {:custom-shortcuts {:workspace {:add-comment "alt+c"}
|
||||
:dashboard {:toggle-theme "alt+m"}
|
||||
:viewer {:select-all "ctrl+a"}}}
|
||||
(ui-shortcuts/import-custom-shortcuts
|
||||
{:profile {:props {:custom-shortcuts current}}
|
||||
:workspace (:workspace imported)}
|
||||
all-raw))))))
|
||||
|
||||
(t/deftest import-custom-shortcuts-clears-context-when-empty
|
||||
(let [current {:workspace {:existing "ctrl+e"}}
|
||||
imported {:workspace {}}
|
||||
all-raw {}]
|
||||
(with-redefs [du/update-profile-props identity]
|
||||
(t/is (= {:custom-shortcuts {:workspace {}}}
|
||||
(ui-shortcuts/import-custom-shortcuts
|
||||
{:profile {:props {:custom-shortcuts current}}
|
||||
:workspace (:workspace imported)}
|
||||
all-raw))))))
|
||||
|
||||
(t/deftest import-custom-shortcuts-disables-conflicting-default
|
||||
(let [imported {:workspace {:select-all "ctrl+a"}}
|
||||
all-raw {:select-all {:command "ctrl+a"}
|
||||
:add-comment {:command "ctrl+a"}}]
|
||||
(with-redefs [du/update-profile-props identity]
|
||||
(t/is (= {:custom-shortcuts {:workspace {:select-all "ctrl+a"
|
||||
:add-comment ""}}}
|
||||
(ui-shortcuts/import-custom-shortcuts imported all-raw))))))
|
||||
|
||||
(t/deftest validate-imported-shortcuts-accepts-viewer-only-import
|
||||
(t/is (= {:valid? true}
|
||||
(sut/validate-imported-shortcuts
|
||||
{:viewer {:next-frame "right"
|
||||
:prev-frame "left"}}))))
|
||||
|
||||
(t/deftest import-custom-shortcuts-disables-duplicate-imported-bindings
|
||||
(let [imported {:workspace {:move-up "ctrl+up"
|
||||
:move-to-top "ctrl+up"}}
|
||||
all-raw {:move-up {:command "ctrl+shift+up"}
|
||||
:move-to-top {:command "ctrl+shift+top"}}]
|
||||
(with-redefs [du/update-profile-props identity]
|
||||
(let [customs (get-in (ui-shortcuts/import-custom-shortcuts imported all-raw)
|
||||
[:custom-shortcuts :workspace])]
|
||||
(t/is (= "" (get customs :move-up))
|
||||
"First imported entry with duplicate command should be cleared")
|
||||
(t/is (= "ctrl+up" (get customs :move-to-top))
|
||||
"Last imported entry with duplicate command should survive")))))
|
||||
|
||||
(t/deftest extract-shortcut-keys-uses-correct-context-override
|
||||
(let [customs {:workspace {:undo "shift+z"
|
||||
:move-nodes "shift+m"}
|
||||
:dashboard {:toggle-theme "alt+t"}
|
||||
:viewer {:next-frame "right"}}]
|
||||
(t/is (= "shift+z" (nth (restore-modal/extract-shortcut-keys :undo customs :workspace) 3))
|
||||
"Workspace override should be used for :undo")
|
||||
(t/is (= "alt+t" (nth (restore-modal/extract-shortcut-keys :toggle-theme customs :dashboard) 3))
|
||||
"Dashboard override should be used for :toggle-theme")
|
||||
(t/is (= "right" (nth (restore-modal/extract-shortcut-keys :next-frame customs :viewer) 3))
|
||||
"Viewer override should be used for :next-frame")
|
||||
(t/is (= "shift+m" (nth (restore-modal/extract-shortcut-keys :move-nodes customs :workspace) 3))
|
||||
"Workspace override should be used for path-specific :move-nodes")))
|
||||
|
||||
(t/deftest extract-shortcut-keys-returns-default-for-each-context
|
||||
(let [result (restore-modal/extract-shortcut-keys :move-nodes {} :workspace)]
|
||||
(t/is (string? (nth result 3))
|
||||
"Should return default command string for :move-nodes via :workspace context")
|
||||
(t/is (not= "" (nth result 3))
|
||||
"Default command should not be empty"))
|
||||
(let [result (restore-modal/extract-shortcut-keys :toggle-theme {} :dashboard)]
|
||||
(t/is (string? (nth result 3))
|
||||
"Should return default command string for :toggle-theme in :dashboard context")
|
||||
(t/is (not= "" (nth result 3))
|
||||
"Default command should not be empty"))
|
||||
(let [result (restore-modal/extract-shortcut-keys :next-frame {} :viewer)]
|
||||
(t/is (nth result 3)
|
||||
"Should return a default command for :next-frame in :viewer context")))
|
||||
@ -2113,17 +2113,10 @@ msgstr "مساحة العمل"
|
||||
msgid "shortcut-subsection.alignment"
|
||||
msgstr "محاذاة"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59
|
||||
msgid "shortcut-subsection.edit"
|
||||
msgstr "عدّل"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "عام"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgstr "عام"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
msgid "shortcut-subsection.main-menu"
|
||||
|
||||
@ -2002,16 +2002,8 @@ msgstr "Espai de treball"
|
||||
msgid "shortcut-subsection.alignment"
|
||||
msgstr "Alineació"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59
|
||||
msgid "shortcut-subsection.edit"
|
||||
msgstr "Edició"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Genèric"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Genèric"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -3242,16 +3242,8 @@ msgstr "Pracovní plocha"
|
||||
msgid "shortcut-subsection.alignment"
|
||||
msgstr "Zarovnání"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59
|
||||
msgid "shortcut-subsection.edit"
|
||||
msgstr "Upravit"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Obecný"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Obecný"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -4143,16 +4143,8 @@ msgstr "Arbeitsbereich"
|
||||
msgid "shortcut-subsection.alignment"
|
||||
msgstr "Ausrichtung"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59
|
||||
msgid "shortcut-subsection.edit"
|
||||
msgstr "Bearbeiten"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Allgemein"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Allgemein"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -2873,6 +2873,11 @@ msgstr "Create new team"
|
||||
msgid "labels.create-team.placeholder"
|
||||
msgstr "Enter new team name"
|
||||
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "labels.current"
|
||||
msgstr "Current"
|
||||
|
||||
#, unused
|
||||
msgid "labels.custom-fonts"
|
||||
msgstr "Custom fonts"
|
||||
@ -2881,6 +2886,10 @@ msgstr "Custom fonts"
|
||||
msgid "labels.dashboard"
|
||||
msgstr "Dashboard"
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "labels.default"
|
||||
msgstr "Default"
|
||||
|
||||
#: src/app/main/ui/dashboard/file_menu.cljs:337, src/app/main/ui/dashboard/fonts.cljs:286, src/app/main/ui/dashboard/fonts.cljs:363, src/app/main/ui/dashboard/fonts.cljs:397, src/app/main/ui/dashboard/project_menu.cljs:115, src/app/main/ui/dashboard/team.cljs:1379, src/app/main/ui/settings/integrations.cljs:389, src/app/main/ui/settings/profile.cljs:117, src/app/main/ui/settings/profile.cljs:128, src/app/main/ui/settings/profile.cljs:129, src/app/main/ui/workspace/sidebar/assets/common.cljs:231, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:223, src/app/main/ui/workspace/sidebar/versions.cljs:258, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:353, src/app/main/ui/workspace/tokens/management/node_context_menu.cljs:111, src/app/main/ui/workspace/tokens/sets/context_menu.cljs:66, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:378
|
||||
msgid "labels.delete"
|
||||
msgstr "Delete"
|
||||
@ -5099,16 +5108,12 @@ msgstr "Workspace"
|
||||
msgid "shortcut-subsection.alignment"
|
||||
msgstr "Alignment"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.edit"
|
||||
msgstr "Edit"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Generic"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Generic"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
@ -5626,11 +5631,79 @@ msgstr "Stop measurement"
|
||||
msgid "shortcuts.thumbnail-set"
|
||||
msgstr "Set thumbnails"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.page"
|
||||
msgstr "Shortcuts page"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:491, src/app/main/ui/workspace/sidebar/shortcuts.cljs:498
|
||||
msgid "shortcuts.title"
|
||||
msgstr "Keyboard shortcuts"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.reset"
|
||||
msgstr "Reset to default"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.disable"
|
||||
msgstr "Disable shortcut"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.disabled-shortcut"
|
||||
msgstr "Disabled shortcut"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.overwritten-by"
|
||||
msgstr "This shortcut has been reassigned to %s"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.no-personalized"
|
||||
msgstr "Head to All to start customizing your shortcuts."
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.no-disabled"
|
||||
msgstr "There are not disabled shortcuts"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.reload-hint"
|
||||
msgstr "After editing shortcuts, reload any other open Penpot tabs to apply the changes."
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.no-shortcuts"
|
||||
msgstr "There are no shortcuts that match your search"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.personalized"
|
||||
msgstr "Personalized"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.disabled"
|
||||
msgstr "Disabled"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.draw-arrow"
|
||||
msgstr "Arrow"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.draw-line"
|
||||
msgstr "Line"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.import-export"
|
||||
msgstr "Import/Export"
|
||||
|
||||
#: src/app/main/ui/shortcuts.cljs
|
||||
msgid "shortcuts.key-combo"
|
||||
msgstr "Press the key combination"
|
||||
|
||||
#: src/app/main/ui/shortcuts.cljs
|
||||
msgid "shortcuts.edit-modal.conflict"
|
||||
msgstr "Combination assigned to \"%s\". Saving will remove it there."
|
||||
|
||||
#: src/app/main/ui/shortcuts.cljs
|
||||
msgid "shortcuts.edit-modal.success"
|
||||
msgstr "This key combination is available and can be assigned without conflicts."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:182
|
||||
msgid "shortcuts.toggle-alignment"
|
||||
msgstr "Toggle dynamic alignment"
|
||||
|
||||
@ -5642,6 +5715,10 @@ msgstr "Toggle assets"
|
||||
msgid "shortcuts.toggle-colorpalette"
|
||||
msgstr "Toggle color palette"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs
|
||||
msgid "shortcuts.toggle-comments-visibility"
|
||||
msgstr "Toggle comments visibility"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:188
|
||||
msgid "shortcuts.toggle-focus-mode"
|
||||
msgstr "Toggle focus mode"
|
||||
@ -5738,6 +5815,26 @@ msgstr "Zoom lense increase"
|
||||
msgid "shortcuts.zoom-selected"
|
||||
msgstr "Zoom to selected"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs
|
||||
msgid "shortcuts.edit-on-settings"
|
||||
msgstr "Customize on settings"
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "restore-shortcuts.modal-title"
|
||||
msgstr "Restore to default configuration"
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "restore-shortcuts.modal-text"
|
||||
msgstr "The following shortcuts will be restored to their default values."
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "restore-shortcuts.acction"
|
||||
msgstr "Acction"
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "restore-shortcuts.restore"
|
||||
msgstr "Restore"
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:191
|
||||
msgid "subscription.banner.create-org-info"
|
||||
msgstr ""
|
||||
|
||||
@ -2808,6 +2808,10 @@ msgstr "Crea un nuevo equipo"
|
||||
msgid "labels.create-team.placeholder"
|
||||
msgstr "Introduce un nuevo nombre de equipo"
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "labels.current"
|
||||
msgstr "Valor Actual"
|
||||
|
||||
#, unused
|
||||
msgid "labels.custom-fonts"
|
||||
msgstr "Fuentes personalizadas"
|
||||
@ -2816,6 +2820,11 @@ msgstr "Fuentes personalizadas"
|
||||
msgid "labels.dashboard"
|
||||
msgstr "Panel"
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "labels.default"
|
||||
msgstr "Valor por defecto"
|
||||
|
||||
|
||||
#: src/app/main/ui/dashboard/file_menu.cljs:337, src/app/main/ui/dashboard/fonts.cljs:286, src/app/main/ui/dashboard/fonts.cljs:363, src/app/main/ui/dashboard/fonts.cljs:397, src/app/main/ui/dashboard/project_menu.cljs:115, src/app/main/ui/dashboard/team.cljs:1379, src/app/main/ui/settings/integrations.cljs:389, src/app/main/ui/settings/profile.cljs:117, src/app/main/ui/settings/profile.cljs:128, src/app/main/ui/settings/profile.cljs:129, src/app/main/ui/workspace/sidebar/assets/common.cljs:231, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:223, src/app/main/ui/workspace/sidebar/versions.cljs:258, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:353, src/app/main/ui/workspace/tokens/management/node_context_menu.cljs:111, src/app/main/ui/workspace/tokens/sets/context_menu.cljs:66, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:378
|
||||
msgid "labels.delete"
|
||||
msgstr "Borrar"
|
||||
@ -4943,6 +4952,7 @@ msgstr "Seleccionar elementos que usan este estilo"
|
||||
msgid "shortcut-section.basics"
|
||||
msgstr "Básicos"
|
||||
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:418
|
||||
msgid "shortcut-section.dashboard"
|
||||
msgstr "Panel"
|
||||
@ -4965,11 +4975,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Editar"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Genérico"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Genérico"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
@ -5475,10 +5481,78 @@ msgstr "Terminar medida"
|
||||
msgid "shortcuts.thumbnail-set"
|
||||
msgstr "Activar miniaturas"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.page"
|
||||
msgstr "Página de atajos"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:491, src/app/main/ui/workspace/sidebar/shortcuts.cljs:498
|
||||
msgid "shortcuts.title"
|
||||
msgstr "Atajos de teclado"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.reset"
|
||||
msgstr "Restablecer valor por defecto"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.disable"
|
||||
msgstr "Deshabilitar atajo"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.disabled-shortcut"
|
||||
msgstr "Atajo deshabilitado"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.overwritten-by"
|
||||
msgstr "Este atajo ha sido reasignado a %s"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.no-personalized"
|
||||
msgstr "Dirígete a TODO para personalizar tus atajos."
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.no-disabled"
|
||||
msgstr "No hay atajos deshabilitados."
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.reload-hint"
|
||||
msgstr "Después de editar un atajo, recarga cualquier pestaña de Penpot para aplicar los cambios."
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.no-shortcuts"
|
||||
msgstr "No hay atajos que coincidan con tu búsqueda."
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.personalized"
|
||||
msgstr "Personalizados"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.disabled"
|
||||
msgstr "Deshabilitado"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.draw-arrow"
|
||||
msgstr "Flecha"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.draw-line"
|
||||
msgstr "Linea"
|
||||
|
||||
#: src/app/main/ui/settings/shortcuts.cljs
|
||||
msgid "shortcuts.import-export"
|
||||
msgstr "Importar/Exportar"
|
||||
|
||||
#: src/app/main/ui/shortcuts.cljs
|
||||
msgid "shortcuts.key-combo"
|
||||
msgstr "Pulsa la combinación de teclas deseada"
|
||||
|
||||
#: src/app/main/ui/shortcuts.cljs
|
||||
msgid "shortcuts.edit-modal.conflict"
|
||||
msgstr "Combinación usada en \"%s\". Guardar este atajo hará que se elimine allí."
|
||||
|
||||
#: src/app/main/ui/shortcuts.cljs
|
||||
msgid "shortcuts.edit-modal.success"
|
||||
msgstr "Esta combinación de teclas está disponible y puede asignarse sin conflictos."
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185
|
||||
msgid "shortcuts.toggle-alignment"
|
||||
msgstr "Alternar alineación"
|
||||
@ -5491,6 +5565,10 @@ msgstr "Mostrar/ocultar recursos"
|
||||
msgid "shortcuts.toggle-colorpalette"
|
||||
msgstr "Mostrar/ocultar paleta de colores"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs
|
||||
msgid "shortcuts.toggle-comments-visibility"
|
||||
msgstr "Activar/desactivar visibilidad de comentarios"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:188
|
||||
msgid "shortcuts.toggle-focus-mode"
|
||||
msgstr "Mostrar/ocultar focus mode"
|
||||
@ -5587,6 +5665,26 @@ msgstr "Incrementar zoom a objetivo"
|
||||
msgid "shortcuts.zoom-selected"
|
||||
msgstr "Zoom a selección"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs
|
||||
msgid "shortcuts.edit-on-settings"
|
||||
msgstr "Personalízalos en ajustes"
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "restore-shortcuts.modal-title"
|
||||
msgstr "Restaurar la configuración predeterminada"
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "restore-shortcuts.modal-text"
|
||||
msgstr "Los siguientes atajos de teclado se restaurarán a sus valores predeterminados."
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "restore-shortcuts.acction"
|
||||
msgstr "Acción"
|
||||
|
||||
#: src/app/main/ui/settings/restore_shortcuts_modal.cljs
|
||||
msgid "restore-shortcuts.restore"
|
||||
msgstr "Restaurar"
|
||||
|
||||
#: src/app/main/ui/dashboard/subscription.cljs:191
|
||||
msgid "subscription.banner.create-org-info"
|
||||
msgstr ""
|
||||
|
||||
@ -2294,11 +2294,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Editatu"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Orokorra"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Orokorra"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -4178,11 +4178,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Modifier"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Générique"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Générique"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -5076,11 +5076,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Édition"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Général"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Général"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -1145,11 +1145,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Editar"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Xenérico"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Xenérico"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:64
|
||||
|
||||
@ -2436,11 +2436,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Tace"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "gamayya"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "gamayya"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -4079,11 +4079,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "עריכה"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "כללי"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "כללי"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -4032,11 +4032,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "संपादन"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "सार्वभौमिक"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "सार्वभौमिक"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -3246,11 +3246,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Uredi"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Generičko"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Generičko"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -3467,11 +3467,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Sunting"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Generik"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Generik"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -4588,11 +4588,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Modifica"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Generico"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Generico"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -3859,11 +3859,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "편집"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "일반"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "일반"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -3786,11 +3786,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Labot"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Vispārējs"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Vispārējs"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -2550,11 +2550,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Sunting"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Generik"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Generik"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -4186,11 +4186,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Bewerken"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Algemeen"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Algemeen"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -2286,11 +2286,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Edytuj"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Ogólny"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Ogólny"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -3042,11 +3042,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Editar"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Geral"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Geral"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -3309,11 +3309,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Editar"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Genérico"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Genérico"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -3878,11 +3878,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Editează"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Generic"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Generic"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -3905,11 +3905,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Редактировать"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Общее"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Общее"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -2812,11 +2812,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Уреди"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Опште"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Опште"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -5040,11 +5040,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Redigera"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Allmän"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Allmän"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -5042,11 +5042,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Düzenle"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Genel"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Genel"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -4113,11 +4113,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "Редагувати"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "Загальні"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "Загальні"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -2318,10 +2318,6 @@ msgstr "Titete"
|
||||
msgid "shortcut-subsection.edit"
|
||||
msgstr "Sàtunkọ"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgstr "àbùdá"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
msgid "shortcut-subsection.main-menu"
|
||||
msgstr "Akojọ ólórì aṣyn"
|
||||
|
||||
@ -3502,11 +3502,7 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "编辑"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgstr "通用"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "通用"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
|
||||
@ -3081,12 +3081,9 @@ msgid "shortcut-subsection.edit"
|
||||
msgstr "編輯"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60
|
||||
msgid "shortcut-subsection.general-dashboard"
|
||||
msgid "shortcut-subsection.generic"
|
||||
msgstr "一般"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61
|
||||
msgid "shortcut-subsection.general-viewer"
|
||||
msgstr "一般"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62
|
||||
msgid "shortcut-subsection.main-menu"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user