mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 23:19:40 +01:00
global: move item resolution logic to @notesnook/common
This commit is contained in:
20
packages/common/src/components/index.ts
Normal file
20
packages/common/src/components/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
export * from "./resolved-item";
|
||||
39
packages/common/src/components/resolved-item.tsx
Normal file
39
packages/common/src/components/resolved-item.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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 { ItemMap, ItemType } from "@notesnook/core";
|
||||
import {
|
||||
ResolvedItemOptions,
|
||||
useResolvedItem
|
||||
} from "../hooks/use-resolved-item";
|
||||
|
||||
type ResolvedItemProps<TItemType extends ItemType> =
|
||||
ResolvedItemOptions<TItemType> & {
|
||||
children: (item: {
|
||||
item: ItemMap[TItemType];
|
||||
data: unknown;
|
||||
}) => React.ReactNode;
|
||||
};
|
||||
export function ResolvedItem<TItemType extends ItemType>(
|
||||
props: ResolvedItemProps<TItemType>
|
||||
) {
|
||||
const { children } = props;
|
||||
const result = useResolvedItem(props);
|
||||
return result ? <>{children(result)}</> : null;
|
||||
}
|
||||
@@ -18,3 +18,5 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./use-time-ago";
|
||||
export * from "./use-promise";
|
||||
export * from "./use-resolved-item";
|
||||
|
||||
89
packages/common/src/hooks/use-promise.ts
Normal file
89
packages/common/src/hooks/use-promise.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
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 { DependencyList, useEffect, useState } from "react";
|
||||
|
||||
export type PromiseResult<T> =
|
||||
| PromisePendingResult<T>
|
||||
| (PromiseSettledResult<T> & { refresh: () => void });
|
||||
|
||||
export interface PromisePendingResult<T> {
|
||||
status: "pending";
|
||||
value?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that creates a promise, takes a signal to abort fetch requests.
|
||||
*/
|
||||
export type PromiseFactoryFn<T> = (signal: AbortSignal) => T | Promise<T>;
|
||||
|
||||
/**
|
||||
* Takes a function that creates a Promise and returns its pending, fulfilled, or rejected result.
|
||||
*
|
||||
* ```ts
|
||||
* const result = usePromise(() => fetch('/api/products'))
|
||||
* ```
|
||||
*
|
||||
* Also takes a list of dependencies, when the dependencies change the promise is recreated.
|
||||
*
|
||||
* ```ts
|
||||
* const result = usePromise(() => fetch(`/api/products/${id}`), [id])
|
||||
* ```
|
||||
*
|
||||
* Can abort a fetch request, a [signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) is provided from the factory function to do so.
|
||||
*
|
||||
* ```ts
|
||||
* const result = usePromise(signal => fetch(`/api/products/${id}`, { signal }), [id])
|
||||
* ```
|
||||
*
|
||||
* @param factory Function that creates the promise.
|
||||
* @param deps If present, promise will be recreated if the values in the list change.
|
||||
*/
|
||||
export function usePromise<T>(
|
||||
factory: PromiseFactoryFn<T>,
|
||||
deps: DependencyList = []
|
||||
): PromiseResult<T> {
|
||||
const [result, setResult] = useState<PromiseResult<T>>({ status: "pending" });
|
||||
|
||||
useEffect(function effect() {
|
||||
if (result.status !== "pending") {
|
||||
setResult((s) => ({
|
||||
...s,
|
||||
status: "pending"
|
||||
}));
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
async function handlePromise() {
|
||||
const [promiseResult] = await Promise.allSettled([factory(signal)]);
|
||||
|
||||
if (!signal.aborted) {
|
||||
setResult({ ...promiseResult, refresh: effect });
|
||||
}
|
||||
}
|
||||
|
||||
handlePromise();
|
||||
|
||||
return () => controller.abort();
|
||||
}, deps);
|
||||
|
||||
return result;
|
||||
}
|
||||
59
packages/common/src/hooks/use-resolved-item.ts
Normal file
59
packages/common/src/hooks/use-resolved-item.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
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 { ItemMap, ItemType, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { usePromise } from "./use-promise";
|
||||
import { resolveItems } from "../utils/resolve-items";
|
||||
|
||||
export type ResolvedItemOptions<TItemType extends ItemType> = {
|
||||
type?: TItemType;
|
||||
items: VirtualizedGrouping<ItemMap[TItemType]>;
|
||||
index: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches & resolves the item from VirtualizedGrouping
|
||||
*/
|
||||
export function useResolvedItem<TItemType extends ItemType>(
|
||||
options: ResolvedItemOptions<TItemType>
|
||||
) {
|
||||
const { index, items, type } = options;
|
||||
const result = usePromise(
|
||||
() => items.item(index, resolveItems),
|
||||
[index, items]
|
||||
);
|
||||
|
||||
if (result.status === "rejected" || !result.value) return null;
|
||||
if (type && result.value.item.type !== type) return null;
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches but does not resolve the item from VirtualizedGrouping
|
||||
*/
|
||||
export function useUnresolvedItem<TItemType extends ItemType>(
|
||||
options: ResolvedItemOptions<TItemType>
|
||||
) {
|
||||
const { index, items, type } = options;
|
||||
const result = usePromise(() => items.item(index), [index, items]);
|
||||
|
||||
if (result.status === "rejected" || !result.value) return null;
|
||||
if (type && result.value.item.type !== type) return null;
|
||||
return result.value;
|
||||
}
|
||||
@@ -20,3 +20,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
export * from "./database";
|
||||
export * from "./utils";
|
||||
export * from "./hooks";
|
||||
export * from "./components";
|
||||
|
||||
@@ -23,3 +23,4 @@ export * from "./time";
|
||||
export * from "./debounce";
|
||||
export * from "./random";
|
||||
export * from "./string";
|
||||
export * from "./resolve-items";
|
||||
|
||||
165
packages/common/src/utils/resolve-items.ts
Normal file
165
packages/common/src/utils/resolve-items.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
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 { Color, Item, Reminder, Notebook, Tag } from "@notesnook/core";
|
||||
import { database as db } from "../database";
|
||||
|
||||
type WithDateEdited<T> = { items: T[]; dateEdited: number };
|
||||
export type NotebooksWithDateEdited = WithDateEdited<Notebook>;
|
||||
export type TagsWithDateEdited = WithDateEdited<Tag>;
|
||||
|
||||
function withDateEdited<
|
||||
T extends { dateEdited: number } | { dateModified: number }
|
||||
>(items: T[]): WithDateEdited<T> {
|
||||
let latestDateEdited = 0;
|
||||
items.forEach((item) => {
|
||||
const date = "dateEdited" in item ? item.dateEdited : item.dateModified;
|
||||
if (latestDateEdited < date) latestDateEdited = date;
|
||||
});
|
||||
return { dateEdited: latestDateEdited, items };
|
||||
}
|
||||
|
||||
export async function resolveItems(ids: string[], items: Item[]) {
|
||||
if (!ids.length || !items.length) return [];
|
||||
|
||||
const { type } = items[0];
|
||||
if (type === "note") return resolveNotes(ids);
|
||||
else if (type === "notebook") {
|
||||
return Promise.all(ids.map((id) => db.notebooks.totalNotes(id)));
|
||||
} else if (type === "tag") {
|
||||
return Promise.all(
|
||||
ids.map((id) => db.relations.from({ id, type: "tag" }, "note").count())
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export type NoteResolvedData = {
|
||||
notebooks?: NotebooksWithDateEdited;
|
||||
reminder?: Reminder;
|
||||
color?: Color;
|
||||
tags?: TagsWithDateEdited;
|
||||
attachments?: {
|
||||
failed: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
|
||||
async function resolveNotes(ids: string[]) {
|
||||
const relations = [
|
||||
...(await db.relations
|
||||
.to({ type: "note", ids }, ["notebook", "tag", "color"])
|
||||
.get()),
|
||||
...(await db.relations.from({ type: "note", ids }, "reminder").get())
|
||||
];
|
||||
|
||||
const relationIds: {
|
||||
notebooks: Set<string>;
|
||||
colors: Set<string>;
|
||||
tags: Set<string>;
|
||||
reminders: Set<string>;
|
||||
} = {
|
||||
colors: new Set(),
|
||||
notebooks: new Set(),
|
||||
tags: new Set(),
|
||||
reminders: new Set()
|
||||
};
|
||||
|
||||
const grouped: Record<
|
||||
string,
|
||||
{
|
||||
notebooks: string[];
|
||||
color?: string;
|
||||
tags: string[];
|
||||
reminder?: string;
|
||||
}
|
||||
> = {};
|
||||
|
||||
for (const relation of relations) {
|
||||
const noteId =
|
||||
relation.toType === "reminder" ? relation.fromId : relation.toId;
|
||||
const data = grouped[noteId] || {
|
||||
notebooks: [],
|
||||
tags: []
|
||||
};
|
||||
|
||||
if (relation.toType === "reminder" && !data.reminder) {
|
||||
data.reminder = relation.toId;
|
||||
relationIds.reminders.add(relation.toId);
|
||||
} else if (relation.fromType === "notebook" && data.notebooks.length < 2) {
|
||||
data.notebooks.push(relation.fromId);
|
||||
relationIds.notebooks.add(relation.fromId);
|
||||
} else if (relation.fromType === "tag" && data.tags.length < 3) {
|
||||
data.tags.push(relation.fromId);
|
||||
relationIds.tags.add(relation.fromId);
|
||||
} else if (relation.fromType === "color" && !data.color) {
|
||||
data.color = relation.fromId;
|
||||
relationIds.colors.add(relation.fromId);
|
||||
}
|
||||
grouped[noteId] = data;
|
||||
}
|
||||
|
||||
const resolved = {
|
||||
notebooks: await db.notebooks.all.records(
|
||||
Array.from(relationIds.notebooks)
|
||||
),
|
||||
tags: await db.tags.all.records(Array.from(relationIds.tags)),
|
||||
colors: await db.colors.all.records(Array.from(relationIds.colors)),
|
||||
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
|
||||
};
|
||||
|
||||
const data: NoteResolvedData[] = [];
|
||||
for (const noteId of ids) {
|
||||
const group = grouped[noteId];
|
||||
if (!group) {
|
||||
data.push({});
|
||||
continue;
|
||||
}
|
||||
|
||||
const attachments = db.attachments?.ofNote(noteId, "all");
|
||||
data.push({
|
||||
color: group.color ? resolved.colors[group.color] : undefined,
|
||||
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
|
||||
tags: withDateEdited(
|
||||
group.tags.map((id) => resolved.tags[id]).filter(Boolean)
|
||||
),
|
||||
notebooks: withDateEdited(
|
||||
group.notebooks.map((id) => resolved.notebooks[id]).filter(Boolean)
|
||||
),
|
||||
attachments: {
|
||||
total: await attachments.count(),
|
||||
failed: await attachments
|
||||
.where((eb) => eb("attachments.failed", "is not", eb.lit(null)))
|
||||
.count()
|
||||
}
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function isNoteResolvedData(data: unknown): data is NoteResolvedData {
|
||||
return (
|
||||
typeof data === "object" &&
|
||||
!!data &&
|
||||
"notebooks" in data &&
|
||||
"reminder" in data &&
|
||||
"color" in data &&
|
||||
"tags" in data
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user