Skip to content

Commit

Permalink
Advanced search (#1180)
Browse files Browse the repository at this point in the history
* Advanced search

* Localization & search query builder & unit tests

* Fix build

* type fix

* review fixes

* Search improvements
  • Loading branch information
Zoltan Takacs authored Feb 17, 2021
1 parent 53cbc51 commit dcc5f2b
Show file tree
Hide file tree
Showing 28 changed files with 1,228 additions and 230 deletions.
1 change: 1 addition & 0 deletions apps/sensenet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"monaco-editor": "0.21.3",
"react": "^16.13.0",
"react-autosuggest": "^10.1.0",
"react-day-picker": "^7.4.8",
"react-dom": "^16.13.0",
"react-markdown": "^5.0.3",
"react-monaco-editor": "0.41.2",
Expand Down
13 changes: 8 additions & 5 deletions apps/sensenet/src/components/dialogs/content-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import { Icon } from '../Icon'
import { DialogTitle, useDialog } from '.'

export interface ContentPickerDialogProps {
defaultValue?: GenericContentWithIsParent
currentPath: string
selectionRoot?: string
content: GenericContent
handleSubmit?: (path: string) => void
required?: boolean
content?: GenericContent
handleSubmit?: (content: GenericContentWithIsParent) => void
}

export const ContentPickerDialog: React.FunctionComponent<ContentPickerDialogProps> = (props) => {
Expand All @@ -32,21 +34,22 @@ export const ContentPickerDialog: React.FunctionComponent<ContentPickerDialogPro
const itemsODataOptions = useMemo(() => ({ filter: '' }), [])

const handleSubmit = async (selection: GenericContentWithIsParent[]) => {
props.handleSubmit?.(selection[0].Path)
props.handleSubmit?.(selection[0])
closeLastDialog()
}

return (
<>
<DialogTitle>
<div className={globalClasses.centeredVertical}>
<Icon item={props.content} style={{ marginRight: '1em' }} />
{props.content && <Icon item={props.content} style={{ marginRight: '1em' }} />}
{localization.title}
</div>
</DialogTitle>

<Picker
repository={repository}
defaultValue={props.defaultValue ? [props.defaultValue] : undefined}
currentPath={props.currentPath}
selectionRoots={selectionRoots}
itemsODataOptions={itemsODataOptions}
Expand All @@ -56,7 +59,7 @@ export const ContentPickerDialog: React.FunctionComponent<ContentPickerDialogPro
actionsContainer={DialogActions}
handleCancel={closeLastDialog}
handleSubmit={handleSubmit}
required={1}
required={props.required ? 1 : 0}
localization={{ cancelButton: localization.cancelButton, submitButton: localization.selectButton }}
classes={{ cancelButton: globalClasses.cancelButton }}
/>
Expand Down
130 changes: 130 additions & 0 deletions apps/sensenet/src/components/dialogs/date-range-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Button, createStyles, DialogActions, DialogContent, makeStyles, Theme } from '@material-ui/core'
import React, { useState } from 'react'
import DayPicker, { DateUtils, Modifier } from 'react-day-picker'
import MomentLocaleUtils from 'react-day-picker/moment'
import { useGlobalStyles } from '../../globalStyles'
import { useLocalization, usePersonalSettings } from '../../hooks'
import { useDialog } from '.'

import 'react-day-picker/lib/style.css'

const useStyles = makeStyles((theme: Theme) => {
return createStyles({
root: {
'& .DayPicker-wrapper, & .DayPicker-NavButton': {
outline: 0,
},
'& .DayPicker-Day': {
borderRadius: '0 !important',
},
'& .DayPicker-Day--selected:not(.DayPicker-Day--disabled):not(.DayPicker-Day--outside)': {
backgroundColor: theme.palette.primary.main,
},
},
})
})

export interface DateRangePickerProps {
defaultValue?: {
from: Date
to: Date
}
handleSubmit: (range: { from?: Date; to?: Date }) => void
}

export const DateRangePicker: React.FunctionComponent<DateRangePickerProps> = (props) => {
const { closeLastDialog } = useDialog()
const globalClasses = useGlobalStyles()
const classes = useStyles()
const localization = useLocalization().dateRangePicker
const personalSettings = usePersonalSettings()
const langCode = personalSettings.language === 'hungarian' ? 'hu' : 'en'

const [from, setFrom] = useState<Date | undefined>(props.defaultValue?.from)
const [to, setTo] = useState<Date | undefined>(props.defaultValue?.to)
const [enteredTo, setEnteredTo] = useState<Date | undefined>(props.defaultValue?.to)

const isSelectingFirstDay = (day: Date) => {
const isBeforeFirstDay = from && DateUtils.isDayBefore(day, from)
const isRangeSelected = from && to
return !from || isBeforeFirstDay || isRangeSelected
}

const handleDayClick = (day: Date) => {
if (from && to && day >= from && day <= to) {
handleResetClick()
return
}
if (isSelectingFirstDay(day)) {
setFrom(day)
setTo(undefined)
setEnteredTo(undefined)
} else {
setTo(day)
setEnteredTo(day)
}
}

const handleDayMouseEnter = (day: Date) => {
if (!isSelectingFirstDay(day)) {
setEnteredTo(day)
}
}

const handleResetClick = () => {
setFrom(undefined)
setTo(undefined)
setEnteredTo(undefined)
}

const modifiers = { start: from, end: enteredTo }
const disabledDays: Modifier = from ? { before: from } : undefined
const selectedDays: Modifier[] = from && enteredTo ? [from, { from, to: enteredTo }] : from ? [from] : []

return (
<>
<DialogContent>
<DayPicker
className={classes.root}
firstDayOfWeek={1}
fixedWeeks
numberOfMonths={2}
fromMonth={from}
toMonth={new Date()}
selectedDays={selectedDays}
disabledDays={disabledDays}
modifiers={modifiers}
onDayClick={handleDayClick}
onDayMouseEnter={handleDayMouseEnter}
localeUtils={MomentLocaleUtils}
locale={langCode}
/>
</DialogContent>
<DialogActions>
<Button
aria-label={localization.resetButton}
disabled={!from || !to}
color="secondary"
variant="contained"
onClick={handleResetClick}>
{localization.resetButton}
</Button>
<Button
aria-label={localization.submitButton}
color="primary"
variant="contained"
onClick={() => props.handleSubmit({ from, to })}>
{localization.submitButton}
</Button>
<Button
aria-label={localization.cancelButton}
className={globalClasses.cancelButton}
onClick={() => closeLastDialog()}>
{localization.cancelButton}
</Button>
</DialogActions>
</>
)
}

export default DateRangePicker
2 changes: 2 additions & 0 deletions apps/sensenet/src/components/dialogs/dialog-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ContentPickerDialogProps,
CopyMoveDialogProps,
CustomActionResultDialogProps,
DateRangePickerProps,
DeleteContentDialogProps,
ExecuteActionDialogProps,
PermissionEditorDialogProps,
Expand Down Expand Up @@ -37,6 +38,7 @@ export type DialogWithProps = (
| { name: 'content-picker'; props: ContentPickerDialogProps }
| { name: 'feedback' }
| { name: 'change-password' }
| { name: 'date-range-picker'; props: DateRangePickerProps }
) & { dialogProps?: Partial<DialogProps> }

type Action = { type: 'PUSH_DIALOG'; dialog: DialogWithProps } | { type: 'POP_DIALOG' } | { type: 'CLOSE_ALL_DIALOGS' }
Expand Down
3 changes: 3 additions & 0 deletions apps/sensenet/src/components/dialogs/dialogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const Restore = React.lazy(() => import('./restore'))
const ContentPicker = React.lazy(() => import('./content-picker'))
const Feedback = React.lazy(() => import('./feedback'))
const ChangePasswordDialog = React.lazy(() => import('./change-password'))
const DateRangePicker = React.lazy(() => import('./date-range-picker'))

function dialogRenderer(dialog: DialogWithProps) {
switch (dialog.name) {
Expand Down Expand Up @@ -60,6 +61,8 @@ function dialogRenderer(dialog: DialogWithProps) {
return <Feedback />
case 'change-password':
return <ChangePasswordDialog />
case 'date-range-picker':
return <DateRangePicker {...dialog.props} />
default:
return null
}
Expand Down
1 change: 1 addition & 0 deletions apps/sensenet/src/components/dialogs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './check-in'
export * from './content-picker'
export * from './copy-move'
export * from './custom-action-result'
export * from './date-range-picker'
export * from './delete'
export * from './dialog-provider'
export * from './dialog-title'
Expand Down
5 changes: 3 additions & 2 deletions apps/sensenet/src/components/dialogs/restore.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PathHelper } from '@sensenet/client-utils'
import { TrashBag } from '@sensenet/default-content-types'
import { GenericContent, TrashBag } from '@sensenet/default-content-types'
import { useLogger, useRepository } from '@sensenet/hooks-react'
import { Button, DialogActions, DialogContent, InputAdornment, TextField } from '@material-ui/core'
import RestoreIcon from '@material-ui/icons/RestoreFromTrash'
Expand Down Expand Up @@ -95,7 +95,8 @@ export function Restore(props: RestoreProps) {
content: props.content,
currentPath: props.content.OriginalPath || '/Root',
selectionRoot: rootPath,
handleSubmit: (path: string) => setDestination(path),
handleSubmit: (content: GenericContent) => setDestination(content.Path),
required: true,
},
dialogProps: { disableBackdropClick: true, open: true, classes: { paper: globalClasses.pickerDialog } },
})
Expand Down
9 changes: 6 additions & 3 deletions apps/sensenet/src/components/dialogs/save-query.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { Button, DialogActions, DialogContent, TextField } from '@material-ui/co
import React, { useState } from 'react'
import { useGlobalStyles } from '../../globalStyles'
import { useLocalization } from '../../hooks'
import { createSearchQuery } from '../../services/search-query-builder'
import { SearchFilters } from '../search'
import { DialogTitle, useDialog } from '.'

export type SaveQueryProps = {
saveName?: string
query: string
filters: SearchFilters
}

export function SaveQuery(props: SaveQueryProps) {
Expand All @@ -30,9 +32,10 @@ export function SaveQuery(props: SaveQueryProps) {
select: ['DisplayName', 'Query'],
},
body: {
query: props.query,
query: createSearchQuery(props.filters).toString(),
displayName: saveName,
queryType: 'Public',
uiFilters: JSON.stringify(props.filters),
},
})
logger.information({
Expand All @@ -52,7 +55,7 @@ export function SaveQuery(props: SaveQueryProps) {
<DialogContent style={{ minWidth: 450 }}>
<TextField
fullWidth={true}
defaultValue={localization.saveInputPlaceholder(props.query)}
defaultValue={localization.saveInputPlaceholder(props.filters.term)}
onChange={(ev) => setSaveName(ev.currentTarget.value)}
/>
</DialogContent>
Expand Down
Loading

0 comments on commit dcc5f2b

Please sign in to comment.