From f772f59d7c5f6aac904d416603fbf21a2c384122 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 21 Aug 2025 22:19:26 +0300 Subject: [PATCH] feat(react/ribbon): port editability select --- apps/client/src/widgets/editability_select.ts | 120 ------------------ .../src/widgets/react/FormDropdownList.tsx | 30 +++++ apps/client/src/widgets/react/FormList.css | 5 + apps/client/src/widgets/react/FormList.tsx | 21 ++- apps/client/src/widgets/react/hooks.tsx | 28 +++- .../src/widgets/ribbon/BasicPropertiesTab.tsx | 43 ++++++- .../ribbon_widgets/basic_properties.ts | 8 -- 7 files changed, 117 insertions(+), 138 deletions(-) delete mode 100644 apps/client/src/widgets/editability_select.ts create mode 100644 apps/client/src/widgets/react/FormDropdownList.tsx create mode 100644 apps/client/src/widgets/react/FormList.css diff --git a/apps/client/src/widgets/editability_select.ts b/apps/client/src/widgets/editability_select.ts deleted file mode 100644 index e7127ca8a..000000000 --- a/apps/client/src/widgets/editability_select.ts +++ /dev/null @@ -1,120 +0,0 @@ -import attributeService from "../services/attributes.js"; -import NoteContextAwareWidget from "./note_context_aware_widget.js"; -import { t } from "../services/i18n.js"; -import type FNote from "../entities/fnote.js"; -import type { EventData } from "../components/app_context.js"; -import { Dropdown } from "bootstrap"; - -type Editability = "auto" | "readOnly" | "autoReadOnlyDisabled"; - -const TPL = /*html*/` - -`; - -export default class EditabilitySelectWidget extends NoteContextAwareWidget { - - private dropdown!: Dropdown; - private $editabilityActiveDesc!: JQuery; - - doRender() { - this.$widget = $(TPL); - - this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]); - - this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc"); - - this.$widget.on("click", ".dropdown-item", async (e) => { - this.dropdown.toggle(); - - const editability = $(e.target).closest("[data-editability]").attr("data-editability"); - - if (!this.note || !this.noteId) { - return; - } - - for (const ownedAttr of this.note.getOwnedLabels()) { - if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) { - await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId); - } - } - - if (editability && editability !== "auto") { - await attributeService.addLabel(this.noteId, editability); - } - }); - } - - async refreshWithNote(note: FNote) { - let editability: Editability = "auto"; - - if (this.note?.isLabelTruthy("readOnly")) { - editability = "readOnly"; - } else if (this.note?.isLabelTruthy("autoReadOnlyDisabled")) { - editability = "autoReadOnlyDisabled"; - } - - const labels = { - auto: t("editability_select.auto"), - readOnly: t("editability_select.read_only"), - autoReadOnlyDisabled: t("editability_select.always_editable") - }; - - this.$widget.find(".dropdown-item").removeClass("selected"); - this.$widget.find(`.dropdown-item[data-editability='${editability}']`).addClass("selected"); - - this.$editabilityActiveDesc.text(labels[editability]); - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) { - this.refresh(); - } - } -} diff --git a/apps/client/src/widgets/react/FormDropdownList.tsx b/apps/client/src/widgets/react/FormDropdownList.tsx new file mode 100644 index 000000000..4bab06948 --- /dev/null +++ b/apps/client/src/widgets/react/FormDropdownList.tsx @@ -0,0 +1,30 @@ +import Dropdown from "./Dropdown"; +import { FormListItem } from "./FormList"; + +interface FormDropdownList { + values: T[]; + keyProperty: keyof T; + titleProperty: keyof T; + descriptionProperty?: keyof T; + currentValue: string; + onChange(newValue: string): void; +} + +export default function FormDropdownList({ values, keyProperty, titleProperty, descriptionProperty, currentValue, onChange }: FormDropdownList) { + const currentValueData = values.find(value => value[keyProperty] === currentValue); + + return ( + + {values.map(item => ( + onChange(item[keyProperty] as string)} + checked={currentValue === item[keyProperty]} + description={descriptionProperty && item[descriptionProperty] as string} + selected={currentValue === item[keyProperty]} + > + {item[titleProperty] as string} + + ))} + + ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/react/FormList.css b/apps/client/src/widgets/react/FormList.css new file mode 100644 index 000000000..62631b131 --- /dev/null +++ b/apps/client/src/widgets/react/FormList.css @@ -0,0 +1,5 @@ +.dropdown-item .description { + font-size: small; + color: var(--muted-text-color); + white-space: normal; +} \ No newline at end of file diff --git a/apps/client/src/widgets/react/FormList.tsx b/apps/client/src/widgets/react/FormList.tsx index ff0da091a..42aeb93d5 100644 --- a/apps/client/src/widgets/react/FormList.tsx +++ b/apps/client/src/widgets/react/FormList.tsx @@ -2,6 +2,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap"; import { ComponentChildren } from "preact"; import Icon from "./Icon"; import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat"; +import "./FormList.css"; interface FormListOpts { children: ComponentChildren; @@ -76,27 +77,33 @@ interface FormListItemOpts { active?: boolean; badges?: FormListBadge[]; disabled?: boolean; - checked?: boolean; + checked?: boolean | null; + selected?: boolean; onClick?: () => void; + description?: string; + className?: string; } -export function FormListItem({ children, icon, value, title, active, badges, disabled, checked, onClick }: FormListItemOpts) { +export function FormListItem({ children, icon, value, title, active, badges, disabled, checked, onClick, description, selected }: FormListItemOpts) { if (checked) { icon = "bx bx-check"; } return (   - {children} - {badges && badges.map(({ className, text }) => ( - {text} - ))} +
+ {children} + {badges && badges.map(({ className, text }) => ( + {text} + ))} + {description &&
{description}
} +
); } diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index bc520dc0e..7c8257ead 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -314,12 +314,12 @@ export function useNoteProperty(note: FNote | null | unde } export function useNoteLabel(note: FNote | undefined | null, labelName: string): [string | null | undefined, (newValue: string) => void] { - const [ labelValue, setNoteValue ] = useState(note?.getLabelValue(labelName)); + const [ labelValue, setLabelValue ] = useState(note?.getLabelValue(labelName)); useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { for (const attr of loadResults.getAttributeRows()) { if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) { - setNoteValue(attr.value ?? null); + setLabelValue(attr.value ?? null); } } }); @@ -334,4 +334,28 @@ export function useNoteLabel(note: FNote | undefined | null, labelName: string): labelValue, setter ] as const; +} + +export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: string): [ boolean | null | undefined, (newValue: boolean) => void] { + const [ labelValue, setLabelValue ] = useState(note?.hasLabel(labelName)); + + useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { + for (const attr of loadResults.getAttributeRows()) { + if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) { + setLabelValue(!attr.isDeleted); + } + } + }); + + const setter = useCallback((value: boolean) => { + if (note) { + if (value) { + attributes.setLabel(note.noteId, labelName, ""); + } else { + attributes.removeOwnedLabelByName(note, labelName); + } + } + }, [note]); + + return [ labelValue, setter ] as const; } \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx b/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx index 5c7ab1937..84de1b425 100644 --- a/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx +++ b/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx @@ -3,7 +3,7 @@ import Dropdown from "../react/Dropdown"; import { NOTE_TYPES } from "../../services/note_types"; import { FormDivider, FormListBadge, FormListItem } from "../react/FormList"; import { t } from "../../services/i18n"; -import { useNoteContext, useNoteProperty, useTriliumOption } from "../react/hooks"; +import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumOption } from "../react/hooks"; import mime_types from "../../services/mime_types"; import { NoteType } from "@triliumnext/commons"; import server from "../../services/server"; @@ -11,6 +11,7 @@ import dialog from "../../services/dialog"; import FormToggle from "../react/FormToggle"; import FNote from "../../entities/fnote"; import protected_session from "../../services/protected_session"; +import FormDropdownList from "../react/FormDropdownList"; export default function BasicPropertiesTab() { const { note } = useNoteContext(); @@ -19,6 +20,7 @@ export default function BasicPropertiesTab() {
+
); } @@ -121,6 +123,45 @@ function ProtectedNoteSwitch({ note }: { note?: FNote | null }) { ) } +function EditabilitySelect({ note }: { note?: FNote | null }) { + const [ readOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); + const [ autoReadOnlyDisabled, setAutoReadOnlyDisabled ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled"); + + const options = useMemo(() => ([ + { + value: "auto", + label: t("editability_select.auto"), + description: t("editability_select.note_is_editable"), + }, + { + value: "readOnly", + label: t("editability_select.read_only"), + description: t("editability_select.note_is_read_only") + }, + { + value: "autoReadOnlyDisabled", + label: t("editability_select.always_editable"), + description: t("editability_select.note_is_always_editable") + } + ]), []); + + return ( +
+ {t("basic_properties.editable")}:   + + { + setReadOnly(editability === "readOnly"); + setAutoReadOnlyDisabled(editability === "autoReadOnlyDisabled"); + }} + /> +
+ ) +} + function findTypeTitle(type?: NoteType, mime?: string | null) { if (type === "code") { const mimeTypes = mime_types.getMimeTypes(); diff --git a/apps/client/src/widgets/ribbon_widgets/basic_properties.ts b/apps/client/src/widgets/ribbon_widgets/basic_properties.ts index 14fb7468f..f80e163a8 100644 --- a/apps/client/src/widgets/ribbon_widgets/basic_properties.ts +++ b/apps/client/src/widgets/ribbon_widgets/basic_properties.ts @@ -10,10 +10,6 @@ import type FNote from "../../entities/fnote.js"; import NoteLanguageWidget from "../note_language.js"; const TPL = /*html*/` -
- ${t("basic_properties.editable")}:   -
-
@@ -27,8 +23,6 @@ const TPL = /*html*/` export default class BasicPropertiesWidget extends NoteContextAwareWidget { - private noteTypeWidget: NoteTypeWidget; - private protectedNoteSwitchWidget: ProtectedNoteSwitchWidget; private editabilitySelectWidget: EditabilitySelectWidget; private bookmarkSwitchWidget: BookmarkSwitchWidget; private sharedSwitchWidget: SharedSwitchWidget; @@ -45,8 +39,6 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget { this.noteLanguageWidget = new NoteLanguageWidget().contentSized(); this.child( - this.noteTypeWidget, - this.protectedNoteSwitchWidget, this.editabilitySelectWidget, this.bookmarkSwitchWidget, this.sharedSwitchWidget,