Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ const Permissions = ({
resourceType,
permissionsLoading,
compactPermissions,
onChangePermissions
onChangePermissions,
manageAnonymousPermissions,
manageRegisteredMemberPermissions
}) => {
const enableGeoLimits = resourceType === ResourceTypes.DATASET;
const isMounted = useIsMounted();
Expand All @@ -105,11 +107,11 @@ const Permissions = ({
let responseOptions;
if (resourceIndex !== -1) {
responseOptions = getResourcePermissions(
data[resourceIndex].allowed_perms.compact
data[resourceIndex].allowed_perms.compact , compactPermissions?.groups ,manageAnonymousPermissions, manageRegisteredMemberPermissions
);
} else {
// set a default permission object
responseOptions = getResourcePermissions(data[0].allowed_perms.compact);
responseOptions = getResourcePermissions(data[0].allowed_perms.compact, compactPermissions?.groups ,manageAnonymousPermissions, manageRegisteredMemberPermissions);
}
isMounted(() => setPermissionsObject(responseOptions));
});
Expand Down
148 changes: 148 additions & 0 deletions geonode_mapstore_client/client/js/plugins/Share/Share.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright 2020, 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 from 'react';
import PropTypes from 'prop-types';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note previous PR moved code about share to another component #2204

We should not include the plugin again if it was removed previously. Now share is a tab in resources details component see DetailsShare.jsx. Please review the code

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Glyphicon } from 'react-bootstrap';

import { createPlugin } from '@mapstore/framework/utils/PluginsUtils';
import { setControlProperty } from '@mapstore/framework/actions/controls';
import Message from '@mapstore/framework/components/I18N/Message';
import controls from '@mapstore/framework/reducers/controls';
import Button from '@mapstore/framework/components/layout/Button';
import { mapInfoSelector } from '@mapstore/framework/selectors/map';

import OverlayContainer from '@js/components/OverlayContainer';
import {
isNewResource,
getResourceId,
getResourceData,
getViewedResourceType
} from '@js/selectors/resource';
import {
canAccessPermissions,
canManageAnonymousPermissions,
canManageRegisteredMemberPermissions,
getDownloadUrlInfo,
getResourceTypesInfo
} from '@js/utils/ResourceUtils';
import SharePageLink from '@js/plugins/Share/SharePageLink';
import ShareEmbedLink from '@js/plugins/Share/ShareEmbedLink';
import Permissions from '@js/plugins/Share/components/Permissions';
import FlexBox from '@mapstore/framework/components/layout/FlexBox';

const getEmbedUrl = (resource) => {
const { formatEmbedUrl = (_resource) => _resource?.embed_url } = getResourceTypesInfo()[resource?.resource_type] || {};
return formatEmbedUrl(resource) ? resource?.embed_url : null;
};
function Share({
enabled,
onClose,
resource,
resourceType
}) {
const embedUrl = getEmbedUrl(resource);
const downloadUrl = getDownloadUrlInfo(resource)?.url;
const manageAnonymousPermissions = canManageAnonymousPermissions(resource);
const manageRegisteredMemberPermissions = canManageRegisteredMemberPermissions(resource);
return (
<OverlayContainer
enabled={enabled}
className="gn-overlay-wrapper"
>
<section className="gn-share-panel">
<div className="gn-share-panel-head">
<h2><Message msgId="gnviewer.shareThisResource" /></h2>
<Button className="square-button gn-share-panel-close" onClick={() => onClose()}>
<Glyphicon glyph="1-close" />
</Button>
</div>
<FlexBox column gap="md" className="gn-share-panel-body">
{canAccessPermissions(resource) && <Permissions resource={resource} manageAnonymousPermissions={manageAnonymousPermissions} manageRegisteredMemberPermissions={manageRegisteredMemberPermissions} />}
{(resourceType === 'document' && !!downloadUrl) && <SharePageLink value={downloadUrl} label={<Message msgId={`gnviewer.directLink`} />} collapsible={false} />}
{embedUrl && <ShareEmbedLink embedUrl={embedUrl} label={<Message msgId={`gnviewer.embed${resourceType}`} />} />}
</FlexBox>
</section>
</OverlayContainer>
);
}

Share.propTypes = {
resourceId: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]),
enabled: PropTypes.bool,
onClose: PropTypes.func
};

Share.defaultProps = {
resourceId: null,
enabled: false,
onClose: () => {}
};

const SharePlugin = connect(
createSelector([
state => state?.controls?.rightOverlay?.enabled === 'Share',
getResourceData,
getViewedResourceType
], (enabled, resource, type) => ({
enabled,
resource,
resourceType: type
})),
{
onClose: setControlProperty.bind(null, 'rightOverlay', 'enabled', false)
}
)(Share);

function ShareButton({
enabled,
variant,
onClick,
size
}) {
return enabled
? <Button
variant={variant || "primary"}
size={size}
onClick={() => onClick()}
>
<Message msgId="share.title"/>
</Button>
: null
;
}

const ConnectedShareButton = connect(
createSelector(
isNewResource,
getResourceId,
mapInfoSelector,
(isNew, resourceId, mapInfo) => ({
enabled: !isNew && (resourceId || mapInfo?.id)
})
),
{
onClick: setControlProperty.bind(null, 'rightOverlay', 'enabled', 'Share')
}
)((ShareButton));

export default createPlugin('Share', {
component: SharePlugin,
containers: {
ActionNavbar: {
name: 'Share',
Component: ConnectedShareButton
}
},
epics: {},
reducers: {
controls
}
});
44 changes: 41 additions & 3 deletions geonode_mapstore_client/client/js/utils/ResourceUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -476,13 +476,51 @@ export const setAvailableResourceTypes = (value) => {
availableResourceTypes = value;
};

export const canManageAnonymousPermissions = (resource) => {
return resourceHasPermission(resource, 'can_manage_anonymous_permissions');
};

export const canManageRegisteredMemberPermissions = (resource) => {
return resourceHasPermission(resource, 'can_manage_registered_member_permissions');
}

/**
* Get the current permission for a specific group.
* @param {*} groupName
* @param {*} groups

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better code clarity and maintainability, it's recommended to use specific types in JSDoc instead of the generic *. For instance, groupName is a string and groups is an array of group objects.

Suggested change
* @param {*} groupName
* @param {*} groups
* @param {string} groupName
* @param {object[]} groups

*/
export const getPermissionForGroup = (groupName, groups) => {
const group = groups?.find(g => g.name === groupName);
return group?.permissions;
};

/**
* Filters permission options for a group if management is disabled.
* If management is disabled, it restricts the options to only the current permission.
* @param {object} options The permissions options object.
* @param {array} groups The list of groups with their current permissions.
* @param {string} groupName The name of the group to filter ('anonymous' or 'registered-members').
* @param {boolean} canManage The flag indicating if the user can manage permissions.
*/
const filterGroupPermissions = (options, groups, groupName, canManage) => {
if (!canManage && options[groupName]) {
const permissionValue = getPermissionForGroup(groupName, groups);
const currentPermission = options[groupName].find(p => p.name === permissionValue);
if (currentPermission) {
options[groupName] = [currentPermission];
}
}
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's avoid to mutate the argument object and return a new object instead, see the example below (please double check the code):

We could also try to check all groups in same function

const filterGroupPermissions = (options, groups, groupNames) => {
    return groupNames.length
        ? Object.fromEntries(Object.keys(options)
        .map((key) => {
            if (groupNames.some(name => name === key)) {
                const permissionValue = getPermissionForGroup(key, groups);
                const currentPermission = options[key].find(p => p.name === permissionValue);
                return currentPermission ? [key, currentPermission] : [key, options[key]]
            }
            return [key, options[key]]
        }))
        : options;
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


/**
* Extracts lists of permissions into an object for use in the Share plugin select elements
* @param {Object} options Permission Object to extract permissions from
* @returns An object containing permissions for each type of user/group
*/
export const getResourcePermissions = (options) => {
const permissionsOptions = {};
export const getResourcePermissions = (options , groups, manageAnonymousPermissions=false, manageRegisteredMemberPermissions=false) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getResourcePermissions function has default values for manageAnonymousPermissions and manageRegisteredMemberPermissions, but not for groups. To improve robustness and make the function signature clearer, consider adding a default value for groups, such as an empty array.

Suggested change
export const getResourcePermissions = (options , groups, manageAnonymousPermissions=false, manageRegisteredMemberPermissions=false) => {
export const getResourcePermissions = (options , groups = [], manageAnonymousPermissions=false, manageRegisteredMemberPermissions=false) => {

filterGroupPermissions(options, groups, 'anonymous', manageAnonymousPermissions);
filterGroupPermissions(options, groups, 'registered-members', manageRegisteredMemberPermissions);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on previous comment, please double check this

const options = filterGroupPermissions(_options, groups, [
  ...(manageAnonymousPermissions ? [] : ['anonymous']),
  ...(manageRegisteredMemberPermissions? [] : ['registered-members']),
])

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

let permissionsOptions = {};
Object.keys(options).forEach((key) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The filterGroupPermissions function mutates the options object. It's a good practice to avoid mutating function arguments to prevent potential side effects. Consider creating a copy of the options object before filtering to ensure the original object remains unchanged.

Suggested change
filterGroupPermissions(options, groups, 'anonymous', manageAnonymousPermissions);
filterGroupPermissions(options, groups, 'registered-members', manageRegisteredMemberPermissions);
let permissionsOptions = {};
Object.keys(options).forEach((key) => {
const optionsCopy = JSON.parse(JSON.stringify(options));
filterGroupPermissions(optionsCopy, groups, 'anonymous', manageAnonymousPermissions);
filterGroupPermissions(optionsCopy, groups, 'registered-members', manageRegisteredMemberPermissions);
let permissionsOptions = {};
Object.keys(optionsCopy).forEach((key) => {

const permissions = options[key];
let selectOptions = [];
Expand Down Expand Up @@ -897,4 +935,4 @@ export const canManageResourceSettings = (resource) => {
export const canAccessPermissions = (resource) => {
const { perms } = resource || {};
return perms?.includes('change_resourcebase_permissions');
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ describe('Test Resource Utils', () => {
}
}
}];
const permissionOptions = getResourcePermissions(data[0].allowed_perms.compact);
const groups = [];
const permissionOptions = getResourcePermissions(data[0].allowed_perms.compact, groups);
Comment on lines +116 to +117

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The test for getResourcePermissions has been updated for the new function signature, but the new permission filtering logic is not covered by any tests. It is crucial to add tests for this new functionality to ensure it works correctly and to prevent future regressions.

Please add test cases that cover:

  • A user who can manage permissions (options should not be filtered).
  • A user who cannot manage permissions (options should be filtered to only the current permission).
  • Scenarios for both anonymous and registered-members groups.
  • Edge cases, such as when a group's current permission is not set.

expect(permissionOptions).toEqual({
test1: [
{ value: 'none', labelId: `gnviewer.nonePermission`, label: 'None' },
Expand Down