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>; 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 = { 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 ( { 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} /> ); } const windowId = useContext(WindowIdContext); return ( {renderNewNoteButtons()} {showsSortOrderButtons() && } ); } 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);