Skip to content

Commit

Permalink
Merge pull request #374 from penge/pin-notes
Browse files Browse the repository at this point in the history
Pin notes
  • Loading branch information
penge authored May 22, 2022
2 parents 07a1e79 + 7fbb965 commit 2904297
Show file tree
Hide file tree
Showing 16 changed files with 171 additions and 98 deletions.
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "jsdom",
testMatch: [
"**/__tests__/**/*.test.ts?(x)"
],
moduleDirectories: ["node_modules", "src"]
};
Original file line number Diff line number Diff line change
@@ -1,40 +1,32 @@
import { Storage, NotesObject, Note } from "shared/storage/schema";
import migrate, { expectedKeys } from "../core";
import { Storage, NotesObject, Note, NotesOrder } from "shared/storage/schema";
import migrate from "../core";
import { expectItems } from "./helpers";

export const expectItems = (items: Storage): void => {
expect(Object.keys(items).sort()).toEqual(expectedKeys.sort());
};

const expectDefaultValues = (myItems?: Storage) => {
const items = (myItems || migrate({}, {})) as Storage;
const expectDefaultValues = (items: Storage) => {
expectItems(items);

// font
expect(items.font).toEqual({
let counter = 0;
const countedExpect = () => {
counter += 1;
return expect;
};

// Appearance
countedExpect()(items.font).toEqual({
id: "helvetica",
name: "Helvetica",
genericFamily: "sans-serif",
fontFamily: "Helvetica, sans-serif",
});
countedExpect()(items.size).toBe(200);
countedExpect()(items.theme).toBe("light");
countedExpect()(items.customTheme).toBe("");
countedExpect()(items.sidebar).toBe(true);
countedExpect()(items.toolbar).toBe(true);

// size
expect(items.size).toBe(200);

// sidebar
expect(items.sidebar).toBe(true);

// toolbar
expect(items.toolbar).toBe(true);

// theme
expect(items.theme).toBe("light");

// custom theme
expect(items.customTheme).toBe("");

// notes
// Notes
const notes = items.notes as NotesObject;
expect(Object.keys(notes).length).toBe(3); // "One", "Two", "Three"
countedExpect()(Object.keys(notes).length).toBe(3); // "One", "Two", "Three"
["One", "Two", "Three"].every((noteName: string) => {
const note = (notes)[noteName] as Note;

Expand All @@ -45,39 +37,47 @@ const expectDefaultValues = (myItems?: Storage) => {

expect(new Date(note.createdTime).getTime()).toEqual(new Date(note.modifiedTime).getTime()); // valid and equal
});

// active
expect(items.active).toBe("One");

// focus
expect(items.focus).toBe(false);

// tab
expect(items.tab).toBe(false);

// tab size
expect(items.tabSize).toBe(-1);

// openNoteOnMouseHover
expect(items.openNoteOnMouseHover).toBe(false);
countedExpect()(items.order).toEqual([]);
countedExpect()(items.active).toBe("One");
countedExpect()(items.setBy).toBe("");
countedExpect()(items.lastEdit).toBe("");

// Options
countedExpect()(items.notesOrder).toBe(NotesOrder.Alphabetical);
countedExpect()(items.focus).toBe(false);
countedExpect()(items.tab).toBe(false);
countedExpect()(items.tabSize).toBe(-1);
countedExpect()(items.autoSync).toBe(false);
countedExpect()(items.openNoteOnMouseHover).toBe(false);

// Expect all keys to be tested
expect(counter).toBe(Object.keys(items).length);
};

it("returns default values", () => {
expectDefaultValues();
it("migrates to default values for an empty storage", () => {
expectDefaultValues(migrate({}, {}));
});

it("fallbacks to default values", () => {
it("fallbacks to default values for any bad values", () => {
const items = migrate({}, {
// Appearance
font: { // must be a valid "font" object
name: "Droid Sans",
},
size: "large", // must be number
theme: "green", // must be "light" or "dark"
customTheme: { background: "#ffffff" }, // must be string
sidebar: "yes", // must be boolean
toolbar: "no", // must be boolean
customTheme: { background: "#ffffff" }, // must be string

// Notes
notes: null, // must be object
order: "AZ", // must be []
active: "Todo", // must be in "notes"
setBy: 1, // must be string
lastEdit: 2, // must be string

// Options
focus: 1, // must be boolean
tab: 1, // must be boolean
tabSize: "Tab", // must be number
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { NotesOrder, Storage } from "shared/storage/schema";
import { Storage, NotesOrder } from "shared/storage/schema";
import migrate from "../core";
import { expectItems } from "./core-default-values.test";
import { expectItems } from "./helpers";

it("sets custom values", () => {
const local = {
it("uses existing values and adds any omitted properties set to their defaults to make a complete Storage", () => {
const existing: Partial<Storage> = {
font: {
id: "roboto-mono",
name: "Roboto Mono",
Expand Down Expand Up @@ -52,12 +52,15 @@ it("sets custom values", () => {
tabSize: 2,
};

const items: Storage = migrate({}, local);
expectItems(Object.assign({}, items));
const items: Storage = migrate({}, existing);
expectItems(items);

// Compare objects
expect(items).toEqual(Object.assign({
// Automatically added properties to make a complete object (interface Storage)
/**
* Omitted properties below are added and set to their defaults
* to make a complete Storage.
* See {@link Storage}
*/
order: [],
notesOrder: NotesOrder.Alphabetical,
sidebar: true,
Expand All @@ -66,5 +69,5 @@ it("sets custom values", () => {
setBy: "",
lastEdit: "",
openNoteOnMouseHover: false,
}, local));
} as Partial<Storage>, existing));
});
4 changes: 4 additions & 0 deletions src/background/init/migrations/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Storage } from "shared/storage/schema";
import { localKeys } from "../core";

export const expectItems = (items: Storage): void => expect(Object.keys(items)).toEqual(localKeys);
8 changes: 4 additions & 4 deletions src/background/init/migrations/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import { NotesObject, NotesOrder, Storage } from "shared/storage/schema";
import { NotesObject, NotesOrder, Storage, StorageKey } from "shared/storage/schema";
import { defaultValuesFactory } from "shared/storage/default-values";
import {
validBoolean,
Expand All @@ -12,7 +12,7 @@ import {
validNotesOrder,
} from "shared/storage/validations";

export const expectedKeys = [
export const localKeys: StorageKey[] = [
// Appearance
"font",
"size",
Expand Down Expand Up @@ -88,8 +88,8 @@ export default (sync: { [key: string]: unknown }, local: { [key: string]: unknow
notes: notes as NotesObject, // already migrated to [3.x]
order: validStringArray(local.order) ? local.order : [],
active: tryNote(local.active as string) || tryNote(defaultValues.active as string) || firstAvailableNote,
setBy: local.setBy as string || "",
lastEdit: local.lastEdit as string || "",
setBy: typeof local.setBy === "string" ? local.setBy : defaultValues.setBy,
lastEdit: typeof local.lastEdit === "string" ? local.lastEdit : defaultValues.lastEdit,

// Options
notesOrder: validNotesOrder(local.notesOrder) ? local.notesOrder : NotesOrder.Alphabetical,
Expand Down
4 changes: 2 additions & 2 deletions src/background/init/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Storage } from "shared/storage/schema";
import migrate, { expectedKeys } from "./core";
import migrate, { localKeys } from "./core";

export const runMigrations = (): void => {
chrome.storage.sync.get(["newtab", "value", "notes"], sync => {
chrome.storage.local.get(expectedKeys, local => {
chrome.storage.local.get(localKeys, local => {
const items: Storage = migrate(sync, local); // migrate notes and options
chrome.storage.local.set(items); // store the migrated data

Expand Down
2 changes: 2 additions & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"Yes": "Yes",
"Lock": "Lock",
"Unlock": "Unlock",
"Pin": "Pin",
"Unpin": "Unpin",
"Duplicate": "Duplicate",
"Export": "Export",
"Google Drive Sync is disabled (see Options)": "Google Drive Sync is disabled (see Options)",
Expand Down
7 changes: 6 additions & 1 deletion src/notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import renameNote from "notes/state/rename-note";
import deleteNote from "notes/state/delete-note";
import duplicateNote from "notes/state/duplicate-note";

import { saveNote, setLocked } from "notes/content/save";
import { saveNote, setLocked, setPinnedTime } from "notes/content/save";
import { sendMessage } from "messages";

import { getActiveFromUrl, getFocusOverride } from "notes/location";
Expand Down Expand Up @@ -631,6 +631,11 @@ const Notes = (): h.JSX.Element => {
setContextMenuProps(null);
tabId && notesRef.current && setLocked(noteName, !(notesProps.notes[noteName].locked ?? false), tabId, notesRef.current);
},
pinned: !!notesProps.notes[noteName].pinnedTime,
onTogglePinnedTime: (noteName) => {
setContextMenuProps(null);
tabId && notesRef.current && setPinnedTime(noteName, (notesProps.notes[noteName].pinnedTime ?? undefined) ? undefined : new Date().toISOString(), tabId, notesRef.current);
},
onDuplicate: (noteName) => {
setContextMenuProps(null);
duplicateNote(noteName);
Expand Down
5 changes: 4 additions & 1 deletion src/notes/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ export interface ContextMenuProps {
onDelete: (noteName: string) => void
locked: boolean
onToggleLocked: (noteName: string) => void
pinned: boolean
onTogglePinnedTime: (noteName: string) => void
onDuplicate: (noteName: string) => void
onExport: (noteName: string) => void
}

const ContextMenu = ({
noteName, x, y, onRename, onDelete, locked, onToggleLocked, onDuplicate, onExport,
noteName, x, y, onRename, onDelete, locked, onToggleLocked, pinned, onTogglePinnedTime, onDuplicate, onExport,
}: ContextMenuProps): h.JSX.Element => {
const [offsetHeight, setOffsetHeight] = useState<number>(0);
const ref = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -44,6 +46,7 @@ const ContextMenu = ({
<div class={clsx("action", locked && "disabled")} onClick={() => !locked && onRename(noteName)}>{t("Rename")}</div>
<div class={clsx("action", locked && "disabled")} onClick={() => !locked && onDelete(noteName)}>{t("Delete")}</div>
<div class="action" onClick={() => onToggleLocked(noteName)}>{locked ? t("Unlock") : t("Lock")}</div>
<div class="action" onClick={() => onTogglePinnedTime(noteName)}>{pinned ? t("Unpin") : t("Pin")}</div>
<div class="action" onClick={() => onDuplicate(noteName)}>{t("Duplicate")}</div>
<div class="action" onClick={() => onExport(noteName)}>{t("Export")}</div>
</div>
Expand Down
56 changes: 45 additions & 11 deletions src/notes/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { h } from "preact";
import { useRef } from "preact/hooks";
import { useRef, useState, useMemo } from "preact/hooks";
import { Os, Sync } from "shared/storage/schema";
import { SidebarNote } from "notes/adapters";
import SidebarNotes from "./SidebarNotes";
import SidebarNotes, { SidebarNotesProps } from "./SidebarNotes";
import SidebarButtons from "./SidebarButtons";
import Drag from "./Drag";

Expand All @@ -24,6 +24,18 @@ const Sidebar = ({
os, notes, canChangeOrder, onChangeOrder, active, width, onActivateNote, onNoteContextMenu, onNewNote, sync, openNoteOnMouseHover,
}: SidebarProps): h.JSX.Element => {
const sidebarRef = useRef<HTMLDivElement>(null);
const [onDraggingNoteOriginator, setOnDraggingNoteOriginator] = useState<string | undefined>(undefined);
const commonSidebarNotesProps: Omit<SidebarNotesProps, "id" | "notes" | "canDragEnter" | "onChangeOrder"> = {
active,
onActivateNote,
onNoteContextMenu,
canChangeOrder,
openNoteOnMouseHover,
setOnDraggingNoteOriginator,
};

const pinnedNotes = useMemo(() => notes.filter((note) => note.pinnedTime), [notes]);
const unpinnedNotes = useMemo(() => notes.filter((note) => !note.pinnedTime), [notes]);

return (
<div
Expand All @@ -34,15 +46,37 @@ const Sidebar = ({
minWidth: width,
}}
>
<SidebarNotes
notes={notes}
active={active}
onActivateNote={onActivateNote}
onNoteContextMenu={onNoteContextMenu}
canChangeOrder={canChangeOrder}
onChangeOrder={onChangeOrder}
openNoteOnMouseHover={openNoteOnMouseHover}
/>
<div id="sidebar-notes-container">
{pinnedNotes.length > 0 && (
<SidebarNotes
id="sidebar-notes-pinned"
notes={pinnedNotes}
{...commonSidebarNotesProps}
canDragEnter={onDraggingNoteOriginator === "sidebar-notes-pinned"}
onChangeOrder={(newOrder) => onChangeOrder([
...newOrder,
...unpinnedNotes.map((note) => note.name),
])}
/>
)}

{pinnedNotes.length > 0 && unpinnedNotes.length > 0 && (
<div id="sidebar-notes-separator" />
)}

{unpinnedNotes.length > 0 && (
<SidebarNotes
id="sidebar-notes-unpinned"
notes={unpinnedNotes}
{...commonSidebarNotesProps}
canDragEnter={onDraggingNoteOriginator === "sidebar-notes-unpinned"}
onChangeOrder={(newOrder) => onChangeOrder([
...pinnedNotes.map((note) => note.name),
...newOrder,
])}
/>
)}
</div>

<SidebarButtons
os={os}
Expand Down
Loading

0 comments on commit 2904297

Please sign in to comment.