diff --git a/client/src/components/LinkField/LinkField.js b/client/src/components/LinkField/LinkField.js index b6d1d77b..a6c1d67b 100644 --- a/client/src/components/LinkField/LinkField.js +++ b/client/src/components/LinkField/LinkField.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import { injectGraphql, loadComponent } from 'lib/Injector'; import fieldHolder from 'components/FieldHolder/FieldHolder'; import LinkPicker from 'components/LinkPicker/LinkPicker'; +import LinkPickerTitle from 'components/LinkPicker/LinkPickerTitle'; import * as toastsActions from 'state/toasts/ToastsActions'; import backend from 'lib/Backend'; import Config from 'lib/Config'; @@ -22,33 +23,20 @@ const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController'; */ const LinkField = ({ value, onChange, types, actions }) => { const linkID = value; - const [typeKey, setTypeKey] = useState(''); const [data, setData] = useState({}); const [editing, setEditing] = useState(false); - /** - * Call back used by LinkModal after the form has been submitted and the response has been received - */ - const onModalSubmit = async (modalData, action, submitFn) => { - const formSchema = await submitFn(); - - // slightly annoyingly, on validation error formSchema at this point will not have an errors node - // instead it will have the original formSchema id used for the GET request to get the formSchema i.e. - // admin/linkfield/schema/linkfield/ - // instead of the one used by the POST submission i.e. - // admin/linkfield/linkForm/ - const hasValidationErrors = formSchema.id.match(/\/schema\/linkfield\/([0-9]+)/); - if (!hasValidationErrors) { - // get link id from formSchema response - const match = formSchema.id.match(/\/linkForm\/([0-9]+)/); - const valueFromSchemaResponse = parseInt(match[1], 10); + const onModalClosed = () => { + setEditing(false); + }; + const onModalSuccess = (value) => { // update component state setEditing(false); // update parent JsonField data id - this is required to update the underlying form field // so that the Page (or other parent DataObject) gets the Link relation ID set - onChange(valueFromSchemaResponse); + onChange(value); // success toast actions.toasts.success( @@ -57,16 +45,13 @@ const LinkField = ({ value, onChange, types, actions }) => { 'Saved link', ) ); - } - - return Promise.resolve(); - }; + } /** * Call back used by LinkPicker when the 'Clear' button is clicked */ - const onClear = () => { - const endpoint = `${Config.getSection(section).form.linkForm.deleteUrl}/${linkID}`; + const onClear = (id) => { + const endpoint = `${Config.getSection(section).form.linkForm.deleteUrl}/${id}`; // CSRF token 'X-SecurityID' headers needs to be present for destructive requests backend.delete(endpoint, {}, { 'X-SecurityID': Config.get('SecurityID') }) .then(() => { @@ -87,7 +72,6 @@ const LinkField = ({ value, onChange, types, actions }) => { }); // update component state - setTypeKey(''); setData({}); // update parent JsonField data ID used to update the underlying form field @@ -95,36 +79,24 @@ const LinkField = ({ value, onChange, types, actions }) => { }; const title = data.Title || ''; - const type = types.hasOwnProperty(typeKey) ? types[typeKey] : {}; - const modalType = typeKey ? types[typeKey] : type; - const handlerName = modalType && modalType.hasOwnProperty('handlerName') - ? modalType.handlerName + const type = types.hasOwnProperty(data.typeKey) ? types[data.typeKey] : {}; + const handlerName = type && type.hasOwnProperty('handlerName') + ? type.handlerName : 'FormBuilderModal'; const LinkModal = loadComponent(`LinkModal.${handlerName}`); const pickerProps = { - title, - description: data.description, - typeTitle: type.title || '', - onEdit: () => { - setEditing(true); - }, - onClear, - onSelect: (key) => { - setTypeKey(key); - setEditing(true); - }, - types: Object.values(types) + onModalSuccess, + onModalClosed, + types }; const modalProps = { typeTitle: type.title || '', - typeKey, - editing, - onSubmit: onModalSubmit, - onClosed: () => { - setEditing(false); - }, + typeKey: data.typeKey, + isOpen: editing, + onSuccess: onModalSuccess, + onClosed: onModalClosed, linkID }; @@ -136,14 +108,21 @@ const LinkField = ({ value, onChange, types, actions }) => { .then(response => response.json()) .then(responseJson => { setData(responseJson); - setTypeKey(responseJson.typeKey); }); } }, [editing, linkID]); return <> - - + {!type.title && } + {type.title && { setEditing(true); }} + />} + { editing && } ; }; diff --git a/client/src/components/LinkModal/LinkModal.js b/client/src/components/LinkModal/LinkModal.js index ec1cc499..734e974e 100644 --- a/client/src/components/LinkModal/LinkModal.js +++ b/client/src/components/LinkModal/LinkModal.js @@ -17,13 +17,37 @@ const buildSchemaUrl = (typeKey, linkID) => { return url.format({ ...parsedURL, search: qs.stringify(parsedQs)}); } -const LinkModal = ({ typeTitle, typeKey, linkID, editing, onSubmit, onClosed}) => { +const LinkModal = ({ typeTitle, typeKey, linkID = 0, isOpen, onSuccess, onClosed}) => { if (!typeKey) { return false; } + + /** + * Call back used by LinkModal after the form has been submitted and the response has been received + */ + const onSubmit = async (modalData, action, submitFn) => { + const formSchema = await submitFn(); + + // slightly annoyingly, on validation error formSchema at this point will not have an errors node + // instead it will have the original formSchema id used for the GET request to get the formSchema i.e. + // admin/linkfield/schema/linkfield/ + // instead of the one used by the POST submission i.e. + // admin/linkfield/linkForm/ + const hasValidationErrors = formSchema.id.match(/\/schema\/linkfield\/([0-9]+)/); + if (!hasValidationErrors) { + // get link id from formSchema response + const match = formSchema.id.match(/\/linkForm\/([0-9]+)/); + const valueFromSchemaResponse = parseInt(match[1], 10); + + onSuccess(valueFromSchemaResponse); + } + + return Promise.resolve(); + }; + return ( -
- {!typeTitle && } - {typeTitle && onEdit()} - />} -
-); +import LinkType from 'types/LinkType'; + +const LinkPicker = ({ types, onSelect, onModalSuccess, onModalClosed }) => { + const [typeKey, setTypeKey] = useState(''); + + const doSelect = (key) => { + if (typeof onSelect === 'function') { + onSelect(key); + } + setTypeKey(key); + } + + const onClosed = () => { + if (typeof onModalClosed === 'function') { + onModalClosed(); + } + setTypeKey(''); + } + + const onSuccess = (value) => { + setTypeKey(''); + onModalSuccess(value); + } + + const type = types.hasOwnProperty(typeKey) ? types[typeKey] : {}; + const modalType = typeKey ? types[typeKey] : type; + const handlerName = modalType && modalType.hasOwnProperty('handlerName') + ? modalType.handlerName + : 'FormBuilderModal'; + const LinkModal = loadComponent(`LinkModal.${handlerName}`); + + const isOpen = Boolean(typeKey); + + const modalProps = { + typeTitle: type.title || '', + typeKey, + isOpen, + onSuccess: onSuccess, + onClosed: onClosed, + }; + + return ( +
+ + { isOpen && } +
+ ); +}; LinkPicker.propTypes = { - ...LinkPickerMenu.propTypes, - title: PropTypes.string, - description: PropTypes.string, - typeTitle: PropTypes.string.isRequired, - onEdit: PropTypes.func.isRequired, - onClear: PropTypes.func.isRequired, - onSelect: PropTypes.func.isRequired, + types: PropTypes.objectOf(LinkType).isRequired, + onSelect: PropTypes.func, + onModalSuccess: PropTypes.func.isRequired, + onModalClosed: PropTypes.func, }; export {LinkPicker as Component}; diff --git a/client/src/components/LinkPicker/LinkPickerTitle.js b/client/src/components/LinkPicker/LinkPickerTitle.js index d925f8bc..d7e26859 100644 --- a/client/src/components/LinkPicker/LinkPickerTitle.js +++ b/client/src/components/LinkPicker/LinkPickerTitle.js @@ -12,22 +12,25 @@ const stopPropagation = (fn) => (e) => { fn && fn(); } -const LinkPickerTitle = ({ title, description, typeTitle, onClear, onClick }) => ( -
- - +const LinkPickerTitle = ({ id, title, description, typeTitle, onClear, onClick }) => ( +
+
+ + +
); LinkPickerTitle.propTypes = { + id: PropTypes.number.isRequired, title: PropTypes.string, description: PropTypes.string, typeTitle: PropTypes.string.isRequired, diff --git a/client/src/entwine/LinkField.js b/client/src/entwine/LinkField.js index ad7a472f..dd7d84db 100644 --- a/client/src/entwine/LinkField.js +++ b/client/src/entwine/LinkField.js @@ -58,7 +58,9 @@ jQuery.entwine('ss', ($) => { */ onunmatch() { const Root = this.getRoot(); - Root.unmount(); + if (Root) { + Root.unmount(); + } }, }); }); diff --git a/src/Controllers/LinkFieldController.php b/src/Controllers/LinkFieldController.php index 9e00462c..d5490860 100644 --- a/src/Controllers/LinkFieldController.php +++ b/src/Controllers/LinkFieldController.php @@ -60,7 +60,7 @@ public function getClientConfig() */ public function linkForm(): Form { - $id = (int) $this->itemIDFromRequest(); + $id = $this->itemIDFromRequest(); if ($id) { $link = Link::get()->byID($id); if (!$link) { @@ -142,7 +142,7 @@ public function save(array $data, Form $form): HTTPResponse } /** @var Link $link */ - $id = (int) $this->itemIDFromRequest(); + $id = $this->itemIDFromRequest(); if ($id) { // Editing an existing Link $operation = 'edit'; @@ -263,7 +263,7 @@ private function createLinkForm(Link $link, string $operation): Form */ private function linkFromRequest(): Link { - $itemID = (int) $this->itemIDFromRequest(); + $itemID = $this->itemIDFromRequest(); if (!$itemID) { $this->jsonError(404, _t('LinkField.INVALID_ID', 'Invalid ID')); } @@ -277,14 +277,14 @@ private function linkFromRequest(): Link /** * Get the $ItemID request param */ - private function itemIDFromRequest(): string + private function itemIDFromRequest(): int { $request = $this->getRequest(); $itemID = (string) $request->param('ItemID'); if (!ctype_digit($itemID)) { $this->jsonError(404, _t('LinkField.INVALID_ID', 'Invalid ID')); } - return $itemID; + return (int) $itemID; } /** diff --git a/src/Form/LinkField.php b/src/Form/LinkField.php index d7aeb980..87c42ee8 100644 --- a/src/Form/LinkField.php +++ b/src/Form/LinkField.php @@ -2,6 +2,7 @@ namespace SilverStripe\LinkField\Form; +use LogicException; use SilverStripe\Forms\FormField; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; @@ -36,7 +37,7 @@ public function saveInto(DataObjectInterface $record) // Check required relation details are available $fieldname = $this->getName(); if (!$fieldname) { - return $this; + throw new LogicException('LinkField must have a name'); } $linkID = $this->dataValue();