Skip to content

Commit

Permalink
Merge pull request #141 from creative-commoners/pulls/4/permissions
Browse files Browse the repository at this point in the history
ENH Tidy up permissions
  • Loading branch information
GuySartorelli authored Dec 21, 2023
2 parents 4e6c662 + f9e765b commit 3528526
Show file tree
Hide file tree
Showing 32 changed files with 586 additions and 144 deletions.
6 changes: 6 additions & 0 deletions babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion client/lang/src/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"LinkField.LINK_DRAFT_TITLE": "Link has draft changes",
"LinkField.LINK_DRAFT_LABEL": "Draft",
"LinkField.LINK_MODIFIED_TITLE": "Link has unpublished changes",
"LinkField.LINK_MODIFIED_LABEL": "Modified"
"LinkField.LINK_MODIFIED_LABEL": "Modified",
"LinkField.CANNOT_CREATE_LINK": "Cannot create link"
}
9 changes: 6 additions & 3 deletions client/src/components/LinkField/LinkField.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController';
* types - types of the Link passed from LinkField entwine
* actions - object of redux actions
* isMulti - whether this field handles multiple links or not
* canCreate - whether this field can create links or not
*/
const LinkField = ({ value = null, onChange, types = [], actions, isMulti = false }) => {
const LinkField = ({ value = null, onChange, types = [], actions, isMulti = false, canCreate }) => {
const [data, setData] = useState({});
const [editingID, setEditingID] = useState(0);

Expand Down Expand Up @@ -145,6 +146,7 @@ const LinkField = ({ value = null, onChange, types = [], actions, isMulti = fals
typeTitle={type.title || ''}
onClear={onClear}
onClick={() => { setEditingID(linkID); }}
canDelete={data[linkID]?.canDelete ? true : false}
/>);
}
return links;
Expand All @@ -154,7 +156,7 @@ const LinkField = ({ value = null, onChange, types = [], actions, isMulti = fals
const renderModal = Boolean(editingID);

return <>
{ renderPicker && <LinkPicker onModalSuccess={onModalSuccess} onModalClosed={onModalClosed} types={types} /> }
{ renderPicker && <LinkPicker canCreate={canCreate} onModalSuccess={onModalSuccess} onModalClosed={onModalClosed} types={types} /> }
<div> { renderLinks() } </div>
{ renderModal && <LinkModalContainer
types={types}
Expand All @@ -171,9 +173,10 @@ const LinkField = ({ value = null, onChange, types = [], actions, isMulti = fals
LinkField.propTypes = {
value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.number]),
onChange: PropTypes.func.isRequired,
types: PropTypes.objectOf(LinkType).isRequired,
types: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired,
isMulti: PropTypes.bool,
canCreate: PropTypes.bool.isRequired,
};

// redux actions loaded into props - used to get toast notifications
Expand Down
16 changes: 14 additions & 2 deletions client/src/components/LinkPicker/LinkPicker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable */
import i18n from 'i18n';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
Expand All @@ -9,7 +10,7 @@ import LinkModalContainer from 'containers/LinkModalContainer';
/**
* Component which allows users to choose a type of link to create, and opens a modal form for it.
*/
const LinkPicker = ({ types, onModalSuccess, onModalClosed }) => {
const LinkPicker = ({ types, onModalSuccess, onModalClosed, canCreate }) => {
const [typeKey, setTypeKey] = useState('');

/**
Expand Down Expand Up @@ -41,6 +42,16 @@ const LinkPicker = ({ types, onModalSuccess, onModalClosed }) => {
const className = classnames('link-picker', 'form-control');
const typeArray = Object.values(types);

if (!canCreate) {
return (
<div className={className}>
<div className="link-picker__cannot-create">
{i18n._t('LinkField.CANNOT_CREATE_LINK', 'Cannot create link')}
</div>
</div>
);
}

return (
<div className={className}>
<LinkPickerMenu types={typeArray} onSelect={handleSelect} />
Expand All @@ -57,9 +68,10 @@ const LinkPicker = ({ types, onModalSuccess, onModalClosed }) => {
};

LinkPicker.propTypes = {
types: PropTypes.objectOf(LinkType).isRequired,
types: PropTypes.array.isRequired,
onModalSuccess: PropTypes.func.isRequired,
onModalClosed: PropTypes.func,
canCreate: PropTypes.bool.isRequired
};

export {LinkPicker as Component};
Expand Down
6 changes: 6 additions & 0 deletions client/src/components/LinkPicker/LinkPicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
}
}

.link-picker__cannot-create {
cursor: default;
flex-grow: 1;
padding: 16px 13px;
}

.link-picker__menu {
flex-grow: 1;
}
Expand Down
19 changes: 9 additions & 10 deletions client/src/components/LinkPicker/LinkPickerMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,25 @@ import LinkType from 'types/LinkType';
const LinkPickerMenu = ({ types, onSelect }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(prevState => !prevState);

return (
<Dropdown
isOpen={isOpen}
toggle={toggle}
className="link-picker__menu"
>
<DropdownToggle className="link-picker__menu-toggle font-icon-plus-1" caret>{i18n._t('LinkField.ADD_LINK', 'Add Link')}</DropdownToggle>
return <Dropdown
isOpen={isOpen}
toggle={toggle}
className="link-picker__menu"
>
<DropdownToggle className="link-picker__menu-toggle font-icon-plus-1" caret>
{i18n._t('LinkField.ADD_LINK', 'Add Link')}
</DropdownToggle>
<DropdownMenu>
{types.map(({key, title}) =>
<DropdownItem key={key} onClick={() => onSelect(key)}>{title}</DropdownItem>
)}
</DropdownMenu>
</Dropdown>
);
};

LinkPickerMenu.propTypes = {
types: PropTypes.arrayOf(LinkType).isRequired,
onSelect: PropTypes.func.isRequired
onSelect: PropTypes.func.isRequired,
};

export default LinkPickerMenu;
16 changes: 14 additions & 2 deletions client/src/components/LinkPicker/LinkPickerTitle.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,16 @@ const getVersionedBadge = (versionState) => {
return <span className={className} title={title}>{label}</span>;
};

const LinkPickerTitle = ({ id, title, description, versionState, typeTitle, onClear, onClick }) => {
const LinkPickerTitle = ({
id,
title,
description,
versionState,
typeTitle,
onClear,
onClick,
canDelete
}) => {
const classes = {
'link-picker__link': true,
'form-control': true,
Expand All @@ -54,7 +63,9 @@ const LinkPickerTitle = ({ id, title, description, versionState, typeTitle, onCl
</small>
</div>
</Button>
<Button className="link-picker__clear" color="link" onClick={stopPropagation(() => onClear(id))}>{i18n._t('LinkField.CLEAR', 'Clear')}</Button>
{canDelete &&
<Button className="link-picker__clear" color="link" onClick={stopPropagation(() => onClear(id))}>{i18n._t('LinkField.CLEAR', 'Clear')}</Button>
}
</div>
};

Expand All @@ -66,6 +77,7 @@ LinkPickerTitle.propTypes = {
typeTitle: PropTypes.string.isRequired,
onClear: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
canDelete: PropTypes.bool.isRequired,
};

export default LinkPickerTitle;
31 changes: 31 additions & 0 deletions client/src/components/LinkPicker/tests/LinkPicker-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* global jest, test */
import React from 'react';
import { render } from '@testing-library/react';
import LinkPicker from '../LinkPicker';

function makeProps(obj = {}) {
return {
types: [{ key: 'phone', title: 'Phone' }],
onModalSuccess: () => {},
onModalClosed: () => {},
...obj
};
}

test('LinkPickerMenu render() should display toggle if can create', () => {
const { container } = render(<LinkPicker {...makeProps({
canCreate: true
})}
/>);
expect(container.querySelectorAll('.link-picker__menu-toggle')).toHaveLength(1);
expect(container.querySelectorAll('.link-picker__cannot-create')).toHaveLength(0);
});

test('LinkPickerMenu render() should display cannot create message if cannot create', () => {
const { container } = render(<LinkPicker {...makeProps({
canCreate: false
})}
/>);
expect(container.querySelectorAll('.link-picker__menu-toggle')).toHaveLength(0);
expect(container.querySelectorAll('.link-picker__cannot-create')).toHaveLength(1);
});
34 changes: 34 additions & 0 deletions client/src/components/LinkPicker/tests/LinkPickerTitle-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* global jest, test */

import React from 'react';
import { render } from '@testing-library/react';
import LinkPickerTitle from '../LinkPickerTitle';

function makeProps(obj = {}) {
return {
id: 1,
title: 'My title',
description: 'My description',
versionState: 'draft',
typeTitle: 'Phone',
onClear: () => {},
onClick: () => {},
...obj
};
}

test('LinkPickerTitle render() should display clear button if can delete', () => {
const { container } = render(<LinkPickerTitle {...makeProps({
canDelete: true
})}
/>);
expect(container.querySelectorAll('.link-picker__clear')).toHaveLength(1);
});

test('LinkPickerTitle render() should not display clear button if cannot delete', () => {
const { container } = render(<LinkPickerTitle {...makeProps({
canDelete: false
})}
/>);
expect(container.querySelectorAll('.link-picker__clear')).toHaveLength(0);
});
3 changes: 1 addition & 2 deletions client/src/containers/LinkModalContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import React from 'react';
import { loadComponent } from 'lib/Injector';
import PropTypes from 'prop-types';
import LinkType from 'types/LinkType';

/**
* Contains the LinkModal and determines which modal component to render based on the link type.
Expand All @@ -29,7 +28,7 @@ const LinkModalContainer = ({ types, typeKey, linkID = 0, isOpen, onSuccess, onC
}

LinkModalContainer.propTypes = {
types: PropTypes.objectOf(LinkType).isRequired,
types: PropTypes.array.isRequired,
typeKey: PropTypes.string.isRequired,
linkID: PropTypes.number,
isOpen: PropTypes.bool.isRequired,
Expand Down
1 change: 1 addition & 0 deletions client/src/entwine/LinkField.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jQuery.entwine('ss', ($) => {
onChange: this.handleChange.bind(this),
isMulti: this.data('is-multi') ?? false,
types: this.data('types') ?? [],
canCreate: this.getInputField().data('can-create') ?? false,
};
},

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"lint-sass": "sass-lint client/src"
},
"jest": {
"testEnvironment": "jsdom",
"roots": [
"client/src"
],
Expand All @@ -51,6 +52,7 @@
"@babel/runtime": "^7.20.0",
"@silverstripe/eslint-config": "^1.0.0",
"@silverstripe/webpack-config": "^2.0.0",
"@testing-library/react": "^14.0.0",
"babel-jest": "^29.2.2",
"jest-cli": "^29.2.2",
"jest-environment-jsdom": "^29.3.1",
Expand Down
1 change: 1 addition & 0 deletions src/Controllers/LinkFieldController.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ private function getLinkData(Link $link): array
$this->jsonError(403, _t('LinkField.UNAUTHORIZED', 'Unauthorized'));
}
$data = $link->jsonSerialize();
$data['canDelete'] = $link->canDelete();
$data['description'] = $link->getDescription();
$data['versionState'] = $link->getVersionedState();
return $data;
Expand Down
10 changes: 10 additions & 0 deletions src/Form/LinkField.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\LinkField\Models\Link;
use SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait;
use SilverStripe\LinkField\Form\Traits\LinkFieldGetOwnerTrait;

/**
* Allows CMS users to edit a Link object.
*/
class LinkField extends FormField
{
use AllowedLinkClassesTrait;
use LinkFieldGetOwnerTrait;

protected $schemaComponent = 'LinkField';

Expand Down Expand Up @@ -60,10 +62,18 @@ public function saveInto(DataObjectInterface $record)
return $this;
}

public function getSchemaStateDefaults()
{
$data = parent::getSchemaStateDefaults();
$data['canCreate'] = $this->getOwner()->canEdit();
return $data;
}

protected function getDefaultAttributes(): array
{
$attributes = parent::getDefaultAttributes();
$attributes['data-value'] = $this->Value();
$attributes['data-can-create'] = $this->getOwner()->canEdit();
return $attributes;
}

Expand Down
5 changes: 5 additions & 0 deletions src/Form/MultiLinkField.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@
use SilverStripe\ORM\RelationList;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\UnsavedRelationList;
use SilverStripe\LinkField\Form\Traits\LinkFieldGetOwnerTrait;
use SilverStripe\LinkField\Models\Link;

/**
* Allows CMS users to edit a Link object.
*/
class MultiLinkField extends FormField
{
use AllowedLinkClassesTrait;
use LinkFieldGetOwnerTrait;

protected $schemaComponent = 'LinkField';

Expand Down Expand Up @@ -72,13 +75,15 @@ public function getSchemaStateDefaults()
{
$data = parent::getSchemaStateDefaults();
$data['value'] = $this->getValueArray();
$data['canCreate'] = $this->getOwner()->canEdit();
return $data;
}

protected function getDefaultAttributes(): array
{
$attributes = parent::getDefaultAttributes();
$attributes['data-value'] = $this->getValueArray();
$attributes['data-can-create'] = $this->getOwner()->canEdit();
return $attributes;
}

Expand Down
3 changes: 3 additions & 0 deletions src/Form/Traits/AllowedLinkClassesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ public function getTypesProps(): string
$typeDefinitions = $this->genarateAllowedTypes();
foreach ($typeDefinitions as $key => $class) {
$type = Injector::inst()->get($class);
if (!$type->canCreate()) {
continue;
}
$typesList[$key] = [
'key' => $key,
'title' => $type->i18n_singular_name(),
Expand Down
Loading

0 comments on commit 3528526

Please sign in to comment.