feat(collections/list): adjustable expansion depth (closes #7669)

This commit is contained in:
Elian Doran 2025-11-26 10:36:53 +02:00
parent f199d85d5b
commit b658f5bd0e
No known key found for this signature in database
5 changed files with 132 additions and 28 deletions

View File

@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "preact/hooks";
import FNote from "../../../entities/fnote";
import Icon from "../../react/Icon";
import { ViewModeProps } from "../interface";
import { useNoteLabelBoolean, useImperativeSearchHighlighlighting } from "../../react/hooks";
import { useImperativeSearchHighlighlighting, useNoteLabel } from "../../react/hooks";
import NoteLink from "../../react/NoteLink";
import "./ListOrGridView.css";
import content_renderer from "../../../services/content_renderer";
@ -14,7 +14,7 @@ import attribute_renderer from "../../../services/attribute_renderer";
import { filterChildNotes, useFilteredNoteIds } from "./utils";
export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const [ isExpanded ] = useNoteLabelBoolean(note, "expanded");
const expandDepth = useExpansionDepth(note);
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const { pageNotes, ...pagination } = usePagination(note, noteIds);
@ -25,7 +25,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
<div class="note-list-container use-tn-links">
{pageNotes?.map(childNote => (
<ListNoteCard note={childNote} parentNote={note} expand={isExpanded} highlightedTokens={highlightedTokens} />
<ListNoteCard note={childNote} parentNote={note} expandDepth={expandDepth} highlightedTokens={highlightedTokens} currentLevel={1} />
))}
</div>
@ -56,12 +56,19 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
);
}
function ListNoteCard({ note, parentNote, expand, highlightedTokens }: { note: FNote, parentNote: FNote, expand?: boolean, highlightedTokens: string[] | null | undefined }) {
const [ isExpanded, setExpanded ] = useState(expand);
function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expandDepth }: {
note: FNote,
parentNote: FNote,
currentLevel: number,
expandDepth: number,
highlightedTokens: string[] | null | undefined
}) {
const [ isExpanded, setExpanded ] = useState(currentLevel <= expandDepth);
const notePath = getNotePath(parentNote, note);
// Reset expand state if switching to another note, or if user manually toggled expansion state.
useEffect(() => setExpanded(expand), [ note, expand ]);
useEffect(() => setExpanded(currentLevel <= expandDepth), [ note, currentLevel, expandDepth ]);
return (
<div
@ -81,7 +88,7 @@ function ListNoteCard({ note, parentNote, expand, highlightedTokens }: { note: F
{isExpanded && <>
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList />
<NoteChildren note={note} parentNote={parentNote} highlightedTokens={highlightedTokens} />
<NoteChildren note={note} parentNote={parentNote} highlightedTokens={highlightedTokens} currentLevel={currentLevel} expandDepth={expandDepth} />
</>}
</div>
)
@ -160,14 +167,25 @@ function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note:
return <div ref={contentRef} className="note-book-content" />;
}
function NoteChildren({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
function NoteChildren({ note, parentNote, highlightedTokens, currentLevel, expandDepth }: {
note: FNote,
parentNote: FNote,
currentLevel: number,
expandDepth: number,
highlightedTokens: string[] | null | undefined
}) {
const [ childNotes, setChildNotes ] = useState<FNote[]>();
useEffect(() => {
filterChildNotes(note).then(setChildNotes);
}, [ note ]);
return childNotes?.map(childNote => <ListNoteCard note={childNote} parentNote={parentNote} highlightedTokens={highlightedTokens} />)
return childNotes?.map(childNote => <ListNoteCard
note={childNote}
parentNote={parentNote}
highlightedTokens={highlightedTokens}
currentLevel={currentLevel + 1} expandDepth={expandDepth}
/>)
}
function getNotePath(parentNote: FNote, childNote: FNote) {
@ -178,3 +196,17 @@ function getNotePath(parentNote: FNote, childNote: FNote) {
return `${parentNote.noteId}/${childNote.noteId}`
}
}
function useExpansionDepth(note: FNote) {
const [ expandDepth ] = useNoteLabel(note, "expanded");
if (expandDepth === null || expandDepth === undefined) { // not defined
return 0;
} else if (expandDepth === "") { // defined without value
return 1;
} else if (expandDepth === "all") {
return Number.MAX_SAFE_INTEGER;
} else {
return parseInt(expandDepth, 10);
}
}

View File

@ -4,14 +4,15 @@ import FormSelect, { FormSelectWithGroups } from "../react/FormSelect";
import { TabContext } from "./ribbon-interface";
import { mapToKeyValueArray } from "../../services/utils";
import { useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxProperty, NumberProperty } from "./collection-properties-config";
import Button from "../react/Button";
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "./collection-properties-config";
import Button, { SplitButton } from "../react/Button";
import { ParentComponent } from "../react/react_utils";
import FNote from "../../entities/fnote";
import FormCheckbox from "../react/FormCheckbox";
import FormTextBox from "../react/FormTextBox";
import { ComponentChildren } from "preact";
import { ViewTypeOptions } from "../collections/interface";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
grid: t("book_properties.grid"),
@ -80,6 +81,8 @@ function mapPropertyView({ note, property }: { note: FNote, property: BookProper
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />
case "checkbox":
return <CheckboxPropertyView note={note} property={property} />
case "number":
@ -97,15 +100,39 @@ function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonP
title={property.title}
icon={property.icon}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
/>
}
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
const parentComponent = useContext(ParentComponent);
const clickContext = parentComponent && {
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
};
return <SplitButton
text={property.label}
icon={property.icon}
onClick={() => clickContext && property.onClick(clickContext)}
>
{clickContext && property.items.map(subproperty => {
if ("type" in subproperty && subproperty) {
return <FormDropdownDivider />
}
return (
<FormListItem onClick={() => clickContext && subproperty.onClick(clickContext)}>{subproperty.label}</FormListItem>
);
})}
</SplitButton>
}
function CheckboxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);

View File

@ -22,7 +22,17 @@ export interface ButtonProperty {
label: string;
title?: string;
icon?: string;
onClick: (context: BookContext) => void;
onClick(context: BookContext): void;
}
export interface SplitButtonProperty extends Omit<ButtonProperty, "type"> {
type: "split-button";
items: ({
label: string;
onClick(context: BookContext): void;
} | {
type: "separator"
})[];
}
export interface NumberProperty {
@ -55,7 +65,7 @@ export interface ComboBoxProperty {
options: (ComboBoxItem | ComboBoxGroup)[];
}
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty;
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty;
interface BookContext {
note: FNote;
@ -87,16 +97,37 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
{
label: t("book_properties.expand"),
title: t("book_properties.expand_all_children"),
type: "button",
type: "split-button",
icon: "bx bx-move-vertical",
async onClick({ note, triggerCommand }) {
const { noteId } = note;
if (!note.isLabelTruthy("expanded")) {
await attributes.addLabel(noteId, "expanded");
onClick: buildExpandListHandler(1),
items: [
{
label: "Expand 1 level",
onClick: buildExpandListHandler(1)
},
{ type: "separator" },
{
label: "Expand 2 levels",
onClick: buildExpandListHandler(2),
},
{
label: "Expand 3 levels",
onClick: buildExpandListHandler(3),
},
{
label: "Expand 4 levels",
onClick: buildExpandListHandler(4),
},
{
label: "Expand 5 levels",
onClick: buildExpandListHandler(5),
},
{ type: "separator" },
{
label: "Expand all children",
onClick: buildExpandListHandler("all"),
}
triggerCommand("refreshNoteList", { noteId });
},
]
}
]
},
@ -185,3 +216,17 @@ function buildMapLayer([ id, layer ]: [ string, MapLayer ]): ComboBoxItem {
label: layer.name
};
}
function buildExpandListHandler(depth: number | "all") {
return async ({ note, triggerCommand }: BookContext) => {
const { noteId } = note;
const existingValue = note.getLabelValue("expanded");
let newValue: string | undefined = typeof depth === "number" ? depth.toString() : depth;
if (depth === 1) newValue = undefined; // maintain existing behaviour
if (newValue === existingValue) return;
await attributes.setLabel(noteId, "expanded", newValue);
triggerCommand("refreshNoteList", { noteId });
}
}

View File

@ -356,7 +356,7 @@ body[dir=rtl] .attribute-list-editor {
display: flex;
flex-wrap: wrap;
gap: 15px;
overflow: hidden;
overflow: visible;
align-items: center;
}

View File

@ -29,7 +29,7 @@ type Labels = {
status: string;
pageSize: number;
geolocation: string;
expanded: boolean;
expanded: string;
"calendar:hideWeekends": boolean;
"calendar:weekNumbers": boolean;
"calendar:view": string;