From 6a80f83634d2a44c475c0b93093f8f2fe85d4e0b Mon Sep 17 00:00:00 2001 From: Sabina Talipova Date: Wed, 24 Jan 2024 11:36:40 +1300 Subject: [PATCH] ENH Add Readonly field status --- client/dist/styles/bundle.css | 2 +- client/src/components/LinkField/LinkField.js | 4 ++ .../LinkField/tests/LinkField-test.js | 1 + .../src/components/LinkPicker/LinkPicker.js | 7 +-- .../src/components/LinkPicker/LinkPicker.scss | 2 +- .../components/LinkPicker/LinkPickerTitle.js | 6 ++- .../LinkPicker/tests/LinkPicker-test.js | 1 + .../LinkPicker/tests/LinkPickerTitle-test.js | 15 +++++++ client/src/entwine/LinkField.js | 1 + src/Controllers/LinkFieldController.php | 44 ++++++++++++++----- src/Form/LinkField.php | 16 +++---- src/Form/LinkField_Readonly.php | 27 ------------ src/Form/MultiLinkField.php | 15 ++++--- src/Form/MultiLinkField_Readonly.php | 27 ------------ 14 files changed, 81 insertions(+), 87 deletions(-) delete mode 100644 src/Form/LinkField_Readonly.php delete mode 100644 src/Form/MultiLinkField_Readonly.php diff --git a/client/dist/styles/bundle.css b/client/dist/styles/bundle.css index b9106c1f..3b8902f0 100644 --- a/client/dist/styles/bundle.css +++ b/client/dist/styles/bundle.css @@ -1 +1 @@ -.link-field__container{position:relative}.link-field__loading{position:absolute;top:0;left:0;width:100%;height:100%}.link-field__loading .cms-content-loading-overlay{position:relative}.link-field__save-record-first{padding-top:7px}.link-picker__link,.link-picker{display:flex;height:auto;width:100%;min-height:54px;background:#fff;padding:0}.link-picker{align-items:stretch;cursor:pointer;box-shadow:none}.link-picker:not(:last-child){margin-bottom:10px}.link-picker.font-icon-link::before{margin:.76925rem}.link-picker__cannot-create{cursor:default;flex-grow:1;padding:16px 13px}.link-picker__menu{flex-grow:1}.link-picker__menu-toggle{width:100%;height:100%;text-align:left}.link-picker__menu-toggle::before{padding:.76925rem}.link-picker__menu-icon{vertical-align:middle;padding-right:.7rem}.link-picker__link{align-items:center;text-align:left;margin-right:0;justify-content:space-between;position:relative;border-top:0;border-top-left-radius:0;border-top-right-radius:0;border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}.link-picker__link:hover,.link-picker__link:focus{text-decoration:none;color:inherit}.link-picker__link::before{top:29px;left:32px;content:" ";position:absolute;border:1px solid #cf3f00;border-radius:100%;bottom:6px;box-shadow:0 0 1px .5px #fff;display:block;height:8px;width:8px;z-index:1}.link-picker__link--draft::before{background-color:#ff7f22}.link-picker__link--modified::before{background-color:#fff7f0}.link-picker__link--unsaved::before,.link-picker__link--unversioned::before,.link-picker__link--published::before{display:none}.link-picker__link--is-first,.link-picker__link--is-sorting{border-top:1px solid #ced5e1;border-top-left-radius:.23rem;border-top-right-radius:.23rem}.link-picker__link--is-last,.link-picker__link--is-sorting{border-bottom:1px solid #ced5e1;border-bottom-left-radius:.23rem;border-bottom-right-radius:.23rem}.link-picker__button{display:flex;align-items:center;flex-grow:1;height:100%;min-width:0;text-align:left;border:none;margin-right:0}.link-picker__button[class*=font-icon-]::before{position:absolute;font-size:1.231rem;padding:.76925rem;margin-right:6px;flex-grow:0}.link-picker__drag-handle{display:none;left:5px;position:absolute;z-index:100}.link-picker__drag-handle:hover{cursor:grab}.link-picker__link:hover .link-picker__drag-handle{display:block}.link-picker__links--dragging *{cursor:grabbing !important}.link-picker__link-detail{flex-grow:1;width:100%;padding-left:3.5rem}.link-picker__delete{flex-grow:0}.link-picker__url{color:#0071c4}.link-picker__type,.link-picker__title-text{display:inline-block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.link-picker__title{display:flex;align-items:center;width:100%}.link-picker__title .badge{color:#cf3f00;background-color:#fff2ea;padding:2px 3px 2px 4px}.link-picker__title-text{min-width:0;margin-right:5px}.link-picker__type{width:100%} +.link-field__container{position:relative}.link-field__loading{position:absolute;top:0;left:0;width:100%;height:100%}.link-field__loading .cms-content-loading-overlay{position:relative}.link-field__save-record-first{padding-top:7px}.link-picker__link,.link-picker{display:flex;height:auto;width:100%;min-height:54px;background:#fff;padding:0}.link-picker{align-items:stretch;cursor:pointer;box-shadow:none}.link-picker:not(:last-child){margin-bottom:10px}.link-picker.font-icon-link::before{margin:.76925rem}.link-picker__cannot-create{cursor:default;flex-grow:1;padding:16px 13px}.link-picker__menu{flex-grow:1}.link-picker__menu-toggle{width:100%;height:100%;text-align:left}.link-picker__menu-toggle::before{padding:.76925rem}.link-picker__menu-icon{vertical-align:middle;padding-right:.7rem}.link-picker__link{align-items:center;text-align:left;margin-right:0;justify-content:space-between;position:relative;border-top:0;border-top-left-radius:0;border-top-right-radius:0;border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}.link-picker__link:hover,.link-picker__link:focus{text-decoration:none;color:inherit}.link-picker__link::before{top:29px;left:32px;content:" ";position:absolute;border:1px solid #cf3f00;border-radius:100%;bottom:6px;box-shadow:0 0 1px .5px #fff;display:block;height:8px;width:8px;z-index:1}.link-picker__link--draft::before{background-color:#ff7f22}.link-picker__link--modified::before{background-color:#fff7f0}.link-picker__link--unsaved::before,.link-picker__link--unversioned::before,.link-picker__link--published::before{display:none}.link-picker__link--readonly{background-color:#eef0f4}.link-picker__link--is-first,.link-picker__link--is-sorting{border-top:1px solid #ced5e1;border-top-left-radius:.23rem;border-top-right-radius:.23rem}.link-picker__link--is-last,.link-picker__link--is-sorting{border-bottom:1px solid #ced5e1;border-bottom-left-radius:.23rem;border-bottom-right-radius:.23rem}.link-picker__button{display:flex;align-items:center;flex-grow:1;height:100%;min-width:0;text-align:left;border:none;margin-right:0}.link-picker__button[class*=font-icon-]::before{position:absolute;font-size:1.231rem;padding:.76925rem;margin-right:6px;flex-grow:0}.link-picker__drag-handle{display:none;left:5px;position:absolute;z-index:100}.link-picker__drag-handle:hover{cursor:grab}.link-picker__link:hover .link-picker__drag-handle{display:block}.link-picker__links--dragging *{cursor:grabbing !important}.link-picker__link-detail{flex-grow:1;width:100%;padding-left:3.5rem}.link-picker__delete{flex-grow:0}.link-picker__url{color:#0071c4}.link-picker__type,.link-picker__title-text{display:inline-block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.link-picker__title{display:flex;align-items:center;width:100%}.link-picker__title .badge{color:#cf3f00;background-color:#fff2ea;padding:2px 3px 2px 4px}.link-picker__title-text{min-width:0;margin-right:5px}.link-picker__type{width:100%} diff --git a/client/src/components/LinkField/LinkField.js b/client/src/components/LinkField/LinkField.js index 1a10a09c..8496b6e0 100644 --- a/client/src/components/LinkField/LinkField.js +++ b/client/src/components/LinkField/LinkField.js @@ -44,6 +44,7 @@ const LinkField = ({ actions, isMulti = false, canCreate, + readonly, ownerID, ownerClass, ownerRelation, @@ -192,6 +193,7 @@ const LinkField = ({ isLast={i === linkIDs.length - 1} isSorting={isSorting} canCreate={canCreate} + readonly={readonly} />); } return links; @@ -255,6 +257,7 @@ const LinkField = ({ onModalClosed={onModalClosed} types={types} canCreate={canCreate} + readonly={readonly} /> } { isMulti &&
{ +const LinkPicker = ({ types, onModalSuccess, onModalClosed, canCreate, readonly }) => { const [typeKey, setTypeKey] = useState(''); /** @@ -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 (
@@ -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}; diff --git a/client/src/components/LinkPicker/LinkPicker.scss b/client/src/components/LinkPicker/LinkPicker.scss index dd50ab09..ca660150 100644 --- a/client/src/components/LinkPicker/LinkPicker.scss +++ b/client/src/components/LinkPicker/LinkPicker.scss @@ -99,7 +99,7 @@ display: none; } - &.readonly { + &--readonly { background-color: $gray-100; } } diff --git a/client/src/components/LinkPicker/LinkPickerTitle.js b/client/src/components/LinkPicker/LinkPickerTitle.js index ae7d145d..bed145a6 100644 --- a/client/src/components/LinkPicker/LinkPickerTitle.js +++ b/client/src/components/LinkPicker/LinkPickerTitle.js @@ -47,6 +47,7 @@ const LinkPickerTitle = ({ isLast, isSorting, canCreate, + readonly, }) => { const { loading } = useContext(LinkFieldContext); const { @@ -66,7 +67,7 @@ const LinkPickerTitle = ({ 'link-picker__link--is-last': isLast, 'link-picker__link--is-sorting': isSorting, 'form-control': true, - 'readonly': !canCreate, + 'link-picker__link--readonly': readonly || !canCreate, }; if (versionState) { classes[`link-picker__link--${versionState}`] = true; @@ -95,7 +96,7 @@ const LinkPickerTitle = ({
- {(canDelete && canCreate) && + {(canDelete && !readonly) && }
@@ -116,6 +117,7 @@ LinkPickerTitle.propTypes = { isLast: PropTypes.bool.isRequired, isSorting: PropTypes.bool.isRequired, canCreate: PropTypes.bool.isRequired, + readonly: PropTypes.bool.isRequired, }; export default LinkPickerTitle; diff --git a/client/src/components/LinkPicker/tests/LinkPicker-test.js b/client/src/components/LinkPicker/tests/LinkPicker-test.js index 1e99394a..19b62da2 100644 --- a/client/src/components/LinkPicker/tests/LinkPicker-test.js +++ b/client/src/components/LinkPicker/tests/LinkPicker-test.js @@ -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 diff --git a/client/src/components/LinkPicker/tests/LinkPickerTitle-test.js b/client/src/components/LinkPicker/tests/LinkPickerTitle-test.js index 2e6c4b43..849b51c6 100644 --- a/client/src/components/LinkPicker/tests/LinkPickerTitle-test.js +++ b/client/src/components/LinkPicker/tests/LinkPickerTitle-test.js @@ -15,6 +15,7 @@ function makeProps(obj = {}) { typeIcon: 'font-icon-phone', canDelete: true, canCreate: true, + readonly: false, onDelete: () => {}, onClick: () => {}, isMulti: false, @@ -90,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 cannot edit', () => { + const { container } = render( + + ); + expect(container.querySelectorAll('.link-picker__link--readonly')).toHaveLength(1); +}); + +test('LinkPickerTitle render() should not have readonly class if can edit', () => { + const { container } = render( + + ); + expect(container.querySelectorAll('.link-picker__link--readonly')).toHaveLength(0); +}); diff --git a/client/src/entwine/LinkField.js b/client/src/entwine/LinkField.js index 6c59ec28..2c8f23e3 100644 --- a/client/src/entwine/LinkField.js +++ b/client/src/entwine/LinkField.js @@ -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, }; }, diff --git a/src/Controllers/LinkFieldController.php b/src/Controllers/LinkFieldController.php index ed82f7ce..eaac088e 100644 --- a/src/Controllers/LinkFieldController.php +++ b/src/Controllers/LinkFieldController.php @@ -379,7 +379,7 @@ private function createLinkForm(Link $link, string $operation): Form // Make readonly if fail can check if ($operation === 'create' && !$link->canCreate() || $operation === 'edit' && !$link->canEdit() - || $this->isReadOnlyField() && !$link->canEdit() + || $this->isReadOnlyField() ) { $form->makeReadonly(); } @@ -395,12 +395,10 @@ private function createLinkForm(Link $link, string $operation): Form */ private function isReadOnlyField(): bool { - $request = $this->getRequest(); - $ownerClass = $request->getVar('ownerClass') ?: $request->postVar('OwnerClass'); + $ownerClass = $this->getOwnerClassFromRequest(); $ownerRelation = $this->ownerRelationFromRequest(); - $isReadOnly = Injector::inst()->get($ownerClass)->getCMSFields()->dataFieldByName($ownerRelation)?->isReadonly(); - return $isReadOnly ?? false; + return (bool) Injector::inst()->get($ownerClass)->getCMSFields()->dataFieldByName($ownerRelation)?->isReadonly(); } /** @@ -485,20 +483,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); @@ -536,6 +555,7 @@ private function ownerRelationFromRequest(): string if (!$ownerRelation) { $this->jsonError(404, _t(__CLASS__ . '.INVALID_OWNER_RELATION', 'Invalid ownerRelation')); } + return $ownerRelation; } } diff --git a/src/Form/LinkField.php b/src/Form/LinkField.php index f9b2591d..823ef6ce 100644 --- a/src/Form/LinkField.php +++ b/src/Form/LinkField.php @@ -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. @@ -36,7 +33,8 @@ public function setValue($value, $data = null) public function getSchemaStateDefaults() { $data = parent::getSchemaStateDefaults(); - $data['canCreate'] = $this->getOwner()->canEdit() && !$this->isReadonly(); + $data['canCreate'] = $this->getOwner()->canEdit(); + $data['readonly'] = $this->isReadonly(); return $data; } @@ -44,7 +42,8 @@ protected function getDefaultAttributes(): array { $attributes = parent::getDefaultAttributes(); $attributes['data-value'] = $this->Value(); - $attributes['data-can-create'] = $this->getOwner()->canEdit() && !$this->isReadonly(); + $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']; @@ -63,13 +62,14 @@ public function getSchemaDataDefaults() return $data; } - /** + /** * Changes this field to the readonly field. */ public function performReadonlyTransformation() { - $copy = $this->castedCopy(LinkField_Readonly::class); + $clone = $this->castedCopy($this); + $clone->setReadonly(true); - return $copy; + return $clone; } } diff --git a/src/Form/LinkField_Readonly.php b/src/Form/LinkField_Readonly.php deleted file mode 100644 index e41b2b91..00000000 --- a/src/Form/LinkField_Readonly.php +++ /dev/null @@ -1,27 +0,0 @@ -name, $this->title); - $field->setValue($this->Value()); - $field->setForm($this->form); - - // Store values to hidden field - $valueField = new HiddenField($this->name); - $valueField->setValue($this->Value()); - $valueField->setForm($this->form); - - return $field->Field() . $valueField->Field(); - } -} diff --git a/src/Form/MultiLinkField.php b/src/Form/MultiLinkField.php index a454fd14..a8540a81 100644 --- a/src/Form/MultiLinkField.php +++ b/src/Form/MultiLinkField.php @@ -53,7 +53,8 @@ public function getSchemaStateDefaults() { $data = parent::getSchemaStateDefaults(); $data['value'] = $this->getValueArray(); - $data['canCreate'] = $this->getOwner()->canEdit() && !$this->isReadonly(); + $data['canCreate'] = $this->getOwner()->canEdit(); + $data['readonly'] = $this->isReadonly(); return $data; } @@ -61,7 +62,8 @@ protected function getDefaultAttributes(): array { $attributes = parent::getDefaultAttributes(); $attributes['data-value'] = $this->getValueArray(); - $attributes['data-can-create'] = $this->getOwner()->canEdit() && !$this->isReadonly(); + $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']; @@ -72,7 +74,7 @@ protected function getDefaultAttributes(): array /** * Extracts the value of this field, normalised as a non-associative array. */ - protected function getValueArray(): array + private function getValueArray(): array { return $this->convertValueToArray($this->Value()); } @@ -156,13 +158,14 @@ private function loadFrom(DataObject $record): void parent::setValue($value); } - /** + /** * Changes this field to the readonly field. */ public function performReadonlyTransformation() { - $copy = $this->castedCopy(MultiLinkField_Readonly::class); + $clone = $this->castedCopy($this); + $clone->setReadonly(true); - return $copy; + return $clone; } } diff --git a/src/Form/MultiLinkField_Readonly.php b/src/Form/MultiLinkField_Readonly.php deleted file mode 100644 index 4cc4768c..00000000 --- a/src/Form/MultiLinkField_Readonly.php +++ /dev/null @@ -1,27 +0,0 @@ -name, $this->title); - $field->setValue($this->getValueArray()); - $field->setForm($this->form); - - // Store values to hidden field - $valueField = new HiddenField($this->name); - $valueField->setValue($this->getValueArray()); - $valueField->setForm($this->form); - - return $field->Field() . $valueField->Field(); - } -}