mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	feat(react/ribbon): port editability select
This commit is contained in:
		| @@ -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*/` |  | ||||||
| <div class="dropdown editability-select-widget"> |  | ||||||
|     <style> |  | ||||||
|     .editability-dropdown { |  | ||||||
|         width: 300px; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     .editability-dropdown .dropdown-item { |  | ||||||
|         display: flex !importamt; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     .editability-dropdown .dropdown-item > div { |  | ||||||
|         margin-left: 10px; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     .editability-dropdown .description { |  | ||||||
|         font-size: small; |  | ||||||
|         color: var(--muted-text-color); |  | ||||||
|         white-space: normal; |  | ||||||
|     } |  | ||||||
|     </style> |  | ||||||
|     <button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm select-button dropdown-toggle editability-button"> |  | ||||||
|         <span class="editability-active-desc">${t("editability_select.auto")}</span> |  | ||||||
|         <span class="caret"></span> |  | ||||||
|     </button> |  | ||||||
|     <div class="editability-dropdown dropdown-menu dropdown-menu-right tn-dropdown-list"> |  | ||||||
|         <a class="dropdown-item" href="#" data-editability="auto"> |  | ||||||
|             <span class="check">✓</span> |  | ||||||
|             <div> |  | ||||||
|                 ${t("editability_select.auto")} |  | ||||||
|                 <div class="description">${t("editability_select.note_is_editable")}</div> |  | ||||||
|             </div> |  | ||||||
|         </a> |  | ||||||
|         <a class="dropdown-item" href="#" data-editability="readOnly"> |  | ||||||
|             <span class="check">✓</span> |  | ||||||
|             <div> |  | ||||||
|                 ${t("editability_select.read_only")} |  | ||||||
|                 <div class="description">${t("editability_select.note_is_read_only")}</div> |  | ||||||
|             </div> |  | ||||||
|         </a> |  | ||||||
|         <a class="dropdown-item" href="#" data-editability="autoReadOnlyDisabled"> |  | ||||||
|             <span class="check">✓</span> |  | ||||||
|             <div> |  | ||||||
|                 ${t("editability_select.always_editable")} |  | ||||||
|                 <div class="description">${t("editability_select.note_is_always_editable")}</div> |  | ||||||
|             </div> |  | ||||||
|         </a> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
| `; |  | ||||||
|  |  | ||||||
| export default class EditabilitySelectWidget extends NoteContextAwareWidget { |  | ||||||
|  |  | ||||||
|     private dropdown!: Dropdown; |  | ||||||
|     private $editabilityActiveDesc!: JQuery<HTMLElement>; |  | ||||||
|  |  | ||||||
|     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(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										30
									
								
								apps/client/src/widgets/react/FormDropdownList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								apps/client/src/widgets/react/FormDropdownList.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | import Dropdown from "./Dropdown"; | ||||||
|  | import { FormListItem } from "./FormList"; | ||||||
|  |  | ||||||
|  | interface FormDropdownList<T> { | ||||||
|  |     values: T[]; | ||||||
|  |     keyProperty: keyof T; | ||||||
|  |     titleProperty: keyof T; | ||||||
|  |     descriptionProperty?: keyof T; | ||||||
|  |     currentValue: string; | ||||||
|  |     onChange(newValue: string): void; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default function FormDropdownList<T>({ values, keyProperty, titleProperty, descriptionProperty, currentValue, onChange }: FormDropdownList<T>) { | ||||||
|  |     const currentValueData = values.find(value => value[keyProperty] === currentValue); | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <Dropdown text={currentValueData?.[titleProperty] ?? ""}> | ||||||
|  |             {values.map(item => ( | ||||||
|  |                 <FormListItem | ||||||
|  |                     onClick={() => onChange(item[keyProperty] as string)} | ||||||
|  |                     checked={currentValue === item[keyProperty]} | ||||||
|  |                     description={descriptionProperty && item[descriptionProperty] as string} | ||||||
|  |                     selected={currentValue === item[keyProperty]} | ||||||
|  |                 > | ||||||
|  |                     {item[titleProperty] as string} | ||||||
|  |                 </FormListItem> | ||||||
|  |             ))} | ||||||
|  |         </Dropdown> | ||||||
|  |     ) | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								apps/client/src/widgets/react/FormList.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/client/src/widgets/react/FormList.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | .dropdown-item .description { | ||||||
|  |     font-size: small; | ||||||
|  |     color: var(--muted-text-color); | ||||||
|  |     white-space: normal; | ||||||
|  | } | ||||||
| @@ -2,6 +2,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap"; | |||||||
| import { ComponentChildren } from "preact"; | import { ComponentChildren } from "preact"; | ||||||
| import Icon from "./Icon"; | import Icon from "./Icon"; | ||||||
| import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat"; | import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat"; | ||||||
|  | import "./FormList.css"; | ||||||
|  |  | ||||||
| interface FormListOpts { | interface FormListOpts { | ||||||
|     children: ComponentChildren; |     children: ComponentChildren; | ||||||
| @@ -76,27 +77,33 @@ interface FormListItemOpts { | |||||||
|     active?: boolean; |     active?: boolean; | ||||||
|     badges?: FormListBadge[]; |     badges?: FormListBadge[]; | ||||||
|     disabled?: boolean; |     disabled?: boolean; | ||||||
|     checked?: boolean; |     checked?: boolean | null; | ||||||
|  |     selected?: boolean; | ||||||
|     onClick?: () => void; |     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) { |     if (checked) { | ||||||
|         icon = "bx bx-check"; |         icon = "bx bx-check"; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <a |         <a | ||||||
|             class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""}`} |             class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""} ${selected ? "selected" : ""}`} | ||||||
|             data-value={value} title={title} |             data-value={value} title={title} | ||||||
|             tabIndex={0} |             tabIndex={0} | ||||||
|             onClick={onClick} |             onClick={onClick} | ||||||
|         > |         > | ||||||
|             <Icon icon={icon} />  |             <Icon icon={icon} />  | ||||||
|  |             <div> | ||||||
|                 {children} |                 {children} | ||||||
|                 {badges && badges.map(({ className, text }) => ( |                 {badges && badges.map(({ className, text }) => ( | ||||||
|                     <span className={`badge ${className ?? ""}`}>{text}</span> |                     <span className={`badge ${className ?? ""}`}>{text}</span> | ||||||
|                 ))} |                 ))} | ||||||
|  |                 {description && <div className="description">{description}</div>} | ||||||
|  |             </div> | ||||||
|         </a> |         </a> | ||||||
|     ); |     ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -314,12 +314,12 @@ export function useNoteProperty<T extends keyof FNote>(note: FNote | null | unde | |||||||
| } | } | ||||||
|  |  | ||||||
| export function useNoteLabel(note: FNote | undefined | null, labelName: string): [string | null | undefined, (newValue: string) => void] { | export function useNoteLabel(note: FNote | undefined | null, labelName: string): [string | null | undefined, (newValue: string) => void] { | ||||||
|     const [ labelValue, setNoteValue ] = useState<string | null | undefined>(note?.getLabelValue(labelName)); |     const [ labelValue, setLabelValue ] = useState<string | null | undefined>(note?.getLabelValue(labelName)); | ||||||
|  |  | ||||||
|     useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { |     useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { | ||||||
|         for (const attr of loadResults.getAttributeRows()) { |         for (const attr of loadResults.getAttributeRows()) { | ||||||
|             if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) { |             if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) { | ||||||
|                 setNoteValue(attr.value ?? null); |                 setLabelValue(attr.value ?? null); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| @@ -335,3 +335,27 @@ export function useNoteLabel(note: FNote | undefined | null, labelName: string): | |||||||
|         setter |         setter | ||||||
|     ] as const; |     ] as const; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: string): [ boolean | null | undefined, (newValue: boolean) => void] { | ||||||
|  |     const [ labelValue, setLabelValue ] = useState<boolean | null | undefined>(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; | ||||||
|  | } | ||||||
| @@ -3,7 +3,7 @@ import Dropdown from "../react/Dropdown"; | |||||||
| import { NOTE_TYPES } from "../../services/note_types"; | import { NOTE_TYPES } from "../../services/note_types"; | ||||||
| import { FormDivider, FormListBadge, FormListItem } from "../react/FormList"; | import { FormDivider, FormListBadge, FormListItem } from "../react/FormList"; | ||||||
| import { t } from "../../services/i18n"; | 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 mime_types from "../../services/mime_types"; | ||||||
| import { NoteType } from "@triliumnext/commons"; | import { NoteType } from "@triliumnext/commons"; | ||||||
| import server from "../../services/server"; | import server from "../../services/server"; | ||||||
| @@ -11,6 +11,7 @@ import dialog from "../../services/dialog"; | |||||||
| import FormToggle from "../react/FormToggle"; | import FormToggle from "../react/FormToggle"; | ||||||
| import FNote from "../../entities/fnote"; | import FNote from "../../entities/fnote"; | ||||||
| import protected_session from "../../services/protected_session"; | import protected_session from "../../services/protected_session"; | ||||||
|  | import FormDropdownList from "../react/FormDropdownList"; | ||||||
|  |  | ||||||
| export default function BasicPropertiesTab() { | export default function BasicPropertiesTab() { | ||||||
|     const { note } = useNoteContext(); |     const { note } = useNoteContext(); | ||||||
| @@ -19,6 +20,7 @@ export default function BasicPropertiesTab() { | |||||||
|         <div className="basic-properties-widget">         |         <div className="basic-properties-widget">         | ||||||
|             <NoteTypeWidget note={note} /> |             <NoteTypeWidget note={note} /> | ||||||
|             <ProtectedNoteSwitch note={note} /> |             <ProtectedNoteSwitch note={note} /> | ||||||
|  |             <EditabilitySelect note={note} /> | ||||||
|         </div> |         </div> | ||||||
|     ); |     ); | ||||||
| } | } | ||||||
| @@ -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 ( | ||||||
|  |         <div class="editability-select-container"> | ||||||
|  |             <span>{t("basic_properties.editable")}:</span>   | ||||||
|  |  | ||||||
|  |             <FormDropdownList | ||||||
|  |                 values={options} | ||||||
|  |                 currentValue={ readOnly ? "readOnly" : autoReadOnlyDisabled ? "autoReadOnlyDisabled" : "auto" } | ||||||
|  |                 keyProperty="value" titleProperty="label" descriptionProperty="description" | ||||||
|  |                 onChange={(editability: string) => { | ||||||
|  |                     setReadOnly(editability === "readOnly"); | ||||||
|  |                     setAutoReadOnlyDisabled(editability === "autoReadOnlyDisabled"); | ||||||
|  |                 }} | ||||||
|  |             /> | ||||||
|  |         </div> | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
| function findTypeTitle(type?: NoteType, mime?: string | null) { | function findTypeTitle(type?: NoteType, mime?: string | null) { | ||||||
|     if (type === "code") { |     if (type === "code") { | ||||||
|         const mimeTypes = mime_types.getMimeTypes(); |         const mimeTypes = mime_types.getMimeTypes(); | ||||||
|   | |||||||
| @@ -10,10 +10,6 @@ import type FNote from "../../entities/fnote.js"; | |||||||
| import NoteLanguageWidget from "../note_language.js"; | import NoteLanguageWidget from "../note_language.js"; | ||||||
|  |  | ||||||
| const TPL = /*html*/` | const TPL = /*html*/` | ||||||
|     <div class="editability-select-container"> |  | ||||||
|         <span>${t("basic_properties.editable")}:</span>   |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div class="bookmark-switch-container"></div> |     <div class="bookmark-switch-container"></div> | ||||||
|  |  | ||||||
|     <div class="shared-switch-container"></div> |     <div class="shared-switch-container"></div> | ||||||
| @@ -27,8 +23,6 @@ const TPL = /*html*/` | |||||||
|  |  | ||||||
| export default class BasicPropertiesWidget extends NoteContextAwareWidget { | export default class BasicPropertiesWidget extends NoteContextAwareWidget { | ||||||
|  |  | ||||||
|     private noteTypeWidget: NoteTypeWidget; |  | ||||||
|     private protectedNoteSwitchWidget: ProtectedNoteSwitchWidget; |  | ||||||
|     private editabilitySelectWidget: EditabilitySelectWidget; |     private editabilitySelectWidget: EditabilitySelectWidget; | ||||||
|     private bookmarkSwitchWidget: BookmarkSwitchWidget; |     private bookmarkSwitchWidget: BookmarkSwitchWidget; | ||||||
|     private sharedSwitchWidget: SharedSwitchWidget; |     private sharedSwitchWidget: SharedSwitchWidget; | ||||||
| @@ -45,8 +39,6 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget { | |||||||
|         this.noteLanguageWidget = new NoteLanguageWidget().contentSized(); |         this.noteLanguageWidget = new NoteLanguageWidget().contentSized(); | ||||||
|  |  | ||||||
|         this.child( |         this.child( | ||||||
|             this.noteTypeWidget, |  | ||||||
|             this.protectedNoteSwitchWidget, |  | ||||||
|             this.editabilitySelectWidget, |             this.editabilitySelectWidget, | ||||||
|             this.bookmarkSwitchWidget, |             this.bookmarkSwitchWidget, | ||||||
|             this.sharedSwitchWidget, |             this.sharedSwitchWidget, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user