Skip to content

Commit

Permalink
ENH Tidy up permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Dec 20, 2023
1 parent 55208f8 commit e11043b
Show file tree
Hide file tree
Showing 29 changed files with 529 additions and 138 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"
}
8 changes: 5 additions & 3 deletions client/src/components/LinkField/LinkField.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController';
* actions - object of redux actions
* isMulti - whether this field handles multiple 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 +145,7 @@ const LinkField = ({ value = null, onChange, types, actions, isMulti = false })
typeTitle={type.title || ''}
onClear={onClear}
onClick={() => { setEditingID(linkID); }}
canDelete={data[linkID]?.canDelete ? true : false}
/>);
}
return links;
Expand All @@ -154,7 +155,7 @@ const LinkField = ({ value = null, onChange, types, actions, isMulti = false })
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 +172,10 @@ const LinkField = ({ value = null, onChange, types, actions, isMulti = false })
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 @@ -50,6 +50,7 @@ jQuery.entwine('ss', ($) => {
value,
onChange: this.handleChange.bind(this),
isMulti: this.data('is-multi') ?? false,
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 @@ -112,6 +112,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
8 changes: 8 additions & 0 deletions src/Form/LinkField.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,18 @@ public function saveInto(DataObjectInterface $record)
return $this;
}

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

protected function getDefaultAttributes(): array
{
$attributes = parent::getDefaultAttributes();
$attributes['data-value'] = $this->Value();
$attributes['data-can-create'] = !$this->isReadonly();
return $attributes;
}
}
3 changes: 3 additions & 0 deletions src/Form/MultiLinkField.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use SilverStripe\ORM\RelationList;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\UnsavedRelationList;
use SilverStripe\LinkField\Models\Link;

/**
* Allows CMS users to edit a Link object.
Expand Down Expand Up @@ -68,13 +69,15 @@ public function getSchemaStateDefaults()
{
$data = parent::getSchemaStateDefaults();
$data['value'] = $this->getValueArray();
$data['canCreate'] = !$this->isReadonly();
return $data;
}

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

Expand Down
6 changes: 5 additions & 1 deletion src/Models/FileLink.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ public function getCMSFields(): FieldList

public function getDescription(): string
{
return $this->File()?->getFilename() ?? '';
$file = $this->File();
if (!$file?->exists() || !$file->canView()) {
return '';
}
return $file->getFilename() ?? '';
}

public function getURL(): string
Expand Down
14 changes: 9 additions & 5 deletions src/Models/SiteTreeLink.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ class SiteTreeLink extends Link

public function getDescription(): string
{
return $this->Page()?->URLSegment ?? '';
$page = $this->Page();
if (!$page->exists() || !$page->canView()) {
return '';
}
return $page->URLSegment ?? '';
}

public function getCMSFields(): FieldList
Expand Down Expand Up @@ -113,15 +117,15 @@ public function getURL(): string
public function getDefaultTitle(): string
{
$page = $this->Page();
$pageExist = $this->Page()->exists();

if (!$pageExist) {
if (!$page->exists()) {
return _t(
static::class . '.MISSING_DEFAULT_TITLE',
'Page missing',
);
}

if (!$page->canView()) {
return '';
}
return $page->Title;
}
}
Loading

0 comments on commit e11043b

Please sign in to comment.