Skip to content

Commit

Permalink
ENH Disable actions users does not have permissions for
Browse files Browse the repository at this point in the history
Co-authored-by: Scott Hutchinson <scott@silverstripe.com>
  • Loading branch information
emteknetnz and Scott Hutchinson committed Feb 23, 2021
1 parent 817c49c commit a30ac14
Show file tree
Hide file tree
Showing 20 changed files with 175 additions and 14 deletions.
2 changes: 1 addition & 1 deletion _config/graphql-legacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ SilverStripe\GraphQL\Manager:
types:
# Expose common static fields for the ElementEditor component to use for preview summaries
DNADesign\Elemental\Models\BaseElement:
fields: [ID, LastEdited, AbsoluteLink, Title, ShowTitle, Sort, BlockSchema, IsPublished, IsLiveVersion]
fields: [ID, LastEdited, AbsoluteLink, Title, ShowTitle, Sort, BlockSchema, IsPublished, IsLiveVersion, canCreate, canPublish, canUnpublish, canDelete]
operations:
copyToStage: true
readOne:
Expand Down
4 changes: 4 additions & 0 deletions _graphql/models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ DNADesign\Elemental\Models\BaseElement:
blockSchema: ObjectType
isPublished: Boolean
isLiveVersion: Boolean
canCreate: Boolean
canPublish: Boolean
canUnpublish: Boolean
canDelete: Boolean
operations:
copyToStage: true
readOne: true
Expand Down
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/dist/styles/bundle.css

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions client/lang/src/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
"ElementArchiveAction.CONFIRM_DELETE": "Are you sure you want to send this block to the archive?",
"ElementArchiveAction.CONFIRM_DELETE_AND_UNPUBLISH": "Warning: This block will be unpublished before being sent to the archive. Are you sure you want to proceed?",
"ElementArchiveAction.ARCHIVE": "Archive",
"ElementArchiveAction.ARCHIVE_PERMISSION_DENY": "Archive, insufficient permissions",
"ElementArchiveAction.DUPLICATE": "Duplicate",
"ElementArchiveAction.DUPLICATE_PERMISSION_DENY": "Duplicate, insufficient permissions",
"ElementHeader.NOTITLE": "Untitled {type} block",
"ElementPublishAction.SUCCESS_NOTIFICATION": "Published '{title}' successfully",
"ElementPublishAction.ERROR_NOTIFICATION": "Error publishing '{title}'",
"ElementPublishAction.PUBLISH": "Publish",
"ElementPublishAction.PUBLISH_PERMISSION_DENY": "Publish, insufficient permissions",
"ElementSaveAction.SUCCESS_NOTIFICATION": "Saved '{title}' successfully",
"ElementSaveAction.ERROR_NOTIFICATION": "Error saving '{title}'",
"ElementSaveAction.SAVE": "Save",
"ElementUnpublishAction.SUCCESS_NOTIFICATION": "Removed '{title}' from the published page",
"ElementUnpublishAction.ERROR_NOTIFICATION": "Error unpublishing '{title}'",
"ElementUnpublishAction.UNPUBLISH": "Unpublish",
"ElementUnpublishAction.UNPUBLISH_PERMISSION_DENY": "Unpublish, insufficient permissions",
"ElementAddElementPopover.SEARCH_BLOCKS": "Search blocks",
"ElementAddNewButton.ADD_BLOCK": "Add block",
"ElementalElement.TITLE": "Edit this {type} block",
Expand Down
5 changes: 3 additions & 2 deletions client/src/components/ElementActions/AbstractAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { elementTypeType } from 'types/elementTypeType';
* Renders an action item for the "more actions" dropdown on elements
*/
const AbstractAction = (props) => {
const { className, title } = props;
const { className, title, label } = props;

const itemProps = {
className: classNames(className, 'dropdown-item'),
Expand All @@ -17,7 +17,7 @@ const AbstractAction = (props) => {

return (
<DropdownItem {...itemProps}>
{title}
{label || title}
</DropdownItem>
);
};
Expand All @@ -30,6 +30,7 @@ AbstractAction.propTypes = {
name: PropTypes.string,
type: elementTypeType,
active: PropTypes.bool,
label: PropTypes.string
};

AbstractAction.defaultProps = {
Expand Down
10 changes: 8 additions & 2 deletions client/src/components/ElementActions/ArchiveAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,15 @@ const ArchiveAction = (MenuComponent) => (props) => {
}
};

const disabled = props.element.canDelete !== undefined && !props.element.canDelete;
const label = i18n._t('ElementArchiveAction.ARCHIVE', 'Archive');
const title = disabled
? i18n._t('ElementArchiveAction.ARCHIVE_PERMISSION_DENY', 'Archive, insufficient permissions')
: label;
const newProps = {
title: i18n._t('ElementArchiveAction.ARCHIVE', 'Archive'),
label,
title,
disabled,
className: 'element-editor__actions-archive',
onClick: handleClick,
toggle: props.toggle,
Expand All @@ -45,7 +52,6 @@ const ArchiveAction = (MenuComponent) => (props) => {
return (
<MenuComponent {...props}>
{props.children}

<AbstractAction {...newProps} />
</MenuComponent>
);
Expand Down
10 changes: 8 additions & 2 deletions client/src/components/ElementActions/DuplicateAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,15 @@ const DuplicateAction = (MenuComponent) => (props) => {
}
};

const disabled = props.element.canCreate !== undefined && !props.element.canCreate;
const label = i18n._t('ElementArchiveAction.DUPLICATE', 'Duplicate');
const title = disabled
? i18n._t('ElementArchiveAction.DUPLICATE_PERMISSION_DENY', 'Duplicate, insufficient permissions')
: label;
const newProps = {
title: i18n._t('ElementArchiveAction.DUPLICATE', 'Duplicate'),
label,
title,
disabled,
className: 'element-editor__actions-duplicate',
onClick: handleClick,
toggle: props.toggle,
Expand All @@ -32,7 +39,6 @@ const DuplicateAction = (MenuComponent) => (props) => {
return (
<MenuComponent {...props}>
{props.children}

<AbstractAction {...newProps} />
</MenuComponent>
);
Expand Down
10 changes: 8 additions & 2 deletions client/src/components/ElementActions/PublishAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,15 @@ const PublishAction = (MenuComponent) => (props) => {
.catch(() => reportPublicationStatus(type.title, title, false));
};

const disabled = props.element.canPublish !== undefined && !props.element.canPublish;
const label = i18n._t('ElementArchiveAction.PUBLISH', 'Publish');
const title = disabled
? i18n._t('ElementArchiveAction.PUBLISH_PERMISSION_DENY', 'Publish, insufficient permissions')
: label;
const newProps = {
title: i18n._t('ElementPublishAction.PUBLISH', 'Publish'),
label,
title,
disabled,
className: 'element-editor__actions-publish',
onClick: handleClick,
toggle: props.toggle,
Expand All @@ -121,7 +128,6 @@ const PublishAction = (MenuComponent) => (props) => {
return (
<MenuComponent {...props}>
{props.children}

{(formDirty || !element.isLiveVersion) && <AbstractAction {...newProps} />}
</MenuComponent>
);
Expand Down
10 changes: 8 additions & 2 deletions client/src/components/ElementActions/UnpublishAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,15 @@ const UnpublishAction = (MenuComponent) => (props) => {
}
};

const disabled = props.element.canUnpublish !== undefined && !props.element.canUnpublish;
const label = i18n._t('ElementArchiveAction.UNPUBLISH', 'Unpublish');
const title = disabled
? i18n._t('ElementArchiveAction.UNPUBLISH_PERMISSION_DENY', 'Unpublish, insufficient permissions')
: label;
const newProps = {
title: i18n._t('ElementUnpublishAction.UNPUBLISH', 'Unpublish'),
label,
title,
disabled,
className: 'element-editor__actions-unpublish',
onClick: handleClick,
toggle: props.toggle,
Expand All @@ -66,7 +73,6 @@ const UnpublishAction = (MenuComponent) => (props) => {
return (
<MenuComponent {...props}>
{props.children}

{element.isPublished && <AbstractAction {...newProps} />}
</MenuComponent>
);
Expand Down
19 changes: 19 additions & 0 deletions client/src/components/ElementActions/tests/AbstractAction-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,23 @@ describe('AbstractAction', () => {
it('adds provided extra classes', () => {
expect(wrapper.find('button').hasClass('foo-bar')).toBe(true);
});

it('uses the label prop for the button label if label prop is supplied', () => {
const wrapperWithLabel = mount(
<AbstractAction
title="My title"
label="My label"
/>
);
expect(wrapperWithLabel.find('button').text()).toBe('My label');
});

it('uses the title prop for the button label if label prop is not supplied', () => {
const wrapperWithLabel = mount(
<AbstractAction
title="My title"
/>
);
expect(wrapperWithLabel.find('button').text()).toBe('My title');
});
});
18 changes: 18 additions & 0 deletions client/src/components/ElementActions/tests/ArchiveAction-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,22 @@ describe('ArchiveAction', () => {
'Warning: This block will be unpublished'
));
});

it('is disabled when user doesn\'t have correct permissions', () => {
const archiveWrapper = mount(
<ActionComponent
title="My abstract action"
element={{
ID: 123,
IsPublished: false,
BlockSchema: { type: 'Test' },
canDelete: false
}}
actions={{ handleArchiveBlock: mockMutation }}
toggle={false}
/>
);

expect(archiveWrapper.find('button').first().prop('disabled')).toBe(true);
});
});
51 changes: 51 additions & 0 deletions client/src/components/ElementActions/tests/DuplicateAction-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-disable import/no-extraneous-dependencies */
/* global jest, describe, it, expect */

import React from 'react';
import { Component as DuplicateAction } from '../DuplicateAction';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

describe('DuplicateAction', () => {
let wrapper = null;
const mockMutation = jest.fn(() => new Promise((resolve) => { resolve(); }));
const WrappedComponent = (props) => <div>{props.children}</div>;
const ActionComponent = DuplicateAction(WrappedComponent);

beforeEach(() => {
wrapper = mount(
<ActionComponent
title="My duplicate action"
element={{
ID: 123,
BlockSchema: { type: 'Test' },
canCreate: true
}}
actions={{ handlePublishBlock: mockMutation }}
toggle={false}
/>
);
});

it('renders a button', () => {
expect(wrapper.find('button').length).toBe(1);
});

it('is disabled when user doesn\'t have correct permissions', () => {
const duplicateWrapper = mount(
<ActionComponent
title="My duplicate action"
element={{
BlockSchema: { type: 'Test' },
canCreate: false
}}
actions={{ handleDuplicateBlock: mockMutation }}
toggle={false}
/>
);

expect(duplicateWrapper.find('button').first().prop('disabled')).toBe(true);
});
});
11 changes: 11 additions & 0 deletions client/src/components/ElementActions/tests/PublishAction-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,15 @@ describe('PublishAction', () => {

expect(draftWrapper.find('button').length).toBe(0);
});

it('is disabled when user doesn\'t have correct permissions', () => {
const draftWrapper = mount(
<ActionComponent
element={{ IsLiveVersion: false, BlockSchema: { type: 'Test' }, canPublish: false }}
actions={{ handlePublishBlock: mockMutation }}
/>
);

expect(draftWrapper.find('button').first().prop('disabled')).toBe(true);
});
});
12 changes: 12 additions & 0 deletions client/src/components/ElementActions/tests/UnpublishAction-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,16 @@ describe('UnpublishAction', () => {
wrapper.find('button').simulate('click');
expect(mockMutation).toHaveBeenCalled();
});

it('is disabled when user doesn\'t have correct permissions', () => {
const unpublishWrapper = mount(
<ActionComponent
element={{ isPublished: true, BlockSchema: { type: 'Test' }, canUnpublish: false }}
actions={{ handleUnpublishBlock: mockMutation }}
type={{ title: 'Some block' }}
/>
);

expect(unpublishWrapper.find('button').first().prop('disabled')).toBe(true);
});
});
9 changes: 9 additions & 0 deletions client/src/components/ElementEditor/ActionMenu.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.element-editor .action-menu {

& .dropdown-item.disabled {
font-style: italic;
pointer-events: initial;
cursor: not-allowed;
color: $gray-500
}
}
1 change: 0 additions & 1 deletion client/src/components/ElementEditor/ElementEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ class ElementEditor extends PureComponent {
* @param afterId
*/
handleDragEnd(sourceId, afterId) {
console.log('drag end', sourceId, afterId);
const { actions: { handleSortBlock }, areaId } = this.props;

handleSortBlock(sourceId, afterId, areaId).then(() => {
Expand Down
4 changes: 4 additions & 0 deletions client/src/state/editor/readBlocksForAreaQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ query ReadBlocksForArea($id:ID!) {
isLiveVersion
isPublished
version
canCreate
canPublish
canUnpublish
canDelete
}
}
}
Expand Down
1 change: 1 addition & 0 deletions client/src/styles/bundle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@import "history";
@import "reports";
@import "textcheckboxgroup";
@import "../components/ElementEditor/ActionMenu";
@import "../components/ElementEditor/AddNewButton";
@import "../components/ElementEditor/Element";
@import "../components/ElementEditor/ElementList";
Expand Down
4 changes: 4 additions & 0 deletions src/Models/BaseElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ class BaseElement extends DataObject
'BlockSchema' => DBObjectType::class,
'IsLiveVersion' => DBBoolean::class,
'IsPublished' => DBBoolean::class,
'canCreate' => DBBoolean::class,
'canPublish' => DBBoolean::class,
'canUnpublish' => DBBoolean::class,
'canDelete' => DBBoolean::class,
];

private static $versioned_gridfield_extensions = true;
Expand Down

0 comments on commit a30ac14

Please sign in to comment.