Files
Trilium/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx

346 lines
14 KiB
TypeScript
Raw Normal View History

2025-08-22 11:24:27 +03:00
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import Dropdown from "../react/Dropdown";
import { NOTE_TYPES } from "../../services/note_types";
2025-08-22 12:34:21 +03:00
import { FormDropdownDivider, FormListBadge, FormListItem } from "../react/FormList";
import { getAvailableLocales, t } from "../../services/i18n";
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
2025-08-21 20:30:12 +03:00
import mime_types from "../../services/mime_types";
import { Locale, LOCALES, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
2025-08-21 20:56:37 +03:00
import server from "../../services/server";
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";
2025-08-22 11:24:27 +03:00
import toast from "../../services/toast";
2025-08-22 11:57:45 +03:00
import branches from "../../services/branches";
import sync from "../../services/sync";
import HelpButton from "../react/HelpButton";
import { TabContext } from "./ribbon-interface";
import Modal from "../react/Modal";
import { CodeMimeTypesList } from "../type_widgets/options/code_notes";
import { ContentLanguagesList } from "../type_widgets/options/i18n";
import { LocaleSelector } from "../type_widgets/options/components/LocaleSelector";
export default function BasicPropertiesTab({ note }: TabContext) {
return (
<div className="basic-properties-widget">
<NoteTypeWidget note={note} />
<ProtectedNoteSwitch note={note} />
<EditabilitySelect note={note} />
2025-08-22 11:24:27 +03:00
<BookmarkSwitch note={note} />
2025-08-22 11:57:45 +03:00
<SharedSwitch note={note} />
<TemplateSwitch note={note} />
2025-08-22 12:34:21 +03:00
<NoteLanguageSwitch note={note} />
</div>
);
}
function NoteTypeWidget({ note }: { note?: FNote | null }) {
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
2025-08-21 20:30:12 +03:00
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
const mimeTypes = useMemo(() => {
mime_types.loadMimeTypes();
return mime_types.getMimeTypes().filter(mimeType => mimeType.enabled)
}, [ codeNotesMimeTypes ]);
const notSelectableNoteTypes = useMemo(() => NOTE_TYPES.filter((nt) => nt.reserved || nt.static).map((nt) => nt.type), []);
2025-08-21 20:56:37 +03:00
const currentNoteType = useNoteProperty(note, "type") ?? undefined;
const currentNoteMime = useNoteProperty(note, "mime");
const [ modalShown, setModalShown ] = useState(false);
2025-08-21 20:56:37 +03:00
const changeNoteType = useCallback(async (type: NoteType, mime?: string) => {
if (!note || (type === currentNoteType && mime === currentNoteMime)) {
return;
}
// Confirm change if the note already has a content.
if (type !== currentNoteType) {
const blob = await note.getBlob();
if (blob?.content && blob.content.trim().length &&
!await (dialog.confirm(t("note_types.confirm-change")))) {
return;
}
}
await server.put(`notes/${note.noteId}/type`, { type, mime });
}, [ note, currentNoteType, currentNoteMime ]);
return (
2025-08-21 20:56:37 +03:00
<div className="note-type-container">
<span>{t("basic_properties.note_type")}:</span> &nbsp;
<Dropdown
dropdownContainerClassName="note-type-dropdown"
2025-08-21 20:56:37 +03:00
text={<span className="note-type-desc">{findTypeTitle(currentNoteType, currentNoteMime)}</span>}
disabled={notSelectableNoteTypes.includes(currentNoteType ?? "text")}
>
2025-08-21 20:56:37 +03:00
{noteTypes.map(({ isNew, isBeta, type, mime, title }) => {
const badges: FormListBadge[] = [];
2025-08-21 20:56:37 +03:00
if (isNew) {
badges.push({
className: "new-note-type-badge",
text: t("note_types.new-feature")
});
}
2025-08-21 20:56:37 +03:00
if (isBeta) {
badges.push({
text: t("note_types.beta-feature")
});
}
2025-08-21 20:56:37 +03:00
const checked = (type === currentNoteType);
if (type !== "code") {
return (
<FormListItem
2025-08-21 20:56:37 +03:00
checked={checked}
badges={badges}
2025-08-21 20:56:37 +03:00
onClick={() => changeNoteType(type, mime)}
>{title}</FormListItem>
);
} else {
return (
<>
2025-08-22 12:34:21 +03:00
<FormDropdownDivider />
2025-08-21 20:56:37 +03:00
<FormListItem
checked={checked}
disabled
2025-08-21 20:56:37 +03:00
>
<strong>{title}</strong>
</FormListItem>
</>
)
}
})}
2025-08-21 20:56:37 +03:00
{mimeTypes.map(({ title, mime }) => (
<FormListItem onClick={() => changeNoteType("code", mime)}>
{title}
</FormListItem>
))}
<FormDropdownDivider />
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
</Dropdown>
<Modal
className="code-mime-types-modal"
title={t("code_mime_types.title")}
show={modalShown} onHidden={() => setModalShown(false)}
size="xl" scrollable
>
<CodeMimeTypesList />
</Modal>
2025-08-21 20:56:37 +03:00
</div>
)
}
function ProtectedNoteSwitch({ note }: { note?: FNote | null }) {
const isProtected = useNoteProperty(note, "isProtected");
return (
<div className="protected-note-switch-container">
<FormToggle
switchOnName={t("protect_note.toggle-on")} switchOnTooltip={t("protect_note.toggle-on-hint")}
switchOffName={t("protect_note.toggle-off")} switchOffTooltip={t("protect_note.toggle-off-hint")}
currentValue={!!isProtected}
2025-08-22 11:24:27 +03:00
onChange={(shouldProtect) => note && protected_session.protectNote(note.noteId, shouldProtect, false)}
/>
</div>
)
}
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> &nbsp;
<FormDropdownList
dropdownContainerClassName="editability-dropdown"
values={options}
currentValue={ readOnly ? "readOnly" : autoReadOnlyDisabled ? "autoReadOnlyDisabled" : "auto" }
keyProperty="value" titleProperty="label" descriptionProperty="description"
onChange={(editability: string) => {
setReadOnly(editability === "readOnly");
setAutoReadOnlyDisabled(editability === "autoReadOnlyDisabled");
}}
/>
</div>
)
}
2025-08-22 11:24:27 +03:00
function BookmarkSwitch({ note }: { note?: FNote | null }) {
const [ isBookmarked, setIsBookmarked ] = useState<boolean>(false);
const refreshState = useCallback(() => {
const isBookmarked = note && !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
setIsBookmarked(!!isBookmarked);
}, [ note ]);
useEffect(() => refreshState(), [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
2025-08-22 11:24:27 +03:00
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
refreshState();
}
});
return (
<div className="bookmark-switch-container">
<FormToggle
switchOnName={t("bookmark_switch.bookmark")} switchOnTooltip={t("bookmark_switch.bookmark_this_note")}
switchOffName={t("bookmark_switch.bookmark")} switchOffTooltip={t("bookmark_switch.remove_bookmark")}
currentValue={isBookmarked}
2025-08-22 11:24:27 +03:00
onChange={async (shouldBookmark) => {
if (!note) return;
const resp = await server.put<ToggleInParentResponse>(`notes/${note.noteId}/toggle-in-parent/_lbBookmarks/${shouldBookmark}`);
if (!resp.success && "message" in resp) {
toast.showError(resp.message);
}
}}
disabled={["root", "_hidden"].includes(note?.noteId ?? "")}
/>
</div>
)
}
function TemplateSwitch({ note }: { note?: FNote | null }) {
const [ isTemplate, setIsTemplate ] = useNoteLabelBoolean(note, "template");
return (
<div className="template-switch-container">
<FormToggle
switchOnName={t("template_switch.template")} switchOnTooltip={t("template_switch.toggle-on-hint")}
switchOffName={t("template_switch.template")} switchOffTooltip={t("template_switch.toggle-off-hint")}
helpPage="KC1HB96bqqHX"
disabled={note?.noteId.startsWith("_options")}
currentValue={isTemplate} onChange={setIsTemplate}
/>
</div>
)
}
2025-08-22 11:57:45 +03:00
function SharedSwitch({ note }: { note?: FNote | null }) {
const [ isShared, setIsShared ] = useState(false);
const refreshState = useCallback(() => {
setIsShared(!!note?.hasAncestor("_share"));
}, [ note ]);
useEffect(() => refreshState(), [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
2025-08-22 11:57:45 +03:00
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
refreshState();
}
});
const switchShareState = useCallback(async (shouldShare: boolean) => {
if (!note) return;
if (shouldShare) {
await branches.cloneNoteToParentNote(note.noteId, "_share");
} else {
if (note?.getParentBranches().length === 1 && !(await dialog.confirm(t("shared_switch.shared-branch")))) {
return;
}
2025-08-22 11:57:45 +03:00
const shareBranch = note?.getParentBranches().find((b) => b.parentNoteId === "_share");
if (!shareBranch?.branchId) return;
await server.remove(`branches/${shareBranch.branchId}?taskId=no-progress-reporting`);
2025-08-22 11:57:45 +03:00
}
sync.syncNow(true);
}, [ note ]);
return (
<div className="shared-switch-container">
<FormToggle
currentValue={isShared}
onChange={switchShareState}
switchOnName={t("shared_switch.shared")} switchOnTooltip={t("shared_switch.toggle-on-title")}
switchOffName={t("shared_switch.shared")} switchOffTooltip={t("shared_switch.toggle-off-title")}
helpPage="R9pX4DGra2Vt"
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
/>
</div>
)
}
2025-08-22 12:34:21 +03:00
function NoteLanguageSwitch({ note }: { note?: FNote | null }) {
const [ languages ] = useTriliumOption("languages");
2025-08-22 12:34:21 +03:00
const DEFAULT_LOCALE = {
id: "",
name: t("note_language.not_set")
};
2025-08-22 15:11:12 +03:00
const [ currentNoteLanguage, setCurrentNoteLanguage ] = useNoteLabel(note, "language");
const [ modalShown, setModalShown ] = useState(false);
2025-08-22 12:34:21 +03:00
const locales = useMemo(() => {
const enabledLanguages = JSON.parse(languages ?? "[]") as string[];
const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id));
return filteredLanguages;
2025-08-22 12:34:21 +03:00
}, [ languages ]);
return (
2025-08-22 12:34:21 +03:00
<div className="note-language-container">
<span>{t("basic_properties.language")}:</span>
&nbsp;
<LocaleSelector
locales={locales}
defaultLocale={DEFAULT_LOCALE}
currentValue={currentNoteLanguage ?? ""} onChange={setCurrentNoteLanguage}
extraChildren={(
<FormListItem
onClick={() => setModalShown(true)}
icon="bx bx-cog"
>{t("note_language.configure-languages")}</FormListItem>
)}
>
2025-08-22 12:34:21 +03:00
</LocaleSelector>
2025-10-08 17:44:17 +03:00
<HelpButton helpPage="B0lcI9xz1r8K" style={{ marginInlineStart: "4px" }} />
<Modal
className="content-languages-modal"
title={t("content_language.title")}
show={modalShown} onHidden={() => setModalShown(false)}
size="lg" scrollable
>
<ContentLanguagesList />
</Modal>
2025-08-22 12:34:21 +03:00
</div>
);
2025-08-22 12:34:21 +03:00
}
function findTypeTitle(type?: NoteType, mime?: string | null) {
if (type === "code") {
const mimeTypes = mime_types.getMimeTypes();
const found = mimeTypes.find((mt) => mt.mime === mime);
return found ? found.title : mime;
} else {
const noteType = NOTE_TYPES.find((nt) => nt.type === type);
return noteType ? noteType.title : type;
}
}