From b5e6dbe91b4250286594c4f6abb0ec0f9e8103ae Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Thu, 2 Nov 2023 20:40:16 +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 | 2 +- client/src/boot/index.js | 5 - client/src/boot/registerComponents.js | 7 - client/src/boot/registerQueries.js | 2 - client/src/boot/registerReducers.js | 26 - client/src/components/LinkField/LinkField.js | 165 ++++-- .../src/components/LinkModal/FileLinkModal.js | 10 + client/src/components/LinkModal/LinkModal.js | 38 +- .../src/components/LinkPicker/LinkPicker.js | 29 +- .../components/LinkPicker/LinkPickerMenu.js | 5 +- .../components/LinkPicker/LinkPickerTitle.js | 13 +- client/src/entwine/JsonField.js | 13 +- .../linkDescription/readLinkDescription.js | 43 -- client/src/state/linkTypes/readLinkTypes.js | 2 +- src/Controllers/LinkFieldController.php | 302 ++++++++++ src/Extensions/AjaxField.php | 33 -- src/Extensions/FormFactoryExtension.php | 55 -- src/Extensions/LeftAndMain.php | 25 - src/Extensions/LeftAndMainExtension.php | 16 + src/Extensions/ModalController.php | 114 ---- src/Form/FormFactory.php | 47 -- src/Form/JsonField.php | 73 +-- 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 | 23 +- src/Models/PhoneLink.php | 6 +- src/Models/SiteTreeLink.php | 17 +- src/ORM/DBJson.php | 76 --- src/ORM/DBLink.php | 36 -- src/Type/Registry.php | 15 +- src/Type/Type.php | 46 -- .../Controllers/LinkFieldControllerTest.php | 532 ++++++++++++++++++ .../Controllers/LinkFieldControllerTest.yml | 4 + .../LinkFieldControllerTest/TestPhoneLink.php | 71 +++ tests/php/Models/LinkTest.php | 14 +- 44 files changed, 1134 insertions(+), 840 deletions(-) delete mode 100644 client/src/boot/registerReducers.js delete mode 100644 client/src/state/linkDescription/readLinkDescription.js 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/ORM/DBJson.php delete mode 100644 src/ORM/DBLink.php delete mode 100644 src/Type/Type.php create mode 100644 tests/php/Controllers/LinkFieldControllerTest.php create mode 100644 tests/php/Controllers/LinkFieldControllerTest.yml create mode 100644 tests/php/Controllers/LinkFieldControllerTest/TestPhoneLink.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..1c6239c0 100644 --- a/client/dist/js/bundle.js +++ b/client/dist/js/bundle.js @@ -1 +1 @@ -!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(){"use strict";var e={274:function(e,t,n){var r=i(n(521)),o=i(n(154));function i(e){return e&&e.__esModule?e:{default:e}}document.addEventListener("DOMContentLoaded",(()=>{(0,r.default)(),(0,o.default)()}))},521:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=i(n(648)),o=i(n(852));function i(e){return e&&e.__esModule?e:{default:e}}var l=()=>{r.default.component.registerMany({LinkField:o.default})};t.default=l},154:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=i(n(648)),o=i(n(689));function i(e){return e&&e.__esModule?e:{default:e}}var l=()=>{r.default.query.register("readLinkTypes",o.default)};t.default=l},852:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=k(n(363)),o=n(827),i=n(624),l=n(648),a=y(n(42)),u=y(n(117)),s=y(n(809)),d=k(n(123)),f=y(n(159)),c=y(n(510)),p=y(n(86));function y(e){return e&&e.__esModule?e:{default:e}}function m(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(m=function(e){return e?n:t})(e)}function k(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=m(t);if(n&&n.has(e))return n.get(e);var r={},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var i in e)if("default"!==i&&Object.prototype.hasOwnProperty.call(e,i)){var l=o?Object.getOwnPropertyDescriptor(e,i):null;l&&(l.get||l.set)?Object.defineProperty(r,i,l):r[i]=e[i]}return r.default=e,n&&n.set(e,r),r}const v="SilverStripe\\LinkField\\Controllers\\LinkFieldController",_=e=>{let{linkID:t,onChange:n,types:o,actions:i}=e;const[l,a]=(0,r.useState)(""),[d,p]=(0,r.useState)({}),[y,m]=(0,r.useState)(!1),k=d.Title||"",_=o.hasOwnProperty(l)?o[l]:{},g={title:k,description:d.description,typeTitle:_.title||"",onEdit:()=>{m(!0)},onClear:()=>{const e=`${c.default.getSection(v).form.linkForm.deleteUrl}/${t}`;f.default.delete(e,{},{"X-SecurityID":c.default.get("SecurityID")}).then((()=>{i.toasts.success("Deleted link")})).catch((()=>{i.toasts.error("Failed to delete link")})),a(""),p({}),n(0)},onSelect:e=>{a(e),m(!0)},types:Object.values(o)},h={typeTitle:_.title||"",typeKey:l,editing:y,onSubmit:async(e,t,r)=>{const o=await r();if(!o.id.match(/\/schema\/linkfield\/([0-9]+)/)){const e=o.id.match(/\/linkForm\/([0-9]+)/),t=parseInt(e[1],10);m(!1),n(t),i.toasts.success("Saved link")}return Promise.resolve()},onClosed:()=>{m(!1)},linkID:t,data:d};return(0,r.useEffect)((()=>{if(!y&&t){const e=`${c.default.getSection(v).form.linkForm.dataUrl}/${t}`;f.default.get(e).then((e=>e.json())).then((e=>{p(e),a(e.typeKey)}))}}),[y,t]),r.default.createElement(r.default.Fragment,null,r.default.createElement(s.default,g),r.default.createElement(u.default,h))};_.propTypes={linkID:p.default.number.isRequired,onChange:p.default.func.isRequired};var g=(0,o.compose)((0,l.injectGraphql)("readLinkTypes"),a.default,(0,i.connect)(null,(e=>({actions:{toasts:(0,o.bindActionCreators)(d,e)}}))))(_);t.default=g},117:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=s(n(363)),o=s(n(912)),i=s(n(872)),l=s(n(902)),a=s(n(510)),u=s(n(86));function s(e){return e&&e.__esModule?e:{default:e}}const d=(e,t)=>{const{schemaUrl:n}=a.default.getSection("SilverStripe\\LinkField\\Controllers\\LinkFieldController").form.linkForm,r=i.default.parse(n),o=l.default.parse(r.query);o.typeKey=e;for(const e of["href","path","pathname"])r[e]=`${r[e]}/${t}`;return i.default.format({...r,search:l.default.stringify(o)})},f=e=>{let{typeTitle:t,typeKey:n,linkID:i,data:l,editing:a,onSubmit:u,onClosed:s}=e;return!!n&&r.default.createElement(o.default,{title:t,isOpen:a,schemaUrl:d(n,i),identifier:"Link.EditingLinkInfo",onSubmit:u})};f.propTypes={typeTitle:u.default.string.isRequired,typeKey:u.default.string.isRequired,linkID:u.default.number.isRequired,data:u.default.object.isRequired,editing:u.default.bool.isRequired,onSubmit:u.default.func.isRequired,onClosed:u.default.func.isRequired};var c=f;t.default=c},809:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=u(n(363)),o=u(n(86)),i=u(n(820)),l=u(n(97)),a=u(n(734));function u(e){return e&&e.__esModule?e:{default:e}}const s=e=>{let{title:t,description:n,typeTitle:o,types:u,onSelect:s,onEdit:d,onClear:f}=e;return r.default.createElement("div",{className:(0,i.default)("link-picker","form-control",{"link-picker--selected":!!o})},!o&&r.default.createElement(l.default,{types:u,onSelect:s}),o&&r.default.createElement(a.default,{title:t,description:n,typeTitle:o,onClear:f,onClick:()=>d()}))};t.Component=s,s.propTypes={...l.default.propTypes,title:o.default.string,description:o.default.string,typeTitle:o.default.string.isRequired,onEdit:o.default.func.isRequired,onClear:o.default.func.isRequired,onSelect:o.default.func.isRequired};var d=s;t.default=d},97:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=s(n(754)),o=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={},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var i in e)if("default"!==i&&Object.prototype.hasOwnProperty.call(e,i)){var l=o?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)),i=s(n(86)),l=n(127),a=s(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 s(e){return e&&e.__esModule?e:{default:e}}const d=e=>{let{types:t,onSelect:n}=e;const[i,a]=(0,o.useState)(!1);return o.default.createElement(l.Dropdown,{isOpen:i,toggle:()=>a((e=>!e)),className:"link-picker__menu"},o.default.createElement(l.DropdownToggle,{className:"link-picker__menu-toggle font-icon-link",caret:!0},r.default._t("Link.ADD_LINK","Add Link")),o.default.createElement(l.DropdownMenu,null,t.map((e=>{let{key:t,title:r}=e;return o.default.createElement(l.DropdownItem,{key:t,onClick:()=>n(t)},r)}))))};d.propTypes={types:i.default.arrayOf(a.default).isRequired,onSelect:i.default.func.isRequired};var f=d;t.default=f},734:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=a(n(754)),o=a(n(363)),i=a(n(86)),l=n(127);function a(e){return e&&e.__esModule?e:{default:e}}const u=e=>t=>{t.nativeEvent.stopImmediatePropagation(),t.preventDefault(),t.nativeEvent.preventDefault(),t.stopPropagation(),e&&e()},s=e=>{let{title:t,description:n,typeTitle:i,onClear:a,onClick:s}=e;return o.default.createElement("div",{className:"link-picker__link"},o.default.createElement(l.Button,{className:"link-picker__button font-icon-link",color:"secondary",onClick:u(s)},o.default.createElement("div",{className:"link-picker__link-detail"},o.default.createElement("div",{className:"link-picker__title"},t),o.default.createElement("small",{className:"link-picker__type"},i,": ",o.default.createElement("span",{className:"link-picker__url"},n)))),o.default.createElement(l.Button,{className:"link-picker__clear",color:"link",onClick:u(a)},r.default._t("Link.CLEAR","Clear")))};s.propTypes={title:i.default.string,description:i.default.string,typeTitle:i.default.string.isRequired,onClear:i.default.func.isRequired,onClick:i.default.func.isRequired};var d=s;t.default=d},115:function(e,t,n){var r=a(n(311)),o=a(n(363)),i=a(n(691)),l=n(648);function a(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,l.loadComponent)(n,t);this.setComponent(r),this.setRoot(i.default.createRoot(this[0])),this._super(),this.refresh()},refresh(){const e=this.getProps(),t=this.getComponent();this.getRoot().render(o.default.createElement(t,u({},e,{noHolder:!0})))},handleChange(t){const n=e(this).data("field-id");e("#"+n).val(t),this.refresh()},getProps(){const t=e(this).data("field-id");return{linkID:Number(e("#"+t).val()),onChange:this.handleChange.bind(this)}},onunmatch(){this.getRoot().unmount()}})}))},689:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=n(648);const o={props(e){const{data:{error:t,readLinkTypes:n,loading:r}}=e,o=t&&t.graphQLErrors&&t.graphQLErrors.map((e=>e.message));return{loading:r,types:n?n.reduce(((e,t)=>({...e,[t.key]:t})),{}):{},graphQLErrors:o}}},{READ:i}=r.graphqlTemplates;var l={apolloConfig:o,templateName:i,pluralName:"LinkTypes",pagination:!1,params:{keys:"[ID]"},args:{root:{keys:"keys"}},fields:["key","title"]};t.default=l},686:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,o=(r=n(86))&&r.__esModule?r:{default:r};var i=o.default.shape({key:o.default.string.isRequired,title:o.default.string.isRequired});t.default=i},159:function(e){e.exports=Backend},510:function(e){e.exports=Config},42:function(e){e.exports=FieldHolder},912:function(e){e.exports=FormBuilderModal},648:function(e){e.exports=Injector},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},123:function(e){e.exports=ToastsActions},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 o=t[r];if(void 0!==o)return o.exports;var i=t[r]={exports:{}};return e[r](i,i.exports,n),i.exports}n(274),n(115)}(); \ No newline at end of file diff --git a/client/src/boot/index.js b/client/src/boot/index.js index 4d7bb2a5..7fac11d0 100644 --- a/client/src/boot/index.js +++ b/client/src/boot/index.js @@ -1,14 +1,9 @@ /* global document */ /* eslint-disable */ -import Config from 'lib/Config'; -import registerReducers from './registerReducers'; import registerComponents from './registerComponents'; import registerQueries from './registerQueries'; document.addEventListener('DOMContentLoaded', () => { registerComponents(); - registerQueries(); - - registerReducers(); }); diff --git a/client/src/boot/registerComponents.js b/client/src/boot/registerComponents.js index 1fd6283b..e933341d 100644 --- a/client/src/boot/registerComponents.js +++ b/client/src/boot/registerComponents.js @@ -1,17 +1,10 @@ /* eslint-disable */ 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..b8468ca0 100644 --- a/client/src/components/LinkField/LinkField.js +++ b/client/src/components/LinkField/LinkField.js @@ -1,90 +1,145 @@ -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'; +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'; +import PropTypes from 'prop-types'; + +// section used in window.ss config +const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController'; + +/** + * linkID - ID of the Link passed from JsonField + * onChange - callback function passed from JsonField - used to update the linkID + * types - injected by the GraphQL query + * actions - object of redux actions + */ +const LinkField = ({ linkID, onChange, types, actions }) => { + const [typeKey, setTypeKey] = useState(''); + const [data, setData] = useState({}); + const [editing, setEditing] = useState(false); -const LinkField = ({ id, loading, Loading, data, LinkPicker, onChange, types, linkDescription, ...props }) => { - if (loading) { - return ; - } + /** + * Call back used by LinkModal after the form has been submitted and the response has been received + */ + const onModalSubmit = async (modalData, action, submitFn) => { + const formSchema = await submitFn(); + + // slightly annoyingly, on validation error formSchema at this point will not have an errors node + // instead it will have the original formSchema id used for the GET request to get the formSchema i.e. + // admin/linkfield/schema/linkfield/ + // instead of the one used by the POST submission i.e. + // admin/linkfield/linkForm/ + const hasValidationErrors = formSchema.id.match(/\/schema\/linkfield\/([0-9]+)/); + if (!hasValidationErrors) { + // get link id from formSchema response + const match = formSchema.id.match(/\/linkForm\/([0-9]+)/); + const id = parseInt(match[1], 10); + + // update component state + setEditing(false); - const [editing, setEditing] = useState(false); - const [newTypeKey, setNewTypeKey] = useState(''); + // update parent JsonField data id - this is required to update the underlying form field + // so that the Page (or other parent DataObject) gets the Link relation ID set + onChange(id); - const onClear = (event) => { - if (typeof onChange !== 'function') { - return; + // success toast + actions.toasts.success('Saved link'); } - onChange(event, { id, value: {} }); + return Promise.resolve(); }; - const { typeKey } = data; - const type = types[typeKey]; - const modalType = newTypeKey ? types[newTypeKey] : type; - - let title = data ? data.Title : ''; + /** + * Call back used by LinkPicker when the 'Clear' button is clicked + */ + const onClear = () => { + const endpoint = `${Config.getSection(section).form.linkForm.deleteUrl}/${linkID}`; + // CSRF token 'X-SecurityID' headers needs to be present for destructive requests + backend.delete(endpoint, {}, { 'X-SecurityID': Config.get('SecurityID') }) + .then(() => { + actions.toasts.success('Deleted link'); + }) + .catch(() => { + actions.toasts.error('Failed to delete link'); + }); + + // update component state + setTypeKey(''); + setData({}); + + // update parent JsonField data ID used to update the underlying form field + onChange(0); + }; - if (!title) { - title = data ? data.TitleRelField : ''; - } + const title = data.Title || ''; + const type = types.hasOwnProperty(typeKey) ? types[typeKey] : {}; - 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 = `${Config.getSection(section).form.linkForm.dataUrl}/${linkID}`; + backend.get(endpoint) + .then(response => response.json()) + .then(responseJson => { + setData(responseJson); + setTypeKey(responseJson.typeKey); + }); + } + }, [editing, linkID]); + + return <> + + + ; +}; - return - - - ; +LinkField.propTypes = { + linkID: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, }; -const stringifyData = (Component) => (({ data, value, ...props }) => { - let dataValue = value || data; - if (typeof dataValue === 'string') { - dataValue = JSON.parse(dataValue); - } - return ; +// redux actions loaded into props - used to get toast notifications +const mapDispatchToProps = (dispatch) => ({ + 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/FileLinkModal.js b/client/src/components/LinkModal/FileLinkModal.js index a5d7631f..d2b22be8 100644 --- a/client/src/components/LinkModal/FileLinkModal.js +++ b/client/src/components/LinkModal/FileLinkModal.js @@ -4,6 +4,8 @@ import i18n from 'i18n'; import React, {useEffect} from 'react'; import InsertMediaModal from 'containers/InsertMediaModal/InsertMediaModal'; import {connect} from "react-redux"; +import LinkType from 'types/LinkType'; +import PropTypes from 'prop-types'; const FileLinkModal = ({type, editing, data, actions, onSubmit, ...props}) => { @@ -46,6 +48,14 @@ const FileLinkModal = ({type, editing, data, actions, onSubmit, ...props}) => { />; } +FileLinkModal.propTypes = { + type: LinkType.isRequired, + editing: PropTypes.bool.isRequired, + data: PropTypes.object.isRequired, + actions: PropTypes.object.isRequired, + onClick: PropTypes.func.isRequired, +}; + function mapStateToProps() { return {}; } diff --git a/client/src/components/LinkModal/LinkModal.js b/client/src/components/LinkModal/LinkModal.js index 7e5ce86d..85996979 100644 --- a/client/src/components/LinkModal/LinkModal.js +++ b/client/src/components/LinkModal/LinkModal.js @@ -1,39 +1,43 @@ /* 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'; +import PropTypes from 'prop-types'; -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, onSubmit, onClosed}) => { + if (!typeKey) { return false; } - return ; } +LinkModal.propTypes = { + typeTitle: PropTypes.string.isRequired, + typeKey: PropTypes.string.isRequired, + linkID: PropTypes.number.isRequired, + data: PropTypes.object.isRequired, + editing: PropTypes.bool.isRequired, + onSubmit: PropTypes.func.isRequired, + onClosed: PropTypes.func.isRequired, +}; + export default LinkModal; diff --git a/client/src/components/LinkPicker/LinkPicker.js b/client/src/components/LinkPicker/LinkPicker.js index 98aab6ec..695cbb47 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 }) => ( +
+ {!typeTitle && } + {typeTitle && onEdit()} + />}
); LinkPicker.propTypes = { ...LinkPickerMenu.propTypes, - link: PropTypes.shape(LinkPickerTitle.propTypes), - onEdit: PropTypes.func, - onClear: PropTypes.func, + title: PropTypes.string, + description: PropTypes.string, + typeTitle: PropTypes.string.isRequired, + onEdit: PropTypes.func.isRequired, + onClear: PropTypes.func.isRequired, + onSelect: PropTypes.func.isRequired, }; - 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,13 +12,13 @@ const stopPropagation = (fn) => (e) => { fn && fn(); } -const LinkPickerTitle = ({ title, type, description, onClear, onClick }) => ( +const LinkPickerTitle = ({ title, description, typeTitle, onClear, onClick }) => (