From 36a75bf8ec600d68c44b614a396ebc592ee196d2 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Tue, 31 Oct 2023 16:58:45 +1300 Subject: [PATCH] NEW LinkFieldController to handle FormSchema --- _config.php | 1 - _config/config.yml | 19 +- _config/types.yml | 1 - _graphql/queries.yml | 3 - _graphql/types.yml | 6 - client/dist/js/bundle.js | 787 +++++++++++++++++- client/dist/styles/bundle.css | 69 +- client/src/boot/index.js | 3 - client/src/boot/registerComponents.js | 3 - client/src/boot/registerQueries.js | 2 - client/src/boot/registerReducers.js | 26 - client/src/components/LinkField/LinkField.js | 175 ++-- client/src/components/LinkModal/LinkModal.js | 25 +- .../src/components/LinkPicker/LinkPicker.js | 27 +- .../components/LinkPicker/LinkPickerMenu.js | 5 +- .../components/LinkPicker/LinkPickerTitle.js | 12 +- client/src/entwine/JsonField.js | 10 +- .../linkDescription/readLinkDescription.js | 43 - client/src/state/linkTypes/readLinkTypes.js | 2 +- docs/en/linkable-migration.md | 247 ------ src/Controllers/LinkFieldController.php | 298 +++++++ src/Extensions/AjaxField.php | 33 - src/Extensions/FormFactoryExtension.php | 55 -- src/Extensions/LeftAndMain.php | 25 - src/Extensions/LeftAndMainExtension.php | 17 + src/Extensions/ModalController.php | 114 --- src/Form/FormFactory.php | 47 -- src/Form/JsonField.php | 41 +- src/Form/LinkField.php | 5 - src/GraphQL/LinkDescriptionResolver.php | 34 - src/GraphQL/LinkTypeResolver.php | 5 +- src/Models/EmailLink.php | 5 +- src/Models/ExternalLink.php | 6 +- src/Models/FileLink.php | 24 +- src/Models/Link.php | 16 +- src/Models/PhoneLink.php | 36 +- src/Models/SiteTreeLink.php | 17 +- src/Type/Registry.php | 15 +- src/Type/Type.php | 46 - tests/php/Models/LinkTest.php | 14 +- 40 files changed, 1389 insertions(+), 930 deletions(-) delete mode 100644 client/src/boot/registerReducers.js delete mode 100644 client/src/state/linkDescription/readLinkDescription.js delete mode 100644 docs/en/linkable-migration.md create mode 100644 src/Controllers/LinkFieldController.php delete mode 100644 src/Extensions/AjaxField.php delete mode 100644 src/Extensions/FormFactoryExtension.php delete mode 100644 src/Extensions/LeftAndMain.php create mode 100644 src/Extensions/LeftAndMainExtension.php delete mode 100644 src/Extensions/ModalController.php delete mode 100644 src/Form/FormFactory.php delete mode 100644 src/GraphQL/LinkDescriptionResolver.php delete mode 100644 src/Type/Type.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..1f778d13 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -1,23 +1,6 @@ --- Name: linkfield --- - SilverStripe\Admin\LeftAndMain: extensions: - - SilverStripe\LinkField\Extensions\LeftAndMain - -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 + - SilverStripe\LinkField\Extensions\LeftAndMainExtension diff --git a/_config/types.yml b/_config/types.yml index 29b5dcbb..84960245 100644 --- a/_config/types.yml +++ b/_config/types.yml @@ -1,7 +1,6 @@ --- Name: linkfield-types --- - SilverStripe\LinkField\Type\Registry: types: cms: diff --git a/_graphql/queries.yml b/_graphql/queries.yml index 9775dadb..8010e060 100644 --- a/_graphql/queries.yml +++ b/_graphql/queries.yml @@ -1,6 +1,3 @@ -'readLinkDescription(dataStr: String!)': - type: LinkDescription - resolver: ['SilverStripe\LinkField\GraphQL\LinkDescriptionResolver', 'resolve'] 'readLinkTypes(keys: [ID])': type: '[LinkType]' resolver: ['SilverStripe\LinkField\GraphQL\LinkTypeResolver', 'resolve'] diff --git a/_graphql/types.yml b/_graphql/types.yml index 70525b16..8af9de12 100644 --- a/_graphql/types.yml +++ b/_graphql/types.yml @@ -1,11 +1,5 @@ -LinkDescription: - description: Given some Link data, computes the matching description - fields: - description: String - LinkType: description: Describe a Type of Link that can be managed by a LinkField fields: key: ID - handlerName: String! title: String! diff --git a/client/dist/js/bundle.js b/client/dist/js/bundle.js index 8726ffc2..2dd3c1d2 100644 --- a/client/dist/js/bundle.js +++ b/client/dist/js/bundle.js @@ -1 +1,786 @@ -!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 _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)(); +}); + +/***/ }), + +/***/ "./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")); +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 + }); +}; +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")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +const registerQueries = () => { + _Injector.default.query.register('readLinkTypes', _readLinkTypes.default); +}; +var _default = registerQueries; +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 _LinkModal = _interopRequireDefault(__webpack_require__(/*! components/LinkModal/LinkModal */ "./client/src/components/LinkModal/LinkModal.js")); +var _LinkPicker = _interopRequireDefault(__webpack_require__(/*! components/LinkPicker/LinkPicker */ "./client/src/components/LinkPicker/LinkPicker.js")); +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; } +const LinkField = _ref => { + let { + value, + onChange, + types, + actions + } = _ref; + const [typeKey, setTypeKey] = (0, _react.useState)(value.typeKey || ''); + const [linkID, setLinkID] = (0, _react.useState)(value.ID || 0); + const [data, setData] = (0, _react.useState)(value); + const [editing, setEditing] = (0, _react.useState)(false); + const onClear = () => { + const endpoint = `${section.form.linkForm.deleteUrl}/${linkID}`; + _Backend.default.delete(endpoint).then(() => { + actions.toasts.success('Deleted link'); + }).catch(() => { + actions.toasts.error('Failed to delete link'); + }); + setTypeKey(''); + setLinkID(0); + setData({}); + if (typeof onChange === 'function') { + onChange({}); + } + }; + const onModalSubmit = async (modalData, action, submitFn) => { + const { + SecurityID, + action_submit: actionSubmit, + ...data + } = modalData; + let id = 0; + const formSchema = await submitFn(); + let match = formSchema.id.match(/\/linkForm\/([0-9]+)/); + if (match) { + id = parseInt(match[1]); + } else { + console.log('I DID NOT MATCH'); + } + if (typeof onChange === 'function') { + data.ID = id; + onChange(data); + } + setLinkID(id); + setEditing(false); + actions.toasts.success('Saved link'); + return Promise.resolve(); + }; + const title = data.Title || data.TitleRelField || ''; + const type = types.hasOwnProperty(typeKey) ? types[typeKey] : {}; + const section = _Config.default.getSection('SilverStripe\\LinkField\\Controllers\\LinkFieldController'); + const pickerProps = { + title, + description: data.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 + }; + (0, _react.useEffect)(() => { + if (!editing && linkID) { + const endpoint = `${section.form.linkForm.dataUrl}/${linkID}`; + _Backend.default.get(endpoint).then(response => response.json()).then(responseJson => { + setData(responseJson); + }); + } + }, [editing, linkID]); + return _react.default.createElement(_react.default.Fragment, null, _react.default.createElement(_LinkPicker.default, pickerProps), _react.default.createElement(_LinkModal.default, modalProps)); +}; +const mapDispatchToProps = dispatch => { + return { + actions: { + toasts: (0, _redux.bindActionCreators)(toastsActions, dispatch) + } + }; +}; +var _default = (0, _redux.compose)((0, _Injector.injectGraphql)('readLinkTypes'), _FieldHolder.default, (0, _reactRedux.connect)(null, mapDispatchToProps))(LinkField); +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 _react = _interopRequireDefault(__webpack_require__(/*! react */ "react")); +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 = (typeKey, linkID) => { + const { + schemaUrl + } = _Config.default.getSection('SilverStripe\\LinkField\\Controllers\\LinkFieldController').form.linkForm; + const parsedURL = _url.default.parse(schemaUrl); + const parsedQs = _qs.default.parse(parsedURL.query); + parsedQs.typeKey = typeKey; + for (const prop of ['href', 'path', 'pathname']) { + parsedURL[prop] = `${parsedURL[prop]}/${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 _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ "prop-types")); +var _reactstrap = __webpack_require__(/*! reactstrap */ "reactstrap"); +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(data) { + const fieldID = $(this).data('field-id'); + $('#' + fieldID).val(JSON.stringify({ + ID: data.ID + })); + }, + getProps() { + const fieldID = $(this).data('field-id'); + const dataStr = $('#' + fieldID).val(); + const value = JSON.parse(dataStr); + return { + value, + onChange: this.handleChange.bind(this) + }; + }, + onunmatch() { + const Root = this.getRoot(); + Root.unmount(); + } + }); +}); + +/***/ }), + +/***/ "./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'] +}; +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; + +/***/ }), + +/***/ "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/boot/index.js b/client/src/boot/index.js index 4d7bb2a5..76dd253b 100644 --- a/client/src/boot/index.js +++ b/client/src/boot/index.js @@ -1,7 +1,5 @@ /* global document */ /* eslint-disable */ -import Config from 'lib/Config'; -import registerReducers from './registerReducers'; import registerComponents from './registerComponents'; import registerQueries from './registerQueries'; @@ -10,5 +8,4 @@ document.addEventListener('DOMContentLoaded', () => { registerQueries(); - registerReducers(); }); diff --git a/client/src/boot/registerComponents.js b/client/src/boot/registerComponents.js index 1fd6283b..63f42243 100644 --- a/client/src/boot/registerComponents.js +++ b/client/src/boot/registerComponents.js @@ -3,15 +3,12 @@ import Injector from 'lib/Injector'; import LinkPicker from 'components/LinkPicker/LinkPicker'; import LinkField from 'components/LinkField/LinkField'; import LinkModal from 'components/LinkModal/LinkModal'; -import FileLinkModal from 'components/LinkModal/FileLinkModal'; - const registerComponents = () => { Injector.component.registerMany({ LinkPicker, LinkField, 'LinkModal.FormBuilderModal': LinkModal, - 'LinkModal.InsertMediaModal': FileLinkModal }); }; diff --git a/client/src/boot/registerQueries.js b/client/src/boot/registerQueries.js index 1d68cfa3..229ae7d9 100644 --- a/client/src/boot/registerQueries.js +++ b/client/src/boot/registerQueries.js @@ -1,10 +1,8 @@ /* eslint-disable */ import Injector from 'lib/Injector'; import readLinkTypes from 'state/linkTypes/readLinkTypes'; -import readLinkDescription from 'state/linkDescription/readLinkDescription'; const registerQueries = () => { Injector.query.register('readLinkTypes', readLinkTypes); - Injector.query.register('readLinkDescription', readLinkDescription); }; export default registerQueries; diff --git a/client/src/boot/registerReducers.js b/client/src/boot/registerReducers.js deleted file mode 100644 index 1abd586a..00000000 --- a/client/src/boot/registerReducers.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable */ -import Injector from 'lib/Injector'; -import { combineReducers } from 'redux'; -// import gallery from 'state/gallery/GalleryReducer'; -// import queuedFiles from 'state/queuedFiles/QueuedFilesReducer'; -// import uploadField from 'state/uploadField/UploadFieldReducer'; -// import previewField from 'state/previewField/PreviewFieldReducer'; -// import imageLoad from 'state/imageLoad/ImageLoadReducer'; -// import displaySearch from 'state/displaySearch/DisplaySearchReducer'; -// import confirmDeletion from 'state/confirmDeletion/ConfirmDeletionReducer'; -// import modal from 'state/modal/ModalReducer'; - -const registerReducers = () => { - // Injector.reducer.register('assetAdmin', combineReducers({ - // gallery, - // queuedFiles, - // uploadField, - // previewField, - // imageLoad, - // displaySearch, - // confirmDeletion, - // modal - // })); -}; - -export default registerReducers; diff --git a/client/src/components/LinkField/LinkField.js b/client/src/components/LinkField/LinkField.js index 5571450f..eba48862 100644 --- a/client/src/components/LinkField/LinkField.js +++ b/client/src/components/LinkField/LinkField.js @@ -1,90 +1,147 @@ -import React, { Fragment, useState } from 'react'; -import { compose } from 'redux'; -import { inject, injectGraphql, loadComponent } from 'lib/Injector'; +import React, { useState, useEffect } from 'react'; +import { bindActionCreators, compose } from 'redux'; +import { connect } from 'react-redux'; +import { injectGraphql } from 'lib/Injector'; import fieldHolder from 'components/FieldHolder/FieldHolder'; - -const LinkField = ({ id, loading, Loading, data, LinkPicker, onChange, types, linkDescription, ...props }) => { - if (loading) { - return ; - } - +import LinkModal from 'components/LinkModal/LinkModal'; +import LinkPicker from 'components/LinkPicker/LinkPicker'; +import * as toastsActions from 'state/toasts/ToastsActions'; +import backend from 'lib/Backend'; +import Config from 'lib/Config'; + +const LinkField = ({ + // value is the initial value of the field passed from JsonField + value, + // onChange is a function passed from JsonField + onChange, + // types is injected by the GraphQL query + types, + // actions is an object of redux actions + actions +}) => { + const [typeKey, setTypeKey] = useState(value.typeKey || ''); + const [linkID, setLinkID] = useState(value.ID || 0); + const [data, setData] = useState(value); const [editing, setEditing] = useState(false); - const [newTypeKey, setNewTypeKey] = useState(''); - const onClear = (event) => { - if (typeof onChange !== 'function') { - return; + const onClear = () => { + const endpoint = `${section.form.linkForm.deleteUrl}/${linkID}`; + backend.delete(endpoint) + .then(() => { + actions.toasts.success('Deleted link'); + }) + .catch(() => { + actions.toasts.error('Failed to delete link'); + }); + + setTypeKey(''); + setLinkID(0); + setData({}); + if (typeof onChange === 'function') { + onChange({}); } - - onChange(event, { id, value: {} }); + }; + + const onModalSubmit = async (modalData, action, submitFn) => { + // extract data from modalData + const { + SecurityID, + action_submit: actionSubmit, + ...data + } = modalData; + + // get link id from formSchema + let id = 0; + const formSchema = await submitFn(); + // handle onsuccess formSchema + let match = formSchema.id.match(/\/linkForm\/([0-9]+)/); + if (match) { + id = parseInt(match[1]); + } else { + console.log('I DID NOT MATCH'); + } + // // handle onfailure formSchema e.g. validation error + // match = formSchema.id.match(/\/schema\/linkfield\/([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(data); + } + + // update component state + setLinkID(id); + setEditing(false); + + // trigger success toast + actions.toasts.success('Saved link'); + + return Promise.resolve(); }; - const { typeKey } = data; - const type = types[typeKey]; - const modalType = newTypeKey ? types[newTypeKey] : type; - - let title = data ? data.Title : ''; - - if (!title) { - title = data ? data.TitleRelField : ''; - } + const title = data.Title || data.TitleRelField || ''; + const type = types.hasOwnProperty(typeKey) ? types[typeKey] : {}; + const section = Config.getSection('SilverStripe\\LinkField\\Controllers\\LinkFieldController'); - const linkProps = { + const pickerProps = { title, - link: type ? { type, title, description: linkDescription } : undefined, - onEdit: () => { setEditing(true); }, + description: data.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, + typeTitle: type.title || '', + typeKey, editing, onSubmit: onModalSubmit, onClosed: () => { setEditing(false); }, + linkID, data }; - 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.linkForm.dataUrl}/${linkID}`; + backend.get(endpoint) + .then(response => response.json()) + .then(responseJson => { + setData(responseJson); + }); + } + }, [editing, linkID]); - return - - - ; + return <> + + + ; }; -const stringifyData = (Component) => (({ data, value, ...props }) => { - let dataValue = value || data; - if (typeof dataValue === 'string') { - dataValue = JSON.parse(dataValue); - } - return ; -}); +const mapDispatchToProps = (dispatch) => { + return { + actions: { + toasts: bindActionCreators(toastsActions, dispatch), + }, + }; +} export default compose( - inject(['LinkPicker', 'Loading']), injectGraphql('readLinkTypes'), - stringifyData, - injectGraphql('readLinkDescription'), - fieldHolder + fieldHolder, + connect(null, mapDispatchToProps) )(LinkField); diff --git a/client/src/components/LinkModal/LinkModal.js b/client/src/components/LinkModal/LinkModal.js index 7e5ce86d..382b870d 100644 --- a/client/src/components/LinkModal/LinkModal.js +++ b/client/src/components/LinkModal/LinkModal.js @@ -1,36 +1,29 @@ /* eslint-disable */ -import i18n from 'i18n'; import React from 'react'; -import PropTypes from 'prop-types'; import FormBuilderModal from 'components/FormBuilderModal/FormBuilderModal'; import url from 'url'; import qs from 'qs'; import Config from 'lib/Config'; -const leftAndMain = 'SilverStripe\\Admin\\LeftAndMain'; - -const buildSchemaUrl = (key, data) => { - - const {schemaUrl} = Config.getSection(leftAndMain).form.DynamicLink; - +const buildSchemaUrl = (typeKey, linkID) => { + const {schemaUrl} = Config.getSection('SilverStripe\\LinkField\\Controllers\\LinkFieldController').form.linkForm; const parsedURL = url.parse(schemaUrl); const parsedQs = qs.parse(parsedURL.query); - parsedQs.key = key; - if (data) { - parsedQs.data = JSON.stringify(data); + parsedQs.typeKey = typeKey; + for (const prop of ['href', 'path', 'pathname']) { + parsedURL[prop] = `${parsedURL[prop]}/${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..a3eb3c70 100644 --- a/client/src/components/LinkPicker/LinkPickerMenu.js +++ b/client/src/components/LinkPicker/LinkPickerMenu.js @@ -1,17 +1,14 @@ /* eslint-disable */ import i18n from 'i18n'; -import React, {useState, setState} from 'react'; -import { inject } from 'lib/Injector'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; -import classnames from 'classnames'; import LinkType from 'types/LinkType'; 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..3f7b4c2c 100644 --- a/client/src/entwine/JsonField.js +++ b/client/src/entwine/JsonField.js @@ -30,13 +30,12 @@ jQuery.entwine('ss', ($) => { const props = this.getProps(); const ReactField = this.getComponent(); const Root = this.getRoot(); - Root.render(); + Root.render(); }, - handleChange(event, {id, value}) { + handleChange(data) { const fieldID = $(this).data('field-id'); - $('#' + fieldID).val(JSON.stringify(value)).trigger('change'); - this.refresh(); + $('#' + fieldID).val(JSON.stringify({ID: data.ID})); }, /** @@ -47,10 +46,9 @@ jQuery.entwine('ss', ($) => { getProps() { const fieldID = $(this).data('field-id'); const dataStr = $('#' + fieldID).val(); - const value = dataStr ? JSON.parse(dataStr) : undefined; + const value = JSON.parse(dataStr); 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 deleted file mode 100644 index 2301bd26..00000000 --- a/client/src/state/linkDescription/readLinkDescription.js +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable */ -import { graphqlTemplates } from '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 } = graphqlTemplates; -const query = { - apolloConfig, - templateName: READ, - pluralName: 'LinkDescription', - pagination: false, - params: { - dataStr: 'String!' - }, - args: { - root: { - dataStr: 'dataStr' - } - }, - fields: ['description'], -}; -export default query; diff --git a/client/src/state/linkTypes/readLinkTypes.js b/client/src/state/linkTypes/readLinkTypes.js index 1bb0bb7e..932032d0 100644 --- a/client/src/state/linkTypes/readLinkTypes.js +++ b/client/src/state/linkTypes/readLinkTypes.js @@ -43,6 +43,6 @@ const query = { keys: 'keys' } }, - fields: ['key', 'title', 'handlerName'], + fields: ['key', 'title'], }; export default query; diff --git a/docs/en/linkable-migration.md b/docs/en/linkable-migration.md deleted file mode 100644 index d58fcc42..00000000 --- a/docs/en/linkable-migration.md +++ /dev/null @@ -1,247 +0,0 @@ -# Instructions - -## Preamble - -This migration process covers shifting data from the `Linkable` tables to the appropriate `LinkField` tables. - -This does not cover usages of `EmbeddedObject` (at least, not at this time). - -**Versioned:** If you have `Versioned` `Linkable`, then the expectation is that you will also `Version` `LinkField`. If -you have not `Versioned` `Linkable`, then the expectation is that you will **not** `Version` `LinkField`. - -**No support for internal links with query params (GET params):** Please be aware that Linkfield does not support -internal links with query params (`?`) out of the box, and therefor the migration task will **remove** any query -params that are present in the Linkable's `Anchor` field. - -## Install Silvesrtripe Linkfield - -Install the Silverstripe Linkfield module: - -```bash -$ composer require silverstripe/linkfield 1.x-dev -``` - -Or if you would like the (experimental) GraphQL 4 version: - -```bash -$ composer require silverstripe/linkfield 2.x-dev -``` - -Optionally, you can also remove the Linkable module (though, you might find it useful to keep around as a reference -while you are upgrading your code). - -Do this step at whatever point makes sense to you. - -```bash -$ composer remove sheadawson/silverstripe-linkable -``` - -## Replace app usages - -You should review how you are using the original `Link` model and `LinkField`, but if you don't have any customisations, -then replacing the old with the new **might** be quite simple. - -If you have used imports (`use` statements), then your first step might just be to search for `use [old];` and replace -with `use [new];` (since the class name references have not changed at all). - -Old: `Sheadawson\Linkable\Models\Link` -New: `SilverStripe\LinkField\Models\Link` - -Old: `Sheadawson\Linkable\Forms\LinkField` -New: `SilverStripe\LinkField\Form\LinkField` - -If you have extensions, new fields, etc, then your replacements might need to be a bit more considered. - -The other key (less easy to automate) thing that you'll need to update is that the old `LinkField` required you to -specify the related field with `ID` appended, whereas the new `LinkField` requires you to specify the field without -`ID` appended. EG. - -Old: `LinkField::create('MyLinkID')` -New: `LinkField::create('MyLink')` - -Search for instances of `LinkField::create` and `new LinkField`, and hopefully that should give you all of the places -where you need to update field name references. - -### Configuration - -Be sure to check how the old module classes are referenced in config `yml` files (eg: `app/_config`). Update -appropriately. - -### Populate module - -If you use the populate module, you will not be able to simply "replace" the namespace. Fixture definitions for the -new Linkfield module are quite different. There are entirely different models for different link types, whereas before -it was just a DB field to specify the type. - -See below for example before/after usage: - -#### Before - -```yml -Sheadawson\Linkable\Models\Link: - internal: - Title: Internal link - Type: SiteTree - SiteTreeID: 1 - external: - Title: External link - Type: URL - URL: https://example.org - file: - Title: File link - Type: File - File: =>SilverStripe\Assets\File.example - phone: - Title: Phone link - Type: Phone - Phone: +64 1 234 567 - email: - Title: Email link - Type: Email - Email: foo@example.org -``` - -#### After - -```yml -SilverStripe\LinkField\Models\SiteTreeLink: - internal: - Title: Internal link - Page: =>Page.home -SilverStripe\LinkField\Models\ExternalLink: - external: - Title: External link - ExternalUrl: https://example.org -SilverStripe\LinkField\Models\FileLink: - file: - Title: File link - File: =>SilverStripe\Assets\File.example -SilverStripe\LinkField\Models\PhoneLink: - phone: - Title: Phone link - Phone: +64 1 234 567 -SilverStripe\LinkField\Models\EmailLink: - email: - Title: Email link - Email: foo@example.org -``` - -## Replace template usages - -Before: You might have had references to `$LinkURL` or `$Link.LinkURL`. -After: These would need to be updated to `$URL` or `$Link.URL` respectively. - -Before: `$OpenInNewWindow` or `$Link.OpenInNewWindow`. -After: `$OpenInNew` or `$Link.OpenInNew` respectively. - -Before: `$Link.TargetAttr` or `$TargetAttr` would output the appropriate `target="xx"`. -After: There is no direct replacement. - -This is an area where you should spend some decent effort to make sure each implementation is outputting as you expect -it to. There may be more "handy" methods that Linkable provided that no longer exist (that we haven't covered above). - -## Table structures - -It's important to understand that we are going from a single table in Linkable to multiple tables in LinkField. - -**Before:** We had 1 table with all data, and one of the field in there specified the type of the Link. -**Now:** We have 1 table for each type of Link, with a base `Link` table for all record. - -## Specify any custom configuration - -Have a look at `LinkableMigrationTask`. There are some configuration properties defined in there: - -- `$link_mapping` -- `$email_mapping` -- `$external_mapping` -- `$file_mapping` -- `$phone_mapping` -- `$sitetree_mapping` - -Each of these specifies how an original field from the `LinkableLink` table will map to one of the new LinkField tables. - -If you previously had some custom fields that needed to be available across **all** Link types, then you're (probably) -going to add this as an extension on the (base) `Link` class. This is going to mean that the new fields will be added -to the `LinkField_Link` table. This means that you need to update the configuration for `$link_mapping` so that we -correctly migrate those field values into the `LinkField_Link` table. - -If you had/have a field that you only want displayed on (say) SiteTree links, then you would want to add that extension -to `SiteTreeLink`. This would create new fields in the `LinkField_SiteTreeLink` table, which will mean you need to -also update the config for `$sitetree_mapping`. - -It's important that you get the correct mappings to the correct tables. - -### Linkable `has_one` to one of your other DataObjects - -An example for the above [Specify any custom configuration](#specify-any-custom-configuration) would be that if one -of your DataObjects `has_many` `Link`. This would require there to be a `has_one` on `Link` back to your DataObject. - -Let's say that our `Page` `has_many` `Link`: -```php -class Page extends SiteTree -{ - private static array $has_many = [ - 'Links' => Link::class, - ]; -} -``` - -This would require a corresponding `has_one` on `Link`: -```yaml -Sheadawson\Linkable\Models\Link: - has_one: - ParentPage: Page -``` - -If we inspect the `LinkableLink` table, we'll see that there is a field called `ParentPageID`. We need to tell the -migration task about this field, and where it needs to migrate to. - -Assuming you keep the same relationship name, you'll want to add the following `$link_mapping` configuration: -```yaml -SilverStripe\LinkField\Tasks\LinkableMigrationTask: - link_mapping: - ParentPageID: ParentPageID -``` - -## Adding support for internal links with query params - -No official support is provided, but you can achieve this through adding your own extensions. - -Add a new field to `SiteTreeLink` to store your query params, EG: - -```php -class SiteTreeLinkExtension extends DataExtension -{ - private static array $db = [ - 'QueryParams' => 'Varchar', - ]; -} -``` - -An extension point called `updateGetURLBeforeAnchor` is available: - -```php -class SiteTreeLinkExtension extends DataExtension -{ - ... - - public function updateGetURLBeforeAnchor(&$url): void - { - // Assumes that you save your QueryParams within prepending the ?, so we append it here - $url .= sprintf('?%s', $this->owner->QueryParams); - } -} -``` - -If you also plan to use the `LinkableMigrationTask`, then there is a configuration that you can enable to tell us where -you would like the query params from the old `AnchorLink` to be migrated. - -Please note: The migration task assumes that you will store your query params without prepending the `?` (following -the same paradigm as our `Anchor` field). - -EG: - -```yaml -SilverStripe\LinkField\Tasks\LinkableMigrationTask: - sitetree_query_params_to: QueryParams -``` diff --git a/src/Controllers/LinkFieldController.php b/src/Controllers/LinkFieldController.php new file mode 100644 index 00000000..c34c3494 --- /dev/null +++ b/src/Controllers/LinkFieldController.php @@ -0,0 +1,298 @@ + 'linkForm', + 'DELETE delete/$ItemID' => 'linkDelete', + 'GET data/$ItemID' => 'linkData', + ]; + + private static $allowed_actions = [ + 'linkForm', + 'linkDelete', + 'linkData', + ]; + + /** + * + */ + public function init() + { + parent::init(); + // TODO have temporarily disabled for easier dev + SecurityToken::disable(); + // Check security token + $request = $this->getRequest(); + if ($request->isAjax() && !SecurityToken::inst()->checkRequest($request)) { + $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'); + } + } + + public function getClientConfig() + { + $clientConfig = parent::getClientConfig(); + $clientConfig['form']['linkForm'] = [ + // schemaUrl will get the $ItemID and ?typeKey dynamically suffixed in LinkModal.js + 'schemaUrl' => $this->Link('schema/linkForm'), + 'deleteUrl' => $this->Link('delete'), + 'dataUrl' => $this->Link('data'), + 'saveMethod' => 'post', + 'formNameTemplate' => sprintf(self::FORM_NAME_TEMPLATE, '{id}'), + ]; + return $clientConfig; + } + + public function linkForm(): 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 { + $typeKey = $this->typeKeyFromRequest(); + $link = (new Registry)->byKey($typeKey); + if (!$link) { + $this->jsonError(400, 'Invalid key'); + } + } + return $this->createLinkForm($link); + } + + /** + * This method is called from LeftAndMain::schema() with the url + * /admin/linkfield/schema/linkForm/ + */ + public function getLinkForm(): Form + { + return $this->linkForm(); + } + + private function createLinkForm(Link $link): Form + { + $id = $link->ID; + + // Create the form + $formFactory = Injector::inst()->get(DefaultFormFactory::class); + $name = sprintf(self::FORM_NAME_TEMPLATE, $id); + /** @var Form $form */ + $form = $formFactory->getForm($this, $name, ['Record' => $link]); + + // Set where the form is submitted to + // Ideally would use getClientConfig() 'saveUrl' instaed, not sure why that doesn't work + $urlSegment = $this->config()->get('url_segment'); + $typeKey = Registry::create()->keyByClassName($link->ClassName); + $form->setFormAction("admin/$urlSegment/linkForm/$id?typeKey=$typeKey"); + + // Add save action button + $title = $id ? 'Update link' : 'Create link'; // todo: _t() + $actions = FieldList::create([ + FormAction::create('save', $title) + ->setSchemaData(['data' => ['buttonStyle' => 'primary']]), + ]); + $form->setActions($actions); + + // Set the form request handler to return a FormSchema response + // This will override 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); + }); + + // Make readonly if cannot edit + if (!$link->canEdit()) { + $form->makeReadonly(); + } + + // Styling + $form->addExtraClass('form--no-dividers'); + + return $form; + } + + /** + * We arrive here from FormRequestHandler::httpSubmission() + * The 'save' method is called because it is the 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 { + // Creating a new Link + $typeKey = $this->typeKeyFromRequest(); + $className = Registry::create()->list()[$typeKey] ?? ''; + 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'); + } + + // Update DataObject from form 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); + + // Create and send FormSchema JSON response + $schemaID = $form->FormAction(); + $response = $this->getSchemaResponse($schemaID, $form, $validationResult); + + // return a 200 for now just to get things to work even though it's + // clearly the wrong code if !$validationResult->isValid(). + // 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 + // Will require a PR to admin to fix this + $response->setStatusCode(200); + return $response; + } + + /** + * 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 linkDelete(): HTTPResponse + { + $link = $this->linkFromRequest(); + 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 linkData(): HTTPResponse + { + $link = $this->linkFromRequest(); + 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->getDescription(); + $response->setBody(json_encode($data)); + return $response; + } + + private function linkFromRequest(): Link + { + $itemID = (int) $this->itemIDFromRequest(); + if (!$itemID) { + $this->jsonError(400, 'Missing ID'); + } + $link = Link::get()->byID($itemID); + if (!$link) { + $this->jsonError(400, 'Invalid ID'); + } + return $link; + } + + private function itemIDFromRequest(): string + { + $request = $this->getRequest(); + $itemID = (string) $request->param('ItemID'); + if (!ctype_digit($itemID)) { + $this->jsonError(400, "Invalid itemID param $itemID"); + } + return $itemID; + } + + private function typeKeyFromRequest(): string + { + $request = $this->getRequest(); + $typeKey = (string) $request->getVar('typeKey'); + if (strlen($typeKey) === 0 || !preg_match('#^[a-z\-]+$#', $typeKey)) { + $this->jsonError(400, "Invalid typeKey $typeKey"); + } + return $typeKey; + } +} 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/FormFactoryExtension.php b/src/Extensions/FormFactoryExtension.php deleted file mode 100644 index 1491f924..00000000 --- a/src/Extensions/FormFactoryExtension.php +++ /dev/null @@ -1,55 +0,0 @@ -exists()) { - // This is a new link, so we don't need to to any further customisation - return; - } - - /** @var FormAction $insertAction */ - $insertAction = $actions->fieldByName('action_insert'); - - if (!$insertAction) { - // We couldn't find the insert action - return; - } - - // Update the title of the action to reflect the link model data state - $insertActionTitle = _t('Admin.EDIT_LINK', 'Edit link'); - $insertAction->setTitle($insertActionTitle); - } -} 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..80b684b9 --- /dev/null +++ b/src/Extensions/LeftAndMainExtension.php @@ -0,0 +1,17 @@ + 'editorAnchorLink', // Matches LeftAndMain::methodSchema args - ]; - - private static array $allowed_actions = [ - 'DynamicLink', - ]; - - /** - * Builds and returns the external link form - * - * @return Form - */ - public function DynamicLink() - { - // Show link text field if requested - $linkDataJsonStr = $this->getOwner()->controller->getRequest()->getVar('data'); - - /** @var OwnerController $owner */ - $owner = $this->getOwner(); - - $factory = FormFactory::singleton(); - - $data = $this->getData(); - - return $factory->getForm( - $owner->getController(), - "{$owner->getName()}/DynamicLink", - $this->getContext() - )->loadDataFrom($data); - } - - /** - * Build the context to pass to the Form Link Factory - * @return array - * @throws HTTPResponse_Exception - */ - private function getContext(): array - { - $linkTypeKey = $this->getOwner()->controller->getRequest()->getVar('key'); - - if (!$linkTypeKey) { - throw new HTTPResponse_Exception(sprintf('key for class "%s" is required', static::class), 400); - } - - $type = Registry::singleton()->byKey($linkTypeKey); - - if (!$type) { - throw new HTTPResponse_Exception(sprintf('%s is not a valid link type', $type), 400); - } - - $data = $this->getData(); - - // Hydrate current model in case data is available, so more options are available for CMS fields customsation - // This allows model-level form customisation - if ($data && array_key_exists('ID', $data) && $data['ID']) { - /** @var Link $type */ - $type = Injector::inst()->create($type->ClassName, $data, DataObject::CREATE_HYDRATED); - } - - return [ - 'LinkData' => $data, - 'LinkType' => $type, - 'LinkTypeKey' => $linkTypeKey, - // TODO this is likely a legacy field, use form validator instead - 'RequireLinkText' => false - ]; - } - - /** - * Extract the Link Data out of the Request. - * @return array - */ - private function getData(): array - { - $data = []; - $dataString = $this->getOwner()->controller->getRequest()->getVar('data'); - - if ($dataString) { - $parsedData = json_decode($dataString, true); - - if (json_last_error() === JSON_ERROR_NONE) { - $data = $parsedData; - } else { - throw new InvalidArgumentException(json_last_error_msg()); - } - } - - return $data; - } -} diff --git a/src/Form/FormFactory.php b/src/Form/FormFactory.php deleted file mode 100644 index e9f57f5c..00000000 --- a/src/Form/FormFactory.php +++ /dev/null @@ -1,47 +0,0 @@ -scaffoldLinkFields($linkData); - $fields->push(HiddenField::create('typeKey')->setValue($context['LinkTypeKey'])); - $this->extend('updateFormFields', $fields, $controller, $name, $context); - - return $fields; - } - - protected function getValidator($controller, $name, $context) - { - if (!array_key_exists('LinkType', $context)) { - return null; - } - - /** @var DataObject|Type $type */ - $type = $context['LinkType']; - - return $type->getCMSCompositeValidator(); - } -} diff --git a/src/Form/JsonField.php b/src/Form/JsonField.php index 4f7ad0b6..e2f78824 100644 --- a/src/Form/JsonField.php +++ b/src/Form/JsonField.php @@ -28,7 +28,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 +40,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/Form/LinkField.php b/src/Form/LinkField.php index bdf7e9de..7f5c1a1e 100644 --- a/src/Form/LinkField.php +++ b/src/Form/LinkField.php @@ -8,9 +8,4 @@ class LinkField extends JsonField { protected $schemaComponent = 'LinkField'; - - public function setValue($value, $data = null) - { - return parent::setValue($value, $data); - } } diff --git a/src/GraphQL/LinkDescriptionResolver.php b/src/GraphQL/LinkDescriptionResolver.php deleted file mode 100644 index 2259f25d..00000000 --- a/src/GraphQL/LinkDescriptionResolver.php +++ /dev/null @@ -1,34 +0,0 @@ - '']; - } - - $type = Registry::singleton()->byKey($typeKey); - - if (!$type) { - return ['description' => '']; - } - - return ['description' => $type->generateLinkDescription($data)]; - } -} diff --git a/src/GraphQL/LinkTypeResolver.php b/src/GraphQL/LinkTypeResolver.php index a61f28f1..7f642607 100644 --- a/src/GraphQL/LinkTypeResolver.php +++ b/src/GraphQL/LinkTypeResolver.php @@ -6,7 +6,7 @@ use InvalidArgumentException; use SilverStripe\GraphQL\Schema\DataObject\Resolver; use SilverStripe\LinkField\Type\Registry; -use SilverStripe\LinkField\Type\Type; +use SilverStripe\LinkField\Models\Link; class LinkTypeResolver extends Resolver { @@ -17,10 +17,9 @@ public static function resolve($obj, $args = [], $context = [], ?ResolveInfo $in } $types = Registry::singleton()->list(); - $flattenType = array_map(function (Type $type, string $key) { + $flattenType = array_map(function (Link $type, string $key) { return [ 'key' => $key, - 'handlerName' => $type->LinkTypeHandlerName(), 'title' => $type->LinkTypeTile() ]; }, $types, array_keys($types)); diff --git a/src/Models/EmailLink.php b/src/Models/EmailLink.php index 410455db..c41143ee 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 getDescription(): string { - return isset($data['Email']) ? $data['Email'] : ''; + return $this->Email ?: ''; } public function getCMSFields(): FieldList @@ -28,7 +28,6 @@ public function getCMSFields(): FieldList $this->beforeUpdateCMSFields(static function (FieldList $fields) { $fields->replaceField('Email', EmailField::create('Email')); }); - return parent::getCMSFields(); } diff --git a/src/Models/ExternalLink.php b/src/Models/ExternalLink.php index c51fb50c..732d620e 100644 --- a/src/Models/ExternalLink.php +++ b/src/Models/ExternalLink.php @@ -15,13 +15,13 @@ class ExternalLink extends Link 'ExternalUrl' => 'Varchar', ]; - public function generateLinkDescription(array $data): string + public function getDescription(): string { - return isset($data['ExternalUrl']) ? $data['ExternalUrl'] : ''; + return $this->ExternalUrl ?: ''; } public function getURL(): string { - return $this->ExternalUrl ?? ''; + return $this->ExternalUrl ?: ''; } } diff --git a/src/Models/FileLink.php b/src/Models/FileLink.php index 0d40b933..21fbd006 100644 --- a/src/Models/FileLink.php +++ b/src/Models/FileLink.php @@ -4,12 +4,6 @@ use SilverStripe\Assets\File; -/** - * A link to a File track in asset-admin - * - * @property int $FileID - * @method File File() - */ class FileLink extends Link { private static string $table_name = 'LinkField_FileLink'; @@ -18,28 +12,14 @@ class FileLink extends Link 'File' => File::class, ]; - public function generateLinkDescription(array $data): string + public function getDescription(): string { - $fileId = $data['FileID'] ?? null; - - if (!$fileId) { - return ''; - } - - $file = File::get()->byID($fileId); - - return $file?->getFilename() ?? ''; - } - - public function LinkTypeHandlerName(): string - { - return 'InsertMediaModal'; + return $this->File()?->getFilename() ?? ''; } public function getURL(): string { $file = $this->File(); - return $file->exists() ? (string) $file->getURL() : ''; } } diff --git a/src/Models/Link.php b/src/Models/Link.php index 87d2de0f..7c973e58 100644 --- a/src/Models/Link.php +++ b/src/Models/Link.php @@ -24,7 +24,7 @@ * @property string $Title * @property bool $OpenInNew */ -class Link extends DataObject implements JsonData, Type +class Link extends DataObject implements JsonData { private static $table_name = 'LinkField_Link'; @@ -40,19 +40,7 @@ class Link extends DataObject implements JsonData, Type */ private ?string $linkType = null; - public function defineLinkTypeRequirements() - { - Requirements::add_i18n_javascript('silverstripe/linkfield:client/lang', false, true); - Requirements::javascript('silverstripe/linkfield:client/dist/js/bundle.js'); - Requirements::css('silverstripe/linkfield:client/dist/styles/bundle.css'); - } - - public function LinkTypeHandlerName(): string - { - return 'FormBuilderModal'; - } - - public function generateLinkDescription(array $data): string + public function getDescription(): string { return ''; } diff --git a/src/Models/PhoneLink.php b/src/Models/PhoneLink.php index 855e3b1c..c4d70fd6 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 getDescription(): 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..5f8b70d2 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 getDescription(): 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/Registry.php b/src/Type/Registry.php index 152138ea..50d96f94 100644 --- a/src/Type/Registry.php +++ b/src/Type/Registry.php @@ -8,6 +8,7 @@ use SilverStripe\Core\Extensible; use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injector; +use SilverStripe\LinkField\Models\Link; /** * Manage the list of known link types @@ -23,10 +24,10 @@ class Registry /** * Find the matching LinkType by its key or null it can't be found. * @param string $key - * @return Type|null + * @return Link|null * @throws InvalidArgumentException */ - public function byKey(string $key): ?Type + public function byKey(string $key): ?Link { /** @var array $types */ $typeDefinitions = self::config()->get('types'); @@ -40,12 +41,12 @@ public function byKey(string $key): ?Type } /** - * @return Type[] + * @return Link[] * @throws InvalidArgumentException */ public function list(): array { - /** @var Type[] $types */ + /** @var Link[] $types */ $types = []; /** @var array $types */ @@ -90,7 +91,7 @@ public function init() * @param array $def * @throws LogicException */ - private function definitionToType(array $def): Type + private function definitionToType(array $def): Link { $className = $def['classname'] ?? null; @@ -98,10 +99,10 @@ private function definitionToType(array $def): Type throw new LogicException(sprintf('%s: All types should reference a valid classname', static::class)); } - /** @var Type $type */ + /** @var Link $type */ $type = Injector::inst()->get($className); - if (!$type instanceof Type) { + if (!$type instanceof Link) { throw new LogicException(sprintf('%s: %s is not a valid link type', static::class, $className)); } diff --git a/src/Type/Type.php b/src/Type/Type.php deleted file mode 100644 index 1d0cb428..00000000 --- a/src/Type/Type.php +++ /dev/null @@ -1,46 +0,0 @@ -objFromFixture(Image::class, 'image-1'); - $image->setFromLocalFile(Director::baseFolder() . '/tests/resources/600x400.png'); + $image->setFromLocalFile(dirname(dirname(dirname(__FILE__))) . '/resources/600x400.png'); $image->write(); $image->publishSingle(); @@ -53,13 +52,6 @@ protected function tearDown(): void parent::tearDown(); } - public function testLinkModel(): void - { - $model = $this->objFromFixture(Link::class, 'link-1'); - - $this->assertEquals('FormBuilderModal', $model->LinkTypeHandlerName()); - } - /** * @throws ValidationException */ @@ -130,8 +122,8 @@ public function testLinkTypeEnabled(array $types, array $expected): void $config->set(Registry::class, 'types', $types); $enabledTypes = Registry::singleton()->list(); - $enabledTypes = array_map(static function (Type $type): string { - return $type->LinkTypeTile(); + $enabledTypes = array_map(static function (Link $link): string { + return $link->LinkTypeTile(); }, $enabledTypes); $enabledTypes = array_values($enabledTypes); sort($enabledTypes, SORT_STRING);