Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH Add Readonly field status #172

Merged
merged 3 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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;
}
}
Loading