From f9e765bd8004ce5857980167c8542e1505c7d45c Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Thu, 21 Dec 2023 17:24:26 +1300 Subject: [PATCH] Merge pull request #144 from creative-commoners/pulls/4/remove-menu ENH Remove LinkFieldController from cms menu --- _config.php | 8 +- babel.config.json | 6 + client/dist/js/bundle.js | 2 +- client/dist/styles/bundle.css | 2 +- client/lang/src/en.json | 3 +- client/src/components/LinkField/LinkField.js | 9 +- .../src/components/LinkPicker/LinkPicker.js | 16 +- .../src/components/LinkPicker/LinkPicker.scss | 6 + .../components/LinkPicker/LinkPickerMenu.js | 19 +- .../components/LinkPicker/LinkPickerTitle.js | 16 +- .../LinkPicker/tests/LinkPicker-test.js | 31 +++ .../LinkPicker/tests/LinkPickerTitle-test.js | 34 +++ client/src/containers/LinkModalContainer.js | 3 +- client/src/entwine/LinkField.js | 1 + package.json | 2 + src/Controllers/LinkFieldController.php | 1 + src/Form/LinkField.php | 10 + src/Form/MultiLinkField.php | 5 + src/Form/Traits/AllowedLinkClassesTrait.php | 3 + src/Form/Traits/LinkFieldGetOwnerTrait.php | 21 ++ src/Models/FileLink.php | 14 +- src/Models/SiteTreeLink.php | 17 +- .../Controllers/LinkFieldControllerTest.php | 259 ++++++++++-------- tests/php/Models/FileLinkTest.php | 38 +++ .../Models/FileLinkTest/TestFileCanView.php | 14 + .../FileLinkTest/TestFileCannotView.php | 14 + tests/php/Models/FileLinkTest/file-a.png | Bin 0 -> 3083 bytes tests/php/Models/FileLinkTest/file-b.png | Bin 0 -> 3083 bytes tests/php/Models/SiteTreeLinkTest.php | 48 ++++ .../SiteTreeLinkTest/TestSiteTreeCanView.php | 14 + .../TestSiteTreeCannotView.php | 14 + .../Traits/AllowedLinkClassesTraitTest.php | 23 +- yarn.lock | 85 +++++- 33 files changed, 589 insertions(+), 149 deletions(-) create mode 100644 babel.config.json create mode 100644 client/src/components/LinkPicker/tests/LinkPicker-test.js create mode 100644 client/src/components/LinkPicker/tests/LinkPickerTitle-test.js create mode 100644 src/Form/Traits/LinkFieldGetOwnerTrait.php create mode 100644 tests/php/Models/FileLinkTest.php create mode 100644 tests/php/Models/FileLinkTest/TestFileCanView.php create mode 100644 tests/php/Models/FileLinkTest/TestFileCannotView.php create mode 100644 tests/php/Models/FileLinkTest/file-a.png create mode 100644 tests/php/Models/FileLinkTest/file-b.png create mode 100644 tests/php/Models/SiteTreeLinkTest.php create mode 100644 tests/php/Models/SiteTreeLinkTest/TestSiteTreeCanView.php create mode 100644 tests/php/Models/SiteTreeLinkTest/TestSiteTreeCannotView.php diff --git a/_config.php b/_config.php index 71c7914c..d4758a88 100644 --- a/_config.php +++ b/_config.php @@ -1,8 +1,6 @@ {(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)),o=u(n(852)),i=u(n(117)),a=u(n(606));function u(e){return e&&e.__esModule?e:{default:e}}var s=()=>{r.default.component.registerMany({LinkPicker:l.default,LinkField:o.default,"LinkModal.FormBuilderModal":i.default,"LinkModal.InsertMediaModal":a.default})};t.default=s},852:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=_(n(363)),l=n(827),o=n(624),i=(n(648),m(n(42))),a=m(n(809)),u=m(n(734)),s=m(n(686)),d=m(n(697)),f=_(n(123)),c=m(n(159)),p=m(n(510)),y=m(n(86)),v=m(n(754));function m(e){return e&&e.__esModule?e:{default:e}}function k(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(k=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=k(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=l?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}const h="SilverStripe\\LinkField\\Controllers\\LinkFieldController",O=e=>{var t;let{value:n=null,onChange:l,types:o=[],actions:i,isMulti:s=!1}=e;const[f,y]=(0,r.useState)({}),[m,k]=(0,r.useState)(0);let _=n;Array.isArray(_)||("number"==typeof _&&0!=_&&(_=[_]),_||(_=[])),(0,r.useEffect)((()=>{if(!m&&_.length>0){const e=[];for(const t of _)e.push(`itemIDs[]=${t}`);const t=`${p.default.getSection(h).form.linkForm.dataUrl}?${e.join("&")}`;c.default.get(t).then((e=>e.json())).then((e=>{y(e)}))}}),[m,n&&n.length]);const O=()=>{k(0)},g=e=>{k(0);const t=[..._];t.includes(e)||t.push(e),l(s?t:t[0]),i.toasts.success(v.default._t("LinkField.SAVE_SUCCESS","Saved link"))},b=e=>{const t=`${p.default.getSection(h).form.linkForm.deleteUrl}/${e}`;c.default.delete(t,{},{"X-SecurityID":p.default.get("SecurityID")}).then((()=>{i.toasts.success(v.default._t("LinkField.DELETE_SUCCESS","Deleted link"))})).catch((()=>{i.toasts.error(v.default._t("LinkField.DELETE_ERROR","Failed to delete link"))}));const n={...f};delete n[e],y(n),l(s?Object.keys(n):0)},M=s||0===Object.keys(f).length,j=Boolean(m);return r.default.createElement(r.default.Fragment,null,M&&r.default.createElement(a.default,{onModalSuccess:g,onModalClosed:O,types:o}),r.default.createElement("div",null," ",(()=>{const e=[];for(const s of _){var t,n,l,i,a;if(!f[s])continue;const d=o.hasOwnProperty(null===(t=f[s])||void 0===t?void 0:t.typeKey)?o[null===(n=f[s])||void 0===n?void 0:n.typeKey]:{};e.push(r.default.createElement(u.default,{key:s,id:s,title:null===(l=f[s])||void 0===l?void 0:l.Title,description:null===(i=f[s])||void 0===i?void 0:i.description,versionState:null===(a=f[s])||void 0===a?void 0:a.versionState,typeTitle:d.title||"",onClear:b,onClick:()=>{k(s)}}))}return e})()," "),j&&r.default.createElement(d.default,{types:o,typeKey:null===(t=f[m])||void 0===t?void 0:t.typeKey,isOpen:Boolean(m),onSuccess:g,onClosed:O,linkID:m}))};O.propTypes={value:y.default.oneOfType([y.default.arrayOf(y.default.number),y.default.number]),onChange:y.default.func.isRequired,types:y.default.objectOf(s.default).isRequired,actions:y.default.object.isRequired,isMulti:y.default.bool};var g=(0,l.compose)(i.default,(0,o.connect)(null,(e=>({actions:{toasts:(0,l.bindActionCreators)(f,e)}}))))(O);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 o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=l?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)),l=s(n(475)),o=n(624),i=s(n(686)),a=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:o,actions:i,onSubmit:a,...u}=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(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 a({FileID:n,Title:r,OpenInNew:l,typeKey:t.key},"",(()=>{}))}},u))};f.propTypes={type:i.default.isRequired,editing:a.default.bool.isRequired,data:a.default.object.isRequired,actions:a.default.object.isRequired,onClick:a.default.func.isRequired};var c=(0,o.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"})}}}))(f);t.default=c},117:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=s(n(363)),l=s(n(912)),o=s(n(872)),i=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=o.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 o.default.format({...r,search:i.default.stringify(l)})},f=e=>{let{typeTitle:t,typeKey:n,linkID:o=0,isOpen:i,onSuccess:a,onClosed:u}=e;if(!n)return!1;return r.default.createElement(l.default,{title:t,isOpen:i,schemaUrl:d(n,o),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);a(t)}return Promise.resolve()},onClosed:u})};f.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 c=f;t.default=c},809:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=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=d(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=l?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)),l=s(n(86)),o=s(n(820)),i=s(n(97)),a=s(n(686)),u=s(n(697));function s(e){return e&&e.__esModule?e:{default:e}}function d(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(d=function(e){return e?n:t})(e)}const f=e=>{let{types:t,onModalSuccess:n,onModalClosed:l}=e;const[a,s]=(0,r.useState)(""),d=""!==a,f=(0,o.default)("link-picker","form-control"),c=Object.values(t);return r.default.createElement("div",{className:f},r.default.createElement(i.default,{types:c,onSelect:e=>{s(e)}}),d&&r.default.createElement(u.default,{types:t,typeKey:a,isOpen:d,onSuccess:e=>{s(""),n(e)},onClosed:()=>{"function"==typeof l&&l(),s("")}}))};t.Component=f,f.propTypes={types:l.default.objectOf(a.default).isRequired,onModalSuccess:l.default.func.isRequired,onModalClosed:l.default.func};var c=f;t.default=c},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 o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=l?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=s(n(86)),i=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[o,a]=(0,l.useState)(!1);return l.default.createElement(i.Dropdown,{isOpen:o,toggle:()=>a((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:o.default.arrayOf(a.default).isRequired,onSelect:o.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=u(n(820)),l=u(n(754)),o=u(n(363)),i=u(n(86)),a=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:f,onClick:c}=e;const p={"link-picker__link":!0,"form-control":!0};u&&(p[` link-picker__link--${u}`]=!0),n&&n.length>25&&(n=n.substring(0,25)+"...");const y=(0,r.default)(p);return o.default.createElement("div",{className:y},o.default.createElement(a.Button,{className:"link-picker__button font-icon-link",color:"secondary",onClick:s(c)},o.default.createElement("div",{className:"link-picker__link-detail"},o.default.createElement("div",{className:"link-picker__title"},o.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 o.default.createElement("span",{className:i,title:t},n)})(u)),o.default.createElement("small",{className:"link-picker__type"},d,": ",o.default.createElement("span",{className:"link-picker__url"},i)))),o.default.createElement(a.Button,{className:"link-picker__clear",color:"link",onClick:s((()=>f(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};var f=d;t.default=f},697:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=a(n(363)),l=n(648),o=a(n(86)),i=a(n(686));function a(e){return e&&e.__esModule?e:{default:e}}const u=e=>{let{types:t,typeKey:n,linkID:o=0,isOpen:i,onSuccess:a,onClosed:u}=e;if(!n)return!1;const s=t.hasOwnProperty(n)?t[n]:{},d=s&&s.hasOwnProperty("handlerName")?s.handlerName:"FormBuilderModal",f=(0,l.loadComponent)(`LinkModal.${d}`);return r.default.createElement(f,{typeTitle:s.title||"",typeKey:n,linkID:o,isOpen:i,onSuccess:a,onClosed:u})};u.propTypes={types:o.default.objectOf(i.default).isRequired,typeKey:o.default.string.isRequired,linkID:o.default.number,isOpen:o.default.bool.isRequired,onSuccess:o.default.func.isRequired,onClosed:o.default.func.isRequired};var s=u;t.default=s},41:function(e,t,n){var r=a(n(311)),l=a(n(363)),o=a(n(691)),i=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-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(o.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")??[]}},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 o=l.default.shape({key:l.default.string.isRequired,title:l.default.string.isRequired});t.default=o},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 o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n(274),n(41)}(); \ No newline at end of file +!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 diff --git a/client/dist/styles/bundle.css b/client/dist/styles/bundle.css index 4abdf5cf..e466a480 100644 --- a/client/dist/styles/bundle.css +++ b/client/dist/styles/bundle.css @@ -1 +1 @@ -.link-picker__link,.link-picker{display:flex;height:auto;width:100%;min-height:54px;background:#fff;padding:0}.link-picker{align-items:stretch;cursor:pointer;box-shadow:none}.link-picker:not(:last-child){margin-bottom:10px}.link-picker.font-icon-link::before{margin:.76925rem}.link-picker__menu{flex-grow:1}.link-picker__menu-toggle{width:100%;height:100%;text-align:left}.link-picker__menu-toggle::before{padding:.76925rem}.link-picker__link{align-items:center;text-align:left;margin-right:0;justify-content:space-between;position:relative}.link-picker__link:not(:last-child){border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}.link-picker__link:not(:first-child){border-top:0;border-top-left-radius:0;border-top-right-radius:0}.link-picker__link:hover,.link-picker__link:focus{background:#eef0f4;text-decoration:none;color:inherit}.link-picker__link::before{top:29px;left:32px;content:" ";position:absolute;border:1px solid #cf3f00;border-radius:100%;bottom:6px;box-shadow:0 0 1px .5px #fff;display:block;height:8px;width:8px;z-index:1}.link-picker__link--draft::before{background-color:#ff7f22}.link-picker__link--modified::before{background-color:#fff7f0}.link-picker__link--unsaved::before,.link-picker__link--published::before{display:none}.link-picker__button{display:flex;align-items:center;flex-grow:1;height:100%;text-align:left;border:none;margin-right:0}.link-picker__button::before{font-size:1.231rem;padding:.76925rem;margin-right:6px;flex-grow:0}.link-picker__link-detail{flex-grow:1}.link-picker__clear{flex-grow:0}.link-picker__url{color:#0071c4}.link-picker__title-text{margin-right:5px}.link-picker__title .badge{color:#cf3f00;background-color:#fff2ea;padding:2px 3px 2px 4px} +.link-picker__link,.link-picker{display:flex;height:auto;width:100%;min-height:54px;background:#fff;padding:0}.link-picker{align-items:stretch;cursor:pointer;box-shadow:none}.link-picker:not(:last-child){margin-bottom:10px}.link-picker.font-icon-link::before{margin:.76925rem}.link-picker__cannot-create{cursor:default;flex-grow:1;padding:16px 13px}.link-picker__menu{flex-grow:1}.link-picker__menu-toggle{width:100%;height:100%;text-align:left}.link-picker__menu-toggle::before{padding:.76925rem}.link-picker__link{align-items:center;text-align:left;margin-right:0;justify-content:space-between;position:relative}.link-picker__link:not(:last-child){border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}.link-picker__link:not(:first-child){border-top:0;border-top-left-radius:0;border-top-right-radius:0}.link-picker__link:hover,.link-picker__link:focus{background:#eef0f4;text-decoration:none;color:inherit}.link-picker__link::before{top:29px;left:32px;content:" ";position:absolute;border:1px solid #cf3f00;border-radius:100%;bottom:6px;box-shadow:0 0 1px .5px #fff;display:block;height:8px;width:8px;z-index:1}.link-picker__link--draft::before{background-color:#ff7f22}.link-picker__link--modified::before{background-color:#fff7f0}.link-picker__link--unsaved::before,.link-picker__link--published::before{display:none}.link-picker__button{display:flex;align-items:center;flex-grow:1;height:100%;text-align:left;border:none;margin-right:0}.link-picker__button::before{font-size:1.231rem;padding:.76925rem;margin-right:6px;flex-grow:0}.link-picker__link-detail{flex-grow:1}.link-picker__clear{flex-grow:0}.link-picker__url{color:#0071c4}.link-picker__title-text{margin-right:5px}.link-picker__title .badge{color:#cf3f00;background-color:#fff2ea;padding:2px 3px 2px 4px} diff --git a/client/lang/src/en.json b/client/lang/src/en.json index daf0a94a..1e7a2bf5 100644 --- a/client/lang/src/en.json +++ b/client/lang/src/en.json @@ -7,5 +7,6 @@ "LinkField.LINK_DRAFT_TITLE": "Link has draft changes", "LinkField.LINK_DRAFT_LABEL": "Draft", "LinkField.LINK_MODIFIED_TITLE": "Link has unpublished changes", - "LinkField.LINK_MODIFIED_LABEL": "Modified" + "LinkField.LINK_MODIFIED_LABEL": "Modified", + "LinkField.CANNOT_CREATE_LINK": "Cannot create link" } diff --git a/client/src/components/LinkField/LinkField.js b/client/src/components/LinkField/LinkField.js index d4b493dc..97d886b6 100644 --- a/client/src/components/LinkField/LinkField.js +++ b/client/src/components/LinkField/LinkField.js @@ -23,8 +23,9 @@ 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 */ -const LinkField = ({ value = null, onChange, types = [], actions, isMulti = false }) => { +const LinkField = ({ value = null, onChange, types = [], actions, isMulti = false, canCreate }) => { const [data, setData] = useState({}); const [editingID, setEditingID] = useState(0); @@ -145,6 +146,7 @@ const LinkField = ({ value = null, onChange, types = [], actions, isMulti = fals typeTitle={type.title || ''} onClear={onClear} onClick={() => { setEditingID(linkID); }} + canDelete={data[linkID]?.canDelete ? true : false} />); } return links; @@ -154,7 +156,7 @@ const LinkField = ({ value = null, onChange, types = [], actions, isMulti = fals const renderModal = Boolean(editingID); return <> - { renderPicker && } + { renderPicker && }
{ renderLinks() }
{ renderModal && { +const LinkPicker = ({ types, onModalSuccess, onModalClosed, canCreate }) => { const [typeKey, setTypeKey] = useState(''); /** @@ -41,6 +42,16 @@ const LinkPicker = ({ types, onModalSuccess, onModalClosed }) => { const className = classnames('link-picker', 'form-control'); const typeArray = Object.values(types); + if (!canCreate) { + return ( +
+
+ {i18n._t('LinkField.CANNOT_CREATE_LINK', 'Cannot create link')} +
+
+ ); + } + return (
@@ -57,9 +68,10 @@ const LinkPicker = ({ types, onModalSuccess, onModalClosed }) => { }; LinkPicker.propTypes = { - types: PropTypes.objectOf(LinkType).isRequired, + types: PropTypes.array.isRequired, onModalSuccess: PropTypes.func.isRequired, onModalClosed: PropTypes.func, + canCreate: PropTypes.bool.isRequired }; export {LinkPicker as Component}; diff --git a/client/src/components/LinkPicker/LinkPicker.scss b/client/src/components/LinkPicker/LinkPicker.scss index b2c913ce..3126e0ff 100644 --- a/client/src/components/LinkPicker/LinkPicker.scss +++ b/client/src/components/LinkPicker/LinkPicker.scss @@ -24,6 +24,12 @@ } } +.link-picker__cannot-create { + cursor: default; + flex-grow: 1; + padding: 16px 13px; +} + .link-picker__menu { flex-grow: 1; } diff --git a/client/src/components/LinkPicker/LinkPickerMenu.js b/client/src/components/LinkPicker/LinkPickerMenu.js index 75eb38ac..61c1d978 100644 --- a/client/src/components/LinkPicker/LinkPickerMenu.js +++ b/client/src/components/LinkPicker/LinkPickerMenu.js @@ -8,26 +8,25 @@ import LinkType from 'types/LinkType'; const LinkPickerMenu = ({ types, onSelect }) => { const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(prevState => !prevState); - - return ( - - {i18n._t('LinkField.ADD_LINK', 'Add Link')} + return + + {i18n._t('LinkField.ADD_LINK', 'Add Link')} + {types.map(({key, title}) => onSelect(key)}>{title} )} - ); }; LinkPickerMenu.propTypes = { types: PropTypes.arrayOf(LinkType).isRequired, - onSelect: PropTypes.func.isRequired + onSelect: PropTypes.func.isRequired, }; export default LinkPickerMenu; diff --git a/client/src/components/LinkPicker/LinkPickerTitle.js b/client/src/components/LinkPicker/LinkPickerTitle.js index ee9852f3..a2d3a290 100644 --- a/client/src/components/LinkPicker/LinkPickerTitle.js +++ b/client/src/components/LinkPicker/LinkPickerTitle.js @@ -29,7 +29,16 @@ const getVersionedBadge = (versionState) => { return {label}; }; -const LinkPickerTitle = ({ id, title, description, versionState, typeTitle, onClear, onClick }) => { +const LinkPickerTitle = ({ + id, + title, + description, + versionState, + typeTitle, + onClear, + onClick, + canDelete +}) => { const classes = { 'link-picker__link': true, 'form-control': true, @@ -54,7 +63,9 @@ const LinkPickerTitle = ({ id, title, description, versionState, typeTitle, onCl
- + {canDelete && + + } }; @@ -66,6 +77,7 @@ LinkPickerTitle.propTypes = { typeTitle: PropTypes.string.isRequired, onClear: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired, + canDelete: PropTypes.bool.isRequired, }; export default LinkPickerTitle; diff --git a/client/src/components/LinkPicker/tests/LinkPicker-test.js b/client/src/components/LinkPicker/tests/LinkPicker-test.js new file mode 100644 index 00000000..cac12c6b --- /dev/null +++ b/client/src/components/LinkPicker/tests/LinkPicker-test.js @@ -0,0 +1,31 @@ +/* global jest, test */ +import React from 'react'; +import { render } from '@testing-library/react'; +import LinkPicker from '../LinkPicker'; + +function makeProps(obj = {}) { + return { + types: [{ key: 'phone', title: 'Phone' }], + onModalSuccess: () => {}, + onModalClosed: () => {}, + ...obj + }; +} + +test('LinkPickerMenu render() should display toggle if can create', () => { + const { container } = render(); + expect(container.querySelectorAll('.link-picker__menu-toggle')).toHaveLength(1); + expect(container.querySelectorAll('.link-picker__cannot-create')).toHaveLength(0); +}); + +test('LinkPickerMenu render() should display cannot create message if cannot create', () => { + const { container } = render(); + expect(container.querySelectorAll('.link-picker__menu-toggle')).toHaveLength(0); + expect(container.querySelectorAll('.link-picker__cannot-create')).toHaveLength(1); +}); diff --git a/client/src/components/LinkPicker/tests/LinkPickerTitle-test.js b/client/src/components/LinkPicker/tests/LinkPickerTitle-test.js new file mode 100644 index 00000000..b45c9b00 --- /dev/null +++ b/client/src/components/LinkPicker/tests/LinkPickerTitle-test.js @@ -0,0 +1,34 @@ +/* global jest, test */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import LinkPickerTitle from '../LinkPickerTitle'; + +function makeProps(obj = {}) { + return { + id: 1, + title: 'My title', + description: 'My description', + versionState: 'draft', + typeTitle: 'Phone', + onClear: () => {}, + onClick: () => {}, + ...obj + }; +} + +test('LinkPickerTitle render() should display clear button if can delete', () => { + const { container } = render(); + expect(container.querySelectorAll('.link-picker__clear')).toHaveLength(1); +}); + +test('LinkPickerTitle render() should not display clear button if cannot delete', () => { + const { container } = render(); + expect(container.querySelectorAll('.link-picker__clear')).toHaveLength(0); +}); diff --git a/client/src/containers/LinkModalContainer.js b/client/src/containers/LinkModalContainer.js index 5232a378..11299475 100644 --- a/client/src/containers/LinkModalContainer.js +++ b/client/src/containers/LinkModalContainer.js @@ -2,7 +2,6 @@ import React from 'react'; import { loadComponent } from 'lib/Injector'; import PropTypes from 'prop-types'; -import LinkType from 'types/LinkType'; /** * Contains the LinkModal and determines which modal component to render based on the link type. @@ -29,7 +28,7 @@ const LinkModalContainer = ({ types, typeKey, linkID = 0, isOpen, onSuccess, onC } LinkModalContainer.propTypes = { - types: PropTypes.objectOf(LinkType).isRequired, + types: PropTypes.array.isRequired, typeKey: PropTypes.string.isRequired, linkID: PropTypes.number, isOpen: PropTypes.bool.isRequired, diff --git a/client/src/entwine/LinkField.js b/client/src/entwine/LinkField.js index ccd45f9f..c3b34fd2 100644 --- a/client/src/entwine/LinkField.js +++ b/client/src/entwine/LinkField.js @@ -51,6 +51,7 @@ jQuery.entwine('ss', ($) => { onChange: this.handleChange.bind(this), isMulti: this.data('is-multi') ?? false, types: this.data('types') ?? [], + canCreate: this.getInputField().data('can-create') ?? false, }; }, diff --git a/package.json b/package.json index 80feff3b..817804aa 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "lint-sass": "sass-lint client/src" }, "jest": { + "testEnvironment": "jsdom", "roots": [ "client/src" ], @@ -51,6 +52,7 @@ "@babel/runtime": "^7.20.0", "@silverstripe/eslint-config": "^1.0.0", "@silverstripe/webpack-config": "^2.0.0", + "@testing-library/react": "^14.0.0", "babel-jest": "^29.2.2", "jest-cli": "^29.2.2", "jest-environment-jsdom": "^29.3.1", diff --git a/src/Controllers/LinkFieldController.php b/src/Controllers/LinkFieldController.php index 27ff9db5..f583728d 100644 --- a/src/Controllers/LinkFieldController.php +++ b/src/Controllers/LinkFieldController.php @@ -113,6 +113,7 @@ private function getLinkData(Link $link): array $this->jsonError(403, _t('LinkField.UNAUTHORIZED', 'Unauthorized')); } $data = $link->jsonSerialize(); + $data['canDelete'] = $link->canDelete(); $data['description'] = $link->getDescription(); $data['versionState'] = $link->getVersionedState(); return $data; diff --git a/src/Form/LinkField.php b/src/Form/LinkField.php index 62ffbe12..3c538193 100644 --- a/src/Form/LinkField.php +++ b/src/Form/LinkField.php @@ -8,6 +8,7 @@ use SilverStripe\ORM\DataObjectInterface; use SilverStripe\LinkField\Models\Link; use SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait; +use SilverStripe\LinkField\Form\Traits\LinkFieldGetOwnerTrait; /** * Allows CMS users to edit a Link object. @@ -15,6 +16,7 @@ class LinkField extends FormField { use AllowedLinkClassesTrait; + use LinkFieldGetOwnerTrait; protected $schemaComponent = 'LinkField'; @@ -60,10 +62,18 @@ public function saveInto(DataObjectInterface $record) return $this; } + public function getSchemaStateDefaults() + { + $data = parent::getSchemaStateDefaults(); + $data['canCreate'] = $this->getOwner()->canEdit(); + return $data; + } + protected function getDefaultAttributes(): array { $attributes = parent::getDefaultAttributes(); $attributes['data-value'] = $this->Value(); + $attributes['data-can-create'] = $this->getOwner()->canEdit(); return $attributes; } diff --git a/src/Form/MultiLinkField.php b/src/Form/MultiLinkField.php index 76632909..ea8fbfef 100644 --- a/src/Form/MultiLinkField.php +++ b/src/Form/MultiLinkField.php @@ -11,6 +11,8 @@ use SilverStripe\ORM\RelationList; use SilverStripe\ORM\SS_List; use SilverStripe\ORM\UnsavedRelationList; +use SilverStripe\LinkField\Form\Traits\LinkFieldGetOwnerTrait; +use SilverStripe\LinkField\Models\Link; /** * Allows CMS users to edit a Link object. @@ -18,6 +20,7 @@ class MultiLinkField extends FormField { use AllowedLinkClassesTrait; + use LinkFieldGetOwnerTrait; protected $schemaComponent = 'LinkField'; @@ -72,6 +75,7 @@ public function getSchemaStateDefaults() { $data = parent::getSchemaStateDefaults(); $data['value'] = $this->getValueArray(); + $data['canCreate'] = $this->getOwner()->canEdit(); return $data; } @@ -79,6 +83,7 @@ protected function getDefaultAttributes(): array { $attributes = parent::getDefaultAttributes(); $attributes['data-value'] = $this->getValueArray(); + $attributes['data-can-create'] = $this->getOwner()->canEdit(); return $attributes; } diff --git a/src/Form/Traits/AllowedLinkClassesTrait.php b/src/Form/Traits/AllowedLinkClassesTrait.php index d704a81f..d21fef27 100644 --- a/src/Form/Traits/AllowedLinkClassesTrait.php +++ b/src/Form/Traits/AllowedLinkClassesTrait.php @@ -88,6 +88,9 @@ public function getTypesProps(): string $typeDefinitions = $this->genarateAllowedTypes(); foreach ($typeDefinitions as $key => $class) { $type = Injector::inst()->get($class); + if (!$type->canCreate()) { + continue; + } $typesList[$key] = [ 'key' => $key, 'title' => $type->i18n_singular_name(), diff --git a/src/Form/Traits/LinkFieldGetOwnerTrait.php b/src/Form/Traits/LinkFieldGetOwnerTrait.php new file mode 100644 index 00000000..0b12bd2a --- /dev/null +++ b/src/Form/Traits/LinkFieldGetOwnerTrait.php @@ -0,0 +1,21 @@ +getForm(); + $owner = $form->getRecord(); + if (!$owner) { + throw new LogicException('Could not determine owner from form'); + } + return $owner; + } +} diff --git a/src/Models/FileLink.php b/src/Models/FileLink.php index a0b5051e..0d118b8e 100644 --- a/src/Models/FileLink.php +++ b/src/Models/FileLink.php @@ -30,7 +30,14 @@ public function getCMSFields(): FieldList public function getDescription(): string { - return $this->File()?->getFilename() ?? ''; + $file = $this->File(); + if (!$file?->exists()) { + return _t(__CLASS__ . '.FILE_DOES_NOT_EXIST', 'File does not exist'); + } + if (!$file->canView()) { + return _t(__CLASS__ . '.CANNOT_VIEW_FILE', 'Cannot view file'); + } + return $file->getFilename() ?? ''; } public function getURL(): string @@ -43,10 +50,7 @@ public function getDefaultTitle(): string { $file = $this->File(); if (!$file->exists()) { - return _t( - static::class . '.MISSING_DEFAULT_TITLE', - 'File missing', - ); + return _t(__CLASS__ . '.MISSING_DEFAULT_TITLE', 'File missing'); } return (string) $this->getDescription(); diff --git a/src/Models/SiteTreeLink.php b/src/Models/SiteTreeLink.php index 9dcf9a90..95dce8b1 100644 --- a/src/Models/SiteTreeLink.php +++ b/src/Models/SiteTreeLink.php @@ -32,7 +32,14 @@ class SiteTreeLink extends Link public function getDescription(): string { - return $this->Page()?->URLSegment ?? ''; + $page = $this->Page(); + if (!$page?->exists()) { + return _t(__CLASS__ . '.PAGE_DOES_NOT_EXIST', 'Page does not exist'); + } + if (!$page->canView()) { + return _t(__CLASS__ . '.CANNOT_VIEW_PAGE', 'Cannot view page'); + } + return $page->URLSegment ?? ''; } public function getCMSFields(): FieldList @@ -113,15 +120,15 @@ public function getURL(): string public function getDefaultTitle(): string { $page = $this->Page(); - $pageExist = $this->Page()->exists(); - - if (!$pageExist) { + if (!$page->exists()) { return _t( static::class . '.MISSING_DEFAULT_TITLE', 'Page missing', ); } - + if (!$page->canView()) { + return ''; + } return $page->Title; } } diff --git a/tests/php/Controllers/LinkFieldControllerTest.php b/tests/php/Controllers/LinkFieldControllerTest.php index 93e49e1b..d8372934 100644 --- a/tests/php/Controllers/LinkFieldControllerTest.php +++ b/tests/php/Controllers/LinkFieldControllerTest.php @@ -6,6 +6,7 @@ use SilverStripe\LinkField\Tests\Controllers\LinkFieldControllerTest\TestPhoneLink; use SilverStripe\Core\Config\Config; use SilverStripe\Security\SecurityToken; +use SilverStripe\Control\HTTPRequest; class LinkFieldControllerTest extends FunctionalTest { @@ -220,15 +221,15 @@ public function provideLinkFormPost(): array // note: not duplicating code paths already tested with provideLinkFormGetSchema() // e.g. Reject Invalid ID return [ - // 'Valid update existing record' => [ - // 'idType' => 'existing', - // 'typeKey' => 'testphone', - // 'dataType' => 'valid', - // 'fail' => '', - // 'expectedCode' => 200, - // 'expectedMessage' => '', - // 'expectedLinkType' => 'existing', - // ], + 'Valid update existing record' => [ + 'idType' => 'existing', + 'typeKey' => 'testphone', + 'dataType' => 'valid', + 'fail' => '', + 'expectedCode' => 200, + 'expectedMessage' => '', + 'expectedLinkType' => 'existing', + ], 'Valid create new record' => [ 'idType' => 'new-record', 'typeKey' => 'testphone', @@ -238,105 +239,147 @@ public function provideLinkFormPost(): array 'expectedMessage' => '', 'expectedLinkType' => 'new-record', ], - // 'Invalid validate()' => [ - // 'idType' => 'existing', - // 'typeKey' => 'testphone', - // 'dataType' => 'valid', - // 'fail' => 'validate', - // 'expectedCode' => 200, - // 'expectedMessage' => 'Fail was validate', - // 'expectedLinkType' => 'existing', - // ], - // 'Invalid getCMSCompositeValidator()' => [ - // 'idType' => 'existing', - // 'typeKey' => 'testphone', - // 'dataType' => 'valid', - // 'fail' => 'cms-composite-validator', - // 'expectedCode' => 200, - // 'expectedMessage' => 'Fail was cms-composite-validator', - // 'expectedLinkType' => 'existing', - // ], - // 'Reject invalid ID' => [ - // 'idType' => 'invalid', - // 'typeKey' => 'testphone', - // 'dataType' => 'valid', - // 'fail' => '', - // 'expectedCode' => 404, - // 'expectedMessage' => 'Invalid ID', - // 'expectedLinkType' => '', - // ], - // 'Reject missing ID' => [ - // 'idType' => 'missing', - // 'typeKey' => 'testphone', - // 'dataType' => 'valid', - // 'fail' => '', - // 'expectedCode' => 404, - // 'expectedMessage' => 'Invalid ID', - // 'expectedLinkType' => '', - // ], - // 'Reject non-numeric ID' => [ - // 'idType' => 'non-numeric', - // 'typeKey' => 'testphone', - // 'dataType' => 'valid', - // 'fail' => '', - // 'expectedCode' => 404, - // 'expectedMessage' => 'Invalid ID', - // 'expectedLinkType' => '', - // ], - // 'Reject invalid typeKey for new record' => [ - // 'idType' => 'new-record', - // 'typeKey' => 'donut', - // 'dataType' => 'valid', - // 'fail' => '', - // 'expectedCode' => 404, - // 'expectedMessage' => 'Invalid typeKey', - // 'expectedLinkType' => '', - // ], - // 'Reject empty data' => [ - // 'idType' => 'existing', - // 'typeKey' => 'testphone', - // 'dataType' => 'empty', - // 'fail' => '', - // 'expectedCode' => 400, - // 'expectedMessage' => 'Empty data', - // 'expectedLinkType' => '', - // ], - // 'Reject invalid-id data' => [ - // 'idType' => 'existing', - // 'typeKey' => 'testphone', - // 'dataType' => 'invalid-id', - // 'fail' => '', - // 'expectedCode' => 400, - // 'expectedMessage' => 'Bad data', - // 'expectedLinkType' => '', - // ], - // 'Reject fail csrf-token' => [ - // 'idType' => 'existing', - // 'typeKey' => 'testphone', - // 'dataType' => 'valid', - // 'fail' => 'csrf-token', - // 'expectedCode' => 400, - // 'expectedMessage' => 'Invalid CSRF token', - // 'expectedLinkType' => '', - // ], - // 'Reject fail canEdit() check existing record' => [ - // 'idType' => 'existing', - // 'typeKey' => 'testphone', - // 'dataType' => 'valid', - // 'fail' => 'can-edit', - // 'expectedCode' => 403, - // 'expectedMessage' => 'Unauthorized', - // 'expectedLinkType' => '', - // ], - // 'Reject fail canCreate() check new record' => [ - // 'idType' => 'new-record', - // 'typeKey' => 'testphone', - // 'dataType' => 'valid', - // 'fail' => 'can-create', - // 'expectedCode' => 403, - // 'expectedMessage' => 'Unauthorized', - // 'expectedLinkType' => '', - // ], + 'Invalid validate()' => [ + 'idType' => 'existing', + 'typeKey' => 'testphone', + 'dataType' => 'valid', + 'fail' => 'validate', + 'expectedCode' => 200, + 'expectedMessage' => 'Fail was validate', + 'expectedLinkType' => 'existing', + ], + 'Invalid getCMSCompositeValidator()' => [ + 'idType' => 'existing', + 'typeKey' => 'testphone', + 'dataType' => 'valid', + 'fail' => 'cms-composite-validator', + 'expectedCode' => 200, + 'expectedMessage' => 'Fail was cms-composite-validator', + 'expectedLinkType' => 'existing', + ], + 'Reject invalid ID' => [ + 'idType' => 'invalid', + 'typeKey' => 'testphone', + 'dataType' => 'valid', + 'fail' => '', + 'expectedCode' => 404, + 'expectedMessage' => 'Invalid ID', + 'expectedLinkType' => '', + ], + 'Reject missing ID' => [ + 'idType' => 'missing', + 'typeKey' => 'testphone', + 'dataType' => 'valid', + 'fail' => '', + 'expectedCode' => 404, + 'expectedMessage' => 'Invalid ID', + 'expectedLinkType' => '', + ], + 'Reject non-numeric ID' => [ + 'idType' => 'non-numeric', + 'typeKey' => 'testphone', + 'dataType' => 'valid', + 'fail' => '', + 'expectedCode' => 404, + 'expectedMessage' => 'Invalid ID', + 'expectedLinkType' => '', + ], + 'Reject invalid typeKey for new record' => [ + 'idType' => 'new-record', + 'typeKey' => 'donut', + 'dataType' => 'valid', + 'fail' => '', + 'expectedCode' => 404, + 'expectedMessage' => 'Invalid typeKey', + 'expectedLinkType' => '', + ], + 'Reject empty data' => [ + 'idType' => 'existing', + 'typeKey' => 'testphone', + 'dataType' => 'empty', + 'fail' => '', + 'expectedCode' => 400, + 'expectedMessage' => 'Empty data', + 'expectedLinkType' => '', + ], + 'Reject invalid-id data' => [ + 'idType' => 'existing', + 'typeKey' => 'testphone', + 'dataType' => 'invalid-id', + 'fail' => '', + 'expectedCode' => 400, + 'expectedMessage' => 'Bad data', + 'expectedLinkType' => '', + ], + 'Reject fail csrf-token' => [ + 'idType' => 'existing', + 'typeKey' => 'testphone', + 'dataType' => 'valid', + 'fail' => 'csrf-token', + 'expectedCode' => 400, + 'expectedMessage' => 'Invalid CSRF token', + 'expectedLinkType' => '', + ], + 'Reject fail canEdit() check existing record' => [ + 'idType' => 'existing', + 'typeKey' => 'testphone', + 'dataType' => 'valid', + 'fail' => 'can-edit', + 'expectedCode' => 403, + 'expectedMessage' => 'Unauthorized', + 'expectedLinkType' => '', + ], + 'Reject fail canCreate() check new record' => [ + 'idType' => 'new-record', + 'typeKey' => 'testphone', + 'dataType' => 'valid', + 'fail' => 'can-create', + 'expectedCode' => 403, + 'expectedMessage' => 'Unauthorized', + 'expectedLinkType' => '', + ], + ]; + } + + /** + * @dataProvider provideLinkFormReadOnly + */ + public function testLinkFormReadonly(string $idType, string $fail, bool $expected): void + { + TestPhoneLink::$fail = $fail; + $id = $this->getID($idType); + $typeKey = 'testphone'; + $url = "/admin/linkfield/schema/linkForm/$id?typeKey=$typeKey"; + $headers = $this->formSchemaHeader(); + $body = $this->get($url, null, $headers)->getBody(); + $json = json_decode($body, true); + $actual = $json['schema']['fields'][0]['children'][0]['readOnly'] ?? false; + $this->assertSame($expected, $actual); + } + + public function provideLinkFormReadOnly(): array + { + return [ + [ + 'idType' => 'existing', + 'fail' => '', + "expected" => false, + ], + [ + 'idType' => 'existing', + 'fail' => 'can-edit', + "expected" => true, + ], + [ + 'idType' => 'new-record', + 'fail' => '', + "expected" => false, + ], + [ + 'idType' => 'new-record', + 'fail' => 'can-create', + "expected" => true, + ], ]; } diff --git a/tests/php/Models/FileLinkTest.php b/tests/php/Models/FileLinkTest.php new file mode 100644 index 00000000..78569990 --- /dev/null +++ b/tests/php/Models/FileLinkTest.php @@ -0,0 +1,38 @@ +assertSame('File does not exist', $link->getDescription()); + // FileLink with a page though cannot view the page + $file = new TestFileCannotView(['Name' => 'not-allowed']); + $file->setFromLocalFile(realpath(__DIR__ .'/FileLinkTest/file-a.png'), 'file-a.png'); + $file->write(); + $link->File = $file->ID; + $link->write(); + $this->assertSame('Cannot view file', $link->getDescription()); + // FileLink with a page that and can view the page + $file = new TestFileCanView(['Name' => 'allowed']); + $file->setFromLocalFile(realpath(__DIR__ .'/FileLinkTest/file-b.png'), 'file-b.png'); + $file->write(); + $link->File = $file->ID; + $link->write(); + $this->assertSame('file-b.png', $link->getDescription()); + } +} diff --git a/tests/php/Models/FileLinkTest/TestFileCanView.php b/tests/php/Models/FileLinkTest/TestFileCanView.php new file mode 100644 index 00000000..50539538 --- /dev/null +++ b/tests/php/Models/FileLinkTest/TestFileCanView.php @@ -0,0 +1,14 @@ +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 Z2LOC}I*2oeDAE7`002ovPDHLkV1nE|yGH;3 literal 0 HcmV?d00001 diff --git a/tests/php/Models/FileLinkTest/file-b.png b/tests/php/Models/FileLinkTest/file-b.png new file mode 100644 index 0000000000000000000000000000000000000000..07e62c1cc239d810ddc965f43a4761874b4afacf GIT binary patch literal 3083 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 Z2LOC}I*2oeDAE7`002ovPDHLkV1nE|yGH;3 literal 0 HcmV?d00001 diff --git a/tests/php/Models/SiteTreeLinkTest.php b/tests/php/Models/SiteTreeLinkTest.php new file mode 100644 index 00000000..618ed003 --- /dev/null +++ b/tests/php/Models/SiteTreeLinkTest.php @@ -0,0 +1,48 @@ +assertSame('Page does not exist', $link->getDescription()); + // SiteTreeLink with a page though cannot view the page + $page = new TestSiteTreeCannotView(['URLSegment' => 'test-a']); + $page->write(); + $link->Page = $page->ID; + $link->write(); + $this->assertSame('Cannot view page', $link->getDescription()); + // SiteTreeLink with a page that and can view the page + $page = new TestSiteTreeCanView(['URLSegment' => 'test-b']); + $page->write(); + $link->Page = $page->ID; + $link->write(); + $this->assertSame('test-b', $link->getDescription()); + } + + public function testGetDefaultTitle(): void + { + // Page does not exist + $link = SiteTreeLink::create(); + $this->assertSame('Page missing', $link->getDefaultTitle()); + // Page exists + $page = new TestSiteTreeCanView(['Title' => 'My test page']); + $page->write(); + $link->Page = $page->ID; + $link->write(); + $this->assertSame('My test page', $link->getDefaultTitle()); + } +} diff --git a/tests/php/Models/SiteTreeLinkTest/TestSiteTreeCanView.php b/tests/php/Models/SiteTreeLinkTest/TestSiteTreeCanView.php new file mode 100644 index 00000000..c8ab74ab --- /dev/null +++ b/tests/php/Models/SiteTreeLinkTest/TestSiteTreeCanView.php @@ -0,0 +1,14 @@ +setAllowedTypes($enabled); } - public function allowedTypesExceptionDataProvider() : array + public function provideTypesExceptionDataProvider() : array { return [ 'allow all with empty array' => [ @@ -98,4 +104,17 @@ public function allowedTypesExceptionDataProvider() : array ], ]; } + + public function testGetTypesPropsCanCreate(): void + { + $linkField = LinkField::create('LinkField'); + $linkField->setAllowedTypes([SiteTreeLink::class, TestPhoneLink::class]); + $json = json_decode($linkField->getTypesProps(), true); + $this->assertTrue(array_key_exists('sitetree', $json)); + $this->assertTrue(array_key_exists('testphone', $json)); + TestPhoneLink::$fail = 'can-create'; + $json = json_decode($linkField->getTypesProps(), true); + $this->assertTrue(array_key_exists('sitetree', $json)); + $this->assertFalse(array_key_exists('testphone', $json)); + } } diff --git a/yarn.lock b/yarn.lock index 002976da..d94c9dff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,6 +36,14 @@ dependencies: "@babel/highlight" "^7.8.3" +"@babel/code-frame@^7.10.4": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" @@ -259,6 +267,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-identifier@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" @@ -297,6 +310,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/highlight@^7.8.3": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" @@ -1735,11 +1757,39 @@ dependencies: "@sinonjs/commons" "^2.0.0" +"@testing-library/dom@^9.0.0": + version "9.3.3" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.3.tgz#108c23a5b0ef51121c26ae92eb3179416b0434f5" + integrity sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.1.3" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + +"@testing-library/react@^14.0.0": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.1.2.tgz#a2b9e9ee87721ec9ed2d7cfc51cc04e474537c32" + integrity sha512-z4p7DVBTPjKM5qDZ0t5ZjzkpSNb+fZy1u6bzO7kk8oeGagpPCAtgh4cx1syrfp7a+QWkM021jGqjJaxJJnXAZg== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^9.0.0" + "@types/react-dom" "^18.0.0" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + "@types/babel__core@^7.1.14": version "7.20.0" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" @@ -1877,6 +1927,13 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== +"@types/react-dom@^18.0.0": + version "18.2.18" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" + integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw== + dependencies: + "@types/react" "*" + "@types/react-transition-group@^4.4.0": version "4.4.5" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" @@ -2333,7 +2390,7 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^5.1.3: +aria-query@5.1.3, aria-query@^5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== @@ -2698,7 +2755,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -3267,6 +3324,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + dom-helpers@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" @@ -5822,6 +5884,11 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.1.tgz#4716408dec51d5d0104732647f584d1f6738b109" integrity sha512-8/HcIENyQnfUTCDizRu9rrDyG6XG/21M4X7/YEGZeD76ZJilFPAUVb/2zysFf7VVO1LEjCDFyHp8pMMvozIrvg== +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -6791,6 +6858,15 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + pretty-format@^29.4.3: version "29.4.3" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.3.tgz#25500ada21a53c9e8423205cf0337056b201244c" @@ -6952,6 +7028,11 @@ react-is@^16.13.1, react-is@^16.4.2, react-is@^16.7.0, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-is@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"