feat: move search bar to navigation panel (#1170)
This commit is contained in:
@@ -186,7 +186,6 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<NoteGroupView application={application} />
|
||||
|
||||
@@ -7,17 +7,13 @@ type Props = {
|
||||
}
|
||||
|
||||
const styles = {
|
||||
base: 'px-2 py-1 text-center rounded-full cursor-pointer transition border border-solid active:border-info active:bg-info active:text-neutral-contrast',
|
||||
base: 'active:border-info active:bg-info active:text-neutral-contrast flex-grow cursor-pointer rounded-full border border-solid px-2 py-1 text-center transition',
|
||||
unselected: 'text-neutral border-secondary-border',
|
||||
selected: 'border-info bg-info text-neutral-contrast',
|
||||
selected: 'text-neutral-contrast border-info bg-info',
|
||||
}
|
||||
|
||||
const Bubble: FunctionComponent<Props> = ({ label, selected, onSelect }) => (
|
||||
<span
|
||||
role="tab"
|
||||
className={`bubble ${styles.base} ${selected ? styles.selected : styles.unselected}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<span role="tab" className={`${styles.base} ${selected ? styles.selected : styles.unselected}`} onClick={onSelect}>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ const ContentList: FunctionComponent<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="infinite-scroll border-t border-solid border-border focus:shadow-none focus:outline-none"
|
||||
className="infinite-scroll focus:shadow-none focus:outline-none"
|
||||
id={ElementIds.ContentList}
|
||||
onScroll={onScroll}
|
||||
onKeyDown={onKeyDown}
|
||||
|
||||
@@ -3,26 +3,15 @@ import { WebApplication } from '@/Application/Application'
|
||||
import { PANEL_NAME_NOTES } from '@/Constants/Constants'
|
||||
import { PrefKey, SystemViewId } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
FunctionComponent,
|
||||
KeyboardEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import ContentList from '@/Components/ContentListView/ContentList'
|
||||
import NoAccountWarning from '@/Components/NoAccountWarning/NoAccountWarning'
|
||||
import SearchOptions from '@/Components/SearchOptions/SearchOptions'
|
||||
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
|
||||
import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||
@@ -38,7 +27,6 @@ type Props = {
|
||||
noAccountWarningController: NoAccountWarningController
|
||||
noteTagsController: NoteTagsController
|
||||
notesController: NotesController
|
||||
searchOptionsController: SearchOptionsController
|
||||
selectionController: SelectedItemsController
|
||||
}
|
||||
|
||||
@@ -51,17 +39,13 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
noAccountWarningController,
|
||||
noteTagsController,
|
||||
notesController,
|
||||
searchOptionsController,
|
||||
selectionController,
|
||||
}) => {
|
||||
const itemsViewPanelRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const {
|
||||
clearFilterText,
|
||||
completedFullSync,
|
||||
createNewNote,
|
||||
noteFilterText,
|
||||
onFilterEnter,
|
||||
optionsSubtitle,
|
||||
paginate,
|
||||
panelTitle,
|
||||
@@ -70,13 +54,10 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
searchBarElement,
|
||||
selectNextItem,
|
||||
selectPreviousItem,
|
||||
setNoteFilterText,
|
||||
} = itemListController
|
||||
|
||||
const { selectedItems } = selectionController
|
||||
|
||||
const [focusedSearch, setFocusedSearch] = useState(false)
|
||||
|
||||
const isFilesSmartView = useMemo(
|
||||
() => navigationController.selected?.uuid === SystemViewId.Files,
|
||||
[navigationController.selected?.uuid],
|
||||
@@ -166,25 +147,6 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
selectionController,
|
||||
])
|
||||
|
||||
const onNoteFilterTextChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
(e) => {
|
||||
setNoteFilterText(e.target.value)
|
||||
},
|
||||
[setNoteFilterText],
|
||||
)
|
||||
|
||||
const onSearchFocused = useCallback(() => setFocusedSearch(true), [])
|
||||
const onSearchBlurred = useCallback(() => setFocusedSearch(false), [])
|
||||
|
||||
const onNoteFilterKeyUp: KeyboardEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (e.key === KeyboardKey.Enter) {
|
||||
onFilterEnter()
|
||||
}
|
||||
},
|
||||
[onFilterEnter],
|
||||
)
|
||||
|
||||
const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
|
||||
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
|
||||
application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
|
||||
@@ -211,7 +173,7 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
ref={itemsViewPanelRef}
|
||||
>
|
||||
<div className="content">
|
||||
<div id="items-title-bar" className="section-title-bar">
|
||||
<div id="items-title-bar" className="section-title-bar border-b border-solid border-border">
|
||||
<div id="items-title-bar-container">
|
||||
<ContentListHeader
|
||||
application={application}
|
||||
@@ -221,34 +183,6 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
isFilesSmartView={isFilesSmartView}
|
||||
optionsSubtitle={optionsSubtitle}
|
||||
/>
|
||||
<div className="filter-section" role="search">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
id="search-bar"
|
||||
className="filter-bar"
|
||||
placeholder="Search"
|
||||
title="Searches notes and files in the currently selected tag"
|
||||
value={noteFilterText}
|
||||
onChange={onNoteFilterTextChange}
|
||||
onKeyUp={onNoteFilterKeyUp}
|
||||
onFocus={onSearchFocused}
|
||||
onBlur={onSearchBlurred}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{noteFilterText && (
|
||||
<button onClick={clearFilterText} id="search-clear-button">
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(focusedSearch || noteFilterText) && (
|
||||
<div className="animate-fade-from-top">
|
||||
<SearchOptions application={application} searchOptions={searchOptionsController} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<NoAccountWarning
|
||||
accountMenuController={accountMenuController}
|
||||
noAccountWarningController={noAccountWarningController}
|
||||
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
PremiumFeatureIcon,
|
||||
RestoreIcon,
|
||||
RichTextIcon,
|
||||
SearchIcon,
|
||||
SecurityIcon,
|
||||
ServerIcon,
|
||||
SettingsIcon,
|
||||
@@ -75,6 +76,7 @@ import {
|
||||
SortDescendingIcon,
|
||||
SpreadsheetsIcon,
|
||||
StarIcon,
|
||||
SubtractIcon,
|
||||
SyncIcon,
|
||||
TasksIcon,
|
||||
ThemesIcon,
|
||||
@@ -89,7 +91,6 @@ import {
|
||||
UserSwitch,
|
||||
WarningIcon,
|
||||
WindowIcon,
|
||||
SubtractIcon,
|
||||
} from '@standardnotes/icons'
|
||||
|
||||
export const ICONS = {
|
||||
@@ -122,13 +123,13 @@ export const ICONS = {
|
||||
'menu-arrow-down': MenuArrowDownIcon,
|
||||
'menu-arrow-right': MenuArrowRightIcon,
|
||||
'menu-close': MenuCloseIcon,
|
||||
'sort-descending': SortDescendingIcon,
|
||||
'pencil-filled': PencilFilledIcon,
|
||||
'pencil-off': PencilOffIcon,
|
||||
'pin-filled': PinFilledIcon,
|
||||
'plain-text': PlainTextIcon,
|
||||
'premium-feature': PremiumFeatureIcon,
|
||||
'rich-text': RichTextIcon,
|
||||
'sort-descending': SortDescendingIcon,
|
||||
'trash-filled': TrashFilledIcon,
|
||||
'trash-sweep': TrashSweepIcon,
|
||||
'user-add': UserAddIcon,
|
||||
@@ -163,6 +164,7 @@ export const ICONS = {
|
||||
pencil: PencilIcon,
|
||||
pin: PinIcon,
|
||||
restore: RestoreIcon,
|
||||
search: SearchIcon,
|
||||
security: SecurityIcon,
|
||||
server: ServerIcon,
|
||||
settings: SettingsIcon,
|
||||
@@ -170,6 +172,7 @@ export const ICONS = {
|
||||
signOut: SignOutIcon,
|
||||
spreadsheets: SpreadsheetsIcon,
|
||||
star: StarIcon,
|
||||
subtract: SubtractIcon,
|
||||
sync: SyncIcon,
|
||||
tasks: TasksIcon,
|
||||
themes: ThemesIcon,
|
||||
@@ -180,7 +183,6 @@ export const ICONS = {
|
||||
user: UserIcon,
|
||||
warning: WarningIcon,
|
||||
window: WindowIcon,
|
||||
subtract: SubtractIcon,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { forwardRef, Fragment, Ref } from 'react'
|
||||
import { DecoratedInputProps } from './DecoratedInputProps'
|
||||
|
||||
const getClassNames = (hasLeftDecorations: boolean, hasRightDecorations: boolean) => {
|
||||
const getClassNames = (hasLeftDecorations: boolean, hasRightDecorations: boolean, roundedFull?: boolean) => {
|
||||
return {
|
||||
container: `flex items-stretch position-relative bg-default border border-solid border-border rounded focus-within:ring-2 focus-within:ring-info overflow-hidden text-sm ${
|
||||
container: `position-relative flex items-stretch overflow-hidden border border-solid border-border bg-default text-sm focus-within:ring-2 focus-within:ring-info ${
|
||||
!hasLeftDecorations && !hasRightDecorations ? 'px-2 py-1.5' : ''
|
||||
}`,
|
||||
input: `w-full border-0 focus:shadow-none focus:outline-none focus:ring-none bg-transparent text-text ${
|
||||
} ${roundedFull ? 'rounded-full' : 'rounded'}`,
|
||||
input: `focus:ring-none w-full border-0 bg-transparent text-text focus:shadow-none focus:outline-none ${
|
||||
!hasLeftDecorations && hasRightDecorations ? 'pl-2' : ''
|
||||
} ${hasRightDecorations ? 'pr-2' : ''}`,
|
||||
disabled: 'bg-passive-5 cursor-not-allowed',
|
||||
@@ -19,27 +19,32 @@ const getClassNames = (hasLeftDecorations: boolean, hasRightDecorations: boolean
|
||||
const DecoratedInput = forwardRef(
|
||||
(
|
||||
{
|
||||
type = 'text',
|
||||
autocomplete = false,
|
||||
className = '',
|
||||
id = '',
|
||||
disabled = false,
|
||||
id,
|
||||
left,
|
||||
right,
|
||||
value,
|
||||
placeholder = '',
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
onKeyDown,
|
||||
autocomplete = false,
|
||||
onKeyUp,
|
||||
placeholder = '',
|
||||
right,
|
||||
type = 'text',
|
||||
title,
|
||||
value,
|
||||
roundedFull,
|
||||
}: DecoratedInputProps,
|
||||
ref: Ref<HTMLInputElement>,
|
||||
) => {
|
||||
const hasLeftDecorations = Boolean(left?.length)
|
||||
const hasRightDecorations = Boolean(right?.length)
|
||||
const classNames = getClassNames(hasLeftDecorations, hasRightDecorations)
|
||||
const classNames = getClassNames(hasLeftDecorations, hasRightDecorations, roundedFull)
|
||||
|
||||
return (
|
||||
<div className={`${classNames.container} ${disabled ? classNames.disabled : ''} ${className}`}>
|
||||
<div className=""></div>
|
||||
{left && (
|
||||
<div className="flex items-center px-2 py-1.5">
|
||||
{left.map((leftChild, index) => (
|
||||
@@ -49,18 +54,21 @@ const DecoratedInput = forwardRef(
|
||||
)}
|
||||
|
||||
<input
|
||||
type={type}
|
||||
id={id}
|
||||
autoComplete={autocomplete ? 'on' : 'off'}
|
||||
className={`${classNames.input} ${disabled ? classNames.disabled : ''}`}
|
||||
data-lpignore={type !== 'password' ? true : false}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
id={id}
|
||||
onBlur={onBlur}
|
||||
onChange={(e) => onChange && onChange((e.target as HTMLInputElement).value)}
|
||||
onFocus={onFocus}
|
||||
onKeyDown={onKeyDown}
|
||||
data-lpignore={type !== 'password' ? true : false}
|
||||
autoComplete={autocomplete ? 'on' : 'off'}
|
||||
onKeyUp={onKeyUp}
|
||||
placeholder={placeholder}
|
||||
ref={ref}
|
||||
title={title}
|
||||
type={type}
|
||||
value={value}
|
||||
/>
|
||||
|
||||
{right && (
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { FocusEventHandler, KeyboardEventHandler, ReactNode } from 'react'
|
||||
|
||||
export type DecoratedInputProps = {
|
||||
type?: 'text' | 'email' | 'password'
|
||||
autocomplete?: boolean
|
||||
className?: string
|
||||
id?: string
|
||||
disabled?: boolean
|
||||
id?: string
|
||||
left?: ReactNode[]
|
||||
right?: ReactNode[]
|
||||
value?: string
|
||||
placeholder?: string
|
||||
onBlur?: FocusEventHandler
|
||||
onChange?: (text: string) => void
|
||||
onFocus?: FocusEventHandler
|
||||
onKeyDown?: KeyboardEventHandler
|
||||
autocomplete?: boolean
|
||||
onKeyUp?: KeyboardEventHandler
|
||||
placeholder?: string
|
||||
right?: ReactNode[]
|
||||
title?: string
|
||||
type?: React.HTMLInputTypeAttribute
|
||||
value?: string
|
||||
roundedFull?: boolean
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ApplicationEvent, PrefKey } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import SearchBar from '@/Components/SearchBar/SearchBar'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -50,6 +51,10 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
ref={setRef}
|
||||
>
|
||||
<div id="navigation-content" className="content">
|
||||
<SearchBar
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
/>
|
||||
<div className="section-title-bar">
|
||||
<div className="section-title-bar-header">
|
||||
<div className="title text-sm">
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { useState, useCallback, KeyboardEventHandler, useRef } from 'react'
|
||||
import SearchOptions from '@/Components/SearchOptions/SearchOptions'
|
||||
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
|
||||
import Icon from '../Icon/Icon'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
type Props = {
|
||||
itemListController: ItemListController
|
||||
searchOptionsController: SearchOptionsController
|
||||
}
|
||||
|
||||
const SearchBar = ({ itemListController, searchOptionsController }: Props) => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const { noteFilterText, setNoteFilterText, clearFilterText, onFilterEnter } = itemListController
|
||||
|
||||
const [focusedSearch, setFocusedSearch] = useState(false)
|
||||
|
||||
const onNoteFilterTextChange = useCallback(
|
||||
(text: string) => {
|
||||
setNoteFilterText(text)
|
||||
},
|
||||
[setNoteFilterText],
|
||||
)
|
||||
|
||||
const onNoteFilterKeyUp: KeyboardEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (e.key === KeyboardKey.Enter) {
|
||||
onFilterEnter()
|
||||
}
|
||||
},
|
||||
[onFilterEnter],
|
||||
)
|
||||
|
||||
const onSearchFocus = useCallback(() => setFocusedSearch(true), [])
|
||||
const onSearchBlur = useCallback(() => setFocusedSearch(false), [])
|
||||
|
||||
const onClearSearch = useCallback(() => {
|
||||
clearFilterText()
|
||||
searchInputRef.current?.focus()
|
||||
}, [clearFilterText])
|
||||
|
||||
return (
|
||||
<div className="px-2.5 pt-2.5 pb-0.5" role="search">
|
||||
<DecoratedInput
|
||||
autocomplete={false}
|
||||
title="Searches notes and files in the currently selected tag"
|
||||
className="placeholder:color-passive-0 rounded-full bg-contrast bg-clip-padding px-1"
|
||||
placeholder="Search"
|
||||
value={noteFilterText}
|
||||
ref={searchInputRef}
|
||||
onBlur={onSearchBlur}
|
||||
onChange={onNoteFilterTextChange}
|
||||
onFocus={onSearchFocus}
|
||||
onKeyUp={onNoteFilterKeyUp}
|
||||
left={[<Icon type="search" className="mr-1 h-4.5 w-4.5 flex-shrink-0 text-passive-1" />]}
|
||||
right={[
|
||||
noteFilterText && (
|
||||
<button
|
||||
onClick={onClearSearch}
|
||||
className="flex h-4.5 w-4.5 items-center justify-center rounded-full border-0 bg-neutral text-neutral-contrast"
|
||||
>
|
||||
<Icon type="close" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
),
|
||||
]}
|
||||
roundedFull
|
||||
/>
|
||||
|
||||
{(focusedSearch || noteFilterText) && (
|
||||
<div className="animate-fade-from-top">
|
||||
<SearchOptions searchOptions={searchOptionsController} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(SearchBar)
|
||||
@@ -1,11 +1,9 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import Bubble from '@/Components/Bubble/Bubble'
|
||||
import { useCallback } from 'react'
|
||||
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
searchOptions: SearchOptionsController
|
||||
}
|
||||
|
||||
@@ -17,7 +15,7 @@ const SearchOptions = ({ searchOptions }: Props) => {
|
||||
}, [searchOptions])
|
||||
|
||||
return (
|
||||
<div role="tablist" className="search-options justify-center" onMouseDown={(e) => e.preventDefault()}>
|
||||
<div role="tablist" className="mt-3 flex flex-wrap gap-2" onMouseDown={(e) => e.preventDefault()}>
|
||||
<Bubble
|
||||
label="Protected Contents"
|
||||
selected={includeProtectedContents}
|
||||
|
||||
Reference in New Issue
Block a user