feat(layout): use collapsible for promoted attributes

This commit is contained in:
Elian Doran
2025-12-15 11:57:18 +02:00
parent fb70029091
commit 158f5ac310
3 changed files with 69 additions and 43 deletions

View File

@@ -155,7 +155,7 @@ export default class DesktopLayout {
.child(<ReadOnlyNoteInfoBar />) .child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfo />) .child(<SharedInfo />)
) )
.child(<PromotedAttributes />) .optChild(!isNewLayout, <PromotedAttributes />)
.child(<SqlTableSchemas />) .child(<SqlTableSchemas />)
.child(<NoteDetail />) .child(<NoteDetail />)
.child(<NoteList media="screen" />) .child(<NoteList media="screen" />)

View File

@@ -1,19 +1,21 @@
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import "./PromotedAttributes.css"; import "./PromotedAttributes.css";
import { useNoteContext, useNoteLabel, useTriliumEvent, useUniqueName } from "./react/hooks";
import { Attribute } from "../services/attribute_parser"; import { UpdateAttributeResponse } from "@triliumnext/commons";
import FAttribute from "../entities/fattribute";
import clsx from "clsx"; import clsx from "clsx";
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import FAttribute from "../entities/fattribute";
import FNote from "../entities/fnote";
import { Attribute } from "../services/attribute_parser";
import attributes from "../services/attributes";
import debounce from "../services/debounce";
import { t } from "../services/i18n"; import { t } from "../services/i18n";
import { DefinitionObject, extractAttributeDefinitionTypeAndName, LabelType } from "../services/promoted_attribute_definition_parser"; import { DefinitionObject, extractAttributeDefinitionTypeAndName, LabelType } from "../services/promoted_attribute_definition_parser";
import server from "../services/server"; import server from "../services/server";
import FNote from "../entities/fnote";
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
import NoteAutocomplete from "./react/NoteAutocomplete";
import ws from "../services/ws"; import ws from "../services/ws";
import { UpdateAttributeResponse } from "@triliumnext/commons"; import { useNoteContext, useNoteLabel, useTriliumEvent, useUniqueName } from "./react/hooks";
import attributes from "../services/attributes"; import NoteAutocomplete from "./react/NoteAutocomplete";
import debounce from "../services/debounce";
interface Cell { interface Cell {
uniqueId: string; uniqueId: string;
@@ -39,6 +41,15 @@ type OnChangeListener = (e: OnChangeEventData) => Promise<void>;
export default function PromotedAttributes() { export default function PromotedAttributes() {
const { note, componentId } = useNoteContext(); const { note, componentId } = useNoteContext();
const [ cells, setCells ] = usePromotedAttributeData(note, componentId); const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
return <PromotedAttributesContent note={note} componentId={componentId} cells={cells} setCells={setCells} />;
}
export function PromotedAttributesContent({ note, componentId, cells, setCells }: {
note: FNote | null | undefined;
componentId: string;
cells: Cell[] | undefined;
setCells: Dispatch<StateUpdater<Cell[] | undefined>>;
}) {
const [ cellToFocus, setCellToFocus ] = useState<Cell>(); const [ cellToFocus, setCellToFocus ] = useState<Cell>();
return ( return (
@@ -62,7 +73,7 @@ export default function PromotedAttributes() {
* *
* The cells are returned as a state since they can also be altered internally if needed, for example to add a new empty cell. * The cells are returned as a state since they can also be altered internally if needed, for example to add a new empty cell.
*/ */
function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] { export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
const [ viewType ] = useNoteLabel(note, "viewType"); const [ viewType ] = useNoteLabel(note, "viewType");
const [ cells, setCells ] = useState<Cell[]>(); const [ cells, setCells ] = useState<Cell[]>();
@@ -156,7 +167,7 @@ function PromotedAttributeCell(props: CellProps) {
{correspondingInput} {correspondingInput}
<MultiplicityCell {...props} /> <MultiplicityCell {...props} />
</div> </div>
) );
} }
const LABEL_MAPPINGS: Record<LabelType, HTMLInputTypeAttribute> = { const LABEL_MAPPINGS: Record<LabelType, HTMLInputTypeAttribute> = {
@@ -219,29 +230,29 @@ function LabelInput({ inputId, ...props }: CellProps & { inputId: string }) {
<label className="tn-checkbox">{inputNode}</label> <label className="tn-checkbox">{inputNode}</label>
</div> </div>
<label for={inputId}>{definition.promotedAlias ?? valueName}</label> <label for={inputId}>{definition.promotedAlias ?? valueName}</label>
</> </>;
} else {
return (
<div className="input-group">
{inputNode}
{ definition.labelType === "color" && <ColorPicker {...props} onChange={onChangeListener} inputId={inputId} />}
{ definition.labelType === "url" && (
<InputButton
className="open-external-link-button"
icon="bx bx-window-open"
title={t("promoted_attributes.open_external_link")}
onClick={(e) => {
const inputEl = document.getElementById(inputId) as HTMLInputElement | null;
const url = inputEl?.value;
if (url) {
window.open(url, "_blank");
}
}}
/>
)}
</div>
);
} }
return (
<div className="input-group">
{inputNode}
{ definition.labelType === "color" && <ColorPicker {...props} onChange={onChangeListener} inputId={inputId} />}
{ definition.labelType === "url" && (
<InputButton
className="open-external-link-button"
icon="bx bx-window-open"
title={t("promoted_attributes.open_external_link")}
onClick={(e) => {
const inputEl = document.getElementById(inputId) as HTMLInputElement | null;
const url = inputEl?.value;
if (url) {
window.open(url, "_blank");
}
}}
/>
)}
</div>
);
} }
@@ -282,7 +293,7 @@ function ColorPicker({ cell, onChange, inputId }: CellProps & {
}} }}
/> />
</> </>
) );
} }
function RelationInput({ inputId, ...props }: CellProps & { inputId: string }) { function RelationInput({ inputId, ...props }: CellProps & { inputId: string }) {
@@ -295,7 +306,7 @@ function RelationInput({ inputId, ...props }: CellProps & { inputId: string }) {
await updateAttribute(note, cell, componentId, value, setCells); await updateAttribute(note, cell, componentId, value, setCells);
}} }}
/> />
) );
} }
function MultiplicityCell({ cell, cells, setCells, setCellToFocus, note, componentId }: CellProps) { function MultiplicityCell({ cell, cells, setCells, setCellToFocus, note, componentId }: CellProps) {
@@ -346,13 +357,13 @@ function MultiplicityCell({ cell, cells, setCells, setCellToFocus, note, compone
name: cell.valueName, name: cell.valueName,
value: "" value: ""
} }
}) });
} }
setCells(cells.toSpliced(index, 1, ...newOnesToInsert)); setCells(cells.toSpliced(index, 1, ...newOnesToInsert));
}} }}
/> />
</td> </td>
) );
} }
function PromotedActionButton({ icon, title, onClick }: { function PromotedActionButton({ icon, title, onClick }: {
@@ -366,7 +377,7 @@ function PromotedActionButton({ icon, title, onClick }: {
title={title} title={title}
onClick={onClick} onClick={onClick}
/> />
) );
} }
function InputButton({ icon, className, title, onClick }: { function InputButton({ icon, className, title, onClick }: {
@@ -381,7 +392,7 @@ function InputButton({ icon, className, title, onClick }: {
title={title} title={title}
onClick={onClick} onClick={onClick}
/> />
) );
} }
function setupTextLabelAutocomplete(el: HTMLInputElement, valueAttr: Attribute, onChangeListener: OnChangeListener) { function setupTextLabelAutocomplete(el: HTMLInputElement, valueAttr: Attribute, onChangeListener: OnChangeListener) {
@@ -406,7 +417,7 @@ function setupTextLabelAutocomplete(el: HTMLInputElement, valueAttr: Attribute,
[ [
{ {
displayKey: "value", displayKey: "value",
source: function (term, cb) { source (term, cb) {
term = term.toLowerCase(); term = term.toLowerCase();
const filtered = attributeValues.filter((attr) => attr.value.toLowerCase().includes(term)); const filtered = attributeValues.filter((attr) => attr.value.toLowerCase().includes(term));

View File

@@ -5,16 +5,18 @@ import clsx from "clsx";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import CollectionProperties from "../note_bars/CollectionProperties"; import CollectionProperties from "../note_bars/CollectionProperties";
import { PromotedAttributesContent, usePromotedAttributeData } from "../PromotedAttributes";
import Collapsible from "../react/Collapsible"; import Collapsible from "../react/Collapsible";
import { useNoteContext, useNoteProperty } from "../react/hooks"; import { useNoteContext, useNoteProperty } from "../react/hooks";
import SearchDefinitionTab from "../ribbon/SearchDefinitionTab"; import SearchDefinitionTab from "../ribbon/SearchDefinitionTab";
export default function NoteTitleActions() { export default function NoteTitleActions() {
const { note, ntxId } = useNoteContext(); const { note, ntxId, componentId } = useNoteContext();
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_"); const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
const noteType = useNoteProperty(note, "type"); const noteType = useNoteProperty(note, "type");
const items = [ const items = [
note && <PromotedAttributes note={note} componentId={componentId} />,
note && noteType === "search" && <SearchProperties note={note} ntxId={ntxId} />, note && noteType === "search" && <SearchProperties note={note} ntxId={ntxId} />,
note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} /> note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />
].filter(Boolean); ].filter(Boolean);
@@ -36,3 +38,16 @@ function SearchProperties({ note, ntxId }: { note: FNote, ntxId: string | null |
</Collapsible> </Collapsible>
); );
} }
function PromotedAttributes({ note, componentId }: { note: FNote | null | undefined, componentId: string }) {
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
if (!cells?.length) return false;
return (note && (
<Collapsible
title={t("promoted_attributes.promoted_attributes")}
>
<PromotedAttributesContent note={note} componentId={componentId} cells={cells} setCells={setCells} />
</Collapsible>
));
}