Skip to content

Commit

Permalink
Add copy filters and resolvable URL query params feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
ewelinagr committed Jan 3, 2025
1 parent 7212381 commit 3ced2b8
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 44 deletions.
2 changes: 2 additions & 0 deletions projects/mercury/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const DATE_FORMAT = 'dd-MM-yyyy';
// The maximum number of items in a list in the right panel, for performance reasons.
// If you change this, also change it in 'MetadataService.java'
export const MAX_LIST_LENGTH = 100;
// Max length of URL in the browser
export const MAX_URL_LENGTH = 2000;

// Metadata schemas
export const SHACL_NS = 'http://www.w3.org/ns/shacl#';
Expand Down
91 changes: 72 additions & 19 deletions projects/mercury/src/metadata/views/MetadataView.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import _ from 'lodash';
import {useHistory} from 'react-router-dom';
import {Button, Grid, Typography} from '@mui/material';
import withStyles from '@mui/styles/withStyles';
import {Assignment, Close} from '@mui/icons-material';
import {Assignment, Close, ContentCopy} from '@mui/icons-material';
import queryString from 'query-string';
import qs from 'qs';

import {SnackbarProvider, useSnackbar} from 'notistack';
import styles from './MetadataView.styles';
import type {MetadataViewFacet, MetadataViewFilter, MetadataViewOptions, ValueType} from './MetadataViewAPI';
import BreadCrumbs from '../../common/components/BreadCrumbs';
Expand Down Expand Up @@ -34,6 +36,7 @@ import usePageTitleUpdater from '../../common/hooks/UsePageTitleUpdater';
import MetadataViewFacetsContext from './MetadataViewFacetsContext';
import {accessLevelForCollection} from '../../collections/collectionUtils';
import InternalMetadataSourceContext from '../metadata-sources/InternalMetadataSourceContext';
import {MAX_URL_LENGTH} from '../../constants';

type ContextualMetadataViewProperties = {
classes: any
Expand Down Expand Up @@ -70,6 +73,7 @@ export const MetadataView = (props: MetadataViewProperties) => {

const {collections} = useContext(CollectionsContext);
const {toggle, selected} = useSingleSelection();
const {enqueueSnackbar} = useSnackbar();

const [filterCandidates, setFilterCandidates] = useState([]);
const [textFiltersObject, setTextFiltersObject] = useState({});
Expand Down Expand Up @@ -199,7 +203,9 @@ export const MetadataView = (props: MetadataViewProperties) => {
};

const areFacetFiltersNonEmpty = useMemo(
() => filters && filters.some(filter => facetsEx.some(facet => facet.name === filter.field)),
() =>
filters &&
filters.some(filter => facetsEx.some(facet => facet.name.toLowerCase() === filter.field.toLowerCase())),
[filters, facetsEx]
);
const areTextFiltersNonEmpty = useMemo(
Expand All @@ -218,6 +224,48 @@ export const MetadataView = (props: MetadataViewProperties) => {
return `${window.location.host}${pathPrefix}?${prefilteringQueryString}`;
};

const copyFiltersUrl = () => {
const queryParams = filters.reduce((acc, filter) => {
acc[filter.field.toLowerCase()] = filter.values.join(',');
return acc;
}, {});
const queryStringFilters = queryString.stringify(queryParams);
const url = `${window.location.protocol}//${window.location.host}/metadata-views?view=${currentView.name}&${queryStringFilters}`;

if (url.length > MAX_URL_LENGTH) {
enqueueSnackbar('Failed to copy metadata view filters URL to clipboard: URL too long');
return;
}
navigator.clipboard
.writeText(url)
.then(() => enqueueSnackbar('Metadata view filters URL copied to clipboard'))
.catch(() => enqueueSnackbar('Failed to copy metadata view filters URL to clipboard'));
};

useEffect(() => {
const queryStringFilters = qs.parse(window.location.search, {ignoreQueryPrefix: true});
if (queryStringFilters && Object.keys(queryStringFilters).length > 0) {
const idTextFilter = queryStringFilters[currentViewIdColumn.name.toLowerCase()];
if (idTextFilter && (!areTextFiltersNonEmpty || !textFiltersObject.keys.includes(currentViewIdColumn))) {
setTextFiltersObject({...textFiltersObject, [currentViewIdColumn.name]: idTextFilter});
}
if (!areFacetFiltersNonEmpty) {
const facetNames = facets.map(f => f.name.toLowerCase());
const newFilters = Object.keys(queryStringFilters)
.filter(k => facetNames.includes(k.toLowerCase()))
.reduce((arr, key) => {
arr.push({
field: key,
values: queryStringFilters[key].split(',')
});
return arr;
}, []);
updateFilters(newFilters);
}
}
// eslint-disable-next-line
}, []);

return (
<BreadcrumbsContext.Provider
value={{
Expand All @@ -233,12 +281,15 @@ export const MetadataView = (props: MetadataViewProperties) => {
<BreadCrumbs additionalSegments={getPathSegments(locationContext)} />
{(areFacetFiltersNonEmpty || areTextFiltersNonEmpty) && (
<Grid container justifyContent="space-between" direction="row-reverse">
<Grid item xs={2} className={classes.clearAllButtonContainer}>
<Grid item xs={2} className={classes.clearAllButtonContainer} justifyContent="space-between">
<Button
className={classes.clearAllButton}
startIcon={<Close />}
onClick={handleClearAllFilters}
className={classes.filterButtons}
startIcon={<ContentCopy />}
onClick={() => copyFiltersUrl(currentView, filters)}
>
Copy filters
</Button>
<Button className={classes.filterButtons} startIcon={<Close />} onClick={handleClearAllFilters}>
Clear all filters
</Button>
</Grid>
Expand Down Expand Up @@ -348,19 +399,21 @@ export const ContextualMetadataView = (props: ContextualMetadataViewProperties)
};

return (
<MetadataView
{...props}
metadataLabel={metadataLabel}
facets={facets}
views={views}
filters={filters}
locationContext={currentViewName === RESOURCES_VIEW && locationContext}
currentViewName={currentViewName}
handleViewChangeRedirect={handleViewChangeRedirect}
updateFilters={updateFilters}
clearFilter={clearFilter}
clearAllFilters={clearAllFilters}
/>
<SnackbarProvider maxSnack={3}>
<MetadataView
{...props}
metadataLabel={metadataLabel}
facets={facets}
views={views}
filters={filters}
locationContext={currentViewName === RESOURCES_VIEW && locationContext}
currentViewName={currentViewName}
handleViewChangeRedirect={handleViewChangeRedirect}
updateFilters={updateFilters}
clearFilter={clearFilter}
clearAllFilters={clearAllFilters}
/>
</SnackbarProvider>
);
};

Expand Down
6 changes: 4 additions & 2 deletions projects/mercury/src/metadata/views/MetadataView.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ const styles = theme => ({
clearAllButtonContainer: {
textAlign: 'end'
},
clearAllButton: {
filterButtons: {
color: theme.palette.primary.contrastText,
background: theme.palette.primary.main
background: theme.palette.primary.main,
marginLeft: 1,
marginBottom: 1
},
activeFilters: {
marginBottom: 10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const MetadataViewActiveFacetFilters = (props: MetadataViewActiveFacetFil
);
}
return filter.values.map(valueIri => {
const value = facet.values.find(val => val.value === valueIri);
const value = facet.values.find(val => val.value.toLowerCase() === valueIri.toLowerCase());
return (
value && (
<Chip
Expand Down Expand Up @@ -95,7 +95,7 @@ export const MetadataViewActiveFacetFilters = (props: MetadataViewActiveFacetFil
) {
return null;
}
const facet = facets.find(f => f.name === filter.field);
const facet = facets.find(f => f.name.toLowerCase() === filter.field.toLowerCase());
if (facet) {
return (
<Grid key={filter.field} item>
Expand Down
8 changes: 7 additions & 1 deletion projects/mercury/src/metadata/views/MetadataViewContext.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
// import {useHistory} from 'react-router-dom';
import type {MetadataViewFilter} from './MetadataViewAPI';
import MetadataViewAPI from './MetadataViewAPI';
import useAsync from '../../common/hooks/UseAsync';
Expand All @@ -11,6 +12,7 @@ const SESSION_STORAGE_METADATA_FILTERS_KEY = 'FAIRSPACE_METADATA_FILTERS';

export const MetadataViewProvider = ({children, metadataViewAPI = MetadataViewAPI, sourceName = ''}) => {
const {data = {}, error, loading, refresh} = useAsync(() => metadataViewAPI.getViews(), []);
// const history = useHistory();

const [filters: MetadataViewFilter[], setFilters] = useStateWithSessionStorage(
`${SESSION_STORAGE_METADATA_FILTERS_KEY}_${sourceName}`,
Expand All @@ -23,11 +25,15 @@ export const MetadataViewProvider = ({children, metadataViewAPI = MetadataViewAP

const clearAllFilters = () => {
setFilters([]);

const queryParams = new URLSearchParams(window.location.search);
const viewParam = queryParams.get('view');
window.history.replaceState(null, '', `${window.location.pathname}?${viewParam ? `view=${viewParam}` : ''}`);
};

const updateFilters = (filterCandidates: MetadataViewFilter[]) => {
setFilters([
...filters.filter(f => !filterCandidates.some(u => u.field === f.field)),
...filters.filter(f => !filterCandidates.some(u => u.field.toLowerCase() === f.field.toLowerCase())),
...filterCandidates.filter(
f =>
(f.values && f.values.length > 0) ||
Expand Down
4 changes: 3 additions & 1 deletion projects/mercury/src/metadata/views/MetadataViewFacets.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ export const MetadataViewFacets = (props: MetadataViewFacetsProperties) => {

const renderSingleFacet = (facet: MetadataViewFacet) => {
const facetOptions = getFilterValues(facet.type, facet);
const activeFilter = [...filterCandidates, ...filters].find(filter => filter.field === facet.name);
const activeFilter = [...filterCandidates, ...filters].find(
filter => filter.field.toLowerCase() === facet.name.toLowerCase()
);
let activeFilterValues = [];
if (activeFilter) {
activeFilterValues = getFilterValues(facet.type, activeFilter);
Expand Down
23 changes: 4 additions & 19 deletions projects/mercury/src/metadata/views/MetadataViewTable.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React, {useCallback, useEffect} from 'react';
import React, {useCallback} from 'react';
import {Checkbox, Link, Table, TableBody, TableCell, TableHead, TableRow} from '@mui/material';
import {Check, Close} from '@mui/icons-material';
import makeStyles from '@mui/styles/makeStyles';
import {Link as RouterLink} from 'react-router-dom';
import qs from 'qs';
import useDeepCompareEffect from 'use-deep-compare-effect';
import type {MetadataViewColumn, MetadataViewData} from './MetadataViewAPI';
import {TextualValueTypes} from './MetadataViewAPI';
import type {MetadataViewEntity, MetadataViewEntityWithLinkedFiles} from './metadataViewUtils';
import {RESOURCES_VIEW} from './metadataViewUtils';
import {stringToBooleanValueOrNull, formatDate} from '../../common/utils/genericUtils';
import {formatDate, stringToBooleanValueOrNull} from '../../common/utils/genericUtils';
import type {Collection} from '../../collections/CollectionAPI';
import {collectionAccessIcon} from '../../collections/collectionUtils';
import {getPathFromIri, redirectLink} from '../../file/fileUtils';
Expand Down Expand Up @@ -52,12 +51,12 @@ const RESOURCE_TYPE_COLUMN = `${RESOURCES_VIEW}_type`;
export const MetadataViewTable = (props: MetadataViewTableProperties) => {
const {columns, visibleColumnNames, loading, data, toggleRow, selected, view, idColumn, history, collections} =
props;
const classes = useStyles();
const {textFiltersObject, setTextFiltersObject} = props;
const {checkboxes, setCheckboxState} = props;
const classes = useStyles();
const visibleColumns = columns.filter(column => visibleColumnNames.includes(column.name));
const dataLinkColumn = columns.find(c => c.type === 'dataLink');
const isResourcesView = view === RESOURCES_VIEW;
const {checkboxes, setCheckboxState} = props;

const isCustomResourceColumn = (column: MetadataViewColumn) =>
isResourcesView && CUSTOM_RESOURCE_COLUMNS.includes(column.name) && column.type === 'Custom';
Expand All @@ -67,11 +66,6 @@ export const MetadataViewTable = (props: MetadataViewTableProperties) => {
return col ? col.access : 'None';
};

const getIdColumnFilterFromSearchParams = () => {
const idColumnName = idColumn.name.toLowerCase();
return qs.parse(window.location.search, {ignoreQueryPrefix: true})[idColumnName];
};

const getResourceType = (row: Map<string, any>) =>
row[RESOURCE_TYPE_COLUMN] && row[RESOURCE_TYPE_COLUMN][0] && row[RESOURCE_TYPE_COLUMN][0].value;

Expand All @@ -82,15 +76,6 @@ export const MetadataViewTable = (props: MetadataViewTableProperties) => {
toggleRow({label, iri, linkedFiles: linkedFiles || []});
}
};
useEffect(() => {
if (!textFiltersObject || !textFiltersObject.keys || !textFiltersObject.keys.includes(idColumn)) {
const idColumnTextFilter = getIdColumnFilterFromSearchParams();
if (idColumnTextFilter) {
setTextFiltersObject({...textFiltersObject, [idColumn.name]: idColumnTextFilter});
}
}
// eslint-disable-next-line
}, []);

const initializeCheckboxes = useCallback(() => {
if (idColumn && data && data.rows) {
Expand Down

0 comments on commit 3ced2b8

Please sign in to comment.