Compare commits

...

32 Commits

Author SHA1 Message Date
Ammar Ahmed
d2bbccf19e editor: fix paddings 2025-03-06 15:40:31 +05:00
Ammar Ahmed
375dd18618 editor: apply margin-top only to direct children of the editor 2025-03-06 15:18:23 +05:00
Ammar Ahmed
82f635305e editor: fix task item padding 2025-03-06 15:08:05 +05:00
Abdullah Atta
00345176f7 web: make editor note properties work like toc 2025-03-04 12:45:36 +05:00
Abdullah Atta
9032e3b4bd web: slightly improve add note button ui 2025-03-04 11:10:20 +05:00
Abdullah Atta
432a9c4e84 web: bump version to 3.1.0-beta.1 2025-03-03 14:24:09 +05:00
Abdullah Atta
aaaa9ebbc5 desktop: set publish channel to beta for beta builds 2025-03-03 14:23:43 +05:00
Abdullah Atta
1ac4dca6b5 web: minor ui fixes 2025-03-03 14:18:39 +05:00
Abdullah Atta
9dc549d0d3 theme: fix button hover colors 2025-03-03 14:07:39 +05:00
Abdullah Atta
775570fc13 web: make notesnook branding a bit smaller 2025-03-03 13:53:35 +05:00
Abdullah Atta
997f0bc93e web: minor ui changes and fixes 2025-03-03 12:48:50 +05:00
Abdullah Atta
a2016e9406 web: fix theme not persisting 2025-03-03 12:35:32 +05:00
Abdullah Atta
87623aced1 web: collapse sidebar on navigate if sidebar is temporarily expanded 2025-03-03 12:24:02 +05:00
Abdullah Atta
24977277d0 desktop: explicitly deny geolocation permission to electron process 2025-03-03 11:55:48 +05:00
Abdullah Atta
2f890fd86b web: fix scrollbar styling in sidebar 2025-03-03 11:51:45 +05:00
Abdullah Atta
0220bb4040 web: reset search on navigate 2025-03-03 11:23:46 +05:00
Abdullah Atta
09a8a0ba6d web: fix expand sidebar button not working 2025-03-03 10:30:30 +05:00
Abdullah Atta
808670f6e1 web: always show "X" button if there is query in search input 2025-03-03 10:24:03 +05:00
Abdullah Atta
62ab799a4a ci: build in beta mode when release track is beta 2025-03-01 15:32:06 +05:00
Abdullah Atta
ecee87dac9 ci: run apt-get update before installing flatpak etc 2025-03-01 15:28:54 +05:00
Abdullah Atta
a5d52ded77 desktop: hide release track selector for flatpak/snap builds 2025-03-01 14:47:18 +05:00
Abdullah Atta
4e786a83f9 web: bump version to 3.1.0-beta.0 2025-03-01 13:20:05 +05:00
Abdullah Atta
bf48ce788d web: use virtualized tree for rendering toc 2025-03-01 13:20:05 +05:00
Abdullah Atta
f6637da274 editor: fix toc level mapping 2025-03-01 13:20:05 +05:00
Abdullah Atta
6cd4a4a041 web: add support for pre-expanding tree nodes on creation 2025-03-01 13:20:05 +05:00
Abdullah Atta
02ece7139e web: do not collapse child nodes when collapsing parent 2025-03-01 13:20:05 +05:00
Abdullah Atta
95203d723b web: fix all tests 2025-03-01 13:20:05 +05:00
Abdullah Atta
b423cef2e6 global: update ui 2025-03-01 13:20:05 +05:00
01zulfi
e46b3ea85f web: fix faulty useEffect
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-01 13:20:05 +05:00
01zulfi
0019c1f7af web: refactor props for Tags component
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-01 13:20:05 +05:00
01zulfi
ffc4e2d263 web: separate component for tab items in sidebar
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-01 13:20:05 +05:00
01zulfi
c01ad2e49c web: new sidebar ui
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-03-01 13:20:05 +05:00
145 changed files with 2982 additions and 4872 deletions

View File

@@ -33,6 +33,14 @@ on:
required: true
default: true
description: "Build for macOS?"
release-track:
type: choice
required: true
default: stable
description: "Select the release track"
options:
- stable
- beta
jobs:
build:
@@ -61,9 +69,14 @@ jobs:
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Generate desktop build
- name: Generate desktop build (stable)
if: ${{ inputs.release-track == 'stable' }}
run: npx nx build:desktop @notesnook/web
- name: Generate desktop build (beta)
if: ${{ inputs.release-track == 'beta' }}
run: BETA=true npx nx build:desktop @notesnook/web
- name: Build desktop bundle
working-directory: ./apps/desktop
run: npm run bundle
@@ -99,6 +112,7 @@ jobs:
- name: Generate flatpak sources
if: inputs.publish-github && inputs.build-linux
run: |
sudo apt-get update
sudo apt-get install -y --quiet flatpak flatpak-builder pipx
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
cd /tmp/flatpak-builder-tools/node

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const path = require("path");
const pkg = require("./package.json");
const buildRoot = process.env.NN_BUILD_ROOT || ".";
const buildFiles = [
@@ -35,6 +36,7 @@ const linuxExecutableName = process.env.NN_PRODUCT_NAME
? process.env.NN_PRODUCT_NAME.toLowerCase().replace(/\s+/g, "-")
: "notesnook";
const year = new Date().getFullYear();
const isBeta = pkg.version.includes("-beta");
module.exports = {
appId: appId,
@@ -194,7 +196,8 @@ module.exports = {
{
provider: "github",
repo: "notesnook",
owner: "streetwriters"
owner: "streetwriters",
channel: isBeta ? "beta" : "latest"
}
]
};

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.29",
"version": "3.1.0-beta.1",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",

View File

@@ -29,7 +29,7 @@ import { dirname } from "path";
import { resolvePath } from "../utils/resolve-path";
import { observable } from "@trpc/server/observable";
import { AssetManager } from "../utils/asset-manager";
import { isFlatpak } from "../utils";
import { isFlatpak, isSnap } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration";
import { rm } from "fs/promises";
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
@@ -49,6 +49,7 @@ const NotificationOptions = z.object({
export const osIntegrationRouter = t.router({
isFlatpak: t.procedure.query(() => isFlatpak()),
isSnap: t.procedure.query(() => isSnap()),
zoomFactor: t.procedure.query(() => config.zoomFactor),
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {

View File

@@ -147,6 +147,11 @@ async function createWindow() {
await AssetManager.loadIcons();
setupDesktopIntegration(config.desktopSettings);
mainWindow.webContents.session.setPermissionRequestHandler(
(webContents, permission, callback) => {
callback(permission === "geolocation" ? false : true);
}
);
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
"http://dictionaries.notesnook.com/"
);

View File

@@ -29,3 +29,7 @@ export function isDevelopment() {
export function isFlatpak() {
return existsSync("/.flatpak-info");
}
export function isSnap() {
return process.env.SNAP !== undefined;
}

View File

@@ -40,10 +40,12 @@ test("remove color", async ({ page }) => {
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.newColor({ title: "red", color: "#ff0000" });
await app.navigation.waitForItem("red");
const colorItem = await app.navigation.findItem("red");
await colorItem?.removeColor();
await expect(colorItem!.locator).toBeHidden();
expect(await app.navigation.findItem("red")).toBeUndefined();
expect(await note?.contextMenu.isColored("red")).toBe(false);
});
@@ -54,6 +56,7 @@ test("rename color", async ({ page }) => {
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.newColor({ title: "red", color: "#ff0000" });
await app.navigation.waitForItem("red");
const colorItem = await app.navigation.findItem("red");
await colorItem?.renameColor("priority-33");

View File

@@ -20,31 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { expect, test } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { NotesViewModel } from "./models/notes-view.model";
import { getTestId } from "./utils";
for (const item of [
{ id: "notebooks", title: "Notebooks" },
{ id: "tags", title: "Tags" }
]) {
test(`drag & hover over ${item.id} should navigate inside`, async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote({
title: `Test note`
});
const navigationItem = await app.navigation.findItem(item.title);
await note?.locator.hover();
await page.mouse.down();
await navigationItem?.locator.hover();
await navigationItem?.locator.hover();
await page.waitForTimeout(1000);
await expect(app.routeHeader).toHaveText(item.title);
});
test.skip(`drag & hover over ${item.id} should navigate inside`, () => {});
}
test(`drag & drop note over Favorites should make the note favorite`, async ({
@@ -85,24 +66,20 @@ test(`drag & drop note over a notebook should get assigned to the notebook`, asy
}) => {
const app = new AppModel(page);
await app.goto();
const notebooks = await app.goToNotebooks();
const notebook = await notebooks.createNotebook({ title: "Test notebook" });
const notes = await app.goToNotes();
const note = await notes.createNote({
title: `Test note`
});
const navigationItem = await app.navigation.findItem("Notebooks");
const notebooks = await app.goToNotebooks();
const notebook = await notebooks.createNotebook({ title: "Test notebook" });
await note?.locator.hover();
await page.mouse.down();
await navigationItem?.locator.hover();
await navigationItem?.locator.hover();
await page.waitForTimeout(1000);
await notebook?.locator.hover();
await notebook?.locator.hover();
await page.mouse.up();
const { notes: notebookNotes } = (await notebook?.openNotebook()) || {};
const notebookNotes = await notebook?.openNotebook();
expect(await notebookNotes?.findNote({ title: "Test note" })).toBeDefined();
});
@@ -111,19 +88,15 @@ test(`drag & drop note over a tag should get assigned to the tag`, async ({
}) => {
const app = new AppModel(page);
await app.goto();
const tags = await app.goToTags();
const tag = await tags.createItem({ title: "Tag" });
const notes = await app.goToNotes();
const note = await notes.createNote({
title: `Test note`
});
const navigationItem = await app.navigation.findItem("Tags");
const tags = await app.goToTags();
const tag = await tags.createItem({ title: "Tag" });
await note?.locator.hover();
await page.mouse.down();
await navigationItem?.locator.hover();
await navigationItem?.locator.hover();
await page.waitForTimeout(1000);
await tag?.locator.hover();
await tag?.locator.hover();
await page.mouse.up();
@@ -185,25 +158,20 @@ test(`drag & drop note over a nested notebook should get assigned to the noteboo
}) => {
const app = new AppModel(page);
await app.goto();
const notebooks = await app.goToNotebooks();
const notebook = await notebooks.createNotebook({
title: "Test notebook",
subNotebooks: [{ title: "Nested notebook" }]
});
const nestedNotebook = await (
await notebook?.openNotebook()
)?.subNotebooks.createNotebook({ title: "Nested notebook" });
const notes = await app.goToNotes();
const note = await notes.createNote({
title: `Test note`
});
const navigationItem = await app.navigation.findItem("Notebooks");
const notebooks = await app.goToNotebooks();
const notebook = await notebooks.createNotebook({
title: "Test notebook"
});
const nestedNotebook = await notebook?.createSubnotebook({
title: "Nested notebook"
});
await note?.locator.hover();
await page.mouse.down();
await navigationItem?.locator.hover();
await navigationItem?.locator.hover();
await page.waitForTimeout(1000);
await nestedNotebook?.locator.hover();
await nestedNotebook?.locator.hover();
await page.mouse.up();
@@ -212,46 +180,3 @@ test(`drag & drop note over a nested notebook should get assigned to the noteboo
const notebookNotes = new NotesViewModel(page, "notebook", "notes");
expect(await notebookNotes?.findNote({ title: "Test note" })).toBeDefined();
});
test(`drag & hover over a nested notebook should navigate inside`, async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notebooks = await app.goToNotebooks();
const notebook = await notebooks.createNotebook({
title: "Test notebook",
subNotebooks: [{ title: "Nested notebook" }]
});
const nestedNotebook = await (
await notebook?.openNotebook()
)?.subNotebooks.createNotebook({ title: "Nested notebook" });
const notes = await app.goToNotes();
const note = await notes.createNote({
title: `Test note`
});
const navigationItem = await app.navigation.findItem("Notebooks");
await navigationItem?.click();
await navigationItem?.click();
await app.goToNotes();
await note?.locator.hover();
await page.mouse.down();
await navigationItem?.locator.hover();
await navigationItem?.locator.hover();
await page.waitForTimeout(1000);
await notebook?.locator.hover();
await notebook?.locator.hover();
await page.waitForTimeout(1000);
await nestedNotebook?.locator.hover();
await nestedNotebook?.locator.hover();
await page.waitForTimeout(1000);
await page.keyboard.press("Escape");
expect(
await page
.locator(getTestId("notebook-header"))
.locator(getTestId("notebook-title"))
.textContent()
).toBe("Nested notebook");
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -30,6 +30,7 @@ import { SearchViewModel } from "./search-view-model";
import { SettingsViewModel } from "./settings-view.model";
import { ToastsModel } from "./toasts.model";
import { TrashViewModel } from "./trash-view.model";
import { ContextMenuModel } from "./context-menu.model";
export class AppModel {
readonly page: Page;
@@ -38,6 +39,7 @@ export class AppModel {
readonly auth: AuthModel;
readonly checkout: CheckoutModel;
readonly routeHeader: Locator;
private readonly profileDropdown: ContextMenuModel;
constructor(page: Page) {
this.page = page;
@@ -46,56 +48,72 @@ export class AppModel {
this.auth = new AuthModel(page);
this.checkout = new CheckoutModel(page);
this.routeHeader = this.page.locator(getTestId("routeHeader"));
this.profileDropdown = new ContextMenuModel(this.page);
}
async goto(isLoggedIn = false) {
await this.page.goto("/");
await this.routeHeader.waitFor({ state: "visible" });
if (!isLoggedIn) await this.navigation.waitForItem("Login");
}
goBack() {
const goBackButton = this.page.locator(getTestId("route-go-back"));
return goBackButton.click();
if (!isLoggedIn)
await this.page
.locator(getTestId("logged-in"))
.waitFor({ state: "hidden" });
}
async goToNotes() {
await this.page.locator(getTestId("tab-home")).click();
await this.navigateTo("Notes");
return new NotesViewModel(this.page, "home", "home");
}
async goToNotebooks() {
await this.navigateTo("Notebooks");
return new NotebooksViewModel(this.page);
await this.page.locator(getTestId("tab-notebooks")).click();
const model = new NotebooksViewModel(this.page);
await model.waitForList();
return model;
}
async goToFavorites() {
await this.page.locator(getTestId("tab-home")).click();
await this.navigateTo("Favorites");
return new NotesViewModel(this.page, "notes", "favorites");
}
async goToReminders() {
await this.page.locator(getTestId("tab-home")).click();
await this.navigateTo("Reminders");
return new RemindersViewModel(this.page);
}
async goToTags() {
await this.navigateTo("Tags");
return new ItemsViewModel(this.page);
await this.page.locator(getTestId("tab-tags")).click();
const model = new ItemsViewModel(this.page);
await model.waitForList();
return model;
}
async goToHome() {
await this.page.locator(getTestId("tab-home")).click();
}
async goToColor(color: string) {
await this.page.locator(getTestId("tab-home")).click();
await this.navigateTo(color);
return new NotesViewModel(this.page, "notes", "notes");
}
async goToTrash() {
await this.page.locator(getTestId("tab-home")).click();
await this.navigateTo("Trash");
return new TrashViewModel(this.page);
}
async goToSettings() {
await this.navigateTo("Settings");
await this.profileDropdown.open(
this.page.locator(getTestId("profile-dropdown")),
"left"
);
await this.profileDropdown.clickOnItem("settings");
return new SettingsViewModel(this.page);
}
@@ -111,7 +129,7 @@ export class AppModel {
async getRouteHeader() {
if (!(await this.routeHeader.isVisible())) return;
return await this.routeHeader.innerText();
return await this.routeHeader.getAttribute("data-header");
}
async isSynced() {

View File

@@ -51,7 +51,11 @@ export class BaseViewModel {
.locator(getTestId("group-header"));
for await (const item of iterateList(locator)) {
if ((await item.locator(getTestId("title")).textContent()) === groupName)
if (
(
await item.locator(getTestId("title")).textContent()
)?.toLowerCase() === groupName.toLowerCase()
)
return item;
}
return undefined;
@@ -95,7 +99,9 @@ export class BaseViewModel {
// }
async press(key: string) {
const itemList = this.list.locator(getTestId(`virtuoso-item-list`, "data-testid"));
const itemList = this.list.locator(
getTestId(`virtuoso-item-list`, "data-testid")
);
await itemList.press(key);
await this.page.waitForTimeout(300);
}

View File

@@ -93,7 +93,6 @@ export class EditorModel {
}
async waitForUnloading() {
await this.page.waitForURL(/#\/notes\/?.+\/create/gm);
await this.searchButton.isDisabled();
await this.page
.locator(".active")
@@ -105,7 +104,6 @@ export class EditorModel {
}
async waitForSaving() {
await this.page.waitForURL(/#\/notes\/?.+\/edit/gm);
await this.page.locator(".active").locator(getTestId("tags")).waitFor();
await this.searchButton.waitFor();
await this.wordCountText.waitFor();

View File

@@ -28,25 +28,22 @@ export class ItemsViewModel extends BaseViewModel {
private readonly createButton: Locator;
constructor(page: Page) {
super(page, "tags", "tags");
this.createButton = page.locator(getTestId(`tags-action-button`));
this.createButton = page.locator(getTestId(`create-tag-button`));
}
async createItem(item: Item) {
const titleToCompare = `#${item.title}`;
await this.createButton.first().click();
await fillItemDialog(this.page, item);
await this.waitForItem(titleToCompare);
await this.waitForItem(item.title);
return await this.findItem(item);
}
async findItem(item: Item) {
const titleToCompare = `#${item.title}`;
for await (const _item of this.iterateItems()) {
const itemModel = new ItemModel(_item, "tag");
const title = await itemModel.getTitle();
if (title === titleToCompare) return itemModel;
if (title === item.title) return itemModel;
}
return undefined;
}

View File

@@ -134,14 +134,14 @@ abstract class BaseProperties {
export class NotePropertiesModel extends BaseProperties {
private readonly propertiesButton: Locator;
private readonly propertiesCloseButton: Locator;
private readonly generalSection: Locator;
private readonly readonlyToggle: ToggleModel;
private readonly sessionItems: Locator;
constructor(page: Page, noteLocator: Locator) {
super(page, noteLocator, "properties");
this.propertiesButton = page.locator(getTestId("Properties"));
this.propertiesCloseButton = page.locator(getTestId("properties-close"));
this.generalSection = page.locator(getTestId("general-section"));
this.readonlyToggle = new ToggleModel(page, `properties-readonly`);
this.sessionItems = page.locator(getTestId("session-item"));
}
@@ -183,12 +183,11 @@ export class NotePropertiesModel extends BaseProperties {
async open() {
await this.propertiesButton.click();
await this.propertiesCloseButton.waitFor();
await this.page.waitForTimeout(1000);
await this.generalSection.waitFor();
}
async close() {
await this.propertiesCloseButton.click();
await this.propertiesButton.click();
}
async getSessionHistory() {

View File

@@ -25,21 +25,21 @@ import { Notebook } from "./types";
import { confirmDialog, fillNotebookDialog } from "./utils";
import { NotesViewModel } from "./notes-view.model";
import { getTestId } from "../utils";
import { SubnotebooksViewModel } from "./subnotebooks-view.model";
import { NotebooksViewModel } from "./notebooks-view.model";
export class NotebookItemModel extends BaseItemModel {
private readonly contextMenu: ContextMenuModel;
constructor(locator: Locator) {
constructor(
locator: Locator,
private readonly notebooks: NotebooksViewModel
) {
super(locator);
this.contextMenu = new ContextMenuModel(this.page);
}
async openNotebook() {
await this.locator.click();
return {
subNotebooks: new SubnotebooksViewModel(this.page),
notes: new NotesViewModel(this.page, "notebook", "notes")
};
return new NotesViewModel(this.page, "notebook", "notes");
}
async editNotebook(notebook: Notebook) {
@@ -49,6 +49,16 @@ export class NotebookItemModel extends BaseItemModel {
await fillNotebookDialog(this.page, notebook);
}
async createSubnotebook(notebook: Notebook) {
await this.contextMenu.open(this.locator);
await this.contextMenu.clickOnItem("add");
await fillNotebookDialog(this.page, notebook);
await this.notebooks.waitForItem(notebook.title);
return await this.notebooks.findNotebook(notebook);
}
async moveToTrash(deleteContainedNotes = false) {
await this.contextMenu.open(this.locator);
await this.contextMenu.clickOnItem("movetotrash");

View File

@@ -30,7 +30,7 @@ export class NotebooksViewModel extends BaseViewModel {
constructor(page: Page) {
super(page, "notebooks", "notebooks");
this.createButton = page
.locator(getTestId("notebooks-action-button"))
.locator(getTestId("create-notebook-button"))
.first();
}
@@ -45,7 +45,7 @@ export class NotebooksViewModel extends BaseViewModel {
async findNotebook(notebook: Partial<Notebook>) {
for await (const item of this.iterateItems()) {
const notebookModel = new NotebookItemModel(item);
const notebookModel = new NotebookItemModel(item, this);
if ((await notebookModel.getTitle()) === notebook.title)
return notebookModel;
}

View File

@@ -38,10 +38,7 @@ export class NotesViewModel extends BaseViewModel {
listType: string
) {
super(page, pageId, listType);
this.createButton = page.locator(
// TODO:
getTestId(`notes-action-button`)
);
this.createButton = page.locator(getTestId(`create-new-note`));
this.editor = new EditorModel(page);
}

View File

@@ -30,7 +30,7 @@ export class RemindersViewModel extends BaseViewModel {
constructor(page: Page) {
super(page, "reminders", "reminders");
this.createButton = page
.locator(getTestId("reminders-action-button"))
.locator(getTestId("create-reminder-button"))
.first();
}

View File

@@ -56,8 +56,8 @@ export class SettingsViewModel {
await confirmDialog(this.page.locator(getTestId("confirm-dialog")));
await this.page
.locator(getTestId("not-logged-in"))
.waitFor({ state: "visible" });
.locator(getTestId("logged-in"))
.waitFor({ state: "hidden" });
}
async getRecoveryKey(password: string) {

View File

@@ -27,11 +27,9 @@ function createRoute(key: string, header: string) {
const routes = [
createRoute("notes", "Notes"),
createRoute("notebooks", "Notebooks"),
createRoute("favorites", "Favorites"),
createRoute("monographs", "Monographs"),
createRoute("reminders", "Reminders"),
createRoute("tags", "Tags"),
createRoute("trash", "Trash")
];

View File

@@ -43,7 +43,8 @@ test("create a note inside a notebook", async ({ page }) => {
await app.goto();
const notebooks = await app.goToNotebooks();
const notebook = await notebooks.createNotebook(NOTEBOOK);
const { notes } = (await notebook?.openNotebook()) || {};
const notes = await notebook?.openNotebook();
await notes?.waitForList();
const note = await notes?.createNote(NOTE);
@@ -55,11 +56,10 @@ test("create a note inside a subnotebook", async ({ page }) => {
await app.goto();
const notebooks = await app.goToNotebooks();
const notebook = await notebooks.createNotebook(NOTEBOOK);
const { subNotebooks } = (await notebook?.openNotebook()) || {};
const subNotebook = await subNotebooks?.createNotebook({
const subNotebook = await notebook?.createSubnotebook({
title: "Subnotebook 1"
});
const { notes } = (await subNotebook?.openNotebook()) || {};
const notes = await subNotebook?.openNotebook();
const note = await notes?.createNote(NOTE);
@@ -80,7 +80,6 @@ test("edit a notebook", async ({ page }) => {
const editedNotebook = await notebooks.findNotebook(item);
expect(editedNotebook).toBeDefined();
expect(await editedNotebook?.getDescription()).toBe(item.description);
});
test("delete a notebook", async ({ page }) => {
@@ -129,29 +128,6 @@ test("permanently delete a notebook", async ({ page }) => {
await expect(trashItem.locator).toBeHidden();
});
test("pin a notebook", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notebooks = await app.goToNotebooks();
const notebook = await notebooks.createNotebook(NOTEBOOK);
await notebook?.pin();
expect(await notebook?.isPinned()).toBe(true);
});
test("unpin a notebook", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notebooks = await app.goToNotebooks();
const notebook = await notebooks.createNotebook(NOTEBOOK);
await notebook?.pin();
await notebook?.unpin();
expect(await notebook?.isPinned()).toBe(false);
});
test("create shortcut of a notebook", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
@@ -161,8 +137,8 @@ test("create shortcut of a notebook", async ({ page }) => {
await notebook?.createShortcut();
expect(await notebook?.isShortcut()).toBe(true);
const allShortcuts = await app.navigation.getShortcuts();
expect(allShortcuts.includes(NOTEBOOK.title)).toBeTruthy();
await app.goToHome();
expect(await app.navigation.findItem(NOTEBOOK.title)).toBeDefined();
});
test("remove shortcut of a notebook", async ({ page }) => {
@@ -184,14 +160,13 @@ test("delete all notes within a notebook", async ({ page }) => {
await app.goto();
const notebooks = await app.goToNotebooks();
const notebook = await notebooks.createNotebook(NOTEBOOK);
let { notes } = (await notebook?.openNotebook()) || {};
let notes = await notebook?.openNotebook();
for (let i = 0; i < 2; ++i) {
await notes?.createNote({
title: `Note ${i}`,
content: NOTE.content
});
}
await app.goBack();
await notebook?.moveToTrash(true);
@@ -254,20 +229,17 @@ test(`sort notebooks`, async ({ page }, info) => {
await notebooks.createNotebook(NOTEBOOK);
}
for (const groupBy of groupByOptions) {
for (const sortBy of sortByOptions) {
for (const orderBy of orderByOptions) {
await test.step(`group by ${groupBy}, sort by ${sortBy}, order by ${orderBy}`, async () => {
const sortResult = await notebooks?.sort({
groupBy,
orderBy,
sortBy
});
if (!sortResult) return;
await expect(notebooks.items).toHaveCount(titles.length);
for (const sortBy of sortByOptions) {
for (const orderBy of orderByOptions) {
await test.step(`sort by ${sortBy}, order by ${orderBy}`, async () => {
const sortResult = await notebooks?.sort({
orderBy,
sortBy
});
}
if (!sortResult) return;
await expect(notebooks.items).toHaveCount(titles.length);
});
}
}
});

View File

@@ -99,8 +99,8 @@ test("create shortcut of a tag", async ({ page }) => {
await tag?.createShortcut();
expect(await tag?.isShortcut()).toBe(true);
const allShortcuts = await app.navigation.getShortcuts();
expect(allShortcuts.includes("hello-world")).toBeTruthy();
await app.goToHome();
expect(await app.navigation.findItem("hello-world")).toBeDefined();
});
test("remove shortcut of a tag", async ({ page }) => {
@@ -204,20 +204,17 @@ test(`sort tags`, async ({ page }, info) => {
if (!tag) continue;
}
for (const groupBy of groupByOptions) {
for (const sortBy of sortByOptions) {
for (const orderBy of orderByOptions) {
await test.step(`group by ${groupBy}, sort by ${sortBy}, order by ${orderBy}`, async () => {
const sortResult = await tags?.sort({
groupBy,
orderBy,
sortBy
});
if (!sortResult) return;
await expect(tags.items).toHaveCount(titles.length);
for (const sortBy of sortByOptions) {
for (const orderBy of orderByOptions) {
await test.step(`sort by ${sortBy}, order by ${orderBy}`, async () => {
const sortResult = await tags?.sort({
orderBy,
sortBy
});
}
if (!sortResult) return;
await expect(tags.items).toHaveCount(titles.length);
});
}
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "3.0.29",
"version": "3.1.0-beta.1",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -75,7 +75,7 @@
"react-modal": "3.16.3",
"react-qrcode-logo": "^2.9.0",
"react-scroll-sync": "^0.11.2",
"react-virtuoso": "^4.6.2",
"react-virtuoso": "^4.12.3",
"snarkdown": "^2.0.0",
"timeago.js": "4.0.2",
"w3c-keyname": "^2.2.6",

View File

@@ -48,11 +48,9 @@ import { AnnouncementDialog } from "./dialogs/announcement-dialog";
import { logger } from "./utils/logger";
import { strings } from "@notesnook/intl";
type AppEffectsProps = {
setShow: (show: boolean) => void;
};
export default function AppEffects({ setShow }: AppEffectsProps) {
export default function AppEffects() {
const refreshNavItems = useStore((store) => store.refreshNavItems);
const toggleListPane = useStore((store) => store.toggleListPane);
const updateLastSynced = useStore((store) => store.updateLastSynced);
const isFocusMode = useStore((store) => store.isFocusMode);
const initUser = useUserStore((store) => store.init);
@@ -234,7 +232,7 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
}, []);
useEffect(() => {
setShow(!isFocusMode);
toggleListPane(!isFocusMode);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFocusMode]);

View File

@@ -1,8 +1,3 @@
:root {
--sash-size: 10px;
--sash-hover-size: 4px;
}
.tabsScroll,
.titlebarLogo,
.theme-scope-titleBar,
@@ -18,101 +13,77 @@
-webkit-app-region: no-drag;
}
/* open-sans-regular - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-family: "Inter";
font-style: normal;
font-display: swap;
font-weight: 400;
src: local(""),
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2")
format("woff2"),
/* Super Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff")
format("woff"),
/* Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf")
format("truetype");
src: local(""), url("./assets/fonts/Inter-Regular.woff2") format("woff2"),
url("./assets/fonts/Inter-Regular.ttf") format("truetype");
}
/* open-sans-600 - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-family: "Inter";
font-style: normal;
font-display: swap;
font-weight: 600;
font-display: swap;
src: local(""),
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.woff2")
format("woff2"),
/* Super Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.woff")
format("woff"),
/* Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600.ttf")
format("truetype");
src: local(""), url("./assets/fonts/Inter-SemiBold.woff2") format("woff2"),
url("./assets/fonts/Inter-SemiBold.ttf") format("truetype");
}
/* open-sans-700 - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
src: local(""),
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2")
format("woff2"),
/* Super Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700.woff")
format("woff"),
/* Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700.ttf")
format("truetype");
font-weight: 700;
src: local(""), url("./assets/fonts/Inter-Bold.woff2") format("woff2"),
url("./assets/fonts/Inter-Bold.ttf") format("truetype");
}
/* open-sans-italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-family: "Inter";
font-style: normal;
font-display: swap;
font-weight: 500;
src: local(""), url("./assets/fonts/Inter-Medium.woff2") format("woff2"),
url("./assets/fonts/Inter-Medium.ttf") format("truetype");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-display: swap;
font-weight: 400;
font-display: swap;
src: local(""),
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2")
format("woff2"),
/* Super Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff")
format("woff"),
/* Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-italic.ttf")
format("truetype");
src: local(""), url("./assets/fonts/Inter-Italic.woff2") format("woff2"),
url("./assets/fonts/Inter-Italic.ttf") format("truetype");
}
/* open-sans-600italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-family: "Inter";
font-style: italic;
font-display: swap;
font-weight: 600;
font-display: swap;
src: local(""),
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.woff2")
format("woff2"),
/* Super Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.woff")
format("woff"),
/* Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-600italic.ttf")
format("truetype");
url("./assets/fonts/Inter-SemiBoldItalic.woff2") format("woff2"),
url("./assets/fonts/Inter-SemiBoldItalic.ttf") format("truetype");
}
/* open-sans-700italic - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: "Open Sans";
font-family: "Inter";
font-style: italic;
font-weight: 700;
font-display: swap;
src: local(""),
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2")
format("woff2"),
/* Super Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff")
format("woff"),
/* Modern Browsers */
url("./assets/fonts/open-sans-v34-vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic-700italic.ttf")
format("truetype");
font-weight: 700;
src: local(""), url("./assets/fonts/Inter-BoldItalic.woff2") format("woff2"),
url("./assets/fonts/Inter-BoldItalic.ttf") format("truetype");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-display: swap;
font-weight: 500;
src: local(""), url("./assets/fonts/Inter-MediumItalic.woff2") format("woff2"),
url("./assets/fonts/Inter-MediumItalic.ttf") format("truetype");
}
.rpv-core__text-layer,

View File

@@ -52,7 +52,6 @@ new WebExtensionRelay();
function App() {
const isMobile = useMobile();
const [show, setShow] = useState(true);
const isFocusMode = useStore((store) => store.isFocusMode);
const { isFocused } = useWindowFocus();
const { isFullscreen } = useWindowControls();
@@ -68,9 +67,6 @@ function App() {
.nav-pane {
opacity: 0.7;
}
.titlebar {
background: var(--background-secondary) !important;
}
`}
/>
)}
@@ -78,15 +74,9 @@ function App() {
<Global
// These styles to make sure the app content doesn't overlap with the traffic lights.
styles={`
.nav-pane,
.mobile-nav-pane {
margin-top: env(titlebar-area-height) !important;
height: calc(100% - env(titlebar-area-height)) !important;
}
.nav-pane.collapsed + .list-pane .route-container-header,
.nav-pane.collapsed + .list-pane.collapsed + .editor-pane .editor-action-bar,
.nav-pane.collapsed + .editor-pane .editor-action-bar {
padding-left: 25px;
.nav-pane .theme-scope-navigationMenu,
.mobile-nav-pane .theme-scope-navigationMenu {
padding-top: env(titlebar-area-height) !important;
}
.editor-pane:first-of-type .editor-action-bar,
.mobile-editor-pane.pane-active .editor-action-bar,
@@ -102,9 +92,9 @@ function App() {
.route-container-header .routeHeader {
font-size: ${getFontSizes().title};
}
.global-split-pane .react-split__sash {
height: calc(100% - ${TITLE_BAR_HEIGHT}px);
}
// .global-split-pane .react-split__sash {
// height: calc(100% - ${TITLE_BAR_HEIGHT}px);
// }
`}
/>
) : null}
@@ -114,7 +104,7 @@ function App() {
<GlobalMenuWrapper />
</div>
</Suspense>
<AppEffects setShow={setShow} />
<AppEffects />
<Flex
id="app"
@@ -126,11 +116,7 @@ function App() {
height: "100%"
}}
>
{isMobile ? (
<MobileAppContents />
) : (
<DesktopAppContents setShow={setShow} show={show} />
)}
{isMobile ? <MobileAppContents /> : <DesktopAppContents />}
<Toaster
containerClassName="toasts-container"
containerStyle={{ bottom: STATUS_BAR_HEIGHT + 10 }}
@@ -142,14 +128,11 @@ function App() {
export default App;
type DesktopAppContentsProps = {
show: boolean;
setShow: (show: boolean) => void;
};
function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
function DesktopAppContents() {
const isFocusMode = useStore((store) => store.isFocusMode);
const isListPaneVisible = useStore((store) => store.isListPaneVisible);
const isTablet = useTablet();
const [isNarrow, setIsNarrow] = useState(isTablet || false);
// const [isNarrow, setIsNarrow] = useState(isTablet || false);
const navPane = useRef<SplitPaneImperativeHandle>(null);
useEffect(() => {
@@ -171,27 +154,26 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
autoSaveId="global-panel-group"
direction="vertical"
onChange={(sizes) => {
setIsNarrow(sizes[0] <= 70);
useStore.setState({ isNavPaneCollapsed: sizes[0] <= 70 });
}}
>
{isFocusMode ? null : (
<Pane
id="nav-pane"
initialSize={180}
initialSize={isTablet ? 0 : 250}
className={`nav-pane`}
snapSize={160}
minSize={50}
snapSize={120}
maxSize={300}
maxSize={isTablet ? 0 : 500}
style={{
overflow: "initial",
zIndex: 3
}}
>
<NavigationMenu
toggleNavigationContainer={(state) => {
setShow(state || !show);
}}
isTablet={isNarrow}
/>
<NavigationMenu onExpand={() => navPane.current?.reset(0)} />
</Pane>
)}
{!isFocusMode && show ? (
{!isFocusMode && isListPaneVisible ? (
<Pane
id="list-pane"
initialSize={380}
@@ -301,7 +283,7 @@ function MobileAppContents() {
flexShrink: 0
}}
>
<NavigationMenu toggleNavigationContainer={() => {}} isTablet={false} />
<NavigationMenu />
</Flex>
<Flex
className="mobile-list-pane"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -46,6 +46,7 @@ import { strings } from "@notesnook/intl";
import { ABYTES, streamablefs } from "../interfaces/fs";
import { type ZipEntry } from "../utils/streams/unzip-stream";
import { ZipFile } from "../utils/streams/zip-stream";
import { ConfirmDialog, showLogoutConfirmation } from "../dialogs/confirm";
export const CREATE_BUTTON_MAP = {
notes: {
@@ -421,3 +422,33 @@ async function restore(
);
}
}
export async function logout() {
const result = await showLogoutConfirmation();
if (!result) return;
if (result.backup) {
try {
await createBackup({ mode: "partial" });
} catch (e) {
logger.error(e, "Failed to take backup before logout");
if (
!(await ConfirmDialog.show({
title: strings.failedToTakeBackup(),
message: strings.failedToTakeBackupMessage(),
negativeButtonText: strings.no(),
positiveButtonText: strings.yes()
}))
)
return;
}
}
await TaskManager.startTask({
type: "modal",
title: strings.loggingOut(),
subtitle: strings.pleaseWait(),
action: () => db.user.logout(true)
});
showToast("success", strings.loggedOut());
}

View File

@@ -33,7 +33,7 @@ function Announcements() {
const dismiss = useAnnouncementStore((store) => store.dismiss);
const announcement = announcements[0];
if (!announcement) return <Notice />;
if (!announcement) return null;
return (
<Flex
mx={1}

View File

@@ -130,7 +130,7 @@ export function Attachment({
return (
<Box
as="tr"
sx={{ height: 30, ":hover": { bg: "hover" } }}
sx={{ height: 25, ":hover": { bg: "hover" } }}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -165,7 +165,8 @@ export function Attachment({
<td>
<Flex
sx={{
alignItems: "center"
alignItems: "center",
ml: 2
}}
>
{status ? (

View File

@@ -18,12 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef } from "react";
import { getHomeRoute, NavigationEvents } from "../../navigation";
import { getHomeRoute, navigate, NavigationEvents } from "../../navigation";
import { store as selectionStore } from "../../stores/selection-store";
import { useStore as useSearchStore } from "../../stores/search-store";
import useRoutes from "../../hooks/use-routes";
import RouteContainer from "../route-container";
import routes from "../../navigation/routes";
import { isRouteResult } from "../../navigation/types";
import { Freeze } from "react-freeze";
import { Flex } from "@theme-ui/components";
@@ -31,7 +31,16 @@ function CachedRouter() {
const [RouteResult, location] = useRoutes(routes, {
fallbackRoute: getHomeRoute(),
hooks: {
beforeNavigate: () => selectionStore.toggleSelectionMode(false)
beforeNavigate: (location) => {
selectionStore.toggleSelectionMode(false);
useSearchStore.getState().resetSearch();
if (location === "/") {
console.log("Redirecting to", getHomeRoute());
navigate(getHomeRoute());
return false;
}
return true;
}
}
});
const cachedRoutes = useRef<Record<string, React.FunctionComponent>>({});
@@ -41,17 +50,17 @@ function CachedRouter() {
NavigationEvents.publish("onNavigate", RouteResult, location);
}, [RouteResult, location]);
if (!RouteResult || !isRouteResult(RouteResult)) return null;
if (RouteResult.key === "general" || !cachedRoutes.current[RouteResult.key])
if (!RouteResult) return null;
if (
RouteResult.key === "general" ||
!cachedRoutes.current[RouteResult.key] ||
RouteResult.noCache
)
cachedRoutes.current[RouteResult.key] =
RouteResult.component as React.FunctionComponent;
return (
<RouteContainer
type={RouteResult.type}
title={RouteResult.title}
buttons={RouteResult.buttons}
>
<RouteContainer {...RouteResult}>
{Object.entries(cachedRoutes.current).map(([key, Component]) => (
<Freeze key={key} freeze={key !== RouteResult.key}>
<Flex

View File

@@ -23,15 +23,11 @@ import {
ArrowLeft,
ArrowRight,
Cross,
EditorFullWidth,
EditorNormalWidth,
ExitFullscreen,
FocusMode,
Fullscreen,
Icon,
Lock,
NewTab,
NormalMode,
Note,
NoteAdd,
NoteRemove,
Pin,
Plus,
@@ -82,9 +78,20 @@ import { showPublishView } from "../publish-view";
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
import useMobile from "../../hooks/use-mobile";
import { strings } from "@notesnook/intl";
import { TITLE_BAR_HEIGHT, getWindowControls } from "../title-bar";
import { getWindowControls } from "../title-bar";
import useTablet from "../../hooks/use-tablet";
import { isMac } from "../../utils/platform";
import { CREATE_BUTTON_MAP } from "../../common";
type ToolButton = {
title: string;
icon: Icon;
enabled?: boolean;
hidden?: boolean;
hideOnMobile?: boolean;
toggled?: boolean;
onClick: () => void;
};
export function EditorActionBar() {
const { isMaximized, isFullscreen, hasNativeWindowControls } =
@@ -98,6 +105,10 @@ export function EditorActionBar() {
activeSession?.id ? store.editors[activeSession?.id] : undefined
);
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
const arePropertiesVisible = useEditorStore(
(store) => store.arePropertiesVisible
);
const isTOCVisible = useEditorStore((store) => store.isTOCVisible);
const monographs = useMonographStore((store) => store.monographs);
const isNotePublished =
activeSession &&
@@ -106,7 +117,7 @@ export function EditorActionBar() {
const isMobile = useMobile();
const isTablet = useTablet();
const tools = [
const tools: ToolButton[] = [
{
title: strings.newTab(),
icon: NewTab,
@@ -150,7 +161,8 @@ export function EditorActionBar() {
activeSession.type !== "locked" &&
activeSession.type !== "diff" &&
activeSession.type !== "conflicted",
onClick: () => useEditorStore.getState().toggleTableOfContents()
onClick: () => useEditorStore.getState().toggleTableOfContents(),
toggled: isTOCVisible
},
{
title: strings.search(),
@@ -161,7 +173,7 @@ export function EditorActionBar() {
activeSession.type !== "locked" &&
activeSession.type !== "diff" &&
activeSession.type !== "conflicted",
onClick: editorManager?.editor?.startSearch
onClick: () => editorManager?.editor?.startSearch()
},
{
title: strings.properties(),
@@ -173,7 +185,8 @@ export function EditorActionBar() {
activeSession.type !== "diff" &&
activeSession.type !== "conflicted" &&
!isFocusMode,
onClick: () => useEditorStore.getState().toggleProperties()
onClick: () => useEditorStore.getState().toggleProperties(),
toggled: arePropertiesVisible
},
...getWindowControls(
hasNativeWindowControls,
@@ -216,6 +229,7 @@ export function EditorActionBar() {
: 1,
pl: 1,
borderLeft: "1px solid var(--border)",
borderBottom: "1px solid var(--border)",
flexShrink: 0
}}
>
@@ -229,7 +243,7 @@ export function EditorActionBar() {
sx={{
p: 1,
alignItems: "center",
bg: "transparent",
bg: tool.toggled ? "background-selected" : "transparent",
display: [
"hideOnMobile" in tool && tool.hideOnMobile ? "none" : "flex",
tool.hidden ? "none" : "flex"
@@ -265,11 +279,24 @@ const TabStrip = React.memo(function TabStrip() {
sx={{
px: 1,
borderRight: "1px solid var(--border)",
borderBottom: "1px solid var(--border)",
alignItems: "center",
flexShrink: 0
}}
onDoubleClick={(e) => e.stopPropagation()}
>
<Button
variant="accent"
{...CREATE_BUTTON_MAP.notes}
data-test-id={`create-new-note`}
sx={{
p: 1,
borderRadius: "100%",
mr: "small"
}}
>
<Plus size={16} color="accentForeground" />
</Button>
<Button
disabled={!canGoBack}
onClick={() => useEditorStore.getState().goBack()}
@@ -292,7 +319,7 @@ const TabStrip = React.memo(function TabStrip() {
<ScrollContainer
className="tabsScroll"
suppressScrollY
style={{ flex: 1, height: TITLE_BAR_HEIGHT }}
style={{ flex: 1, height: "100%" }}
trackStyle={() => ({
backgroundColor: "transparent",
"--ms-track-size": "6px"
@@ -415,6 +442,9 @@ const TabStrip = React.memo(function TabStrip() {
);
}}
/>
<div
style={{ width: "100%", borderBottom: "1px solid var(--border)" }}
/>
</Flex>
</ScrollContainer>
</Flex>
@@ -495,6 +525,9 @@ function Tab(props: TabProps) {
cursor: "pointer",
pl: 2,
borderRight: "1px solid var(--border)",
borderBottom: isActive
? "1px solid transparent"
: "1px solid var(--border)",
":last-of-type": { borderRight: 0 },
transform: CSS.Transform.toString(transform),
@@ -509,7 +542,7 @@ function Tab(props: TabProps) {
"& .closeTabButton": {
opacity: 1
},
bg: isActive ? "hover-selected" : "hover"
bg: isActive ? "background-selected" : "hover"
}
}}
onContextMenu={(e) => {
@@ -553,14 +586,13 @@ function Tab(props: TabProps) {
onClick: onRevealInList,
isHidden: !onRevealInList
},
{ type: "separator", key: "sep2" },
{ type: "separator", key: "sep2", isHidden: !onRevealInList },
{
type: "button",
key: "pin",
title: strings.pin(),
onClick: onPin,
isChecked: isPinned,
icon: Pin.path
isChecked: isPinned
}
]);
}}
@@ -576,7 +608,11 @@ function Tab(props: TabProps) {
data-test-id={`tab-icon${isUnsaved ? "-unsaved" : ""}`}
size={14}
color={
isUnsaved ? "accent-error" : isActive ? "accent-selected" : "icon"
isUnsaved
? "icon-error"
: isActive
? "icon-selected"
: "icon-secondary"
}
/>
<Text
@@ -588,7 +624,7 @@ function Tab(props: TabProps) {
overflowX: "hidden",
pointerEvents: "none",
maxWidth: 120,
color: isActive ? "paragraph-selected" : "paragraph"
color: isActive ? "paragraph-selected" : "paragraph-secondary"
}}
ml={1}
>

View File

@@ -119,7 +119,8 @@ const deferredSave = debounceWithId(saveContent, 100);
export default function TabsView() {
const tabs = useEditorStore((store) => store.tabs);
const documentPreview = useEditorStore((store) => store.documentPreview);
const activeTab = useEditorStore((store) => store.getActiveTab());
const activeTabId = useEditorStore((store) => store.activeTabId);
const activeSession = useEditorStore((store) => store.getActiveSession());
const arePropertiesVisible = useEditorStore(
(store) => store.arePropertiesVisible
);
@@ -128,17 +129,6 @@ export default function TabsView() {
return (
<>
<Flex
className="editor-action-bar"
sx={{
zIndex: 2,
height: TITLE_BAR_HEIGHT,
borderBottom: "1px solid var(--border)"
}}
>
<EditorActionBar />
</Flex>
<ScopedThemeProvider
scope="editor"
ref={dropRef}
@@ -150,7 +140,24 @@ export default function TabsView() {
flexDirection: "column"
}}
>
<SplitPane direction="vertical" autoSaveId={"editor-panels"}>
<Flex
className="editor-action-bar"
sx={{
zIndex: 2,
height: TITLE_BAR_HEIGHT,
bg: "background-secondary"
// borderBottom: "1px solid var(--border)"
}}
>
<EditorActionBar />
</Flex>
<SplitPane
style={{
position: "relative"
}}
direction="vertical"
autoSaveId={"editor-panels"}
>
<Pane id="editor-panel" className="editor-pane">
{tabs.map((tab) => {
const session = useEditorStore
@@ -158,7 +165,7 @@ export default function TabsView() {
.getSession(tab.sessionId);
if (!session) return null;
return (
<Freeze key={session.id} freeze={tab.id !== activeTab?.id}>
<Freeze key={session.id} freeze={tab.id !== activeTabId}>
{session.type === "locked" ? (
<UnlockNoteView session={session} />
) : session.type === "conflicted" ||
@@ -209,16 +216,18 @@ export default function TabsView() {
</Pane>
) : null}
{isTOCVisible && activeTab ? (
{isTOCVisible && activeSession ? (
<Pane id="table-of-contents-pane" initialSize={300} minSize={300}>
<TableOfContents sessionId={activeTab.sessionId} />
<TableOfContents sessionId={activeSession.id} />
</Pane>
) : null}
{arePropertiesVisible && activeSession && (
<Pane id="properties-pane" initialSize={250} minSize={250}>
<Properties sessionId={activeSession.id} />
</Pane>
)}
</SplitPane>
<DropZone overlayRef={overlayRef} />
{arePropertiesVisible && activeTab && (
<Properties sessionId={activeTab.sessionId} />
)}
</ScopedThemeProvider>
</>
);

View File

@@ -40,7 +40,7 @@ class EditorManager extends BaseStore<EditorManager> {
toolbarConfig?: ToolbarDefinition;
editorConfig: EditorConfig = Config.get("editorConfig", {
fontFamily: "sans-serif",
fontSize: 16,
fontSize: 14,
zoom: EDITOR_ZOOM.DEFAULT
});
editors: Record<string, EditorContext> = {};

View File

@@ -36,17 +36,22 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useState } from "react";
import { Cross } from "../icons";
import React, { useEffect, useLayoutEffect, useState } from "react";
import { ChevronDown, ChevronRight, Circle, Cross } from "../icons";
import { useEditorStore } from "../../stores/editor-store";
import ScrollContainer from "../scroll-container";
import { ScopedThemeProvider } from "../theme-provider";
import { Section } from "../properties";
import { scrollIntoViewById } from "@notesnook/editor";
import { scrollIntoViewById, TOCItem } from "@notesnook/editor";
import { Button, Flex, Text } from "@theme-ui/components";
import { useEditorManager } from "./manager";
import { TITLE_BAR_HEIGHT } from "../title-bar";
import { strings } from "@notesnook/intl";
import {
TreeNode,
VirtualizedTree,
VirtualizedTreeHandle
} from "../virtualized-tree";
type TableOfContentsProps = {
sessionId: string;
@@ -54,34 +59,35 @@ type TableOfContentsProps = {
function TableOfContents(props: TableOfContentsProps) {
const { sessionId } = props;
const [active, setActive] = useState<string[]>([]);
const toggleTableOfContents = useEditorStore(
(store) => store.toggleTableOfContents
);
const treeRef = React.useRef<VirtualizedTreeHandle<TOCItem>>(null);
const tableOfContents = useEditorManager(
(store) => store.editors[sessionId]?.tableOfContents || []
);
useEffect(() => {
useLayoutEffect(() => {
treeRef.current?.refresh();
const editorScroll = document.getElementById(`editorScroll_${sessionId}`);
if (!editorScroll) return;
function onScroll() {
const scrollTop = editorScroll?.scrollTop || 0;
const height = editorScroll?.clientHeight || 0;
const active = tableOfContents.filter((t, i, array) => {
const next = array.at(i + 1);
for (let i = 0; i < tableOfContents.length; i++) {
const toc = tableOfContents[i];
const element = document.getElementById(`toc-${toc.id}`);
if (!element) continue;
const next = tableOfContents.at(i + 1);
const viewport = scrollTop + height - 160;
const lessThanNext = next ? scrollTop <= next.top : true;
const isInViewport = t.top <= viewport && t.top >= scrollTop;
const isActive = scrollTop >= t.top && lessThanNext;
return isInViewport || isActive;
});
setActive(active.map((a) => a.id));
const isInViewport = toc.top <= viewport && toc.top >= scrollTop;
const isActive = scrollTop >= toc.top && lessThanNext;
element.classList.toggle("active", isActive || isInViewport);
}
}
editorScroll.addEventListener("scroll", onScroll);
editorScroll?.addEventListener("scroll", onScroll);
onScroll();
return () => {
editorScroll.removeEventListener("scroll", onScroll);
editorScroll?.removeEventListener("scroll", onScroll);
};
}, [sessionId, tableOfContents]);
@@ -107,56 +113,98 @@ function TableOfContents(props: TableOfContentsProps) {
flexDirection: "column"
}}
>
<ScrollContainer>
<Section
title={strings.toc()}
buttonPosition="right"
button={
<Cross
data-test-id="toc-close"
onClick={() => toggleTableOfContents(false)}
size={18}
sx={{ mr: 1, cursor: "pointer" }}
/>
}
<Section title={strings.toc()} sx={{ flex: 1 }}>
<Flex
sx={{
mt: 1,
flexDirection: "column",
mx: 1,
flex: 1
}}
>
<Flex sx={{ mt: 2, flexDirection: "column" }}>
{tableOfContents.length <= 0 ? (
<Text
variant="body"
sx={{
pl: 1
}}
>
{strings.noHeadingsFound()}.
</Text>
) : (
tableOfContents.map((t) => (
{tableOfContents.length <= 0 ? (
<Text
variant="body"
sx={{
pl: 1
}}
>
{strings.noHeadingsFound()}.
</Text>
) : (
<VirtualizedTree
treeRef={treeRef}
rootId="root"
itemHeight={27}
getChildNodes={async (id, depth) => {
const nodes: TreeNode<TOCItem>[] = [];
for (let i = 0; i < tableOfContents.length; i++) {
const item = tableOfContents[i];
if (item.level !== depth + 1) continue;
nodes.push({
id: item.id,
data: item,
depth: depth + 1,
parentId: id,
hasChildren:
i + 1 < tableOfContents.length &&
tableOfContents[i + 1].level > item.level,
expanded: true
});
}
return nodes;
}}
renderItem={({ item, collapse, expand, expanded }) => (
<Button
variant="menuitem"
key={t.id}
key={item.id}
id={`toc-${item.id}`}
sx={{
display: "flex",
alignItems: "center",
gap: 1,
width: "100%",
textAlign: "left",
paddingLeft: `${t.level * 10 + (t.level - 1) * 10}px`,
borderRadius: "default",
marginLeft: `${
item.depth === 0 ? 0 : 5 + 10 * item.depth
}px`,
px: 1,
py: 1,
pr: 1,
borderLeft: "5px solid transparent",
borderColor: active.includes(t.id)
? "accent-selected"
: "transparent",
color: active.includes(t.id)
? "accent-selected"
: "paragraph"
"&.active": {
color: "accent-selected"
},
"&.active path": {
fill: "var(--accent-selected) !important"
}
}}
onClick={() => scrollIntoViewById(t.id)}
onClick={() => scrollIntoViewById(item.id)}
>
{t.title}
{item.hasChildren ? (
<Button
variant="secondary"
sx={{ bg: "transparent", p: 0, borderRadius: 100 }}
onClick={(e) => {
e.stopPropagation();
expanded ? collapse() : expand();
}}
>
{expanded ? (
<ChevronDown size={14} color="icon-secondary" />
) : (
<ChevronRight size={14} color="icon-secondary" />
)}
</Button>
) : (
<Circle size={5} color="icon-secondary" />
)}
{item.data.title}
</Button>
))
)}
</Flex>
</Section>
</ScrollContainer>
)}
></VirtualizedTree>
)}
</Flex>
</Section>
</ScopedThemeProvider>
</Flex>
);

View File

@@ -25,6 +25,7 @@ import { PasswordVisible, PasswordInvisible, Icon } from "../icons";
import { useStore as useThemeStore } from "../../stores/theme-store";
type Action = {
id?: string;
testId?: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
@@ -146,7 +147,7 @@ function Field(props: FieldProps) {
</Flex>
)}
{rightActions.length > 0 ? (
{leftActions.length > 0 ? (
<Flex
sx={{
position: "absolute",
@@ -160,6 +161,7 @@ function Field(props: FieldProps) {
>
{leftActions.map((action) => (
<Button
id={action.id}
key={action.testId}
type="button"
variant={"secondary"}
@@ -189,6 +191,7 @@ function Field(props: FieldProps) {
) : null}
{rightActions.length > 0 ? (
<Flex
className="rightActions"
sx={{
position: "absolute",
top: 0,
@@ -201,6 +204,7 @@ function Field(props: FieldProps) {
>
{rightActions.map((action) => (
<Button
id={action.id}
key={action.testId}
type="button"
variant={"secondary"}
@@ -222,7 +226,7 @@ function Field(props: FieldProps) {
{action.component ? (
action.component
) : action.icon ? (
<action.icon size={20} />
<action.icon size={16} />
) : null}
</Button>
))}

View File

@@ -309,21 +309,20 @@ function GroupHeader(props: GroupHeaderProps) {
}
});
}}
mx={1}
my={1}
py={1}
pl={1}
pr={0}
bg="var(--background-secondary)"
// mx={1}
// my={1}
// py={1}
// pl={1}
// pr={0}
sx={{
borderRadius: "default",
cursor: "pointer",
border: isMenuTarget ? "1px solid" : "none",
borderColor: isMenuTarget ? "accent" : "transparent",
px: 1,
py: 1,
borderBottom: "1px solid var(--border)",
// border: isMenuTarget ? "1px solid" : "none",
// borderColor: isMenuTarget ? "accent" : "transparent",
":focus": {
border: "1px solid",
borderColor: "accent",
outline: "none"
borderColor: "accent"
},
alignItems: "center",
justifyContent: "space-between"
@@ -335,15 +334,16 @@ function GroupHeader(props: GroupHeaderProps) {
data-test-id="title"
variant="subtitle"
sx={{
fontSize: "body",
fontSize: "subBody",
fontWeight: "medium",
color: title === "Conflicted" ? "error" : "accent"
}}
>
{title}
{title.toUpperCase()}
</Text>
{index === 0 && (
<Flex mr={1}>
<Flex>
{groupingKey && (
<IconButton
testId={`${groupingKey}-sort-button`}

View File

@@ -221,7 +221,14 @@ import {
mdiChatQuestionOutline,
mdiNoteRemoveOutline,
mdiTabPlus,
mdiRadar
mdiRadar,
mdiLinkBoxOutline,
mdiHistory,
mdiArrowCollapseLeft,
mdiArrowCollapseRight,
mdiHamburger,
mdiNotePlus,
mdiNoteEditOutline
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -316,6 +323,7 @@ export function createIcon(path: string, rotate = false) {
}
export const Plus = createIcon(mdiPlus);
export const NoteAdd = createIcon(mdiNoteEditOutline);
export const Note = createIcon(mdiNoteOutline);
export const NoteRemove = createIcon(mdiNoteRemoveOutline);
export const Notes = createIcon(mdiNoteMultipleOutline);
@@ -565,3 +573,10 @@ export const Coupon = createIcon(mdiTagOutline);
export const Support = createIcon(mdiChatQuestionOutline);
export const NewTab = createIcon(mdiTabPlus);
export const Radar = createIcon(mdiRadar);
export const LinkedTo = createIcon(mdiVectorLink);
export const ReferencedIn = createIcon(mdiLink);
export const SessionHistory = createIcon(mdiHistory);
export const ColorRemove = createIcon(mdiCloseCircleOutline);
export const ExpandSidebar = createIcon(mdiArrowCollapseRight);
export const HamburgerMenu = createIcon(mdiMenu);

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { forwardRef, useEffect, useRef, useState } from "react";
import { Flex, Button } from "@theme-ui/components";
import { SxProp } from "@theme-ui/core";
import { Plus } from "../icons";
import {
useStore as useSelectionStore,
@@ -32,10 +33,11 @@ import {
} from "./list-profiles";
import Announcements from "../announcements";
import { ListLoader } from "../loaders/list-loader";
import ScrollContainer from "../scroll-container";
import ScrollContainer, { ScrollContainerProps } from "../scroll-container";
import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation";
import { VirtualizedGrouping, GroupingKey, Item } from "@notesnook/core";
import {
Components,
FlatScrollIntoViewLocation,
ItemProps,
ScrollerProps,
@@ -48,8 +50,9 @@ import { AppEventManager, AppEvents } from "../../common/app-events";
export const CustomScrollbarsVirtualList = forwardRef<
HTMLDivElement,
ScrollerProps
ScrollerProps & ScrollContainerProps
>(function CustomScrollbarsVirtualList(props, ref) {
console.log({ props, ref });
return (
<ScrollContainer
{...props}
@@ -62,6 +65,7 @@ export const CustomScrollbarsVirtualList = forwardRef<
});
type ListContainerProps = {
type: GroupingKey;
group?: GroupingKey;
items: VirtualizedGrouping<Item>;
compact?: boolean;
@@ -74,9 +78,21 @@ type ListContainerProps = {
button?: {
onClick: () => void;
};
};
Scroller?: Components["Scroller"];
} & SxProp;
function ListContainer(props: ListContainerProps) {
const { group, items, context, refresh, header, button, compact } = props;
const {
type,
group,
items,
context,
refresh,
header,
button,
compact,
sx,
Scroller
} = props;
const [focusedGroupIndex, setFocusedGroupIndex] = useState(-1);
@@ -180,7 +196,7 @@ function ListContainer(props: ListContainerProps) {
return (
<Flex
variant="columnFill"
sx={{ overflow: "hidden" }}
sx={{ overflow: "hidden", ...sx }}
onDragOver={(e) => e.preventDefault()}
onDrop={props.onDrop}
>
@@ -200,7 +216,7 @@ function ListContainer(props: ListContainerProps) {
<Flex
ref={listContainerRef}
variant="columnFill"
data-test-id={`${group}-list`}
data-test-id={`${type}-list`}
>
<Virtuoso
ref={listRef}
@@ -210,7 +226,7 @@ function ListContainer(props: ListContainerProps) {
onBlur={() => setFocusedGroupIndex(-1)}
onKeyDown={(e) => onKeyDown(e.nativeEvent)}
components={{
Scroller: CustomScrollbarsVirtualList,
Scroller: Scroller || CustomScrollbarsVirtualList,
Item: VirtuosoItem,
Header: ListHeader
}}

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Note from "../note";
import Notebook from "../notebook";
import Tag from "../tag";
import TrashItem from "../trash-item";
import { db } from "../../common/db";
@@ -28,6 +27,7 @@ import { getSortValue } from "@notesnook/core";
import { GroupingKey, Item } from "@notesnook/core";
import { isNoteResolvedData } from "@notesnook/common";
import { Attachment } from "../attachment";
import { Notebook } from "../notebook";
const SINGLE_LINE_HEIGHT = 1.4;
const DEFAULT_LINE_HEIGHT =
@@ -57,17 +57,15 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
/>
);
}
case "trash":
return <TrashItem item={item} date={getDate(item, group)} />;
case "notebook":
return (
<Notebook
item={item}
totalNotes={typeof data === "number" ? data : 0}
date={getDate(item, group)}
compact={compact}
/>
);
case "trash":
return <TrashItem item={item} date={getDate(item, group)} />;
case "reminder":
return <Reminder item={item} compact={compact} />;
case "tag":

View File

@@ -142,11 +142,9 @@ function ListItem<TItem extends Item, TContext>(
}}
tabIndex={-1}
sx={{
pl: 1,
pr: 2,
py: 1,
mb: "1px",
height: "inherit",
px: 1,
py: isCompact ? 0 : 1,
height: isCompact ? 25 : "inherit",
cursor: "pointer",
position: "relative",
overflow: "hidden",
@@ -158,8 +156,8 @@ function ListItem<TItem extends Item, TContext>(
opacity: isDisabled ? 0.7 : 1,
borderLeft: "5px solid",
borderLeftColor: isFocused ? accent : "transparent",
// borderLeft: "5px solid",
// borderLeftColor: isFocused ? accent : "transparent",
backgroundColor: selected ? "background-selected" : background,
@@ -201,16 +199,14 @@ function ListItem<TItem extends Item, TContext>(
<Text
dir="auto"
data-test-id={`title`}
variant={isCompact ? "body" : "subtitle"}
variant={"body"}
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontWeight: isCompact ? "body" : "bold",
fontWeight: isCompact ? "body" : "medium",
color:
selected && heading === "heading"
? `${heading}-selected`
: heading,
selected && heading === "heading" ? `heading-selected` : heading,
display: "block"
}}
>
@@ -227,6 +223,7 @@ function ListItem<TItem extends Item, TContext>(
dir="auto"
data-test-id={`description`}
sx={{
mt: "small",
color: selected ? "paragraph-selected" : "paragraph",
lineHeight: `1.2rem`,
overflow: "hidden",
@@ -241,15 +238,7 @@ function ListItem<TItem extends Item, TContext>(
{props.body}
</Text>
)}
{props.footer ? (
<Box
ml={isCompact ? 1 : 0}
mt={isCompact ? 0 : 1}
sx={{ flexShrink: 0 }}
>
{props.footer}
</Box>
) : null}
{props.footer ? <>{props.footer}</> : null}
</Flex>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -17,11 +17,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Button, Flex, FlexProps, Image, Text } from "@theme-ui/components";
import { Button, Flex, FlexProps, Text } from "@theme-ui/components";
import { Menu } from "../../hooks/use-menu";
import useMobile from "../../hooks/use-mobile";
import { PropsWithChildren } from "react";
import { Icon, Shortcut } from "../icons";
import { Icon } from "../icons";
import { SchemeColors, createButtonVariant } from "@notesnook/theme";
import { MenuItem } from "@notesnook/ui";
import { useSortable } from "@dnd-kit/sortable";
@@ -30,16 +29,12 @@ import { AppEventManager, AppEvents } from "../../common/app-events";
type NavigationItemProps = {
icon?: Icon;
image?: string;
color?: SchemeColors;
title: string;
isTablet?: boolean;
title?: string;
isCollapsed?: boolean;
isLoading?: boolean;
isShortcut?: boolean;
tag?: string;
selected?: boolean;
onClick?: () => void;
count?: number;
menuItems?: MenuItem[];
};
@@ -50,23 +45,18 @@ function NavigationItem(
) {
const {
icon: Icon,
image,
color,
title,
isLoading,
isShortcut,
tag,
children,
isTablet,
isCollapsed,
selected,
onClick,
menuItems,
count,
sx,
containerRef,
...restProps
} = props;
const isMobile = useMobile();
return (
<Flex
@@ -87,119 +77,60 @@ function NavigationItem(
}
),
borderRadius: "default",
mx: 1,
p: 0,
mt: isTablet ? 1 : "3px",
px: isCollapsed ? 1 : 2,
py: 1,
alignItems: "center",
position: "relative",
":first-of-type": { mt: 1 },
":last-of-type": { mb: 1 },
":focus": { bg: selected ? "hover-selected" : "hover" },
...sx
// ":hover:not(:disabled)": {
// bg: "hover",
// filter: "brightness(100%)"
// }
}}
onClick={() => {
AppEventManager.publish(AppEvents.toggleSideMenu, false);
if (onClick) onClick();
}}
data-test-id={`navigation-item`}
title={title}
onContextMenu={(e) => {
if (!menuItems) return;
e.preventDefault();
e.stopPropagation();
Menu.openMenu(menuItems);
}}
>
<Button
data-test-id={`navigation-item`}
<Flex
sx={{
px: isTablet ? 1 : 2,
p: 0,
flex: 1,
alignItems: "center",
justifyContent: isTablet ? "center" : "flex-start",
display: "flex"
}}
title={title}
onContextMenu={(e) => {
if (!menuItems) return;
e.preventDefault();
e.stopPropagation();
Menu.openMenu(menuItems);
}}
onClick={() => {
AppEventManager.publish(AppEvents.toggleSideMenu, false);
if (onClick) onClick();
justifyContent: isCollapsed ? "center" : "flex-start"
}}
>
{image ? (
<Image
src={image}
sx={{ borderRadius: 50, size: 20, minWidth: 20, flexShrink: 0 }}
/>
) : Icon ? (
{Icon ? (
<Icon
size={isTablet ? 16 : 15}
size={isCollapsed ? 16 : 14}
color={color || (selected ? "icon-selected" : "icon")}
rotate={isLoading}
/>
) : null}
{isShortcut && (
<Shortcut
size={8}
sx={{ position: "absolute", bottom: "8px", left: "6px" }}
color={color || "icon"}
data-test-id="shortcut"
/>
)}
<Text
variant="body"
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontWeight: selected ? "bold" : "normal",
color: selected ? "paragraph-selected" : "paragraph",
fontSize: "subtitle",
display: isTablet ? "none" : "block"
}}
ml={1}
data-test-id="title"
>
{title}
{/* {tag && (
<Text
variant="subBody"
as="span"
sx={{
bg: "accent",
color: "white",
ml: 1,
px: "small",
borderRadius: "default"
}}
>
{tag}
</Text>
)} */}
</Text>
</Button>
{children ? (
children
) : !isTablet && count !== undefined ? (
<Text
variant="subBody"
sx={{
mr: 1,
px: "3px",
borderRadius: "default"
}}
>
{count > 100 ? "100+" : count}
</Text>
) : !isTablet && tag ? (
<Text
variant="subBody"
sx={{
mr: 1,
borderRadius: "100px"
}}
>
{tag}
</Text>
) : null}
{isCollapsed ? null : (
<Text
variant="body"
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontWeight: "normal",
color: selected ? "paragraph-selected" : "paragraph"
}}
ml={1}
data-test-id="title"
>
{title}
</Text>
)}
</Flex>
{children && !isCollapsed ? children : null}
</Flex>
);
}

View File

@@ -0,0 +1,72 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { createButtonVariant } from "@notesnook/theme";
import { Button, Flex, FlexProps } from "@theme-ui/components";
import { Icon } from "../icons";
type TabItemProps = {
icon: Icon;
title?: string;
selected?: boolean;
onClick?: () => void;
};
export function TabItem(props: TabItemProps & FlexProps) {
const {
icon: Icon,
color,
title,
selected,
onClick,
sx,
...restProps
} = props;
return (
<Flex
{...restProps}
sx={{
...createButtonVariant(
selected ? "background-selected" : "transparent",
"transparent",
{
hover: {
bg: selected ? "hover-selected" : "hover"
}
}
),
borderRadius: "default",
alignItems: "center",
justifyContent: "center",
position: "relative",
":focus": { bg: selected ? "hover-selected" : "hover" },
p: 1,
...sx
}}
data-test-id={`tab-item`}
title={title}
onClick={() => {
if (onClick) onClick();
}}
>
<Icon size={16} color={color || (selected ? "icon-selected" : "icon")} />
</Flex>
);
}

View File

@@ -20,13 +20,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
NoteResolvedData,
exportContent,
getFormattedDate,
getFormattedReminderTime
} from "@notesnook/common";
import {
Color,
Note as NoteType,
Notebook as NotebookItem,
Tag,
Tag as TagType,
createInternalLink,
hosts,
isReminderActive,
@@ -87,6 +88,7 @@ import {
StarOutline,
Sync,
SyncOff,
Tag,
Tag2,
Tag as TagIcon,
Trash
@@ -149,35 +151,8 @@ function Note(props: NoteProps) {
useEditorStore.getState().openSession(note, { openInNewTab: true })
}
header={
<Flex
sx={{ alignItems: "center", flexWrap: "wrap", gap: 1, mt: "small" }}
>
{context?.type !== "notebook" &&
notebooks?.items.map((notebook) => (
<IconTag
key={notebook.id}
onClick={() => {
navigate(`/notebooks/${notebook.id}`);
}}
text={notebook.title}
icon={Notebook}
/>
))}
{reminder && isReminderActive(reminder) ? (
<IconTag
icon={Reminder}
text={getFormattedReminderTime(reminder, true)}
title={reminder.title}
styles={
isReminderToday(reminder)
? {
icon: { color: primary },
text: { color: primary }
}
: {}
}
/>
) : null}
<Flex sx={{ alignItems: "center", mb: 1 }}>
<Text variant="subBody">{getFormattedDate(date, "date")}</Text>
</Flex>
}
footer={
@@ -186,7 +161,9 @@ function Note(props: NoteProps) {
fontSize: "subBody",
color: "paragraph-secondary",
alignItems: "center",
gap: 1
gap: 1,
flexWrap: "wrap",
mt: "small"
}}
>
{compact ? (
@@ -203,12 +180,12 @@ function Note(props: NoteProps) {
{note.localOnly && <SyncOff size={13} />}
<TimeAgo
{/* <TimeAgo
sx={{ flexShrink: 0 }}
locale="en_short"
live={true}
datetime={date}
/>
/> */}
{attachments?.total ? (
<Flex sx={{ alignItems: "center", justifyContent: "center" }}>
@@ -226,9 +203,7 @@ function Note(props: NoteProps) {
</Flex>
) : null}
{note.pinned && !props.context && (
<Pin size={13} color={primary} />
)}
{note.pinned && !props.context && <Pin size={13} />}
{locked && <Lock size={13} data-test-id={`locked`} />}
@@ -240,28 +215,49 @@ function Note(props: NoteProps) {
{tags?.items.map((tag) => {
return (
<Button
data-test-id={`tag-item`}
<IconTag
testId={`tag-item`}
key={tag.id}
variant="anchor"
title={strings.goToTag(tag.title)}
onClick={(e) => {
e.stopPropagation();
if (!tag.id)
return showToast("error", strings.tagNotFound());
navigate(`/tags/${tag.id}`);
}}
sx={{
maxWidth: `calc(100% / ${tags.items.length})`,
overflow: "hidden",
textOverflow: "ellipsis",
color: "var(--paragraph-secondary)"
}}
>
#{tag.title}
</Button>
text={tag.title}
title={strings.goToTag(tag.title)}
icon={TagIcon}
/>
);
})}
{context?.type !== "notebook" &&
notebooks?.items.map((notebook) => (
<IconTag
key={notebook.id}
onClick={() => {
navigate(`/notebooks/${notebook.id}`);
}}
text={notebook.title}
icon={Notebook}
/>
))}
{reminder && isReminderActive(reminder) ? (
<IconTag
icon={Reminder}
text={getFormattedReminderTime(reminder, true)}
title={reminder.title}
styles={
isReminderToday(reminder)
? {
icon: { color: primary },
text: { color: primary }
}
: {}
}
/>
) : null}
</>
)}
</Flex>
@@ -735,8 +731,8 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
type: "lazy-loader",
key: "tags-lazy-loader",
async items() {
const tags: Map<string, Tag> = new Map();
const tagShortcuts: Map<string, Tag> = new Map();
const tags: Map<string, TagType> = new Map();
const tagShortcuts: Map<string, TagType> = new Map();
const linkedTags = await db.relations
.to({ ids, type: "note" }, "tag")

View File

@@ -0,0 +1,136 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from "react";
import { useStore as useAppStore } from "../stores/app-store";
import { hashNavigate } from "../navigation";
import { Button, Flex, Text } from "@theme-ui/components";
import { Edit, RemoveShortcutLink, ShortcutLink } from "./icons";
import { useStore as useNotebookStore } from "../stores/notebook-store";
import { db } from "../common/db";
import { getFormattedDate } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { Notebook } from "@notesnook/core";
import { TITLE_BAR_HEIGHT } from "./title-bar";
export function NotebookHeader(props: {
notebook: Notebook;
totalNotes?: number;
}) {
// const moreCrumbsRef = useRef<HTMLButtonElement>(null);
const notebooks = useNotebookStore((store) => store.notebooks);
const [notebook, setNotebook] = useState<Notebook | undefined>(
props.notebook
);
const [totalNotes, setTotalNotes] = useState(props.totalNotes);
// const [crumbs, setCrumbs] = useState<{ id: string; title: string }[]>([]);
const [isShortcut, setIsShortcut] = useState(false);
const shortcuts = useAppStore((store) => store.shortcuts);
const addToShortcuts = useAppStore((store) => store.addToShortcuts);
useEffect(() => {
setIsShortcut(shortcuts.findIndex((p) => p.id === props.notebook.id) > -1);
}, [shortcuts, props.notebook.id]);
useEffect(() => {
(async function () {
setNotebook(await db.notebooks.notebook(props.notebook.id));
})();
}, [notebooks, props.notebook]);
useEffect(() => {
if (props.totalNotes === undefined)
db.relations
.from(props.notebook, "note")
.count()
.then((count) => setTotalNotes(count));
else setTotalNotes(props.totalNotes);
}, [props.notebook, props.totalNotes]);
// useEffect(() => {
// (async function () {
// setCrumbs(await db.notebooks.breadcrumbs(context.id));
// })();
// }, [context.id]);
if (!notebook) return null;
const { title, description, dateEdited } = notebook;
return (
<Flex
data-test-id="notebook-header"
sx={{
flexDirection: "column",
p: 1,
pb: 4,
bg: "var(--background-secondary)",
borderBottom: "1px solid var(--border)"
}}
>
<Flex sx={{ alignItems: "center", justifyContent: "space-between" }}>
<Flex sx={{ alignItems: "center", gap: 2 }}>
<Text variant="subBody">{getFormattedDate(dateEdited, "date")}</Text>
<Text variant="subBody">{strings.notes(totalNotes || 0)}</Text>
</Flex>
<Flex sx={{ alignItems: "center", gap: 1 }}>
<Button
variant="secondary"
sx={{
borderRadius: 100,
p: 1
}}
title={
isShortcut ? strings.removeShortcut() : strings.createShortcut()
}
onClick={() => addToShortcuts(notebook)}
>
{isShortcut ? (
<RemoveShortcutLink size={16} />
) : (
<ShortcutLink size={14} />
)}
</Button>
<Button
variant="secondary"
sx={{
borderRadius: 100,
p: 1
}}
title={strings.editNotebook()}
onClick={() => hashNavigate(`/notebooks/${notebook.id}/edit`)}
>
<Edit size={14} />
</Button>
</Flex>
</Flex>
<Text
data-test-id="notebook-title"
variant="heading"
sx={{ fontSize: "title", mt: 2 }}
>
{title}
</Text>
{description && (
<Text variant="body" sx={{ mt: 1 }}>
{description}
</Text>
)}
</Flex>
);
}

View File

@@ -17,59 +17,75 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef } from "react";
import { Flex, Text } from "@theme-ui/components";
import ListItem from "../list-item";
import { store } from "../../stores/notebook-store";
import { useStore as useNotesStore } from "../../stores/note-store";
import { store as appStore } from "../../stores/app-store";
import { db } from "../../common/db";
import { Button, Flex, Text } from "@theme-ui/components";
import { store, useStore as useNotesStore } from "../../stores/note-store";
import { Notebook as NotebookType } from "@notesnook/core";
import {
Topic as TopicIcon,
PinFilled,
ChevronDown,
ChevronRight,
NotebookEdit,
Notebook as NotebookIcon,
Pin,
Plus,
RemoveShortcutLink,
Shortcut,
Trash
Trash,
Notebook as NotebookIcon
} from "../icons";
import { hashNavigate, navigate } from "../../navigation";
import IconTag from "../icon-tag";
import { Multiselect } from "../../common/multi-select";
import { getFormattedDate } from "@notesnook/common";
import { MenuItem } from "@notesnook/ui";
import { Notebook as NotebookType } from "@notesnook/core";
import { hashNavigate, navigate } from "../../navigation";
import { useRef } from "react";
import { handleDrop } from "../../common/drop-handler";
import { useDragHandler } from "../../hooks/use-drag-handler";
import { AddNotebookDialog } from "../../dialogs/add-notebook-dialog";
import { useStore as useSelectionStore } from "../../stores/selection-store";
import { store as appStore } from "../../stores/app-store";
import { Multiselect } from "../../common/multi-select";
import { strings } from "@notesnook/intl";
import { db } from "../../common/db";
type NotebookProps = {
item: NotebookType;
totalNotes: number;
date: number;
compact?: boolean;
totalNotes?: number;
isExpandable?: boolean;
isExpanded?: boolean;
expand?: () => void;
collapse?: () => void;
refresh?: () => void;
depth?: number;
};
function Notebook(props: NotebookProps) {
const { item, totalNotes, date, compact } = props;
const notebook = item;
export function Notebook(props: NotebookProps) {
const {
item,
totalNotes = 0,
isExpandable = false,
isExpanded = false,
expand = () => {},
collapse = () => {},
refresh = () => {},
depth = 0
} = props;
const isOpened = useNotesStore(
(store) =>
store.context?.type === "notebook" && store.context.id === item.id
);
const dragTimeout = useRef(0);
const { isDragEntering, isDragLeaving } = useDragHandler(`id_${notebook.id}`);
const { isDragEntering, isDragLeaving } = useDragHandler(`id_${item.id}`);
return (
<ListItem
draggable
isCompact={compact}
item={notebook}
onClick={() => openNotebook(notebook, totalNotes)}
isFocused={isOpened}
isCompact
item={item}
onClick={() => navigate(`/notebooks/${item.id}`)}
onDragEnter={(e) => {
if (!isDragEntering(e)) return;
e.currentTarget.focus();
dragTimeout.current = setTimeout(
() => openNotebook(notebook, totalNotes),
1000
) as unknown as number;
dragTimeout.current = setTimeout(() => {
expand();
}, 700) as unknown as number;
}}
onDragLeave={(e) => {
if (!isDragLeaving(e)) return;
@@ -77,89 +93,97 @@ function Notebook(props: NotebookProps) {
}}
onDrop={async (e) => {
clearTimeout(dragTimeout.current);
handleDrop(e.dataTransfer, notebook);
handleDrop(e.dataTransfer, item);
}}
onKeyPress={async (e) => {
if (e.key === "Delete") {
if (e.code === "Space") {
e.stopPropagation();
if (isExpandable) isExpanded ? collapse() : expand();
else navigate(`/notebooks/${item.id}`);
} else if (e.code === "Delete") {
e.stopPropagation();
await Multiselect.moveNotebooksToTrash(
useSelectionStore.getState().selectedItems
);
}
}}
title={notebook.title}
body={notebook.description as string}
menuItems={notebookMenuItems}
footer={
<>
{compact ? (
<>
<Text variant="subBody">{strings.notes(totalNotes)}</Text>
</>
) : (
<>
{notebook?.topics && (
<Flex mb={1} sx={{ gap: 1 }}>
{notebook.topics.slice(0, 3).map((topic) => (
<IconTag
key={topic.id}
text={topic.title}
icon={TopicIcon}
onClick={() => {
navigate(`/notebooks/${notebook.id}/${topic.id}`);
}}
/>
))}
</Flex>
title={
<Flex
sx={{ alignItems: "center", justifyContent: "center", gap: "small" }}
>
{isExpandable ? (
<Button
variant="secondary"
sx={{ bg: "transparent", p: 0, borderRadius: 100 }}
onClick={(e) => {
e.stopPropagation();
isExpanded ? collapse() : expand();
}}
>
{isExpanded ? (
<ChevronDown
size={14}
color={isOpened ? "icon-selected" : "icon"}
/>
) : (
<ChevronRight
size={14}
color={isOpened ? "icon-selected" : "icon"}
/>
)}
<Flex
sx={{
fontSize: "subBody",
color: "var(--paragraph-secondary)",
alignItems: "center",
fontFamily: "body"
}}
>
{notebook.pinned && (
<PinFilled color="accent" size={13} sx={{ mr: 1 }} />
)}
{getFormattedDate(date, "date")}
<Text as="span" mx={1} sx={{ color: "inherit" }}>
</Text>
<Text sx={{ color: "inherit" }}>
{strings.notes(totalNotes)}
</Text>
</Flex>
</>
</Button>
) : (
<NotebookIcon
size={14}
color={isOpened ? "icon-selected" : "icon"}
/>
)}
</>
<Text
data-test-id={`title`}
variant={"body"}
color={isOpened ? "paragraph-selected" : "paragraph"}
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontWeight: "body",
display: "block"
}}
>
{item.title}
</Text>
</Flex>
}
footer={<Text variant="subBody">{totalNotes}</Text>}
menuItems={notebookMenuItems}
context={{ refresh }}
sx={{
mb: "small",
borderRadius: "default",
paddingLeft: `${5 + (depth === 0 ? 0 : 15 * depth)}px`
}}
/>
);
}
export default React.memo(Notebook, (prev, next) => {
const prevItem = prev.item;
const nextItem = next.item;
return (
prev.date === next.date &&
prevItem.pinned === nextItem.pinned &&
prevItem.title === nextItem.title &&
prevItem.description === nextItem.description &&
prev.totalNotes === next.totalNotes &&
prev.compact === next.compact
);
});
export const notebookMenuItems: (
notebook: NotebookType,
ids?: string[]
) => MenuItem[] = (notebook, ids = []) => {
ids?: string[],
context?: { refresh?: () => void }
) => MenuItem[] = (notebook, ids = [], context) => {
const defaultNotebook = db.settings.getDefaultNotebook();
return [
{
type: "button",
key: "add",
title: strings.newNotebook(),
icon: Plus.path,
onClick: () =>
AddNotebookDialog.show({ parentId: notebook.id }).then((res) =>
res ? context?.refresh?.() : null
)
},
{ type: "separator", key: "sepep2" },
{
type: "button",
key: "edit",
@@ -181,15 +205,6 @@ export const notebookMenuItems: (
);
}
},
{
type: "button",
key: "pin",
icon: Pin.path,
title: strings.pin(),
isChecked: notebook.pinned,
onClick: () => store.pin(!notebook.pinned, ...ids),
multiSelect: true
},
{
type: "button",
key: "shortcut",
@@ -213,13 +228,3 @@ export const notebookMenuItems: (
}
];
};
async function openNotebook(notebook: NotebookType, totalNotes?: number) {
await useNotesStore.getState().setContext({
type: "notebook",
id: notebook.id,
item: notebook,
totalNotes: totalNotes
});
navigate(`/notebooks/${notebook.id}`);
}

View File

@@ -39,36 +39,24 @@ function Notice() {
<Flex
sx={{
cursor: "pointer",
borderRadius: "default",
borderRadius: "large",
":hover": { bg: "hover" },
alignItems: "center"
// minWidth: 250
alignItems: "center",
bg: "background-secondary",
border: "1px solid var(--border)",
mx: 1,
mb: 1,
p: 1,
gap: 1
}}
p={1}
onClick={() => NoticeData.action()}
mx={1}
>
<Flex sx={{ flex: 1, alignItems: "center" }}>
<NoticeData.icon
size={18}
color="accent"
sx={{ bg: "shade", mr: 2, p: 2, borderRadius: 80 }}
/>
<Flex sx={{ flex: 1, alignItems: "center", gap: 2 }}>
<NoticeData.icon size={20} color="accent" sx={{ ml: 1 }} />
<Flex
variant="columnCenter"
sx={{ alignItems: "flex-start", overflow: "hidden" }}
>
<Text
variant="body"
sx={{
fontSize: "body",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
}}
>
{NoticeData.title}
</Text>
<Text
variant="subBody"
sx={{
@@ -80,6 +68,17 @@ function Notice() {
>
{NoticeData.subtitle}
</Text>
<Text
variant="body"
sx={{
color: "heading-secondary",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
}}
>
{NoticeData.title}
</Text>
</Flex>
</Flex>
{NoticeData.dismissable && (
@@ -97,12 +96,11 @@ function Notice() {
sx={{
borderRadius: 50,
p: 1,
mr: 1,
bg: "transparent"
}}
variant="accentSecondary"
variant="secondary"
>
<Dismiss size={20} color="accent" />
<Dismiss size={20} color="icon" />
</Button>
)}
</Flex>

View File

@@ -28,9 +28,12 @@ import {
Circle,
Checkmark,
ChevronDown,
ChevronRight
ChevronRight,
LinkedTo,
ReferencedIn as ReferencedInIcon,
Note as NoteIcon
} from "../icons";
import { Box, Button, Flex, Text } from "@theme-ui/components";
import { Box, Button, Flex, Text, FlexProps } from "@theme-ui/components";
import {
useEditorStore,
ReadonlyEditorSession,
@@ -45,7 +48,8 @@ import {
getFormattedDate,
usePromise,
ResolvedItem,
useResolvedItem
useResolvedItem,
useUnresolvedItem
} from "@notesnook/common";
import { ScopedThemeProvider } from "../theme-provider";
import { ListItemWrapper } from "../list-container/list-profiles";
@@ -53,6 +57,7 @@ import { VirtualizedList } from "../virtualized-list";
import { SessionItem } from "../session-item";
import {
ContentBlock,
InternalLink,
Note,
VirtualizedGrouping,
createInternalLink,
@@ -117,23 +122,10 @@ function EditorProperties(props: EditorPropertiesProps) {
if (isFocusMode || !session) return null;
return (
<Flex
css={`@keyframes slideIn {
from {
transform: translateX(600px);
}
to {
transform: translateX(0);
}`}
sx={{
transform: "translateX(600)",
animation: "0.1s ease-out 0s 1 slideIn",
display: "flex",
position: "absolute",
top: TITLE_BAR_HEIGHT,
right: 0,
zIndex: 999,
height: "100%",
width: "300px",
width: "100%",
borderLeft: "1px solid",
borderLeftColor: "border"
}}
@@ -150,79 +142,86 @@ function EditorProperties(props: EditorPropertiesProps) {
}}
>
<ScrollContainer>
<Section
title={strings.properties()}
button={
<ArrowLeft
data-test-id="properties-close"
onClick={() => toggleProperties(false)}
size={18}
sx={{ mr: 1, cursor: "pointer" }}
/>
}
{/* <Flex
sx={{
alignItems: "center",
gap: 1
}}
>
<ArrowLeft
data-test-id="properties-close"
onClick={() => toggleProperties(false)}
size={18}
sx={{ cursor: "pointer" }}
/>
<Text variant="subtitle">{strings.properties()}</Text>
</Flex> */}
<Flex
data-test-id="general-section"
sx={{ flexDirection: "column", gap: 1 }}
>
<Section title="Properties">
<Flex sx={{ flexDirection: "column", gap: 1, px: 2, pt: 1 }}>
{session.type === "deleted" ? null : (
<>
{tools.map((tool) => (
<Toggle
{...tool}
key={tool.key}
isOn={
tool.property === "locked"
? "locked" in session && !!session.locked
: !!session.note[tool.property]
}
onToggle={() => changeToggleState(tool.key, session)}
testId={`properties-${tool.key}`}
/>
))}
</>
)}
{metadataItems.map((item) => (
<Flex
key={item.key}
sx={{
alignItems: "center",
justifyContent: "space-between",
py: "small"
}}
>
<Text
variant="body"
sx={{
overflow: "hidden",
textOverflow: "ellipsis"
}}
>
{item.label}
</Text>
<Text
className="selectable"
variant="subBody"
sx={{ fontSize: "body", flexShrink: 0 }}
>
{item.value(session.note[item.key])}
</Text>
</Flex>
))}
{session.type === "deleted" ? null : (
<Colors noteId={session.note.id} color={session.color} />
)}
</Flex>
</Section>
{session.type === "deleted" ? null : (
<>
{tools.map((tool) => (
<Toggle
{...tool}
key={tool.key}
isOn={
tool.property === "locked"
? "locked" in session && !!session.locked
: !!session.note[tool.property]
}
onToggle={() => changeToggleState(tool.key, session)}
testId={`properties-${tool.key}`}
/>
))}
<InternalLinks noteId={session.note.id} />
<Notebooks noteId={session.note.id} />
<Reminders noteId={session.note.id} />
<Attachments noteId={session.note.id} />
<SessionHistory noteId={session.note.id} />
</>
)}
{metadataItems.map((item) => (
<Flex
key={item.key}
py={2}
px={1}
sx={{
borderBottom: "1px solid var(--separator)",
alignItems: "center",
justifyContent: "space-between",
gap: 1
}}
>
<Text
variant="subBody"
sx={{
fontSize: "body",
overflow: "hidden",
textOverflow: "ellipsis"
}}
>
{item.label}
</Text>
<Text
className="selectable"
variant="subBody"
sx={{ fontSize: "body", flexShrink: 0 }}
>
{item.value(session.note[item.key])}
</Text>
</Flex>
))}
{session.type === "deleted" ? null : (
<Colors noteId={session.note.id} color={session.color} />
)}
</Section>
{session.type === "deleted" ? null : (
<>
<InternalLinks noteId={session.note.id} />
<Notebooks noteId={session.note.id} />
<Reminders noteId={session.note.id} />
<Attachments noteId={session.note.id} />
<SessionHistory noteId={session.note.id} />
</>
)}
</Flex>
</ScrollContainer>
</ScopedThemeProvider>
</Flex>
@@ -243,46 +242,61 @@ function InternalLinks({ noteId }: { noteId: string }) {
tabIndex === InternalLinksTabs.LINKED_NOTES
? db.relations.from({ id: noteId, type: "note" }, "note")
: db.relations.to({ id: noteId, type: "note" }, "note");
return links.selector.sorted(db.settings.getGroupOptions("notes"));
return links.selector
.fields(["notes.id", "notes.title"])
.sorted(db.settings.getGroupOptions("notes"));
}, [tabIndex, noteId]);
return (
<Flex sx={{ flexDirection: "column", mt: 2 }}>
<Flex sx={{ flexDirection: "column" }}>
<Flex
sx={{
justifyContent: "stretch",
borderRadius: "default",
overflow: "hidden",
mx: 1,
borderTop: "1px solid var(--border)",
borderBottom: "1px solid var(--border)",
py: 1,
px: 2,
justifyContent: "space-between",
alignItems: "center",
mb: 1
}}
>
{[strings.linkedNotes(), strings.referencedIn()].map((title, index) => (
<Button
key={title}
variant="secondary"
sx={{
flex: 1,
borderRadius: 0,
color: tabIndex === index ? "accent-selected" : "paragraph",
bg:
tabIndex === index
? "background-selected"
: "background-secondary"
}}
onClick={() => {
setTabIndex(index);
setExpandedId(undefined);
}}
>
{title}
</Button>
))}
<Flex
sx={{
gap: 1
}}
>
{[LinkedTo, ReferencedInIcon].map((Icon, index) => (
<Button
key={index.toString()}
variant="secondary"
sx={{
p: 1,
color: tabIndex === index ? "accent-selected" : "paragraph",
bg: tabIndex === index ? "background-selected" : "transparent"
}}
onClick={() => {
setTabIndex(index);
setExpandedId(undefined);
}}
>
<Icon
size={16}
color={tabIndex === index ? "icon-selected" : "icon"}
/>
</Button>
))}
</Flex>
<Text variant="body" color="paragraph-secondary">
({result.status === "fulfilled" ? result.value.length : 0}){" "}
{tabIndex === InternalLinksTabs.LINKED_NOTES
? strings.linkedNotes()
: strings.referencedIn()}{" "}
</Text>
</Flex>
{result.status === "fulfilled" &&
(result.value.length === 0 ? (
<Text variant="body" mx={1}>
<Text variant="body" mx={2}>
{tabIndex === InternalLinksTabs.LINKED_NOTES
? strings.notLinked()
: strings.notReferenced()}
@@ -323,9 +337,9 @@ function InternalLinkItem({
};
}) {
const { items, tabIndex, noteId, isExpanded, toggleExpand } = context;
const item = useResolvedItem({ items, index });
const item = useUnresolvedItem({ items, index, type: "note" });
if (!item || item.item?.type !== "note") return null;
if (!item) return null;
if (tabIndex === InternalLinksTabs.LINKED_NOTES)
return (
@@ -357,90 +371,108 @@ function LinkedNote({
isExpanded: boolean;
}) {
const [blocks, setBlocks] = useState<ContentBlock[]>([]);
const linkedBlocks = usePromise(
async () =>
(await db.notes.internalLinks(noteId)).filter(
(l) => l.id === item.id && !!l.params?.blockId
),
[item.id]
);
return (
<>
<Button
variant="menuitem"
sx={{
p: 1,
width: "100%",
textAlign: "left",
display: "flex",
justifyContent: "start",
alignItems: "center",
borderBottom: isExpanded ? "none" : "1px solid var(--border)"
}}
onClick={() => useEditorStore.getState().openSession(item)}
>
<Box
onClick={async (e) => {
e.stopPropagation();
if (isExpanded) return toggleExpand();
const blocks = await db.notes.contentBlocks(item.id);
const linkedBlocks = (await db.notes.internalLinks(noteId)).filter(
(l) => l.id === item.id
);
setBlocks(
linkedBlocks.length > 0
? blocks.filter((a) =>
linkedBlocks.some((l) => l.params?.blockId === a.id)
)
: []
);
toggleExpand();
<Flex sx={{ width: "100%", alignItems: "center" }}>
<Button
variant="menuitem"
sx={{
flex: 1,
p: 1,
mx: 2,
borderRadius: "default",
textAlign: "left",
display: "flex",
justifyContent: "start",
alignItems: "center",
gap: "small"
// borderBottom: isExpanded ? "none" : "1px solid var(--border)"
}}
onClick={() => useEditorStore.getState().openSession(item)}
>
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</Box>
<Text>{item.title}</Text>
</Button>
{linkedBlocks.status === "fulfilled" &&
linkedBlocks.value.length > 0 ? (
<Button
variant="secondary"
sx={{ bg: "transparent", p: 0, borderRadius: 100 }}
onClick={async (e) => {
e.stopPropagation();
if (isExpanded) return toggleExpand();
setBlocks(
(await db.notes.contentBlocks(item.id)).filter((a) =>
linkedBlocks.value.some((l) => l.params?.blockId === a.id)
)
);
toggleExpand();
}}
>
{isExpanded ? (
<ChevronDown size={14} />
) : (
<ChevronRight size={14} />
)}
</Button>
) : (
<NoteIcon size={14} />
)}
<Text>{item.title}</Text>
</Button>
</Flex>
{isExpanded
? blocks.map((block) => (
<Button
key={block.id}
variant="menuitem"
sx={{
p: 1,
pl: 4,
gap: 1,
width: "100%",
textAlign: "left",
display: "flex",
alignItems: "center",
borderBottom: "1px solid var(--border)"
}}
onClick={() =>
useEditorStore
.getState()
.openSession(item, { activeBlockId: block.id })
}
>
<Text
variant="subBody"
<Flex key={block.id} sx={{ width: "100%", alignItems: "center" }}>
<Button
variant="menuitem"
sx={{
bg: "background-secondary",
p: "small",
flexShrink: 0,
px: 1,
flex: 1,
borderRadius: "default",
alignSelf: "flex-start"
p: 1,
mx: 2,
pl: 4,
gap: 1,
textAlign: "left",
display: "flex",
alignItems: "center"
}}
onClick={() =>
useEditorStore
.getState()
.openSession(item, { activeBlockId: block.id })
}
>
{block.type.toUpperCase()}
</Text>
<Text
variant="body"
sx={{
fontSize: "subBody",
fontFamily: "monospace",
whiteSpace: "pre-wrap"
}}
>
{block.content}
</Text>
</Button>
<Text
variant="subBody"
sx={{
bg: "background-secondary",
p: "small",
flexShrink: 0,
px: 1,
borderRadius: "default",
alignSelf: "flex-start"
}}
>
{block.type.toUpperCase()}
</Text>
<Text
variant="body"
sx={{
fontSize: "subBody",
fontFamily: "monospace",
whiteSpace: "pre-wrap"
}}
>
{block.content}
</Text>
</Button>
</Flex>
))
: null}
</>
@@ -464,41 +496,51 @@ function ReferencedIn({
return (
<>
<Button
variant="menuitem"
sx={{
p: 1,
width: "100%",
textAlign: "left",
display: "flex",
justifyContent: "start",
alignItems: "center",
borderBottom: isExpanded ? "none" : "1px solid var(--border)"
}}
onClick={() => useEditorStore.getState().openSession(item)}
>
<Box
onClick={async (e) => {
e.stopPropagation();
if (isExpanded) return toggleExpand();
const blocks = await db.notes.contentBlocksWithLinks(item.id);
setBlocks(
blocks
.filter((b) =>
b.content.includes(createInternalLink("note", noteId))
)
.map((block) => ({
id: block.id,
links: highlightInternalLinks(block, noteId)
}))
);
toggleExpand();
<Flex sx={{ width: "100%", alignItems: "center" }}>
<Button
variant="menuitem"
sx={{
flex: 1,
p: 1,
mx: 2,
borderRadius: "default",
textAlign: "left",
display: "flex",
justifyContent: "start",
alignItems: "center",
gap: "small"
}}
onClick={() => useEditorStore.getState().openSession(item)}
>
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</Box>
<Text variant="body">{item.title}</Text>
</Button>
<Button
variant="secondary"
sx={{ bg: "transparent", p: 0, borderRadius: 100 }}
onClick={async (e) => {
e.stopPropagation();
if (isExpanded) return toggleExpand();
const blocks = await db.notes.contentBlocksWithLinks(item.id);
setBlocks(
blocks
.filter((b) =>
b.content.includes(createInternalLink("note", noteId))
)
.map((block) => ({
id: block.id,
links: highlightInternalLinks(block, noteId)
}))
);
toggleExpand();
}}
>
{isExpanded ? (
<ChevronDown size={16} />
) : (
<ChevronRight size={16} />
)}
</Button>
<Text variant="body">{item.title}</Text>
</Button>
</Flex>
{isExpanded
? blocks.map((block) => (
<>
@@ -507,13 +549,16 @@ function ReferencedIn({
key={index.toString()}
variant="menuitem"
sx={{
flex: 1,
borderRadius: "default",
p: 1,
mx: 2,
pl: 4,
pr: 2,
gap: 1,
width: "100%",
textAlign: "left",
borderBottom: "1px solid var(--border)"
whiteSpace: "pre-wrap",
display: "flex",
flexDirection: "row",
gap: 2
}}
onClick={() =>
useEditorStore
@@ -521,23 +566,27 @@ function ReferencedIn({
.openSession(item, { activeBlockId: block.id })
}
>
{link.map((slice) => (
<Text
key={slice.text}
variant="body"
sx={{
color: slice.highlighted
? "accent-selected"
: "paragraph",
fontWeight: slice.highlighted ? "bold" : "normal",
textDecoration: slice.highlighted
? "underline solid var(--accent-selected)"
: "none"
}}
>
{slice.text}
</Text>
))}
<Text variant="subBody">{index + 1}.</Text>
<Text as="div" variant="body">
{link.map((slice) =>
slice.highlighted ? (
<Text
key={slice.text}
as="span"
sx={{
color: "accent-selected",
fontWeight: "bold",
textDecoration:
"underline solid var(--accent-selected)"
}}
>
{slice.text}
</Text>
) : (
<>{slice.text}</>
)
)}
</Text>
</Button>
))}
</>
@@ -551,11 +600,10 @@ function Colors({ noteId, color }: { noteId: string; color?: string }) {
const result = usePromise(() => db.colors.all.items(), [color]);
return (
<Flex
py={2}
px={1}
sx={{
cursor: "pointer",
justifyContent: "start"
justifyContent: "start",
gap: "small"
}}
>
{result.status === "fulfilled" &&
@@ -574,7 +622,7 @@ function Colors({ noteId, color }: { noteId: string; color?: string }) {
data-test-id={`properties-${c.title}`}
>
<Circle
size={35}
size={25}
color={c.colorCode}
data-test-id={`toggle-state-${isChecked ? "on" : "off"}`}
/>
@@ -604,10 +652,14 @@ function Notebooks({ noteId }: { noteId: string }) {
if (result.status !== "fulfilled" || result.value.length <= 0) return null;
return (
<Section title={strings.notebooks()}>
<Section
title={strings.notebooks()}
sx={{ borderTop: "1px solid var(--border)" }}
>
<VirtualizedList
style={{ marginTop: 5 }}
mode="fixed"
estimatedSize={50}
estimatedSize={25}
getItemKey={(index) => result.value.key(index)}
items={result.value.placeholders}
renderItem={({ index }) => (
@@ -633,10 +685,14 @@ function Reminders({ noteId }: { noteId: string }) {
if (result.status !== "fulfilled" || result.value.length <= 0) return null;
return (
<Section title={strings.dataTypesPluralCamelCase.reminder()}>
<Section
sx={{ borderTop: "1px solid var(--border)" }}
title={strings.dataTypesPluralCamelCase.reminder()}
>
<VirtualizedList
mode="fixed"
estimatedSize={54}
style={{ marginTop: 5, marginLeft: 10, marginRight: 10 }}
estimatedSize={48}
getItemKey={(index) => result.value.key(index)}
items={result.value.placeholders}
renderItem={({ index }) => (
@@ -661,12 +717,19 @@ function Attachments({ noteId }: { noteId: string }) {
if (result.status !== "fulfilled" || result.value.length <= 0) return null;
return (
<Section title={strings.dataTypesPluralCamelCase.attachment()}>
<Section
title={strings.dataTypesPluralCamelCase.attachment()}
sx={{ borderTop: "1px solid var(--border)" }}
>
<VirtualizedTable
estimatedSize={30}
estimatedSize={25}
getItemKey={(index) => result.value.key(index)}
items={result.value.placeholders}
style={{ tableLayout: "fixed", width: "100%" }}
style={{
marginTop: 5,
tableLayout: "fixed",
width: "100%"
}}
header={
<tr>
<th style={{ width: "75%" }} />
@@ -695,12 +758,16 @@ function SessionHistory({ noteId }: { noteId: string }) {
return (
<Section
sx={{
borderTop: "1px solid var(--border)"
}}
title={strings.noteHistory()}
subtitle={strings.noteHistoryNotice[0]()}
>
<VirtualizedList
mode="dynamic"
estimatedSize={28}
style={{ marginLeft: 10, marginRight: 10, marginTop: 5 }}
getItemKey={(index) => result.value.key(index)}
items={result.value.placeholders}
renderItem={({ index }) => (
@@ -714,43 +781,43 @@ function SessionHistory({ noteId }: { noteId: string }) {
}
type SectionProps = {
title: string;
title?: string;
subtitle?: string;
button?: JSX.Element;
buttonPosition?: "left" | "right";
};
} & FlexProps;
export function Section({
title,
subtitle,
button,
buttonPosition = "left",
children
children,
sx,
...otherProps
}: PropsWithChildren<SectionProps>) {
return (
<Flex
sx={{
borderRadius: "default",
flexDirection: "column"
// borderRadius: "default",
flexDirection: "column",
// bg: "background-secondary",
// border: "1px solid var(--border)",
...sx
}}
{...otherProps}
>
<Flex
mx={1}
mt={2}
sx={{
alignItems: "center",
justifyContent:
buttonPosition === "right" ? "space-between" : "flex-start"
}}
>
{buttonPosition === "left" && button}
<Text variant="subtitle">{title}</Text>
{buttonPosition === "right" && button}
</Flex>
{subtitle && (
<Text variant="subBody" mb={1} mx={1}>
{subtitle}
</Text>
)}
{title || subtitle ? (
<Flex
sx={{
flexDirection: "column",
borderBottom: "1px solid var(--border)",
p: 2
}}
>
{title && (
<Text variant="subBody" sx={{ fontWeight: "medium" }}>
{title.toUpperCase()}
</Text>
)}
{subtitle && <Text variant="subBody">{subtitle}</Text>}
</Flex>
) : null}
{children}
</Flex>
);

View File

@@ -32,10 +32,7 @@ function Toggle(props: ToggleProps) {
return (
<Flex
py={2}
px={1}
sx={{
borderBottom: "1px solid var(--separator)",
cursor: "pointer",
alignItems: "center",
justifyContent: "space-between",
@@ -49,7 +46,6 @@ function Toggle(props: ToggleProps) {
<Flex
sx={{
alignItems: "center",
display: "flex"
}}
data-test-id={`toggle-state-${isOn ? "on" : "off"}`}
@@ -68,7 +64,12 @@ function Toggle(props: ToggleProps) {
</Text>
</Flex>
<Switch
sx={{ m: 0, bg: isOn ? "accent" : "icon-secondary", flexShrink: 0 }}
sx={{
m: 0,
bg: isOn ? "accent" : "icon-secondary",
flexShrink: 0,
scale: 0.75
}}
checked={isOn}
onClick={(e) => e.stopPropagation()}
/>

View File

@@ -71,9 +71,9 @@ function Reminder(props: ReminderProps) {
<ListItem
item={item}
title={reminder.title}
body={reminder.description}
body={compact ? undefined : reminder.description}
isDisabled={reminder.disabled}
isCompact={compact}
isCompact={false}
onClick={() => EditReminderDialog.show({ reminderId: reminder.id })}
onKeyPress={async (e) => {
if (e.key === "Delete") {
@@ -82,11 +82,19 @@ function Reminder(props: ReminderProps) {
);
}
}}
sx={
compact
? {
borderRadius: "default"
}
: {}
}
footer={
<Flex
sx={{
alignItems: "center",
gap: 1
gap: 1,
mt: 1
}}
>
{reminder.disabled ? null : <PriorityIcon size={14} />}

View File

@@ -17,17 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { PropsWithChildren } from "react";
import { Button, Flex, Text } from "@theme-ui/components";
import { ArrowLeft, Menu, Search, Plus, Close } from "../icons";
import { useStore } from "../../stores/app-store";
import { PropsWithChildren, useEffect, useRef } from "react";
import { Box } from "@theme-ui/components";
import { Close, AddReminder } from "../icons";
import { useStore as useSearchStore } from "../../stores/search-store";
import useMobile from "../../hooks/use-mobile";
import { debounce, usePromise } from "@notesnook/common";
import Field from "../field";
import { strings } from "@notesnook/intl";
import { TITLE_BAR_HEIGHT } from "../title-bar";
import { AppEventManager, AppEvents } from "../../common/app-events";
import { RouteResult } from "../../navigation/types";
import { CREATE_BUTTON_MAP } from "../../common";
export type RouteContainerButtons = {
search?: {
@@ -43,16 +42,12 @@ export type RouteContainerButtons = {
};
};
export type RouteContainerProps = {
type: string;
title?: string | (() => Promise<string | undefined>);
buttons?: RouteContainerButtons;
};
export type RouteContainerProps = RouteResult;
function RouteContainer(props: PropsWithChildren<RouteContainerProps>) {
const { type, title, buttons, children } = props;
const { children } = props;
return (
<>
<Header type={type} title={title} buttons={buttons} />
<Header {...props} />
{children}
</>
);
@@ -61,7 +56,7 @@ function RouteContainer(props: PropsWithChildren<RouteContainerProps>) {
export default RouteContainer;
function Header(props: RouteContainerProps) {
const { buttons, type } = props;
const { type } = props;
const titlePromise = usePromise<string | undefined>(
() => (typeof props.title === "string" ? props.title : props.title?.()),
[props.title]
@@ -69,146 +64,96 @@ function Header(props: RouteContainerProps) {
const isMobile = useMobile();
const isSearching = useSearchStore((store) => store.isSearching);
const query = useSearchStore((store) => store.query);
const inputRef = useRef<HTMLInputElement>(null);
if (isSearching)
return (
<Flex
sx={{
alignItems: "center",
justifyContent: "center",
height: TITLE_BAR_HEIGHT,
zIndex: 2,
px: 1
}}
className="route-container-header search-container"
>
<Field
data-test-id="search-input"
autoFocus
id="search"
name="search"
variant="borderless"
type="text"
sx={{ m: 0, flex: 1, gap: 0 }}
styles={{ input: { p: "5px", m: 0 } }}
defaultValue={query}
placeholder={strings.typeAKeyword()}
onChange={debounce(
(e) => useSearchStore.setState({ query: e.target.value }),
250
)}
onKeyUp={(e) => {
if (e.key === "Escape")
useSearchStore.setState({
isSearching: false,
searchType: undefined
});
}}
action={{
icon: Close,
testId: "search-button",
onClick: () =>
useSearchStore.setState({
isSearching: false,
searchType: undefined
})
}}
/>
</Flex>
);
useEffect(() => {
if (inputRef.current && inputRef.current.value !== query) {
inputRef.current.value = query || "";
}
}, [query]);
useEffect(() => {
if (isSearching) inputRef.current?.focus();
}, [isSearching]);
return (
<Flex
className="route-container-header"
<Box
sx={{
px: 1,
alignItems: "center",
justifyContent: "space-between",
height: TITLE_BAR_HEIGHT,
zIndex: 2
bg: type === "notebook" ? "background-secondary" : "transparent",
zIndex: 2,
p: 1
}}
className="route-container-header search-container"
data-test-id="routeHeader"
data-header={
titlePromise.status === "fulfilled" ? titlePromise.value || type : type
}
>
<Flex
py={1}
<Field
inputRef={inputRef}
data-test-id="search-input"
id="search"
name="search"
type="text"
sx={{
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
gap: 1
bg: "background",
m: 0,
mr: 0,
borderRadius: "large",
gap: 0
}}
>
{buttons?.back ? (
<Button
{...buttons.back}
data-test-id="route-go-back"
sx={{ p: 0, flexShrink: 0 }}
>
<ArrowLeft size={20} />
</Button>
) : (
<Button
onClick={() =>
AppEventManager.publish(AppEvents.toggleSideMenu, true)
styles={{
input: {
m: 0,
p: "7.5px",
fontSize: "body",
"::placeholder": {
textAlign: "center"
},
"& + .rightActions #search-action-button": {
opacity: query ? 1 : 0
},
"&:focus + .rightActions #search-action-button": {
opacity: 1
}
sx={{ p: 0, flexShrink: 0 }}
>
<Menu
sx={{
display: ["block", "none", "none"],
size: 23
}}
size={24}
/>
</Button>
}
}}
defaultValue={query}
placeholder={strings.searchInRoute(
titlePromise.status === "fulfilled"
? titlePromise.value || type
: type
)}
{titlePromise.status === "fulfilled" && titlePromise.value && (
<Text
className="routeHeader"
variant="heading"
data-test-id="routeHeader"
color="heading"
>
{titlePromise.value}
</Text>
onChange={debounce(
(e) => useSearchStore.setState({ query: e.target.value }),
250
)}
</Flex>
<Flex sx={{ flexShrink: 0, gap: 2 }}>
{buttons?.search && (
<Button
title={buttons.search.title}
onClick={() =>
useSearchStore.setState({ isSearching: true, searchType: type })
onKeyUp={(e) => {
if (e.key === "Escape") useSearchStore.getState().resetSearch();
else useSearchStore.setState({ isSearching: true, searchType: type });
}}
rightActions={[
{
icon: Close,
id: "search-action-button",
testId: "search-button",
onClick: () => {
if (inputRef.current) inputRef.current.value = "";
useSearchStore.getState().resetSearch();
}
data-test-id={"open-search"}
sx={{ p: 0 }}
>
<Search
size={24}
sx={{
size: 24
}}
/>
</Button>
)}
{!isMobile && buttons?.create && (
<Button
{...buttons.create}
data-test-id={`${type}-action-button`}
sx={{ p: 0 }}
>
<Plus
color="accentForeground"
size={18}
sx={{
height: 24,
width: 24,
bg: "accent",
borderRadius: 100
}}
/>
</Button>
)}
</Flex>
</Flex>
},
...(type === "reminders"
? [
{
icon: AddReminder,
testId: "create-reminder-button",
...CREATE_BUTTON_MAP.reminders
}
]
: [])
]}
/>
</Box>
);
}

View File

@@ -21,28 +21,30 @@ import React, { PropsWithChildren, useLayoutEffect } from "react";
import { MacScrollbar, MacScrollbarProps } from "mac-scrollbar";
import "mac-scrollbar/dist/mac-scrollbar.css";
type ScrollContainerProps = {
export type ScrollContainerProps = {
style?: React.CSSProperties;
forwardedRef?: (ref: HTMLDivElement | null) => void;
};
} & MacScrollbarProps;
const ScrollContainer = ({
children,
forwardedRef,
style,
...props
}: PropsWithChildren<ScrollContainerProps>) => {
return (
<MacScrollbar
suppressScrollX
minThumbSize={40}
{...props}
ref={(div) => {
forwardedRef && forwardedRef(div as HTMLDivElement);
}}
style={{
position: "relative",
height: "100%"
height: "100%",
...style
}}
suppressScrollX
minThumbSize={40}
>
{children}
</MacScrollbar>

View File

@@ -40,6 +40,7 @@ export function SessionItem(props: SessionItemProps) {
py={1}
px={1}
sx={{
borderRadius: "default",
cursor: "pointer",
bg: "transparent",
":hover": {

View File

@@ -0,0 +1,41 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { forwardRef } from "react";
import { ScrollerProps } from "react-virtuoso";
import ScrollContainer from "./scroll-container";
export const SidebarScroller = forwardRef<HTMLDivElement, ScrollerProps>(
function CustomScroller(props, ref) {
return (
<ScrollContainer
{...props}
trackStyle={() => ({
width: 3
})}
thumbStyle={() => ({ width: 3 })}
suppressScrollX={true}
forwardedRef={(sRef) => {
if (typeof ref === "function") ref(sRef);
else if (ref) ref.current = sRef;
}}
/>
);
}
);

View File

@@ -60,6 +60,7 @@ type PaneOptions = {
export type SplitPaneImperativeHandle = {
collapse: (index: number) => void;
expand: (index: number) => void;
reset: (index: number) => void;
isCollapsed: (index: number) => boolean;
};
export const SplitPane = React.forwardRef<
@@ -285,6 +286,13 @@ export const SplitPane = React.forwardRef<
currentPane.expandedSize = undefined;
setSizes(paneSizes.current, wrapSize.current);
},
reset: (index: number) => {
const currentPane = paneSizes.current[index];
currentPane.collapsed = false;
currentPane.size = currentPane.initialSize;
currentPane.expandedSize = undefined;
setSizes(paneSizes.current, wrapSize.current);
},
isCollapsed: (index: number) => {
return paneSizes.current[index].collapsed;
}

View File

@@ -55,5 +55,5 @@ body.react-split--disabled {
}
.split-sash-content.split-sash-content-active {
background-color: var(--accent);
background-color: transparent;
}

View File

@@ -80,6 +80,7 @@ function StatusBar() {
size={7}
color={"var(--icon-success)"}
sx={{ p: "small" }}
data-test-id="logged-in"
/>
) : (
<Button
@@ -92,7 +93,11 @@ function StatusBar() {
height: "100%"
}}
>
<Circle size={7} color={"var(--icon-error)"} />
<Circle
size={7}
color={"var(--icon-error)"}
data-test-id="logged-in"
/>
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
{strings.emailNotConfirmed()}
</Text>
@@ -101,22 +106,6 @@ function StatusBar() {
<SyncStatus />
</>
) : isLoggedIn === false ? (
<Button
variant="statusitem"
onClick={() => hardNavigate("/login")}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
data-test-id="not-logged-in"
>
<Circle size={7} color="var(--icon-error)" />
<Text variant="subBody" ml={1} sx={{ color: "paragraph" }}>
{strings.notLoggedIn()}
</Text>
</Button>
) : null}
{activeCredentials().length > 0 && (
<Button

View File

@@ -1,197 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import ListItem from "../list-item";
import { Flex, Text } from "@theme-ui/components";
import { useStore as useNotesStore } from "../../stores/note-store";
import { Notebook } from "@notesnook/core";
import { notebookMenuItems } from "../notebook";
import { ChevronDown, ChevronRight, Plus } from "../icons";
import { MenuItem } from "@notesnook/ui";
import { navigate } from "../../navigation";
import { useCallback, useRef } from "react";
import { handleDrop } from "../../common/drop-handler";
import { useDragHandler } from "../../hooks/use-drag-handler";
import { AddNotebookDialog } from "../../dialogs/add-notebook-dialog";
import { useStore as useSelectionStore } from "../../stores/selection-store";
import { Multiselect } from "../../common/multi-select";
import { strings } from "@notesnook/intl";
type SubNotebookProps = {
item: Notebook;
totalNotes: number;
isExpandable: boolean;
isExpanded: boolean;
expand: () => void;
collapse: () => void;
refresh?: () => void;
depth: number;
rootId: string;
};
function SubNotebook(props: SubNotebookProps) {
const {
item,
totalNotes,
isExpandable,
isExpanded,
expand,
collapse,
refresh,
depth,
rootId
} = props;
const isOpened = useNotesStore(
(store) =>
store.context?.type === "notebook" && store.context.id === item.id
);
const dragTimeout = useRef(0);
const { isDragEntering, isDragLeaving } = useDragHandler(`id_${item.id}`);
const openNotebook = useCallback(async () => {
if (isOpened) return;
focus();
expand();
await useNotesStore.getState().setContext({
type: "notebook",
id: item.id,
item,
totalNotes
});
navigate(`/notebooks/${rootId}/${item.id}`);
}, [expand, focus, isOpened, item, rootId, totalNotes]);
return (
<ListItem
draggable
isFocused={isOpened}
isCompact
item={item}
onClick={() => openNotebook()}
onDragEnter={(e) => {
if (!isDragEntering(e)) return;
e.currentTarget.focus();
focus();
dragTimeout.current = setTimeout(() => {
openNotebook();
}, 1000) as unknown as number;
}}
onDragLeave={(e) => {
if (!isDragLeaving(e)) return;
clearTimeout(dragTimeout.current);
}}
onDrop={async (e) => {
clearTimeout(dragTimeout.current);
handleDrop(e.dataTransfer, item);
}}
onKeyPress={async (e) => {
if (e.code === "Space") {
e.stopPropagation();
if (isExpandable) isExpanded ? collapse() : expand();
else if (!isOpened) {
focus();
await useNotesStore.getState().setContext({
type: "notebook",
id: item.id,
item,
totalNotes
});
navigate(`/notebooks/${rootId}/${item.id}`);
}
} else if (e.code === "Delete") {
e.stopPropagation();
await Multiselect.moveNotebooksToTrash(
useSelectionStore.getState().selectedItems
);
}
}}
title={
<Flex sx={{ alignItems: "center", justifyContent: "center" }}>
{isExpandable ? (
isExpanded ? (
<ChevronDown
size={16}
onClick={(e) => {
e.stopPropagation();
collapse();
}}
/>
) : (
<ChevronRight
size={16}
onClick={(e) => {
e.stopPropagation();
expand();
}}
/>
)
) : null}
<Text
data-test-id={`title`}
variant={"body"}
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontWeight: "body",
display: "block"
}}
>
{item.title}
</Text>
</Flex>
}
footer={<Text variant="subBody">{totalNotes}</Text>}
menuItems={subNotebookMenuItems}
context={{ refresh }}
sx={{
paddingLeft:
depth === 0
? isExpandable
? 0
: "5px"
: `${16 * depth - (isExpandable ? 5 : 0)}px`
}}
/>
);
}
export default SubNotebook;
const subNotebookMenuItems: (
notebook: Notebook,
ids?: string[],
context?: { refresh?: () => void }
) => MenuItem[] = (notebook, ids = [], context) => {
const menuItems = notebookMenuItems(notebook, ids);
return [
{
type: "button",
key: "add",
title: strings.newNotebook(),
icon: Plus.path,
onClick: () =>
AddNotebookDialog.show({ parentId: notebook.id }).then((res) =>
res ? context?.refresh?.() : null
)
},
{ type: "separator", key: "sepep2" },
...menuItems
];
};

View File

@@ -19,34 +19,56 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import ListItem from "../list-item";
import { navigate } from "../../navigation";
import { Text } from "@theme-ui/components";
import { Flex, Text } from "@theme-ui/components";
import { store as appStore } from "../../stores/app-store";
import { db } from "../../common/db";
import { Edit, Shortcut, DeleteForver } from "../icons";
import { Edit, Shortcut, DeleteForver, Tag as TagIcon } from "../icons";
import { MenuItem } from "@notesnook/ui";
import { Tag as TagType } from "@notesnook/core";
import { handleDrop } from "../../common/drop-handler";
import { EditTagDialog } from "../../dialogs/item-dialog";
import { useStore as useSelectionStore } from "../../stores/selection-store";
import { useStore as useNoteStore } from "../../stores/note-store";
import { Multiselect } from "../../common/multi-select";
import { strings } from "@notesnook/intl";
type TagProps = { item: TagType; totalNotes: number };
function Tag(props: TagProps) {
const { item, totalNotes } = props;
const { id, title } = item;
const { id } = item;
const isSelected = useNoteStore(
(store) => store.context?.type === "tag" && store.context.id === id
);
return (
<ListItem
item={item}
isCompact
isFocused={isSelected}
sx={{
borderRadius: "default",
mb: "small"
}}
title={
<Text as="span" variant="body" data-test-id={`title`}>
<Text as="span" sx={{ color: "accent" }}>
{"#"}
<Flex
sx={{ alignItems: "center", justifyContent: "center", gap: "small" }}
>
<TagIcon size={14} color={isSelected ? "icon-selected" : "icon"} />
<Text
data-test-id={`title`}
variant={"body"}
color={isSelected ? "paragraph-selected" : "paragraph"}
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontWeight: "body",
display: "block"
}}
>
{item.title}
</Text>
{title}
</Text>
</Flex>
}
footer={
<Text mt={1} variant="subBody">
@@ -58,6 +80,8 @@ function Tag(props: TagProps) {
await Multiselect.deleteTags(
useSelectionStore.getState().selectedItems
);
} else if (e.key === "Enter") {
navigate(`/tags/${id}`);
}
}}
menuItems={tagMenuItems}

View File

@@ -86,7 +86,7 @@ export function TitleBar({ isUnderlay = isMac() }: { isUnderlay?: boolean }) {
scope="titleBar"
className="titlebar"
sx={{
background: "background",
// background: "background",
height: TITLE_BAR_HEIGHT,
minHeight: TITLE_BAR_HEIGHT,
maxHeight: TITLE_BAR_HEIGHT,
@@ -95,7 +95,7 @@ export function TitleBar({ isUnderlay = isMac() }: { isUnderlay?: boolean }) {
flexShrink: 0,
width: "100%",
zIndex: 1,
borderBottom: "1px solid var(--border)",
//borderBottom: "1px solid var(--border)",
...(isUnderlay
? {
position: "absolute",

View File

@@ -19,7 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useEffect, useImperativeHandle, useRef, useState } from "react";
import { usePersistentState } from "../../hooks/use-persistent-state";
import { ItemProps, Virtuoso, VirtuosoHandle } from "react-virtuoso";
import {
Components,
ItemProps,
Virtuoso,
VirtuosoHandle
} from "react-virtuoso";
import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation";
import { CustomScrollbarsVirtualList, waitForElement } from "../list-container";
@@ -33,6 +38,7 @@ export type TreeNode<T = any> = {
depth: number;
hasChildren: boolean;
data: T;
expanded?: boolean;
};
type ExpandedIds = Record<string, boolean>;
type TreeViewProps<T> = {
@@ -56,6 +62,9 @@ type TreeViewProps<T> = {
bulkSelect?: (ids: string[]) => void;
deselectAll?: () => void;
isSelected?: (id: string) => boolean;
style?: React.CSSProperties;
Scroller?: Components["Scroller"];
};
export function VirtualizedTree<T>(props: TreeViewProps<T>) {
const {
@@ -72,7 +81,8 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
onSelect,
deselectAll,
bulkSelect,
testId
testId,
Scroller
} = props;
const [nodes, setNodes] = useState<TreeNode<T>[]>([]);
const [expandedIds, setExpandedIds] = usePersistentState<ExpandedIds>(
@@ -85,8 +95,15 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
treeRef,
() => ({
async refresh() {
const children = await getChildNodes(rootId, -1);
setNodes([]);
const { children } = await fetchChildren(
rootId,
-1,
expandedIds,
getChildNodes
);
setNodes(children);
setExpandedIds(expandedIds);
},
async refreshItem(index, item) {
const node = nodes[index];
@@ -103,7 +120,12 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
return;
}
const children = await fetchChildren(
// TODO: double check
if (node.hasChildren) {
expandedIds[node.id] = true;
}
const { children } = await fetchChildren(
node.id,
node.depth,
expandedIds,
@@ -121,6 +143,7 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
},
...children
);
setExpandedIds(expandedIds);
setNodes(filtered);
}
}),
@@ -168,68 +191,75 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
});
useEffect(() => {
fetchChildren(rootId, -1, expandedIds, getChildNodes).then(setNodes);
fetchChildren(rootId, -1, expandedIds, getChildNodes).then(
({ children, expandedIds }) => {
setNodes(children);
setExpandedIds(expandedIds);
}
);
console.log("fetching");
}, [rootId]);
console.log(expandedIds);
return (
<Virtuoso
data-test-id={testId}
ref={list}
data={nodes}
computeItemKey={(i, item) => item.id}
totalCount={nodes.length}
computeItemKey={(i) => nodes[i].id}
fixedItemHeight={itemHeight}
onKeyDown={(e) => onKeyDown(e.nativeEvent)}
context={{
onMouseUp
}}
components={{
Scroller: CustomScrollbarsVirtualList,
Scroller: (Scroller as any) || CustomScrollbarsVirtualList,
Item: VirtuosoItem,
EmptyPlaceholder: Placeholder
}}
itemContent={(index, node) => (
<Node
item={node}
index={index}
expanded={expandedIds[node.id]}
collapse={() => {
if (!expandedIds[node.id]) return;
itemContent={(index) => {
const node = nodes[index];
return (
<Node
item={node}
index={index}
expanded={expandedIds[node.id]}
collapse={() => {
if (!expandedIds[node.id]) return;
const expanded = { ...expandedIds, [node.id]: false };
setNodes((tree) => {
const removeIds: string[] = [];
for (const treeNode of tree) {
if (
treeNode.parentId === node.id ||
removeIds.includes(treeNode.parentId)
) {
expanded[treeNode.id] = false;
removeIds.push(treeNode.id);
const expanded = { ...expandedIds, [node.id]: false };
setNodes((tree) => {
const removeIds: string[] = [];
for (const treeNode of tree) {
if (
treeNode.parentId === node.id ||
removeIds.includes(treeNode.parentId)
) {
removeIds.push(treeNode.id);
}
}
}
return tree.filter((n) => !removeIds.includes(n.id));
});
setExpandedIds(expanded);
}}
expand={async () => {
if (expandedIds[node.id]) return;
return tree.filter((n) => !removeIds.includes(n.id));
});
setExpandedIds(expanded);
}}
expand={async () => {
if (expandedIds[node.id]) return;
setExpandedIds({ ...expandedIds, [node.id]: true });
const children = await fetchChildren(
node.id,
node.depth,
expandedIds,
getChildNodes
);
setNodes((tree) => {
const copy = tree.slice();
copy.splice(index + 1, 0, ...children);
return copy;
});
}}
/>
)}
const { children } = await fetchChildren(
node.id,
node.depth,
expandedIds,
getChildNodes
);
setExpandedIds({ ...expandedIds, [node.id]: true });
setNodes((tree) => {
const copy = tree.slice();
copy.splice(index + 1, 0, ...children);
return copy;
});
}}
/>
);
}}
/>
);
}
@@ -265,8 +295,14 @@ async function fetchChildren<T>(
const children = await getChildNodes(id, depth);
for (let i = 0; i < children.length; i++) {
const childNode = children[i];
if (expandedIds[childNode.id]) {
const nodes = await fetchChildren(
if (
expandedIds[childNode.id] ||
(expandedIds[childNode.id] === undefined &&
childNode.expanded &&
childNode.hasChildren)
) {
expandedIds[childNode.id] = true;
const { children: nodes } = await fetchChildren(
childNode.id,
childNode.depth,
expandedIds,
@@ -276,5 +312,5 @@ async function fetchChildren<T>(
i += nodes.length;
}
}
return children;
return { children, expandedIds };
}

View File

@@ -457,12 +457,12 @@ const Sidebar = memo(
backgroundColor: "background"
}}
>
<Flex sx={{ flexDirection: "column" }}>
<Flex sx={{ flexDirection: "column", gap: "small", mx: 2, mt: 2 }}>
<Input
id="search"
name="search"
placeholder={strings.search()}
sx={{ m: 2, mb: 0, width: "auto", bg: "background", py: "7px" }}
sx={{ mb: 2, width: "auto", bg: "background", py: "7px" }}
onChange={(e) => {
setRoute(e.target.value ? "none" : "all");
if (e.target.value) filter(e.target.value);
@@ -473,13 +473,14 @@ const Sidebar = memo(
key={item.id}
icon={item.icon}
title={item.title}
count={counts[item.id]}
onClick={() => {
onRouteChange(item.id);
setRoute(item.id);
}}
selected={route === item.id}
/>
>
<Text variant="subBody">{counts[item.id]}</Text>
</NavigationItem>
))}
</Flex>
<Flex sx={{ flexDirection: "column" }}>

Some files were not shown because too many files have changed in this diff Show More