mirror of
https://github.com/zadam/trilium.git
synced 2025-12-17 21:04:24 +01:00
New layout: status bar (#8021)
This commit is contained in:
commit
15b5885982
@ -265,7 +265,7 @@ export type CommandMappings = {
|
||||
|
||||
reEvaluateRightPaneVisibility: CommandData;
|
||||
runActiveNote: CommandData;
|
||||
scrollContainerToCommand: CommandData & {
|
||||
scrollContainerTo: CommandData & {
|
||||
position: number;
|
||||
};
|
||||
scrollToEnd: CommandData;
|
||||
|
||||
@ -44,7 +44,6 @@ import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
import Breadcrumb from "../widgets/Breadcrumb.jsx";
|
||||
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
||||
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
|
||||
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
||||
@ -52,7 +51,7 @@ import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx";
|
||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||
import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx";
|
||||
import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx";
|
||||
import NoteStatusBar from "../widgets/NoteStatusBar.jsx";
|
||||
import StatusBar from "../widgets/layout/StatusBar.jsx";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@ -134,6 +133,7 @@ export default class DesktopLayout {
|
||||
.filling()
|
||||
.collapsible()
|
||||
.id("center-pane")
|
||||
.optChild(isNewLayout, <StandaloneRibbonAdapter component={FormattingToolbar} />)
|
||||
.child(
|
||||
new SplitNoteContainer(() =>
|
||||
new NoteWrapperWidget()
|
||||
@ -141,7 +141,6 @@ export default class DesktopLayout {
|
||||
new FlexContainer("row")
|
||||
.class("breadcrumb-row")
|
||||
.cssBlock(".breadcrumb-row > * { margin: 5px; }")
|
||||
.child(<Breadcrumb />)
|
||||
.optChild(isNewLayout, <BreadcrumbBadges />)
|
||||
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
||||
.child(<MovePaneButton direction="left" />)
|
||||
@ -152,7 +151,7 @@ export default class DesktopLayout {
|
||||
)
|
||||
.optChild(!isFloatingTitlebar, titleRow)
|
||||
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
|
||||
.optChild(isNewLayout, <StandaloneRibbonAdapter component={FormattingToolbar} />)
|
||||
.optChild(isNewLayout, <Ribbon />)
|
||||
.child(new WatchedFileUpdateStatusWidget())
|
||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||
.child(
|
||||
@ -178,14 +177,10 @@ export default class DesktopLayout {
|
||||
...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
|
||||
...this.customWidgets.get("note-detail-pane")
|
||||
)
|
||||
.optChild(isNewLayout, (
|
||||
<Ribbon>
|
||||
<NoteStatusBar />
|
||||
</Ribbon>
|
||||
))
|
||||
)
|
||||
)
|
||||
.child(...this.customWidgets.get("center-pane"))
|
||||
|
||||
)
|
||||
.child(
|
||||
new RightPaneContainer()
|
||||
@ -194,8 +189,10 @@ export default class DesktopLayout {
|
||||
.child(...this.customWidgets.get("right-pane"))
|
||||
)
|
||||
)
|
||||
.optChild(!launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
||||
)
|
||||
)
|
||||
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
||||
.child(<CloseZenModeButton />)
|
||||
|
||||
// Desktop-specific dialogs.
|
||||
|
||||
@ -154,7 +154,7 @@ button.btn.btn-success kbd {
|
||||
color: var(--button-group-active-button-text-color);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* Input boxes
|
||||
*/
|
||||
|
||||
@ -399,7 +399,8 @@ button.select-button.dropdown-toggle.btn:active {
|
||||
select:focus,
|
||||
select.form-select:focus,
|
||||
select.form-control:focus,
|
||||
.select-button.dropdown-toggle.btn:focus {
|
||||
.select-button.dropdown-toggle.btn:focus,
|
||||
.select-button.focus-outline:focus {
|
||||
box-shadow: unset;
|
||||
outline: 3px solid var(--input-focus-outline-color);
|
||||
outline-offset: 0;
|
||||
@ -422,7 +423,7 @@ optgroup {
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* File input
|
||||
*
|
||||
* <label class="tn-file-input tn-input-field">
|
||||
@ -784,4 +785,4 @@ input[type="range"] {
|
||||
scrollbar-color: unset;
|
||||
scrollbar-width: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2146,15 +2146,24 @@
|
||||
"shared_publicly_description": "This note has been published online at {{- link}}, and is publicly accessible.\n\nClick to navigate to the shared note or right click for more options.",
|
||||
"shared_locally": "Shared locally",
|
||||
"shared_locally_description": "This note is shared on the local network only at {{- link}}.\n\nClick to navigate to the shared note or right click for more options.",
|
||||
"backlinks_one": "{{count}} backlink",
|
||||
"backlinks_other": "{{count}} backlinks",
|
||||
"backlinks_description_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.",
|
||||
"backlinks_description_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks.",
|
||||
"clipped_note": "Web clip",
|
||||
"clipped_note_description": "This note was originally taken from {{url}}.\n\nClick to navigate to the source webpage.",
|
||||
"execute_script": "Run script",
|
||||
"execute_script_description": "This note is a script note. Click to execute the script.",
|
||||
"execute_sql": "Run SQL",
|
||||
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query."
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "Change the language of the entire content",
|
||||
"note_info_title": "View information about this note such as the creation/modification date or the note size.",
|
||||
"backlinks_title_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.",
|
||||
"backlinks_title_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks.",
|
||||
"attachments_title_one": "This note has {{count}} attachment. Click to open the list of attachments in a new tab.",
|
||||
"attachments_title_other": "This note has {{count}} attachments. Click to open the list of attachments in a new tab.",
|
||||
"attributes_one": "{{count}} attribute",
|
||||
"attributes_other": "{{count}} attributes",
|
||||
"attributes_title": "Click to open a dedicated pane to edit this note's owned attributes, as well as to see the list of inherited attributes.",
|
||||
"note_paths_title": "Click to see the paths where this note is placed into the tree.",
|
||||
"code_note_switcher": "Change language mode"
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,6 @@
|
||||
&.read-only-badge { --color: #e33f3b; }
|
||||
&.share-badge { --color: #3b82f6; }
|
||||
&.clipped-note-badge { --color: #57a2a5; }
|
||||
&.backlinks-badge { color: var(--badge-text-color); }
|
||||
&.execute-badge { --color: #f59e0b; }
|
||||
|
||||
a {
|
||||
@ -61,30 +60,6 @@
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
&.dropdown-note-info-badge {
|
||||
.dropdown-menu.show ul {
|
||||
list-style-type: none;
|
||||
padding: 0.5em;
|
||||
margin: 0;
|
||||
display: table;
|
||||
|
||||
li {
|
||||
display: table-row;
|
||||
|
||||
> strong {
|
||||
display: table-cell;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: table-cell;
|
||||
user-select: text;
|
||||
padding-left: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-badge {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@ -5,56 +5,22 @@ import { ComponentChildren, MouseEventHandler } from "preact";
|
||||
import { useRef } from "preact/hooks";
|
||||
|
||||
import { t } from "../services/i18n";
|
||||
import { formatDateTime } from "../utils/formatters";
|
||||
import { BacklinksList, useBacklinkCount } from "./FloatingButtonsDefinitions";
|
||||
import Dropdown, { DropdownProps } from "./react/Dropdown";
|
||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "./react/hooks";
|
||||
import Icon from "./react/Icon";
|
||||
import { NoteSizeWidget, useNoteMetadata } from "./ribbon/NoteInfoTab";
|
||||
import { useShareInfo } from "./shared_info";
|
||||
import FNote from "../entities/fnote";
|
||||
|
||||
export default function BreadcrumbBadges() {
|
||||
return (
|
||||
<div className="breadcrumb-badges">
|
||||
<ReadOnlyBadge />
|
||||
<ShareBadge />
|
||||
<BacklinksBadge />
|
||||
<ClippedNoteBadge />
|
||||
<ExecuteBadge />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) {
|
||||
const { metadata, ...sizeProps } = useNoteMetadata(note);
|
||||
|
||||
return (note &&
|
||||
<BadgeWithDropdown
|
||||
icon="bx bx-info-circle"
|
||||
className="note-info-badge"
|
||||
dropdownOptions={{ dropdownOptions: { autoClose: "outside" } }}
|
||||
>
|
||||
<ul>
|
||||
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
|
||||
<NoteInfoValue text={t("note_info_widget.modified")} value={formatDateTime(metadata?.dateModified)} />
|
||||
<NoteInfoValue text={t("note_info_widget.type")} value={<span>{note.type} {note.mime && <span>({note.mime})</span>}</span>} />
|
||||
<NoteInfoValue text={t("note_info_widget.note_id")} value={<code>{note.noteId}</code>} />
|
||||
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
|
||||
</ul>
|
||||
</BadgeWithDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteInfoValue({ text, title, value }: { text: string; title?: string, value: ComponentChildren }) {
|
||||
return (
|
||||
<li>
|
||||
<strong title={title}>{text}{": "}</strong>
|
||||
<span>{value}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function ReadOnlyBadge() {
|
||||
const { note, noteContext } = useNoteContext();
|
||||
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
||||
@ -98,24 +64,6 @@ function ShareBadge() {
|
||||
);
|
||||
}
|
||||
|
||||
function BacklinksBadge() {
|
||||
const { note, viewScope } = useNoteContext();
|
||||
const count = useBacklinkCount(note, viewScope?.viewMode === "default");
|
||||
return (note && count > 0 &&
|
||||
<BadgeWithDropdown
|
||||
className="backlinks-badge backlinks-widget"
|
||||
icon="bx bx-revision"
|
||||
text={t("breadcrumb_badges.backlinks", { count })}
|
||||
tooltip={t("breadcrumb_badges.backlinks_description", { count })}
|
||||
dropdownOptions={{
|
||||
dropdownContainerClassName: "backlinks-items"
|
||||
}}
|
||||
>
|
||||
<BacklinksList note={note} />
|
||||
</BadgeWithDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function ClippedNoteBadge() {
|
||||
const { note } = useNoteContext();
|
||||
const [ pageUrl ] = useNoteLabel(note, "pageUrl");
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
.note-status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline: 1em;
|
||||
|
||||
.dropdown {
|
||||
font-size: 0.85em;
|
||||
|
||||
.dropdown-toggle {
|
||||
padding: 0.1em 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import "./NoteStatusBar.css";
|
||||
|
||||
import { t } from "../services/i18n";
|
||||
import { openInAppHelpFromUrl } from "../services/utils";
|
||||
import { FormListItem } from "./react/FormList";
|
||||
import { useNoteContext } from "./react/hooks";
|
||||
import { NoteLanguageSelector } from "./ribbon/BasicPropertiesTab";
|
||||
|
||||
export default function NoteStatusBar() {
|
||||
const { note } = useNoteContext();
|
||||
|
||||
return (
|
||||
<div className="note-status-bar">
|
||||
<NoteLanguageSelector
|
||||
note={note}
|
||||
extraChildren={(
|
||||
<FormListItem
|
||||
onClick={() => openInAppHelpFromUrl("veGu4faJErEM")}
|
||||
icon="bx bx-help-circle"
|
||||
>{t("note_language.help-on-languages")}</FormListItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -3,11 +3,12 @@ import { useNoteContext, useNoteProperty } from "./react/hooks";
|
||||
|
||||
export default function NoteTitleDetails() {
|
||||
const { note } = useNoteContext();
|
||||
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
|
||||
return (
|
||||
<div className="title-details">
|
||||
{note && noteType === "book" && <CollectionProperties note={note} />}
|
||||
{note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -104,10 +104,12 @@ function BrowserOnlyOptions() {
|
||||
}
|
||||
|
||||
function DevelopmentOptions() {
|
||||
const [ layoutOrientation ] = useTriliumOption("layoutOrientation");
|
||||
|
||||
return <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem disabled>Development Options</FormListItem>
|
||||
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features">
|
||||
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={layoutOrientation === "horizontal"}>
|
||||
{experimentalFeatures.map((feature) => (
|
||||
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />
|
||||
))}
|
||||
|
||||
@ -49,7 +49,7 @@ export default class ScrollingContainer extends Container<BasicWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
scrollContainerToCommand({ position }: CommandListenerData<"scrollContainerToCommand">) {
|
||||
scrollContainerToCommand({ position }: CommandListenerData<"scrollContainerTo">) {
|
||||
this.$widget.scrollTop(position);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,60 +1,6 @@
|
||||
.breadcrumb-row {
|
||||
.breadcrumb {
|
||||
position: relative;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
container-type: inline-size;
|
||||
|
||||
@container (max-width: 700px) {
|
||||
.breadcrumb-badges {
|
||||
flex-shrink: 0;
|
||||
|
||||
>* {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-badge {
|
||||
flex-shrink: 0;
|
||||
padding: 0 2px;
|
||||
|
||||
>* {
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
.breadcrumb {
|
||||
.btn.icon-action {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-action {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.experimental-feature-new-layout .breadcrumb-row {
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
.component.breadcrumb {
|
||||
contain: none;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
@ -62,7 +8,6 @@ body.experimental-feature-new-layout .breadcrumb-row {
|
||||
gap: 0.25em;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 85%;
|
||||
|
||||
> span,
|
||||
> span > span {
|
||||
@ -108,7 +53,6 @@ body.experimental-feature-new-layout .breadcrumb-row {
|
||||
.breadcrumb-last-item {
|
||||
text-decoration: none;
|
||||
color: unset;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
input {
|
||||
@ -3,25 +3,23 @@ import "./Breadcrumb.css";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import { Fragment } from "preact/jsx-runtime";
|
||||
|
||||
import NoteContext from "../components/note_context";
|
||||
import froca from "../services/froca";
|
||||
import ActionButton from "./react/ActionButton";
|
||||
import Dropdown from "./react/Dropdown";
|
||||
import { FormListItem } from "./react/FormList";
|
||||
import { useChildNotes, useNoteContext, useNoteLabel, useNoteProperty } from "./react/hooks";
|
||||
import Icon from "./react/Icon";
|
||||
import NoteLink from "./react/NoteLink";
|
||||
import link_context_menu from "../menus/link_context_menu";
|
||||
import { TitleEditor } from "./collections/board";
|
||||
import server from "../services/server";
|
||||
import { NoteInfoBadge } from "./BreadcrumbBadges";
|
||||
import appContext from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import link_context_menu from "../../menus/link_context_menu";
|
||||
import froca from "../../services/froca";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormListItem } from "../react/FormList";
|
||||
import { useChildNotes, useNoteLabel, useNoteProperty } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
|
||||
const COLLAPSE_THRESHOLD = 5;
|
||||
const INITIAL_ITEMS = 2;
|
||||
const FINAL_ITEMS = 2;
|
||||
|
||||
export default function Breadcrumb() {
|
||||
const { note, noteContext } = useNoteContext();
|
||||
export default function Breadcrumb({ note, noteContext }: { note: FNote, noteContext: NoteContext }) {
|
||||
const notePath = buildNotePaths(noteContext?.notePathArray);
|
||||
|
||||
return (
|
||||
@ -65,6 +63,7 @@ function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined
|
||||
|
||||
return (note &&
|
||||
<ActionButton
|
||||
className="root-note"
|
||||
icon={note.getIcon()}
|
||||
text={title ?? ""}
|
||||
onClick={() => noteContext?.setNote("root")}
|
||||
@ -87,30 +86,20 @@ function BreadcrumbLink({ notePath }: { notePath: string }) {
|
||||
function BreadcrumbLastItem({ notePath }: { notePath: string }) {
|
||||
const noteId = notePath.split("/").at(-1);
|
||||
const [ note ] = useState(() => froca.getNoteFromCache(noteId!));
|
||||
const [ isEditing, setIsEditing ] = useState(false);
|
||||
const title = useNoteProperty(note, "title");
|
||||
|
||||
if (!note) return null;
|
||||
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className="breadcrumb-last-item tn-link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsEditing(true);
|
||||
}}
|
||||
>{title}</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TitleEditor
|
||||
currentValue={title}
|
||||
save={(newTitle) => { return server.put(`notes/${noteId}/title`, { title: newTitle.trim() }); }}
|
||||
dismiss={() => setIsEditing(false)}
|
||||
/>
|
||||
<a
|
||||
href="#"
|
||||
className="breadcrumb-last-item tn-link"
|
||||
onClick={() => {
|
||||
const activeNtxId = appContext.tabManager.activeNtxId;
|
||||
const scrollingContainer = document.querySelector(`[data-ntx-id="${activeNtxId}"] .scrolling-container`);
|
||||
scrollingContainer?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}}
|
||||
>{title}</a>
|
||||
);
|
||||
}
|
||||
|
||||
@ -122,7 +111,6 @@ function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { inde
|
||||
if (index === notePathLength - 1) {
|
||||
return <>
|
||||
<BreadcrumbLastItem notePath={notePath} />
|
||||
<NoteInfoBadge note={noteContext?.note} />
|
||||
</>;
|
||||
}
|
||||
|
||||
@ -136,7 +124,7 @@ function BreadcrumbSeparator({ notePath, noteContext, activeNotePath }: { notePa
|
||||
noSelectButtonStyle
|
||||
buttonClassName="icon-action"
|
||||
hideToggleArrow
|
||||
dropdownOptions={{ popperConfig: { strategy: "fixed" } }}
|
||||
dropdownOptions={{ popperConfig: { strategy: "fixed", placement: "top" } }}
|
||||
>
|
||||
<BreadcrumbSeparatorDropdownContent notePath={notePath} noteContext={noteContext} activeNotePath={activeNotePath} />
|
||||
</Dropdown>
|
||||
114
apps/client/src/widgets/layout/StatusBar.css
Normal file
114
apps/client/src/widgets/layout/StatusBar.css
Normal file
@ -0,0 +1,114 @@
|
||||
.component.status-bar {
|
||||
contain: none;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
background-color: var(--left-pane-background-color);
|
||||
|
||||
> .status-bar-main-row {
|
||||
min-height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline: 0.25em;
|
||||
font-size: 0.85em;
|
||||
|
||||
> .breadcrumb {
|
||||
flex-grow: 1;
|
||||
--icon-button-size: 23px;
|
||||
}
|
||||
|
||||
> .actions-row {
|
||||
padding: 0.1em;
|
||||
display: flex;
|
||||
gap: 0.1em;
|
||||
|
||||
.btn {
|
||||
padding: 0 0.5em !important;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
|
||||
span:first-of-type {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.active,
|
||||
&.dropdown-toggle.show,
|
||||
&:focus,
|
||||
&:hover {
|
||||
background: var(--input-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-bar-dropdown-button {
|
||||
&:after {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.dropdown-toggle {
|
||||
padding: 0.1em 0.25em;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-note-info {
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0.5em;
|
||||
margin: 0;
|
||||
display: table;
|
||||
|
||||
li {
|
||||
display: table-row;
|
||||
|
||||
> strong {
|
||||
display: table-cell;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
> span {
|
||||
display: table-cell;
|
||||
user-select: text;
|
||||
padding-left: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-note-paths {
|
||||
.note-paths-widget {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.note-path-list {
|
||||
margin: 1em;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-code-note-switcher {
|
||||
max-height: 90vh;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
> .attribute-list {
|
||||
font-size: 0.9em;
|
||||
padding: 0.5em 0.75em;
|
||||
|
||||
.inherited-attributes-widget > div {
|
||||
padding: 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.attribute-list-editor {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
382
apps/client/src/widgets/layout/StatusBar.tsx
Normal file
382
apps/client/src/widgets/layout/StatusBar.tsx
Normal file
@ -0,0 +1,382 @@
|
||||
import "./StatusBar.css";
|
||||
|
||||
import { Locale } from "@triliumnext/commons";
|
||||
import clsx from "clsx";
|
||||
import { type ComponentChildren } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import { ViewScope } from "../../services/link";
|
||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||
import { formatDateTime } from "../../utils/formatters";
|
||||
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
|
||||
import Dropdown, { DropdownProps } from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useActiveNoteContext, useLegacyImperativeHandlers, useNoteProperty, useStaticTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { ContentLanguagesModal, NoteTypeCodeNoteList, NoteTypeOptionsModal, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab";
|
||||
import AttributeEditor, { AttributeEditorImperativeHandlers } from "../ribbon/components/AttributeEditor";
|
||||
import InheritedAttributesTab from "../ribbon/InheritedAttributesTab";
|
||||
import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab";
|
||||
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
|
||||
import { useAttachments } from "../type_widgets/Attachment";
|
||||
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
|
||||
import Breadcrumb from "./Breadcrumb";
|
||||
import server from "../../services/server";
|
||||
|
||||
interface StatusBarContext {
|
||||
note: FNote;
|
||||
notePath: string | null | undefined;
|
||||
noteContext: NoteContext;
|
||||
viewScope?: ViewScope;
|
||||
hoistedNoteId?: string;
|
||||
}
|
||||
|
||||
export default function StatusBar() {
|
||||
const { note, notePath, noteContext, viewScope, hoistedNoteId } = useActiveNoteContext();
|
||||
const [ attributesShown, setAttributesShown ] = useState(false);
|
||||
const context: StatusBarContext | undefined | null = note && noteContext && { note, notePath, noteContext, viewScope, hoistedNoteId };
|
||||
const attributesContext: AttributesProps | undefined | null = context && { ...context, attributesShown, setAttributesShown };
|
||||
const isHiddenNote = note?.isInHiddenSubtree();
|
||||
|
||||
return (
|
||||
<div className="status-bar">
|
||||
{attributesContext && <AttributesPane {...attributesContext} />}
|
||||
|
||||
<div className="status-bar-main-row">
|
||||
{context && attributesContext && <>
|
||||
<Breadcrumb {...context} />
|
||||
|
||||
<div className="actions-row">
|
||||
<CodeNoteSwitcher {...context} />
|
||||
<LanguageSwitcher {...context} />
|
||||
{!isHiddenNote && <NotePaths {...context} />}
|
||||
<AttributesButton {...attributesContext} />
|
||||
<AttachmentCount {...context} />
|
||||
<BacklinksBadge {...context} />
|
||||
<NoteInfoBadge {...context} />
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions, dropdownOptions, ...dropdownProps }: Omit<DropdownProps, "hideToggleArrow" | "title" | "titlePosition"> & {
|
||||
title: string;
|
||||
icon?: string;
|
||||
}) {
|
||||
return (
|
||||
<Dropdown
|
||||
buttonClassName={clsx("status-bar-dropdown-button", buttonClassName)}
|
||||
titlePosition="top"
|
||||
titleOptions={{
|
||||
popperConfig: {
|
||||
...titleOptions?.popperConfig,
|
||||
strategy: "fixed"
|
||||
},
|
||||
...titleOptions
|
||||
}}
|
||||
dropdownOptions={{
|
||||
autoClose: "outside",
|
||||
popperConfig: {
|
||||
strategy: "fixed",
|
||||
placement: "top"
|
||||
},
|
||||
...dropdownOptions
|
||||
}}
|
||||
text={<>
|
||||
{icon && (<><Icon icon={icon} /> </>)}
|
||||
{text}
|
||||
</>}
|
||||
{...dropdownProps}
|
||||
>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
interface StatusBarButtonBaseProps {
|
||||
className?: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
text?: string | number;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
type StatusBarButtonWithCommand = StatusBarButtonBaseProps & { triggerCommand: CommandNames; };
|
||||
type StatusBarButtonWithClick = StatusBarButtonBaseProps & { onClick: () => void; };
|
||||
|
||||
function StatusBarButton({ className, icon, text, title, active, ...restProps }: StatusBarButtonWithCommand | StatusBarButtonWithClick) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
useStaticTooltip(buttonRef, {
|
||||
placement: "top",
|
||||
fallbackPlacements: [ "top" ],
|
||||
popperConfig: { strategy: "fixed" },
|
||||
title
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
className={clsx("btn select-button focus-outline", className, active && "active")}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if ("triggerCommand" in restProps) {
|
||||
parentComponent?.triggerCommand(restProps.triggerCommand);
|
||||
} else {
|
||||
restProps.onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={icon} /> {text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
//#region Language Switcher
|
||||
function LanguageSwitcher({ note }: StatusBarContext) {
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
|
||||
const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id);
|
||||
|
||||
return (
|
||||
<>
|
||||
{note.type === "text" && <StatusBarDropdown
|
||||
icon="bx bx-globe"
|
||||
title={t("status_bar.language_title")}
|
||||
text={<span dir={activeLocale?.rtl ? "rtl" : "ltr"}>{getLocaleName(activeLocale)}</span>}
|
||||
>
|
||||
{processedLocales.map((locale, index) =>
|
||||
(typeof locale === "object") ? (
|
||||
<FormListItem
|
||||
key={locale.id}
|
||||
rtl={locale.rtl}
|
||||
checked={locale.id === currentNoteLanguage}
|
||||
onClick={() => setCurrentNoteLanguage(locale.id)}
|
||||
>{locale.name}</FormListItem>
|
||||
) : (
|
||||
<FormDropdownDivider key={`divider-${index}`} />
|
||||
)
|
||||
)}
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
onClick={() => openInAppHelpFromUrl("veGu4faJErEM")}
|
||||
icon="bx bx-help-circle"
|
||||
>{t("note_language.help-on-languages")}</FormListItem>
|
||||
<FormListItem
|
||||
onClick={() => setModalShown(true)}
|
||||
icon="bx bx-cog"
|
||||
>{t("note_language.configure-languages")}</FormListItem>
|
||||
</StatusBarDropdown>}
|
||||
{createPortal(
|
||||
<ContentLanguagesModal modalShown={modalShown} setModalShown={setModalShown} />,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function getLocaleName(locale: Locale | null | undefined) {
|
||||
if (!locale) return "";
|
||||
if (!locale.id) return "-";
|
||||
if (locale.name.length <= 4 || locale.rtl) return locale.name; // Some locales like Japanese and Chinese look better than their ID.
|
||||
return locale.id
|
||||
.replace("_", "-")
|
||||
.toLocaleUpperCase();
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Note info
|
||||
export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) {
|
||||
const { metadata, ...sizeProps } = useNoteMetadata(note);
|
||||
|
||||
return (note &&
|
||||
<StatusBarDropdown
|
||||
icon="bx bx-info-circle"
|
||||
title={t("status_bar.note_info_title")}
|
||||
dropdownContainerClassName="dropdown-note-info"
|
||||
>
|
||||
<ul>
|
||||
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
|
||||
<NoteInfoValue text={t("note_info_widget.modified")} value={formatDateTime(metadata?.dateModified)} />
|
||||
<NoteInfoValue text={t("note_info_widget.type")} value={<span>{note.type} {note.mime && <span>({note.mime})</span>}</span>} />
|
||||
<NoteInfoValue text={t("note_info_widget.note_id")} value={<code>{note.noteId}</code>} />
|
||||
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
|
||||
</ul>
|
||||
</StatusBarDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteInfoValue({ text, title, value }: { text: string; title?: string, value: ComponentChildren }) {
|
||||
return (
|
||||
<li>
|
||||
<strong title={title}>{text}{": "}</strong>
|
||||
<span>{value}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Backlinks
|
||||
function BacklinksBadge({ note, viewScope }: StatusBarContext) {
|
||||
const count = useBacklinkCount(note, viewScope?.viewMode === "default");
|
||||
return (note && count > 0 &&
|
||||
<StatusBarDropdown
|
||||
className="backlinks-badge backlinks-widget"
|
||||
icon="bx bx-revision"
|
||||
text={count}
|
||||
title={t("status_bar.backlinks_title", { count })}
|
||||
dropdownContainerClassName="backlinks-items"
|
||||
>
|
||||
<BacklinksList note={note} />
|
||||
</StatusBarDropdown>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Attachment count
|
||||
function AttachmentCount({ note }: StatusBarContext) {
|
||||
const attachments = useAttachments(note);
|
||||
const count = attachments.length;
|
||||
|
||||
return (note && count > 0 &&
|
||||
<StatusBarButton
|
||||
className="attachment-count-button"
|
||||
icon="bx bx-paperclip"
|
||||
text={count}
|
||||
title={t("status_bar.attachments_title", { count })}
|
||||
triggerCommand="showAttachments"
|
||||
/>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Attributes
|
||||
interface AttributesProps extends StatusBarContext {
|
||||
attributesShown: boolean;
|
||||
setAttributesShown: (shown: boolean) => void;
|
||||
}
|
||||
|
||||
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
|
||||
const [ count, setCount ] = useState(note.attributes.length);
|
||||
|
||||
// React to note changes.
|
||||
useEffect(() => {
|
||||
setCount(note.attributes.length);
|
||||
}, [ note ]);
|
||||
|
||||
// React to changes in count.
|
||||
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
|
||||
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
||||
setCount(note.attributes.length);
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<StatusBarButton
|
||||
className="attributes-button"
|
||||
icon="bx bx-list-check"
|
||||
title={t("status_bar.attributes_title")}
|
||||
text={t("status_bar.attributes", { count })}
|
||||
active={attributesShown}
|
||||
onClick={() => setAttributesShown(!attributesShown)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AttributesPane({ note, noteContext, attributesShown, setAttributesShown }: AttributesProps) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const api = useRef<AttributeEditorImperativeHandlers>(null);
|
||||
|
||||
const context = parentComponent && {
|
||||
componentId: parentComponent.componentId,
|
||||
note,
|
||||
hidden: !note
|
||||
};
|
||||
|
||||
// Show on keyboard shortcuts.
|
||||
useTriliumEvents([ "addNewLabel", "addNewRelation" ], () => setAttributesShown(true));
|
||||
|
||||
// Interaction with the attribute editor.
|
||||
useLegacyImperativeHandlers(useMemo(() => ({
|
||||
saveAttributesCommand: () => api.current?.save(),
|
||||
reloadAttributesCommand: () => api.current?.refresh(),
|
||||
updateAttributeListCommand: ({ attributes }) => api.current?.renderOwnedAttributes(attributes)
|
||||
}), [ api ]));
|
||||
|
||||
return (context &&
|
||||
<div className={clsx("attribute-list", !attributesShown && "hidden-ext")}>
|
||||
<InheritedAttributesTab {...context} />
|
||||
|
||||
<AttributeEditor
|
||||
{...context}
|
||||
api={api}
|
||||
ntxId={noteContext.ntxId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Note paths
|
||||
function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) {
|
||||
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
||||
|
||||
return (
|
||||
<StatusBarDropdown
|
||||
title={t("status_bar.note_paths_title")}
|
||||
dropdownContainerClassName="dropdown-note-paths"
|
||||
icon="bx bx-link-alt"
|
||||
text={sortedNotePaths?.length}
|
||||
>
|
||||
<NotePathsWidget
|
||||
sortedNotePaths={sortedNotePaths}
|
||||
currentNotePath={notePath}
|
||||
/>
|
||||
</StatusBarDropdown>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Code note switcher
|
||||
function CodeNoteSwitcher({ note }: StatusBarContext) {
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
const currentNoteMime = useNoteProperty(note, "mime");
|
||||
const mimeTypes = useMimeTypes();
|
||||
const correspondingMimeType = useMemo(() => (
|
||||
mimeTypes.find(m => m.mime === currentNoteMime)
|
||||
), [ mimeTypes, currentNoteMime ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusBarDropdown
|
||||
icon="bx bx-code-curly"
|
||||
text={correspondingMimeType?.title}
|
||||
title={t("status_bar.code_note_switcher")}
|
||||
dropdownContainerClassName="dropdown-code-note-switcher"
|
||||
dropdownOptions={{ autoClose: true }}
|
||||
>
|
||||
<NoteTypeCodeNoteList
|
||||
currentMimeType={currentNoteMime}
|
||||
mimeTypes={mimeTypes}
|
||||
changeNoteType={(type, mime) => server.put(`notes/${note.noteId}/type`, { type, mime })}
|
||||
setModalShown={() => setModalShown(true)}
|
||||
/>
|
||||
</StatusBarDropdown>
|
||||
{createPortal(
|
||||
<NoteTypeOptionsModal modalShown={modalShown} setModalShown={setModalShown} />,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
@ -316,7 +316,7 @@ export function useNoteContext() {
|
||||
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
|
||||
|
||||
return {
|
||||
note: note,
|
||||
note,
|
||||
noteId: noteContext?.note?.noteId,
|
||||
notePath: noteContext?.notePath,
|
||||
hoistedNoteId: noteContext?.hoistedNoteId,
|
||||
@ -327,7 +327,65 @@ export function useNoteContext() {
|
||||
parentComponent,
|
||||
isReadOnlyTemporarilyDisabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link useNoteContext}, but instead of using the note context from the split container that the component is part of, it uses the active note context instead
|
||||
* (the note currently focused by the user).
|
||||
*/
|
||||
export function useActiveNoteContext() {
|
||||
const [ noteContext, setNoteContext ] = useState<NoteContext | undefined>(appContext.tabManager.getActiveContext() ?? undefined);
|
||||
const [ notePath, setNotePath ] = useState<string | null | undefined>();
|
||||
const [ note, setNote ] = useState<FNote | null | undefined>();
|
||||
const [ , setViewScope ] = useState<ViewScope>();
|
||||
const [ isReadOnlyTemporarilyDisabled, setIsReadOnlyTemporarilyDisabled ] = useState<boolean | null | undefined>(noteContext?.viewScope?.isReadOnly);
|
||||
const [ refreshCounter, setRefreshCounter ] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noteContext) {
|
||||
setNoteContext(appContext.tabManager.getActiveContext() ?? undefined);
|
||||
}
|
||||
}, [ noteContext ]);
|
||||
|
||||
useEffect(() => {
|
||||
setNote(noteContext?.note);
|
||||
}, [ notePath ]);
|
||||
|
||||
useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], () => {
|
||||
const noteContext = appContext.tabManager.getActiveContext() ?? undefined;
|
||||
setNoteContext(noteContext);
|
||||
setNotePath(noteContext?.notePath);
|
||||
setViewScope(noteContext?.viewScope);
|
||||
});
|
||||
useTriliumEvent("frocaReloaded", () => {
|
||||
setNote(noteContext?.note);
|
||||
});
|
||||
useTriliumEvent("noteTypeMimeChanged", ({ noteId }) => {
|
||||
if (noteId === note?.noteId) {
|
||||
setRefreshCounter(refreshCounter + 1);
|
||||
}
|
||||
});
|
||||
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
|
||||
if (eventNoteContext.ntxId === noteContext?.ntxId) {
|
||||
setIsReadOnlyTemporarilyDisabled(eventNoteContext?.viewScope?.readOnlyTemporarilyDisabled);
|
||||
}
|
||||
});
|
||||
|
||||
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
|
||||
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
|
||||
|
||||
return {
|
||||
note,
|
||||
noteId: noteContext?.note?.noteId,
|
||||
notePath: noteContext?.notePath,
|
||||
hoistedNoteId: noteContext?.hoistedNoteId,
|
||||
ntxId: noteContext?.ntxId,
|
||||
viewScope: noteContext?.viewScope,
|
||||
componentId: parentComponent.componentId,
|
||||
noteContext,
|
||||
parentComponent,
|
||||
isReadOnlyTemporarilyDisabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { NoteType, ToggleInParentResponse } from "@triliumnext/commons";
|
||||
import { MimeType, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
||||
@ -65,13 +65,15 @@ function NoteTypeWidget({ note }: { note?: FNote | null }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note, setModalShown }: { currentNoteType?: NoteType, currentNoteMime?: string | null, note?: FNote | null, setModalShown: Dispatch<StateUpdater<boolean>> }) {
|
||||
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
|
||||
export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note, setModalShown, noCodeNotes }: {
|
||||
currentNoteType?: NoteType;
|
||||
currentNoteMime?: string | null;
|
||||
note?: FNote | null;
|
||||
setModalShown: Dispatch<StateUpdater<boolean>>;
|
||||
noCodeNotes?: boolean;
|
||||
}) {
|
||||
const mimeTypes = useMimeTypes();
|
||||
const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []);
|
||||
const mimeTypes = useMemo(() => {
|
||||
mime_types.loadMimeTypes();
|
||||
return mime_types.getMimeTypes().filter(mimeType => mimeType.enabled);
|
||||
}, [ codeNotesMimeTypes ]);
|
||||
const changeNoteType = useCallback(async (type: NoteType, mime?: string) => {
|
||||
if (!note || (type === currentNoteType && mime === currentNoteMime)) {
|
||||
return;
|
||||
@ -107,7 +109,7 @@ export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note
|
||||
}
|
||||
|
||||
const checked = (type === currentNoteType);
|
||||
if (type !== "code") {
|
||||
if (noCodeNotes || type !== "code") {
|
||||
return (
|
||||
<FormListItem
|
||||
checked={checked}
|
||||
@ -130,8 +132,25 @@ export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note
|
||||
}
|
||||
})}
|
||||
|
||||
{!noCodeNotes && <NoteTypeCodeNoteList mimeTypes={mimeTypes} changeNoteType={changeNoteType} setModalShown={setModalShown} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteType, setModalShown }: {
|
||||
currentMimeType?: string;
|
||||
mimeTypes: MimeType[];
|
||||
changeNoteType(type: NoteType, mime: string): void;
|
||||
setModalShown(shown: boolean): void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{mimeTypes.map(({ title, mime }) => (
|
||||
<FormListItem onClick={() => changeNoteType("code", mime)}>
|
||||
<FormListItem
|
||||
key={mime}
|
||||
checked={mime === currentMimeType}
|
||||
onClick={() => changeNoteType("code", mime)}
|
||||
>
|
||||
{title}
|
||||
</FormListItem>
|
||||
))}
|
||||
@ -142,7 +161,16 @@ export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note
|
||||
);
|
||||
}
|
||||
|
||||
function NoteTypeOptionsModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
|
||||
export function useMimeTypes() {
|
||||
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
|
||||
const mimeTypes = useMemo(() => {
|
||||
mime_types.loadMimeTypes();
|
||||
return mime_types.getMimeTypes().filter(mimeType => mimeType.enabled);
|
||||
}, [ codeNotesMimeTypes ]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
return mimeTypes;
|
||||
}
|
||||
|
||||
export function NoteTypeOptionsModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
|
||||
return (
|
||||
<Modal
|
||||
className="code-mime-types-modal"
|
||||
@ -330,28 +358,17 @@ function NoteLanguageSwitch({ note }: { note?: FNote | null }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function NoteLanguageSelector({ note, extraChildren }: { note: FNote | null | undefined, extraChildren?: ComponentChildren }) {
|
||||
export function NoteLanguageSelector({ note }: { note: FNote | null | undefined }) {
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
const [ languages ] = useTriliumOption("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));
|
||||
return filteredLanguages;
|
||||
}, [ languages ]);
|
||||
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LocaleSelector
|
||||
locales={locales}
|
||||
defaultLocale={DEFAULT_LOCALE}
|
||||
currentValue={currentNoteLanguage ?? ""} onChange={setCurrentNoteLanguage}
|
||||
currentValue={currentNoteLanguage} onChange={setCurrentNoteLanguage}
|
||||
extraChildren={<>
|
||||
{extraChildren}
|
||||
<FormListItem
|
||||
onClick={() => setModalShown(true)}
|
||||
icon="bx bx-cog"
|
||||
@ -366,7 +383,22 @@ export function NoteLanguageSelector({ note, extraChildren }: { note: FNote | nu
|
||||
);
|
||||
}
|
||||
|
||||
function ContentLanguagesModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
|
||||
export function useLanguageSwitcher(note: FNote | null | undefined) {
|
||||
const [ languages ] = useTriliumOption("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));
|
||||
return filteredLanguages;
|
||||
}, [ languages ]);
|
||||
return { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage };
|
||||
}
|
||||
|
||||
export function ContentLanguagesModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) {
|
||||
return (
|
||||
<Modal
|
||||
className="content-languages-modal"
|
||||
|
||||
@ -9,7 +9,7 @@ import RawHtml from "../react/RawHtml";
|
||||
import { joinElements } from "../react/react_utils";
|
||||
import AttributeDetailWidget from "../attribute_widgets/attribute_detail";
|
||||
|
||||
export default function InheritedAttributesTab({ note, componentId }: TabContext) {
|
||||
export default function InheritedAttributesTab({ note, componentId }: Pick<TabContext, "note" | "componentId">) {
|
||||
const [ inheritedAttributes, setInheritedAttributes ] = useState<FAttribute[]>();
|
||||
const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget());
|
||||
|
||||
@ -34,7 +34,7 @@ export default function InheritedAttributesTab({ note, componentId }: TabContext
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<div className="inherited-attributes-widget">
|
||||
<div className="inherited-attributes-container selectable-text">
|
||||
@ -83,4 +83,4 @@ function InheritedAttribute({ attribute, onClick }: { attribute: FAttribute, onC
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,11 +184,16 @@ function EditabilityDropdown({ note }: { note: FNote }) {
|
||||
function NoteTypeDropdown({ note }: { note: FNote }) {
|
||||
const currentNoteType = useNoteProperty(note, "type") ?? undefined;
|
||||
const currentNoteMime = useNoteProperty(note, "mime");
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
|
||||
return (
|
||||
<FormDropdownSubmenu title={t("basic_properties.note_type")} icon="bx bx-file" dropStart>
|
||||
<NoteTypeDropdownContent currentNoteType={currentNoteType} currentNoteMime={currentNoteMime} note={note} setModalShown={setModalShown} />
|
||||
<NoteTypeDropdownContent
|
||||
currentNoteType={currentNoteType}
|
||||
currentNoteMime={currentNoteMime}
|
||||
note={note}
|
||||
setModalShown={() => { /* no-op since no code notes are displayed here */ }}
|
||||
noCodeNotes
|
||||
/>
|
||||
</FormDropdownSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,33 +1,23 @@
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
|
||||
import FNote, { NotePathRecord } from "../../entities/fnote";
|
||||
import { t } from "../../services/i18n";
|
||||
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
|
||||
import Button from "../react/Button";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { NotePathRecord } from "../../entities/fnote";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { joinElements } from "../react/react_utils";
|
||||
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
|
||||
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
|
||||
const [ sortedNotePaths, setSortedNotePaths ] = useState<NotePathRecord[]>();
|
||||
|
||||
function refresh() {
|
||||
if (!note) return;
|
||||
setSortedNotePaths(note
|
||||
.getSortedNotePathRecords(hoistedNoteId)
|
||||
.filter((notePath) => !notePath.isHidden));
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note?.noteId ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
const noteId = note?.noteId;
|
||||
if (!noteId) return;
|
||||
if (loadResults.getBranchRows().find((branch) => branch.noteId === noteId)
|
||||
|| loadResults.isNoteReloaded(noteId)) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
||||
return <NotePathsWidget sortedNotePaths={sortedNotePaths} currentNotePath={notePath} />;
|
||||
}
|
||||
|
||||
export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
|
||||
sortedNotePaths: NotePathRecord[] | undefined;
|
||||
currentNotePath?: string | null | undefined;
|
||||
}) {
|
||||
return (
|
||||
<div class="note-paths-widget">
|
||||
<>
|
||||
@ -38,7 +28,8 @@ export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabConte
|
||||
<ul className="note-path-list">
|
||||
{sortedNotePaths?.length ? sortedNotePaths.map(sortedNotePath => (
|
||||
<NotePath
|
||||
currentNotePath={notePath}
|
||||
key={sortedNotePath.notePath}
|
||||
currentNotePath={currentNotePath}
|
||||
notePathRecord={sortedNotePath}
|
||||
/>
|
||||
)) : undefined}
|
||||
@ -50,12 +41,35 @@ export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabConte
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function useSortedNotePaths(note: FNote | null | undefined, hoistedNoteId?: string) {
|
||||
const [ sortedNotePaths, setSortedNotePaths ] = useState<NotePathRecord[]>();
|
||||
|
||||
function refresh() {
|
||||
if (!note) return;
|
||||
setSortedNotePaths(note
|
||||
.getSortedNotePathRecords(hoistedNoteId)
|
||||
.filter((notePath) => !notePath.isHidden));
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note, hoistedNoteId ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
const noteId = note?.noteId;
|
||||
if (!noteId) return;
|
||||
if (loadResults.getBranchRows().find((branch) => branch.noteId === noteId)
|
||||
|| loadResults.isNoteReloaded(noteId)) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return sortedNotePaths;
|
||||
}
|
||||
|
||||
function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: string | null, notePathRecord?: NotePathRecord }) {
|
||||
const notePath = notePathRecord?.notePath ?? [];
|
||||
const notePathString = useMemo(() => notePath.join("/"), [ notePath ]);
|
||||
const notePath = notePathRecord?.notePath;
|
||||
const notePathString = useMemo(() => (notePath ?? []).join("/"), [ notePath ]);
|
||||
|
||||
const [ classes, icons ] = useMemo(() => {
|
||||
const classes: string[] = [];
|
||||
@ -68,17 +82,17 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
||||
if (!notePathRecord || notePathRecord.isInHoistedSubTree) {
|
||||
classes.push("path-in-hoisted-subtree");
|
||||
} else {
|
||||
icons.push({ icon: "bx bx-trending-up", title: t("note_paths.outside_hoisted") })
|
||||
icons.push({ icon: "bx bx-trending-up", title: t("note_paths.outside_hoisted") });
|
||||
}
|
||||
|
||||
if (notePathRecord?.isArchived) {
|
||||
classes.push("path-archived");
|
||||
icons.push({ icon: "bx bx-archive", title: t("note_paths.archived") })
|
||||
icons.push({ icon: "bx bx-archive", title: t("note_paths.archived") });
|
||||
}
|
||||
|
||||
if (notePathRecord?.isSearch) {
|
||||
classes.push("path-search");
|
||||
icons.push({ icon: "bx bx-search", title: t("note_paths.search") })
|
||||
icons.push({ icon: "bx bx-search", title: t("note_paths.search") });
|
||||
}
|
||||
|
||||
return [ classes.join(" "), icons ];
|
||||
@ -87,7 +101,7 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
||||
// Determine the full note path (for the links) of every component of the current note path.
|
||||
const pathSegments: string[] = [];
|
||||
const fullNotePaths: string[] = [];
|
||||
for (const noteId of notePath) {
|
||||
for (const noteId of notePath ?? []) {
|
||||
pathSegments.push(noteId);
|
||||
fullNotePaths.push(pathSegments.join("/"));
|
||||
}
|
||||
@ -95,12 +109,12 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
||||
return (
|
||||
<li class={classes}>
|
||||
{joinElements(fullNotePaths.map(notePath => (
|
||||
<NoteLink notePath={notePath} noPreview />
|
||||
<NoteLink key={notePath} notePath={notePath} noPreview />
|
||||
)), NOTE_PATH_TITLE_SEPARATOR)}
|
||||
|
||||
{icons.map(({ icon, title }) => (
|
||||
<span class={icon} title={title} />
|
||||
<span key={title} class={icon} title={title} />
|
||||
))}
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useMemo, useRef } from "preact/hooks";
|
||||
|
||||
import { useLegacyImperativeHandlers, useTriliumEvents } from "../react/hooks";
|
||||
import AttributeEditor, { AttributeEditorImperativeHandlers } from "./components/AttributeEditor";
|
||||
import { TabContext } from "./ribbon-interface";
|
||||
@ -25,5 +26,5 @@ export default function OwnedAttributesTab({ note, hidden, activate, ntxId, ...r
|
||||
<AttributeEditor api={api} ntxId={ntxId} note={note} {...restProps} hidden={hidden} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useElementSize, useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
|
||||
import "./style.css";
|
||||
|
||||
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
||||
import { EventNames } from "../../components/app_context";
|
||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
|
||||
import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
||||
import clsx from "clsx";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { EventNames } from "../../components/app_context";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
|
||||
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
||||
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
|
||||
import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
||||
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
|
||||
|
||||
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
|
||||
|
||||
@ -45,16 +46,6 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr
|
||||
refresh();
|
||||
}, [ note, noteType, isReadOnlyTemporarilyDisabled ]);
|
||||
|
||||
// Manage height.
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const size = useElementSize(containerRef);
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !size) return;
|
||||
const parentEl = containerRef.current.closest<HTMLDivElement>(".note-split");
|
||||
if (!parentEl) return;
|
||||
parentEl.style.setProperty("--ribbon-height", `${size.height}px`);
|
||||
}, [ size ]);
|
||||
|
||||
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
|
||||
useEffect(() => {
|
||||
if (!computedTabs) return;
|
||||
@ -79,7 +70,6 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr
|
||||
const shouldShowRibbon = (noteContext?.viewScope?.viewMode === "default" && !noteContext.noteId?.startsWith("_options"));
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx("ribbon-container", !shouldShowRibbon && "hidden-ext")}
|
||||
style={{ contain: "none" }}
|
||||
>
|
||||
@ -133,7 +123,7 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: string; title: string; active: boolean, onClick: () => void, toggleCommand?: KeyboardActionNames }) {
|
||||
@ -156,7 +146,7 @@ function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: stri
|
||||
|
||||
<div class="ribbon-tab-spacer" />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function shouldShowTab(showConfig: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined), context: TitleContext) {
|
||||
|
||||
@ -97,22 +97,22 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
title: t("owned_attribute_list.owned_attributes"),
|
||||
icon: "bx bx-list-check",
|
||||
content: OwnedAttributesTab,
|
||||
show: ({note}) => !note?.isLaunchBarConfig(),
|
||||
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
|
||||
toggleCommand: "toggleRibbonTabOwnedAttributes",
|
||||
stayInDom: true
|
||||
stayInDom: !isNewLayout
|
||||
},
|
||||
{
|
||||
title: t("inherited_attribute_list.title"),
|
||||
icon: "bx bx-list-plus",
|
||||
content: InheritedAttributesTab,
|
||||
show: ({note}) => !note?.isLaunchBarConfig(),
|
||||
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
|
||||
toggleCommand: "toggleRibbonTabInheritedAttributes"
|
||||
},
|
||||
{
|
||||
title: t("note_paths.title"),
|
||||
icon: "bx bx-collection",
|
||||
content: NotePathsTab,
|
||||
show: true,
|
||||
show: !isNewLayout,
|
||||
toggleCommand: "toggleRibbonTabNotePaths"
|
||||
},
|
||||
{
|
||||
@ -125,7 +125,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
||||
{
|
||||
title: t("similar_notes.title"),
|
||||
icon: "bx bx-bar-chart",
|
||||
show: ({ note }) => note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
|
||||
show: ({ note }) => !isNewLayout && note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
|
||||
content: SimilarNotesTab,
|
||||
toggleCommand: "toggleRibbonTabSimilarNotes"
|
||||
},
|
||||
|
||||
@ -355,6 +355,10 @@ body[dir=rtl] .attribute-list-editor {
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
padding: 14px 12px 13px 12px;
|
||||
|
||||
a.reference-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
@ -418,6 +422,10 @@ body[dir=rtl] .attribute-list-editor {
|
||||
|
||||
/* #region Experimental layout */
|
||||
body.experimental-feature-new-layout {
|
||||
.ribbon-top-row {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ribbon-container {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
|
||||
@ -26,24 +26,13 @@ import ws from "../../services/ws";
|
||||
import appContext from "../../components/app_context";
|
||||
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
|
||||
import options from "../../services/options";
|
||||
import FNote from "../../entities/fnote";
|
||||
|
||||
/**
|
||||
* Displays the full list of attachments of a note and allows the user to interact with them.
|
||||
*/
|
||||
export function AttachmentList({ note }: TypeWidgetProps) {
|
||||
const [ attachments, setAttachments ] = useState<FAttachment[]>([]);
|
||||
|
||||
function refresh() {
|
||||
note.getAttachments().then(attachments => setAttachments(Array.from(attachments)));
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note ]);
|
||||
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttachmentRows().some((att) => att.attachmentId && att.ownerId === note.noteId)) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
const attachments = useAttachments(note);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -59,7 +48,25 @@ export function AttachmentList({ note }: TypeWidgetProps) {
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function useAttachments(note: FNote) {
|
||||
const [ attachments, setAttachments ] = useState<FAttachment[]>([]);
|
||||
|
||||
function refresh() {
|
||||
note.getAttachments().then(attachments => setAttachments(Array.from(attachments)));
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note ]);
|
||||
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttachmentRows().some((att) => att.attachmentId && att.ownerId === note.noteId)) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
function AttachmentListHeader({ noteId }: { noteId: string }) {
|
||||
|
||||
@ -8,11 +8,40 @@ import { FormDropdownDivider, FormListItem } from "../../../react/FormList";
|
||||
export function LocaleSelector({ id, locales, currentValue, onChange, defaultLocale, extraChildren }: {
|
||||
id?: string;
|
||||
locales: Locale[],
|
||||
currentValue: string,
|
||||
currentValue: string | null | undefined,
|
||||
onChange: (newLocale: string) => void,
|
||||
defaultLocale?: Locale,
|
||||
extraChildren?: ComponentChildren
|
||||
extraChildren?: ComponentChildren,
|
||||
}) {
|
||||
const currentValueWithDefault = currentValue ?? defaultLocale?.id ?? "";
|
||||
const { activeLocale, processedLocales } = useProcessedLocales(locales, defaultLocale, currentValueWithDefault);
|
||||
return (
|
||||
<Dropdown id={id} text={activeLocale?.name}>
|
||||
{processedLocales.map((locale, index) => (
|
||||
(typeof locale === "object") ? (
|
||||
<FormListItem
|
||||
key={locale.id}
|
||||
rtl={locale.rtl}
|
||||
checked={locale.id === currentValue}
|
||||
onClick={() => {
|
||||
onChange(locale.id);
|
||||
}}
|
||||
>{locale.name}</FormListItem>
|
||||
) : (
|
||||
<FormDropdownDivider key={`divider-${index}`} />
|
||||
)
|
||||
))}
|
||||
{extraChildren && (
|
||||
<>
|
||||
<FormDropdownDivider />
|
||||
{extraChildren}
|
||||
</>
|
||||
)}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export function useProcessedLocales(locales: Locale[], defaultLocale: Locale | undefined, currentValue: string) {
|
||||
const activeLocale = defaultLocale?.id === currentValue ? defaultLocale : locales.find(l => l.id === currentValue);
|
||||
|
||||
const processedLocales = useMemo(() => {
|
||||
@ -35,28 +64,8 @@ export function LocaleSelector({ id, locales, currentValue, onChange, defaultLoc
|
||||
];
|
||||
}
|
||||
|
||||
if (extraChildren) {
|
||||
items.push("---");
|
||||
}
|
||||
return items;
|
||||
}, [ locales ]);
|
||||
}, [ locales, defaultLocale ]);
|
||||
|
||||
return (
|
||||
<Dropdown id={id} text={activeLocale?.name}>
|
||||
{processedLocales.map(locale => {
|
||||
if (typeof locale === "object") {
|
||||
return <FormListItem
|
||||
rtl={locale.rtl}
|
||||
checked={locale.id === currentValue}
|
||||
onClick={() => {
|
||||
onChange(locale.id);
|
||||
}}
|
||||
>{locale.name}</FormListItem>
|
||||
} else {
|
||||
return <FormDropdownDivider />
|
||||
}
|
||||
})}
|
||||
{extraChildren}
|
||||
</Dropdown>
|
||||
)
|
||||
return { activeLocale, processedLocales };
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user