mirror of
https://github.com/zadam/trilium.git
synced 2025-12-18 05:14:24 +01:00
New layout: status bar (#8021)
This commit is contained in:
commit
15b5885982
@ -265,7 +265,7 @@ export type CommandMappings = {
|
|||||||
|
|
||||||
reEvaluateRightPaneVisibility: CommandData;
|
reEvaluateRightPaneVisibility: CommandData;
|
||||||
runActiveNote: CommandData;
|
runActiveNote: CommandData;
|
||||||
scrollContainerToCommand: CommandData & {
|
scrollContainerTo: CommandData & {
|
||||||
position: number;
|
position: number;
|
||||||
};
|
};
|
||||||
scrollToEnd: CommandData;
|
scrollToEnd: CommandData;
|
||||||
|
|||||||
@ -44,7 +44,6 @@ import NoteDetail from "../widgets/NoteDetail.jsx";
|
|||||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||||
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
||||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||||
import Breadcrumb from "../widgets/Breadcrumb.jsx";
|
|
||||||
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
|
||||||
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
|
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
|
||||||
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
|
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 StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||||
import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx";
|
import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx";
|
||||||
import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx";
|
import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx";
|
||||||
import NoteStatusBar from "../widgets/NoteStatusBar.jsx";
|
import StatusBar from "../widgets/layout/StatusBar.jsx";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@ -134,6 +133,7 @@ export default class DesktopLayout {
|
|||||||
.filling()
|
.filling()
|
||||||
.collapsible()
|
.collapsible()
|
||||||
.id("center-pane")
|
.id("center-pane")
|
||||||
|
.optChild(isNewLayout, <StandaloneRibbonAdapter component={FormattingToolbar} />)
|
||||||
.child(
|
.child(
|
||||||
new SplitNoteContainer(() =>
|
new SplitNoteContainer(() =>
|
||||||
new NoteWrapperWidget()
|
new NoteWrapperWidget()
|
||||||
@ -141,7 +141,6 @@ export default class DesktopLayout {
|
|||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
.class("breadcrumb-row")
|
.class("breadcrumb-row")
|
||||||
.cssBlock(".breadcrumb-row > * { margin: 5px; }")
|
.cssBlock(".breadcrumb-row > * { margin: 5px; }")
|
||||||
.child(<Breadcrumb />)
|
|
||||||
.optChild(isNewLayout, <BreadcrumbBadges />)
|
.optChild(isNewLayout, <BreadcrumbBadges />)
|
||||||
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
||||||
.child(<MovePaneButton direction="left" />)
|
.child(<MovePaneButton direction="left" />)
|
||||||
@ -152,7 +151,7 @@ export default class DesktopLayout {
|
|||||||
)
|
)
|
||||||
.optChild(!isFloatingTitlebar, titleRow)
|
.optChild(!isFloatingTitlebar, titleRow)
|
||||||
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
|
.optChild(!isNewLayout, <Ribbon><NoteActions /></Ribbon>)
|
||||||
.optChild(isNewLayout, <StandaloneRibbonAdapter component={FormattingToolbar} />)
|
.optChild(isNewLayout, <Ribbon />)
|
||||||
.child(new WatchedFileUpdateStatusWidget())
|
.child(new WatchedFileUpdateStatusWidget())
|
||||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||||
.child(
|
.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("node-detail-pane"), // typo, let's keep it for a while as BC
|
||||||
...this.customWidgets.get("note-detail-pane")
|
...this.customWidgets.get("note-detail-pane")
|
||||||
)
|
)
|
||||||
.optChild(isNewLayout, (
|
|
||||||
<Ribbon>
|
|
||||||
<NoteStatusBar />
|
|
||||||
</Ribbon>
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.child(...this.customWidgets.get("center-pane"))
|
.child(...this.customWidgets.get("center-pane"))
|
||||||
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
new RightPaneContainer()
|
new RightPaneContainer()
|
||||||
@ -194,8 +189,10 @@ export default class DesktopLayout {
|
|||||||
.child(...this.customWidgets.get("right-pane"))
|
.child(...this.customWidgets.get("right-pane"))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.optChild(!launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
||||||
.child(<CloseZenModeButton />)
|
.child(<CloseZenModeButton />)
|
||||||
|
|
||||||
// Desktop-specific dialogs.
|
// Desktop-specific dialogs.
|
||||||
|
|||||||
@ -399,7 +399,8 @@ button.select-button.dropdown-toggle.btn:active {
|
|||||||
select:focus,
|
select:focus,
|
||||||
select.form-select:focus,
|
select.form-select:focus,
|
||||||
select.form-control: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;
|
box-shadow: unset;
|
||||||
outline: 3px solid var(--input-focus-outline-color);
|
outline: 3px solid var(--input-focus-outline-color);
|
||||||
outline-offset: 0;
|
outline-offset: 0;
|
||||||
|
|||||||
@ -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_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": "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.",
|
"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": "Web clip",
|
||||||
"clipped_note_description": "This note was originally taken from {{url}}.\n\nClick to navigate to the source webpage.",
|
"clipped_note_description": "This note was originally taken from {{url}}.\n\nClick to navigate to the source webpage.",
|
||||||
"execute_script": "Run script",
|
"execute_script": "Run script",
|
||||||
"execute_script_description": "This note is a script note. Click to execute the script.",
|
"execute_script_description": "This note is a script note. Click to execute the script.",
|
||||||
"execute_sql": "Run SQL",
|
"execute_sql": "Run SQL",
|
||||||
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query."
|
"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; }
|
&.read-only-badge { --color: #e33f3b; }
|
||||||
&.share-badge { --color: #3b82f6; }
|
&.share-badge { --color: #3b82f6; }
|
||||||
&.clipped-note-badge { --color: #57a2a5; }
|
&.clipped-note-badge { --color: #57a2a5; }
|
||||||
&.backlinks-badge { color: var(--badge-text-color); }
|
|
||||||
&.execute-badge { --color: #f59e0b; }
|
&.execute-badge { --color: #f59e0b; }
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@ -61,30 +60,6 @@
|
|||||||
min-width: 500px;
|
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 {
|
.breadcrumb-badge {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,56 +5,22 @@ import { ComponentChildren, MouseEventHandler } from "preact";
|
|||||||
import { useRef } from "preact/hooks";
|
import { useRef } from "preact/hooks";
|
||||||
|
|
||||||
import { t } from "../services/i18n";
|
import { t } from "../services/i18n";
|
||||||
import { formatDateTime } from "../utils/formatters";
|
|
||||||
import { BacklinksList, useBacklinkCount } from "./FloatingButtonsDefinitions";
|
|
||||||
import Dropdown, { DropdownProps } from "./react/Dropdown";
|
import Dropdown, { DropdownProps } from "./react/Dropdown";
|
||||||
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "./react/hooks";
|
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "./react/hooks";
|
||||||
import Icon from "./react/Icon";
|
import Icon from "./react/Icon";
|
||||||
import { NoteSizeWidget, useNoteMetadata } from "./ribbon/NoteInfoTab";
|
|
||||||
import { useShareInfo } from "./shared_info";
|
import { useShareInfo } from "./shared_info";
|
||||||
import FNote from "../entities/fnote";
|
|
||||||
|
|
||||||
export default function BreadcrumbBadges() {
|
export default function BreadcrumbBadges() {
|
||||||
return (
|
return (
|
||||||
<div className="breadcrumb-badges">
|
<div className="breadcrumb-badges">
|
||||||
<ReadOnlyBadge />
|
<ReadOnlyBadge />
|
||||||
<ShareBadge />
|
<ShareBadge />
|
||||||
<BacklinksBadge />
|
|
||||||
<ClippedNoteBadge />
|
<ClippedNoteBadge />
|
||||||
<ExecuteBadge />
|
<ExecuteBadge />
|
||||||
</div>
|
</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() {
|
function ReadOnlyBadge() {
|
||||||
const { note, noteContext } = useNoteContext();
|
const { note, noteContext } = useNoteContext();
|
||||||
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
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() {
|
function ClippedNoteBadge() {
|
||||||
const { note } = useNoteContext();
|
const { note } = useNoteContext();
|
||||||
const [ pageUrl ] = useNoteLabel(note, "pageUrl");
|
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() {
|
export default function NoteTitleDetails() {
|
||||||
const { note } = useNoteContext();
|
const { note } = useNoteContext();
|
||||||
|
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
|
||||||
const noteType = useNoteProperty(note, "type");
|
const noteType = useNoteProperty(note, "type");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="title-details">
|
<div className="title-details">
|
||||||
{note && noteType === "book" && <CollectionProperties note={note} />}
|
{note && !isHiddenNote && noteType === "book" && <CollectionProperties note={note} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,10 +104,12 @@ function BrowserOnlyOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DevelopmentOptions() {
|
function DevelopmentOptions() {
|
||||||
|
const [ layoutOrientation ] = useTriliumOption("layoutOrientation");
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<FormDropdownDivider />
|
<FormDropdownDivider />
|
||||||
<FormListItem disabled>Development Options</FormListItem>
|
<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) => (
|
{experimentalFeatures.map((feature) => (
|
||||||
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />
|
<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);
|
this.$widget.scrollTop(position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,60 +1,6 @@
|
|||||||
.breadcrumb-row {
|
|
||||||
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 {
|
.breadcrumb {
|
||||||
.btn.icon-action {
|
position: relative;
|
||||||
width: 16px;
|
align-items: center;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-action {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body.experimental-feature-new-layout .breadcrumb-row {
|
|
||||||
padding-inline-end: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component.breadcrumb {
|
|
||||||
contain: none;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -62,7 +8,6 @@ body.experimental-feature-new-layout .breadcrumb-row {
|
|||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-width: 85%;
|
|
||||||
|
|
||||||
> span,
|
> span,
|
||||||
> span > span {
|
> span > span {
|
||||||
@ -108,7 +53,6 @@ body.experimental-feature-new-layout .breadcrumb-row {
|
|||||||
.breadcrumb-last-item {
|
.breadcrumb-last-item {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: unset;
|
color: unset;
|
||||||
cursor: text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
@ -3,25 +3,23 @@ import "./Breadcrumb.css";
|
|||||||
import { useMemo, useState } from "preact/hooks";
|
import { useMemo, useState } from "preact/hooks";
|
||||||
import { Fragment } from "preact/jsx-runtime";
|
import { Fragment } from "preact/jsx-runtime";
|
||||||
|
|
||||||
import NoteContext from "../components/note_context";
|
import appContext from "../../components/app_context";
|
||||||
import froca from "../services/froca";
|
import NoteContext from "../../components/note_context";
|
||||||
import ActionButton from "./react/ActionButton";
|
import FNote from "../../entities/fnote";
|
||||||
import Dropdown from "./react/Dropdown";
|
import link_context_menu from "../../menus/link_context_menu";
|
||||||
import { FormListItem } from "./react/FormList";
|
import froca from "../../services/froca";
|
||||||
import { useChildNotes, useNoteContext, useNoteLabel, useNoteProperty } from "./react/hooks";
|
import ActionButton from "../react/ActionButton";
|
||||||
import Icon from "./react/Icon";
|
import Dropdown from "../react/Dropdown";
|
||||||
import NoteLink from "./react/NoteLink";
|
import { FormListItem } from "../react/FormList";
|
||||||
import link_context_menu from "../menus/link_context_menu";
|
import { useChildNotes, useNoteLabel, useNoteProperty } from "../react/hooks";
|
||||||
import { TitleEditor } from "./collections/board";
|
import Icon from "../react/Icon";
|
||||||
import server from "../services/server";
|
import NoteLink from "../react/NoteLink";
|
||||||
import { NoteInfoBadge } from "./BreadcrumbBadges";
|
|
||||||
|
|
||||||
const COLLAPSE_THRESHOLD = 5;
|
const COLLAPSE_THRESHOLD = 5;
|
||||||
const INITIAL_ITEMS = 2;
|
const INITIAL_ITEMS = 2;
|
||||||
const FINAL_ITEMS = 2;
|
const FINAL_ITEMS = 2;
|
||||||
|
|
||||||
export default function Breadcrumb() {
|
export default function Breadcrumb({ note, noteContext }: { note: FNote, noteContext: NoteContext }) {
|
||||||
const { note, noteContext } = useNoteContext();
|
|
||||||
const notePath = buildNotePaths(noteContext?.notePathArray);
|
const notePath = buildNotePaths(noteContext?.notePathArray);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -65,6 +63,7 @@ function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined
|
|||||||
|
|
||||||
return (note &&
|
return (note &&
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
className="root-note"
|
||||||
icon={note.getIcon()}
|
icon={note.getIcon()}
|
||||||
text={title ?? ""}
|
text={title ?? ""}
|
||||||
onClick={() => noteContext?.setNote("root")}
|
onClick={() => noteContext?.setNote("root")}
|
||||||
@ -87,33 +86,23 @@ function BreadcrumbLink({ notePath }: { notePath: string }) {
|
|||||||
function BreadcrumbLastItem({ notePath }: { notePath: string }) {
|
function BreadcrumbLastItem({ notePath }: { notePath: string }) {
|
||||||
const noteId = notePath.split("/").at(-1);
|
const noteId = notePath.split("/").at(-1);
|
||||||
const [ note ] = useState(() => froca.getNoteFromCache(noteId!));
|
const [ note ] = useState(() => froca.getNoteFromCache(noteId!));
|
||||||
const [ isEditing, setIsEditing ] = useState(false);
|
|
||||||
const title = useNoteProperty(note, "title");
|
const title = useNoteProperty(note, "title");
|
||||||
|
|
||||||
if (!note) return null;
|
if (!note) return null;
|
||||||
|
|
||||||
if (!isEditing) {
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className="breadcrumb-last-item tn-link"
|
className="breadcrumb-last-item tn-link"
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
e.preventDefault();
|
const activeNtxId = appContext.tabManager.activeNtxId;
|
||||||
setIsEditing(true);
|
const scrollingContainer = document.querySelector(`[data-ntx-id="${activeNtxId}"] .scrolling-container`);
|
||||||
|
scrollingContainer?.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
}}
|
}}
|
||||||
>{title}</a>
|
>{title}</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<TitleEditor
|
|
||||||
currentValue={title}
|
|
||||||
save={(newTitle) => { return server.put(`notes/${noteId}/title`, { title: newTitle.trim() }); }}
|
|
||||||
dismiss={() => setIsEditing(false)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { index: number, notePathLength: number, notePath: string, noteContext: NoteContext | undefined }) {
|
function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { index: number, notePathLength: number, notePath: string, noteContext: NoteContext | undefined }) {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
return <BreadcrumbRoot noteContext={noteContext} />;
|
return <BreadcrumbRoot noteContext={noteContext} />;
|
||||||
@ -122,7 +111,6 @@ function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { inde
|
|||||||
if (index === notePathLength - 1) {
|
if (index === notePathLength - 1) {
|
||||||
return <>
|
return <>
|
||||||
<BreadcrumbLastItem notePath={notePath} />
|
<BreadcrumbLastItem notePath={notePath} />
|
||||||
<NoteInfoBadge note={noteContext?.note} />
|
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +124,7 @@ function BreadcrumbSeparator({ notePath, noteContext, activeNotePath }: { notePa
|
|||||||
noSelectButtonStyle
|
noSelectButtonStyle
|
||||||
buttonClassName="icon-action"
|
buttonClassName="icon-action"
|
||||||
hideToggleArrow
|
hideToggleArrow
|
||||||
dropdownOptions={{ popperConfig: { strategy: "fixed" } }}
|
dropdownOptions={{ popperConfig: { strategy: "fixed", placement: "top" } }}
|
||||||
>
|
>
|
||||||
<BreadcrumbSeparatorDropdownContent notePath={notePath} noteContext={noteContext} activeNotePath={activeNotePath} />
|
<BreadcrumbSeparatorDropdownContent notePath={notePath} noteContext={noteContext} activeNotePath={activeNotePath} />
|
||||||
</Dropdown>
|
</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}`);
|
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
note: note,
|
note,
|
||||||
noteId: noteContext?.note?.noteId,
|
noteId: noteContext?.note?.noteId,
|
||||||
notePath: noteContext?.notePath,
|
notePath: noteContext?.notePath,
|
||||||
hoistedNoteId: noteContext?.hoistedNoteId,
|
hoistedNoteId: noteContext?.hoistedNoteId,
|
||||||
@ -327,7 +327,65 @@ export function useNoteContext() {
|
|||||||
parentComponent,
|
parentComponent,
|
||||||
isReadOnlyTemporarilyDisabled
|
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 { ComponentChildren } from "preact";
|
||||||
import { createPortal } from "preact/compat";
|
import { createPortal } from "preact/compat";
|
||||||
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
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>> }) {
|
export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note, setModalShown, noCodeNotes }: {
|
||||||
const [ codeNotesMimeTypes ] = useTriliumOption("codeNotesMimeTypes");
|
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 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) => {
|
const changeNoteType = useCallback(async (type: NoteType, mime?: string) => {
|
||||||
if (!note || (type === currentNoteType && mime === currentNoteMime)) {
|
if (!note || (type === currentNoteType && mime === currentNoteMime)) {
|
||||||
return;
|
return;
|
||||||
@ -107,7 +109,7 @@ export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checked = (type === currentNoteType);
|
const checked = (type === currentNoteType);
|
||||||
if (type !== "code") {
|
if (noCodeNotes || type !== "code") {
|
||||||
return (
|
return (
|
||||||
<FormListItem
|
<FormListItem
|
||||||
checked={checked}
|
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 }) => (
|
{mimeTypes.map(({ title, mime }) => (
|
||||||
<FormListItem onClick={() => changeNoteType("code", mime)}>
|
<FormListItem
|
||||||
|
key={mime}
|
||||||
|
checked={mime === currentMimeType}
|
||||||
|
onClick={() => changeNoteType("code", mime)}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</FormListItem>
|
</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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="code-mime-types-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 [ modalShown, setModalShown ] = useState(false);
|
||||||
const [ languages ] = useTriliumOption("languages");
|
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
|
||||||
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<LocaleSelector
|
<LocaleSelector
|
||||||
locales={locales}
|
locales={locales}
|
||||||
defaultLocale={DEFAULT_LOCALE}
|
defaultLocale={DEFAULT_LOCALE}
|
||||||
currentValue={currentNoteLanguage ?? ""} onChange={setCurrentNoteLanguage}
|
currentValue={currentNoteLanguage} onChange={setCurrentNoteLanguage}
|
||||||
extraChildren={<>
|
extraChildren={<>
|
||||||
{extraChildren}
|
|
||||||
<FormListItem
|
<FormListItem
|
||||||
onClick={() => setModalShown(true)}
|
onClick={() => setModalShown(true)}
|
||||||
icon="bx bx-cog"
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="content-languages-modal"
|
className="content-languages-modal"
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import RawHtml from "../react/RawHtml";
|
|||||||
import { joinElements } from "../react/react_utils";
|
import { joinElements } from "../react/react_utils";
|
||||||
import AttributeDetailWidget from "../attribute_widgets/attribute_detail";
|
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 [ inheritedAttributes, setInheritedAttributes ] = useState<FAttribute[]>();
|
||||||
const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget());
|
const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget());
|
||||||
|
|
||||||
|
|||||||
@ -184,11 +184,16 @@ function EditabilityDropdown({ note }: { note: FNote }) {
|
|||||||
function NoteTypeDropdown({ note }: { note: FNote }) {
|
function NoteTypeDropdown({ note }: { note: FNote }) {
|
||||||
const currentNoteType = useNoteProperty(note, "type") ?? undefined;
|
const currentNoteType = useNoteProperty(note, "type") ?? undefined;
|
||||||
const currentNoteMime = useNoteProperty(note, "mime");
|
const currentNoteMime = useNoteProperty(note, "mime");
|
||||||
const [ modalShown, setModalShown ] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormDropdownSubmenu title={t("basic_properties.note_type")} icon="bx bx-file" dropStart>
|
<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>
|
</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 { t } from "../../services/i18n";
|
||||||
|
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import { useTriliumEvent } from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
||||||
import { NotePathRecord } from "../../entities/fnote";
|
|
||||||
import NoteLink from "../react/NoteLink";
|
import NoteLink from "../react/NoteLink";
|
||||||
import { joinElements } from "../react/react_utils";
|
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) {
|
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
|
||||||
const [ sortedNotePaths, setSortedNotePaths ] = useState<NotePathRecord[]>();
|
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
||||||
|
return <NotePathsWidget sortedNotePaths={sortedNotePaths} currentNotePath={notePath} />;
|
||||||
function refresh() {
|
|
||||||
if (!note) return;
|
|
||||||
setSortedNotePaths(note
|
|
||||||
.getSortedNotePathRecords(hoistedNoteId)
|
|
||||||
.filter((notePath) => !notePath.isHidden));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(refresh, [ note?.noteId ]);
|
export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
|
||||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
sortedNotePaths: NotePathRecord[] | undefined;
|
||||||
const noteId = note?.noteId;
|
currentNotePath?: string | null | undefined;
|
||||||
if (!noteId) return;
|
}) {
|
||||||
if (loadResults.getBranchRows().find((branch) => branch.noteId === noteId)
|
|
||||||
|| loadResults.isNoteReloaded(noteId)) {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="note-paths-widget">
|
<div class="note-paths-widget">
|
||||||
<>
|
<>
|
||||||
@ -38,7 +28,8 @@ export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabConte
|
|||||||
<ul className="note-path-list">
|
<ul className="note-path-list">
|
||||||
{sortedNotePaths?.length ? sortedNotePaths.map(sortedNotePath => (
|
{sortedNotePaths?.length ? sortedNotePaths.map(sortedNotePath => (
|
||||||
<NotePath
|
<NotePath
|
||||||
currentNotePath={notePath}
|
key={sortedNotePath.notePath}
|
||||||
|
currentNotePath={currentNotePath}
|
||||||
notePathRecord={sortedNotePath}
|
notePathRecord={sortedNotePath}
|
||||||
/>
|
/>
|
||||||
)) : undefined}
|
)) : undefined}
|
||||||
@ -50,12 +41,35 @@ export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabConte
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
</div>
|
</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 }) {
|
function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: string | null, notePathRecord?: NotePathRecord }) {
|
||||||
const notePath = notePathRecord?.notePath ?? [];
|
const notePath = notePathRecord?.notePath;
|
||||||
const notePathString = useMemo(() => notePath.join("/"), [ notePath ]);
|
const notePathString = useMemo(() => (notePath ?? []).join("/"), [ notePath ]);
|
||||||
|
|
||||||
const [ classes, icons ] = useMemo(() => {
|
const [ classes, icons ] = useMemo(() => {
|
||||||
const classes: string[] = [];
|
const classes: string[] = [];
|
||||||
@ -68,17 +82,17 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
|||||||
if (!notePathRecord || notePathRecord.isInHoistedSubTree) {
|
if (!notePathRecord || notePathRecord.isInHoistedSubTree) {
|
||||||
classes.push("path-in-hoisted-subtree");
|
classes.push("path-in-hoisted-subtree");
|
||||||
} else {
|
} 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) {
|
if (notePathRecord?.isArchived) {
|
||||||
classes.push("path-archived");
|
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) {
|
if (notePathRecord?.isSearch) {
|
||||||
classes.push("path-search");
|
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 ];
|
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.
|
// Determine the full note path (for the links) of every component of the current note path.
|
||||||
const pathSegments: string[] = [];
|
const pathSegments: string[] = [];
|
||||||
const fullNotePaths: string[] = [];
|
const fullNotePaths: string[] = [];
|
||||||
for (const noteId of notePath) {
|
for (const noteId of notePath ?? []) {
|
||||||
pathSegments.push(noteId);
|
pathSegments.push(noteId);
|
||||||
fullNotePaths.push(pathSegments.join("/"));
|
fullNotePaths.push(pathSegments.join("/"));
|
||||||
}
|
}
|
||||||
@ -95,12 +109,12 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
|
|||||||
return (
|
return (
|
||||||
<li class={classes}>
|
<li class={classes}>
|
||||||
{joinElements(fullNotePaths.map(notePath => (
|
{joinElements(fullNotePaths.map(notePath => (
|
||||||
<NoteLink notePath={notePath} noPreview />
|
<NoteLink key={notePath} notePath={notePath} noPreview />
|
||||||
)), NOTE_PATH_TITLE_SEPARATOR)}
|
)), NOTE_PATH_TITLE_SEPARATOR)}
|
||||||
|
|
||||||
{icons.map(({ icon, title }) => (
|
{icons.map(({ icon, title }) => (
|
||||||
<span class={icon} title={title} />
|
<span key={title} class={icon} title={title} />
|
||||||
))}
|
))}
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useMemo, useRef } from "preact/hooks";
|
import { useMemo, useRef } from "preact/hooks";
|
||||||
|
|
||||||
import { useLegacyImperativeHandlers, useTriliumEvents } from "../react/hooks";
|
import { useLegacyImperativeHandlers, useTriliumEvents } from "../react/hooks";
|
||||||
import AttributeEditor, { AttributeEditorImperativeHandlers } from "./components/AttributeEditor";
|
import AttributeEditor, { AttributeEditorImperativeHandlers } from "./components/AttributeEditor";
|
||||||
import { TabContext } from "./ribbon-interface";
|
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} />
|
<AttributeEditor api={api} ntxId={ntxId} note={note} {...restProps} hidden={hidden} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 "./style.css";
|
||||||
|
|
||||||
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
|
||||||
import { EventNames } from "../../components/app_context";
|
|
||||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||||
import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition";
|
|
||||||
import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
|
||||||
import clsx from "clsx";
|
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 { 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);
|
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
|
||||||
|
|
||||||
@ -45,16 +46,6 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr
|
|||||||
refresh();
|
refresh();
|
||||||
}, [ note, noteType, isReadOnlyTemporarilyDisabled ]);
|
}, [ 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.
|
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!computedTabs) return;
|
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"));
|
const shouldShowRibbon = (noteContext?.viewScope?.viewMode === "default" && !noteContext.noteId?.startsWith("_options"));
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
|
||||||
className={clsx("ribbon-container", !shouldShowRibbon && "hidden-ext")}
|
className={clsx("ribbon-container", !shouldShowRibbon && "hidden-ext")}
|
||||||
style={{ contain: "none" }}
|
style={{ contain: "none" }}
|
||||||
>
|
>
|
||||||
@ -133,7 +123,7 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: string; title: string; active: boolean, onClick: () => void, toggleCommand?: KeyboardActionNames }) {
|
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" />
|
<div class="ribbon-tab-spacer" />
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shouldShowTab(showConfig: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined), context: TitleContext) {
|
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"),
|
title: t("owned_attribute_list.owned_attributes"),
|
||||||
icon: "bx bx-list-check",
|
icon: "bx bx-list-check",
|
||||||
content: OwnedAttributesTab,
|
content: OwnedAttributesTab,
|
||||||
show: ({note}) => !note?.isLaunchBarConfig(),
|
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
|
||||||
toggleCommand: "toggleRibbonTabOwnedAttributes",
|
toggleCommand: "toggleRibbonTabOwnedAttributes",
|
||||||
stayInDom: true
|
stayInDom: !isNewLayout
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("inherited_attribute_list.title"),
|
title: t("inherited_attribute_list.title"),
|
||||||
icon: "bx bx-list-plus",
|
icon: "bx bx-list-plus",
|
||||||
content: InheritedAttributesTab,
|
content: InheritedAttributesTab,
|
||||||
show: ({note}) => !note?.isLaunchBarConfig(),
|
show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(),
|
||||||
toggleCommand: "toggleRibbonTabInheritedAttributes"
|
toggleCommand: "toggleRibbonTabInheritedAttributes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("note_paths.title"),
|
title: t("note_paths.title"),
|
||||||
icon: "bx bx-collection",
|
icon: "bx bx-collection",
|
||||||
content: NotePathsTab,
|
content: NotePathsTab,
|
||||||
show: true,
|
show: !isNewLayout,
|
||||||
toggleCommand: "toggleRibbonTabNotePaths"
|
toggleCommand: "toggleRibbonTabNotePaths"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -125,7 +125,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
|||||||
{
|
{
|
||||||
title: t("similar_notes.title"),
|
title: t("similar_notes.title"),
|
||||||
icon: "bx bx-bar-chart",
|
icon: "bx bx-bar-chart",
|
||||||
show: ({ note }) => note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
|
show: ({ note }) => !isNewLayout && note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"),
|
||||||
content: SimilarNotesTab,
|
content: SimilarNotesTab,
|
||||||
toggleCommand: "toggleRibbonTabSimilarNotes"
|
toggleCommand: "toggleRibbonTabSimilarNotes"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -355,6 +355,10 @@ body[dir=rtl] .attribute-list-editor {
|
|||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 14px 12px 13px 12px;
|
padding: 14px 12px 13px 12px;
|
||||||
|
|
||||||
|
a.reference-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
@ -418,6 +422,10 @@ body[dir=rtl] .attribute-list-editor {
|
|||||||
|
|
||||||
/* #region Experimental layout */
|
/* #region Experimental layout */
|
||||||
body.experimental-feature-new-layout {
|
body.experimental-feature-new-layout {
|
||||||
|
.ribbon-top-row {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ribbon-container {
|
.ribbon-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
|||||||
@ -26,24 +26,13 @@ import ws from "../../services/ws";
|
|||||||
import appContext from "../../components/app_context";
|
import appContext from "../../components/app_context";
|
||||||
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
|
import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons";
|
||||||
import options from "../../services/options";
|
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.
|
* Displays the full list of attachments of a note and allows the user to interact with them.
|
||||||
*/
|
*/
|
||||||
export function AttachmentList({ note }: TypeWidgetProps) {
|
export function AttachmentList({ note }: TypeWidgetProps) {
|
||||||
const [ attachments, setAttachments ] = useState<FAttachment[]>([]);
|
const attachments = useAttachments(note);
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -59,7 +48,25 @@ export function AttachmentList({ note }: TypeWidgetProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 }) {
|
function AttachmentListHeader({ noteId }: { noteId: string }) {
|
||||||
|
|||||||
@ -8,11 +8,40 @@ import { FormDropdownDivider, FormListItem } from "../../../react/FormList";
|
|||||||
export function LocaleSelector({ id, locales, currentValue, onChange, defaultLocale, extraChildren }: {
|
export function LocaleSelector({ id, locales, currentValue, onChange, defaultLocale, extraChildren }: {
|
||||||
id?: string;
|
id?: string;
|
||||||
locales: Locale[],
|
locales: Locale[],
|
||||||
currentValue: string,
|
currentValue: string | null | undefined,
|
||||||
onChange: (newLocale: string) => void,
|
onChange: (newLocale: string) => void,
|
||||||
defaultLocale?: Locale,
|
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 activeLocale = defaultLocale?.id === currentValue ? defaultLocale : locales.find(l => l.id === currentValue);
|
||||||
|
|
||||||
const processedLocales = useMemo(() => {
|
const processedLocales = useMemo(() => {
|
||||||
@ -35,28 +64,8 @@ export function LocaleSelector({ id, locales, currentValue, onChange, defaultLoc
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extraChildren) {
|
|
||||||
items.push("---");
|
|
||||||
}
|
|
||||||
return items;
|
return items;
|
||||||
}, [ locales ]);
|
}, [ locales, defaultLocale ]);
|
||||||
|
|
||||||
return (
|
return { activeLocale, processedLocales };
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user