Compare commits

..

1 Commits

Author SHA1 Message Date
alihamuh
29a9483aae web: added useEffect to handle note count change in notebook-view 2024-05-07 12:40:52 +05:00
7 changed files with 104 additions and 198 deletions

View File

@@ -38,7 +38,7 @@ await fs.rm("./build/", { force: true, recursive: true });
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
await exec(
"yarn nx build:desktop @notesnook/web",
"npx nx build:desktop @notesnook/web",
path.join(__dirname, "..", "..", "..")
);
}
@@ -48,7 +48,7 @@ if (os.platform() === "linux") await patchBetterSQLite3();
if (os.platform() === "win32")
await exec(
`yarn prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
`npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
path.join(__dirname, "..", "node_modules", "sodium-native")
);
@@ -58,15 +58,15 @@ await fs.cp(path.join(webAppPath, "build"), "build", {
});
if (args.variant === "mas") {
await exec(`yarn run bundle:mas`);
await exec(`npm run bundle:mas`);
} else {
await exec(`yarn run bundle`);
await exec(`npm run bundle`);
}
await exec(`yarn tsc`);
await exec(`npx tsc`);
if (args.run) {
await exec(`yarn electron-builder --dir --x64`);
await exec(`npx electron-builder --dir --x64`);
if (process.platform === "win32") {
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
} else if (process.platform === "darwin") {

View File

@@ -53,11 +53,11 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("yarn electron-builder install-app-deps");
await exec("npx electron-builder install-app-deps");
}
await exec(`yarn run bundle`);
execAsync(`yarn`, [`tsc`]);
await exec(`npm run bundle`);
execAsync(`npx`, [`tsc`]);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");
@@ -66,7 +66,7 @@ async function onChange(first) {
if (first) {
await spawnAndWaitUntil(
["yarn", "nx", "start:desktop", "@notesnook/web"],
["npx", "nx", "start:desktop", "@notesnook/web"],
path.join(__dirname, "..", "..", ".."),
(data) => data.includes("Network: use --host to expose")
);
@@ -78,7 +78,7 @@ async function onChange(first) {
}
execAsync(
"yarn",
"npx",
["electron", path.join("build", "electron.js")],
true,
cleanup

View File

@@ -164,8 +164,6 @@ function SubNotebooks({
const contextNotes = useNotesStore((store) => store.contextNotes);
const context = useNotesStore((store) => store.context);
const [title, setTitle] = useState<string>();
const saveViewState = useCallback((id: string) => {
if (!treeRef.current?.viewState) return;
Config.set(`${id}:viewState`, treeRef.current.viewState[id]);
@@ -200,16 +198,7 @@ function SubNotebooks({
});
}, [contextNotes, context]);
useEffect(() => {
(async function () {
const notebook = await db.notebooks.notebook(rootId);
if (notebook) setTitle(notebook.title);
})();
});
if (!rootId) return null;
// if (!notebook) return null;
// const { title } = notebook;
return (
<Flex
@@ -242,7 +231,7 @@ function SubNotebooks({
<Flex sx={{ alignItems: "center" }}>
{isCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
<Text variant="subBody" sx={{ fontSize: 11 }}>
{title}
NOTEBOOKS
</Text>
</Flex>
<Flex sx={{ alignItems: "center" }}>
@@ -307,6 +296,17 @@ function SubNotebooks({
};
},
async getTreeItem(itemId) {
if (itemId === "root") {
return {
data: { notebook: { title: "Root" } },
index: itemId,
isFolder: true,
canMove: false,
canRename: false,
children: [rootId]
};
}
const notebook = (await db.notebooks.notebook(itemId as string))!;
const children = await db.relations
.from({ type: "notebook", id: itemId as string }, "notebook")
@@ -331,6 +331,18 @@ function SubNotebooks({
.get();
return itemIds.reduce((prev, id) => {
if (id === "root") {
prev.push({
data: { notebook: { title: "Root" } },
index: id,
isFolder: true,
canMove: false,
canRename: false,
children: [rootId]
});
return prev;
}
const notebook = notebooks[id];
if (!notebook) return prev;
@@ -389,7 +401,7 @@ function SubNotebooks({
{children}
</div>
)}
rootItem={rootId}
rootItem="root"
treeLabel="Tree Example"
/>
</UncontrolledTreeEnvironment>
@@ -436,6 +448,13 @@ function NotebookHeader({
})();
}, [context.id, totalNotes, notebook]);
useEffect(() => {
(async function () {
if ((await db.notebooks.totalNotes(context.id)) !== totalNotes)
setTotalNotes(await db.notebooks.totalNotes(context.id));
})();
});
useEffect(() => {
(async function () {
setCrumbs(await db.notebooks.breadcrumbs(context.id));

View File

@@ -95,7 +95,6 @@ class Database {
isInitialized = false;
eventManager = new EventManager();
sseMutex = new Mutex();
_fs?: FileStorage;
storage: StorageAccessor = () => {
if (!this.options?.storage)
@@ -110,10 +109,7 @@ class Database {
throw new Error(
"Database not initialized. Did you forget to call db.setup()?"
);
return (
this._fs ||
(this._fs = new FileStorage(this.options.fs, this.tokenManager))
);
return new FileStorage(this.options.fs, this.tokenManager);
};
crypto: CryptoAccessor = () => {

View File

@@ -22,12 +22,10 @@ import { CURRENT_DATABASE_VERSION } from "../common.js";
import Migrator from "./migrator.js";
import Database from "../api/index.js";
import {
Attachment,
Item,
MaybeDeletedItem,
Note,
Notebook,
Relation,
ValueOf,
isDeleted
} from "../types.js";
@@ -38,7 +36,6 @@ import { DatabaseCollection } from "./index.js";
import { DefaultColors } from "../collections/colors.js";
import { toChunks } from "../utils/array.js";
import { logger } from "../logger.js";
import { clone } from "../utils/clone.js";
type BackupDataItem = MaybeDeletedItem<Item> | string[];
type BackupPlatform = "web" | "mobile" | "node";
@@ -95,44 +92,10 @@ function isEncryptedBackup(
return "encrypted" in backup ? backup.encrypted : isCipher(backup.data);
}
/**
* Due to a bug in v3.0, legacy backups were created with version set to 6.1
* while their actual data was at version 5.9. This caused various issues when
* restoring such a backup.
* This function tries to work around that bug by detecting the version based on
* the actual data.
*/
function isLegacyBackup(data: BackupDataItem[]) {
const note = data.find(
(c): c is Note => !isDeleted(c) && !Array.isArray(c) && c.type === "note"
);
if (note)
return (
"color" in note ||
"notebooks" in note ||
"tags" in note ||
"locked" in note
);
const notebook = data.find(
(c): c is Notebook =>
!isDeleted(c) && !Array.isArray(c) && c.type === "notebook"
);
if (notebook) return "topics" in notebook;
const attachment = data.find(
(c): c is Attachment =>
!isDeleted(c) && !Array.isArray(c) && c.type === "attachment"
);
if (attachment) return "noteIds" in attachment;
const relation = data.find(
(c): c is Relation =>
!isDeleted(c) && !Array.isArray(c) && c.type === "relation"
);
if (relation) return "from" in relation || "to" in relation;
return false;
function isLegacyBackupFile(
backup: LegacyBackupFile | BackupFile
): backup is LegacyBackupFile {
return backup.version <= 5.8;
}
const MAX_CHUNK_SIZE = 10 * 1024 * 1024;
@@ -258,7 +221,7 @@ export default class Backup {
yield {
path: `${chunkIndex++}-${encrypt ? "encrypted" : "plain"}-${hash}`,
data: `{
"version": 5.9,
"version": ${CURRENT_DATABASE_VERSION},
"type": "${type}",
"date": ${Date.now()},
"data": ${itemsJSON},
@@ -434,16 +397,13 @@ export default class Backup {
if (!data) throw new Error("No data found.");
const normalizedData: BackupDataItem[] = Array.isArray(data)
? (data as BackupDataItem[])
: typeof data === "object"
? Object.values(data)
: [];
await this.migrateData(
normalizedData,
backup.version === 6.1 && isLegacyBackup(normalizedData)
? 5.9
: backup.version
Array.isArray(data)
? (data as BackupDataItem[])
: typeof data === "object"
? Object.values(data)
: [],
backup.version
);
}

View File

@@ -30,22 +30,16 @@ import { logger } from "../logger";
export type FileStorageAccessor = () => FileStorage;
export type DownloadableFile = {
filename: string;
// metadata: AttachmentMetadata;
chunkSize: number;
};
export type QueueItem = DownloadableFile & {
cancel?: (reason?: string) => Promise<void>;
operation?: Promise<boolean>;
};
export class FileStorage {
id = Date.now();
downloads = new Map<string, QueueItem>();
uploads = new Map<string, QueueItem>();
groups = {
downloads: new Map<string, Set<string>>(),
uploads: new Map<string, Set<string>>()
};
downloads = new Map<string, QueueItem[]>();
uploads = new Map<string, QueueItem[]>();
constructor(
private readonly fs: IFileStorage,
private readonly tokenManager: TokenManager
@@ -56,26 +50,12 @@ export class FileStorage {
groupId: string,
eventData?: Record<string, unknown>
) {
let current = 0;
const token = await this.tokenManager.getAccessToken();
const total = files.length;
const group = this.groups.downloads.get(groupId) || new Set();
files.forEach((f) => group.add(f.filename));
this.groups.downloads.set(groupId, group);
let current = 0;
this.downloads.set(groupId, files);
for (const file of files as QueueItem[]) {
if (!group.has(file.filename)) continue;
const download = this.downloads.get(file.filename);
if (download && download.operation) {
logger.debug("[queueDownloads] duplicate download", {
filename: file.filename,
groupId
});
await download.operation;
continue;
}
const { filename, chunkSize } = file;
if (await this.exists(filename)) {
current++;
@@ -88,13 +68,6 @@ export class FileStorage {
continue;
}
EV.publish(EVENTS.fileDownload, {
total,
current,
groupId,
filename
});
const url = `${hosts.API_HOST}/s3?name=${filename}`;
const { execute, cancel } = this.fs.downloadFile(filename, {
url,
@@ -102,15 +75,15 @@ export class FileStorage {
headers: { Authorization: `Bearer ${token}` }
});
file.cancel = cancel;
file.operation = execute()
.catch(() => false)
.finally(() => {
this.downloads.delete(filename);
group.delete(filename);
});
this.downloads.set(filename, file);
const result = await file.operation;
EV.publish(EVENTS.fileDownload, {
total,
current,
groupId,
filename
});
const result = await execute().catch(() => false);
if (eventData)
EV.publish(EVENTS.fileDownloaded, {
success: result,
@@ -121,31 +94,17 @@ export class FileStorage {
eventData
});
}
this.downloads.delete(groupId);
}
async queueUploads(files: DownloadableFile[], groupId: string) {
let current = 0;
const token = await this.tokenManager.getAccessToken();
const total = files.length;
const group = this.groups.uploads.get(groupId) || new Set();
files.forEach((f) => group.add(f.filename));
this.groups.uploads.set(groupId, group);
let current = 0;
this.uploads.set(groupId, files);
for (const file of files as QueueItem[]) {
if (!group.has(file.filename)) continue;
const upload = this.uploads.get(file.filename);
if (upload && upload.operation) {
logger.debug("[queueUploads] duplicate upload", {
filename: file.filename,
groupId
});
await file.operation;
continue;
}
const { filename, chunkSize } = file;
let error = null;
const url = `${hosts.API_HOST}/s3?name=${filename}`;
const { execute, cancel } = this.fs.uploadFile(filename, {
chunkSize,
@@ -153,16 +112,6 @@ export class FileStorage {
headers: { Authorization: `Bearer ${token}` }
});
file.cancel = cancel;
file.operation = execute()
.catch((e) => {
logger.error(e, "failed to upload attachment", { hash: filename });
error = e;
return false;
})
.finally(() => {
this.uploads.delete(filename);
group.delete(filename);
});
EV.publish(EVENTS.fileUpload, {
total,
@@ -171,8 +120,13 @@ export class FileStorage {
filename
});
this.uploads.set(filename, file);
const result = await file.operation;
let error = null;
const result = await execute().catch((e) => {
logger.error(e, "failed to upload attachment", { hash: filename });
error = e;
return false;
});
EV.publish(EVENTS.fileUploaded, {
error,
success: result,
@@ -182,67 +136,44 @@ export class FileStorage {
filename
});
}
this.uploads.delete(groupId);
}
async downloadFile(groupId: string, filename: string, chunkSize: number) {
if (await this.exists(filename)) return true;
const download = this.downloads.get(filename);
if (download && download.operation) {
logger.debug("[downloadFile] duplicate download", { filename, groupId });
return await download.operation;
}
logger.debug("[downloadFile] downloading", { filename, groupId });
const url = `${hosts.API_HOST}/s3?name=${filename}`;
const file: QueueItem = { filename, chunkSize };
const token = await this.tokenManager.getAccessToken();
const group = this.groups.downloads.get(groupId) || new Set();
const { execute, cancel } = this.fs.downloadFile(filename, {
url,
chunkSize,
headers: { Authorization: `Bearer ${token}` }
});
file.cancel = cancel;
file.operation = execute().finally(() => {
this.downloads.delete(filename);
group.delete(filename);
});
this.downloads.set(filename, file);
this.groups.downloads.set(groupId, group.add(filename));
return await file.operation;
this.downloads.set(groupId, [{ cancel, filename, chunkSize }]);
const result = await execute();
this.downloads.delete(groupId);
return result;
}
async cancel(groupId: string) {
const queues = [
{
type: "download",
ids: this.groups.downloads.get(groupId),
files: this.downloads
},
{
type: "upload",
ids: this.groups.uploads.get(groupId),
files: this.uploads
}
].filter((a) => !!a.ids);
{ type: "download", files: this.downloads.get(groupId) },
{ type: "upload", files: this.uploads.get(groupId) }
].filter((a) => !!a.files);
for (const queue of queues) {
if (!queue.ids) continue;
for (const filename of queue.ids) {
const file = queue.files.get(filename);
if (file?.cancel) await file.cancel("Operation canceled.");
queue.ids.delete(filename);
if (!queue.files) continue;
for (let i = 0; i < queue.files.length; ++i) {
const file = queue.files[i];
if (file.cancel) await file.cancel("Operation canceled.");
queue.files.splice(i, 1);
}
if (queue.type === "download") {
this.groups.downloads.delete(groupId);
this.downloads.delete(groupId);
EV.publish(EVENTS.downloadCanceled, { groupId, canceled: true });
} else if (queue.type === "upload") {
this.groups.uploads.delete(groupId);
this.uploads.delete(groupId);
EV.publish(EVENTS.uploadCanceled, { groupId, canceled: true });
}
}

View File

@@ -41,7 +41,6 @@ import {
} from "../types";
import { IndexedCollection } from "./indexed-collection";
import { SQLCollection } from "./sql-collection";
import { logger } from "../logger";
export type RawItem = MaybeDeletedItem<Item>;
type MigratableCollection = {
@@ -137,16 +136,10 @@ class Migrator {
for (let i = 0; i < items.length; ++i) {
const item = items[i];
// can be true due to corrupted data.
if (Array.isArray(item)) {
logger.debug("Skipping item during migration to SQLite", {
table,
version,
item
});
continue;
}
if (Array.isArray(item)) continue;
if (!item) continue;
const itemId = item.id;
let migrated = await migrateItem(
item,
version,
@@ -168,7 +161,14 @@ class Migrator {
);
}
if (migrated !== "skip") toAdd.push(item);
if (migrated === true) {
toAdd.push(item);
// if id changed after migration, we need to delete the old one.
if (item.id !== itemId) {
// await collection.deleteItem(itemId);
}
}
}
if (toAdd.length > 0) {
@@ -217,7 +217,7 @@ class Migrator {
);
}
if (!migrated || migrated === "skip") continue;
if (!migrated) continue;
toAdd.push(item);