mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Compare commits
32 Commits
fix-androi
...
fix-task-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2bbccf19e | ||
|
|
375dd18618 | ||
|
|
82f635305e | ||
|
|
00345176f7 | ||
|
|
9032e3b4bd | ||
|
|
432a9c4e84 | ||
|
|
aaaa9ebbc5 | ||
|
|
1ac4dca6b5 | ||
|
|
9dc549d0d3 | ||
|
|
775570fc13 | ||
|
|
997f0bc93e | ||
|
|
a2016e9406 | ||
|
|
87623aced1 | ||
|
|
24977277d0 | ||
|
|
2f890fd86b | ||
|
|
0220bb4040 | ||
|
|
09a8a0ba6d | ||
|
|
808670f6e1 | ||
|
|
62ab799a4a | ||
|
|
ecee87dac9 | ||
|
|
a5d52ded77 | ||
|
|
4e786a83f9 | ||
|
|
bf48ce788d | ||
|
|
f6637da274 | ||
|
|
6cd4a4a041 | ||
|
|
02ece7139e | ||
|
|
95203d723b | ||
|
|
b423cef2e6 | ||
|
|
e46b3ea85f | ||
|
|
0019c1f7af | ||
|
|
ffc4e2d263 | ||
|
|
c01ad2e49c |
16
.github/workflows/desktop.publish.yml
vendored
16
.github/workflows/desktop.publish.yml
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
746
apps/desktop/package-lock.json
generated
746
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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/"
|
||||
);
|
||||
|
||||
@@ -29,3 +29,7 @@ export function isDevelopment() {
|
||||
export function isFlatpak() {
|
||||
return existsSync("/.flatpak-info");
|
||||
}
|
||||
|
||||
export function isSnap() {
|
||||
return process.env.SNAP !== undefined;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 |
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
];
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
1384
apps/web/package-lock.json
generated
1384
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
BIN
apps/web/src/assets/fonts/Inter-Bold.ttf
Normal file
BIN
apps/web/src/assets/fonts/Inter-Bold.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-Bold.woff2
Normal file
BIN
apps/web/src/assets/fonts/Inter-Bold.woff2
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-BoldItalic.ttf
Normal file
BIN
apps/web/src/assets/fonts/Inter-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-BoldItalic.woff2
Normal file
BIN
apps/web/src/assets/fonts/Inter-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-Italic.ttf
Normal file
BIN
apps/web/src/assets/fonts/Inter-Italic.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-Italic.woff2
Normal file
BIN
apps/web/src/assets/fonts/Inter-Italic.woff2
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-Medium.ttf
Normal file
BIN
apps/web/src/assets/fonts/Inter-Medium.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-Medium.woff2
Normal file
BIN
apps/web/src/assets/fonts/Inter-Medium.woff2
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-MediumItalic.ttf
Normal file
BIN
apps/web/src/assets/fonts/Inter-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-MediumItalic.woff2
Normal file
BIN
apps/web/src/assets/fonts/Inter-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-Regular.ttf
Normal file
BIN
apps/web/src/assets/fonts/Inter-Regular.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-Regular.woff2
Normal file
BIN
apps/web/src/assets/fonts/Inter-Regular.woff2
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-SemiBold.ttf
Normal file
BIN
apps/web/src/assets/fonts/Inter-SemiBold.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-SemiBold.woff2
Normal file
BIN
apps/web/src/assets/fonts/Inter-SemiBold.woff2
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-SemiBoldItalic.ttf
Normal file
BIN
apps/web/src/assets/fonts/Inter-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
apps/web/src/assets/fonts/Inter-SemiBoldItalic.woff2
Normal file
BIN
apps/web/src/assets/fonts/Inter-SemiBoldItalic.woff2
Normal file
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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> = {};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
72
apps/web/src/components/navigation-menu/tab-item.tsx
Normal file
72
apps/web/src/components/navigation-menu/tab-item.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
136
apps/web/src/components/notebook-header.tsx
Normal file
136
apps/web/src/components/notebook-header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -40,6 +40,7 @@ export function SessionItem(props: SessionItemProps) {
|
||||
py={1}
|
||||
px={1}
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
cursor: "pointer",
|
||||
bg: "transparent",
|
||||
":hover": {
|
||||
|
||||
41
apps/web/src/components/sidebar-scroller.tsx
Normal file
41
apps/web/src/components/sidebar-scroller.tsx
Normal 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;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -55,5 +55,5 @@ body.react-split--disabled {
|
||||
}
|
||||
|
||||
.split-sash-content.split-sash-content-active {
|
||||
background-color: var(--accent);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
];
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user