mirror of
https://github.com/zadam/trilium.git
synced 2025-10-29 18:49:00 +01:00
346 lines
14 KiB
TypeScript
346 lines
14 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
|
import Dropdown from "../react/Dropdown";
|
|
import { NOTE_TYPES } from "../../services/note_types";
|
|
import { FormDropdownDivider, FormListBadge, FormListItem } from "../react/FormList";
|
|
import { getAvailableLocales, t } from "../../services/i18n";
|
|
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEventBeta, useTriliumOption, useTriliumOptionBeta, useTriliumOptionJson } from "../react/hooks";
|
|
import mime_types from "../../services/mime_types";
|
|
import { Locale, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
|
|
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";
|
|
import toast from "../../services/toast";
|
|
import branches from "../../services/branches";
|
|
import sync from "../../services/sync";
|
|
import appContext from "../../components/app_context";
|
|
|
|
export default function BasicPropertiesTab() {
|
|
const { note } = useNoteContext();
|
|
|
|
return (
|
|
<div className="basic-properties-widget">
|
|
<NoteTypeWidget note={note} />
|
|
<ProtectedNoteSwitch note={note} />
|
|
<EditabilitySelect note={note} />
|
|
<BookmarkSwitch note={note} />
|
|
<SharedSwitch note={note} />
|
|
<TemplateSwitch note={note} />
|
|
<NoteLanguageSwitch note={note} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function NoteTypeWidget({ note }: { note?: FNote | null }) {
|
|
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
|
|
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
|
|
const mimeTypes = useMemo(() => mime_types.getMimeTypes().filter(mimeType => mimeType.enabled), [ codeNotesMimeTypes ]);
|
|
const notSelectableNoteTypes = useMemo(() => NOTE_TYPES.filter((nt) => nt.reserved || nt.static).map((nt) => nt.type), []);
|
|
|
|
const currentNoteType = useNoteProperty(note, "type") ?? undefined;
|
|
const currentNoteMime = useNoteProperty(note, "mime");
|
|
|
|
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 (
|
|
<div className="note-type-container">
|
|
<span>{t("basic_properties.note_type")}:</span>
|
|
<Dropdown
|
|
dropdownContainerClassName="note-type-dropdown"
|
|
text={<span className="note-type-desc">{findTypeTitle(currentNoteType, currentNoteMime)}</span>}
|
|
disabled={notSelectableNoteTypes.includes(currentNoteType ?? "text")}
|
|
>
|
|
{noteTypes.map(({ isNew, isBeta, type, mime, title }) => {
|
|
const badges: FormListBadge[] = [];
|
|
if (isNew) {
|
|
badges.push({
|
|
className: "new-note-type-badge",
|
|
text: t("note_types.new-feature")
|
|
});
|
|
}
|
|
if (isBeta) {
|
|
badges.push({
|
|
text: t("note_types.beta-feature")
|
|
});
|
|
}
|
|
|
|
const checked = (type === currentNoteType);
|
|
if (type !== "code") {
|
|
return (
|
|
<FormListItem
|
|
checked={checked}
|
|
badges={badges}
|
|
onClick={() => changeNoteType(type, mime)}
|
|
>{title}</FormListItem>
|
|
);
|
|
} else {
|
|
return (
|
|
<>
|
|
<FormDropdownDivider />
|
|
<FormListItem
|
|
checked={checked}
|
|
disabled
|
|
>
|
|
<strong>{title}</strong>
|
|
</FormListItem>
|
|
</>
|
|
)
|
|
}
|
|
})}
|
|
|
|
{mimeTypes.map(({ title, mime }) => (
|
|
<FormListItem onClick={() => changeNoteType("code", mime)}>
|
|
{title}
|
|
</FormListItem>
|
|
))}
|
|
</Dropdown>
|
|
</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}
|
|
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>
|
|
|
|
<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>
|
|
)
|
|
}
|
|
|
|
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 ]);
|
|
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
|
|
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}
|
|
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>
|
|
)
|
|
}
|
|
|
|
function SharedSwitch({ note }: { note?: FNote | null }) {
|
|
const [ isShared, setIsShared ] = useState(false);
|
|
const refreshState = useCallback(() => {
|
|
setIsShared(!!note?.hasAncestor("_share"));
|
|
}, [ note ]);
|
|
|
|
useEffect(() => refreshState(), [ note ]);
|
|
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
|
|
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;
|
|
}
|
|
|
|
const shareBranch = note?.getParentBranches().find((b) => b.parentNoteId === "_share");
|
|
if (!shareBranch?.branchId) return;
|
|
await server.remove(`branches/${shareBranch.branchId}?taskId=no-progress-reporting`);
|
|
}
|
|
|
|
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>
|
|
)
|
|
}
|
|
|
|
function NoteLanguageSwitch({ note }: { note?: FNote | null }) {
|
|
const [ languages ] = useTriliumOptionBeta("languages");
|
|
const DEFAULT_LOCALE = {
|
|
id: "",
|
|
name: t("note_language.not_set")
|
|
};
|
|
|
|
const [ currentNoteLanguage, setCurrentNoteLanguage ] = useNoteLabel(note, "language");
|
|
|
|
const locales = useMemo(() => {
|
|
const enabledLanguages = JSON.parse(languages ?? "[]") as string[];
|
|
const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id));
|
|
const leftToRightLanguages = filteredLanguages.filter((l) => !l.rtl);
|
|
const rightToLeftLanguages = filteredLanguages.filter((l) => l.rtl);
|
|
|
|
let locales: ("---" | Locale)[] = [
|
|
DEFAULT_LOCALE
|
|
];
|
|
|
|
if (leftToRightLanguages.length > 0) {
|
|
locales = [
|
|
...locales,
|
|
"---",
|
|
...leftToRightLanguages
|
|
];
|
|
}
|
|
|
|
if (rightToLeftLanguages.length > 0) {
|
|
locales = [
|
|
...locales,
|
|
"---",
|
|
...rightToLeftLanguages
|
|
];
|
|
}
|
|
|
|
// This will separate the list of languages from the "Configure languages" button.
|
|
// If there is at least one language.
|
|
locales.push("---");
|
|
return locales;
|
|
}, [ languages ]);
|
|
|
|
|
|
return (
|
|
<div className="note-language-container">
|
|
<Dropdown>
|
|
{locales.map(locale => {
|
|
if (typeof locale === "object") {
|
|
const checked = locale.id === (currentNoteLanguage ?? "");
|
|
return <FormListItem
|
|
rtl={locale.rtl}
|
|
checked={checked}
|
|
onClick={() => setCurrentNoteLanguage(locale.id)}
|
|
>{locale.name}</FormListItem>
|
|
} else {
|
|
return <FormDropdownDivider />
|
|
}
|
|
})}
|
|
|
|
<FormListItem
|
|
onClick={() => appContext.tabManager.openContextWithNote("_optionsLocalization", { activate: true })}
|
|
>{t("note_language.configure-languages")}</FormListItem>
|
|
</Dropdown>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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;
|
|
}
|
|
} |