@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
395 lines (393 loc) • 12.4 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, { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/KeyboardArrowDown';
import { camelize } from '../../utils/string';
import { makeStyles } from 'tss-react/mui';
import CheckIcon from '@mui/icons-material/Check';
import SiteSearchSortBy from '../SiteSearchSortBy';
import SiteSearchSortOrder from '../SiteSearchSortOrder';
import SiteSearchFilter from '../SiteSearchFilter';
import PathSelector from '../SiteSearchPathSelector';
import Button from '@mui/material/Button';
import palette from '../../styles/palette';
import MuiAccordion, { accordionClasses } from '@mui/material/Accordion';
import MuiAccordionSummary, { accordionSummaryClasses } from '@mui/material/AccordionSummary';
import AccordionDetails from '@mui/material/AccordionDetails';
import Divider from '@mui/material/Divider';
import { useSpreadState } from '../../hooks/useSpreadState';
import { styled } from '@mui/material/styles';
import useContentTypes from '../../hooks/useContentTypes';
import { getMimeTypeTranslation } from '../../utils/mimeTypes';
import Box from '@mui/material/Box';
const AccordionSummary = styled(MuiAccordionSummary)(() => ({
[`&.${accordionSummaryClasses.expanded}`]: {
minHeight: 0,
[`& .${accordionSummaryClasses.content}`]: {
margin: '12px 0'
}
}
}));
const Accordion = styled(MuiAccordion)(() => ({
[`&.${accordionClasses.expanded}`]: { margin: 'auto' }
}));
const useStyles = makeStyles()((theme) => ({
header: {
width: '100%',
padding: '10px 15px 10px 20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
border: 'none',
color: theme.palette.mode === 'dark' ? palette.white : ''
},
accordionTitle: {
display: 'flex',
fontWeight: 600,
alignItems: 'center'
},
divider: {
width: 'auto',
margin: '0 10px'
}
}));
const messages = defineMessages({
path: {
id: 'words.path',
defaultMessage: 'Path'
},
sortBy: {
id: 'searchFilter.sortBy',
defaultMessage: 'Sort By'
},
relevance: {
id: 'words.relevance',
defaultMessage: 'Relevance'
},
internalName: {
id: 'searchFilter.internalName',
defaultMessage: 'Name'
},
width: {
id: 'words.width',
defaultMessage: 'Width'
},
contentType: {
id: 'searchFilter.contentType',
defaultMessage: 'Content Type'
},
mimeType: {
id: 'searchFilter.mimeType',
defaultMessage: 'MIME Type'
},
size: {
id: 'searchFilter.size',
defaultMessage: 'Content Size'
},
lastEditDate: {
id: 'searchFilter.lastEditDate',
defaultMessage: 'Last Edit Date'
},
height: {
id: 'words.height',
defaultMessage: 'Height'
},
clearFilters: {
id: 'searchFilter.clearFilters',
defaultMessage: 'Clear Filters'
},
expandAll: {
id: 'common.expandAll',
defaultMessage: 'Expand All'
},
collapseAll: {
id: 'common.collapseAll',
defaultMessage: 'Collapse All'
}
});
const filterToFacet = (filterKey, filterValue) => {
const isMultiple = typeof filterValue === 'object';
const isDate = !isMultiple && filterValue.includes('TODATE');
const isRange = !isMultiple && !isDate && filterValue.includes('TO');
const name = filterKey;
let values = {};
if (isMultiple) {
Object.keys(filterValue).forEach((value) => {
values[value] = 0;
});
} else if (isDate) {
const deserializedValue = filterValue.match(/(.+)TODATE(.+)ID(.+)/);
const id = deserializedValue[3].replace(filterKey, '');
const from = deserializedValue[1] === 'null' ? null : deserializedValue[1];
const to = deserializedValue[2] === 'null' ? null : deserializedValue[2];
values[id] = {
count: 0,
from,
to
};
} else {
const deserializedValue = filterValue.match(/(.+)?TO(.+)?/);
const rangeStart = deserializedValue[1];
const rangeEnd = deserializedValue[2];
const id = `${rangeStart !== null && rangeStart !== void 0 ? rangeStart : '*'}-${
rangeEnd !== null && rangeEnd !== void 0 ? rangeEnd : '*'
}`;
values[id] = {
count: 0,
from: rangeStart !== null && rangeStart !== void 0 ? rangeStart : null,
to: rangeEnd !== null && rangeEnd !== void 0 ? rangeEnd : null
};
}
return {
date: isDate,
multiple: isMultiple,
name,
range: isRange,
values
};
};
export function SiteSearchFilters(props) {
const { classes } = useStyles();
// region const { ... } = props
const {
sortBy,
sortOrder,
facets,
handleFilterChange,
mode,
checkedFilters,
setCheckedFilters,
clearFilters,
handleClearClick,
selectedPath,
setSelectedPath
} = props;
// endregion
const { formatMessage } = useIntl();
const [expanded, setExpanded] = useSpreadState({
sortBy: false,
path: false
});
const contentTypes = useContentTypes();
let filterKeys = [];
let facetsLookupTable = {};
const facetLabelLookup = {};
const parsedSelectedPath =
selectedPath === null || selectedPath === void 0
? void 0
: selectedPath.trim().replace('.+', '').replace(/\/$/, '');
// Don't want the root path set everytime select path changes. Only when select mode is first detected for it to get set once.
// TODO: Ultimately, the rootPath should change to be driven by a different explicit prop other than `mode`.
// eslint-disable-next-line react-hooks/exhaustive-deps
const rootPath = useMemo(() => (mode === 'select' ? parsedSelectedPath : null), [mode]);
const addFacetValuesLabels = (facet) => {
Object.keys(facet.values).forEach((value) => {
var _a, _b;
let label = value;
if (facet.name === 'content-type') {
label =
(_b =
(_a = contentTypes === null || contentTypes === void 0 ? void 0 : contentTypes[value]) === null ||
_a === void 0
? void 0
: _a.name) !== null && _b !== void 0
? _b
: value;
} else if (facet.name === 'mime-type') {
label = getMimeTypeTranslation(value, formatMessage);
}
facetLabelLookup[value] = label;
});
};
facets.forEach((facet) => {
filterKeys.push(facet.name);
facetsLookupTable[facet.name] = facet;
addFacetValuesLabels(facet);
});
// Add filters already selected not coming from facets
Object.keys(checkedFilters).forEach((filterKey) => {
if (!filterKeys.includes(filterKey)) {
filterKeys.push(filterKey);
facetsLookupTable[filterKey] = filterToFacet(filterKey, checkedFilters[filterKey]);
}
});
const handleExpandClick = (item) => {
setExpanded({ [item]: !expanded[item] });
};
const pathToFilter = (path) => {
if (path) {
if (path.endsWith('/')) {
return `${path}.+`;
} else {
return `${path}/.+`;
}
} else {
return undefined;
}
};
const onPathSelected = (path) => {
handleFilterChange({
name: 'path',
value: pathToFilter(path)
});
setSelectedPath(path);
};
const expandCollapseAll = (expand) => {
const state = {};
state.path = expand;
state.sortBy = expand;
filterKeys.forEach((key) => {
state[key] = expand;
});
setExpanded(state);
};
return React.createElement(
Box,
null,
React.createElement(
Box,
{ sx: { p: 1, display: 'flex', justifyContent: 'space-evenly' } },
React.createElement(
Button,
{ size: 'small', onClick: () => expandCollapseAll(true) },
formatMessage(messages.expandAll)
),
React.createElement(
Button,
{ size: 'small', onClick: () => expandCollapseAll(false) },
formatMessage(messages.collapseAll)
),
React.createElement(
Button,
{
size: 'small',
disabled: Object.keys(checkedFilters).length === 0 && !Boolean(selectedPath),
onClick: clearFilters
},
formatMessage(messages.clearFilters)
)
),
React.createElement(Divider, { className: classes.divider }),
React.createElement(
Accordion,
{ expanded: expanded.sortBy, elevation: 0, onChange: () => handleExpandClick('sortBy') },
React.createElement(
AccordionSummary,
{ expandIcon: React.createElement(ExpandMoreIcon, null) },
React.createElement(
Typography,
{ className: classes.accordionTitle },
formatMessage(messages.sortBy),
sortBy && React.createElement(CheckIcon, null)
)
),
React.createElement(
AccordionDetails,
null,
React.createElement(
React.Fragment,
null,
React.createElement(SiteSearchSortBy, {
sortBy: sortBy,
filterKeys: filterKeys,
handleFilterChange: handleFilterChange
}),
React.createElement(SiteSearchSortOrder, {
sortOrder: sortOrder,
sortBy: sortBy,
handleFilterChange: handleFilterChange
})
)
)
),
React.createElement(
Accordion,
{ expanded: expanded.path, elevation: 0, onChange: () => handleExpandClick('path') },
React.createElement(
AccordionSummary,
{ expandIcon: React.createElement(ExpandMoreIcon, null) },
React.createElement(
Typography,
{ className: classes.accordionTitle },
formatMessage(messages.path),
selectedPath && React.createElement(CheckIcon, null)
)
),
React.createElement(
AccordionDetails,
null,
React.createElement(PathSelector, {
value: parsedSelectedPath,
onPathSelected: onPathSelected,
rootPath: rootPath
})
)
),
filterKeys.map((key) => {
var _a;
return React.createElement(
Accordion,
{
key: key,
expanded: (_a = expanded[key]) !== null && _a !== void 0 ? _a : false,
elevation: 0,
onChange: () => handleExpandClick(key)
},
React.createElement(
AccordionSummary,
{ expandIcon: React.createElement(ExpandMoreIcon, null) },
React.createElement(
Typography,
{ className: classes.accordionTitle },
formatMessage(messages[camelize(key)]),
checkedFilters[key] && React.createElement(CheckIcon, null)
)
),
React.createElement(
AccordionDetails,
null,
React.createElement(SiteSearchFilter, {
facet: key,
handleFilterChange: handleFilterChange,
checkedFilters: checkedFilters,
setCheckedFilters: setCheckedFilters,
facetsLookupTable: facetsLookupTable,
facetLabelLookup: facetLabelLookup,
handleClearClick: handleClearClick
})
)
);
})
);
}
export default SiteSearchFilters;