一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
359 行
10 KiB
TypeScript
359 行
10 KiB
TypeScript
import { AppState } from '../../app.reducer';
|
|
import * as React from 'react';
|
|
import { useEffect, useRef, useMemo, useContext } from 'react';
|
|
import SearchBar from '../SearchBar/SearchBar';
|
|
import Button, { ButtonLevel, ButtonSize, buttonSizePx } from '../Button/Button';
|
|
import CommandService from '@joplin/lib/services/CommandService';
|
|
import { runtime as focusSearchRuntime } from './commands/focusSearch';
|
|
import Note from '@joplin/lib/models/Note';
|
|
import { notesSortOrderNextField } from '@joplin/lib/services/sortOrder/notesSortOrderUtils';
|
|
import { _ } from '@joplin/lib/locale';
|
|
import { connect } from 'react-redux';
|
|
import styled from 'styled-components';
|
|
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
|
import { getTrashFolderId } from '@joplin/lib/services/trash';
|
|
import { Breakpoints } from '../NoteList/utils/types';
|
|
import { stateUtils } from '@joplin/lib/reducer';
|
|
import { WindowIdContext } from '../NewWindowOrIFrame';
|
|
|
|
interface Props {
|
|
showNewNoteButtons: boolean;
|
|
sortOrderButtonsVisible: boolean;
|
|
sortOrderField: string;
|
|
sortOrderReverse: boolean;
|
|
notesParentType: string;
|
|
height: number;
|
|
width: number;
|
|
newNoteButtonEnabled: boolean;
|
|
newTodoButtonEnabled: boolean;
|
|
openTodayNoteButtonEnabled: boolean;
|
|
setNewNoteButtonElement: React.Dispatch<React.SetStateAction<Element>>;
|
|
lineCount: number;
|
|
breakpoint: number;
|
|
dynamicBreakpoints: Breakpoints;
|
|
buttonSize: ButtonSize;
|
|
padding: number;
|
|
buttonVerticalGap: number;
|
|
}
|
|
|
|
interface StyleProps {
|
|
theme: { backgroundColor3: string };
|
|
padding?: number;
|
|
buttonVerticalGap?: number;
|
|
size?: ButtonSize;
|
|
}
|
|
|
|
const StyledRoot = styled.div`
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 6px 10px;
|
|
background-color: ${(props: StyleProps) => props.theme.backgroundColor3};
|
|
gap: ${(props: StyleProps) => props.buttonVerticalGap}px;
|
|
`;
|
|
|
|
const StyledButton = styled(Button)`
|
|
width: auto;
|
|
height: 26px;
|
|
min-height: 26px;
|
|
min-width: 0;
|
|
max-width: none;
|
|
padding: 0 8px;
|
|
font-size: 12px;
|
|
border-radius: 6px;
|
|
white-space: nowrap;
|
|
|
|
.fa, .fas, .far {
|
|
font-size: 11px;
|
|
}
|
|
|
|
span[class*="StyledTitle"], span {
|
|
font-size: 12px;
|
|
}
|
|
`;
|
|
|
|
const StyledPairButtonL = styled(Button)`
|
|
border-radius: 4px 0 0 4px;
|
|
min-width: ${(props: StyleProps) => buttonSizePx(props)}px;
|
|
max-width: ${(props: StyleProps) => buttonSizePx(props)}px;
|
|
height: 26px;
|
|
min-height: 26px;
|
|
`;
|
|
|
|
const StyledPairButtonR = styled(Button)`
|
|
min-width: 8px;
|
|
border-radius: 0 4px 4px 0;
|
|
border-width: 1px 1px 1px 0;
|
|
width: auto;
|
|
height: 26px;
|
|
min-height: 26px;
|
|
padding: 0 6px;
|
|
`;
|
|
|
|
const TopRow = styled.div<{ $lineCount: number }>`
|
|
display: ${props => (props.$lineCount === 1 ? 'flex' : 'grid')};
|
|
grid-template-columns: ${props => (props.$lineCount === 1 ? 'none' : '1fr 1fr 1fr')};
|
|
flex-direction: row;
|
|
flex: 0 0 auto;
|
|
align-items: center;
|
|
gap: 4px;
|
|
min-width: 0;
|
|
`;
|
|
|
|
const BottomRow = styled.div`
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
gap: 8px;
|
|
`;
|
|
|
|
const SortOrderButtonsContainer = styled.div`
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex: 0 0 auto;
|
|
`;
|
|
|
|
function NoteListControls(props: Props) {
|
|
const searchBarRef = useRef(null);
|
|
const newTodoButtonRef = useRef(null);
|
|
const noteControlsRef = useRef(null);
|
|
const searchAndSortRef = useRef(null);
|
|
|
|
const breakpoint = props.breakpoint;
|
|
const dynamicBreakpoints = props.dynamicBreakpoints;
|
|
const lineCount = props.lineCount;
|
|
|
|
const noteButtonText = useMemo(() => {
|
|
if (breakpoint === dynamicBreakpoints.Sm) {
|
|
return '';
|
|
} else if (breakpoint === dynamicBreakpoints.Md || lineCount === 1) {
|
|
const localized = _('note');
|
|
return localized === 'note' ? '笔记' : localized;
|
|
} else {
|
|
return _('New note');
|
|
}
|
|
}, [breakpoint, dynamicBreakpoints, lineCount]);
|
|
|
|
const todoButtonText = useMemo(() => {
|
|
if (breakpoint === dynamicBreakpoints.Sm) {
|
|
return '';
|
|
} else if (breakpoint === dynamicBreakpoints.Md || lineCount === 1) {
|
|
const localized = _('to-do');
|
|
return localized === 'to-do' ? '待办' : localized;
|
|
} else {
|
|
return _('New to-do');
|
|
}
|
|
}, [breakpoint, dynamicBreakpoints, lineCount]);
|
|
|
|
const todayButtonText = useMemo(() => {
|
|
if (breakpoint === dynamicBreakpoints.Sm) {
|
|
return '';
|
|
} else {
|
|
const localized = _('Today');
|
|
return localized === 'Today' ? '今日' : localized;
|
|
}
|
|
}, [breakpoint, dynamicBreakpoints]);
|
|
|
|
const noteIcon = useMemo(() => {
|
|
if (breakpoint === dynamicBreakpoints.Sm) {
|
|
return 'icon-note';
|
|
} else {
|
|
return 'fas fa-plus';
|
|
}
|
|
}, [breakpoint, dynamicBreakpoints]);
|
|
|
|
const todoIcon = useMemo(() => {
|
|
if (breakpoint === dynamicBreakpoints.Sm) {
|
|
return 'far fa-check-square';
|
|
} else {
|
|
return 'fas fa-plus';
|
|
}
|
|
}, [breakpoint, dynamicBreakpoints]);
|
|
|
|
const todayIcon = useMemo(() => {
|
|
return 'far fa-calendar-alt';
|
|
}, []);
|
|
|
|
const showTooltip = useMemo(() => {
|
|
if (breakpoint === dynamicBreakpoints.Sm) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}, [breakpoint, dynamicBreakpoints.Sm]);
|
|
|
|
useEffect(() => {
|
|
if (lineCount === 1) {
|
|
noteControlsRef.current.style.flexDirection = 'row';
|
|
if (searchAndSortRef.current) {
|
|
searchAndSortRef.current.style.flex = '1 1 auto';
|
|
searchAndSortRef.current.style.minWidth = '120px';
|
|
}
|
|
} else {
|
|
noteControlsRef.current.style.flexDirection = 'column';
|
|
if (searchAndSortRef.current) {
|
|
searchAndSortRef.current.style.flex = '1 1 auto';
|
|
searchAndSortRef.current.style.minWidth = '0';
|
|
}
|
|
}
|
|
}, [lineCount]);
|
|
|
|
useEffect(() => {
|
|
CommandService.instance().registerRuntime('focusSearch', focusSearchRuntime(searchBarRef));
|
|
|
|
return function() {
|
|
CommandService.instance().unregisterRuntime('focusSearch');
|
|
};
|
|
}, []);
|
|
|
|
function onNewTodoButtonClick() {
|
|
void CommandService.instance().execute('newTodo');
|
|
}
|
|
|
|
function onNewNoteButtonClick() {
|
|
void CommandService.instance().execute('newNote');
|
|
}
|
|
|
|
function onOpenTodayNoteButtonClick() {
|
|
void CommandService.instance().execute('openTodayNote');
|
|
}
|
|
|
|
function onSortOrderFieldButtonClick() {
|
|
void CommandService.instance().execute('toggleNotesSortOrderField');
|
|
}
|
|
|
|
function onSortOrderReverseButtonClick() {
|
|
void CommandService.instance().execute('toggleNotesSortOrderReverse');
|
|
}
|
|
|
|
function sortOrderFieldTooltip() {
|
|
const term1 = CommandService.instance().label('toggleNotesSortOrderField');
|
|
const field = props.sortOrderField;
|
|
const term2 = Note.fieldToLabel(field);
|
|
const term3 = Note.fieldToLabel(notesSortOrderNextField(field));
|
|
return `${term1}:\n ${term2} -> ${term3}`;
|
|
}
|
|
|
|
function sortOrderFieldIcon() {
|
|
const defaultIcon = 'fas fa-cog';
|
|
|
|
const field = props.sortOrderField;
|
|
const iconMap: Record<string, string> = {
|
|
user_updated_time: 'far fa-calendar-alt',
|
|
user_created_time: 'far fa-calendar-plus',
|
|
title: 'fas fa-font',
|
|
order: 'fas fa-wrench',
|
|
todo_due: 'fas fa-calendar-check',
|
|
todo_completed: 'fas fa-check',
|
|
};
|
|
return `${iconMap[field] || defaultIcon} ${field}`;
|
|
}
|
|
|
|
function sortOrderReverseIcon() {
|
|
return props.sortOrderReverse ? 'fas fa-long-arrow-alt-up' : 'fas fa-long-arrow-alt-down';
|
|
}
|
|
|
|
function showsSortOrderButtons() {
|
|
let visible = props.sortOrderButtonsVisible;
|
|
if (props.notesParentType === 'Search') visible = false;
|
|
return visible;
|
|
}
|
|
|
|
function renderNewNoteButtons() {
|
|
if (!props.showNewNoteButtons) return null;
|
|
|
|
return (
|
|
<TopRow className="new-note-todo-buttons" $lineCount={lineCount}>
|
|
<StyledButton
|
|
ref={(el: Element) => {
|
|
props.setNewNoteButtonElement(el);
|
|
}}
|
|
className="new-note-button"
|
|
tooltip={ showTooltip ? CommandService.instance().label('newNote') : '' }
|
|
iconName={noteIcon}
|
|
title={_('%s', noteButtonText)}
|
|
level={ButtonLevel.Primary}
|
|
size={props.buttonSize}
|
|
onClick={onNewNoteButtonClick}
|
|
disabled={!props.newNoteButtonEnabled}
|
|
/>
|
|
<StyledButton
|
|
className="today-note-button"
|
|
tooltip={ showTooltip ? CommandService.instance().label('openTodayNote') : '' }
|
|
iconName={todayIcon}
|
|
title={_('%s', todayButtonText)}
|
|
level={ButtonLevel.Secondary}
|
|
size={props.buttonSize}
|
|
onClick={onOpenTodayNoteButtonClick}
|
|
disabled={!props.openTodayNoteButtonEnabled}
|
|
/>
|
|
<StyledButton ref={newTodoButtonRef}
|
|
className="new-todo-button"
|
|
tooltip={ showTooltip ? CommandService.instance().label('newTodo') : '' }
|
|
iconName={todoIcon}
|
|
title={_('%s', todoButtonText)}
|
|
level={ButtonLevel.Secondary}
|
|
size={props.buttonSize}
|
|
onClick={onNewTodoButtonClick}
|
|
disabled={!props.newTodoButtonEnabled}
|
|
/>
|
|
</TopRow>
|
|
);
|
|
}
|
|
|
|
const windowId = useContext(WindowIdContext);
|
|
return (
|
|
<StyledRoot ref={noteControlsRef} padding={props.padding} buttonVerticalGap={props.buttonVerticalGap}>
|
|
{renderNewNoteButtons()}
|
|
<BottomRow ref={searchAndSortRef} className="search-and-sort">
|
|
<SearchBar inputRef={searchBarRef} windowId={windowId}/>
|
|
{showsSortOrderButtons() &&
|
|
<SortOrderButtonsContainer>
|
|
<StyledPairButtonL
|
|
className="sort-order-field-button"
|
|
tooltip={sortOrderFieldTooltip()}
|
|
iconName={sortOrderFieldIcon()}
|
|
level={ButtonLevel.Secondary}
|
|
size={props.buttonSize}
|
|
onClick={onSortOrderFieldButtonClick}
|
|
/>
|
|
<StyledPairButtonR
|
|
className="sort-order-reverse-button"
|
|
tooltip={CommandService.instance().label('toggleNotesSortOrderReverse')}
|
|
iconName={sortOrderReverseIcon()}
|
|
level={ButtonLevel.Secondary}
|
|
size={props.buttonSize}
|
|
onClick={onSortOrderReverseButtonClick}
|
|
/>
|
|
</SortOrderButtonsContainer>
|
|
}
|
|
</BottomRow>
|
|
</StyledRoot>
|
|
);
|
|
}
|
|
|
|
interface ConnectProps {
|
|
windowId: string;
|
|
}
|
|
|
|
const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
|
const whenClauseContext = stateToWhenClauseContext(state, { windowId: ownProps.windowId });
|
|
const windowState = stateUtils.windowStateById(state, ownProps.windowId);
|
|
const hasFolderForNewNotes = whenClauseContext.selectedFolderIsValid
|
|
&& windowState.selectedFolderId !== getTrashFolderId();
|
|
|
|
return {
|
|
showNewNoteButtons: hasFolderForNewNotes,
|
|
newNoteButtonEnabled: CommandService.instance().isEnabled('newNote', whenClauseContext),
|
|
newTodoButtonEnabled: CommandService.instance().isEnabled('newTodo', whenClauseContext),
|
|
openTodayNoteButtonEnabled: CommandService.instance().isEnabled('openTodayNote', whenClauseContext),
|
|
sortOrderButtonsVisible: state.settings['notes.sortOrder.buttonsVisible'],
|
|
sortOrderField: state.settings['notes.sortOrder.field'],
|
|
sortOrderReverse: state.settings['notes.sortOrder.reverse'],
|
|
notesParentType: windowState.notesParentType,
|
|
};
|
|
};
|
|
|
|
export default connect(mapStateToProps)(NoteListControls);
|