Skip to content

Commit

Permalink
shortcuts - support to import/export items
Browse files Browse the repository at this point in the history
  • Loading branch information
benlau committed Feb 23, 2024
1 parent d1dfba5 commit 602e88c
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 46 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
}
],
"import/prefer-default-export": "off",
"no-empty-function": "off"
"no-empty-function": "off",
"no-alert": "off"
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"prepare": "npm run dist",
"update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force",
"lint": "eslint . --ext .ts --ext .js --ext .tsx",
"lint:fix": "eslint . --ext .ts --ext .js --fix --ext .tsx",
"lint:fix": "eslint . --ext .ts --ext .js --ext .tsx --fix",
"test": "jest --silent false --verbose false",
"test:watch": "jest --watch --silent false --verbose false",
"updateVersion": "webpack --env joplin-plugin-config=updateVersion"
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"shortcuts.import_export_tooltip": "Import/Export",
"shortcuts.import": "Import",
"shortcuts.export": "Export",
"shortcuts.imported": "Imported",
"shortcuts.import_error": "Invalid shortcuts file. Please check the file content and try again.",
"scratchpad.title": "Scratchpad",
"scratchpad.enable": "Enable Scratchpad",
"recentnotes.title": "Recent Notes",
Expand Down
4 changes: 3 additions & 1 deletion src/locales/zh_TW.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{
"backlinks.title": "反向鏈結",
"shortcuts.title": "捷徑",
"shortcuts.import": "匯入",
"shortcuts.export": "匯出",
"shortcuts.drag_note_here": "將筆記拉到這裡",
"scratchpad.title": "拍紙簿",
"recentnotes.title": "最近更新",
"recentnotes.title": "最近瀏覽",
"notedialog.swap": "交換",
"notedialog.swap_tooltip": "交換編輯中的筆記與側欄的筆記",
"notedialog.note_editor": "編輯器",
Expand Down
3 changes: 2 additions & 1 deletion src/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export default class Panel {
async onLoaded() {
const {
joplinService,
joplinRepo,
} = this.servicePool;

await this.servicePool.onLoaded();
Expand Down Expand Up @@ -256,7 +257,7 @@ export default class Panel {
const currentTheme = await joplinService.queryThemeType();
const { serviceWorkerFunctions } = this.servicePool;

const locale = await joplin.settings.globalValue("locale");
const locale = await joplinRepo.settingsLoadGlobal("locale", "en");

this.joplinRepo.panelPostMessage({
type: "dddot.start",
Expand Down
9 changes: 9 additions & 0 deletions src/tools/shortcuts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export default class Shortcuts extends Tool {
case "shortcuts.tool.removeLink":
this.removeLink(message.id);
return undefined;
case "shortcuts.importShortcuts":
this.importShortcuts(message.shortcuts);
break;
case "shortcuts.onImportExportClicked":
this.showOverlay();
return undefined;
Expand Down Expand Up @@ -142,4 +145,10 @@ export default class Shortcuts extends Tool {
type: "shortcuts.showOverlay",
});
}

async importShortcuts(shortcuts: Link[]) {
this.linkListModel.rehydrate(shortcuts);
await this.save();
this.refresh(this.read());
}
}
54 changes: 44 additions & 10 deletions src/tools/shortcuts/overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import { Overlay } from "../../views/overlay";
import { PrimaryButton } from "../../views/primarybutton";

import { Link } from "../../types/link";

export type ShortcutsStorage = {
version: "v1",
shortcuts: Link[],
}
import { ShortcutsStorage, ShortcutsStorageValidator } from "./types";

export function ShortcutsOverlay(props: {
links?: Link[];
Expand All @@ -20,7 +16,7 @@ export function ShortcutsOverlay(props: {

const exportShortcuts = React.useCallback(() => {
const storage = {
version: "v1",
version: 1,
shortcuts: links ?? [],
} as ShortcutsStorage;

Expand All @@ -29,17 +25,55 @@ export function ShortcutsOverlay(props: {
elem.setAttribute("href", data);
elem.setAttribute("download", "shortcuts.json");
elem.click();
elem.remove();
}, [links]);

const importShortcuts = React.useCallback(() => {
const elem = document.createElement("input") as HTMLInputElement;
elem.type = "file";
elem.style.display = "none";
document.body.appendChild(elem);
elem.accept = "application/json";
elem.onchange = (e: Event) => {
if (e.target instanceof HTMLInputElement) {
const { files } = e.target;
const file = files[0];

const reader = new FileReader();
reader.addEventListener(
"load",
() => {
try {
const content = JSON.parse(reader.result as string);
const validator = new ShortcutsStorageValidator();
const res = validator.validate(content);
if (!res) {
throw new Error();
}
DDDot.postMessage({
type: "shortcuts.importShortcuts",
shortcuts: content.shortcuts,
});
alert(t("shortcuts.imported"));
props.onClose();
} catch {
alert(t("shortcuts.import_error"));
}
},
);
reader.readAsText(file);
}
};
elem.click();
}, []);

return (
<Overlay
header={
(
<h3>{t("shortcuts.title")}</h3>)}
header={(<h3>{t("shortcuts.title")}</h3>)}
onClose={props.onClose}
>
<div className="flex flex-row gap-2">
<PrimaryButton>
<PrimaryButton onClick={importShortcuts}>
{t("shortcuts.import")}
</PrimaryButton>
<PrimaryButton onClick={exportShortcuts}>
Expand Down
24 changes: 24 additions & 0 deletions src/tools/shortcuts/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Link } from "../../types/link";

export type ShortcutsStorage = {
version: 1,
shortcuts: Link[],
}

export class ShortcutsStorageValidator {
validateLink(link: Link) {
return link.id !== undefined
&& link.title !== undefined
&& link.type !== undefined
&& link.isTodo !== undefined
&& link.isTodoCompleted !== undefined;
}

validate(storage: any) {
try {
return storage.version === 1 && storage.shortcuts.every(this.validateLink);
} catch (e) {
return false;
}
}
}
95 changes: 63 additions & 32 deletions tests/shortcuts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,80 @@ import Shortcuts from "../src/tools/shortcuts";
import JoplinRepo from "../src/repo/joplinrepo";
import ServicePool from "../src/services/servicepool";
import { LinkMonad } from "../src/types/link";
import { ShortcutsStorageValidator } from "../src/tools/shortcuts/types";
import LinkListModel from "../src/models/linklistmodel";

jest.mock("../src/repo/joplinrepo");
jest.mock("../src/repo/platformrepo", () => ({
default: jest.fn().mockImplementation(() => ({ isLinux: () => true })),
}));

test("removeNote - it should ask for confirmation", async () => {
const joplinRepo: any = new JoplinRepo();
const servicePool = new ServicePool(joplinRepo);
const tool = new Shortcuts(servicePool);

const ids = ["1", "2", "3"];
const model = new LinkListModel();
ids.forEach((id) => {
const link = LinkMonad.createNoteLink(id, `title:${id}`);
model.push(link);
// eslint-disable-next-line func-names
jest.mock("../src/repo/platformrepo", () => function () {
this.isLinux = () => true;
});

describe("Shortcuts Tool", () => {
test("removeNote - it should ask for confirmation", async () => {
const joplinRepo: any = new JoplinRepo();
const servicePool = new ServicePool(joplinRepo);
const tool = new Shortcuts(servicePool);

const ids = ["1", "2", "3"];
const model = new LinkListModel();
ids.forEach((id) => {
const link = LinkMonad.createNoteLink(id, `title:${id}`);
model.push(link);
});

tool.linkListModel = model;
joplinRepo.dialogOpen.mockReturnValue({ id: "ok" });

await tool.removeLink("1");

expect(tool.linkListModel.links.length).toBe(ids.length - 1);
});

tool.linkListModel = model;
joplinRepo.dialogOpen.mockReturnValue({ id: "ok" });
test("removeNote - user could refuse to remove", async () => {
const joplinRepo: any = new JoplinRepo();
const servicePool = new ServicePool(joplinRepo);
const tool = new Shortcuts(servicePool);

await tool.removeLink("1");
const ids = ["1", "2", "3"];
const model = new LinkListModel();
ids.forEach((id) => {
const link = LinkMonad.createNoteLink(id, `title:${id}`);
model.push(link);
});

expect(tool.linkListModel.links.length).toBe(ids.length - 1);
});
tool.linkListModel = model;
joplinRepo.dialogOpen.mockReturnValue({ id: "cancel" });

test("removeNote - user could refuse to remove", async () => {
const joplinRepo: any = new JoplinRepo();
const servicePool = new ServicePool(joplinRepo);
const tool = new Shortcuts(servicePool);
await tool.removeLink("1");

const ids = ["1", "2", "3"];
const model = new LinkListModel();
ids.forEach((id) => {
const link = LinkMonad.createNoteLink(id, `title:${id}`);
model.push(link);
expect(tool.linkListModel.links.length).toBe(ids.length);
});
});

tool.linkListModel = model;
joplinRepo.dialogOpen.mockReturnValue({ id: "cancel" });
describe("shortcuts storage", () => {
test("validate - it should return true if the link is valid", () => {
const storage = {
version: 1,
shortcuts: [
{
id: "123",
title: "123",
type: "note",
isTodo: false,
isTodoCompleted: false,
},
],
};

await tool.removeLink("1");
const validator = new ShortcutsStorageValidator();
const result = validator.validate(storage);
expect(result).toBe(true);
});

expect(tool.linkListModel.links.length).toBe(ids.length);
test("validate - it should return false for invalid format", () => {
const validator = new ShortcutsStorageValidator();
expect(validator.validate({})).toBe(false);
expect(validator.validate(undefined)).toBe(false);
});
});

0 comments on commit 602e88c

Please sign in to comment.