diff --git a/src/api/ologApi.js b/src/api/ologApi.js index 7f15179..d30f2c9 100644 --- a/src/api/ologApi.js +++ b/src/api/ologApi.js @@ -17,7 +17,6 @@ */ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; -import { withoutCacheBust } from "hooks/useSanitizedSearchParams"; import customization from "config/customization"; import { useCallback } from "react"; @@ -90,19 +89,14 @@ export const ologApi = createApi({ keepUnusedDataFor: 0, // Don't cache anything endpoints: builder => ({ searchLogs: builder.query({ - query: ({searchParams, searchPageParams}) => { - return { - url: '/logs/search', - params: { - // remove cache bust so we don't send it to the server - // we must do it *here* rather than where useSearchLogsQuery - // is called because we want repeated searches of the same - // search to trigger calling the api (e.g. polling) - ...withoutCacheBust(searchParams), - ...searchPageParams - } - } + query: ({query, title, desc, start, end, level, logbooks, tags, owner, attachments, from, size, sort}) => { + return { + url: '/logs/search', + params: { + query, title, desc, start, end, level, logbooks, tags, owner, attachments, from, size, sort + } } + } }), getTags: builder.query({ query: () => ({ diff --git a/src/beta/components/log/EntryTypeChip.jsx b/src/beta/components/log/EntryTypeChip.jsx index acb8653..9f5a773 100644 --- a/src/beta/components/log/EntryTypeChip.jsx +++ b/src/beta/components/log/EntryTypeChip.jsx @@ -1,12 +1,12 @@ import { Chip } from "@mui/material"; import React from "react"; -export const EntryTypeChip = ({name, ...props}) => { +export const EntryTypeChip = ({value, ...props}) => { return ( { } {log?.logbooks?.map(it => )}, - Tags: {log?.tags?.map(it => )}, - "Entry Type": + Logbooks: {log?.logbooks?.map(it => )}, + Tags: {log?.tags?.map(it => )}, + "Entry Type": }} /> diff --git a/src/beta/components/log/LogDetails/MetadataTable.js b/src/beta/components/log/LogDetails/MetadataTable.js index 6acce1f..a8756a2 100644 --- a/src/beta/components/log/LogDetails/MetadataTable.js +++ b/src/beta/components/log/LogDetails/MetadataTable.js @@ -9,7 +9,7 @@ const Value = styled(Typography)({ gridColumn: "span / span" }); -const MetadataTable = styled(({data, className}) => { +const MetadataTable = styled(({data, KeyProps, ValueProps, className}) => { return ( { > {Object.entries(data).map(entry => - {entry[0]} - {entry[1]} + {entry[0]} + {entry[1]} )} diff --git a/src/beta/components/log/LogbookChip.jsx b/src/beta/components/log/LogbookChip.jsx index 4ea0a48..2cefb97 100644 --- a/src/beta/components/log/LogbookChip.jsx +++ b/src/beta/components/log/LogbookChip.jsx @@ -1,11 +1,11 @@ import React from "react"; import { Chip } from "@mui/material"; -export const LogbookChip = ({name, ...props}) => { +export const LogbookChip = ({value, ...props}) => { return ( { +export const TagChip = ({value, ...props}) => { return ( } size="small" variant="outlined" diff --git a/src/beta/components/log/TextChip.jsx b/src/beta/components/log/TextChip.jsx new file mode 100644 index 0000000..b0425d2 --- /dev/null +++ b/src/beta/components/log/TextChip.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import { Chip } from "@mui/material"; + +export const TextChip = ({name, value, ...props}) => { + return ( + + ); +}; \ No newline at end of file diff --git a/src/beta/components/search/AdvancedSearch.jsx b/src/beta/components/search/AdvancedSearch.jsx new file mode 100644 index 0000000..c81387e --- /dev/null +++ b/src/beta/components/search/AdvancedSearch.jsx @@ -0,0 +1,142 @@ +import React, { useEffect } from "react"; +import { Button, Stack, Typography } from "@mui/material"; +import TextInput from "components/shared/input/TextInput"; +import WizardDateInput from "components/shared/input/WizardDateInput"; +import EntryTypeSelect from "components/shared/input/managed/EntryTypeSelect"; +import LogbooksMultiSelect from "components/shared/input/managed/LogbooksMultiSelect"; +import TagsMultiSelect from "components/shared/input/managed/TagsMultiSelect"; +import { useForm, useWatch } from "react-hook-form"; +import { defaultSearchParams } from "features/searchParamsReducer"; +import FilterAltIcon from '@mui/icons-material/FilterAlt'; + + +const isDate = (obj) => { + return obj instanceof Date && !isNaN(obj); +} +const toDate = (dateString) => { + if(isDate(dateString)) { + return new Date(dateString); + } else { + return null; + } +} + +export const AdvancedSearch = ({search, onSearchChange, onSearchSubmit}) => { + + const values = {...defaultSearchParams, ...search}; + + const form = useForm({ + defaultValues: defaultSearchParams, + values: values + }); + + const { control, handleSubmit, getValues } = form; + + const liveValues = useWatch({control, defaultValue: values}); + + // send updated form values on any form change + useEffect(() => { + onSearchChange(liveValues); + }, [liveValues, onSearchChange]); + + // invoke submit callback when submitted + const onSubmit = () => { + if (onSearchSubmit) { + onSearchSubmit(); + } + } + + return ( + + Filters + + + + + + + + { + const startDate = toDate(val); + const endDate = toDate(getValues('end')); + if(startDate && endDate) { + return startDate <= endDate || 'Start date cannot come after end date' + } else { + return true; + } + } + } + }} + /> + { + const startDate = toDate(getValues('start')); + const endDate = toDate(val); + if(startDate && endDate) { + return endDate > startDate || 'End date cannot come before start date' + } else { + return true; + } + } + } + }} + /> + + + + + + + ); + +}; \ No newline at end of file diff --git a/src/beta/components/search/SearchParamsBadges.jsx b/src/beta/components/search/SearchParamsBadges.jsx new file mode 100644 index 0000000..c5bbce9 --- /dev/null +++ b/src/beta/components/search/SearchParamsBadges.jsx @@ -0,0 +1,55 @@ +import React from "react"; +import { Stack } from "@mui/material"; +import { EntryTypeChip } from "../log/EntryTypeChip"; +import { LogbookChip } from "../log/LogbookChip"; +import { TagChip } from "../log/TagChip"; +import { TextChip } from "../log/TextChip"; +import { defaultSearchParams } from "features/searchParamsReducer"; + +const LogbooksList = ({logbooks, onChange}) => { + + const onDelete = (logbook) => { + const updated = logbooks.filter(it => it.name !== logbook.name); + onChange(updated); + } + + return ( + <> + {logbooks.map(logbook => onDelete(logbook)} />)} + + ) +} + +const TagsList = ({tags, onChange}) => { + + const onDelete = (tag) => { + const updated = tags.filter(it => it.name !== tag.name); + onChange(updated); + } + + return ( + <> + {tags.map(tag => onDelete(tag)} />)} + + ) +} + +export const SearchParamsBadges = ({search, onSearch}) => { + + const { title, level, desc, owner, attachments, start, end, tags, logbooks } = search; + + return ( + + {title ? onSearch({...search, title: defaultSearchParams.title})} /> : null} + {level ? onSearch({...search, level: defaultSearchParams.level})} /> : null} + {desc ? onSearch({...search, desc: defaultSearchParams.desc})} /> : null} + {owner ? onSearch({...search, owner: defaultSearchParams.owner})} /> : null} + {attachments ? onSearch({...search, attachments: defaultSearchParams.attachments})} /> : null} + {start ? onSearch({...search, start: defaultSearchParams.start})} /> : null} + {end ? onSearch({...search, end: defaultSearchParams.end})} /> : null} + {tags ? onSearch({...search, tags: val})} /> : null} + {logbooks ? onSearch({...search, logbooks: val})} /> : null} + + ); + +}; \ No newline at end of file diff --git a/src/beta/components/search/SearchResultList/AdvancedSearchDrawer.jsx b/src/beta/components/search/SearchResultList/AdvancedSearchDrawer.jsx new file mode 100644 index 0000000..4d99a3c --- /dev/null +++ b/src/beta/components/search/SearchResultList/AdvancedSearchDrawer.jsx @@ -0,0 +1,46 @@ +import { Box, Drawer } from "@mui/material"; +import { defaultSearchParams, updateSearchParams } from "features/searchParamsReducer"; +import React, { useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { AdvancedSearch } from "../AdvancedSearch"; + +export const AdvancedSearchDrawer = ({searchParams, advancedSearchOpen, setAdvancedSearchOpen}) => { + + const dispatch = useDispatch(); + + // Keep a separate local state so we can decide when to submit it + const [localSearchParams, setLocalSearchParams] = useState(defaultSearchParams); + + // sync changes to external search params with local state + useEffect(() => { + setLocalSearchParams(searchParams); + }, [searchParams]) + + // When search changes, update local state + const onSearchChange = (values) => { + setLocalSearchParams(values); + } + + // When closed: + // - close the drawer (obviously) + // - update the external search params state + const onAdvancedSearchClose = () => { + setAdvancedSearchOpen(false); + dispatch(updateSearchParams(localSearchParams)); + } + + return ( + + + + + + ); +}; \ No newline at end of file diff --git a/src/beta/components/search/SearchResultList/SearchResultSingleItem.jsx b/src/beta/components/search/SearchResultList/SearchResultSingleItem.jsx index 8043759..a43c7fb 100644 --- a/src/beta/components/search/SearchResultList/SearchResultSingleItem.jsx +++ b/src/beta/components/search/SearchResultList/SearchResultSingleItem.jsx @@ -45,8 +45,8 @@ export const SearchResultSingleItem = ({log, selected, onClick}) => { /> - {log?.logbooks?.map(it => )} - {log?.tags?.map(it => )} + {log?.logbooks?.map(it => )} + {log?.tags?.map(it => )} ); diff --git a/src/beta/components/search/SearchResults.js b/src/beta/components/search/SearchResults.js index 0421cd6..78dd5d9 100644 --- a/src/beta/components/search/SearchResults.js +++ b/src/beta/components/search/SearchResults.js @@ -1,22 +1,27 @@ -import { Alert, Box, Divider, IconButton, LinearProgress, Stack, TablePagination, Typography, styled } from "@mui/material"; +import { Alert, Badge, Box, Divider, IconButton, LinearProgress, Stack, TablePagination, Typography, styled } from "@mui/material"; import { ologApi, removeEmptyKeys } from "api/ologApi"; import customization from "config/customization"; import { updateCurrentLogEntry, useCurrentLogEntry } from "features/currentLogEntryReducer"; import { updateSearchPageParams, useSearchPageParams } from "features/searchPageParamsReducer"; -import { useSearchParams } from "features/searchParamsReducer"; -import React, { useEffect, useState } from "react"; +import { updateSearchParams, useSearchParams } from "features/searchParamsReducer"; +import React, { useEffect, useMemo, useState } from "react"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; import SearchResultList from "./SearchResultList"; import SimpleSearch from "./SimpleSearch"; import { SortToggleButton } from "./SortToggleButton"; import FilterAltIcon from '@mui/icons-material/FilterAlt'; +import { SearchParamsBadges } from "./SearchParamsBadges"; +import { AdvancedSearchDrawer } from "./SearchResultList/AdvancedSearchDrawer"; +import { useAdvancedSearch } from "features/advancedSearchReducer"; +import { withCacheBust } from "hooks/useSanitizedSearchParams"; export const SearchResults = styled(({className}) => { const dispatch = useDispatch(); const navigate = useNavigate(); + const { active: advancedSearchActive, fieldCount: advancedSearchFieldCount } = useAdvancedSearch(); const currentLogEntry = useCurrentLogEntry(); const searchParams = useSearchParams(); const searchPageParams = useSearchPageParams(); @@ -28,6 +33,40 @@ export const SearchResults = styled(({className}) => { ? searchPageParams?.size : customization.defaultPageSize ); + const [advancedSearchOpen, setAdvancedSearchOpen] = useState(false); + + const searchLogsQuery = useMemo(() => { + + let params = { + ...searchPageParams, + sort: searchPageParams.dateDescending ? "down" : "up" + }; + + if (advancedSearchActive) { + params = { + ...params, + ...searchParams + }; + if(params.tags) { + params.tags = params.tags.map(it => it.name); + } + if(params.logbooks) { + params.logbooks = params.logbooks.map(it => it.name); + } + if(params.query) { + delete params.query; + } + } else { + params = { + ...params, + query: searchParams.query, + start: searchParams.start + }; + } + + return withCacheBust(removeEmptyKeys(params)); + + }, [searchPageParams, searchParams, advancedSearchActive]); const { data: searchResults={ @@ -37,13 +76,7 @@ export const SearchResults = styled(({className}) => { error, isFetching: loading } = ologApi.endpoints.searchLogs.useQuery( - { - searchParams: {...removeEmptyKeys({...searchParams})}, - searchPageParams: { - ...searchPageParams, - sort: searchPageParams.dateDescending ? "down" : "up" - } - }, + searchLogsQuery, { pollingInterval: customization.defaultSearchFrequency, refetchOnMountOrArgChange: true, @@ -89,19 +122,23 @@ export const SearchResults = styled(({className}) => { divider={} position="relative" > + - + { advancedSearchActive ? null : } + dispatch(updateSearchParams(vals))} /> - Search Results + Results {searchResults.hitCount > 0 ? {searchPageParams.from + 1}-{remaining} of {count} : null } - - + setAdvancedSearchOpen(true)}> + + + @@ -126,7 +163,7 @@ export const SearchResults = styled(({className}) => { onRowClick={onRowClick} /> : - No Results + No records found } { - const searchParams = useSearchParams(); const dispatch = useDispatch(); + const searchParams = useSearchParams(); + const { active: advancedSearchActive } = useAdvancedSearch(); - const { control, handleSubmit, setValue, watch, getValues } = useForm({ - defaultValues: { query: searchParams?.query, start: searchParams?.start }, + const { control, handleSubmit, setValue, getValues } = useForm({ + defaultValues: { query: defaultSearchParams.query, start: defaultSearchParams.start }, values: { query: searchParams?.query, start: searchParams?.start } }); - const startDateValue = watch("start"); - const onAccept = (momentDate) => { - console.log({onAccept: momentDate}) setValue("start", momentDate.format(DATE_FORMAT)); onSubmit(getValues()); } - const onReject = () => { - setValue("start", null); - onSubmit(getValues()); - } const onSubmit = (data) => { - console.log({onSubmit: data, searchParams}) const params = { ...searchParams, ...data } - dispatch(forceUpdateSearchParams(params)); + dispatch(updateSearchParams(params)); } return ( - { defaultValue="" fullWidth InputProps={{ - endAdornment: ( + disabled: advancedSearchActive, + endAdornment: - ) }} /> - - { - startDateValue ? : null - } - ) - }; export default SimpleSearch; \ No newline at end of file diff --git a/src/beta/components/search/SimpleSearch/index.js b/src/beta/components/search/SimpleSearch/index.js deleted file mode 100644 index 4f3c77f..0000000 --- a/src/beta/components/search/SimpleSearch/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import SimpleSearch from "./SimpleSearch"; - -export { SimpleSearch }; -export default SimpleSearch; \ No newline at end of file diff --git a/src/components/Filters/Filters.js b/src/components/Filters/Filters.js index 6ee59e3..8664c2c 100644 --- a/src/components/Filters/Filters.js +++ b/src/components/Filters/Filters.js @@ -17,7 +17,7 @@ */ import { useForm } from 'react-hook-form'; import { useDispatch } from 'react-redux'; -import { forceUpdateSearchParams, useSearchParams } from 'features/searchParamsReducer'; +import { updateSearchParams, useSearchParams } from 'features/searchParamsReducer'; import { updateSearchPageParams, useSearchPageParams } from 'features/searchPageParamsReducer'; import Collapse from './Collapse'; import TextInput from 'components/shared/input/TextInput'; @@ -54,7 +54,7 @@ const Filters = ({showFilters, className}) => { const updatedSearchPageParams = {...searchPageParams, sort: data.sort} dispatch(updateSearchPageParams(updatedSearchPageParams)); } - dispatch(forceUpdateSearchParams(updatedSearchParams)); + dispatch(updateSearchParams(updatedSearchParams)); } diff --git a/src/components/LogEntriesView/LogEntriesView.js b/src/components/LogEntriesView/LogEntriesView.js index ac0e134..8b8a600 100644 --- a/src/components/LogEntriesView/LogEntriesView.js +++ b/src/components/LogEntriesView/LogEntriesView.js @@ -30,6 +30,7 @@ import Filters from 'components/Filters'; import { Typography, styled } from '@mui/material'; import { grey } from '@mui/material/colors'; import { useSearchParams } from 'features/searchParamsReducer'; +import { withCacheBust } from 'hooks/useSanitizedSearchParams'; const ContentContainer = styled("div")( ({theme}) => ({ height: "100%", @@ -94,19 +95,23 @@ const LogEntriesView = () => { const searchParams = useSearchParams(); const searchPageParams = useSearchPageParams(); const searchLogsQuery = useMemo(() => { - - const sanitizedSearchParams = {...searchParams}; - if(searchParams.tags) { - sanitizedSearchParams.tags = searchParams.tags.map(it => it.name); - } - if(searchParams.logbooks) { - sanitizedSearchParams.logbooks = searchParams.logbooks.map(it => it.name); - } - - return { - searchParams: removeEmptyKeys(sanitizedSearchParams), - searchPageParams - }; + + let params = { + ...searchPageParams, + ...searchParams + }; + + if(params.tags) { + params.tags = params.tags.map(it => it.name); + } + if(params.logbooks) { + params.logbooks = params.logbooks.map(it => it.name); + } + if(params.query) { + delete params.query; + } + + return withCacheBust(removeEmptyKeys(params)); }, [searchParams, searchPageParams]); diff --git a/src/components/log/EntryEditor/Description/Description.js b/src/components/log/EntryEditor/Description/Description.js index 1787f93..f5c67a6 100644 --- a/src/components/log/EntryEditor/Description/Description.js +++ b/src/components/log/EntryEditor/Description/Description.js @@ -155,17 +155,17 @@ const Description = ({form, attachmentsDisabled }) => { /> - - - - CommonMark Formatting Help - - - } - label="CommonMark Formatting Help" - /> + + + + + CommonMark Formatting Help + + +