mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
web: changes to use new grouping api
This commit is contained in:
@@ -28,7 +28,8 @@ import {
|
||||
SortDesc,
|
||||
DetailedView,
|
||||
CompactView,
|
||||
Icon
|
||||
Icon,
|
||||
Loading
|
||||
} from "../icons";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
@@ -203,8 +204,8 @@ type GroupHeaderProps = {
|
||||
groupingKey: GroupingKey;
|
||||
index: number;
|
||||
|
||||
groups: GroupHeaderType[];
|
||||
onJump: (title: string) => void;
|
||||
groups: () => Promise<{ index: number; group: GroupHeaderType }[]>;
|
||||
onJump: (index: number) => void;
|
||||
refresh: () => void;
|
||||
onSelectGroup: () => void;
|
||||
isFocused: boolean;
|
||||
@@ -261,18 +262,29 @@ function GroupHeader(props: GroupHeaderProps) {
|
||||
onSelectGroup();
|
||||
return;
|
||||
}
|
||||
if (groups.length <= 0) return;
|
||||
e.stopPropagation();
|
||||
const items: MenuItem[] = groups.map((group) => {
|
||||
const groupTitle = group.title.toString();
|
||||
return {
|
||||
type: "button",
|
||||
key: groupTitle,
|
||||
title: groupTitle,
|
||||
onClick: () => onJump(groupTitle),
|
||||
checked: group.title === title
|
||||
};
|
||||
});
|
||||
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
key: "groups",
|
||||
type: "lazy-loader",
|
||||
loader: <Loading sx={{ my: 2 }} />,
|
||||
async items() {
|
||||
const items = await groups();
|
||||
return items.map(({ group, index }) => {
|
||||
const groupTitle = group.title.toString();
|
||||
return {
|
||||
type: "button",
|
||||
key: groupTitle,
|
||||
title: groupTitle,
|
||||
onClick: () => onJump(index),
|
||||
checked: group.title === title
|
||||
} as MenuItem;
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
openMenu(items, {
|
||||
title: "Jump to group",
|
||||
position: {
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
store as selectionStore
|
||||
} from "../../stores/selection-store";
|
||||
import GroupHeader from "../group-header";
|
||||
import { DEFAULT_ITEM_HEIGHT, ListItemWrapper } from "./list-profiles";
|
||||
import { ListItemWrapper } from "./list-profiles";
|
||||
import Announcements from "../announcements";
|
||||
import { ListLoader } from "../loaders/list-loader";
|
||||
import { FlexScrollContainer } from "../scroll-container";
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from "@notesnook/core";
|
||||
import { VirtualizedList } from "../virtualized-list";
|
||||
import { Virtualizer } from "@tanstack/react-virtual";
|
||||
import { useResolvedItem } from "./resolved-item";
|
||||
|
||||
type ListContainerProps = {
|
||||
group?: GroupingKey;
|
||||
@@ -125,72 +126,74 @@ function ListContainer(props: ListContainerProps) {
|
||||
{header ? header : <Announcements />}
|
||||
<VirtualizedList
|
||||
virtualizerRef={listRef}
|
||||
estimatedSize={DEFAULT_ITEM_HEIGHT}
|
||||
estimatedSize={50}
|
||||
getItemKey={(index) => items.getKey(index)}
|
||||
items={items.ids}
|
||||
mode="dynamic"
|
||||
tabIndex={-1}
|
||||
overscan={10}
|
||||
onBlur={() => setFocusedGroupIndex(-1)}
|
||||
onKeyDown={(e) => onKeyDown(e.nativeEvent)}
|
||||
itemWrapperProps={(_, index) => ({
|
||||
onFocus: () => onFocus(index),
|
||||
onMouseDown: (e) => onMouseDown(e.nativeEvent, index)
|
||||
})}
|
||||
renderItem={({ index, item }) => {
|
||||
if (isGroupHeader(item)) {
|
||||
if (!group) return null;
|
||||
return (
|
||||
<GroupHeader
|
||||
groupingKey={group}
|
||||
refresh={refresh}
|
||||
title={item.title}
|
||||
isFocused={index === focusedGroupIndex}
|
||||
index={index}
|
||||
onSelectGroup={() => {
|
||||
let endIndex;
|
||||
for (
|
||||
let i = index + 1;
|
||||
i < props.items.ids.length;
|
||||
++i
|
||||
) {
|
||||
if (typeof props.items.ids[i] === "object") {
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setSelectedItems([
|
||||
...selectionStore.get().selectedItems,
|
||||
...props.items.ids.slice(
|
||||
index,
|
||||
endIndex || props.items.ids.length
|
||||
)
|
||||
]);
|
||||
}}
|
||||
groups={props.items.groups}
|
||||
onJump={(title: string) => {
|
||||
const index = props.items.ids.findIndex(
|
||||
(v) => isGroupHeader(v) && v.title === title
|
||||
);
|
||||
if (index < 0) return;
|
||||
listRef.current?.scrollToIndex(index, {
|
||||
align: "center",
|
||||
behavior: "auto"
|
||||
});
|
||||
setFocusedGroupIndex(index);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
renderItem={function ItemRenderer({ index }) {
|
||||
const resolvedItem = useResolvedItem({ index, items });
|
||||
// console.log("Rendering:", index);
|
||||
if (!resolvedItem)
|
||||
return <div style={{ height: 50, width: "100%" }}></div>;
|
||||
|
||||
return (
|
||||
<ListItemWrapper
|
||||
key={item}
|
||||
items={items}
|
||||
id={item}
|
||||
context={context}
|
||||
group={group}
|
||||
compact={compact}
|
||||
/>
|
||||
<>
|
||||
{resolvedItem.group && group ? (
|
||||
<GroupHeader
|
||||
groupingKey={group}
|
||||
refresh={refresh}
|
||||
title={resolvedItem.group.title}
|
||||
isFocused={index === focusedGroupIndex}
|
||||
index={index}
|
||||
onSelectGroup={() => {
|
||||
let endIndex;
|
||||
for (
|
||||
let i = index + 1;
|
||||
i < props.items.ids.length;
|
||||
++i
|
||||
) {
|
||||
if (typeof props.items.ids[i] === "object") {
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setSelectedItems([
|
||||
...selectionStore.get().selectedItems,
|
||||
...props.items.ids.slice(
|
||||
index,
|
||||
endIndex || props.items.ids.length
|
||||
)
|
||||
]);
|
||||
}}
|
||||
groups={async () =>
|
||||
items.groups ? items.groups() : []
|
||||
}
|
||||
onJump={(index) => {
|
||||
listRef.current?.scrollToIndex(index, {
|
||||
align: "center",
|
||||
behavior: "auto"
|
||||
});
|
||||
setFocusedGroupIndex(index);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<ListItemWrapper
|
||||
key={resolvedItem.item.id}
|
||||
item={resolvedItem.item}
|
||||
data={resolvedItem.data}
|
||||
context={context}
|
||||
group={group}
|
||||
compact={compact}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -25,9 +25,9 @@ import { db } from "../../common/db";
|
||||
import Reminder from "../reminder";
|
||||
import { Context } from "./types";
|
||||
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
|
||||
import { GroupingKey, Item, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { GroupingKey, Item } from "@notesnook/core";
|
||||
import { Attachment } from "../attachment";
|
||||
import { isNoteResolvedData, useResolvedItem } from "./resolved-item";
|
||||
import { isNoteResolvedData } from "./resolved-item";
|
||||
|
||||
const SINGLE_LINE_HEIGHT = 1.4;
|
||||
const DEFAULT_LINE_HEIGHT =
|
||||
@@ -36,21 +36,16 @@ export const DEFAULT_ITEM_HEIGHT = SINGLE_LINE_HEIGHT * 4 * DEFAULT_LINE_HEIGHT;
|
||||
|
||||
type ListItemWrapperProps = {
|
||||
group?: GroupingKey;
|
||||
items: VirtualizedGrouping<Item>;
|
||||
id: string;
|
||||
item: Item;
|
||||
data?: unknown;
|
||||
context?: Context;
|
||||
compact?: boolean;
|
||||
simplified?: boolean;
|
||||
};
|
||||
|
||||
export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
const { group, compact, context, simplified } = props;
|
||||
const { group, compact, context, simplified, item, data } = props;
|
||||
|
||||
const resolvedItem = useResolvedItem(props);
|
||||
if (!resolvedItem)
|
||||
return <div style={{ height: DEFAULT_ITEM_HEIGHT, width: "100%" }} />;
|
||||
|
||||
const { data, item } = resolvedItem;
|
||||
switch (item.type) {
|
||||
case "note": {
|
||||
return (
|
||||
|
||||
@@ -37,7 +37,7 @@ import React from "react";
|
||||
type ResolvedItemProps<TItemType extends ItemType> = {
|
||||
type: TItemType;
|
||||
items: VirtualizedGrouping<ItemMap[TItemType]>;
|
||||
id: string;
|
||||
index: number;
|
||||
children: (item: {
|
||||
item: ItemMap[TItemType];
|
||||
data: unknown;
|
||||
@@ -46,8 +46,11 @@ type ResolvedItemProps<TItemType extends ItemType> = {
|
||||
export function ResolvedItem<TItemType extends ItemType>(
|
||||
props: ResolvedItemProps<TItemType>
|
||||
) {
|
||||
const { id, items, children, type } = props;
|
||||
const result = usePromise(() => items.item(id, resolveItems), [id, items]);
|
||||
const { index, items, children, type } = props;
|
||||
const result = usePromise(
|
||||
() => items.item(index, resolveItems),
|
||||
[index, items]
|
||||
);
|
||||
|
||||
if (result.status !== "fulfilled" || !result.value) return null;
|
||||
|
||||
@@ -55,9 +58,14 @@ export function ResolvedItem<TItemType extends ItemType>(
|
||||
return <>{children(result.value)}</>;
|
||||
}
|
||||
|
||||
export function useResolvedItem(props: Omit<ResolvedItemProps, "children">) {
|
||||
const { id, items } = props;
|
||||
const result = usePromise(() => items.item(id, resolveItems), [id, items]);
|
||||
export function useResolvedItem(
|
||||
props: Omit<ResolvedItemProps<ItemType>, "children" | "type">
|
||||
) {
|
||||
const { index, items } = props;
|
||||
const result = usePromise(
|
||||
() => items.item(index, resolveItems),
|
||||
[index, items]
|
||||
);
|
||||
|
||||
if (result.status !== "fulfilled" || !result.value) return null;
|
||||
return result.value;
|
||||
@@ -74,20 +82,17 @@ function withDateEdited<
|
||||
return { dateEdited: latestDateEdited, items };
|
||||
}
|
||||
|
||||
export async function resolveItems(ids: string[], items: Record<string, Item>) {
|
||||
const { type } = items[ids[0]];
|
||||
export async function resolveItems(ids: string[], items: Item[]) {
|
||||
const { type } = items[0];
|
||||
if (type === "note") return resolveNotes(ids);
|
||||
else if (type === "notebook") {
|
||||
const data: Record<string, number> = {};
|
||||
for (const id of ids) data[id] = await db.notebooks.totalNotes(id);
|
||||
return data;
|
||||
return Promise.all(ids.map((id) => db.notebooks.totalNotes(id)));
|
||||
} else if (type === "tag") {
|
||||
const data: Record<string, number> = {};
|
||||
for (const id of ids)
|
||||
data[id] = await db.relations.from({ id, type: "tag" }, "note").count();
|
||||
return data;
|
||||
return Promise.all(
|
||||
ids.map((id) => db.relations.from({ id, type: "tag" }, "note").count())
|
||||
);
|
||||
}
|
||||
return {};
|
||||
return [];
|
||||
}
|
||||
|
||||
type NoteResolvedData = {
|
||||
@@ -161,17 +166,17 @@ async function resolveNotes(ids: string[]) {
|
||||
};
|
||||
console.timeEnd("resolve");
|
||||
|
||||
const data: Record<string, NoteResolvedData> = {};
|
||||
const data: NoteResolvedData[] = [];
|
||||
for (const noteId in grouped) {
|
||||
const group = grouped[noteId];
|
||||
data[noteId] = {
|
||||
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])),
|
||||
notebooks: withDateEdited(
|
||||
group.notebooks.map((id) => resolved.notebooks[id])
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export type VirtualizedListProps<T> = {
|
||||
renderItem: (props: { item: T; index: number }) => JSX.Element | null;
|
||||
scrollMargin?: number;
|
||||
itemGap?: number;
|
||||
overscan?: number;
|
||||
} & BoxProps;
|
||||
export function VirtualizedList<T>(props: VirtualizedListProps<T>) {
|
||||
const {
|
||||
@@ -47,6 +48,7 @@ export function VirtualizedList<T>(props: VirtualizedListProps<T>) {
|
||||
virtualizerRef,
|
||||
itemWrapperProps,
|
||||
itemGap,
|
||||
overscan = 5,
|
||||
...containerProps
|
||||
} = props;
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -58,7 +60,7 @@ export function VirtualizedList<T>(props: VirtualizedListProps<T>) {
|
||||
getScrollElement: () =>
|
||||
scrollElement || containerRef.current?.closest(".ms-container") || null,
|
||||
scrollMargin: scrollMargin || containerRef.current?.offsetTop || 0,
|
||||
overscan: 5
|
||||
overscan
|
||||
});
|
||||
|
||||
if (virtualizerRef) virtualizerRef.current = virtualizer;
|
||||
|
||||
@@ -42,6 +42,23 @@ function Home() {
|
||||
useStore.getState().refresh();
|
||||
}, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// (async function () {
|
||||
|
||||
// // const titles =
|
||||
// // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");
|
||||
// // for (let i = 0; i < 10000; ++i) {
|
||||
// // await db.notes.add({
|
||||
// // title: `${
|
||||
// // titles[getRandom(0, titles.length)]
|
||||
// // } Some other title of mine`
|
||||
// // });
|
||||
// // if (i % 100 === 0) console.log(i);
|
||||
// // }
|
||||
// // console.log("DONE");
|
||||
// })();
|
||||
// }, []);
|
||||
|
||||
if (!notes) return <Placeholder context="notes" />;
|
||||
return (
|
||||
<ListContainer
|
||||
|
||||
@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { EVENTS } from "../common";
|
||||
import {
|
||||
GroupHeader,
|
||||
GroupOptions,
|
||||
Item,
|
||||
MaybeDeletedItem,
|
||||
|
||||
@@ -207,7 +207,9 @@ function MenuContainer(props: PropsWithChildren<MenuContainerProps>) {
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
<ScrollContainer suppressScrollX>{children}</ScrollContainer>
|
||||
<ScrollContainer suppressScrollX style={{ maxHeight: 400 }}>
|
||||
{children}
|
||||
</ScrollContainer>
|
||||
{/* <FlexScrollContainer>{children}</FlexScrollContainer> */}
|
||||
</Box>
|
||||
);
|
||||
@@ -264,7 +266,15 @@ function LazyLoader(props: {
|
||||
})();
|
||||
}, [item]);
|
||||
|
||||
return isLoading ? <></> : <>{items.map(mapper)}</>;
|
||||
return isLoading ? (
|
||||
item.loader ? (
|
||||
<>{item.loader}</>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
) : (
|
||||
<>{items.map(mapper)}</>
|
||||
);
|
||||
}
|
||||
|
||||
export * from "./types";
|
||||
|
||||
@@ -38,6 +38,7 @@ export type MenuPopupItem = BaseMenuItem<"popup"> & {
|
||||
};
|
||||
|
||||
export type LazyMenuItemsLoader = BaseMenuItem<"lazy-loader"> & {
|
||||
loader?: React.ReactNode;
|
||||
items: () => Promise<MenuItem[]>;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user