mirror of
https://github.com/zadam/trilium.git
synced 2025-11-28 19:44:24 +01:00
feat(collections/list): adjustable expansion depth (closes #7669)
This commit is contained in:
parent
f199d85d5b
commit
b658f5bd0e
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,7 +356,7 @@ body[dir=rtl] .attribute-list-editor {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user