From 98bf63e94b043c752cc43c42db69056591c06b6e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Nov 2025 21:58:52 +0200 Subject: [PATCH 01/36] chore(promoted_attributes): start with empty widget --- apps/client/src/layouts/desktop_layout.tsx | 4 ++-- apps/client/src/layouts/mobile_layout.tsx | 4 ++-- apps/client/src/widgets/PromotedAttributes.tsx | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 apps/client/src/widgets/PromotedAttributes.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index cbce8fb18..3128581a9 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -21,7 +21,6 @@ import NoteTreeWidget from "../widgets/note_tree.js"; import NoteWrapperWidget from "../widgets/note_wrapper.js"; import options from "../services/options.js"; import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js"; -import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; import QuickSearchWidget from "../widgets/quick_search.js"; import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx"; import Ribbon from "../widgets/ribbon/Ribbon.jsx"; @@ -45,6 +44,7 @@ import utils from "../services/utils.js"; import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js"; import NoteDetail from "../widgets/NoteDetail.jsx"; import RightPanelWidget from "../widgets/sidebar/RightPanelWidget.jsx"; +import PromotedAttributes from "../widgets/PromotedAttributes.jsx"; export default class DesktopLayout { @@ -141,7 +141,7 @@ export default class DesktopLayout { .child() .child() ) - .child(new PromotedAttributesWidget()) + .child() .child() .child() .child() diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index c51ef9e92..e08d48b61 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -13,7 +13,6 @@ import NoteTitleWidget from "../widgets/note_title.js"; import ContentHeader from "../widgets/containers/content_header.js"; import NoteTreeWidget from "../widgets/note_tree.js"; import NoteWrapperWidget from "../widgets/note_wrapper.js"; -import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; import QuickSearchWidget from "../widgets/quick_search.js"; import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx"; import RootContainer from "../widgets/containers/root_container.js"; @@ -29,6 +28,7 @@ import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button import type AppContext from "../components/app_context.js"; import NoteDetail from "../widgets/NoteDetail.jsx"; import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx"; +import PromotedAttributes from "../widgets/PromotedAttributes.jsx"; const MOBILE_CSS = ` - - -`; - // TODO: Deduplicate interface AttributeResult { attributeId: string; @@ -126,36 +30,12 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { } doRender() { - this.$widget = $(TPL); this.contentSized(); - this.$container = this.$widget.find(".promoted-attributes-container"); - } - - getTitle(note: FNote) { - const promotedDefAttrs = note.getPromotedDefinitionAttributes(); - - if (promotedDefAttrs.length === 0) { - return { show: false }; - } - - return { - show: true, - activate: options.is("promotedAttributesOpenInRibbon"), - title: t("promoted_attributes.promoted_attributes"), - icon: "bx bx-table" - }; } async refreshWithNote(note: FNote) { this.$container.empty(); - const promotedDefAttrs = note.getPromotedDefinitionAttributes(); - const ownedAttributes = note.getOwnedAttributes(); - // attrs are not resorted if position changes after the initial load - // promoted attrs are sorted primarily by order of definitions, but with multi-valued promoted attrs - // the order of attributes is important as well - ownedAttributes.sort((a, b) => a.position - b.position); - if (promotedDefAttrs.length === 0 || note.getLabelValue("viewType") === "table") { this.toggleInt(false); return; @@ -163,33 +43,13 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { const $cells: JQuery[] = []; - for (const definitionAttr of promotedDefAttrs) { - const valueType = definitionAttr.name.startsWith("label:") ? "label" : "relation"; - const valueName = definitionAttr.name.substr(valueType.length + 1); - - let valueAttrs = ownedAttributes.filter((el) => el.name === valueName && el.type === valueType) as Attribute[]; - - if (valueAttrs.length === 0) { - valueAttrs.push({ - attributeId: "", - type: valueType, - name: valueName, - value: "" - }); - } - - if (definitionAttr.getDefinition().multiplicity === "single") { - valueAttrs = valueAttrs.slice(0, 1); - } - - for (const valueAttr of valueAttrs) { + for (const valueAttr of valueAttrs) { const $cell = await this.createPromotedAttributeCell(definitionAttr, valueAttr, valueName); if ($cell) { $cells.push($cell); } } - } // we replace the whole content in one step, so there can't be any race conditions // (previously we saw promoted attributes doubling) From 33c3fb7de012d5cd614aff8b901750e2570403bc Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Nov 2025 22:25:32 +0200 Subject: [PATCH 03/36] chore(react/promoted_attributes): reintroduce labels --- .../client/src/widgets/PromotedAttributes.css | 4 +++ .../client/src/widgets/PromotedAttributes.tsx | 33 ++++++++++++++++--- .../client/src/widgets/promoted_attributes.ts | 12 ------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/apps/client/src/widgets/PromotedAttributes.css b/apps/client/src/widgets/PromotedAttributes.css index ea95fd455..45ebad5b2 100644 --- a/apps/client/src/widgets/PromotedAttributes.css +++ b/apps/client/src/widgets/PromotedAttributes.css @@ -4,6 +4,10 @@ body.mobile .promoted-attributes-widget { overflow: auto; } +.component.promoted-attributes-widget { + contain: none; +} + .promoted-attributes-container { margin: 0 1.5em; overflow: auto; diff --git a/apps/client/src/widgets/PromotedAttributes.tsx b/apps/client/src/widgets/PromotedAttributes.tsx index 58ed87336..49b4c33ef 100644 --- a/apps/client/src/widgets/PromotedAttributes.tsx +++ b/apps/client/src/widgets/PromotedAttributes.tsx @@ -2,10 +2,17 @@ import { useEffect, useState } from "preact/hooks"; import "./PromotedAttributes.css"; import { useNoteContext } from "./react/hooks"; import { Attribute } from "../services/attribute_parser"; +import FAttribute from "../entities/fattribute"; + +interface Cell { + definitionAttr: FAttribute; + valueAttr: Attribute; + valueName: string; +} export default function PromotedAttributes() { const { note } = useNoteContext(); - const [ cells, setCells ] = useState(); + const [ cells, setCells ] = useState(); useEffect(() => { if (!note) return; @@ -16,7 +23,7 @@ export default function PromotedAttributes() { // the order of attributes is important as well ownedAttributes.sort((a, b) => a.position - b.position); - const cells: Attribute[] = []; + const cells: Cell[] = []; for (const definitionAttr of promotedDefAttrs) { const valueType = definitionAttr.name.startsWith("label:") ? "label" : "relation"; const valueName = definitionAttr.name.substr(valueType.length + 1); @@ -36,7 +43,9 @@ export default function PromotedAttributes() { valueAttrs = valueAttrs.slice(0, 1); } - cells.push(...valueAttrs); + for (const valueAttr of valueAttrs) { + cells.push({ definitionAttr, valueAttr, valueName }); + } } setCells(cells); }, [ note ]); @@ -44,8 +53,24 @@ export default function PromotedAttributes() { return (
- + {cells?.map(cell => )}
); } + +function PromotedAttributeCell({ cell }: { cell: Cell }) { + const { valueName, valueAttr, definitionAttr } = cell; + const inputId = `value-${valueAttr.attributeId}`; + const definition = definitionAttr.getDefinition(); + + return ( +
+ + +
+ ) +} diff --git a/apps/client/src/widgets/promoted_attributes.ts b/apps/client/src/widgets/promoted_attributes.ts index 7620bfa4b..4ddcb7d7e 100644 --- a/apps/client/src/widgets/promoted_attributes.ts +++ b/apps/client/src/widgets/promoted_attributes.ts @@ -21,14 +21,6 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { private $container!: JQuery; - get name() { - return "promotedAttributes"; - } - - get toggleCommand() { - return "toggleRibbonTabPromotedAttributes"; - } - doRender() { this.contentSized(); } @@ -59,11 +51,8 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { async createPromotedAttributeCell(definitionAttr: FAttribute, valueAttr: Attribute, valueName: string) { const definition = definitionAttr.getDefinition(); - const id = `value-${valueAttr.attributeId}`; const $input = $("") - .prop("tabindex", 200 + definitionAttr.position) - .prop("id", id) .attr("data-attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId ?? "" : "") // if not owned, we'll force creation of a new attribute instead of updating the inherited one .attr("data-attribute-type", valueAttr.type) .attr("data-attribute-name", valueAttr.name) @@ -79,7 +68,6 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget { const $wrapper = $('