Skip to content

Commit

Permalink
Add ability to pin tabs to they are always first in the toolbar (#47)
Browse files Browse the repository at this point in the history
* Add ability to pin tabs to they are always first in the toolbar

* Reorder tabs when pinning
  • Loading branch information
EddieAbbondanzio authored Apr 2, 2023
1 parent 3fe3413 commit 72a2635
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 22 deletions.
3 changes: 2 additions & 1 deletion packages/marqus-desktop/src/main/ipc/plugins/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ import { IpcPlugin } from "..";
import * as fs from "fs";
import { Config } from "../../../shared/domain/config";
import { getConfigDirectory } from "./config";
import { getLatestSchemaVersion } from "../../schemas/utils";

export const APP_STATE_FILE = "appState.json";
export const APP_STATE_DEFAULTS = {
version: 2,
version: getLatestSchemaVersion(APP_STATE_SCHEMAS),
sidebar: {
scroll: 0,
sort: NoteSort.Alphanumeric,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { app } from "electron";
import { z } from "zod";
import { AppStateV1 } from "./1_initialDefinition";
import { ModelViewState, Section } from "../../../shared/ui/app";
Expand Down
77 changes: 77 additions & 0 deletions packages/marqus-desktop/src/main/schemas/appState/3_addIsPinned.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { z } from "zod";
import { ModelViewState, Section } from "../../../shared/ui/app";
import { DATE_OR_STRING_SCHEMA, PX_REGEX } from "../../../shared/domain";
import { NoteSort } from "../../../shared/domain/note";
import { AppStateV2 } from "./2_addViewState";

export interface AppStateV3 {
version: number;
sidebar: Sidebar;
editor: Editor;
focused: Section[];
}

interface Sidebar {
searchString?: string;
hidden?: boolean;
width: string;
scroll: number;
selected?: string[];
expanded?: string[];
sort: NoteSort;
}

interface Editor {
isEditing: boolean;
scroll: number;
tabs: EditorTab[];
tabsScroll: number;
activeTabNoteId?: string;
}

interface EditorTab {
noteId: string;
noteContent?: string;
lastActive?: Date | string;
viewState?: ModelViewState["viewState"];
isPinned?: boolean;
}

export const appStateV3 = z.preprocess(
obj => {
const state = obj as AppStateV2 | AppStateV3;
if (state.version === 2) {
state.version = 3;
}

return state;
},
z.object({
version: z.literal(3),
sidebar: z.object({
width: z.string().regex(PX_REGEX),
scroll: z.number(),
hidden: z.boolean().optional(),
selected: z.array(z.string()).optional(),
expanded: z.array(z.string()).optional(),
sort: z.nativeEnum(NoteSort),
searchString: z.string().optional(),
}),
editor: z.object({
isEditing: z.boolean(),
scroll: z.number(),
tabs: z.array(
z.object({
noteId: z.string(),
// Intentionally omitted noteContent
lastActive: DATE_OR_STRING_SCHEMA.optional(),
viewState: z.any().optional(),
isPinned: z.boolean().optional(),
}),
),
tabsScroll: z.number(),
activeTabNoteId: z.string().optional(),
}),
focused: z.array(z.nativeEnum(Section)),
}),
);
2 changes: 2 additions & 0 deletions packages/marqus-desktop/src/main/schemas/appState/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { appStateV1 } from "./1_initialDefinition";
import { appStateV2 } from "./2_addViewState";
import { appStateV3 } from "./3_addIsPinned";

export const APP_STATE_SCHEMAS = {
1: appStateV1,
2: appStateV2,
3: appStateV3,
};
1 change: 1 addition & 0 deletions packages/marqus-desktop/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export async function loadInitialState(
.map(t => ({
note: getNoteById(notes, t.noteId, false),
lastActive: t.lastActive,
isPinned: t.isPinned,
}))
.filter(t => t.note != null) as EditorTab[];

Expand Down
68 changes: 54 additions & 14 deletions packages/marqus-desktop/src/renderer/components/EditorTab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { faFile, faTimes } from "@fortawesome/free-solid-svg-icons";
import {
faFile,
faTimes,
faThumbtack,
} from "@fortawesome/free-solid-svg-icons";
import React, { useEffect, useRef } from "react";
import styled from "styled-components";
import { m0, mr2, p2, px2, THEME } from "../css";
Expand All @@ -10,12 +14,14 @@ export interface EditorTabProps {
notePath: string;
noteName: string;
active?: boolean;
isPinned?: boolean;
onClick: (noteId: string) => void;
onClose: (noteId: string) => void;
onUnpin: (noteId: string) => void;
}

export function EditorTab(props: EditorTabProps): JSX.Element {
const { noteId, noteName, notePath, active } = props;
const { noteId, noteName, notePath, active, isPinned } = props;

const wrapper = useRef(null! as HTMLAnchorElement);
useEffect(() => {
Expand All @@ -26,11 +32,37 @@ export function EditorTab(props: EditorTabProps): JSX.Element {
}
}, [active]);

const onDeleteClick = (ev: React.MouseEvent<HTMLElement>) => {
// Need to stop prop otherwise it'll trigger onClick of tab.
ev.stopPropagation();
props.onClose(noteId);
};
let action;
if (isPinned) {
const onUnpinClick = (ev: React.MouseEvent<HTMLElement>) => {
// Need to stop prop otherwise it'll trigger onClick of tab.
ev.stopPropagation();
props.onUnpin(noteId);
};

action = (
<StyledPinnedIcon
icon={faThumbtack}
title={`Unpin ${noteName}`}
onClick={onUnpinClick}
/>
);
} else {
const onDeleteClick = (ev: React.MouseEvent<HTMLElement>) => {
// Need to stop prop otherwise it'll trigger onClick of tab.
ev.stopPropagation();
props.onClose(noteId);
};

action = (
<StyledDeleteIcon
icon={faTimes}
onClick={onDeleteClick}
className="delete"
title={`Close ${noteName}`}
/>
);
}

return (
<StyledTab
Expand All @@ -45,12 +77,7 @@ export function EditorTab(props: EditorTabProps): JSX.Element {
<StyledNoteIcon icon={faFile} size="lg" />
<StyledText>{noteName}</StyledText>
</FlexRow>
<StyledDelete
icon={faTimes}
onClick={onDeleteClick}
className="delete"
title={`Close ${noteName}`}
/>
{action}
</StyledTab>
);
}
Expand Down Expand Up @@ -101,7 +128,7 @@ const StyledText = styled.span`
min-width: 0;
`;

const StyledDelete = styled(Icon)`
const StyledDeleteIcon = styled(Icon)`
border-radius: 0.4rem;
${p2}
${m0}
Expand All @@ -114,6 +141,19 @@ const StyledDelete = styled(Icon)`
}
`;

const StyledPinnedIcon = styled(Icon)`
border-radius: 0.4rem;
${p2}
${m0}
color: ${THEME.editor.toolbar.deleteColor};
&:hover {
cursor: pointer;
color: ${THEME.editor.toolbar.unpinHoverColor};
background-color: ${THEME.editor.toolbar.deleteHoverBackground};
}
`;

export function getEditorTabAttribute(element: HTMLElement): string | null {
const parent = element.closest(`[${EDITOR_TAB_ATTRIBUTE}]`);
if (parent != null) {
Expand Down
78 changes: 72 additions & 6 deletions packages/marqus-desktop/src/renderer/components/EditorToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export function EditorToolbar(props: EditorToolbarProps): JSX.Element {
await store.dispatch("editor.closeTab", noteId);
};

const onUnpin = async (noteId: string) => {
await store.dispatch("editor.unpinTab", noteId);
};

// Put pinned tabs note first
for (const tab of editor.tabs) {
const note = getNoteById(notes, tab.note.id);
const notePath = getFullPath(notes, note);
Expand All @@ -58,8 +63,10 @@ export function EditorToolbar(props: EditorToolbarProps): JSX.Element {
noteName={note.name}
notePath={notePath}
active={activeTabNoteId === note.id}
isPinned={tab.isPinned}
onClick={onClick}
onClose={onClose}
onUnpin={onUnpin}
/>,
);
}
Expand Down Expand Up @@ -142,29 +149,38 @@ export function EditorToolbar(props: EditorToolbarProps): JSX.Element {
break;

case "editor.closeAllTabs":
noteIdsToClose = editor.tabs.map(t => t.note.id);
noteIdsToClose = editor.tabs
.filter(t => !t.isPinned)
.map(t => t.note.id);
break;

case "editor.closeOtherTabs":
noteIdsToClose = editor.tabs
.filter(t => t.note.id !== (value ?? editor.activeTabNoteId))
.filter(
t => !t.isPinned && t.note.id !== (value ?? editor.activeTabNoteId),
)
.map(t => t.note.id);
break;

case "editor.closeTabsToLeft": {
const leftLimit = editor.tabs.findIndex(
const start = editor.tabs.findIndex(t => !t.isPinned);

const end = editor.tabs.findIndex(
t => t.note.id === (value ?? editor.activeTabNoteId),
);
noteIdsToClose = editor.tabs.slice(0, leftLimit).map(t => t.note.id);
noteIdsToClose = editor.tabs.slice(start, end).map(t => t.note.id);
break;
}

case "editor.closeTabsToRight": {
const rightLimit = editor.tabs.findIndex(
const firstNonPinnedIndex = editor.tabs.findIndex(t => !t.isPinned);
const activeTabIndex = editor.tabs.findIndex(
t => t.note.id === (value ?? editor.activeTabNoteId),
);

noteIdsToClose = editor.tabs.slice(rightLimit + 1).map(t => t.note.id);
const end = Math.max(firstNonPinnedIndex, activeTabIndex + 1);

noteIdsToClose = editor.tabs.slice(end).map(t => t.note.id);
break;
}

Expand Down Expand Up @@ -237,6 +253,8 @@ export function EditorToolbar(props: EditorToolbarProps): JSX.Element {
store.on("editor.previousTab", switchToPreviousTab);
store.on("editor.updateTabsScroll", updateTabsScroll);
store.on("editor.deleteNote", deleteNote);
store.on("editor.pinTab", pinTab);
store.on("editor.unpinTab", unpinTab);

return () => {
store.off("editor.openTab", openTab);
Expand All @@ -255,6 +273,8 @@ export function EditorToolbar(props: EditorToolbarProps): JSX.Element {
store.off("editor.previousTab", switchToPreviousTab);
store.off("editor.updateTabsScroll", updateTabsScroll);
store.off("editor.deleteNote", deleteNote);
store.off("editor.pinTab", pinTab);
store.off("editor.unpinTab", unpinTab);
};
}, [store, switchToNextTab, switchToPreviousTab]);

Expand Down Expand Up @@ -430,6 +450,52 @@ export const updateTabsScroll: Listener<"editor.updateTabsScroll"> = async (
}
};

export const pinTab: Listener<"editor.pinTab"> = async (
{ value: tabNoteId },
ctx,
) => {
if (tabNoteId == null) {
return;
}

const state = ctx.getState();
if (state.editor.tabs.findIndex(t => t.note.id === tabNoteId) === -1) {
return;
}

ctx.setUI(prev => {
const tabs = prev.editor.tabs;
const tab = tabs.find(t => t.note.id === tabNoteId)!;
tab.isPinned = true;
prev.editor.tabs = orderBy(tabs, ["isPinned"], ["asc"]);

return prev;
});
};

export const unpinTab: Listener<"editor.unpinTab"> = async (
{ value: tabNoteId },
ctx,
) => {
if (tabNoteId == null) {
return;
}

const state = ctx.getState();
if (state.editor.tabs.findIndex(t => t.note.id === tabNoteId) === -1) {
return;
}

ctx.setUI(prev => {
const tabs = prev.editor.tabs;
const tab = tabs.find(t => t.note.id === tabNoteId)!;
delete tab.isPinned;
prev.editor.tabs = orderBy(tabs, ["isPinned"], ["asc"]);

return prev;
});
};

function setActiveTab(
ctx: StoreContext,
activeTabNoteId: string | undefined,
Expand Down
2 changes: 2 additions & 0 deletions packages/marqus-desktop/src/renderer/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export const THEME = {
deleteColor: OpenColor.gray[6],
deleteHoverColor: OpenColor.red[7],
deleteHoverBackground: OpenColor.gray[3],
unpinHoverColor: OpenColor.gray[8],
unpinHoverBackground: OpenColor.gray[3],
scrollbarColor: OpenColor.gray[5],
},
},
Expand Down
Loading

0 comments on commit 72a2635

Please sign in to comment.