Skip to content

Commit

Permalink
Fix #10783 Support for favorites (#10795)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Lorenzo Natali <[email protected]>
  • Loading branch information
allyoucanmap and offtherailz authored Feb 10, 2025
1 parent ec8726a commit 77bd26e
Show file tree
Hide file tree
Showing 34 changed files with 1,019 additions and 589 deletions.
17 changes: 17 additions & 0 deletions docs/developer-guide/mapstore-migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ This is a list of things to check if you want to update from a previous version
- Optionally check also accessory files like `.eslinrc`, if you want to keep aligned with lint standards.
- Follow the instructions below, in order, from your version to the one you want to update to.

## Migration from 2024.02.00 to 2025.01.00

### Add Favorite plugin to localConfig.json

The new Favorite plugin should be added inside the plugins `maps` section of the `localConfig.json` to visualize the button on the resource cards

```diff
{
"plugins": {
...,
"maps": [
...,
+ { "name": "Favorites" }
],
...
}
}
## Migration from 2024.01.02 to 2024.02.00

### NodeJS and NPM update
Expand Down
22 changes: 21 additions & 1 deletion web/client/api/GeoStoreDAO.js
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,27 @@ const Api = {
return postUser;
}
},
errorParser
errorParser,
/**
* add a resource to user favorites
* @param {string} userId user identifier
* @param {string} resourceId resource identifier
* @param {object} options additional axios options
*/
addFavoriteResource: (userId, resourceId, options) => {
const url = `/users/user/${userId}/favorite/${resourceId}`;
return axios.post(url, undefined, Api.addBaseUrl(parseOptions(options))).then((response) => response.data);
},
/**
* remove a resource from user favorites
* @param {string} userId user identifier
* @param {string} resourceId resource identifier
* @param {object} options additional axios options
*/
removeFavoriteResource: (userId, resourceId, options) => {
const url = `/users/user/${userId}/favorite/${resourceId}`;
return axios.delete(url, Api.addBaseUrl(parseOptions(options))).then((response) => response.data);
}
};

export default Api;
25 changes: 25 additions & 0 deletions web/client/api/__tests__/GeoStoreDAO-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -423,4 +423,29 @@ describe('Test correctness of the GeoStore APIs', () => {
done(e);
});
});

it('addFavoriteResource', (done) => {
mockAxios.onPost().reply((data) => {
try {
expect(data.url).toEqual('/users/user/10/favorite/15');
done();
} catch (e) {
done(e);
}
return [200];
});
API.addFavoriteResource("10", "15");
});
it('removeFavoriteResource', (done) => {
mockAxios.onDelete().reply((data) => {
try {
expect(data.url).toEqual('/users/user/10/favorite/15');
done();
} catch (e) {
done(e);
}
return [200];
});
API.removeFavoriteResource("10", "15");
});
});
1 change: 1 addition & 0 deletions web/client/configs/localConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,7 @@
]
}
},
{ "name": "Favorites" },
{
"name": "ResourcesFiltersForm",
"cfg": {
Expand Down
40 changes: 40 additions & 0 deletions web/client/plugins/ResourcesCatalog/Favorites.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2025, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import { createPlugin } from '../../utils/PluginsUtils';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { userSelector } from '../../selectors/security';
import { getRouterLocation } from './selectors/resources';
import { searchResources } from './actions/resources';
import Favorites from './containers/Favorites';

const ConnectedFavorites = connect(
createStructuredSelector({
user: userSelector,
location: getRouterLocation
}),
{
onSearch: searchResources
}
)(Favorites);

/**
* renders a button inside the resource card to add/remove a resource to user favorites
* @name Favorites.
*/
export default createPlugin('Favorites', {
component: () => null,
containers: {
ResourcesGrid: {
target: 'card-buttons',
position: 0,
Component: ConnectedFavorites
}
}
});
6 changes: 6 additions & 0 deletions web/client/plugins/ResourcesCatalog/ResourcesFiltersForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ function ResourcesFiltersForm({
type: 'filter',
disableIf: '{!state("userrole")}'
},
{
id: 'favorite',
labelId: 'resourcesCatalog.favorites',
type: 'filter',
disableIf: '{!state("userrole")}'
},
{
id: 'map',
labelId: 'resourcesCatalog.mapsFilter',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('resources api', () => {
mockAxios.onPost().replyOnce((config) => {
try {
expect(config.url).toBe('/extjs/search/list');
expect(config.params).toEqual({ includeAttributes: true, start: 24, limit: 24, sortBy: 'name', sortOrder: 'asc' });
expect(config.params).toEqual({ includeAttributes: true, start: 24, limit: 24, sortBy: 'name', sortOrder: 'asc', favoritesOnly: true });
let json;
xml2js.parseString(config.data, { explicitArray: false }, (ignore, result) => {
json = result;
Expand Down Expand Up @@ -155,7 +155,7 @@ describe('resources api', () => {
params: {
'page': 2,
'pageSize': 24,
'f': ['map', 'featured', 'my-resources'],
'f': ['map', 'featured', 'my-resources', 'favorite'],
'q': 'A',
'filter{ctx.in}': ['contextName'],
'filter{group.in}': ['group01'],
Expand Down
4 changes: 3 additions & 1 deletion web/client/plugins/ResourcesCatalog/api/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const requestResources = ({
} = params || {};
const sortBy = sort.replace('-', '');
const sortOrder = sort.includes('-') ? 'desc' : 'asc';
const f = castArray(query.f || []);
return searchListByAttributes(getFilter({
q,
user,
Expand All @@ -159,7 +160,8 @@ export const requestResources = ({
start: parseFloat(page - 1) * pageSize,
limit: pageSize,
sortBy,
sortOrder
sortOrder,
...(f.includes('favorite') ? { favoritesOnly: true } : {})
}
})
.toPromise()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const ResourceCardButton = ({
square,
variant,
borderTransparent,
loading,
...props
}) => {
function handleOnClick(event) {
Expand All @@ -47,9 +48,10 @@ const ResourceCardButton = ({
tooltipId={square && labelId ? labelId : null}
onClick={handleOnClick}
>
{glyph ? <><Icon type={iconType} glyph={glyph}/></> : null}
{glyph && labelId ? ' ' : null}
{labelId && !square ? <Message msgId={labelId} /> : null}
{!loading && glyph ? <><Icon type={iconType} glyph={glyph}/></> : null}
{!loading && glyph && labelId ? ' ' : null}
{!loading && labelId && !square ? <Message msgId={labelId} /> : null}
{loading ? <Spinner /> : null}
</ButtonWithTooltip>
);
};
Expand Down
92 changes: 92 additions & 0 deletions web/client/plugins/ResourcesCatalog/containers/Favorites.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2025, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import url from 'url';
import useIsMounted from '../hooks/useIsMounted';
import GeoStoreDAO from '../../../api/GeoStoreDAO';
import { castArray } from 'lodash';

/**
* Favorites button component
* @prop {object} user user properties
* @prop {class|function} component a valid component
* @prop {object} resource resource properties
* @prop {object} location router location
* @prop {function} onSearch trigger a refresh request after changing the favorite association
* @prop {number} delayTime delay time to complete the request
* @prop {string} renderType define the component type (eg. menuItem)
*/
function Favorites({
user,
component,
resource,
location,
onSearch,
delayTime,
renderType
}) {
const { query } = url.parse(location?.search || '', true);
const f = castArray(query.f || []);
const isMounted = useIsMounted();
const [loading, setLoading] = useState(false);
const [isFavorite, setIsFavorite] = useState(!!resource?.isFavorite);

function handleOnClick() {
if (!loading) {
setLoading(true);
const promise = isFavorite
? GeoStoreDAO.removeFavoriteResource
: GeoStoreDAO.addFavoriteResource;
promise(user?.id, resource?.id)
.then(() => isMounted(() => {
setIsFavorite(!isFavorite);
}))
.finally(() =>
setTimeout(() => isMounted(() => {
// apply a delay to show the spinner
// and give a feedback to the user
setLoading(false);
if (f.includes('favorite')) {
onSearch({ refresh: true });
}
}), delayTime)
);
}
}
const Component = component;
return Component && resource?.id && user?.id
? (
<Component
glyph={isFavorite ? 'heart' : 'heart-o'}
iconType="glyphicon"
labelId={!loading || renderType === 'menuItem' ? `resourcesCatalog.${isFavorite ? 'removeFromFavorites' : 'addToFavorites'}` : undefined}
square
onClick={handleOnClick}
loading={loading}
/>
)
: null;
}

Favorites.propTypes = {
user: PropTypes.object,
component: PropTypes.any,
resource: PropTypes.object,
location: PropTypes.object,
onSearch: PropTypes.func,
delayTime: PropTypes.number
};

Favorites.defaultProps = {
onSearch: () => {},
delayTime: 500
};

export default Favorites;
Loading

0 comments on commit 77bd26e

Please sign in to comment.