diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index 749036598..850aa3fba 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -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 }
{pageNotes?.map(childNote => (
-
+
))}
@@ -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 (
-
+
>}
)
@@ -160,14 +167,25 @@ function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note:
return ;
}
-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();
useEffect(() => {
filterChildNotes(note).then(setChildNotes);
}, [ note ]);
- return childNotes?.map(childNote => )
+ return childNotes?.map(childNote => )
}
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);
+ }
+}
diff --git a/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx b/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx
index e961ae1f0..7a5f9b6b1 100644
--- a/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx
+++ b/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx
@@ -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 = {
grid: t("book_properties.grid"),
@@ -80,6 +81,8 @@ function mapPropertyView({ note, property }: { note: FNote, property: BookProper
switch (property.type) {
case "button":
return
+ case "split-button":
+ return
case "checkbox":
return
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 clickContext && property.onClick(clickContext)}
+ >
+ {clickContext && property.items.map(subproperty => {
+ if ("type" in subproperty && subproperty) {
+ return
+ }
+
+ return (
+ clickContext && subproperty.onClick(clickContext)}>{subproperty.label}
+ );
+ })}
+
+}
+
function CheckboxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
diff --git a/apps/client/src/widgets/ribbon/collection-properties-config.ts b/apps/client/src/widgets/ribbon/collection-properties-config.ts
index 76a2193c5..5161e1496 100644
--- a/apps/client/src/widgets/ribbon/collection-properties-config.ts
+++ b/apps/client/src/widgets/ribbon/collection-properties-config.ts
@@ -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 {
+ 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 = {
{
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 });
+ }
+}
diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css
index f47c6d662..290d1b30e 100644
--- a/apps/client/src/widgets/ribbon/style.css
+++ b/apps/client/src/widgets/ribbon/style.css
@@ -356,7 +356,7 @@ body[dir=rtl] .attribute-list-editor {
display: flex;
flex-wrap: wrap;
gap: 15px;
- overflow: hidden;
+ overflow: visible;
align-items: center;
}
diff --git a/packages/commons/src/lib/attribute_names.ts b/packages/commons/src/lib/attribute_names.ts
index e186cf88a..767f05872 100644
--- a/packages/commons/src/lib/attribute_names.ts
+++ b/packages/commons/src/lib/attribute_names.ts
@@ -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;