From 864a07f5d7a32c05e47887d93753c65a76baa952 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Thu, 21 Dec 2023 14:54:18 +1300 Subject: [PATCH] ENH Save relations on link creation --- client/dist/js/bundle.js | 2 +- client/src/components/LinkField/LinkField.js | 49 +++++++++-- client/src/components/LinkModal/LinkModal.js | 9 +- client/src/entwine/LinkField.js | 7 +- src/Controllers/LinkFieldController.php | 86 +++++++++++++++++++ src/Form/LinkField.php | 11 +++ src/Form/MultiLinkField.php | 31 ++----- src/Form/Traits/LinkFieldGetOwnerTrait.php | 15 ++++ .../Controllers/LinkFieldControllerTest.php | 63 ++++++++++++-- .../Controllers/LinkFieldControllerTest.yml | 3 + tests/php/Form/LinkFieldTest.php | 43 ---------- 11 files changed, 236 insertions(+), 83 deletions(-) delete mode 100644 tests/php/Form/LinkFieldTest.php diff --git a/client/dist/js/bundle.js b/client/dist/js/bundle.js index af7a89c3..48dce1d0 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){var r,l=(r=n(521))&&r.__esModule?r:{default:r};document.addEventListener("DOMContentLoaded",(()=>{(0,l.default)()}))},521:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(648)),l=u(n(809)),a=u(n(852)),i=u(n(117)),o=u(n(606));function u(e){return e&&e.__esModule?e:{default:e}}var s=()=>{r.default.component.registerMany({LinkPicker:l.default,LinkField:a.default,"LinkModal.FormBuilderModal":i.default,"LinkModal.InsertMediaModal":o.default})};t.default=s},852:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=k(n(363)),l=n(827),a=n(624),i=(n(648),v(n(42))),o=v(n(809)),u=v(n(734)),s=(v(n(686)),v(n(697))),d=k(n(123)),c=v(n(159)),f=v(n(510)),p=v(n(86)),y=v(n(754));function v(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={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&Object.prototype.hasOwnProperty.call(e,a)){var i=l?Object.getOwnPropertyDescriptor(e,a):null;i&&(i.get||i.set)?Object.defineProperty(r,a,i):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}const _="SilverStripe\\LinkField\\Controllers\\LinkFieldController",h=e=>{var t;let{value:n=null,onChange:l,types:a=[],actions:i,isMulti:d=!1,canCreate:p}=e;const[v,m]=(0,r.useState)({}),[k,h]=(0,r.useState)(0);let g=n;Array.isArray(g)||("number"==typeof g&&0!=g&&(g=[g]),g||(g=[])),(0,r.useEffect)((()=>{if(!k&&g.length>0){const e=[];for(const t of g)e.push(`itemIDs[]=${t}`);const t=`${f.default.getSection(_).form.linkForm.dataUrl}?${e.join("&")}`;c.default.get(t).then((e=>e.json())).then((e=>{m(e)}))}}),[k,n&&n.length]);const O=()=>{h(0)},b=e=>{h(0);const t=[...g];t.includes(e)||t.push(e),l(d?t:t[0]),i.toasts.success(y.default._t("LinkField.SAVE_SUCCESS","Saved link"))},M=e=>{const t=`${f.default.getSection(_).form.linkForm.deleteUrl}/${e}`;c.default.delete(t,{},{"X-SecurityID":f.default.get("SecurityID")}).then((()=>{i.toasts.success(y.default._t("LinkField.DELETE_SUCCESS","Deleted link"))})).catch((()=>{i.toasts.error(y.default._t("LinkField.DELETE_ERROR","Failed to delete link"))}));const n={...v};delete n[e],m(n),l(d?Object.keys(n):0)},C=d||0===Object.keys(v).length,E=Boolean(k);return r.default.createElement(r.default.Fragment,null,C&&r.default.createElement(o.default,{canCreate:p,onModalSuccess:b,onModalClosed:O,types:a}),r.default.createElement("div",null," ",(()=>{const e=[];for(const d of g){var t,n,l,i,o,s;if(!v[d])continue;const c=a.hasOwnProperty(null===(t=v[d])||void 0===t?void 0:t.typeKey)?a[null===(n=v[d])||void 0===n?void 0:n.typeKey]:{};e.push(r.default.createElement(u.default,{key:d,id:d,title:null===(l=v[d])||void 0===l?void 0:l.Title,description:null===(i=v[d])||void 0===i?void 0:i.description,versionState:null===(o=v[d])||void 0===o?void 0:o.versionState,typeTitle:c.title||"",onClear:M,onClick:()=>{h(d)},canDelete:!(null===(s=v[d])||void 0===s||!s.canDelete)}))}return e})()," "),E&&r.default.createElement(s.default,{types:a,typeKey:null===(t=v[k])||void 0===t?void 0:t.typeKey,isOpen:Boolean(k),onSuccess:b,onClosed:O,linkID:k}))};h.propTypes={value:p.default.oneOfType([p.default.arrayOf(p.default.number),p.default.number]),onChange:p.default.func.isRequired,types:p.default.array.isRequired,actions:p.default.object.isRequired,isMulti:p.default.bool,canCreate:p.default.bool.isRequired};var g=(0,l.compose)(i.default,(0,a.connect)(null,(e=>({actions:{toasts:(0,l.bindActionCreators)(d,e)}}))))(h);t.default=g},606:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;s(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=u(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&Object.prototype.hasOwnProperty.call(e,a)){var i=l?Object.getOwnPropertyDescriptor(e,a):null;i&&(i.get||i.set)?Object.defineProperty(r,a,i):r[a]=e[a]}r.default=e,n&&n.set(e,r);return r}(n(363)),l=s(n(475)),a=n(624),i=s(n(686)),o=s(n(86));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}}function d(){return d=Object.assign?Object.assign.bind():function(e){for(var t=1;t{let{type:t,editing:n,data:a,actions:i,onSubmit:o,...u}=e;if(!t)return!1;(0,r.useEffect)((()=>{n?i.initModal():i.reset()}),[n]);const s=a?{ID:a.FileID,Description:a.Title,TargetBlank:!!a.OpenInNew}:{};return r.default.createElement(l.default,d({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:l}=e;return o({FileID:n,Title:r,OpenInNew:l,typeKey:t.key},"",(()=>{}))}},u))};c.propTypes={type:i.default.isRequired,editing:o.default.bool.isRequired,data:o.default.object.isRequired,actions:o.default.object.isRequired,onClick:o.default.func.isRequired};var f=(0,a.connect)((function(){return{}}),(function(e){return{actions:{initModal:()=>e({type:"INIT_FORM_SCHEMA_STACK",payload:{formSchema:{type:"insert-link",nextType:"admin"}}}),reset:()=>e({type:"RESET"})}}}))(c);t.default=f},117:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=s(n(363)),l=s(n(912)),a=s(n(872)),i=s(n(902)),o=s(n(510)),u=s(n(86));function s(e){return e&&e.__esModule?e:{default:e}}const d=(e,t)=>{const{schemaUrl:n}=o.default.getSection("SilverStripe\\LinkField\\Controllers\\LinkFieldController").form.linkForm,r=a.default.parse(n),l=i.default.parse(r.query);l.typeKey=e;for(const e of["href","path","pathname"])r[e]=`${r[e]}/${t}`;return a.default.format({...r,search:i.default.stringify(l)})},c=e=>{let{typeTitle:t,typeKey:n,linkID:a=0,isOpen:i,onSuccess:o,onClosed:u}=e;if(!n)return!1;return r.default.createElement(l.default,{title:t,isOpen:i,schemaUrl:d(n,a),identifier:"Link.EditingLinkInfo",onSubmit:async(e,t,n)=>{const r=await n();if(!r.id.match(/\/schema\/linkfield\/([0-9]+)/)){const e=r.id.match(/\/linkForm\/([0-9]+)/),t=parseInt(e[1],10);o(t)}return Promise.resolve()},onClosed:u})};c.propTypes={typeTitle:u.default.string.isRequired,typeKey:u.default.string.isRequired,linkID:u.default.number,isOpen:u.default.bool.isRequired,onSuccess:u.default.func.isRequired,onClosed:u.default.func.isRequired};var f=c;t.default=f},809:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=d(n(754)),l=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=s(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&Object.prototype.hasOwnProperty.call(e,a)){var i=l?Object.getOwnPropertyDescriptor(e,a):null;i&&(i.get||i.set)?Object.defineProperty(r,a,i):r[a]=e[a]}r.default=e,n&&n.set(e,r);return r}(n(363)),a=d(n(86)),i=d(n(820)),o=d(n(97)),u=(d(n(686)),d(n(697)));function s(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(s=function(e){return e?n:t})(e)}function d(e){return e&&e.__esModule?e:{default:e}}const c=e=>{let{types:t,onModalSuccess:n,onModalClosed:a,canCreate:s}=e;const[d,c]=(0,l.useState)(""),f=""!==d,p=(0,i.default)("link-picker","form-control"),y=Object.values(t);return s?l.default.createElement("div",{className:p},l.default.createElement(o.default,{types:y,onSelect:e=>{c(e)}}),f&&l.default.createElement(u.default,{types:t,typeKey:d,isOpen:f,onSuccess:e=>{c(""),n(e)},onClosed:()=>{"function"==typeof a&&a(),c("")}})):l.default.createElement("div",{className:p},l.default.createElement("div",{className:"link-picker__cannot-create"},r.default._t("LinkField.CANNOT_CREATE_LINK","Cannot create link")))};t.Component=c,c.propTypes={types:a.default.array.isRequired,onModalSuccess:a.default.func.isRequired,onModalClosed:a.default.func,canCreate:a.default.bool.isRequired};var f=c;t.default=f},97:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=s(n(754)),l=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={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&Object.prototype.hasOwnProperty.call(e,a)){var i=l?Object.getOwnPropertyDescriptor(e,a):null;i&&(i.get||i.set)?Object.defineProperty(r,a,i):r[a]=e[a]}r.default=e,n&&n.set(e,r);return r}(n(363)),a=s(n(86)),i=n(127),o=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[a,o]=(0,l.useState)(!1);return l.default.createElement(i.Dropdown,{isOpen:a,toggle:()=>o((e=>!e)),className:"link-picker__menu"},l.default.createElement(i.DropdownToggle,{className:"link-picker__menu-toggle font-icon-plus-1",caret:!0},r.default._t("LinkField.ADD_LINK","Add Link")),l.default.createElement(i.DropdownMenu,null,t.map((e=>{let{key:t,title:r}=e;return l.default.createElement(i.DropdownItem,{key:t,onClick:()=>n(t)},r)}))))};d.propTypes={types:a.default.arrayOf(o.default).isRequired,onSelect:a.default.func.isRequired};var c=d;t.default=c},734:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(820)),l=u(n(754)),a=u(n(363)),i=u(n(86)),o=n(127);function u(e){return e&&e.__esModule?e:{default:e}}const s=e=>t=>{t.nativeEvent.stopImmediatePropagation(),t.preventDefault(),t.nativeEvent.preventDefault(),t.stopPropagation(),e&&e()},d=e=>{let{id:t,title:n,description:i,versionState:u,typeTitle:d,onClear:c,onClick:f,canDelete:p}=e;const y={"link-picker__link":!0,"form-control":!0};u&&(y[` link-picker__link--${u}`]=!0),n&&n.length>25&&(n=n.substring(0,25)+"...");const v=(0,r.default)(y);return a.default.createElement("div",{className:v},a.default.createElement(o.Button,{className:"link-picker__button font-icon-link",color:"secondary",onClick:s(f)},a.default.createElement("div",{className:"link-picker__link-detail"},a.default.createElement("div",{className:"link-picker__title"},a.default.createElement("span",{className:"link-picker__title-text"},n),(e=>{let t="",n="";if("draft"===e)t=l.default._t("LinkField.LINK_DRAFT_TITLE","Link has draft changes"),n=l.default._t("LinkField.LINK_DRAFT_LABEL","Draft");else{if("modified"!==e)return null;t=l.default._t("LinkField.LINK_MODIFIED_TITLE","Link has unpublished changes"),n=l.default._t("LinkField.LINK_MODIFIED_LABEL","Modified")}const i=(0,r.default)("badge",`status-${e}`);return a.default.createElement("span",{className:i,title:t},n)})(u)),a.default.createElement("small",{className:"link-picker__type"},d,": ",a.default.createElement("span",{className:"link-picker__url"},i)))),p&&a.default.createElement(o.Button,{className:"link-picker__clear",color:"link",onClick:s((()=>c(t)))},l.default._t("LinkField.CLEAR","Clear")))};d.propTypes={id:i.default.number.isRequired,title:i.default.string,description:i.default.string,versionState:i.default.string,typeTitle:i.default.string.isRequired,onClear:i.default.func.isRequired,onClick:i.default.func.isRequired,canDelete:i.default.bool.isRequired};var c=d;t.default=c},697:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=i(n(363)),l=n(648),a=i(n(86));function i(e){return e&&e.__esModule?e:{default:e}}const o=e=>{let{types:t,typeKey:n,linkID:a=0,isOpen:i,onSuccess:o,onClosed:u}=e;if(!n)return!1;const s=t.hasOwnProperty(n)?t[n]:{},d=s&&s.hasOwnProperty("handlerName")?s.handlerName:"FormBuilderModal",c=(0,l.loadComponent)(`LinkModal.${d}`);return r.default.createElement(c,{typeTitle:s.title||"",typeKey:n,linkID:a,isOpen:i,onSuccess:o,onClosed:u})};o.propTypes={types:a.default.array.isRequired,typeKey:a.default.string.isRequired,linkID:a.default.number,isOpen:a.default.bool.isRequired,onSuccess:a.default.func.isRequired,onClosed:a.default.func.isRequired};var u=o;t.default=u},41:function(e,t,n){var r=o(n(311)),l=o(n(363)),a=o(n(691)),i=n(648);function o(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-linkfield").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(a.default.createRoot(this[0])),this._super(),this.refresh()},refresh(){const e=this.getProps();this.getInputField().val(e.value);const t=this.getComponent();this.getRoot().render(l.default.createElement(t,u({},e,{noHolder:!0})))},handleChange(e){this.getInputField().data("value",e),this.refresh()},getProps(){return{value:this.getInputField().data("value"),onChange:this.handleChange.bind(this),isMulti:this.data("is-multi")??!1,types:this.data("types")??[],canCreate:this.getInputField().data("can-create")??!1}},getInputField(){const t=this.data("field-id");return e(`#${t}`)},onunmatch(){const e=this.getRoot();e&&e.unmount()}})}))},686:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,l=(r=n(86))&&r.__esModule?r:{default:r};var a=l.default.shape({key:l.default.string.isRequired,title:l.default.string.isRequired});t.default=a},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},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},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 l=t[r];if(void 0!==l)return l.exports;var a=t[r]={exports:{}};return e[r](a,a.exports,n),a.exports}n(274),n(41)}(); \ No newline at end of file +!function(){"use strict";var e={274:function(e,t,n){var r,o=(r=n(521))&&r.__esModule?r:{default:r};document.addEventListener("DOMContentLoaded",(()=>{(0,o.default)()}))},521:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(648)),o=u(n(809)),a=u(n(852)),l=u(n(117)),i=u(n(606));function u(e){return e&&e.__esModule?e:{default:e}}var s=()=>{r.default.component.registerMany({LinkPicker:o.default,LinkField:a.default,"LinkModal.FormBuilderModal":l.default,"LinkModal.InsertMediaModal":i.default})};t.default=s},852:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.LinkFieldContext=void 0;var r=_(n(363)),o=n(827),a=n(624),l=(n(648),k(n(42))),i=k(n(809)),u=k(n(734)),s=(k(n(686)),k(n(697))),d=_(n(123)),c=k(n(159)),f=k(n(510)),p=k(n(86)),y=k(n(754)),v=k(n(872));function k(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 _(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 a in e)if("default"!==a&&Object.prototype.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}const g=(0,r.createContext)(null);t.LinkFieldContext=g;const O="SilverStripe\\LinkField\\Controllers\\LinkFieldController",h=e=>{var t;let{value:n=null,onChange:o,types:a=[],actions:l,isMulti:d=!1,canCreate:p,ownerID:k,ownerClass:m,ownerRelation:_}=e;const[h,b]=(0,r.useState)({}),[C,M]=(0,r.useState)(0);let w=n;Array.isArray(w)||("number"==typeof w&&0!=w&&(w=[w]),w||(w=[])),(0,r.useEffect)((()=>{if(!C&&w.length>0){const e=[];for(const t of w)e.push(`itemIDs[]=${t}`);const t=`${f.default.getSection(O).form.linkForm.dataUrl}?${e.join("&")}`;c.default.get(t).then((e=>e.json())).then((e=>{b(e)}))}}),[C,n&&n.length]);const R=()=>{M(0)},j=e=>{M(0);const t=[...w];t.includes(e)||t.push(e),o(d?t:t[0]),l.toasts.success(y.default._t("LinkField.SAVE_SUCCESS","Saved link"))},D=e=>{let t=`${f.default.getSection(O).form.linkForm.deleteUrl}/${e}`;const n=v.default.parse(t),r=qs.parse(n.query);r.ownerID=k,r.ownerClass=m,r.ownerRelation=_,t=v.default.format({...n,search:qs.stringify(r)}),c.default.delete(t,{},{"X-SecurityID":f.default.get("SecurityID")}).then((()=>{l.toasts.success(y.default._t("LinkField.DELETE_SUCCESS","Deleted link"))})).catch((()=>{l.toasts.error(y.default._t("LinkField.DELETE_ERROR","Failed to delete link"))}));const a={...h};delete a[e],b(a),o(d?Object.keys(a):0)},E=d||0===Object.keys(h).length,I=Boolean(C);return r.default.createElement(g.Provider,{value:{ownerID:k,ownerClass:m,ownerRelation:_}},E&&r.default.createElement(i.default,{onModalSuccess:j,onModalClosed:R,types:a,canCreate:p}),r.default.createElement("div",null," ",(()=>{const e=[];for(const d of w){var t,n,o,l,i,s;if(!h[d])continue;const c=a.hasOwnProperty(null===(t=h[d])||void 0===t?void 0:t.typeKey)?a[null===(n=h[d])||void 0===n?void 0:n.typeKey]:{};e.push(r.default.createElement(u.default,{key:d,id:d,title:null===(o=h[d])||void 0===o?void 0:o.Title,description:null===(l=h[d])||void 0===l?void 0:l.description,versionState:null===(i=h[d])||void 0===i?void 0:i.versionState,typeTitle:c.title||"",onClear:D,onClick:()=>{M(d)},canDelete:!(null===(s=h[d])||void 0===s||!s.canDelete)}))}return e})()," "),I&&r.default.createElement(s.default,{types:a,typeKey:null===(t=h[C])||void 0===t?void 0:t.typeKey,isOpen:Boolean(C),onSuccess:j,onClosed:R,linkID:C,ownerID:k,ownerClass:m,ownerRelation:_}))};h.propTypes={value:p.default.oneOfType([p.default.arrayOf(p.default.number),p.default.number]),onChange:p.default.func.isRequired,types:p.default.array.isRequired,actions:p.default.object.isRequired,isMulti:p.default.bool,canCreate:p.default.bool.isRequired,ownerID:p.default.number.isRequired,ownerClass:p.default.string.isRequired,ownerRelation:p.default.string.isRequired};var b=(0,o.compose)(l.default,(0,a.connect)(null,(e=>({actions:{toasts:(0,o.bindActionCreators)(d,e)}}))))(h);t.default=b},606:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;s(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=u(t);if(n&&n.has(e))return n.get(e);var r={},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&Object.prototype.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}r.default=e,n&&n.set(e,r);return r}(n(363)),o=s(n(475)),a=n(624),l=s(n(686)),i=s(n(86));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}}function d(){return d=Object.assign?Object.assign.bind():function(e){for(var t=1;t{let{type:t,editing:n,data:a,actions:l,onSubmit:i,...u}=e;if(!t)return!1;(0,r.useEffect)((()=>{n?l.initModal():l.reset()}),[n]);const s=a?{ID:a.FileID,Description:a.Title,TargetBlank:!!a.OpenInNew}:{};return r.default.createElement(o.default,d({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:o}=e;return i({FileID:n,Title:r,OpenInNew:o,typeKey:t.key},"",(()=>{}))}},u))};c.propTypes={type:l.default.isRequired,editing:i.default.bool.isRequired,data:i.default.object.isRequired,actions:i.default.object.isRequired,onClick:i.default.func.isRequired};var f=(0,a.connect)((function(){return{}}),(function(e){return{actions:{initModal:()=>e({type:"INIT_FORM_SCHEMA_STACK",payload:{formSchema:{type:"insert-link",nextType:"admin"}}}),reset:()=>e({type:"RESET"})}}}))(c);t.default=f},117:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;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=c(t);if(n&&n.has(e))return n.get(e);var r={},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&Object.prototype.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}r.default=e,n&&n.set(e,r);return r}(n(363)),o=d(n(912)),a=n(852),l=d(n(872)),i=d(n(902)),u=d(n(510)),s=d(n(86));function d(e){return e&&e.__esModule?e:{default:e}}function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(c=function(e){return e?n:t})(e)}const f=(e,t)=>{const{schemaUrl:n}=u.default.getSection("SilverStripe\\LinkField\\Controllers\\LinkFieldController").form.linkForm,o=l.default.parse(n),s=i.default.parse(o.query);s.typeKey=e;const{ownerID:d,ownerClass:c,ownerRelation:f}=(0,r.useContext)(a.LinkFieldContext);s.ownerID=d,s.ownerClass=c,s.ownerRelation=f;for(const e of["href","path","pathname"])o[e]=`${o[e]}/${t}`;return l.default.format({...o,search:i.default.stringify(s)})},p=e=>{let{typeTitle:t,typeKey:n,linkID:a=0,isOpen:l,onSuccess:i,onClosed:u}=e;if(!n)return!1;return r.default.createElement(o.default,{title:t,isOpen:l,schemaUrl:f(n,a),identifier:"Link.EditingLinkInfo",onSubmit:async(e,t,n)=>{const r=await n();if(!r.id.match(/\/schema\/linkfield\/([0-9]+)/)){const e=r.id.match(/\/linkForm\/([0-9]+)/),t=parseInt(e[1],10);i(t)}return Promise.resolve()},onClosed:u})};p.propTypes={typeTitle:s.default.string.isRequired,typeKey:s.default.string.isRequired,linkID:s.default.number,isOpen:s.default.bool.isRequired,onSuccess:s.default.func.isRequired,onClosed:s.default.func.isRequired};var y=p;t.default=y},809:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=d(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=s(t);if(n&&n.has(e))return n.get(e);var r={},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&Object.prototype.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}r.default=e,n&&n.set(e,r);return r}(n(363)),a=d(n(86)),l=d(n(820)),i=d(n(97)),u=(d(n(686)),d(n(697)));function s(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(s=function(e){return e?n:t})(e)}function d(e){return e&&e.__esModule?e:{default:e}}const c=e=>{let{types:t,onModalSuccess:n,onModalClosed:a,canCreate:s}=e;const[d,c]=(0,o.useState)(""),f=""!==d,p=(0,l.default)("link-picker","form-control"),y=Object.values(t);return s?o.default.createElement("div",{className:p},o.default.createElement(i.default,{types:y,onSelect:e=>{c(e)}}),f&&o.default.createElement(u.default,{types:t,typeKey:d,isOpen:f,onSuccess:e=>{c(""),n(e)},onClosed:()=>{"function"==typeof a&&a(),c("")}})):o.default.createElement("div",{className:p},o.default.createElement("div",{className:"link-picker__cannot-create"},r.default._t("LinkField.CANNOT_CREATE_LINK","Cannot create link")))};t.Component=c,c.propTypes={types:a.default.array.isRequired,onModalSuccess:a.default.func.isRequired,onModalClosed:a.default.func,canCreate:a.default.bool.isRequired};var f=c;t.default=f},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 a in e)if("default"!==a&&Object.prototype.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}r.default=e,n&&n.set(e,r);return r}(n(363)),a=s(n(86)),l=n(127),i=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[a,i]=(0,o.useState)(!1);return o.default.createElement(l.Dropdown,{isOpen:a,toggle:()=>i((e=>!e)),className:"link-picker__menu"},o.default.createElement(l.DropdownToggle,{className:"link-picker__menu-toggle font-icon-plus-1",caret:!0},r.default._t("LinkField.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:a.default.arrayOf(i.default).isRequired,onSelect:a.default.func.isRequired};var c=d;t.default=c},734:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(820)),o=u(n(754)),a=u(n(363)),l=u(n(86)),i=n(127);function u(e){return e&&e.__esModule?e:{default:e}}const s=e=>t=>{t.nativeEvent.stopImmediatePropagation(),t.preventDefault(),t.nativeEvent.preventDefault(),t.stopPropagation(),e&&e()},d=e=>{let{id:t,title:n,description:l,versionState:u,typeTitle:d,onClear:c,onClick:f,canDelete:p}=e;const y={"link-picker__link":!0,"form-control":!0};u&&(y[` link-picker__link--${u}`]=!0),n&&n.length>25&&(n=n.substring(0,25)+"...");const v=(0,r.default)(y);return a.default.createElement("div",{className:v},a.default.createElement(i.Button,{className:"link-picker__button font-icon-link",color:"secondary",onClick:s(f)},a.default.createElement("div",{className:"link-picker__link-detail"},a.default.createElement("div",{className:"link-picker__title"},a.default.createElement("span",{className:"link-picker__title-text"},n),(e=>{let t="",n="";if("draft"===e)t=o.default._t("LinkField.LINK_DRAFT_TITLE","Link has draft changes"),n=o.default._t("LinkField.LINK_DRAFT_LABEL","Draft");else{if("modified"!==e)return null;t=o.default._t("LinkField.LINK_MODIFIED_TITLE","Link has unpublished changes"),n=o.default._t("LinkField.LINK_MODIFIED_LABEL","Modified")}const l=(0,r.default)("badge",`status-${e}`);return a.default.createElement("span",{className:l,title:t},n)})(u)),a.default.createElement("small",{className:"link-picker__type"},d,": ",a.default.createElement("span",{className:"link-picker__url"},l)))),p&&a.default.createElement(i.Button,{className:"link-picker__clear",color:"link",onClick:s((()=>c(t)))},o.default._t("LinkField.CLEAR","Clear")))};d.propTypes={id:l.default.number.isRequired,title:l.default.string,description:l.default.string,versionState:l.default.string,typeTitle:l.default.string.isRequired,onClear:l.default.func.isRequired,onClick:l.default.func.isRequired,canDelete:l.default.bool.isRequired};var c=d;t.default=c},697:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=l(n(363)),o=n(648),a=l(n(86));function l(e){return e&&e.__esModule?e:{default:e}}const i=e=>{let{types:t,typeKey:n,linkID:a=0,isOpen:l,onSuccess:i,onClosed:u}=e;if(!n)return!1;const s=t.hasOwnProperty(n)?t[n]:{},d=s&&s.hasOwnProperty("handlerName")?s.handlerName:"FormBuilderModal",c=(0,o.loadComponent)(`LinkModal.${d}`);return r.default.createElement(c,{typeTitle:s.title||"",typeKey:n,linkID:a,isOpen:l,onSuccess:i,onClosed:u})};i.propTypes={types:a.default.array.isRequired,typeKey:a.default.string.isRequired,linkID:a.default.number,isOpen:a.default.bool.isRequired,onSuccess:a.default.func.isRequired,onClosed:a.default.func.isRequired};var u=i;t.default=u},41:function(e,t,n){var r=i(n(311)),o=i(n(363)),a=i(n(691)),l=n(648);function i(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-linkfield").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(a.default.createRoot(this[0])),this._super(),this.refresh()},refresh(){const e=this.getProps();this.getInputField().val(e.value);const t=this.getComponent();this.getRoot().render(o.default.createElement(t,u({},e,{noHolder:!0})))},handleChange(e){this.getInputField().data("value",e),this.refresh()},getProps(){const e=this.getInputField();return{value:e.data("value"),ownerID:e.data("owner-id"),ownerClass:e.data("owner-class"),ownerRelation:e.data("owner-relation"),onChange:this.handleChange.bind(this),isMulti:this.data("is-multi")??!1,types:this.data("types")??[],canCreate:this.getInputField().data("can-create")??!1}},getInputField(){const t=this.data("field-id");return e(`#${t}`)},onunmatch(){const e=this.getRoot();e&&e.unmount()}})}))},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 a=o.default.shape({key:o.default.string.isRequired,title:o.default.string.isRequired});t.default=a},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},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},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 a=t[r]={exports:{}};return e[r](a,a.exports,n),a.exports}n(274),n(41)}(); \ No newline at end of file diff --git a/client/src/components/LinkField/LinkField.js b/client/src/components/LinkField/LinkField.js index 97d886b6..06b661c2 100644 --- a/client/src/components/LinkField/LinkField.js +++ b/client/src/components/LinkField/LinkField.js @@ -1,5 +1,5 @@ /* eslint-disable */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, createContext } from 'react'; import { bindActionCreators, compose } from 'redux'; import { connect } from 'react-redux'; import { injectGraphql } from 'lib/Injector'; @@ -13,6 +13,9 @@ import backend from 'lib/Backend'; import Config from 'lib/Config'; import PropTypes from 'prop-types'; import i18n from 'i18n'; +import url from 'url'; + +export const LinkFieldContext = createContext(null); // section used in window.ss config const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController'; @@ -23,9 +26,22 @@ const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController'; * types - types of the Link passed from LinkField entwine * actions - object of redux actions * isMulti - whether this field handles multiple links or not - * canCreate - whether this field can create links or not + * canCreate - whether this field can create new links or not + * ownerID - ID of the owner DataObject + * ownerClass - class name of the owner DataObject + * ownerRelation - name of the relation on the owner DataObject */ -const LinkField = ({ value = null, onChange, types = [], actions, isMulti = false, canCreate }) => { +const LinkField = ({ + value = null, + onChange, + types = [], + actions, + isMulti = false, + canCreate, + ownerID, + ownerClass, + ownerRelation, +}) => { const [data, setData] = useState({}); const [editingID, setEditingID] = useState(0); @@ -94,7 +110,13 @@ const LinkField = ({ value = null, onChange, types = [], actions, isMulti = fals * Update the component when the 'Clear' button in the LinkPicker is clicked */ const onClear = (linkID) => { - const endpoint = `${Config.getSection(section).form.linkForm.deleteUrl}/${linkID}`; + let endpoint = `${Config.getSection(section).form.linkForm.deleteUrl}/${linkID}`; + const parsedURL = url.parse(endpoint); + const parsedQs = qs.parse(parsedURL.query); + parsedQs.ownerID = ownerID; + parsedQs.ownerClass = ownerClass; + parsedQs.ownerRelation = ownerRelation; + endpoint = url.format({ ...parsedURL, search: qs.stringify(parsedQs)}); // CSRF token 'X-SecurityID' headers needs to be present for destructive requests backend.delete(endpoint, {}, { 'X-SecurityID': Config.get('SecurityID') }) .then(() => { @@ -155,9 +177,14 @@ const LinkField = ({ value = null, onChange, types = [], actions, isMulti = fals const renderPicker = isMulti || Object.keys(data).length === 0; const renderModal = Boolean(editingID); - return <> - { renderPicker && } -
{ renderLinks() }
+ return + { renderPicker && } +
{ renderLinks() }
{ renderModal && } - ; +
; }; LinkField.propTypes = { @@ -177,6 +207,9 @@ LinkField.propTypes = { actions: PropTypes.object.isRequired, isMulti: PropTypes.bool, canCreate: PropTypes.bool.isRequired, + ownerID: PropTypes.number.isRequired, + ownerClass: PropTypes.string.isRequired, + ownerRelation: PropTypes.string.isRequired, }; // redux actions loaded into props - used to get toast notifications diff --git a/client/src/components/LinkModal/LinkModal.js b/client/src/components/LinkModal/LinkModal.js index 4d518ec8..d590a6cd 100644 --- a/client/src/components/LinkModal/LinkModal.js +++ b/client/src/components/LinkModal/LinkModal.js @@ -1,6 +1,7 @@ /* eslint-disable */ -import React from 'react'; +import React, { useContext } from 'react' import FormBuilderModal from 'components/FormBuilderModal/FormBuilderModal'; +import { LinkFieldContext } from 'components/LinkField/LinkField'; import url from 'url'; import qs from 'qs'; import Config from 'lib/Config'; @@ -11,13 +12,17 @@ const buildSchemaUrl = (typeKey, linkID) => { const parsedURL = url.parse(schemaUrl); const parsedQs = qs.parse(parsedURL.query); parsedQs.typeKey = typeKey; + const { ownerID, ownerClass, ownerRelation } = useContext(LinkFieldContext); + parsedQs.ownerID = ownerID; + parsedQs.ownerClass = ownerClass; + parsedQs.ownerRelation = ownerRelation; for (const prop of ['href', 'path', 'pathname']) { parsedURL[prop] = `${parsedURL[prop]}/${linkID}`; } return url.format({ ...parsedURL, search: qs.stringify(parsedQs)}); } -const LinkModal = ({ typeTitle, typeKey, linkID = 0, isOpen, onSuccess, onClosed}) => { +const LinkModal = ({ typeTitle, typeKey, linkID = 0, isOpen, onSuccess, onClosed }) => { if (!typeKey) { return false; } diff --git a/client/src/entwine/LinkField.js b/client/src/entwine/LinkField.js index c3b34fd2..da2ab2c7 100644 --- a/client/src/entwine/LinkField.js +++ b/client/src/entwine/LinkField.js @@ -45,9 +45,12 @@ jQuery.entwine('ss', ($) => { * @returns {Object} */ getProps() { - const value = this.getInputField().data('value'); + const inputField = this.getInputField(); return { - value, + value: inputField.data('value'), + ownerID: inputField.data('owner-id'), + ownerClass: inputField.data('owner-class'), + ownerRelation: inputField.data('owner-relation'), onChange: this.handleChange.bind(this), isMulti: this.data('is-multi') ?? false, types: this.data('types') ?? [], diff --git a/src/Controllers/LinkFieldController.php b/src/Controllers/LinkFieldController.php index f583728d..608da657 100644 --- a/src/Controllers/LinkFieldController.php +++ b/src/Controllers/LinkFieldController.php @@ -16,11 +16,13 @@ use SilverStripe\ORM\ValidationResult; use SilverStripe\Control\Controller; use SilverStripe\Control\HTTPRequest; +use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\HiddenField; use SilverStripe\LinkField\Form\LinkField; use SilverStripe\LinkField\Services\LinkTypeService; use SilverStripe\ORM\DataList; +use SilverStripe\ORM\DataObject; class LinkFieldController extends LeftAndMain { @@ -46,6 +48,7 @@ public function getClientConfig() $clientConfig['form']['linkForm'] = [ // schema() is defined on LeftAndMain // schemaUrl will get the $ItemID and ?typeKey dynamically suffixed in LinkModal.js + // as well as ownerID, OwnerClass and OwnerRelation 'schemaUrl' => $this->Link('schema/linkForm'), 'deleteUrl' => $this->Link('delete'), 'dataUrl' => $this->Link('data'), @@ -135,6 +138,16 @@ public function linkDelete(): HTTPResponse } // delete() will also delete any published version immediately $link->delete(); + // Update owner object if this Link is on a has_one relation on the owner + $owner = $this->ownerFromRequest(); + $ownerRelation = $this->ownerRelationFromRequest(); + $config = Config::forClass($owner->ClassName); + $hasOne = $config->get('has_one'); + if (array_key_exists($ownerRelation, $hasOne) && $owner->canEdit()) { + $owner->$ownerRelation = null; + $owner->write(); + } + // Send response $response = $this->getResponse(); $response->addHeader('Content-type', 'application/json'); $response->setBody(json_encode(['success' => true])); @@ -215,6 +228,16 @@ public function save(array $data, Form $form): HTTPResponse $link->write(); } + // Update owner object if this Link is on a has_one relation on the owner + $owner = $this->ownerFromRequest(); + $ownerRelation = $this->ownerRelationFromRequest(); + $config = Config::forClass($owner->ClassName); + $hasOne = $config->get('has_one'); + if (array_key_exists($ownerRelation, $hasOne) && $owner->canEdit()) { + $owner->$ownerRelation = $link; + $owner->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 @@ -241,6 +264,15 @@ private function createLinkForm(Link $link, string $operation): Form /** @var Form $form */ $form = $formFactory->getForm($this, $name, ['Record' => $link]); + // Add hidden form fields for OwnerID, OwnerClass and OwnerRelation + if ($operation === 'create') { + $owner = $this->ownerFromRequest(); + $form->Fields()->push(HiddenField::create('OwnerID')->setValue($owner->ID)); + $form->Fields()->push(HiddenField::create('OwnerClass')->setValue($owner->ClassName)); + $ownerRelation = $this->ownerRelationFromRequest(); + $form->Fields()->push(HiddenField::create('OwnerRelation')->setValue($ownerRelation)); + } + // Set where the form is submitted to $typeKey = LinkTypeService::create()->keyByClassName($link->ClassName); $form->setFormAction($this->Link("linkForm/$id?typeKey=$typeKey")); @@ -358,4 +390,58 @@ private function typeKeyFromRequest(): string } return $typeKey; } + + /** + * Get the owner based on the query string params ownerID, ownerClass, ownerRelation + * OR the POST vars OwnerID, OwnerClass, OwnerRelation + */ + private function ownerFromRequest(): DataObject + { + $request = $this->getRequest(); + $ownerID = (int) ($request->getVar('ownerID') ?: $request->postVar('OwnerID')); + if ($ownerID === 0) { + $this->jsonError(404, _t('LinkField.INVALID_OWNER_ID', 'Invalid ownerID')); + } + $ownerClass = $request->getVar('ownerClass') ?: $request->postVar('OwnerClass'); + if (!is_a($ownerClass, DataObject::class, true)) { + $this->jsonError(404, _t('LinkField.INVALID_OWNER_CLASS', 'Invalid ownerClass')); + } + $ownerRelation = $this->ownerRelationFromRequest(); + $config = Config::forClass($ownerClass); + $hasOne = $config->get('has_one'); + $hasMany = $config->get('has_many'); + $matchedRelation = false; + foreach ([$hasOne, $hasMany] as $property) { + if (!array_key_exists($ownerRelation, $property)) { + continue; + } + $className = $property[$ownerRelation]; + if (is_a($className, Link::class, true)) { + $matchedRelation = true; + break; + } + } + if ($matchedRelation) { + /** @var DataObject $ownerClass */ + $owner = $ownerClass::get()->byID($ownerID); + if ($owner) { + return $owner; + } + } + $this->jsonError(404, _t('LinkField.INVALID_OWNER', 'Invalid Owner')); + } + + /** + * Get the owner relation based on the query string param ownerRelation + * OR the POST var OwnerRelation + */ + private function ownerRelationFromRequest(): string + { + $request = $this->getRequest(); + $ownerRelation = $request->getVar('ownerRelation') ?: $request->postVar('OwnerRelation'); + if (!$ownerRelation) { + $this->jsonError(404, _t('LinkField.INVALID_OWNER_RELATION', 'Invalid ownerRelation')); + } + return $ownerRelation; + } } diff --git a/src/Form/LinkField.php b/src/Form/LinkField.php index 3c538193..81ac1276 100644 --- a/src/Form/LinkField.php +++ b/src/Form/LinkField.php @@ -3,6 +3,9 @@ namespace SilverStripe\LinkField\Form; use LogicException; +use SilverStripe\CMS\Controllers\CMSPageEditController; +use SilverStripe\CMS\Controllers\ContentController; +use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Forms\FormField; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; @@ -74,6 +77,10 @@ protected function getDefaultAttributes(): array $attributes = parent::getDefaultAttributes(); $attributes['data-value'] = $this->Value(); $attributes['data-can-create'] = $this->getOwner()->canEdit(); + $ownerFields = $this->getOwnerFields(); + $attributes['data-owner-id'] = $ownerFields['ID']; + $attributes['data-owner-class'] = $ownerFields['Class']; + $attributes['data-owner-relation'] = $ownerFields['Relation']; return $attributes; } @@ -81,6 +88,10 @@ public function getSchemaDataDefaults() { $data = parent::getSchemaDataDefaults(); $data['types'] = json_decode($this->getTypesProps()); + $ownerFields = $this->getOwnerFields(); + $data['ownerID'] = $ownerFields['ID']; + $data['ownerClass'] = $ownerFields['Class']; + $data['ownerRelation'] = $ownerFields['Relation']; return $data; } } diff --git a/src/Form/MultiLinkField.php b/src/Form/MultiLinkField.php index ea8fbfef..319d1c6c 100644 --- a/src/Form/MultiLinkField.php +++ b/src/Form/MultiLinkField.php @@ -5,6 +5,7 @@ use LogicException; use SilverStripe\Forms\FormField; use SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait; +use SilverStripe\LinkField\Form\Traits\LinkFieldGetOwnerTrait; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Relation; @@ -41,33 +42,15 @@ public function setValue($value, $data = null) return parent::setValue($ids, $data); } - public function saveInto(DataObjectInterface $record) - { - $fieldName = $this->getName(); - if (!$fieldName) { - throw new LogicException('LinkField must have a name'); - } - - $relation = $record->hasMethod($fieldName) ? $record->$fieldName() : null; - if (!$relation) { - throw new LogicException("{$record->ClassName} is missing the relation '$fieldName'"); - } - - // Use RelationList rather than Relation here since some Relation classes don't allow setting value - but RelationList does. - if (!($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) { - throw new LogicException("'$fieldName()' method on {$record->ClassName} doesn't return a relation list"); - } else { - $relation->setByIDList($this->getValueArray() ?? []); - } - - return $this; - } - public function getSchemaDataDefaults() { $data = parent::getSchemaDataDefaults(); $data['isMulti'] = true; $data['types'] = json_decode($this->getTypesProps()); + $ownerFields = $this->getOwnerFields(); + $data['ownerID'] = $ownerFields['ID']; + $data['ownerClass'] = $ownerFields['Class']; + $data['ownerRelation'] = $ownerFields['Relation']; return $data; } @@ -84,6 +67,10 @@ protected function getDefaultAttributes(): array $attributes = parent::getDefaultAttributes(); $attributes['data-value'] = $this->getValueArray(); $attributes['data-can-create'] = $this->getOwner()->canEdit(); + $ownerFields = $this->getOwnerFields(); + $attributes['data-owner-id'] = $ownerFields['ID']; + $attributes['data-owner-class'] = $ownerFields['Class']; + $attributes['data-owner-relation'] = $ownerFields['Relation']; return $attributes; } diff --git a/src/Form/Traits/LinkFieldGetOwnerTrait.php b/src/Form/Traits/LinkFieldGetOwnerTrait.php index 0b12bd2a..05f359d0 100644 --- a/src/Form/Traits/LinkFieldGetOwnerTrait.php +++ b/src/Form/Traits/LinkFieldGetOwnerTrait.php @@ -18,4 +18,19 @@ private function getOwner(): DataObject } return $owner; } + + private function getOwnerFields(): array + { + $owner = $this->getOwner(); + $relation = $this->getName(); + // Elemental content block + if (preg_match('#^PageElements_[0-9]+_(.+)$#', $relation, $matches)) { + $relation = $matches[1]; + } + return [ + 'ID' => $owner->ID, + 'Class' => $owner::class, + 'Relation' => $relation, + ]; + } } diff --git a/tests/php/Controllers/LinkFieldControllerTest.php b/tests/php/Controllers/LinkFieldControllerTest.php index d8372934..bd2a4a53 100644 --- a/tests/php/Controllers/LinkFieldControllerTest.php +++ b/tests/php/Controllers/LinkFieldControllerTest.php @@ -2,11 +2,13 @@ namespace SilverStripe\LinkField\Tests\Controllers; +use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Dev\FunctionalTest; use SilverStripe\LinkField\Tests\Controllers\LinkFieldControllerTest\TestPhoneLink; use SilverStripe\Core\Config\Config; use SilverStripe\Security\SecurityToken; use SilverStripe\Control\HTTPRequest; +use SilverStripe\LinkField\Tests\Models\LinkTest\LinkOwner; class LinkFieldControllerTest extends FunctionalTest { @@ -14,6 +16,7 @@ class LinkFieldControllerTest extends FunctionalTest protected static $extra_dataobjects = [ TestPhoneLink::class, + LinkOwner::class, ]; private $securityTokenWasEnabled = false; @@ -50,11 +53,17 @@ public function testLinkFormGetSchema( string $expectedMessage ): void { TestPhoneLink::$fail = $fail; + $owner = $this->getFixtureLinkOwner(); + $ownerID = $owner->ID; + $ownerClass = urlencode($owner->ClassName); + $ownerRelation = 'Link'; $id = $this->getID($idType); if ($id === -1) { - $url = "/admin/linkfield/schema/linkForm?typeKey=$typeKey"; + $url = "/admin/linkfield/schema/linkForm?typeKey=$typeKey&ownerID=$ownerID&ownerClass=$ownerClass" . + "&ownerRelation=$ownerRelation"; } else { - $url = "/admin/linkfield/schema/linkForm/$id?typeKey=$typeKey"; + $url = "/admin/linkfield/schema/linkForm/$id?typeKey=$typeKey&ownerID=$ownerID&ownerClass=$ownerClass" . + "&ownerRelation=$ownerRelation"; } $headers = $this->formSchemaHeader(); $response = $this->get($url, null, $headers); @@ -73,6 +82,18 @@ public function testLinkFormGetSchema( // state node is flattened, unlike schema node $this->assertSame($expectedValue, $formSchema['state']['fields'][4]['value']); $this->assertFalse(array_key_exists('errors', $formSchema)); + if ($idType === 'new-record') { + $this->assertSame('OwnerID', $formSchema['state']['fields'][6]['name']); + $this->assertSame($ownerID, $formSchema['state']['fields'][6]['value']); + $this->assertSame('OwnerClass', $formSchema['state']['fields'][7]['name']); + $this->assertSame($owner->ClassName, $formSchema['state']['fields'][7]['value']); + $this->assertSame('OwnerRelation', $formSchema['state']['fields'][8]['name']); + $this->assertSame($ownerRelation, $formSchema['state']['fields'][8]['value']); + } else { + $this->assertNotSame('OwnerID', $formSchema['state']['fields'][6]['name']); + $this->assertFalse(array_key_exists(7, $formSchema['state']['fields'])); + $this->assertFalse(array_key_exists(8, $formSchema['state']['fields'])); + } } } @@ -151,6 +172,11 @@ public function testLinkFormPost( string $expectedLinkType ): void { TestPhoneLink::$fail = $fail; + $owner = $this->getFixtureLinkOwner(); + $ownerID = $owner->ID; + $ownerClass = urlencode($owner->ClassName); + $ownerRelation = 'Link'; + $ownerLinkID = $owner->LinkID; $id = $this->getID($idType); if ($dataType === 'valid') { $data = $this->getFixtureLink()->jsonSerialize(); @@ -166,7 +192,8 @@ public function testLinkFormPost( if ($fail) { $data['Fail'] = $fail; } - $url = "/admin/linkfield/linkForm/$id?typeKey=$typeKey"; + $url = "/admin/linkfield/linkForm/$id?typeKey=$typeKey&ownerID=$ownerID&ownerClass=$ownerClass" . + "&ownerRelation=$ownerRelation"; $headers = $this->formSchemaHeader(); if ($fail !== 'csrf-token') { $headers = array_merge($headers, $this->csrfTokenheader()); @@ -207,11 +234,23 @@ public function testLinkFormPost( // Phone was note updated on PhoneLink dataobject $link = TestPhoneLink::get()->byID($newID); $this->assertSame($link->Phone, '0123456789'); + // LinkOwner.Link relation was not updated (refetch dataobject first) + $owner = $this->getFixtureLinkOwner(); + $this->assertSame($owner->LinkID, $ownerLinkID); + if ($idType === 'new-record') { + $this->assertsame($newID, $ownerLinkID); + } } else { $this->assertEmpty($formSchema['errors']); // Phone was updated on PhoneLink dataobject $link = TestPhoneLink::get()->byID($newID); $this->assertSame($link->Phone, '9876543210'); + // LinkOwner.Link relation was updated (refetch dataobject first) + $owner = $this->getFixtureLinkOwner(); + $this->assertSame($owner->LinkID, $newID); + if ($idType === 'new-record') { + $this->assertNotSame($newID, $ownerLinkID); + } } } } @@ -453,12 +492,17 @@ public function testLinkDelete( string $expectedMessage ): void { TestPhoneLink::$fail = $fail; + $owner = $this->getFixtureLinkOwner(); + $ownerID = $owner->ID; + $ownerClass = urlencode($owner->ClassName); + $ownerRelation = 'Link'; + $ownerLinkID = $owner->LinkID; $id = $this->getID($idType); $fixtureID = $this->getFixtureLink()->ID; if ($id === -1) { - $url = "/admin/linkfield/delete"; + $url = "/admin/linkfield/delete?ownerID=$ownerID&ownerClass=$ownerClass&ownerRelation=$ownerRelation"; } else { - $url = "/admin/linkfield/delete/$id"; + $url = "/admin/linkfield/delete/$id?ownerID=$ownerID&ownerClass=$ownerClass&ownerRelation=$ownerRelation"; } $headers = []; if ($fail !== 'csrf-token') { @@ -471,8 +515,12 @@ public function testLinkDelete( $jsonError = json_decode($response->getBody(), true); $this->assertSame($expectedMessage, $jsonError['errors'][0]['value']); $this->assertNotNull(TestPhoneLink::get()->byID($fixtureID)); + $owner = $this->getFixtureLinkOwner(); + $this->assertSame($ownerLinkID, $owner->LinkID); } else { $this->assertNull(TestPhoneLink::get()->byID($fixtureID)); + $owner = $this->getFixtureLinkOwner(); + $this->assertSame(0, $owner->LinkID); } $this->assertTrue(true); } @@ -530,6 +578,11 @@ private function getFixtureLink(): TestPhoneLink return $this->objFromFixture(TestPhoneLink::class, 'TestPhoneLink01'); } + private function getFixtureLinkOwner(): LinkOwner + { + return $this->objFromFixture(LinkOwner::class, 'TestLinkOwner01'); + } + private function getID(string $idType): mixed { $link = $this->getFixtureLink(); diff --git a/tests/php/Controllers/LinkFieldControllerTest.yml b/tests/php/Controllers/LinkFieldControllerTest.yml index 6d96f850..5b4b633f 100644 --- a/tests/php/Controllers/LinkFieldControllerTest.yml +++ b/tests/php/Controllers/LinkFieldControllerTest.yml @@ -2,3 +2,6 @@ SilverStripe\LinkField\Tests\Controllers\LinkFieldControllerTest\TestPhoneLink: TestPhoneLink01: Title: My phone link Phone: 0123456789 +SilverStripe\LinkField\Tests\Models\LinkTest\LinkOwner: + TestLinkOwner01: + Link: =>SilverStripe\LinkField\Tests\Controllers\LinkFieldControllerTest\TestPhoneLink.TestPhoneLink01 diff --git a/tests/php/Form/LinkFieldTest.php b/tests/php/Form/LinkFieldTest.php deleted file mode 100644 index 4fb94aec..00000000 --- a/tests/php/Form/LinkFieldTest.php +++ /dev/null @@ -1,43 +0,0 @@ -write(); - $owner = new LinkOwner(); - $owner->write(); - - // Save link into owner - $field->setValue($link->ID); - $field->saveInto($owner); - // Get the link again - the new values are in the DB. - $link = Link::get()->byID($link->ID); - - // Validate - $this->assertSame($link->ID, $owner->LinkID); - $this->assertSame($owner->ID, $link->OwnerID); - $this->assertSame($owner->ClassName, $link->OwnerClass); - $this->assertSame('Link', $link->OwnerRelation); - } -}