@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
240 lines (238 loc) • 9.07 kB
JavaScript
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useMemo, useRef, useState } from 'react';
import queryString from 'query-string';
import { Subject } from 'rxjs';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { deserializeSearchFilters, initialSearchParameters, setCheckedParameterFromURL, useSearchState } from './utils';
import SearchUI from '../SearchUI';
import { UNDEFINED } from '../../utils/constants';
import { useLocation, useNavigate } from 'react-router-dom';
export function URLDrivenSearch(props) {
const { mode = 'default', onSelect, embedded = false, onAcceptSelection, onClose } = props;
const location = useLocation();
const push = useNavigate();
// region hooks
const refs = useRef({ createQueryString: null });
const queryParams = useMemo(() => queryString.parse(location.search), [location.search]);
const searchParameters = useMemo(() => setSearchParameters(initialSearchParameters, queryParams), [queryParams]);
const onSearch$ = useMemo(() => new Subject(), []);
const theme = useTheme();
const desktopScreen = useMediaQuery(theme.breakpoints.up('md'));
// endregion
// region state
const [keyword, setKeyword] = useState(queryParams['keywords'] || '');
const [checkedFilters, setCheckedFilters] = useState({});
// endregion
// region useSearchState({ ... })
const {
error,
isFetching,
areAllSelected,
selected,
currentView,
onActionClicked,
selectionOptions,
onHeaderButtonClick,
handleClearSelected,
handleSelect,
handleSelectAll,
onPreview,
guestBase,
searchResults,
selectedPath,
clearPath,
onSelectedPathChanges,
drawerOpen,
toggleDrawer,
handleChangeView
} = useSearchState({
searchParameters,
onSelect
});
// endregion
refs.current.createQueryString = createQueryString;
useEffect(() => {
const subscription = onSearch$.pipe(debounceTime(400), distinctUntilChanged()).subscribe((keywords) => {
if (!keywords) keywords = undefined;
let qs = refs.current.createQueryString({ name: 'keywords', value: keywords }, false, { offset: UNDEFINED });
push({
pathname: '/',
search: qs ? `?${qs}` : ''
});
});
return () => subscription.unsubscribe();
}, [push, onSearch$]);
useEffect(() => {
setCheckedFilters(setCheckedParameterFromURL(queryParams));
}, [queryParams, setCheckedFilters]);
function handleSearchKeyword(keyword) {
setKeyword(keyword);
onSearch$.next(keyword);
}
function handleFilterChange(filter, isFilter) {
let qs = createQueryString(filter, isFilter, { offset: UNDEFINED });
if (qs || location.search) {
push({
pathname: '/',
search: `?${qs}`
});
} else {
return false;
}
}
function clearFilter(facet) {
if (checkedFilters[facet]) {
if (typeof checkedFilters[facet] === 'string') {
setCheckedFilters({ ...checkedFilters, [facet]: '' });
} else {
let emptyFilter = { ...checkedFilters[facet] };
Object.keys(emptyFilter).forEach((name) => {
emptyFilter[name] = false;
});
setCheckedFilters({ ...checkedFilters, [facet]: emptyFilter });
}
}
handleFilterChange({ name: facet, value: undefined }, true);
}
function clearFilters() {
Object.keys(checkedFilters).map((filter) => clearFilter(filter));
// TODO: Should change the path clearing to depend on a more specific prop (e.g. `pathLock`)
if (mode !== 'select') {
handleFilterChange({ name: 'path', value: UNDEFINED });
onSelectedPathChanges(UNDEFINED);
clearPath();
}
}
// isFilter: It means that the filter is nested on object filter
function createQueryString(filter, isFilter = false, overrideQueryParams = {}) {
let newFilters;
let filters = queryParams['filters'];
filters = filters ? JSON.parse(filters) : {};
if (isFilter) {
filters[filter.name] = filter.value;
queryParams.filters = JSON.stringify(filters);
if (queryParams.filters === '{}') {
queryParams.filters = undefined;
}
newFilters = { ...queryParams, ...overrideQueryParams };
} else {
queryParams.filters = JSON.stringify(filters);
if (queryParams.filters === '{}') {
queryParams.filters = undefined;
}
// queryParams['sortBy'] === undefined: this means the current filter is the default === _score
if (
filter.name === 'sortBy' &&
(queryParams['sortBy'] === '_score' || queryParams['sortBy'] === undefined) &&
filter.value !== '_score'
) {
newFilters = { ...queryParams, [filter.name]: filter.value, sortOrder: 'asc', ...overrideQueryParams };
} else if (filter.name === 'sortBy' && queryParams['sortBy'] !== '_score' && filter.value === '_score') {
newFilters = { ...queryParams, [filter.name]: filter.value, sortOrder: 'desc', ...overrideQueryParams };
} else {
newFilters = { ...queryParams, [filter.name]: filter.value, ...overrideQueryParams };
}
}
return queryString.stringify(newFilters);
}
function setSearchParameters(initialSearchParameters, queryParams) {
let formatParameters = {
...queryParams,
...(queryParams.limit && { limit: Number(queryParams.limit) }),
...(queryParams.offset && { offset: Number(queryParams.offset) })
};
if (formatParameters.filters) {
formatParameters.filters = JSON.parse(formatParameters.filters);
formatParameters.filters = deserializeSearchFilters(formatParameters.filters);
}
return { ...initialSearchParameters, ...formatParameters };
}
function handleChangePage(event, newPage) {
let offset = newPage * searchParameters.limit;
let qs = refs.current.createQueryString({ name: 'offset', value: offset });
push({
pathname: '/',
search: `?${qs}`
});
}
function handleChangeRowsPerPage(event) {
let qs = refs.current.createQueryString({ name: 'limit', value: parseInt(event.target.value, 10) });
push({
pathname: '/',
search: `?${qs}`
});
}
const onCheckedFiltersChanges = (checkedFilters) => {
setCheckedFilters(checkedFilters);
};
return React.createElement(SearchUI, {
sortBy: queryParams['sortBy'],
sortOrder: queryParams['sortOrder'],
currentView: currentView,
embedded: embedded,
keyword: Array.isArray(keyword) ? keyword.join(' ') : keyword,
mode: mode,
checkedFilters: checkedFilters,
desktopScreen: desktopScreen,
drawerOpen: drawerOpen,
searchResults: searchResults,
selectedPath: selectedPath,
clearFilter: clearFilter,
toggleDrawer: toggleDrawer,
clearFilters: clearFilters,
handleChangeView: handleChangeView,
handleFilterChange: handleFilterChange,
handleSearchKeyword: handleSearchKeyword,
onSelectedPathChanges: onSelectedPathChanges,
onCheckedFiltersChanges: onCheckedFiltersChanges,
error: error,
isFetching: isFetching,
areAllSelected: areAllSelected,
guestBase: guestBase,
handleChangePage: handleChangePage,
handleChangeRowsPerPage: handleChangeRowsPerPage,
handleClearSelected: handleClearSelected,
handleSelect: handleSelect,
handleSelectAll: handleSelectAll,
onAcceptSelection: onAcceptSelection,
onActionClicked: onActionClicked,
onClose: onClose,
onHeaderButtonClick: onHeaderButtonClick,
onPreview: onPreview,
searchParameters: searchParameters,
selected: selected,
selectionOptions: selectionOptions
});
}
export default URLDrivenSearch;