From 40722cbfa35b4e545370c4b74625e1d27e87b185 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Tue, 31 Oct 2023 13:30:43 +1300 Subject: [PATCH] NEW LinkFieldController to handle FormSchema --- _config.php | 1 - _config/config.yml | 10 +- _graphql/queries.yml | 2 +- client/dist/js/bundle.js | 1001 ++++++++++++++++- client/dist/styles/bundle.css | 69 +- client/src/components/LinkField/LinkField.js | 176 ++- client/src/components/LinkModal/LinkModal.js | 21 +- .../src/components/LinkPicker/LinkPicker.js | 27 +- .../components/LinkPicker/LinkPickerMenu.js | 1 - .../components/LinkPicker/LinkPickerTitle.js | 12 +- client/src/entwine/JsonField.js | 3 +- .../linkDescription/readLinkDescription.js | 4 +- src/Controllers/LinkFieldController.php | 454 ++++++++ src/Extensions/AjaxField.php | 33 - src/Extensions/LeftAndMain.php | 25 - src/Extensions/LeftAndMainExtension.php | 36 + src/Form/FormFactory.php | 4 +- src/Form/JsonField.php | 43 +- src/GraphQL/LinkDescriptionResolver.php | 30 +- src/Models/EmailLink.php | 4 +- src/Models/ExternalLink.php | 4 +- src/Models/FileLink.php | 41 +- src/Models/Link.php | 2 +- src/Models/PhoneLink.php | 36 +- src/Models/SiteTreeLink.php | 17 +- src/Type/Type.php | 2 +- 26 files changed, 1818 insertions(+), 240 deletions(-) create mode 100644 src/Controllers/LinkFieldController.php delete mode 100644 src/Extensions/AjaxField.php delete mode 100644 src/Extensions/LeftAndMain.php create mode 100644 src/Extensions/LeftAndMainExtension.php diff --git a/_config.php b/_config.php index 9e519d4a..71c7914c 100644 --- a/_config.php +++ b/_config.php @@ -5,5 +5,4 @@ // Avoid creating global variables call_user_func(function () { - }); diff --git a/_config/config.yml b/_config/config.yml index 553ede50..f1d647c3 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -4,20 +4,12 @@ Name: linkfield SilverStripe\Admin\LeftAndMain: extensions: - - SilverStripe\LinkField\Extensions\LeftAndMain + - SilverStripe\LinkField\Extensions\LeftAndMainExtension SilverStripe\Admin\ModalController: extensions: - SilverStripe\LinkField\Extensions\ModalController -SilverStripe\Forms\TreeDropdownField: - extensions: - - SilverStripe\LinkField\Extensions\AjaxField - -SilverStripe\CMS\Forms\AnchorSelectorField: - extensions: - - SilverStripe\LinkField\Extensions\AjaxField - SilverStripe\LinkField\Form\FormFactory: extensions: - SilverStripe\LinkField\Extensions\FormFactoryExtension diff --git a/_graphql/queries.yml b/_graphql/queries.yml index 9775dadb..978f8d56 100644 --- a/_graphql/queries.yml +++ b/_graphql/queries.yml @@ -1,4 +1,4 @@ -'readLinkDescription(dataStr: String!)': +'readLinkDescription(linkID: Int!)': type: LinkDescription resolver: ['SilverStripe\LinkField\GraphQL\LinkDescriptionResolver', 'resolve'] 'readLinkTypes(keys: [ID])': diff --git a/client/dist/js/bundle.js b/client/dist/js/bundle.js index 8726ffc2..c7952039 100644 --- a/client/dist/js/bundle.js +++ b/client/dist/js/bundle.js @@ -1 +1,1000 @@ -!function(){"use strict";var e={274:function(e,t,n){i(n(510));var r=i(n(180)),a=i(n(521)),o=i(n(154));function i(e){return e&&e.__esModule?e:{default:e}}document.addEventListener("DOMContentLoaded",(()=>{(0,a.default)(),(0,o.default)(),(0,r.default)()}))},521:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(648)),a=u(n(809)),o=u(n(852)),i=u(n(117)),l=u(n(606));function u(e){return e&&e.__esModule?e:{default:e}}var d=()=>{r.default.component.registerMany({LinkPicker:a.default,LinkField:o.default,"LinkModal.FormBuilderModal":i.default,"LinkModal.InsertMediaModal":l.default})};t.default=d},154:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=i(n(648)),a=i(n(689)),o=i(n(287));function i(e){return e&&e.__esModule?e:{default:e}}var l=()=>{r.default.query.register("readLinkTypes",a.default),r.default.query.register("readLinkDescription",o.default)};t.default=l},180:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r;(r=n(648))&&r.__esModule,n(827);var a=()=>{};t.default=a},852:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,a=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=u(t);if(n&&n.has(e))return n.get(e);var r={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=a?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),o=n(827),i=n(648),l=(r=n(42))&&r.__esModule?r:{default:r};function u(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(u=function(e){return e?n:t})(e)}function d(){return d=Object.assign?Object.assign.bind():function(e){for(var t=1;tt=>{let{data:n,value:r,...o}=t,i=r||n;return"string"==typeof i&&(i=JSON.parse(i)),a.default.createElement(e,d({dataStr:JSON.stringify(i)},o,{data:i}))}),(0,i.injectGraphql)("readLinkDescription"),l.default)((e=>{let{id:t,loading:n,Loading:r,data:o,LinkPicker:l,onChange:u,types:d,linkDescription:s,...f}=e;if(n)return a.default.createElement(r,null);const[c,p]=(0,a.useState)(!1),[y,v]=(0,a.useState)(""),{typeKey:g}=o,m=d[g],k=y?d[y]:m;let _=o?o.Title:"";_||(_=o?o.TitleRelField:"");const O={title:_,link:m?{type:m,title:_,description:s}:void 0,onEdit:()=>{p(!0)},onClear:e=>{"function"==typeof u&&u(e,{id:t,value:{}})},onSelect:e=>{v(e),p(!0)},types:Object.values(d)},h={type:k,editing:c,onSubmit:(e,n,r)=>{const{SecurityID:a,action_insert:o,...i}=e;return"function"==typeof u&&u(event,{id:t,value:i}),p(!1),v(""),Promise.resolve()},onClosed:()=>{p(!1)},data:o},b=k?k.handlerName:"FormBuilderModal",j=(0,i.loadComponent)(`LinkModal.${b}`);return a.default.createElement(a.Fragment,null,a.default.createElement(l,O),a.default.createElement(j,h))}));t.default=s},606:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;i(n(754));var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=o(t);if(n&&n.has(e))return n.get(e);var r={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var i in e)if("default"!==i&&Object.prototype.hasOwnProperty.call(e,i)){var l=a?Object.getOwnPropertyDescriptor(e,i):null;l&&(l.get||l.set)?Object.defineProperty(r,i,l):r[i]=e[i]}r.default=e,n&&n.set(e,r);return r}(n(363)),a=i(n(475));function o(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(o=function(e){return e?n:t})(e)}function i(e){return e&&e.__esModule?e:{default:e}}function l(){return l=Object.assign?Object.assign.bind():function(e){for(var t=1;te({type:"INIT_FORM_SCHEMA_STACK",payload:{formSchema:{type:"insert-link",nextType:"admin"}}}),reset:()=>e({type:"RESET"})}}}))((e=>{let{type:t,editing:n,data:o,actions:i,onSubmit:u,...d}=e;if(!t)return!1;(0,r.useEffect)((()=>{n?i.initModal():i.reset()}),[n]);const s=o?{ID:o.FileID,Description:o.Title,TargetBlank:!!o.OpenInNew}:{};return r.default.createElement(a.default,l({isOpen:n,type:"insert-link",title:!1,bodyClassName:"modal__dialog",className:"insert-link__dialog-wrapper--internal",fileAttributes:s,onInsert:e=>{let{ID:n,Description:r,TargetBlank:a}=e;return u({FileID:n,Title:r,OpenInNew:a,typeKey:t.key},"",(()=>{}))}},d))}));t.default=u},117:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;u(n(754));var r=u(n(363)),a=(u(n(86)),u(n(912))),o=u(n(872)),i=u(n(902)),l=u(n(510));function u(e){return e&&e.__esModule?e:{default:e}}function d(){return d=Object.assign?Object.assign.bind():function(e){for(var t=1;t{const{schemaUrl:n}=l.default.getSection("SilverStripe\\Admin\\LeftAndMain").form.DynamicLink,r=o.default.parse(n),a=i.default.parse(r.query);return a.key=e,t&&(a.data=JSON.stringify(t)),o.default.format({...r,search:i.default.stringify(a)})};var f=e=>{let{type:t,editing:n,data:o,...i}=e;return!!t&&r.default.createElement(a.default,d({title:t.title,isOpen:n,schemaUrl:s(t.key,o),identifier:"Link.EditingLinkInfo"},i))};t.default=f},809:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;u(n(754));var r=u(n(363)),a=(n(648),u(n(86))),o=(n(127),u(n(820))),i=u(n(97)),l=u(n(734));u(n(686));function u(e){return e&&e.__esModule?e:{default:e}}function d(){return d=Object.assign?Object.assign.bind():function(e){for(var t=1;t{let{types:t,onSelect:n,link:a,onEdit:u,onClear:s}=e;return r.default.createElement("div",{className:(0,o.default)("link-picker","form-control",{"link-picker--selected":a})},void 0===a&&r.default.createElement(i.default,{types:t,onSelect:n}),a&&r.default.createElement(l.default,d({},a,{onClear:s,onClick:()=>a&&u&&u(a)})))};t.Component=s,s.propTypes={...i.default.propTypes,link:a.default.shape(l.default.propTypes),onEdit:a.default.func,onClear:a.default.func};var f=s;t.default=f},97:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=d(n(754)),a=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=u(t);if(n&&n.has(e))return n.get(e);var r={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=a?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),o=(n(648),d(n(86))),i=n(127),l=(d(n(820)),d(n(686)));function u(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(u=function(e){return e?n:t})(e)}function d(e){return e&&e.__esModule?e:{default:e}}const s=e=>{let{types:t,onSelect:n}=e;const[o,l]=(0,a.useState)(!1);return a.default.createElement(i.Dropdown,{isOpen:o,toggle:()=>l((e=>!e)),className:"link-picker__menu"},a.default.createElement(i.DropdownToggle,{className:"link-picker__menu-toggle font-icon-link",caret:!0},r.default._t("Link.ADD_LINK","Add Link")),a.default.createElement(i.DropdownMenu,null,t.map((e=>{let{key:t,title:r}=e;return a.default.createElement(i.DropdownItem,{key:t,onClick:()=>n(t)},r)}))))};s.propTypes={types:o.default.arrayOf(l.default).isRequired,onSelect:o.default.func.isRequired};var f=s;t.default=f},734:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(754)),a=u(n(363)),o=u(n(86)),i=u(n(686)),l=n(127);function u(e){return e&&e.__esModule?e:{default:e}}const d=e=>t=>{t.nativeEvent.stopImmediatePropagation(),t.preventDefault(),t.nativeEvent.preventDefault(),t.stopPropagation(),e&&e()},s=e=>{let{title:t,type:n,description:o,onClear:i,onClick:u}=e;return a.default.createElement("div",{className:"link-picker__link"},a.default.createElement(l.Button,{className:"link-picker__button font-icon-link",color:"secondary",onClick:d(u)},a.default.createElement("div",{className:"link-picker__link-detail"},a.default.createElement("div",{className:"link-picker__title"},t),a.default.createElement("small",{className:"link-picker__type"},n.title,": ",a.default.createElement("span",{className:"link-picker__url"},o)))),a.default.createElement(l.Button,{className:"link-picker__clear",color:"link",onClick:d(i)},r.default._t("Link.CLEAR","Clear")))};s.propTypes={title:o.default.string.isRequired,type:i.default,description:o.default.string,onClear:o.default.func,onClick:o.default.func};var f=s;t.default=f},115:function(e,t,n){var r=l(n(311)),a=l(n(363)),o=l(n(691)),i=n(648);function l(e){return e&&e.__esModule?e:{default:e}}function u(){return u=Object.assign?Object.assign.bind():function(e){for(var t=1;t{e(".js-injector-boot .entwine-jsonfield").entwine({Component:null,Root:null,onmatch(){const e=this.closest(".cms-content").attr("id"),t=e?{context:e}:{},n=this.data("schema-component"),r=(0,i.loadComponent)(n,t);this.setComponent(r),this.setRoot(o.default.createRoot(this[0])),this._super(),this.refresh()},refresh(){const e=this.getProps(),t=this.getComponent();this.getRoot().render(a.default.createElement(t,u({},e,{noHolder:!0})))},handleChange(t,n){let{id:r,value:a}=n;const o=e(this).data("field-id");e("#"+o).val(JSON.stringify(a)).trigger("change"),this.refresh()},getProps(){const t=e(this).data("field-id"),n=e("#"+t).val();return{id:t,value:n?JSON.parse(n):void 0,onChange:this.handleChange.bind(this)}},onunmatch(){this.getRoot().unmount()}})}))},287:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=n(648);const a={props(e){const{data:{error:t,readLinkDescription:n,loading:r}}=e,a=t&&t.graphQLErrors&&t.graphQLErrors.map((e=>e.message));return{loading:r,linkDescription:n?n.description:"",graphQLErrors:a}}},{READ:o}=r.graphqlTemplates;var i={apolloConfig:a,templateName:o,pluralName:"LinkDescription",pagination:!1,params:{dataStr:"String!"},args:{root:{dataStr:"dataStr"}},fields:["description"]};t.default=i},689:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=n(648);const a={props(e){const{data:{error:t,readLinkTypes:n,loading:r}}=e,a=t&&t.graphQLErrors&&t.graphQLErrors.map((e=>e.message));return{loading:r,types:n?n.reduce(((e,t)=>({...e,[t.key]:t})),{}):{},graphQLErrors:a}}},{READ:o}=r.graphqlTemplates;var i={apolloConfig:a,templateName:o,pluralName:"LinkTypes",pagination:!1,params:{keys:"[ID]"},args:{root:{keys:"keys"}},fields:["key","title","handlerName"]};t.default=i},686:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,a=(r=n(86))&&r.__esModule?r:{default:r};var o=a.default.shape({key:a.default.string.isRequired,title:a.default.string.isRequired});t.default=o},510:function(e){e.exports=Config},42:function(e){e.exports=FieldHolder},912:function(e){e.exports=FormBuilderModal},648:function(e){e.exports=Injector},475:function(e){e.exports=InsertMediaModal},872:function(e){e.exports=NodeUrl},86:function(e){e.exports=PropTypes},363:function(e){e.exports=React},691:function(e){e.exports=ReactDomClient},624:function(e){e.exports=ReactRedux},127:function(e){e.exports=Reactstrap},827:function(e){e.exports=Redux},820:function(e){e.exports=classnames},754:function(e){e.exports=i18n},311:function(e){e.exports=jQuery},902:function(e){e.exports=qs}},t={};function n(r){var a=t[r];if(void 0!==a)return a.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n(274),n(115)}(); \ No newline at end of file +/******/ (function() { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ({ + +/***/ "./client/src/boot/index.js": +/*!**********************************!*\ + !*** ./client/src/boot/index.js ***! + \**********************************/ +/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { + + + +var _Config = _interopRequireDefault(__webpack_require__(/*! lib/Config */ "lib/Config")); +var _registerReducers = _interopRequireDefault(__webpack_require__(/*! ./registerReducers */ "./client/src/boot/registerReducers.js")); +var _registerComponents = _interopRequireDefault(__webpack_require__(/*! ./registerComponents */ "./client/src/boot/registerComponents.js")); +var _registerQueries = _interopRequireDefault(__webpack_require__(/*! ./registerQueries */ "./client/src/boot/registerQueries.js")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +document.addEventListener('DOMContentLoaded', () => { + (0, _registerComponents.default)(); + (0, _registerQueries.default)(); + (0, _registerReducers.default)(); +}); + +/***/ }), + +/***/ "./client/src/boot/registerComponents.js": +/*!***********************************************!*\ + !*** ./client/src/boot/registerComponents.js ***! + \***********************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _Injector = _interopRequireDefault(__webpack_require__(/*! lib/Injector */ "lib/Injector")); +var _LinkPicker = _interopRequireDefault(__webpack_require__(/*! components/LinkPicker/LinkPicker */ "./client/src/components/LinkPicker/LinkPicker.js")); +var _LinkField = _interopRequireDefault(__webpack_require__(/*! components/LinkField/LinkField */ "./client/src/components/LinkField/LinkField.js")); +var _LinkModal = _interopRequireDefault(__webpack_require__(/*! components/LinkModal/LinkModal */ "./client/src/components/LinkModal/LinkModal.js")); +var _FileLinkModal = _interopRequireDefault(__webpack_require__(/*! components/LinkModal/FileLinkModal */ "./client/src/components/LinkModal/FileLinkModal.js")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +const registerComponents = () => { + _Injector.default.component.registerMany({ + LinkPicker: _LinkPicker.default, + LinkField: _LinkField.default, + 'LinkModal.FormBuilderModal': _LinkModal.default, + 'LinkModal.InsertMediaModal': _FileLinkModal.default + }); +}; +var _default = registerComponents; +exports["default"] = _default; + +/***/ }), + +/***/ "./client/src/boot/registerQueries.js": +/*!********************************************!*\ + !*** ./client/src/boot/registerQueries.js ***! + \********************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _Injector = _interopRequireDefault(__webpack_require__(/*! lib/Injector */ "lib/Injector")); +var _readLinkTypes = _interopRequireDefault(__webpack_require__(/*! state/linkTypes/readLinkTypes */ "./client/src/state/linkTypes/readLinkTypes.js")); +var _readLinkDescription = _interopRequireDefault(__webpack_require__(/*! state/linkDescription/readLinkDescription */ "./client/src/state/linkDescription/readLinkDescription.js")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +const registerQueries = () => { + _Injector.default.query.register('readLinkTypes', _readLinkTypes.default); + _Injector.default.query.register('readLinkDescription', _readLinkDescription.default); +}; +var _default = registerQueries; +exports["default"] = _default; + +/***/ }), + +/***/ "./client/src/boot/registerReducers.js": +/*!*********************************************!*\ + !*** ./client/src/boot/registerReducers.js ***! + \*********************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _Injector = _interopRequireDefault(__webpack_require__(/*! lib/Injector */ "lib/Injector")); +var _redux = __webpack_require__(/*! redux */ "redux"); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +const registerReducers = () => {}; +var _default = registerReducers; +exports["default"] = _default; + +/***/ }), + +/***/ "./client/src/components/LinkField/LinkField.js": +/*!******************************************************!*\ + !*** ./client/src/components/LinkField/LinkField.js ***! + \******************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _react = _interopRequireWildcard(__webpack_require__(/*! react */ "react")); +var _redux = __webpack_require__(/*! redux */ "redux"); +var _reactRedux = __webpack_require__(/*! react-redux */ "react-redux"); +var _Injector = __webpack_require__(/*! lib/Injector */ "lib/Injector"); +var _FieldHolder = _interopRequireDefault(__webpack_require__(/*! components/FieldHolder/FieldHolder */ "components/FieldHolder/FieldHolder")); +var toastsActions = _interopRequireWildcard(__webpack_require__(/*! state/toasts/ToastsActions */ "state/toasts/ToastsActions")); +var _Backend = _interopRequireDefault(__webpack_require__(/*! lib/Backend */ "lib/Backend")); +var _Config = _interopRequireDefault(__webpack_require__(/*! lib/Config */ "lib/Config")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } +const LinkField = _ref => { + let { + loading, + Loading, + data, + LinkPicker, + onChange, + types, + actions, + ...props + } = _ref; + const [editing, setEditing] = (0, _react.useState)(false); + const [typeKey, setTypeKey] = (0, _react.useState)(data.typeKey || ''); + const [linkID, setLinkID] = (0, _react.useState)(data.ID || 0); + const [linkData, setLinkData] = (0, _react.useState)({}); + const onClear = event => { + const endpoint = `${section.form.DynamicLink.deleteUrl}/${linkID}`; + _Backend.default.delete(endpoint).then(() => { + actions.toasts.success('Deleted link'); + }).catch(() => { + actions.toasts.error('Failed to delete link'); + }); + setLinkID(0); + setTypeKey(''); + if (typeof onChange === 'function') { + onChange(event, {}); + } + }; + const onModalSubmit = async (modalData, action, submitFn) => { + const { + SecurityID, + action_submit: actionSubmit, + ...data + } = modalData; + let id = 0; + const formSchema = await submitFn(); + if (typeof formSchema !== 'undefined') { + let match = formSchema.id.match(/\/linkForm\/[a-z\-]+\/([0-9]+)$/); + if (match) { + id = parseInt(match[1]); + } + match = formSchema.id.match(/\/schema\/linkfield\/[a-z\-]+\/([0-9]+)$/); + if (match) { + id = parseInt(match[1]); + } + } + if (typeof onChange === 'function') { + data.ID = id; + onChange(null, data); + } + setLinkID(id); + setEditing(false); + actions.toasts.success('Saved link'); + return Promise.resolve(); + }; + const section = _Config.default.getSection('SilverStripe\\Admin\\LeftAndMain'); + const handlerName = 'FormBuilderModal'; + const LinkModal = (0, _Injector.loadComponent)(`LinkModal.${handlerName}`); + const hasLinkData = Object.keys(linkData).length > 0; + const theData = hasLinkData ? linkData : data; + const title = theData.Title || theData.TitleRelField || ''; + const type = types.hasOwnProperty(typeKey) ? types[typeKey] : {}; + const pickerProps = { + title, + description: theData.description, + typeTitle: type.title || '', + onEdit: () => { + setEditing(true); + }, + onClear, + onSelect: key => { + setTypeKey(key); + setEditing(true); + }, + types: Object.values(types) + }; + const modalProps = { + typeTitle: type.title || '', + typeKey, + editing, + onSubmit: onModalSubmit, + onClosed: () => { + setEditing(false); + }, + linkID, + data: theData + }; + (0, _react.useEffect)(() => { + if (!editing && linkID) { + const endpoint = `${section.form.DynamicLink.dataUrl}/${linkID}`; + _Backend.default.get(endpoint).then(response => response.json()).then(responseJson => { + setLinkData(responseJson); + }); + } + }, [editing, linkID]); + if (loading) { + return _react.default.createElement(Loading, null); + } + return _react.default.createElement(_react.default.Fragment, null, _react.default.createElement(LinkPicker, pickerProps), _react.default.createElement(LinkModal, modalProps)); +}; +const stringifyData = Component => _ref2 => { + let { + value, + data, + ...props + } = _ref2; + let dataValue = value || data; + if (typeof dataValue === 'string') { + dataValue = JSON.parse(dataValue); + } + return _react.default.createElement(Component, _extends({}, props, { + data: dataValue + })); +}; +const mapDispatchToProps = dispatch => { + return { + actions: { + toasts: (0, _redux.bindActionCreators)(toastsActions, dispatch) + } + }; +}; +var _default = (0, _redux.compose)((0, _Injector.inject)(['LinkPicker', 'Loading']), (0, _Injector.injectGraphql)('readLinkTypes'), stringifyData, _FieldHolder.default, (0, _reactRedux.connect)(null, mapDispatchToProps))(LinkField); +exports["default"] = _default; + +/***/ }), + +/***/ "./client/src/components/LinkModal/FileLinkModal.js": +/*!**********************************************************!*\ + !*** ./client/src/components/LinkModal/FileLinkModal.js ***! + \**********************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _i18n = _interopRequireDefault(__webpack_require__(/*! i18n */ "i18n")); +var _react = _interopRequireWildcard(__webpack_require__(/*! react */ "react")); +var _InsertMediaModal = _interopRequireDefault(__webpack_require__(/*! containers/InsertMediaModal/InsertMediaModal */ "containers/InsertMediaModal/InsertMediaModal")); +var _reactRedux = __webpack_require__(/*! react-redux */ "react-redux"); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } +const FileLinkModal = _ref => { + let { + type, + editing, + data, + actions, + onSubmit, + ...props + } = _ref; + if (!type) { + return false; + } + (0, _react.useEffect)(() => { + if (editing) { + actions.initModal(); + } else { + actions.reset(); + } + }, [editing]); + const attrs = data ? { + ID: data.FileID, + Description: data.Title, + TargetBlank: data.OpenInNew ? true : false + } : {}; + const onInsert = _ref2 => { + let { + ID, + Description, + TargetBlank + } = _ref2; + return onSubmit({ + FileID: ID, + Title: Description, + OpenInNew: TargetBlank, + typeKey: type.key + }, '', () => {}); + }; + return _react.default.createElement(_InsertMediaModal.default, _extends({ + isOpen: editing, + type: "insert-link", + title: false, + bodyClassName: "modal__dialog", + className: "insert-link__dialog-wrapper--internal", + fileAttributes: attrs, + onInsert: onInsert + }, props)); +}; +function mapStateToProps() { + return {}; +} +function mapDispatchToProps(dispatch) { + return { + actions: { + initModal: () => dispatch({ + type: 'INIT_FORM_SCHEMA_STACK', + payload: { + formSchema: { + type: 'insert-link', + nextType: 'admin' + } + } + }), + reset: () => dispatch({ + type: 'RESET' + }) + } + }; +} +var _default = (0, _reactRedux.connect)(mapStateToProps, mapDispatchToProps)(FileLinkModal); +exports["default"] = _default; + +/***/ }), + +/***/ "./client/src/components/LinkModal/LinkModal.js": +/*!******************************************************!*\ + !*** ./client/src/components/LinkModal/LinkModal.js ***! + \******************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _i18n = _interopRequireDefault(__webpack_require__(/*! i18n */ "i18n")); +var _react = _interopRequireDefault(__webpack_require__(/*! react */ "react")); +var _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ "prop-types")); +var _FormBuilderModal = _interopRequireDefault(__webpack_require__(/*! components/FormBuilderModal/FormBuilderModal */ "components/FormBuilderModal/FormBuilderModal")); +var _url = _interopRequireDefault(__webpack_require__(/*! url */ "url")); +var _qs = _interopRequireDefault(__webpack_require__(/*! qs */ "qs")); +var _Config = _interopRequireDefault(__webpack_require__(/*! lib/Config */ "lib/Config")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } +const buildSchemaUrl = (key, linkID) => { + const { + schemaUrl + } = _Config.default.getSection('SilverStripe\\Admin\\LeftAndMain').form.DynamicLink; + const parsedURL = _url.default.parse(schemaUrl); + const parsedQs = _qs.default.parse(parsedURL.query); + for (const prop of ['href', 'path', 'pathname']) { + parsedURL[prop] = `${parsedURL[prop]}/${key}/${linkID}`; + } + return _url.default.format({ + ...parsedURL, + search: _qs.default.stringify(parsedQs) + }); +}; +const LinkModal = _ref => { + let { + typeTitle, + typeKey, + linkID, + data, + editing, + ...props + } = _ref; + if (!typeKey) { + return false; + } + return _react.default.createElement(_FormBuilderModal.default, _extends({ + title: typeTitle, + isOpen: editing, + schemaUrl: buildSchemaUrl(typeKey, linkID), + identifier: "Link.EditingLinkInfo" + }, props)); +}; +var _default = LinkModal; +exports["default"] = _default; + +/***/ }), + +/***/ "./client/src/components/LinkPicker/LinkPicker.js": +/*!********************************************************!*\ + !*** ./client/src/components/LinkPicker/LinkPicker.js ***! + \********************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = exports.Component = void 0; +var _react = _interopRequireDefault(__webpack_require__(/*! react */ "react")); +var _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ "prop-types")); +var _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ "classnames")); +var _LinkPickerMenu = _interopRequireDefault(__webpack_require__(/*! ./LinkPickerMenu */ "./client/src/components/LinkPicker/LinkPickerMenu.js")); +var _LinkPickerTitle = _interopRequireDefault(__webpack_require__(/*! ./LinkPickerTitle */ "./client/src/components/LinkPicker/LinkPickerTitle.js")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +const LinkPicker = _ref => { + let { + title, + description, + typeTitle, + types, + onSelect, + onEdit, + onClear, + ...props + } = _ref; + return _react.default.createElement("div", { + className: (0, _classnames.default)('link-picker', 'form-control', { + 'link-picker--selected': typeTitle ? true : false + }) + }, !typeTitle && _react.default.createElement(_LinkPickerMenu.default, { + types: types, + onSelect: onSelect + }), typeTitle && _react.default.createElement(_LinkPickerTitle.default, { + title: title, + description: description, + typeTitle: typeTitle, + onClear: onClear, + onClick: () => onEdit() + })); +}; +exports.Component = LinkPicker; +LinkPicker.propTypes = { + ..._LinkPickerMenu.default.propTypes, + title: _propTypes.default.string, + description: _propTypes.default.string, + typeTitle: _propTypes.default.string, + onEdit: _propTypes.default.func, + onClear: _propTypes.default.func, + onSelect: _propTypes.default.func +}; +var _default = LinkPicker; +exports["default"] = _default; + +/***/ }), + +/***/ "./client/src/components/LinkPicker/LinkPickerMenu.js": +/*!************************************************************!*\ + !*** ./client/src/components/LinkPicker/LinkPickerMenu.js ***! + \************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _i18n = _interopRequireDefault(__webpack_require__(/*! i18n */ "i18n")); +var _react = _interopRequireWildcard(__webpack_require__(/*! react */ "react")); +var _Injector = __webpack_require__(/*! lib/Injector */ "lib/Injector"); +var _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ "prop-types")); +var _reactstrap = __webpack_require__(/*! reactstrap */ "reactstrap"); +var _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ "classnames")); +var _LinkType = _interopRequireDefault(__webpack_require__(/*! types/LinkType */ "./client/src/types/LinkType.js")); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +const LinkPickerMenu = _ref => { + let { + types, + onSelect + } = _ref; + const [isOpen, setIsOpen] = (0, _react.useState)(false); + const toggle = () => setIsOpen(prevState => !prevState); + return _react.default.createElement(_reactstrap.Dropdown, { + isOpen: isOpen, + toggle: toggle, + className: "link-picker__menu" + }, _react.default.createElement(_reactstrap.DropdownToggle, { + className: "link-picker__menu-toggle font-icon-link", + caret: true + }, _i18n.default._t('Link.ADD_LINK', 'Add Link')), _react.default.createElement(_reactstrap.DropdownMenu, null, types.map(_ref2 => { + let { + key, + title + } = _ref2; + return _react.default.createElement(_reactstrap.DropdownItem, { + key: key, + onClick: () => onSelect(key) + }, title); + }))); +}; +LinkPickerMenu.propTypes = { + types: _propTypes.default.arrayOf(_LinkType.default).isRequired, + onSelect: _propTypes.default.func.isRequired +}; +var _default = LinkPickerMenu; +exports["default"] = _default; + +/***/ }), + +/***/ "./client/src/components/LinkPicker/LinkPickerTitle.js": +/*!*************************************************************!*\ + !*** ./client/src/components/LinkPicker/LinkPickerTitle.js ***! + \*************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _i18n = _interopRequireDefault(__webpack_require__(/*! i18n */ "i18n")); +var _react = _interopRequireDefault(__webpack_require__(/*! react */ "react")); +var _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ "prop-types")); +var _reactstrap = __webpack_require__(/*! reactstrap */ "reactstrap"); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +const stopPropagation = fn => e => { + e.nativeEvent.stopImmediatePropagation(); + e.preventDefault(); + e.nativeEvent.preventDefault(); + e.stopPropagation(); + fn && fn(); +}; +const LinkPickerTitle = _ref => { + let { + title, + description, + typeTitle, + onClear, + onClick, + ...props + } = _ref; + return _react.default.createElement("div", { + className: "link-picker__link" + }, _react.default.createElement(_reactstrap.Button, { + className: "link-picker__button font-icon-link", + color: "secondary", + onClick: stopPropagation(onClick) + }, _react.default.createElement("div", { + className: "link-picker__link-detail" + }, _react.default.createElement("div", { + className: "link-picker__title" + }, title), _react.default.createElement("small", { + className: "link-picker__type" + }, typeTitle, ":\xA0", _react.default.createElement("span", { + className: "link-picker__url" + }, description)))), _react.default.createElement(_reactstrap.Button, { + className: "link-picker__clear", + color: "link", + onClick: stopPropagation(onClear) + }, _i18n.default._t('Link.CLEAR', 'Clear'))); +}; +LinkPickerTitle.propTypes = { + title: _propTypes.default.string.isRequired, + linkTypeTitle: _propTypes.default.string, + description: _propTypes.default.string, + onClear: _propTypes.default.func, + onClick: _propTypes.default.func +}; +var _default = LinkPickerTitle; +exports["default"] = _default; + +/***/ }), + +/***/ "./client/src/entwine/JsonField.js": +/*!*****************************************!*\ + !*** ./client/src/entwine/JsonField.js ***! + \*****************************************/ +/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { + + + +var _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ "jquery")); +var _react = _interopRequireDefault(__webpack_require__(/*! react */ "react")); +var _client = _interopRequireDefault(__webpack_require__(/*! react-dom/client */ "react-dom/client")); +var _Injector = __webpack_require__(/*! lib/Injector */ "lib/Injector"); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } +_jquery.default.entwine('ss', $ => { + $('.js-injector-boot .entwine-jsonfield').entwine({ + Component: null, + Root: null, + onmatch() { + const cmsContent = this.closest('.cms-content').attr('id'); + const context = cmsContent ? { + context: cmsContent + } : {}; + const schemaComponent = this.data('schema-component'); + const ReactField = (0, _Injector.loadComponent)(schemaComponent, context); + this.setComponent(ReactField); + this.setRoot(_client.default.createRoot(this[0])); + this._super(); + this.refresh(); + }, + refresh() { + const props = this.getProps(); + const ReactField = this.getComponent(); + const Root = this.getRoot(); + Root.render(_react.default.createElement(ReactField, _extends({}, props, { + noHolder: true + }))); + }, + handleChange(event, value) { + const fieldID = $(this).data('field-id'); + $('#' + fieldID).val(JSON.stringify(value)).trigger('change'); + this.refresh(); + }, + getProps() { + const fieldID = $(this).data('field-id'); + const dataStr = $('#' + fieldID).val(); + const value = dataStr ? JSON.parse(dataStr) : undefined; + return { + value, + onChange: this.handleChange.bind(this) + }; + }, + onunmatch() { + const Root = this.getRoot(); + Root.unmount(); + } + }); +}); + +/***/ }), + +/***/ "./client/src/state/linkDescription/readLinkDescription.js": +/*!*****************************************************************!*\ + !*** ./client/src/state/linkDescription/readLinkDescription.js ***! + \*****************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _Injector = __webpack_require__(/*! lib/Injector */ "lib/Injector"); +const apolloConfig = { + props(props) { + const { + data: { + error, + readLinkDescription, + loading: networkLoading + } + } = props; + const errors = error && error.graphQLErrors && error.graphQLErrors.map(graphQLError => graphQLError.message); + const linkDescription = readLinkDescription ? readLinkDescription.description : ''; + return { + loading: networkLoading, + linkDescription, + graphQLErrors: errors + }; + } +}; +const { + READ +} = _Injector.graphqlTemplates; +const query = { + apolloConfig, + templateName: READ, + pluralName: 'LinkDescription', + pagination: false, + params: { + linkID: 'Int!' + }, + args: { + root: { + linkID: 'linkID' + } + }, + fields: ['description'] +}; +var _default = query; +exports["default"] = _default; + +/***/ }), + +/***/ "./client/src/state/linkTypes/readLinkTypes.js": +/*!*****************************************************!*\ + !*** ./client/src/state/linkTypes/readLinkTypes.js ***! + \*****************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _Injector = __webpack_require__(/*! lib/Injector */ "lib/Injector"); +const apolloConfig = { + props(props) { + const { + data: { + error, + readLinkTypes, + loading: networkLoading + } + } = props; + const errors = error && error.graphQLErrors && error.graphQLErrors.map(graphQLError => graphQLError.message); + const types = readLinkTypes ? readLinkTypes.reduce((accumulator, type) => ({ + ...accumulator, + [type.key]: type + }), {}) : {}; + return { + loading: networkLoading, + types, + graphQLErrors: errors + }; + } +}; +const { + READ +} = _Injector.graphqlTemplates; +const query = { + apolloConfig, + templateName: READ, + pluralName: 'LinkTypes', + pagination: false, + params: { + keys: '[ID]' + }, + args: { + root: { + keys: 'keys' + } + }, + fields: ['key', 'title', 'handlerName'] +}; +var _default = query; +exports["default"] = _default; + +/***/ }), + +/***/ "./client/src/types/LinkType.js": +/*!**************************************!*\ + !*** ./client/src/types/LinkType.js ***! + \**************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ "prop-types")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +const LinkType = _propTypes.default.shape({ + key: _propTypes.default.string.isRequired, + title: _propTypes.default.string.isRequired +}); +var _default = LinkType; +exports["default"] = _default; + +/***/ }), + +/***/ "lib/Backend": +/*!**************************!*\ + !*** external "Backend" ***! + \**************************/ +/***/ (function(module) { + +module.exports = Backend; + +/***/ }), + +/***/ "lib/Config": +/*!*************************!*\ + !*** external "Config" ***! + \*************************/ +/***/ (function(module) { + +module.exports = Config; + +/***/ }), + +/***/ "components/FieldHolder/FieldHolder": +/*!******************************!*\ + !*** external "FieldHolder" ***! + \******************************/ +/***/ (function(module) { + +module.exports = FieldHolder; + +/***/ }), + +/***/ "components/FormBuilderModal/FormBuilderModal": +/*!***********************************!*\ + !*** external "FormBuilderModal" ***! + \***********************************/ +/***/ (function(module) { + +module.exports = FormBuilderModal; + +/***/ }), + +/***/ "lib/Injector": +/*!***************************!*\ + !*** external "Injector" ***! + \***************************/ +/***/ (function(module) { + +module.exports = Injector; + +/***/ }), + +/***/ "containers/InsertMediaModal/InsertMediaModal": +/*!***********************************!*\ + !*** external "InsertMediaModal" ***! + \***********************************/ +/***/ (function(module) { + +module.exports = InsertMediaModal; + +/***/ }), + +/***/ "url": +/*!**************************!*\ + !*** external "NodeUrl" ***! + \**************************/ +/***/ (function(module) { + +module.exports = NodeUrl; + +/***/ }), + +/***/ "prop-types": +/*!****************************!*\ + !*** external "PropTypes" ***! + \****************************/ +/***/ (function(module) { + +module.exports = PropTypes; + +/***/ }), + +/***/ "react": +/*!************************!*\ + !*** external "React" ***! + \************************/ +/***/ (function(module) { + +module.exports = React; + +/***/ }), + +/***/ "react-dom/client": +/*!*********************************!*\ + !*** external "ReactDomClient" ***! + \*********************************/ +/***/ (function(module) { + +module.exports = ReactDomClient; + +/***/ }), + +/***/ "react-redux": +/*!*****************************!*\ + !*** external "ReactRedux" ***! + \*****************************/ +/***/ (function(module) { + +module.exports = ReactRedux; + +/***/ }), + +/***/ "reactstrap": +/*!*****************************!*\ + !*** external "Reactstrap" ***! + \*****************************/ +/***/ (function(module) { + +module.exports = Reactstrap; + +/***/ }), + +/***/ "redux": +/*!************************!*\ + !*** external "Redux" ***! + \************************/ +/***/ (function(module) { + +module.exports = Redux; + +/***/ }), + +/***/ "state/toasts/ToastsActions": +/*!********************************!*\ + !*** external "ToastsActions" ***! + \********************************/ +/***/ (function(module) { + +module.exports = ToastsActions; + +/***/ }), + +/***/ "classnames": +/*!*****************************!*\ + !*** external "classnames" ***! + \*****************************/ +/***/ (function(module) { + +module.exports = classnames; + +/***/ }), + +/***/ "i18n": +/*!***********************!*\ + !*** external "i18n" ***! + \***********************/ +/***/ (function(module) { + +module.exports = i18n; + +/***/ }), + +/***/ "jquery": +/*!*************************!*\ + !*** external "jQuery" ***! + \*************************/ +/***/ (function(module) { + +module.exports = jQuery; + +/***/ }), + +/***/ "qs": +/*!*********************!*\ + !*** external "qs" ***! + \*********************/ +/***/ (function(module) { + +module.exports = qs; + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. +!function() { +/*!**************************************!*\ + !*** ./client/src/bundles/bundle.js ***! + \**************************************/ + + +__webpack_require__(/*! boot */ "./client/src/boot/index.js"); +__webpack_require__(/*! entwine/JsonField */ "./client/src/entwine/JsonField.js"); +}(); +/******/ })() +; +//# sourceMappingURL=bundle.js.map \ No newline at end of file diff --git a/client/dist/styles/bundle.css b/client/dist/styles/bundle.css index 5c415c8c..9b3eaaf0 100644 --- a/client/dist/styles/bundle.css +++ b/client/dist/styles/bundle.css @@ -1 +1,68 @@ -.link-picker{display:flex;height:auto;min-height:54px;background:#fff;width:100%;align-items:stretch;cursor:pointer;padding:0;box-shadow:none}.link-picker.font-icon-link::before{margin:.76925rem}.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__link{display:flex;align-items:center;width:100%;text-align:left;border:none;margin-right:0;justify-content:space-between}.link-picker__link:hover,.link-picker__link:focus{background:#eef0f4;text-decoration:none;color:inherit}.link-picker__button{display:flex;align-items:center;flex-grow:1;height:100%;text-align:left;border:none;margin-right:0}.link-picker__button::before{font-size:1.231rem;padding:.76925rem;margin-right:6px;flex-grow:0}.link-picker__link-detail{flex-grow:1}.link-picker__clear{flex-grow:0}.link-picker__url{color:#0071c4} +/*!*****************************************************************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[0].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[0].use[2]!./node_modules/resolve-url-loader/index.js??ruleSet[1].rules[0].use[3]!./node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[0].use[4]!./client/src/styles/bundle.scss ***! + \*****************************************************************************************************************************************************************************************************************************************************************************************************************************/ +.link-picker { + display: flex; + height: auto; + min-height: 54px; + background: white; + width: 100%; + align-items: stretch; + cursor: pointer; + padding: 0; + box-shadow: none; +} +.link-picker.font-icon-link::before { + margin: 0.76925rem; +} +.link-picker__menu { + flex-grow: 1; +} +.link-picker__menu-toggle { + width: 100%; + height: 100%; + text-align: left; +} +.link-picker__menu-toggle::before { + padding: 0.76925rem; +} +.link-picker__link { + display: flex; + align-items: center; + width: 100%; + text-align: left; + border: none; + margin-right: 0; + justify-content: space-between; +} +.link-picker__link:hover, .link-picker__link:focus { + background: #eef0f4; + text-decoration: none; + color: inherit; +} +.link-picker__button { + display: flex; + align-items: center; + flex-grow: 1; + height: 100%; + text-align: left; + border: none; + margin-right: 0; +} +.link-picker__button::before { + font-size: 1.231rem; + padding: 0.76925rem; + margin-right: 6px; + flex-grow: 0; +} +.link-picker__link-detail { + flex-grow: 1; +} +.link-picker__clear { + flex-grow: 0; +} +.link-picker__url { + color: #0071c4; +} + +/*# sourceMappingURL=bundle.css.map*/ \ No newline at end of file diff --git a/client/src/components/LinkField/LinkField.js b/client/src/components/LinkField/LinkField.js index 5571450f..89e1dde3 100644 --- a/client/src/components/LinkField/LinkField.js +++ b/client/src/components/LinkField/LinkField.js @@ -1,90 +1,170 @@ -import React, { Fragment, useState } from 'react'; -import { compose } from 'redux'; +import React, { useState, useEffect } from 'react'; +import { bindActionCreators, compose } from 'redux'; +import { connect } from 'react-redux'; import { inject, injectGraphql, loadComponent } from 'lib/Injector'; import fieldHolder from 'components/FieldHolder/FieldHolder'; +import * as toastsActions from 'state/toasts/ToastsActions'; +import backend from 'lib/Backend'; +import Config from 'lib/Config'; -const LinkField = ({ id, loading, Loading, data, LinkPicker, onChange, types, linkDescription, ...props }) => { - if (loading) { - return ; - } +const LinkField = ({ loading, Loading, data, LinkPicker, onChange, types, actions, ...props}) => { const [editing, setEditing] = useState(false); - const [newTypeKey, setNewTypeKey] = useState(''); + const [typeKey, setTypeKey] = useState(data.typeKey || ''); + const [linkID, setLinkID] = useState(data.ID || 0); + const [linkData, setLinkData] = useState({}); const onClear = (event) => { - if (typeof onChange !== 'function') { - return; + // deleteUrl is set via LeftAndMainExtension.php + const endpoint = `${section.form.DynamicLink.deleteUrl}/${linkID}`; + backend.delete(endpoint) + .then(() => { + actions.toasts.success('Deleted link'); + }) + .catch(() => { + actions.toasts.error('Failed to delete link'); + }); + + setLinkID(0); + setTypeKey(''); + if (typeof onChange === 'function') { + onChange(event, {}); } - - onChange(event, { id, value: {} }); + }; + + const onModalSubmit = async (modalData, action, submitFn) => { + const { + SecurityID, + action_submit: actionSubmit, + ...data + } = modalData; + + // get id from formSchema + let id = 0; + const formSchema = await submitFn(); + // formSchema will be undefined for FileLink which uses asset-admin graphql when using InsertMediaModal rather than FormBuilderModal + if (typeof formSchema !== 'undefined') { + // onsuccess formSchema.id + let match = formSchema.id.match(/\/linkForm\/[a-z\-]+\/([0-9]+)$/); + if (match) { + id = parseInt(match[1]); + } + // onfailure formSchema.id + match = formSchema.id.match(/\/schema\/linkfield\/[a-z\-]+\/([0-9]+)$/); + if (match) { + id = parseInt(match[1]); + } + } + + // update parent JsonField data id - this is required to update the underlying + // so that the Page (or other parent DataObject) gets the Link relation ID set + if (typeof onChange === 'function') { + data.ID = id; + onChange(null, data); + } + + // update component state + setLinkID(id); + setEditing(false); + // setTypeKey(''); + + // trigger success toast + actions.toasts.success('Saved link'); + + return Promise.resolve(); }; - const { typeKey } = data; - const type = types[typeKey]; - const modalType = newTypeKey ? types[newTypeKey] : type; + const section = Config.getSection('SilverStripe\\Admin\\LeftAndMain'); + // const typeKey = data.typeKey; + // const type = types[typeKey] || {}; + // const linkType = newTypeKey ? types[newTypeKey] : type; + // const handlerName = modalType ? modalType.handlerName : 'FormBuilderModal'; + const handlerName = 'FormBuilderModal'; + const LinkModal = loadComponent(`LinkModal.${handlerName}`); - let title = data ? data.Title : ''; + // jsonFieldData is the initial value of the field passed from JsonField + // linkData is XHR'd in from the endpoint afterwards + const hasLinkData = Object.keys(linkData).length > 0; + const theData = hasLinkData ? linkData : data; + const title = theData.Title || theData.TitleRelField || ''; - if (!title) { - title = data ? data.TitleRelField : ''; - } + const type = types.hasOwnProperty(typeKey) ? types[typeKey] : {}; - const linkProps = { + const pickerProps = { title, - link: type ? { type, title, description: linkDescription } : undefined, - onEdit: () => { setEditing(true); }, + description: theData.description, + typeTitle: type.title || '', + onEdit: () => { + setEditing(true); + }, onClear, onSelect: (key) => { - setNewTypeKey(key); + setTypeKey(key); setEditing(true); }, types: Object.values(types) }; - const onModalSubmit = (modalData, action, submitFn) => { - const { SecurityID, action_insert: actionInsert, ...value } = modalData; - - if (typeof onChange === 'function') { - onChange(event, { id, value }); - } - - setEditing(false); - setNewTypeKey(''); - - return Promise.resolve(); - }; - const modalProps = { - type: modalType, + // type: modalType, + typeTitle: type.title || '', + typeKey, editing, onSubmit: onModalSubmit, onClosed: () => { setEditing(false); }, - data + linkID, + data: theData }; - const handlerName = modalType ? modalType.handlerName : 'FormBuilderModal'; - const LinkModal = loadComponent(`LinkModal.${handlerName}`); + // read data from endpoint and update component state + useEffect(() => { + if (!editing && linkID) { + const endpoint = `${section.form.DynamicLink.dataUrl}/${linkID}`; + backend.get(endpoint) + .then(response => response.json()) + .then(responseJson => { + setLinkData(responseJson); + }); + } + }, [editing, linkID]); + + if (loading) { + return ; + } - return - - - ; + return <> + + + ; }; -const stringifyData = (Component) => (({ data, value, ...props }) => { +const stringifyData = (Component) => (({ value, data, ...props }) => { let dataValue = value || data; if (typeof dataValue === 'string') { dataValue = JSON.parse(dataValue); } - return ; + return ; }); +const mapDispatchToProps = (dispatch) => { + return { + actions: { + toasts: bindActionCreators(toastsActions, dispatch), + }, + }; +} + export default compose( inject(['LinkPicker', 'Loading']), injectGraphql('readLinkTypes'), - stringifyData, - injectGraphql('readLinkDescription'), - fieldHolder + stringifyData, // get rid of this? + // injectGraphql('readLinkDescription'), + fieldHolder, + connect(null, mapDispatchToProps) )(LinkField); diff --git a/client/src/components/LinkModal/LinkModal.js b/client/src/components/LinkModal/LinkModal.js index 7e5ce86d..b9ebade3 100644 --- a/client/src/components/LinkModal/LinkModal.js +++ b/client/src/components/LinkModal/LinkModal.js @@ -7,30 +7,29 @@ import url from 'url'; import qs from 'qs'; import Config from 'lib/Config'; -const leftAndMain = 'SilverStripe\\Admin\\LeftAndMain'; +const buildSchemaUrl = (key, linkID) => { -const buildSchemaUrl = (key, data) => { - - const {schemaUrl} = Config.getSection(leftAndMain).form.DynamicLink; + const {schemaUrl} = Config.getSection('SilverStripe\\Admin\\LeftAndMain').form.DynamicLink; const parsedURL = url.parse(schemaUrl); const parsedQs = qs.parse(parsedURL.query); - parsedQs.key = key; - if (data) { - parsedQs.data = JSON.stringify(data); + // parsedQs.key = key; // todo: change to request param + for (const prop of ['href', 'path', 'pathname']) { + // parsedURL[prop] += `/${id}`; + parsedURL[prop] = `${parsedURL[prop]}/${key}/${linkID}`; } return url.format({ ...parsedURL, search: qs.stringify(parsedQs)}); } -const LinkModal = ({type, editing, data, ...props}) => { - if (!type) { +const LinkModal = ({ typeTitle, typeKey, linkID, data, editing, ...props }) => { + if (!typeKey) { return false; } return ; diff --git a/client/src/components/LinkPicker/LinkPicker.js b/client/src/components/LinkPicker/LinkPicker.js index 98aab6ec..62cd7217 100644 --- a/client/src/components/LinkPicker/LinkPicker.js +++ b/client/src/components/LinkPicker/LinkPicker.js @@ -1,30 +1,33 @@ /* eslint-disable */ -import i18n from 'i18n'; import React from 'react'; -import { inject } from 'lib/Injector'; import PropTypes from 'prop-types'; -import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem, Button } from 'reactstrap'; import classnames from 'classnames'; import LinkPickerMenu from './LinkPickerMenu'; import LinkPickerTitle from './LinkPickerTitle'; -import LinkType from 'types/LinkType'; -const LinkPicker = ({ types, onSelect, link, onEdit, onClear }) => ( -
- {link === undefined && } - {link && link && onEdit && onEdit(link)}/>} +const LinkPicker = ({ title, description, typeTitle, types, onSelect, onEdit, onClear, ...props }) => { + return
+ {!typeTitle && } + {typeTitle && onEdit()} + />}
-); +} LinkPicker.propTypes = { ...LinkPickerMenu.propTypes, - link: PropTypes.shape(LinkPickerTitle.propTypes), + title: PropTypes.string, + description: PropTypes.string, + typeTitle: PropTypes.string, onEdit: PropTypes.func, onClear: PropTypes.func, + onSelect: PropTypes.func }; - export {LinkPicker as Component}; export default LinkPicker; diff --git a/client/src/components/LinkPicker/LinkPickerMenu.js b/client/src/components/LinkPicker/LinkPickerMenu.js index 036f8392..75c5687b 100644 --- a/client/src/components/LinkPicker/LinkPickerMenu.js +++ b/client/src/components/LinkPicker/LinkPickerMenu.js @@ -11,7 +11,6 @@ const LinkPickerMenu = ({ types, onSelect }) => { const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(prevState => !prevState); - return ( (e) => { @@ -13,24 +12,25 @@ const stopPropagation = (fn) => (e) => { fn && fn(); } -const LinkPickerTitle = ({ title, type, description, onClear, onClick }) => ( -
+const LinkPickerTitle = ({ title, description, typeTitle, onClear, onClick, ...props }) => { + return
-); +}; LinkPickerTitle.propTypes = { title: PropTypes.string.isRequired, - type: LinkType, + linkTypeTitle: PropTypes.string, description: PropTypes.string, onClear: PropTypes.func, onClick: PropTypes.func diff --git a/client/src/entwine/JsonField.js b/client/src/entwine/JsonField.js index ee8f22c4..6d924f17 100644 --- a/client/src/entwine/JsonField.js +++ b/client/src/entwine/JsonField.js @@ -33,7 +33,7 @@ jQuery.entwine('ss', ($) => { Root.render(); }, - handleChange(event, {id, value}) { + handleChange(event, value) { const fieldID = $(this).data('field-id'); $('#' + fieldID).val(JSON.stringify(value)).trigger('change'); this.refresh(); @@ -50,7 +50,6 @@ jQuery.entwine('ss', ($) => { const value = dataStr ? JSON.parse(dataStr) : undefined; return { - id: fieldID, value, onChange: this.handleChange.bind(this) }; diff --git a/client/src/state/linkDescription/readLinkDescription.js b/client/src/state/linkDescription/readLinkDescription.js index 2301bd26..1c051fe3 100644 --- a/client/src/state/linkDescription/readLinkDescription.js +++ b/client/src/state/linkDescription/readLinkDescription.js @@ -31,11 +31,11 @@ const query = { pluralName: 'LinkDescription', pagination: false, params: { - dataStr: 'String!' + linkID: 'Int!' }, args: { root: { - dataStr: 'dataStr' + linkID: 'linkID' } }, fields: ['description'], diff --git a/src/Controllers/LinkFieldController.php b/src/Controllers/LinkFieldController.php new file mode 100644 index 00000000..f3cfa347 --- /dev/null +++ b/src/Controllers/LinkFieldController.php @@ -0,0 +1,454 @@ + 'apiSaveForm', + + // 'GET linkForm/$ItemID/field/$FieldName/$FieldAction/$FieldArg' => 'handleLinkFormFieldRequest', + + 'linkForm/$LinkKey/$ItemID' => 'getLinkForm', + 'DELETE deleteLink/$ItemID' => 'apiDeleteLink', + 'GET data/$ItemID' => 'apiLinkData', + + ]; + + private static $allowed_actions = [ + // 'linkForm' needs to defined for the benefit of LeftAndMain::schema() even though + // 'getLinkForm()' is the method that's ultimately called + // it's pretty unintuitive and could be made better, though it works for now + 'linkForm', + // 'apiSaveForm', + + 'apiDeleteLink', + 'apiLinkData', + + 'getLinkForm', // remove this? + + // 'handleLinkFormFieldRequest', + ]; + + /** + * + */ + public function init() + { + parent::init(); + // TODO have temporarily disabled for easier dev + SecurityToken::disable(); + // Check security token + if (!SecurityToken::inst()->checkRequest($this->getRequest())) { + $this->jsonError(400, 'Invalid CSRF token'); + } + // Add full X-Formschema-Request header to request to ensure entire FormSchema is returned + // This means we don't need to worry about adding them in javascript + // This makes dev life easier and we should update this to be the default behaviour + $request = $this->getRequest(); + $header = $request->getHeader(LeftAndMain::SCHEMA_HEADER) ?? ''; + if ($header === '' || $header === 'auto') { + // this will overwrite any existing header + $request->addHeader(LeftAndMain::SCHEMA_HEADER, 'auto,schema,state,errors'); + } + // add in arbitary key so that linkKeyFromRequest() doesn't complain about key missing + // yes this is a double up with what's happening in handleLinkFormFieldRequest() + // if (!$request->getVar('key')) { + // // add in an arbitary key so that getLinkForm() doesn't complain about key missing + // // this isn't actually used for the HTTPResponse + // $key = array_keys((new Registry)->list())[0]; + // $request->offsetSet('key', $key); + // } + } + + // /** + // * This is wrong, should rely on the existing Silverstripe individual form pieces + // * to find its way to get there + // * + // * Handle a linkform field request e.g. TreeDropdownField in SiteTreeLink + // * requests will look like this for a Link with an ID of 23 + // * /admin/linkfield/saveForm/23/field/PageID/tree + // */ + // public function handleLinkFormFieldRequest(): HTTPResponse + // { + // $request = $this->getRequest(); + // $fieldAction = $request->param('FieldAction'); + // if (!$request->getVar('key')) { + // // add in an arbitary key so that getLinkForm() doesn't complain about key missing + // // this isn't actually used for the HTTPResponse + // $key = array_keys((new Registry)->list())[0]; + // $request->offsetSet('key', $key); + // } + + // $form = $this->getLinkForm(); + // $formRequestHandler = $form->getRequestHandler(); + // // note the other param in this request $FieldName is important as it's used in + // // FormRequestHandler::handleField() to work out which field to extract from the form + // $field = $formRequestHandler->handleField($request); + + // // Special logic for AnchorSelectorField + // if (is_a($field, AnchorSelectorField::class)) { + // // AnchorSelectorField::anchors() expects a $request with a $PageID + // $pageID = $request->param('FieldArg'); + // $routeParams = $request->routeParams(); + // $routeParams['PageID'] = $pageID; + // $request->setRouteParams($routeParams); + // // AnchorSelectorField::anchors() uses $this->getRequest() rather than $request arg + // // so we need to ensure the field is the correct request object + // $field->setRequest($request); + // } + + // // e.g. TreeDropdownField::tree($request); + // $ret = $field->$fieldAction($request); + // if (!is_a($ret, HTTPResponse::class)) { + // $response = $this->getResponse(); + // if (is_array($ret) || is_object($ret)) { + // $ret = json_encode($ret); + // } + // $response->setBody($ret); + // $response->addHeader('Content-type', 'application/json'); + // $ret = $response; + // } + // return $ret; + // } + + /** + * TODOL Is this actually used? I think LeftAndMainExtension::updateClientConfig() was what's used? + */ + // public function getClientConfig() + // { + // $clientConfig = parent::getClientConfig(); + // $clientConfig['form']['DynamicLink'] = [ // << todo: rename to linkForm or something + // // will probably need to update something in client + // // schemaUrl will get the $LinkType and $ItemID dynamically suffixed in LinkModal.js + // 'schemaUrl' => $this->Link('schema/linkForm'), + // // 'saveUrl' => $this->Link('saveForm'), + // 'saveUrl' => $this->Link(), + // 'saveMethod' => 'post', + // 'payloadFormat' => 'json', + // 'formNameTemplate' => sprintf(self::FORM_NAME_TEMPLATE, '{id}'), + // ]; + + // // Configuration that is available per element type + // $clientConfig['elementTypes'] = ElementTypeRegistry::generate()->getDefinitions(); + + // return $clientConfig; + // } + + + /** + * This method is called from LeftAndMain::schema() + * /admin/linkfield/schema/linkForm/ + * + * Adapted from ElementalAreaController::getElementForm() + * + * @return Form + */ + public function getLinkForm(): Form + { + // saving an existing form + $id = (int) $this->itemIDFromRequest(); + + if ($id) { + // ORM will automatically get the correct subclass e.g. EmailLink + $link = Link::get()->byID($id); + if (!$link) { + $this->jsonError(400, 'Invalid link id'); + } + } else { + $linkKey = $this->linkKeyFromRequest(); + $link = (new Registry)->byKey($linkKey); + if (!$link) { + $this->jsonError(400, 'Invalid key'); + } + } + return $this->createLinkForm($link); + } + + private function createLinkForm(Link $link): Form + { + $id = $link->ID; + + // $formFactory = FormFactory::create(); // << extends LinkFormFactory + $formFactory = new DefaultFormFactory; // << feed it a DataObject with $context['Record'] = $obj + + /** @var Form $form */ + $form = $formFactory->getForm( + $this, + sprintf(self::FORM_NAME_TEMPLATE, $id), + [ + // This is from when we were using LinkFormFactory + // 'LinkType' => $obj, + // 'LinkTypeKey' => $linkTypeKey, + + // This is for DefaultFormFactory + 'Record' => $link, + + // the following is here for (silverstripe/admin) LinkFormFactory::getRequiredContext() + // 'RequireLinkText' => false + ] + ); + + // LinkFormFactory has this styling + $form->addExtraClass('form--no-dividers'); + + $linkKey = (new Registry)->keyByClassName($link->ClassName); + // TODO: remove this, use request param for this + // $form->Fields()->push(HiddenField::create('typeKey')->setValue($linkKey)); + + // This is required so that when the form is submitted it goes to the right place + // this really should be solved by LeftAndMainExtension::updateClientConfig() + // where it sets 'saveUrl' ... :shrug: ... not sure why it's not + // shouldn't have to set here + $urlSegment = $this->config()->get('url_segment'); + // $form->setFormAction("admin/$urlSegment/saveForm/$id"); + // $form->setFormAction("admin/$urlSegment/linkForm/$id"); + $form->setFormAction("admin/$urlSegment/linkForm/$linkKey/$id"); + + // Add save action button + $title = $id ? 'Update link' : 'Create link'; // todo: _t() + + // Copied from LinkFormFactory::getFormActions() + $actions = FieldList::create([ + FormAction::create('save', $title) + ->setSchemaData(['data' => ['buttonStyle' => 'primary']]), + ]); + $form->setActions($actions); + + // This is required so that FormRequestHandler::getValidationErrorResponse() will return + // a FormSchema response rather than the default FormRequestHandler::getAjaxErrorResponse() + // which isn't useful + $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id) { + $schemaId = Controller::join_links( + $this->Link('schema'), + $this->config()->get('url_segment'), + $id + ); + return $this->getSchemaResponse($schemaId, $form, $errors); + }); + + if (!$link->canEdit()) { + $form->makeReadonly(); + } + + // TODO shrug - do i need this (probably not) + $form->setRequestHandler( + LeftAndMainFormRequestHandler::create($form, [$id]) + ); + + return $form; + } + + /** + * We end up here via FormRequestHandler::httpSubmission() + * We end up at the 'save' method specifically because this is FormAction set on the Form + */ + public function save(array $data, Form $form): HTTPResponse + { + if (empty($data)) { + $this->jsonError(400, 'Empty data'); + } + // Ensure that ItemID url param is used as the source of truth for ID. ID shouldn't be in the + // POST request, though just remove it in case someone tries to add it in + if (isset($data['ID'])) { + unset($data['ID']); + } + + /** @var Link $link */ + $id = (int) $this->itemIDFromRequest(); + if ($id) { + // Editing an existing Link + $link = Link::get()->byID($id); + } else { + // Createing a new Link + $key = $this->linkKeyFromRequest(); + /** @var Link $className */ + $className = (new Registry)->list()[$key] ?? ''; + if (!$className) { + $this->jsonError(400, 'Invalid key'); + } + $link = $className::create(); + } + + // Ensure the DataObject can be edited by the current user + if (!$link || !$link->canEdit()) { + $this->jsonError(403, 'Unauthorized'); + } + + // Get form and populate it with POSTed data + // $form = $this->createLinkForm($link); + // $form->loadDataFrom($data); // <<<<<<<<<<<< TODO Do I still need this? + + // Validate the Form + // TODO: this is probably not needed as Form validation is already done in + // FormRequestHandler::httpSubmission() + // TODO: what happens when the form fails validation? looks like there' a ->redirectBack() in there? + // $validationResult = $form->validationResult(); + + // Update DataObject from from data + $form->saveInto($link); + + // Special logic for FileLink + if (is_a($link, FileLink::class)) { + // FileField value will come in as $postVars['File']['Files'][0]; + // $form->saveInto($link); doesn't seem to handle this + $link->FileID = $data['File']['Files'][0] ?? 0; + } + + // DataObject validation + // thrown ValidationException will be caught in FormRequestHandler::httpSubmission() + // Note: Form (as opposed to DataObject) validate() is run in FormRequestHandler::httpSubmission() + $validationResult = $link->validate(); + if (!$validationResult->isValid()) { + throw new ValidationException($validationResult); + } + + if ($link->isChanged()) { + $link->write(); + } + + // Create a new Form so that it has the correct ID for the DataObject when creating + // a new DataObject, as well as anything else on the DataObject that may have been + // updated in an extension hook. We do this so that the FormSchema state is correct + // before returning it in the response + $form = $this->createLinkForm($link); + + // // TODO: probably not - handled in FormRequestHandler + // // Validate the DataObject and combine the two validation results together + // $validationResult->combineAnd($link->validate()); + + // // Write to the DataObject if things are valid and it's appropriate to do so + // if ($validationResult->isValid()) { + // if ($link->isChanged()) { + // $link->write(); + // } + // // Create a new Form so that it has the correct ID for the DataObject when creating + // // a new DataObject, as well as anything else on the DataObject that may have been + // // updated in an extension hook. We do this so that the FormSchema state is correct + // // before returning it in the response + // $form = $this->createLinkForm($link); + // } + + // Create and send FormSchema JSON response + $schemaID = $form->FormAction(); + $response = $this->getSchemaResponse($schemaID, $form, $validationResult); + + // returning a 400 means that FormBuilder.js::handleSubmit() submitFn() + // that will end up in the catch() .. throw error block and the error + // will just end up in the javascript console + // $response->setStatusCode(400); + // return a 200 for now just to get things to work even though it's + // clearly the wrong code if !$validationResult->isValid(). + // Will require a PR to admin to fix this + $response->setStatusCode(200); + return $response; + } + + private function itemIDFromRequest(): string + { + $request = $this->getRequest(); + if ($request->param('Action') === 'schema') { + // routing done by LeftAndMain::schema() + $itemID = (string) $request->param('OtherItemID'); + } else { + // routing done by this class + $itemID = (string) $request->param('ItemID'); + } + if (!ctype_digit($itemID)) { + $this->jsonError(400, "Invalid itemID param $itemID"); + } + return $itemID; + } + + private function linkKeyFromRequest(): string + { + $request = $this->getRequest(); + + if ($request->param('Action') === 'schema') { + // routing done by LeftAndMain::schema() + $linkKey = (string) $request->param('ItemID'); + } else { + // routing done by this class + $linkKey = (string) $request->param('LinkKey'); + } + if (strlen($linkKey) === 0 || !preg_match('#^[a-z\-]+$#', $linkKey)) { + $this->jsonError(400, "Invalid linkKey param $linkKey"); + } + return $linkKey; + } + + /** + * I tried to use GraphQL but could work out apollo so make this instead + * this query did work in graphql ide + * mutation { deleteLinks(ids: [44]) } + */ + public function apiDeleteLink(): HTTPResponse + { + $link = $this->getLinkFromParam(); + if (!$link->canDelete()) { + $this->jsonError(403, 'Unable to delete Link'); + } + // This will also delete any published version immediately + $link->delete(); + $response = $this->getResponse(); + $response->addHeader('Content-type', 'application/json'); + $response->setBody(json_encode(['success' => true])); + return $response; + } + + /** + * I tried to use GraphQL but it stopped making requests once I switched it to + * passing an ID instead of the entire JSON blob of the link - seems like there's + * some logic where passing the same param means don't refetch? + */ + public function apiLinkData(): HTTPResponse + { + $link = $this->getLinkFromParam(); + if (!$link->canView()) { + $this->jsonError(403, 'Unable to view link'); + } + $response = $this->getResponse(); + $response->addHeader('Content-type', 'application/json'); + $data = $link->jsonSerialize(); + $data['description'] = $link->getLinkDescription(); + $response->setBody(json_encode($data)); + return $response; + } + + private function getLinkFromParam(): Link + { + $id = (int) $this->getRequest()->param('ItemID'); + if (!$id) { + $this->jsonError(400, 'Missing ID'); + } + $link = Link::get()->byID($id); + if (!$link) { + $this->jsonError(400, 'Invalid ID'); + } + return $link; + } +} diff --git a/src/Extensions/AjaxField.php b/src/Extensions/AjaxField.php deleted file mode 100644 index 7764c65c..00000000 --- a/src/Extensions/AjaxField.php +++ /dev/null @@ -1,33 +0,0 @@ -getOwner(); - $formName = $owner->getForm()->getName(); - - if ($formName !== 'Modals/DynamicLink') { - return; - } - - $request = $owner->getForm()->getController()->getRequest(); - $key = $request->getVar('key'); - - $link .= strpos($link, '?') === false ? '?' : '&'; - $link .= "key={$key}"; - } -} diff --git a/src/Extensions/LeftAndMain.php b/src/Extensions/LeftAndMain.php deleted file mode 100644 index d1173a3d..00000000 --- a/src/Extensions/LeftAndMain.php +++ /dev/null @@ -1,25 +0,0 @@ -init(); - } - - public function updateClientConfig(&$clientConfig) - { - $clientConfig['form']['DynamicLink'] = [ - 'schemaUrl' => $this->getOwner()->Link('methodSchema/Modals/DynamicLink'), - ]; - } -} diff --git a/src/Extensions/LeftAndMainExtension.php b/src/Extensions/LeftAndMainExtension.php new file mode 100644 index 00000000..5b10bde4 --- /dev/null +++ b/src/Extensions/LeftAndMainExtension.php @@ -0,0 +1,36 @@ +init(); + } + + public function updateClientConfig(&$clientConfig) + { + $urlSegment = 'linkfield'; + $owner = $this->getOwner(); + $clientConfig['form']['DynamicLink'] = [ + // schemaUrl will get the $LinkType and $ItemID dynamically suffixed in LinkModal.js + 'schemaUrl' => $owner->Link("$urlSegment/schema/linkForm"), + 'saveUrl' => $owner->Link("$urlSegment/getLinkForm/save"), // not used at all? uses setFormAction() + // 'saveUrl' => $owner->Link("$urlSegment/saveForm"), // << change this? + 'deleteUrl' => $owner->Link("$urlSegment/deleteLink"), + 'dataUrl' => $owner->Link("$urlSegment/data"), + 'saveMethod' => 'post', + 'payloadFormat' => 'json', // not true? + 'formNameTemplate' => sprintf(LinkFieldController::FORM_NAME_TEMPLATE, '{id}'), + ]; + } +} diff --git a/src/Form/FormFactory.php b/src/Form/FormFactory.php index e9f57f5c..81e58b05 100644 --- a/src/Form/FormFactory.php +++ b/src/Form/FormFactory.php @@ -9,6 +9,8 @@ use SilverStripe\ORM\DataObject; /** + * TODO problably just rid of this class and add logic directly in LinkFieldController::getLinkForm() + * * Create Form schema for the LinkField based on a key provided by the request. */ class FormFactory extends LinkFormFactory @@ -18,7 +20,7 @@ protected function getFormFields($controller, $name, $context) /** @var Type $type */ $type = $context['LinkType']; - if (!$type instanceof Type) { + if (!is_a($type, Type::class)) { throw new LogicException(sprintf('%s: LinkType must be provided and must be an instance of Type', static::class)); } diff --git a/src/Form/JsonField.php b/src/Form/JsonField.php index 4f7ad0b6..6514b72a 100644 --- a/src/Form/JsonField.php +++ b/src/Form/JsonField.php @@ -2,11 +2,13 @@ namespace SilverStripe\LinkField\Form; +use Exception; use InvalidArgumentException; use SilverStripe\Forms\FormField; use SilverStripe\LinkField\JsonData; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; +use SilverStripe\LinkField\Models\Link; /** * Field designed to edit complex data passed as a JSON string. Other FormFields can be built on top of this one. @@ -28,7 +30,7 @@ public function setValue($value, $data = null) } /** - * @param DataObject|DataObjectInterface $record + * @param DataObject|DataObjectInterface $record - A DataObject such as a Page * @return $this */ public function saveInto(DataObjectInterface $record) @@ -40,37 +42,14 @@ public function saveInto(DataObjectInterface $record) return $this; } - $dataValue = $this->dataValue(); - $value = is_string($dataValue) ? $this->parseString($this->dataValue()) : $dataValue; - - if ($class = DataObject::getSchema()->hasOneComponent(get_class($record), $fieldname)) { - /** @var JsonData|DataObject $jsonDataObject */ - - $jsonDataObjectID = $record->{"{$fieldname}ID"}; - - if ($jsonDataObjectID && $jsonDataObject = $record->$fieldname) { - if ($value) { - $jsonDataObject = $jsonDataObject->setData($value); - $this->extend('onBeforeLinkEdit', $jsonDataObject, $record); - $jsonDataObject->write(); - $this->extend('onAfterLinkEdit', $jsonDataObject, $record); - } else { - $this->extend('onBeforeLinkDelete', $jsonDataObject, $record); - $jsonDataObject->delete(); - $record->{"{$fieldname}ID"} = 0; - $this->extend('onAfterLinkDelete', $jsonDataObject, $record); - } - } elseif ($value) { - $jsonDataObject = new $class(); - $jsonDataObject = $jsonDataObject->setData($value); - $this->extend('onBeforeLinkCreate', $jsonDataObject, $record); - $jsonDataObject->write(); - $record->{"{$fieldname}ID"} = $jsonDataObject->ID; - $this->extend('onAfterLinkCreate', $jsonDataObject, $record); - } - } elseif ((DataObject::getSchema()->databaseField(get_class($record), $fieldname))) { - $record->{$fieldname} = $value; - } + // This code will all change in a later PR once we refactor out the JSON data format + // This method used to update the Link DataObject which was a relation + // Update the Pages has_one relationship ID using the existing JSON data format + $data = $this->parseString($this->dataValue()); + // Not 'ID' key is perfectly valid e.g. removing an existing link + $linkID = $data['ID'] ?? 0; + $dbColumn = $fieldname . 'ID'; + $record->$dbColumn = $linkID; return $this; } diff --git a/src/GraphQL/LinkDescriptionResolver.php b/src/GraphQL/LinkDescriptionResolver.php index 2259f25d..920cb274 100644 --- a/src/GraphQL/LinkDescriptionResolver.php +++ b/src/GraphQL/LinkDescriptionResolver.php @@ -3,32 +3,22 @@ namespace SilverStripe\LinkField\GraphQL; use GraphQL\Type\Definition\ResolveInfo; -use InvalidArgumentException; use SilverStripe\GraphQL\Schema\DataObject\Resolver; -use SilverStripe\LinkField\Type\Registry; +use SilverStripe\LinkField\Models\Link; class LinkDescriptionResolver extends Resolver { public static function resolve($obj, $args = [], $context = [], ?ResolveInfo $info = null) { - $data = json_decode($args['dataStr'], true); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw new InvalidArgumentException('data must be a valid JSON string'); - } - - $typeKey = $data['typeKey'] ?? null; - - if (!$typeKey) { - return ['description' => '']; + $linkID = $args['linkID']; + $description = ''; + if ($linkID) { + /** @var Link $link */ + $link = Link::get()->byID($linkID); + if ($link) { + $description = $link->getLinkDescription(); + } } - - $type = Registry::singleton()->byKey($typeKey); - - if (!$type) { - return ['description' => '']; - } - - return ['description' => $type->generateLinkDescription($data)]; + return ['description' => $description]; } } diff --git a/src/Models/EmailLink.php b/src/Models/EmailLink.php index 410455db..854ab70d 100644 --- a/src/Models/EmailLink.php +++ b/src/Models/EmailLink.php @@ -18,9 +18,9 @@ class EmailLink extends Link 'Email' => 'Varchar(255)', ]; - public function generateLinkDescription(array $data): string + public function getLinkDescription(): string { - return isset($data['Email']) ? $data['Email'] : ''; + return $this->Email; } public function getCMSFields(): FieldList diff --git a/src/Models/ExternalLink.php b/src/Models/ExternalLink.php index c51fb50c..05c4ff41 100644 --- a/src/Models/ExternalLink.php +++ b/src/Models/ExternalLink.php @@ -15,9 +15,9 @@ class ExternalLink extends Link 'ExternalUrl' => 'Varchar', ]; - public function generateLinkDescription(array $data): string + public function getLinkDescription(): string { - return isset($data['ExternalUrl']) ? $data['ExternalUrl'] : ''; + return $this->ExternalUrl; } public function getURL(): string diff --git a/src/Models/FileLink.php b/src/Models/FileLink.php index 0d40b933..5bb59a42 100644 --- a/src/Models/FileLink.php +++ b/src/Models/FileLink.php @@ -2,7 +2,9 @@ namespace SilverStripe\LinkField\Models; +use SilverStripe\AssetAdmin\Forms\UploadField; use SilverStripe\Assets\File; +use SilverStripe\Forms\FieldList; /** * A link to a File track in asset-admin @@ -18,22 +20,27 @@ class FileLink extends Link 'File' => File::class, ]; - public function generateLinkDescription(array $data): string + public function getLinkDescription(): string { - $fileId = $data['FileID'] ?? null; - - if (!$fileId) { - return ''; - } - - $file = File::get()->byID($fileId); - - return $file?->getFilename() ?? ''; + return $this->File()?->getFilename() ?? ''; } public function LinkTypeHandlerName(): string { - return 'InsertMediaModal'; + // 3.x + // Can't really use this as there's no FormSchema/FormBuilder, so doesn't work with + // the new way of doing things + // return 'InsertMediaModal'; + // + // Default for Link - this is currently a bit busted it requires the user + // to NOT click the 'Insert file" because this will submit the Link Form seemingly without + // the file that was just selected (presumably the JS in that component expects so callback + // to have been passed, normally used in WYSIWYG context?) + // Instead they need to click the "back" arrow before clicking + // "Insert" and then finally "Create link". You also cannot upload a new file as you'll + // end up with cannot handle sub-URLS on class LinkFieldController + // /admin/linkfield/saveForm/59/field/File/upload + return 'FormBuilderModal'; } public function getURL(): string @@ -42,4 +49,16 @@ public function getURL(): string return $file->exists() ? (string) $file->getURL() : ''; } + + public function getCMSFields(): FieldList + { + $fields = parent::getCMSFields(); + $fields->addFieldsToTab( + 'Root.Main', + [ + new UploadField('File', 'Dumb file upload field'), + ] + ); + return $fields; + } } diff --git a/src/Models/Link.php b/src/Models/Link.php index 87d2de0f..3badc322 100644 --- a/src/Models/Link.php +++ b/src/Models/Link.php @@ -52,7 +52,7 @@ public function LinkTypeHandlerName(): string return 'FormBuilderModal'; } - public function generateLinkDescription(array $data): string + public function getLinkDescription(): string { return ''; } diff --git a/src/Models/PhoneLink.php b/src/Models/PhoneLink.php index 855e3b1c..471c0787 100644 --- a/src/Models/PhoneLink.php +++ b/src/Models/PhoneLink.php @@ -2,6 +2,10 @@ namespace SilverStripe\LinkField\Models; +use SilverStripe\Forms\CompositeValidator; +use SilverStripe\Forms\RequiredFields; +use SilverStripe\Forms\Validator; + /** * A link to a phone number * @@ -15,13 +19,41 @@ class PhoneLink extends Link 'Phone' => 'Varchar(255)', ]; - public function generateLinkDescription(array $data): string + public function getLinkDescription(): string { - return isset($data['Phone']) ? $data['Phone'] : ''; + return $this->Phone; } public function getURL(): string { return $this->Phone ? sprintf('tel:%s', $this->Phone) : ''; } + + public function validate() + { + $result = parent::validate(); + $phone = $this->Phone; + if ($phone == 'x') { + $result->addFieldError('Phone', 'Cannot be x - DataObject::validate()'); + } + return $result; + } + + public function getCMSCompositeValidator(): CompositeValidator + { + $validator = parent::getCMSCompositeValidator(); + $validator->addValidator(new class extends Validator { + public function php($data): bool + { + $valid = true; + $phone = $data['Phone']; + if ($phone == 'y') { + $valid = false; + $this->validationError('Phone', 'Cannot be y -- DataObject::getCMSCompositeValidator()'); + } + return $valid; + } + }); + return $validator; + } } diff --git a/src/Models/SiteTreeLink.php b/src/Models/SiteTreeLink.php index f3f611d8..093eb066 100644 --- a/src/Models/SiteTreeLink.php +++ b/src/Models/SiteTreeLink.php @@ -30,22 +30,9 @@ class SiteTreeLink extends Link 'Page' => SiteTree::class, ]; - public function generateLinkDescription(array $data): string + public function getLinkDescription(): string { - $pageId = $data['PageID'] ?? null; - - if (!$pageId) { - return ''; - } - - /** @var SiteTree $page */ - $page = SiteTree::get()->byID($pageId); - - if (!$page?->exists()) { - return ''; - } - - return $page->URLSegment ?: ''; + return $this->Page()?->URLSegment ?? ''; } public function getCMSFields(): FieldList diff --git a/src/Type/Type.php b/src/Type/Type.php index 1d0cb428..eab74d17 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -23,7 +23,7 @@ public function LinkTypeHandlerName(): string; /** * What should be the link description be given this data. */ - public function generateLinkDescription(array $data): string; + public function getLinkDescription(): string; /** * Human readbale title of this link type