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 FNote from "../../../entities/fnote";
|
||||||
import Icon from "../../react/Icon";
|
import Icon from "../../react/Icon";
|
||||||
import { ViewModeProps } from "../interface";
|
import { ViewModeProps } from "../interface";
|
||||||
import { useNoteLabelBoolean, useImperativeSearchHighlighlighting } from "../../react/hooks";
|
import { useImperativeSearchHighlighlighting, useNoteLabel } from "../../react/hooks";
|
||||||
import NoteLink from "../../react/NoteLink";
|
import NoteLink from "../../react/NoteLink";
|
||||||
import "./ListOrGridView.css";
|
import "./ListOrGridView.css";
|
||||||
import content_renderer from "../../../services/content_renderer";
|
import content_renderer from "../../../services/content_renderer";
|
||||||
@ -14,7 +14,7 @@ import attribute_renderer from "../../../services/attribute_renderer";
|
|||||||
import { filterChildNotes, useFilteredNoteIds } from "./utils";
|
import { filterChildNotes, useFilteredNoteIds } from "./utils";
|
||||||
|
|
||||||
export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
|
export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
|
||||||
const [ isExpanded ] = useNoteLabelBoolean(note, "expanded");
|
const expandDepth = useExpansionDepth(note);
|
||||||
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
|
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
|
||||||
const { pageNotes, ...pagination } = usePagination(note, noteIds);
|
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">
|
<div class="note-list-container use-tn-links">
|
||||||
{pageNotes?.map(childNote => (
|
{pageNotes?.map(childNote => (
|
||||||
<ListNoteCard note={childNote} parentNote={note} expand={isExpanded} highlightedTokens={highlightedTokens} />
|
<ListNoteCard note={childNote} parentNote={note} expandDepth={expandDepth} highlightedTokens={highlightedTokens} currentLevel={1} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</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 }) {
|
function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expandDepth }: {
|
||||||
const [ isExpanded, setExpanded ] = useState(expand);
|
note: FNote,
|
||||||
|
parentNote: FNote,
|
||||||
|
currentLevel: number,
|
||||||
|
expandDepth: number,
|
||||||
|
highlightedTokens: string[] | null | undefined
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const [ isExpanded, setExpanded ] = useState(currentLevel <= expandDepth);
|
||||||
const notePath = getNotePath(parentNote, note);
|
const notePath = getNotePath(parentNote, note);
|
||||||
|
|
||||||
// Reset expand state if switching to another note, or if user manually toggled expansion state.
|
// 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -81,7 +88,7 @@ function ListNoteCard({ note, parentNote, expand, highlightedTokens }: { note: F
|
|||||||
|
|
||||||
{isExpanded && <>
|
{isExpanded && <>
|
||||||
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList />
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
@ -160,14 +167,25 @@ function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note:
|
|||||||
return <div ref={contentRef} className="note-book-content" />;
|
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[]>();
|
const [ childNotes, setChildNotes ] = useState<FNote[]>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
filterChildNotes(note).then(setChildNotes);
|
filterChildNotes(note).then(setChildNotes);
|
||||||
}, [ note ]);
|
}, [ 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) {
|
function getNotePath(parentNote: FNote, childNote: FNote) {
|
||||||
@ -178,3 +196,17 @@ function getNotePath(parentNote: FNote, childNote: FNote) {
|
|||||||
return `${parentNote.noteId}/${childNote.noteId}`
|
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 { TabContext } from "./ribbon-interface";
|
||||||
import { mapToKeyValueArray } from "../../services/utils";
|
import { mapToKeyValueArray } from "../../services/utils";
|
||||||
import { useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
|
import { useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
|
||||||
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxProperty, NumberProperty } from "./collection-properties-config";
|
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "./collection-properties-config";
|
||||||
import Button from "../react/Button";
|
import Button, { SplitButton } from "../react/Button";
|
||||||
import { ParentComponent } from "../react/react_utils";
|
import { ParentComponent } from "../react/react_utils";
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
import FormCheckbox from "../react/FormCheckbox";
|
import FormCheckbox from "../react/FormCheckbox";
|
||||||
import FormTextBox from "../react/FormTextBox";
|
import FormTextBox from "../react/FormTextBox";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import { ViewTypeOptions } from "../collections/interface";
|
import { ViewTypeOptions } from "../collections/interface";
|
||||||
|
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||||
|
|
||||||
const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
|
const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||||
grid: t("book_properties.grid"),
|
grid: t("book_properties.grid"),
|
||||||
@ -80,6 +81,8 @@ function mapPropertyView({ note, property }: { note: FNote, property: BookProper
|
|||||||
switch (property.type) {
|
switch (property.type) {
|
||||||
case "button":
|
case "button":
|
||||||
return <ButtonPropertyView note={note} property={property} />
|
return <ButtonPropertyView note={note} property={property} />
|
||||||
|
case "split-button":
|
||||||
|
return <SplitButtonPropertyView note={note} property={property} />
|
||||||
case "checkbox":
|
case "checkbox":
|
||||||
return <CheckboxPropertyView note={note} property={property} />
|
return <CheckboxPropertyView note={note} property={property} />
|
||||||
case "number":
|
case "number":
|
||||||
@ -106,6 +109,30 @@ function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonP
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 }) {
|
function CheckboxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
|
||||||
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
|
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,17 @@ export interface ButtonProperty {
|
|||||||
label: string;
|
label: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
icon?: 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 {
|
export interface NumberProperty {
|
||||||
@ -55,7 +65,7 @@ export interface ComboBoxProperty {
|
|||||||
options: (ComboBoxItem | ComboBoxGroup)[];
|
options: (ComboBoxItem | ComboBoxGroup)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty;
|
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty;
|
||||||
|
|
||||||
interface BookContext {
|
interface BookContext {
|
||||||
note: FNote;
|
note: FNote;
|
||||||
@ -87,16 +97,37 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
|||||||
{
|
{
|
||||||
label: t("book_properties.expand"),
|
label: t("book_properties.expand"),
|
||||||
title: t("book_properties.expand_all_children"),
|
title: t("book_properties.expand_all_children"),
|
||||||
type: "button",
|
type: "split-button",
|
||||||
icon: "bx bx-move-vertical",
|
icon: "bx bx-move-vertical",
|
||||||
async onClick({ note, triggerCommand }) {
|
onClick: buildExpandListHandler(1),
|
||||||
const { noteId } = note;
|
items: [
|
||||||
if (!note.isLabelTruthy("expanded")) {
|
{
|
||||||
await attributes.addLabel(noteId, "expanded");
|
label: "Expand 1 level",
|
||||||
}
|
onClick: buildExpandListHandler(1)
|
||||||
|
|
||||||
triggerCommand("refreshNoteList", { noteId });
|
|
||||||
},
|
},
|
||||||
|
{ 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"),
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -185,3 +216,17 @@ function buildMapLayer([ id, layer ]: [ string, MapLayer ]): ComboBoxItem {
|
|||||||
label: layer.name
|
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;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ type Labels = {
|
|||||||
status: string;
|
status: string;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
geolocation: string;
|
geolocation: string;
|
||||||
expanded: boolean;
|
expanded: string;
|
||||||
"calendar:hideWeekends": boolean;
|
"calendar:hideWeekends": boolean;
|
||||||
"calendar:weekNumbers": boolean;
|
"calendar:weekNumbers": boolean;
|
||||||
"calendar:view": string;
|
"calendar:view": string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user