Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #10783 Support for favorites #10795

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/developer-guide/mapstore-migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ index 62ddda0..62ce070 100644

Some libraries has been updated. if you have a MapStore project make sure to keep the versions aligned with the main product.

## Migration from 2024.02.02 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.00 to 2024.01.02

### Enable showing credits/attribution text in Print config
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
90 changes: 90 additions & 0 deletions web/client/plugins/ResourcesCatalog/containers/Favorites.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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
*/
function Favorites({
user,
component,
resource,
location,
onSearch,
delayTime
}) {
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 ? `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