refactor(client): move note properties mapping outside collection properties

This commit is contained in:
Elian Doran 2026-02-15 13:13:18 +02:00
parent 8af7b3c81a
commit 29855112c8
No known key found for this signature in database
3 changed files with 206 additions and 198 deletions

View File

@ -2,18 +2,16 @@ import "./CollectionProperties.css";
import { t } from "i18next";
import { ComponentChildren } from "preact";
import { useContext, useRef } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import { useRef } from "preact/hooks";
import FNote from "../../entities/fnote";
import { ViewTypeOptions } from "../collections/interface";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
import FormTextBox from "../react/FormTextBox";
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault, useNoteProperty, useTriliumEvent } from "../react/hooks";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useNoteProperty, useTriliumEvent } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
import { CheckBoxProperty, ViewProperty } from "../react/NotePropertyMenu";
import { bookPropertiesConfig } from "../ribbon/collection-properties-config";
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
export const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
@ -107,127 +105,3 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
</Dropdown>
);
}
function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
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":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
return (
<FormListItem
icon={property.icon}
title={property.title}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
>{property.label}</FormListItem>
);
}
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
const parentComponent = useContext(ParentComponent);
const ItemsComponent = property.items;
const clickContext = parentComponent && {
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
};
return (parentComponent &&
<FormDropdownSubmenu
icon={property.icon ?? "bx bx-empty"}
title={property.label}
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
>
<ItemsComponent note={note} parentComponent={parentComponent} />
</FormDropdownSubmenu>
);
}
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
//@ts-expect-error Interop with text box which takes in string values even for numbers.
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
const disabled = property.disabled?.(note);
return (
<FormListItem
icon={property.icon}
disabled={disabled}
onClick={(e) => e.stopPropagation()}
>
{property.label}
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) }}
min={property.min ?? 0}
disabled={disabled}
/>
</FormListItem>
);
}
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
const [ value, setValue ] = useNoteLabelWithDefault(note, property.bindToLabel, property.defaultValue ?? "");
function renderItem(option: ComboBoxItem) {
return (
<FormListItem
key={option.value}
checked={value === option.value}
onClick={() => setValue(option.value)}
>
{option.label}
</FormListItem>
);
}
return (
<FormDropdownSubmenu
title={property.label}
icon={property.icon ?? "bx bx-empty"}
>
{(property.options).map((option, index) => {
if ("items" in option) {
return (
<Fragment key={option.title}>
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
{option.items.map(renderItem)}
{index < property.options.length - 1 && <FormDropdownDivider />}
</Fragment>
);
}
return renderItem(option);
})}
</FormDropdownSubmenu>
);
}
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormListToggleableItem
icon={property.icon}
title={property.label}
currentValue={value}
onChange={setValue}
/>
);
}

View File

@ -0,0 +1,194 @@
import { FilterLabelsByType } from "@triliumnext/commons";
import { Fragment, VNode } from "preact";
import { useContext } from "preact/hooks";
import Component from "../../components/component";
import FNote from "../../entities/fnote";
import NoteContextAwareWidget from "../note_context_aware_widget";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "./FormList";
import FormTextBox from "./FormTextBox";
import { useNoteLabelBoolean, useNoteLabelWithDefault } from "./hooks";
import { ParentComponent } from "./react_utils";
export interface ClickContext {
note: FNote;
triggerCommand: NoteContextAwareWidget["triggerCommand"];
}
export interface CheckBoxProperty {
type: "checkbox",
label: string;
bindToLabel: FilterLabelsByType<boolean>;
icon?: string;
}
export interface ButtonProperty {
type: "button",
label: string;
title?: string;
icon?: string;
onClick(context: ClickContext): void;
}
export interface SplitButtonProperty extends Omit<ButtonProperty, "type"> {
type: "split-button";
items({ note, parentComponent }: { note: FNote, parentComponent: Component }): VNode;
}
export interface NumberProperty {
type: "number",
label: string;
bindToLabel: FilterLabelsByType<number>;
width?: number;
min?: number;
icon?: string;
disabled?: (note: FNote) => boolean;
}
export interface ComboBoxItem {
value: string;
label: string;
}
interface ComboBoxGroup {
title: string;
items: ComboBoxItem[];
}
export interface ComboBoxProperty {
type: "combobox",
label: string;
icon?: string;
bindToLabel: FilterLabelsByType<string>;
/**
* The default value is used when the label is not set.
*/
defaultValue?: string;
options: (ComboBoxItem | ComboBoxGroup)[];
}
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty;
export function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
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":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
return (
<FormListItem
icon={property.icon}
title={property.title}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
>{property.label}</FormListItem>
);
}
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
const parentComponent = useContext(ParentComponent);
const ItemsComponent = property.items;
const clickContext = parentComponent && {
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
};
return (parentComponent &&
<FormDropdownSubmenu
icon={property.icon ?? "bx bx-empty"}
title={property.label}
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
>
<ItemsComponent note={note} parentComponent={parentComponent} />
</FormDropdownSubmenu>
);
}
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
//@ts-expect-error Interop with text box which takes in string values even for numbers.
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
const disabled = property.disabled?.(note);
return (
<FormListItem
icon={property.icon}
disabled={disabled}
onClick={(e) => e.stopPropagation()}
>
{property.label}
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) }}
min={property.min ?? 0}
disabled={disabled}
/>
</FormListItem>
);
}
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
const [ value, setValue ] = useNoteLabelWithDefault(note, property.bindToLabel, property.defaultValue ?? "");
function renderItem(option: ComboBoxItem) {
return (
<FormListItem
key={option.value}
checked={value === option.value}
onClick={() => setValue(option.value)}
>
{option.label}
</FormListItem>
);
}
return (
<FormDropdownSubmenu
title={property.label}
icon={property.icon ?? "bx bx-empty"}
>
{(property.options).map((option, index) => {
if ("items" in option) {
return (
<Fragment key={option.title}>
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
{option.items.map(renderItem)}
{index < property.options.length - 1 && <FormDropdownDivider />}
</Fragment>
);
}
return renderItem(option);
})}
</FormDropdownSubmenu>
);
}
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormListToggleableItem
icon={property.icon}
title={property.label}
currentValue={value}
onChange={setValue}
/>
);
}

View File

@ -1,79 +1,19 @@
import { t } from "i18next";
import Component from "../../components/component";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import NoteContextAwareWidget from "../note_context_aware_widget";
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, type MapLayer } from "../collections/geomap/map_layer";
import { ViewTypeOptions } from "../collections/interface";
import { FilterLabelsByType } from "@triliumnext/commons";
import { DEFAULT_THEME, getPresentationThemes } from "../collections/presentation/themes";
import { VNode } from "preact";
import { useNoteLabel } from "../react/hooks";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import Component from "../../components/component";
import { useNoteLabel } from "../react/hooks";
import { BookProperty, ClickContext, ComboBoxItem } from "../react/NotePropertyMenu";
interface BookConfig {
properties: BookProperty[];
}
export interface CheckBoxProperty {
type: "checkbox",
label: string;
bindToLabel: FilterLabelsByType<boolean>;
icon?: string;
}
export interface ButtonProperty {
type: "button",
label: string;
title?: string;
icon?: string;
onClick(context: BookContext): void;
}
export interface SplitButtonProperty extends Omit<ButtonProperty, "type"> {
type: "split-button";
items({ note, parentComponent }: { note: FNote, parentComponent: Component }): VNode;
}
export interface NumberProperty {
type: "number",
label: string;
bindToLabel: FilterLabelsByType<number>;
width?: number;
min?: number;
icon?: string;
disabled?: (note: FNote) => boolean;
}
export interface ComboBoxItem {
value: string;
label: string;
}
interface ComboBoxGroup {
title: string;
items: ComboBoxItem[];
}
export interface ComboBoxProperty {
type: "combobox",
label: string;
icon?: string;
bindToLabel: FilterLabelsByType<string>;
/**
* The default value is used when the label is not set.
*/
defaultValue?: string;
options: (ComboBoxItem | ComboBoxGroup)[];
}
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty;
interface BookContext {
note: FNote;
triggerCommand: NoteContextAwareWidget["triggerCommand"];
}
export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
grid: {
properties: []
@ -211,7 +151,7 @@ function ListExpandDepth(context: { note: FNote, parentComponent: Component }) {
<FormDropdownDivider />
<ListExpandDepthButton label={t("book_properties.expand_all_levels")} depth="all" checked={currentDepth === "all"} {...context} />
</>
)
);
}
function ListExpandDepthButton({ label, depth, note, parentComponent, checked }: { label: string, depth: number | "all", note: FNote, parentComponent: Component, checked?: boolean }) {
@ -226,7 +166,7 @@ function ListExpandDepthButton({ label, depth, note, parentComponent, checked }:
}
function buildExpandListHandler(depth: number | "all") {
return async ({ note, triggerCommand }: BookContext) => {
return async ({ note, triggerCommand }: ClickContext) => {
const { noteId } = note;
const existingValue = note.getLabelValue("expanded");
@ -236,5 +176,5 @@ function buildExpandListHandler(depth: number | "all") {
await attributes.setLabel(noteId, "expanded", newValue);
triggerCommand("refreshNoteList", { noteId });
}
};
}