Skip to content

Commit

Permalink
ENH Add Readonly field status (#172)
Browse files Browse the repository at this point in the history
* ENH Add Readonly field status

* ENH Add Readonly field status

* ENH Add info message for readonly fields
  • Loading branch information
sabina-talipova authored Jan 24, 2024
1 parent fce5499 commit 9bb3a1a
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 34 deletions.
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.

44 changes: 28 additions & 16 deletions client/src/components/LinkField/LinkField.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController';
* actions - object of redux actions
* isMulti - whether this field handles multiple links or not
* canCreate - whether this field can create new links or not
* readonly - whether this field is readonly or not
* ownerID - ID of the owner DataObject
* ownerClass - class name of the owner DataObject
* ownerRelation - name of the relation on the owner DataObject
Expand All @@ -44,6 +45,7 @@ const LinkField = ({
actions,
isMulti = false,
canCreate,
readonly,
ownerID,
ownerClass,
ownerRelation,
Expand Down Expand Up @@ -191,11 +193,34 @@ const LinkField = ({
isFirst={i === 0}
isLast={i === linkIDs.length - 1}
isSorting={isSorting}
canCreate={canCreate}
readonly={readonly}
/>);
}
return links;
};

const sortableLinks = () => {
if (isMulti && !readonly) {
return <div className={linksClassName}>
<DndContext modifiers={[restrictToVerticalAxis, restrictToParentElement]}
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext
items={linkIDs}
strategy={verticalListSortingStrategy}
>
{links}
</SortableContext>
</DndContext>
</div>
}
return <div>{links}</div>
};

const handleDragStart = (event) => {
setLinksClassName(classnames({
'link-picker__links': true,
Expand Down Expand Up @@ -254,23 +279,9 @@ const LinkField = ({
onModalClosed={onModalClosed}
types={types}
canCreate={canCreate}
readonly={readonly}
/> }
{ isMulti && <div className={linksClassName}>
<DndContext modifiers={[restrictToVerticalAxis, restrictToParentElement]}
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext
items={linkIDs}
strategy={verticalListSortingStrategy}
>
{links}
</SortableContext>
</DndContext>
</div> }
{ !isMulti && <div>{links}</div>}
{sortableLinks()}
{ renderModal && <LinkModalContainer
types={types}
typeKey={data[editingID]?.typeKey}
Expand All @@ -291,6 +302,7 @@ LinkField.propTypes = {
actions: PropTypes.object.isRequired,
isMulti: PropTypes.bool,
canCreate: PropTypes.bool.isRequired,
readonly: PropTypes.bool.isRequired,
ownerID: PropTypes.number.isRequired,
ownerClass: PropTypes.string.isRequired,
ownerRelation: PropTypes.string.isRequired,
Expand Down
1 change: 1 addition & 0 deletions client/src/components/LinkField/tests/LinkField-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function makeProps(obj = {}) {
},
isMulti: false,
canCreate: true,
readonly: false,
ownerID: 123,
ownerClass: 'Page',
ownerRelation: 'MyRelation',
Expand Down
7 changes: 4 additions & 3 deletions client/src/components/LinkPicker/LinkPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,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, canCreate }) => {
const LinkPicker = ({ types, onModalSuccess, onModalClosed, canCreate, readonly }) => {
const [typeKey, setTypeKey] = useState('');

/**
Expand Down Expand Up @@ -43,7 +43,7 @@ const LinkPicker = ({ types, onModalSuccess, onModalClosed, canCreate }) => {
const allowedTypes = typeArray.filter(type => type.allowed);
const message = i18n._t('LinkField.CANNOT_CREATE_LINK', 'Cannot create link');

if (!canCreate || allowedTypes.length === 0) {
if (!canCreate || allowedTypes.length === 0 || readonly) {
return (
<div className={className}>
<div className="link-picker__cannot-create">
Expand Down Expand Up @@ -72,7 +72,8 @@ LinkPicker.propTypes = {
types: PropTypes.object.isRequired,
onModalSuccess: PropTypes.func.isRequired,
onModalClosed: PropTypes.func,
canCreate: PropTypes.bool.isRequired
canCreate: PropTypes.bool.isRequired,
readonly: PropTypes.bool.isRequired,
};

export {LinkPicker as Component};
Expand Down
4 changes: 4 additions & 0 deletions client/src/components/LinkPicker/LinkPicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@
&--published::before {
display: none;
}

&--readonly {
background-color: $gray-100;
}
}

.link-picker__link--is-first,
Expand Down
9 changes: 7 additions & 2 deletions client/src/components/LinkPicker/LinkPickerTitle.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const LinkPickerTitle = ({
isFirst,
isLast,
isSorting,
canCreate,
readonly,
}) => {
const { loading } = useContext(LinkFieldContext);
const {
Expand All @@ -65,6 +67,7 @@ const LinkPickerTitle = ({
'link-picker__link--is-last': isLast,
'link-picker__link--is-sorting': isSorting,
'form-control': true,
'link-picker__link--readonly': readonly || !canCreate,
};
if (versionState) {
classes[`link-picker__link--${versionState}`] = true;
Expand All @@ -80,7 +83,7 @@ const LinkPickerTitle = ({
{...attributes}
{...listeners}
>
{ isMulti && <div className="link-picker__drag-handle"><i className="font-icon-drag-handle"></i></div> }
{ (isMulti && !readonly) && <div className="link-picker__drag-handle"><i className="font-icon-drag-handle"></i></div> }
<Button disabled={loading} className={`link-picker__button ${typeIcon}`} color="secondary" onClick={stopPropagation(onClick)}>
<div className="link-picker__link-detail">
<div className="link-picker__title">
Expand All @@ -93,7 +96,7 @@ const LinkPickerTitle = ({
</small>
</div>
</Button>
{canDelete &&
{(canDelete && !readonly) &&
<Button disabled={loading} className="link-picker__delete" color="link" onClick={stopPropagation(() => onDelete(id))}>{deleteText}</Button>
}
</div>
Expand All @@ -113,6 +116,8 @@ LinkPickerTitle.propTypes = {
isFirst: PropTypes.bool.isRequired,
isLast: PropTypes.bool.isRequired,
isSorting: PropTypes.bool.isRequired,
canCreate: PropTypes.bool.isRequired,
readonly: PropTypes.bool.isRequired,
};

export default LinkPickerTitle;
1 change: 1 addition & 0 deletions client/src/components/LinkPicker/tests/LinkPicker-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ function makeProps(obj = {}) {
return {
types: { phone: { key: 'phone', title: 'Phone', icon: 'font-icon-phone', allowed: true } },
canCreate: true,
readonly: false,
onModalSuccess: () => {},
onModalClosed: () => {},
...obj
Expand Down
16 changes: 16 additions & 0 deletions client/src/components/LinkPicker/tests/LinkPickerTitle-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ function makeProps(obj = {}) {
typeTitle: 'Phone',
typeIcon: 'font-icon-phone',
canDelete: true,
canCreate: true,
readonly: false,
onDelete: () => {},
onClick: () => {},
isMulti: false,
Expand Down Expand Up @@ -89,3 +91,17 @@ test('LinkPickerTitle main button should not fire the onClick callback while loa
fireEvent.click(container.querySelector('button.link-picker__button'));
expect(mockOnClick).toHaveBeenCalledTimes(0);
});

test('LinkPickerTitle render() should have readonly class if set to readonly', () => {
const { container } = render(<LinkFieldContext.Provider value={{ loading: false }}>
<LinkPickerTitle {...makeProps({ readonly: true })} />
</LinkFieldContext.Provider>);
expect(container.querySelectorAll('.link-picker__link--readonly')).toHaveLength(1);
});

test('LinkPickerTitle render() should not have readonly class if set to readonly', () => {
const { container } = render(<LinkFieldContext.Provider value={{ loading: false }}>
<LinkPickerTitle {...makeProps({ readonly: false })} />
</LinkFieldContext.Provider>);
expect(container.querySelectorAll('.link-picker__link--readonly')).toHaveLength(0);
});
1 change: 1 addition & 0 deletions client/src/entwine/LinkField.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jQuery.entwine('ss', ($) => {
isMulti: this.data('is-multi') ?? false,
types: this.data('types') ?? {},
canCreate: inputField.data('can-create') ? true : false,
readonly: inputField.data('readonly') ? true : false,
};
},

Expand Down
51 changes: 43 additions & 8 deletions src/Controllers/LinkFieldController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use SilverStripe\LinkField\Services\LinkTypeService;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Security;

class LinkFieldController extends LeftAndMain
{
Expand Down Expand Up @@ -377,7 +378,8 @@ private function createLinkForm(Link $link, string $operation): Form

// Make readonly if fail can check
if ($operation === 'create' && !$link->canCreate()
|| $operation === 'edit' && !$link->canEdit()) {
|| $operation === 'edit' && !$link->canEdit()
|| $this->isReadOnlyField()) {
$form->makeReadonly();
}

Expand All @@ -387,6 +389,17 @@ private function createLinkForm(Link $link, string $operation): Form
return $form;
}

/**
* Get is the owner LinkField is readonly
*/
private function isReadOnlyField(): bool
{
$ownerClass = $this->getOwnerClassFromRequest();
$ownerRelation = $this->ownerRelationFromRequest();

return (bool) Injector::inst()->get($ownerClass)->getCMSFields()->dataFieldByName($ownerRelation)?->isReadonly();
}

/**
* Get a Link object based on the $ItemID request param
*/
Expand Down Expand Up @@ -469,20 +482,41 @@ private function typeKeyFromRequest(): string
}

/**
* Get the owner based on the query string params ownerID, ownerClass, ownerRelation
* OR the POST vars OwnerID, OwnerClass, OwnerRelation
* Get the owner class based on the query string param OwnerClass
*/
private function ownerFromRequest(): DataObject
private function getOwnerClassFromRequest(): string
{
$request = $this->getRequest();
$ownerID = (int) ($request->getVar('ownerID') ?: $request->postVar('OwnerID'));
if ($ownerID === 0) {
$this->jsonError(404, _t(__CLASS__ . '.INVALID_OWNER_ID', 'Invalid ownerID'));
}
$ownerClass = $request->getVar('ownerClass') ?: $request->postVar('OwnerClass');
if (!is_a($ownerClass, DataObject::class, true)) {
$this->jsonError(404, _t(__CLASS__ . '.INVALID_OWNER_CLASS', 'Invalid ownerClass'));
}

return $ownerClass;
}

/**
* Get the owner ID based on the query string param OwnerID
*/
private function getOwnerIDFromRequest(): int
{
$request = $this->getRequest();
$ownerID = (int) ($request->getVar('ownerID') ?: $request->postVar('OwnerID'));
if ($ownerID === 0) {
$this->jsonError(404, _t(__CLASS__ . '.INVALID_OWNER_ID', 'Invalid ownerID'));
}

return $ownerID;
}

/**
* Get the owner based on the query string params ownerID, ownerClass, ownerRelation
* OR the POST vars OwnerID, OwnerClass, OwnerRelation
*/
private function ownerFromRequest(): DataObject
{
$ownerID = $this->getOwnerIDFromRequest();
$ownerClass = $this->getOwnerClassFromRequest();
$ownerRelation = $this->ownerRelationFromRequest();
/** @var DataObject $obj */
$obj = Injector::inst()->get($ownerClass);
Expand Down Expand Up @@ -520,6 +554,7 @@ private function ownerRelationFromRequest(): string
if (!$ownerRelation) {
$this->jsonError(404, _t(__CLASS__ . '.INVALID_OWNER_RELATION', 'Invalid ownerRelation'));
}

return $ownerRelation;
}
}
16 changes: 13 additions & 3 deletions src/Form/LinkField.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@

namespace SilverStripe\LinkField\Form;

use LogicException;
use SilverStripe\Forms\FormField;
use SilverStripe\LinkField\Models\Link;
use SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait;
use SilverStripe\LinkField\Form\Traits\LinkFieldGetOwnerTrait;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface;

/**
* Allows CMS users to edit a Link object.
Expand Down Expand Up @@ -37,6 +34,7 @@ public function getSchemaStateDefaults()
{
$data = parent::getSchemaStateDefaults();
$data['canCreate'] = $this->getOwner()->canEdit();
$data['readonly'] = $this->isReadonly();
return $data;
}

Expand All @@ -45,6 +43,7 @@ protected function getDefaultAttributes(): array
$attributes = parent::getDefaultAttributes();
$attributes['data-value'] = $this->Value();
$attributes['data-can-create'] = $this->getOwner()->canEdit();
$attributes['data-readonly'] = $this->isReadonly();
$ownerFields = $this->getOwnerFields();
$attributes['data-owner-id'] = $ownerFields['ID'];
$attributes['data-owner-class'] = $ownerFields['Class'];
Expand All @@ -62,4 +61,15 @@ public function getSchemaDataDefaults()
$data['ownerRelation'] = $ownerFields['Relation'];
return $data;
}

/**
* Changes this field to the readonly field.
*/
public function performReadonlyTransformation()
{
$clone = clone $this;
$clone->setReadonly(true);

return $clone;
}
}
13 changes: 13 additions & 0 deletions src/Form/MultiLinkField.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public function getSchemaStateDefaults()
$data = parent::getSchemaStateDefaults();
$data['value'] = $this->getValueArray();
$data['canCreate'] = $this->getOwner()->canEdit();
$data['readonly'] = $this->isReadonly();
return $data;
}

Expand All @@ -62,6 +63,7 @@ protected function getDefaultAttributes(): array
$attributes = parent::getDefaultAttributes();
$attributes['data-value'] = $this->getValueArray();
$attributes['data-can-create'] = $this->getOwner()->canEdit();
$attributes['data-readonly'] = $this->isReadonly();
$ownerFields = $this->getOwnerFields();
$attributes['data-owner-id'] = $ownerFields['ID'];
$attributes['data-owner-class'] = $ownerFields['Class'];
Expand Down Expand Up @@ -155,4 +157,15 @@ private function loadFrom(DataObject $record): void
$value = array_values($relation->getIDList() ?? []);
parent::setValue($value);
}

/**
* Changes this field to the readonly field.
*/
public function performReadonlyTransformation()
{
$clone = clone $this;
$clone->setReadonly(true);

return $clone;
}
}

0 comments on commit 9bb3a1a

Please sign in to comment.