diff --git a/_graphql/queries.yml b/_graphql/queries.yml index 9775dadb..5ccb987a 100644 --- a/_graphql/queries.yml +++ b/_graphql/queries.yml @@ -1,5 +1,5 @@ 'readLinkDescription(dataStr: String!)': - type: LinkDescription + type: '[LinkDescription]' resolver: ['SilverStripe\LinkField\GraphQL\LinkDescriptionResolver', 'resolve'] 'readLinkTypes(keys: [ID])': type: '[LinkType]' diff --git a/_graphql/types.yml b/_graphql/types.yml index 70525b16..883394cd 100644 --- a/_graphql/types.yml +++ b/_graphql/types.yml @@ -1,6 +1,8 @@ LinkDescription: description: Given some Link data, computes the matching description fields: + id: ID + title: String description: String LinkType: @@ -9,3 +11,4 @@ LinkType: key: ID handlerName: String! title: String! + icon: String! diff --git a/client/dist/js/bundle.js b/client/dist/js/bundle.js index 8726ffc2..916f74b7 100644 --- a/client/dist/js/bundle.js +++ b/client/dist/js/bundle.js @@ -1 +1 @@ -!function(){"use strict";var e={274:function(e,t,n){i(n(510));var r=i(n(180)),a=i(n(521)),o=i(n(154));function i(e){return e&&e.__esModule?e:{default:e}}document.addEventListener("DOMContentLoaded",(()=>{(0,a.default)(),(0,o.default)(),(0,r.default)()}))},521:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(648)),a=u(n(809)),o=u(n(852)),i=u(n(117)),l=u(n(606));function u(e){return e&&e.__esModule?e:{default:e}}var d=()=>{r.default.component.registerMany({LinkPicker:a.default,LinkField:o.default,"LinkModal.FormBuilderModal":i.default,"LinkModal.InsertMediaModal":l.default})};t.default=d},154:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=i(n(648)),a=i(n(689)),o=i(n(287));function i(e){return e&&e.__esModule?e:{default:e}}var l=()=>{r.default.query.register("readLinkTypes",a.default),r.default.query.register("readLinkDescription",o.default)};t.default=l},180:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r;(r=n(648))&&r.__esModule,n(827);var a=()=>{};t.default=a},852:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,a=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=u(t);if(n&&n.has(e))return n.get(e);var r={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=a?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),o=n(827),i=n(648),l=(r=n(42))&&r.__esModule?r:{default:r};function u(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(u=function(e){return e?n:t})(e)}function d(){return d=Object.assign?Object.assign.bind():function(e){for(var t=1;tt=>{let{data:n,value:r,...o}=t,i=r||n;return"string"==typeof i&&(i=JSON.parse(i)),a.default.createElement(e,d({dataStr:JSON.stringify(i)},o,{data:i}))}),(0,i.injectGraphql)("readLinkDescription"),l.default)((e=>{let{id:t,loading:n,Loading:r,data:o,LinkPicker:l,onChange:u,types:d,linkDescription:s,...f}=e;if(n)return a.default.createElement(r,null);const[c,p]=(0,a.useState)(!1),[y,v]=(0,a.useState)(""),{typeKey:g}=o,m=d[g],k=y?d[y]:m;let _=o?o.Title:"";_||(_=o?o.TitleRelField:"");const O={title:_,link:m?{type:m,title:_,description:s}:void 0,onEdit:()=>{p(!0)},onClear:e=>{"function"==typeof u&&u(e,{id:t,value:{}})},onSelect:e=>{v(e),p(!0)},types:Object.values(d)},h={type:k,editing:c,onSubmit:(e,n,r)=>{const{SecurityID:a,action_insert:o,...i}=e;return"function"==typeof u&&u(event,{id:t,value:i}),p(!1),v(""),Promise.resolve()},onClosed:()=>{p(!1)},data:o},b=k?k.handlerName:"FormBuilderModal",j=(0,i.loadComponent)(`LinkModal.${b}`);return a.default.createElement(a.Fragment,null,a.default.createElement(l,O),a.default.createElement(j,h))}));t.default=s},606:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;i(n(754));var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=o(t);if(n&&n.has(e))return n.get(e);var r={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var i in e)if("default"!==i&&Object.prototype.hasOwnProperty.call(e,i)){var l=a?Object.getOwnPropertyDescriptor(e,i):null;l&&(l.get||l.set)?Object.defineProperty(r,i,l):r[i]=e[i]}r.default=e,n&&n.set(e,r);return r}(n(363)),a=i(n(475));function o(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(o=function(e){return e?n:t})(e)}function i(e){return e&&e.__esModule?e:{default:e}}function l(){return l=Object.assign?Object.assign.bind():function(e){for(var t=1;te({type:"INIT_FORM_SCHEMA_STACK",payload:{formSchema:{type:"insert-link",nextType:"admin"}}}),reset:()=>e({type:"RESET"})}}}))((e=>{let{type:t,editing:n,data:o,actions:i,onSubmit:u,...d}=e;if(!t)return!1;(0,r.useEffect)((()=>{n?i.initModal():i.reset()}),[n]);const s=o?{ID:o.FileID,Description:o.Title,TargetBlank:!!o.OpenInNew}:{};return r.default.createElement(a.default,l({isOpen:n,type:"insert-link",title:!1,bodyClassName:"modal__dialog",className:"insert-link__dialog-wrapper--internal",fileAttributes:s,onInsert:e=>{let{ID:n,Description:r,TargetBlank:a}=e;return u({FileID:n,Title:r,OpenInNew:a,typeKey:t.key},"",(()=>{}))}},d))}));t.default=u},117:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;u(n(754));var r=u(n(363)),a=(u(n(86)),u(n(912))),o=u(n(872)),i=u(n(902)),l=u(n(510));function u(e){return e&&e.__esModule?e:{default:e}}function d(){return d=Object.assign?Object.assign.bind():function(e){for(var t=1;t{const{schemaUrl:n}=l.default.getSection("SilverStripe\\Admin\\LeftAndMain").form.DynamicLink,r=o.default.parse(n),a=i.default.parse(r.query);return a.key=e,t&&(a.data=JSON.stringify(t)),o.default.format({...r,search:i.default.stringify(a)})};var f=e=>{let{type:t,editing:n,data:o,...i}=e;return!!t&&r.default.createElement(a.default,d({title:t.title,isOpen:n,schemaUrl:s(t.key,o),identifier:"Link.EditingLinkInfo"},i))};t.default=f},809:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;u(n(754));var r=u(n(363)),a=(n(648),u(n(86))),o=(n(127),u(n(820))),i=u(n(97)),l=u(n(734));u(n(686));function u(e){return e&&e.__esModule?e:{default:e}}function d(){return d=Object.assign?Object.assign.bind():function(e){for(var t=1;t{let{types:t,onSelect:n,link:a,onEdit:u,onClear:s}=e;return r.default.createElement("div",{className:(0,o.default)("link-picker","form-control",{"link-picker--selected":a})},void 0===a&&r.default.createElement(i.default,{types:t,onSelect:n}),a&&r.default.createElement(l.default,d({},a,{onClear:s,onClick:()=>a&&u&&u(a)})))};t.Component=s,s.propTypes={...i.default.propTypes,link:a.default.shape(l.default.propTypes),onEdit:a.default.func,onClear:a.default.func};var f=s;t.default=f},97:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=d(n(754)),a=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=u(t);if(n&&n.has(e))return n.get(e);var r={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=a?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),o=(n(648),d(n(86))),i=n(127),l=(d(n(820)),d(n(686)));function u(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(u=function(e){return e?n:t})(e)}function d(e){return e&&e.__esModule?e:{default:e}}const s=e=>{let{types:t,onSelect:n}=e;const[o,l]=(0,a.useState)(!1);return a.default.createElement(i.Dropdown,{isOpen:o,toggle:()=>l((e=>!e)),className:"link-picker__menu"},a.default.createElement(i.DropdownToggle,{className:"link-picker__menu-toggle font-icon-link",caret:!0},r.default._t("Link.ADD_LINK","Add Link")),a.default.createElement(i.DropdownMenu,null,t.map((e=>{let{key:t,title:r}=e;return a.default.createElement(i.DropdownItem,{key:t,onClick:()=>n(t)},r)}))))};s.propTypes={types:o.default.arrayOf(l.default).isRequired,onSelect:o.default.func.isRequired};var f=s;t.default=f},734:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(754)),a=u(n(363)),o=u(n(86)),i=u(n(686)),l=n(127);function u(e){return e&&e.__esModule?e:{default:e}}const d=e=>t=>{t.nativeEvent.stopImmediatePropagation(),t.preventDefault(),t.nativeEvent.preventDefault(),t.stopPropagation(),e&&e()},s=e=>{let{title:t,type:n,description:o,onClear:i,onClick:u}=e;return a.default.createElement("div",{className:"link-picker__link"},a.default.createElement(l.Button,{className:"link-picker__button font-icon-link",color:"secondary",onClick:d(u)},a.default.createElement("div",{className:"link-picker__link-detail"},a.default.createElement("div",{className:"link-picker__title"},t),a.default.createElement("small",{className:"link-picker__type"},n.title,": ",a.default.createElement("span",{className:"link-picker__url"},o)))),a.default.createElement(l.Button,{className:"link-picker__clear",color:"link",onClick:d(i)},r.default._t("Link.CLEAR","Clear")))};s.propTypes={title:o.default.string.isRequired,type:i.default,description:o.default.string,onClear:o.default.func,onClick:o.default.func};var f=s;t.default=f},115:function(e,t,n){var r=l(n(311)),a=l(n(363)),o=l(n(691)),i=n(648);function l(e){return e&&e.__esModule?e:{default:e}}function u(){return u=Object.assign?Object.assign.bind():function(e){for(var t=1;t{e(".js-injector-boot .entwine-jsonfield").entwine({Component:null,Root:null,onmatch(){const e=this.closest(".cms-content").attr("id"),t=e?{context:e}:{},n=this.data("schema-component"),r=(0,i.loadComponent)(n,t);this.setComponent(r),this.setRoot(o.default.createRoot(this[0])),this._super(),this.refresh()},refresh(){const e=this.getProps(),t=this.getComponent();this.getRoot().render(a.default.createElement(t,u({},e,{noHolder:!0})))},handleChange(t,n){let{id:r,value:a}=n;const o=e(this).data("field-id");e("#"+o).val(JSON.stringify(a)).trigger("change"),this.refresh()},getProps(){const t=e(this).data("field-id"),n=e("#"+t).val();return{id:t,value:n?JSON.parse(n):void 0,onChange:this.handleChange.bind(this)}},onunmatch(){this.getRoot().unmount()}})}))},287:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=n(648);const a={props(e){const{data:{error:t,readLinkDescription:n,loading:r}}=e,a=t&&t.graphQLErrors&&t.graphQLErrors.map((e=>e.message));return{loading:r,linkDescription:n?n.description:"",graphQLErrors:a}}},{READ:o}=r.graphqlTemplates;var i={apolloConfig:a,templateName:o,pluralName:"LinkDescription",pagination:!1,params:{dataStr:"String!"},args:{root:{dataStr:"dataStr"}},fields:["description"]};t.default=i},689:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=n(648);const a={props(e){const{data:{error:t,readLinkTypes:n,loading:r}}=e,a=t&&t.graphQLErrors&&t.graphQLErrors.map((e=>e.message));return{loading:r,types:n?n.reduce(((e,t)=>({...e,[t.key]:t})),{}):{},graphQLErrors:a}}},{READ:o}=r.graphqlTemplates;var i={apolloConfig:a,templateName:o,pluralName:"LinkTypes",pagination:!1,params:{keys:"[ID]"},args:{root:{keys:"keys"}},fields:["key","title","handlerName"]};t.default=i},686:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,a=(r=n(86))&&r.__esModule?r:{default:r};var o=a.default.shape({key:a.default.string.isRequired,title:a.default.string.isRequired});t.default=o},510:function(e){e.exports=Config},42:function(e){e.exports=FieldHolder},912:function(e){e.exports=FormBuilderModal},648:function(e){e.exports=Injector},475:function(e){e.exports=InsertMediaModal},872:function(e){e.exports=NodeUrl},86:function(e){e.exports=PropTypes},363:function(e){e.exports=React},691:function(e){e.exports=ReactDomClient},624:function(e){e.exports=ReactRedux},127:function(e){e.exports=Reactstrap},827:function(e){e.exports=Redux},820:function(e){e.exports=classnames},754:function(e){e.exports=i18n},311:function(e){e.exports=jQuery},902:function(e){e.exports=qs}},t={};function n(r){var a=t[r];if(void 0!==a)return a.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n(274),n(115)}(); \ No newline at end of file +!function(){"use strict";var e={274:function(e,t,n){var r=i(n(180)),a=i(n(521)),o=i(n(154));function i(e){return e&&e.__esModule?e:{default:e}}document.addEventListener("DOMContentLoaded",(()=>{(0,a.default)(),(0,o.default)(),(0,r.default)()}))},521:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=d(n(648)),a=d(n(809)),o=d(n(909)),i=d(n(852)),l=d(n(184)),u=d(n(117)),f=d(n(606));function d(e){return e&&e.__esModule?e:{default:e}}var s=()=>{r.default.component.registerMany({LinkPicker:a.default,LinkField:i.default,MultiLinkPicker:o.default,MultiLinkField:l.default,"LinkModal.FormBuilderModal":u.default,"LinkModal.InsertMediaModal":f.default})};t.default=s},154:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=i(n(648)),a=i(n(689)),o=i(n(287));function i(e){return e&&e.__esModule?e:{default:e}}var l=()=>{r.default.query.register("readLinkTypes",a.default),r.default.query.register("readLinkDescription",o.default)};t.default=l},180:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n=()=>{};t.default=n},513:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.linkFieldPropTypes=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=f(t);if(n&&n.has(e))return n.get(e);var r={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=a?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),a=n(648),o=u(n(86)),i=u(n(686)),l=u(n(384));function u(e){return e&&e.__esModule?e:{default:e}}function f(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(f=function(e){return e?n:t})(e)}const d=e=>{let{id:t,loading:n,Loading:o,Picker:i,onChange:l,types:u,clearLinkData:f,buildLinkProps:d,updateLinkData:s,selectLinkData:c}=e;if(n)return r.default.createElement(o,null);const[p,y]=(0,r.useState)(!1),[v,g]=(0,r.useState)(""),m=c(p),k=u[m&&m.typeKey||v],h={...d(),onEdit:e=>{y(e)},onClear:(e,n)=>{"function"==typeof l&&l(e,{id:t,value:f(n)})},onSelect:e=>{g(e),y(!0)},types:Object.values(u)},_={type:k,editing:!1!==p,onSubmit:e=>{const{SecurityID:n,action_insert:r,...a}=e;return"function"==typeof l&&l(void 0,{id:t,value:s(a)}),y(!1),g(""),Promise.resolve()},onClosed:()=>(y(!1),Promise.resolve()),data:m},b=k?k.handlerName:"FormBuilderModal",O=(0,a.loadComponent)(`LinkModal.${b}`);return r.default.createElement(r.Fragment,null,r.default.createElement(i,h),r.default.createElement(O,_))},s={id:o.default.string.isRequired,loading:o.default.bool,Loading:o.default.elementType,data:o.default.any,Picker:o.default.elementType,onChange:o.default.func,types:o.default.objectOf(i.default),linkDescriptions:o.default.arrayOf(l.default)};t.linkFieldPropTypes=s,d.propTypes={...s,clearLinkData:o.default.func.isRequired,buildLinkProps:o.default.func.isRequired,updateLinkData:o.default.func.isRequired,selectLinkData:o.default.func.isRequired};var c=d;t.default=c},701:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(363)),a=n(827),o=n(732),i=n(648),l=u(n(42));function u(e){return e&&e.__esModule?e:{default:e}}function f(){return f=Object.assign?Object.assign.bind():function(e){for(var t=1;tt=>{let{data:n,value:a,...o}=t,i=a||n;return"string"==typeof i&&(i=JSON.parse(i)),r.default.createElement(e,f({dataStr:JSON.stringify(i)},o,{data:i}))}),(0,i.injectGraphql)("readLinkTypes"),(0,i.injectGraphql)("readLinkDescription"),o.withApollo,l.default);t.default=d},480:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=i(n(363)),a=i(n(86)),o=i(n(820));function i(e){return e&&e.__esModule?e:{default:e}}const l=e=>{let{className:t,children:n}=e;return r.default.createElement("div",{className:(0,o.default)("link-box","form-control",t)},n)};l.propTypes={className:a.default.string};var u=l;t.default=u},852:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=d(n(363)),a=n(827),o=n(648),i=(d(n(86)),d(n(266))),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=f(t);if(n&&n.has(e))return n.get(e);var r={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=a?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(513)),u=d(n(701));function f(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(f=function(e){return e?n:t})(e)}function d(e){return e&&e.__esModule?e:{default:e}}function s(){return s=Object.assign?Object.assign.bind():function(e){for(var t=1;t{const t={buildLinkProps:()=>{const{data:t,linkDescriptions:n,types:r}=e,{typeKey:a}=t,o=r[a],i=n.length>0?n[0]:{},{title:l,description:u}=i;return{title:l,description:u,type:o||void 0}},clearLinkData:()=>({}),updateLinkData:e=>e,selectLinkData:()=>e.data};return r.default.createElement(l.default,s({},e,t))};t.Component=c,c.propTypes={...l.linkFieldPropTypes,data:i.default};var p=(0,a.compose)((0,o.inject)(["LinkPicker","Loading"],((e,t)=>({Picker:e,Loading:t}))),u.default)(c);t.default=p},606:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,a=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=i(t);if(n&&n.has(e))return n.get(e);var r={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),o=(r=n(475))&&r.__esModule?r:{default:r};function i(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(i=function(e){return e?n:t})(e)}function l(){return l=Object.assign?Object.assign.bind():function(e){for(var t=1;te({type:"INIT_FORM_SCHEMA_STACK",payload:{formSchema:{type:"insert-link",nextType:"admin"}}}),reset:()=>e({type:"RESET"})}}}))((e=>{let{type:t,editing:n,data:r,actions:i,onSubmit:u,...f}=e;if(!t)return!1;(0,a.useEffect)((()=>{n?i.initModal():i.reset()}),[n]);const d=r?{ID:r.FileID,Description:r.Title,TargetBlank:!!r.OpenInNew}:{};return a.default.createElement(o.default,l({isOpen:n,type:"insert-link",title:!1,bodyClassName:"modal__dialog",className:"insert-link__dialog-wrapper--internal",fileAttributes:d,onInsert:e=>{let{ID:n,Description:a,TargetBlank:o}=e;return u({FileID:n,ID:r?r.ID:void 0,Title:a,OpenInNew:o,typeKey:t.key},"",(()=>{}))}},f))}));t.default=u},117:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(363)),a=u(n(912)),o=u(n(872)),i=u(n(902)),l=u(n(510));function u(e){return e&&e.__esModule?e:{default:e}}function f(){return f=Object.assign?Object.assign.bind():function(e){for(var t=1;t{const{schemaUrl:n}=l.default.getSection("SilverStripe\\Admin\\LeftAndMain").form.DynamicLink,r=o.default.parse(n),a=i.default.parse(r.query);return a.key=e,t&&(a.data=JSON.stringify(t)),o.default.format({...r,search:i.default.stringify(a)})};var s=e=>{let{type:t,editing:n,data:o,...i}=e;return!!t&&r.default.createElement(a.default,f({title:t.title,isOpen:n,schemaUrl:d(t.key,o),identifier:"Link.EditingLinkInfo"},i))};t.default=s},809:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=d(n(363)),a=d(n(86)),o=d(n(820)),i=d(n(97)),l=d(n(734)),u=d(n(480)),f=d(n(686));function d(e){return e&&e.__esModule?e:{default:e}}const s=e=>{let{types:t,onSelect:n,title:a,description:f,type:d,onEdit:s,onClear:c}=e;return r.default.createElement(u.default,{className:(0,o.default)("link-picker",{"link-picker--selected":d})},d?r.default.createElement(l.default,{description:f,title:a,type:d,onClear:c,onClick:()=>s&&s()}):r.default.createElement(i.default,{types:t,onSelect:n}))};t.Component=s,s.propTypes={...i.default.propTypes,onEdit:a.default.func,onClear:a.default.func,title:a.default.string,description:a.default.string,type:f.default};var c=s;t.default=c},97:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=f(n(754)),a=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=u(t);if(n&&n.has(e))return n.get(e);var r={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=a?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),o=f(n(86)),i=n(127),l=f(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 f(e){return e&&e.__esModule?e:{default:e}}const d=e=>{let{types:t,onSelect:n}=e;const[o,l]=(0,a.useState)(!1);return a.default.createElement(i.Dropdown,{isOpen:o,toggle:()=>l((e=>!e)),className:"link-menu"},a.default.createElement(i.DropdownToggle,{className:"link-menu__toggle font-icon-link",caret:!0},r.default._t("Link.ADD_LINK","Add Link")),a.default.createElement(i.DropdownMenu,null,t.map((e=>{let{key:t,title:r,icon:o}=e;return a.default.createElement(i.DropdownItem,{className:`font-icon-${o||"link"}`,key:t,onClick:()=>n(t)},r)}))))};d.propTypes={types:o.default.arrayOf(l.default).isRequired,onSelect:o.default.func.isRequired};var s=d;t.default=s},734:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(754)),a=u(n(363)),o=u(n(86)),i=u(n(686)),l=n(127);function u(e){return e&&e.__esModule?e:{default:e}}const f=e=>t=>{t.nativeEvent.stopImmediatePropagation(),t.preventDefault(),t.nativeEvent.preventDefault(),t.stopPropagation(),e&&e()},d=e=>{let{title:t,type:n,description:o,onClear:i,onClick:u,className:d}=e;return a.default.createElement(l.Button,{className:classnames("link-title",`font-icon-${n.icon||"link"}`,d),color:"secondary",onClick:f(u)},a.default.createElement("div",{className:"link-title__detail"},a.default.createElement("div",{className:"link-title__title"},t),a.default.createElement("small",{className:"link-title__type"},n.title,": ",a.default.createElement("span",{className:"link-title__url"},o))),a.default.createElement(l.Button,{tag:"a",className:"link-title__clear",color:"link",onClick:f(i)},r.default._t("Link.CLEAR","Clear")))};d.propTypes={title:o.default.string.isRequired,type:i.default,description:o.default.string,onClear:o.default.func,onClick:o.default.func},d.defaultProps={type:{}};var s=d;t.default=s},184:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=c(n(363)),a=n(827),o=n(648),i=c(n(86)),l=n(614),u=c(n(266)),f=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={},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=a?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(513)),d=c(n(701));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 c(e){return e&&e.__esModule?e:{default:e}}function p(){return p=Object.assign?Object.assign.bind():function(e){for(var t=1;t{const t={buildLinkProps:()=>{return{links:(t=e.data,n=e.linkDescriptions,t.map((e=>{const t=n.find((t=>{let{id:n}=t;return n.toString()===e.ID.toString()}));return{...e,...t}})))};var t,n},clearLinkData:t=>e.data.filter((e=>{let{ID:n}=e;return n!==t})),updateLinkData:t=>{const{data:n}=e;return t.ID?n.map((e=>e.ID===t.ID?t:e)):[...n,{...t,ID:(0,l.v4)(),isNew:!0}]},selectLinkData:t=>e.data.find((e=>{let{ID:n}=e;return n===t}))||void 0};return r.default.createElement(f.default,p({},e,t))};t.Component=y,y.propTypes={...f.linkFieldPropTypes,data:i.default.arrayOf(u.default)};var v=(0,a.compose)((0,o.inject)(["MultiLinkPicker","Loading"],((e,t)=>({Picker:e,Loading:t}))),d.default)(y);t.default=v},909:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=u(n(363)),a=u(n(86)),o=u(n(97)),i=u(n(734)),l=u(n(480));function u(e){return e&&e.__esModule?e:{default:e}}function f(){return f=Object.assign?Object.assign.bind():function(e){for(var t=1;t{let{types:t,onSelect:n,links:a,onEdit:u,onClear:d}=e;return r.default.createElement("div",{className:"multi-link-picker"},r.default.createElement(l.default,{className:"multi-link-picker__picker"},r.default.createElement(o.default,{types:t,onSelect:n})),a.length>0&&r.default.createElement(l.default,{className:"multi-link-picker__list"},a.map((e=>{let{ID:n,...a}=e;return r.default.createElement(i.default,f({},a,{className:"multi-link-picker__link",type:t.find((e=>e.key===a.typeKey)),key:`${n} ${a.description}`,onClear:e=>d(e,n),onClick:()=>u(n)}))}))))};t.Component=d,d.propTypes={...o.default.propTypes,links:a.default.arrayOf(a.default.shape(i.default.propTypes)),onEdit:a.default.func,onClear:a.default.func};var s=d;t.default=s},115:function(e,t,n){var r=l(n(311)),a=l(n(363)),o=l(n(691)),i=n(648);function l(e){return e&&e.__esModule?e:{default:e}}function u(){return u=Object.assign?Object.assign.bind():function(e){for(var t=1;t{e(".js-injector-boot .entwine-jsonfield").entwine({Component:null,Root:null,onmatch(){const e=this.closest(".cms-content").attr("id"),t=e?{context:e}:{},n=this.data("schema-component"),r=(0,i.loadComponent)(n,t);this.setComponent(r),this.setRoot(o.default.createRoot(this[0])),this._super(),this.refresh()},refresh(){const e=this.getProps(),t=this.getComponent();this.getRoot().render(a.default.createElement(t,u({},e,{noHolder:!0})))},handleChange(t,n){let{value:r}=n;const a=e(this).data("field-id");e(`#${a}`).val(JSON.stringify(r)).trigger("change"),this.refresh()},getProps(){const t=e(this).data("field-id"),n=e(`#${t}`).val();return{id:t,value:n?JSON.parse(n):void 0,onChange:this.handleChange.bind(this)}},onunmatch(){this.getRoot().unmount()}})}))},287:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=n(648);const a={props(e){const{data:{error:t,readLinkDescription:n,loading:r}}=e;return{loading:r,linkDescriptions:n||[],graphQLErrors:t&&t.graphQLErrors&&t.graphQLErrors.map((e=>e.message))}}},{READ:o}=r.graphqlTemplates;var i={apolloConfig:a,templateName:o,pluralName:"LinkDescription",pagination:!1,params:{dataStr:"String!"},args:{root:{dataStr:"dataStr"}},fields:["id","description","title"]};t.default=i},689:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=n(648);const a={props(e){const{data:{error:t,readLinkTypes:n,loading:r}}=e,a=t&&t.graphQLErrors&&t.graphQLErrors.map((e=>e.message));return{loading:r,types:n?n.reduce(((e,t)=>({...e,[t.key]:t})),{}):{},graphQLErrors:a}}},{READ:o}=r.graphqlTemplates;var i={apolloConfig:a,templateName:o,pluralName:"LinkTypes",pagination:!1,params:{keys:"[ID]"},args:{root:{keys:"keys"}},fields:["key","title","handlerName","icon"]};t.default=i},266:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,a=(r=n(86))&&r.__esModule?r:{default:r};var o=a.default.shape({typeKey:a.default.string,Title:a.default.string,OpenInNew:a.default.bool});t.default=o},384:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,a=(r=n(86))&&r.__esModule?r:{default:r};var o=a.default.shape({title:a.default.string,description:a.default.string});t.default=o},686:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,a=(r=n(86))&&r.__esModule?r:{default:r};var o=a.default.shape({key:a.default.string.isRequired,icon:a.default.string,title:a.default.string.isRequired});t.default=o},614:function(e,t,n){var r;n.r(t),n.d(t,{NIL:function(){return T},parse:function(){return g},stringify:function(){return c},v1:function(){return v},v3:function(){return L},v4:function(){return C},v5:function(){return S},validate:function(){return l},version:function(){return R}});var a=new Uint8Array(16);function o(){if(!r&&!(r="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto)))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return r(a)}var i=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;for(var l=function(e){return"string"==typeof e&&i.test(e)},u=[],f=0;f<256;++f)u.push((f+256).toString(16).substr(1));var d,s,c=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=(u[e[t+0]]+u[e[t+1]]+u[e[t+2]]+u[e[t+3]]+"-"+u[e[t+4]]+u[e[t+5]]+"-"+u[e[t+6]]+u[e[t+7]]+"-"+u[e[t+8]]+u[e[t+9]]+"-"+u[e[t+10]]+u[e[t+11]]+u[e[t+12]]+u[e[t+13]]+u[e[t+14]]+u[e[t+15]]).toLowerCase();if(!l(n))throw TypeError("Stringified UUID is invalid");return n},p=0,y=0;var v=function(e,t,n){var r=t&&n||0,a=t||new Array(16),i=(e=e||{}).node||d,l=void 0!==e.clockseq?e.clockseq:s;if(null==i||null==l){var u=e.random||(e.rng||o)();null==i&&(i=d=[1|u[0],u[1],u[2],u[3],u[4],u[5]]),null==l&&(l=s=16383&(u[6]<<8|u[7]))}var f=void 0!==e.msecs?e.msecs:Date.now(),v=void 0!==e.nsecs?e.nsecs:y+1,g=f-p+(v-y)/1e4;if(g<0&&void 0===e.clockseq&&(l=l+1&16383),(g<0||f>p)&&void 0===e.nsecs&&(v=0),v>=1e4)throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");p=f,y=v,s=l;var m=(1e4*(268435455&(f+=122192928e5))+v)%4294967296;a[r++]=m>>>24&255,a[r++]=m>>>16&255,a[r++]=m>>>8&255,a[r++]=255&m;var k=f/4294967296*1e4&268435455;a[r++]=k>>>8&255,a[r++]=255&k,a[r++]=k>>>24&15|16,a[r++]=k>>>16&255,a[r++]=l>>>8|128,a[r++]=255&l;for(var h=0;h<6;++h)a[r+h]=i[h];return t||c(a)};var g=function(e){if(!l(e))throw TypeError("Invalid UUID");var t,n=new Uint8Array(16);return n[0]=(t=parseInt(e.slice(0,8),16))>>>24,n[1]=t>>>16&255,n[2]=t>>>8&255,n[3]=255&t,n[4]=(t=parseInt(e.slice(9,13),16))>>>8,n[5]=255&t,n[6]=(t=parseInt(e.slice(14,18),16))>>>8,n[7]=255&t,n[8]=(t=parseInt(e.slice(19,23),16))>>>8,n[9]=255&t,n[10]=(t=parseInt(e.slice(24,36),16))/1099511627776&255,n[11]=t/4294967296&255,n[12]=t>>>24&255,n[13]=t>>>16&255,n[14]=t>>>8&255,n[15]=255&t,n};var m="6ba7b810-9dad-11d1-80b4-00c04fd430c8",k="6ba7b811-9dad-11d1-80b4-00c04fd430c8";function h(e,t,n){function r(e,r,a,o){if("string"==typeof e&&(e=function(e){e=unescape(encodeURIComponent(e));for(var t=[],n=0;n>>9<<4)+1}function b(e,t){var n=(65535&e)+(65535&t);return(e>>16)+(t>>16)+(n>>16)<<16|65535&n}function O(e,t,n,r,a,o){return b((i=b(b(t,e),b(r,o)))<<(l=a)|i>>>32-l,n);var i,l}function j(e,t,n,r,a,o,i){return O(t&n|~t&r,e,t,a,o,i)}function M(e,t,n,r,a,o,i){return O(t&r|n&~r,e,t,a,o,i)}function P(e,t,n,r,a,o,i){return O(t^n^r,e,t,a,o,i)}function w(e,t,n,r,a,o,i){return O(n^(t|~r),e,t,a,o,i)}var D=function(e){if("string"==typeof e){var t=unescape(encodeURIComponent(e));e=new Uint8Array(t.length);for(var n=0;n>5]>>>a%32&255,i=parseInt(r.charAt(o>>>4&15)+r.charAt(15&o),16);t.push(i)}return t}(function(e,t){e[t>>5]|=128<>5]|=(255&e[r/8])<>>32-t}var N=function(e){var t=[1518500249,1859775393,2400959708,3395469782],n=[1732584193,4023233417,2562383102,271733878,3285377520];if("string"==typeof e){var r=unescape(encodeURIComponent(e));e=[];for(var a=0;a>>0;h=k,k=m,m=I(g,30)>>>0,g=v,v=O}n[0]=n[0]+v>>>0,n[1]=n[1]+g>>>0,n[2]=n[2]+m>>>0,n[3]=n[3]+k>>>0,n[4]=n[4]+h>>>0}return[n[0]>>24&255,n[0]>>16&255,n[0]>>8&255,255&n[0],n[1]>>24&255,n[1]>>16&255,n[1]>>8&255,255&n[1],n[2]>>24&255,n[2]>>16&255,n[2]>>8&255,255&n[2],n[3]>>24&255,n[3]>>16&255,n[3]>>8&255,255&n[3],n[4]>>24&255,n[4]>>16&255,n[4]>>8&255,255&n[4]]},S=h("v5",80,N),T="00000000-0000-0000-0000-000000000000";var R=function(e){if(!l(e))throw TypeError("Invalid UUID");return parseInt(e.substr(14,1),16)}},732:function(e){e.exports=ApolloClientReactHoc},510:function(e){e.exports=Config},42:function(e){e.exports=FieldHolder},912:function(e){e.exports=FormBuilderModal},648:function(e){e.exports=Injector},475:function(e){e.exports=InsertMediaModal},872:function(e){e.exports=NodeUrl},86:function(e){e.exports=PropTypes},363:function(e){e.exports=React},691:function(e){e.exports=ReactDomClient},624:function(e){e.exports=ReactRedux},127:function(e){e.exports=Reactstrap},827:function(e){e.exports=Redux},820:function(e){e.exports=classnames},754:function(e){e.exports=i18n},311:function(e){e.exports=jQuery},902:function(e){e.exports=qs}},t={};function n(r){var a=t[r];if(void 0!==a)return a.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n(274),n(115)}(); \ No newline at end of file diff --git a/client/dist/styles/bundle.css b/client/dist/styles/bundle.css index 5c415c8c..56d27d44 100644 --- a/client/dist/styles/bundle.css +++ b/client/dist/styles/bundle.css @@ -1 +1 @@ -.link-picker{display:flex;height:auto;min-height:54px;background:#fff;width:100%;align-items:stretch;cursor:pointer;padding:0;box-shadow:none}.link-picker.font-icon-link::before{margin:.76925rem}.link-picker__menu{flex-grow:1}.link-picker__menu-toggle{width:100%;height:100%;text-align:left}.link-picker__menu-toggle::before{padding:.76925rem}.link-picker__link{display:flex;align-items:center;width:100%;text-align:left;border:none;margin-right:0;justify-content:space-between}.link-picker__link:hover,.link-picker__link:focus{background:#eef0f4;text-decoration:none;color:inherit}.link-picker__button{display:flex;align-items:center;flex-grow:1;height:100%;text-align:left;border:none;margin-right:0}.link-picker__button::before{font-size:1.231rem;padding:.76925rem;margin-right:6px;flex-grow:0}.link-picker__link-detail{flex-grow:1}.link-picker__clear{flex-grow:0}.link-picker__url{color:#0071c4} +.link-menu{width:100%;height:100%}.link-menu.font-icon-link::before{margin:.76925rem}.link-menu__menu{flex-grow:1}.link-menu__toggle{width:100%;height:100%;text-align:left}.link-menu__toggle::before{padding:.76925rem}.link-title{display:flex;align-items:center;width:100%;text-align:left;border:none;margin-right:0}.link-title:hover,.link-title:focus{background:#eef0f4;text-decoration:none;color:inherit}.link-title__link{display:flex;align-items:center;width:100%;text-align:left;border:none;margin-right:0;justify-content:space-between}.link-title__link:hover,.link-title__link:focus{background:#eef0f4;text-decoration:none;color:inherit}.link-title__button{display:flex;align-items:center;flex-grow:1;height:100%;text-align:left;border:none;margin-right:0}.link-title__button::before{font-size:1.231rem;padding:.76925rem;margin-right:6px;flex-grow:0}.link-title__detail{flex-grow:1}.link-title__clear{flex-grow:0}.link-title__url{color:#0071c4}.link-box{display:flex;height:54px;background:#fff;width:100%;align-items:stretch;cursor:pointer;padding:0;box-shadow:none}.link-box.font-icon-link::before{margin:.76925rem}.multi-link-picker__picker{margin-bottom:.76925rem}.multi-link-picker__list{flex-wrap:wrap;height:auto}.multi-link-picker__link{width:100%;border-top:1px solid #ced5e1;border-radius:0}.multi-link-picker__link:first-child{border-top:none} diff --git a/client/src/boot/index.js b/client/src/boot/index.js index 4d7bb2a5..b506deed 100644 --- a/client/src/boot/index.js +++ b/client/src/boot/index.js @@ -1,6 +1,4 @@ /* global document */ -/* eslint-disable */ -import Config from 'lib/Config'; import registerReducers from './registerReducers'; import registerComponents from './registerComponents'; import registerQueries from './registerQueries'; diff --git a/client/src/boot/registerComponents.js b/client/src/boot/registerComponents.js index 1fd6283b..19a803d9 100644 --- a/client/src/boot/registerComponents.js +++ b/client/src/boot/registerComponents.js @@ -1,7 +1,9 @@ /* eslint-disable */ import Injector from 'lib/Injector'; import LinkPicker from 'components/LinkPicker/LinkPicker'; +import MultiLinkPicker from 'components/MultiLinkPicker/MultiLinkPicker'; import LinkField from 'components/LinkField/LinkField'; +import MultiLinkField from 'components/MultiLinkField/MultiLinkField'; import LinkModal from 'components/LinkModal/LinkModal'; import FileLinkModal from 'components/LinkModal/FileLinkModal'; @@ -10,6 +12,8 @@ const registerComponents = () => { Injector.component.registerMany({ LinkPicker, LinkField, + MultiLinkPicker, + MultiLinkField, 'LinkModal.FormBuilderModal': LinkModal, 'LinkModal.InsertMediaModal': FileLinkModal }); diff --git a/client/src/boot/registerReducers.js b/client/src/boot/registerReducers.js index 1abd586a..6a9a6419 100644 --- a/client/src/boot/registerReducers.js +++ b/client/src/boot/registerReducers.js @@ -1,26 +1,4 @@ -/* eslint-disable */ -import Injector from 'lib/Injector'; -import { combineReducers } from 'redux'; -// import gallery from 'state/gallery/GalleryReducer'; -// import queuedFiles from 'state/queuedFiles/QueuedFilesReducer'; -// import uploadField from 'state/uploadField/UploadFieldReducer'; -// import previewField from 'state/previewField/PreviewFieldReducer'; -// import imageLoad from 'state/imageLoad/ImageLoadReducer'; -// import displaySearch from 'state/displaySearch/DisplaySearchReducer'; -// import confirmDeletion from 'state/confirmDeletion/ConfirmDeletionReducer'; -// import modal from 'state/modal/ModalReducer'; - const registerReducers = () => { - // Injector.reducer.register('assetAdmin', combineReducers({ - // gallery, - // queuedFiles, - // uploadField, - // previewField, - // imageLoad, - // displaySearch, - // confirmDeletion, - // modal - // })); }; export default registerReducers; diff --git a/client/src/components/AbstractLinkField/AbstractLinkField.js b/client/src/components/AbstractLinkField/AbstractLinkField.js new file mode 100644 index 00000000..69b1f86a --- /dev/null +++ b/client/src/components/AbstractLinkField/AbstractLinkField.js @@ -0,0 +1,106 @@ +import React, { Fragment, useState } from 'react'; +import { loadComponent } from 'lib/Injector'; +import PropTypes from 'prop-types'; +import LinkType from '../../types/LinkType'; +import LinkSummary from '../../types/LinkSummary'; + +/** + * Underlying implementation of the LinkField. This is used for both the Single LinkField + * and MultiLinkField. It should not be used directly. + */ +const AbstractLinkField = ({ + id, loading, Loading, Picker, onChange, types, + clearLinkData, buildLinkProps, updateLinkData, selectLinkData +}) => { + // Render a loading indicator if we're still fetching some data from the server + if (loading) { + return ; + } + + // When editing is true, wu display a modal to let the user edit the link data + const [editingId, setEditingId] = useState(false); + // newTypeKey define what link type we are using for brand new links + const [newTypeKey, setNewTypeKey] = useState(''); + + const selectedLinkData = selectLinkData(editingId); + const modalType = types[(selectedLinkData && selectedLinkData.typeKey) || newTypeKey]; + + // When the use clears the link data, we call onchange with an empty object + const onClear = (event, linkId) => { + if (typeof onChange === 'function') { + onChange(event, { id, value: clearLinkData(linkId) }); + } + }; + + const linkProps = { + ...buildLinkProps(), + onEdit: (linkId) => { setEditingId(linkId); }, + onClear, + onSelect: (key) => { + setNewTypeKey(key); + setEditingId(true); + }, + types: Object.values(types) + }; + + const onModalSubmit = (submittedData) => { + // Remove unneeded keys from submitted data + // eslint-disable-next-line camelcase + const { SecurityID, action_insert, ...newLinkData } = submittedData; + if (typeof onChange === 'function') { + // onChange expect an event object which we don't have + onChange(undefined, { id, value: updateLinkData(newLinkData) }); + } + // Close the modal + setEditingId(false); + setNewTypeKey(''); + return Promise.resolve(); + }; + + const modalProps = { + type: modalType, + editing: editingId !== false, + onSubmit: onModalSubmit, + onClosed: () => { + setEditingId(false); + return Promise.resolve(); + }, + data: selectedLinkData + }; + + // Different link types might have different Link modal + const handlerName = modalType ? modalType.handlerName : 'FormBuilderModal'; + const LinkModal = loadComponent(`LinkModal.${handlerName}`); + + return ( + + + + + ); +}; + +/** + * These props are expected to be passthrough from tho parent component. + */ +export const linkFieldPropTypes = { + id: PropTypes.string.isRequired, + loading: PropTypes.bool, + Loading: PropTypes.elementType, + data: PropTypes.any, + Picker: PropTypes.elementType, + onChange: PropTypes.func, + types: PropTypes.objectOf(LinkType), + linkDescriptions: PropTypes.arrayOf(LinkSummary), +}; + +AbstractLinkField.propTypes = { + ...linkFieldPropTypes, + // These props need to be provided by the specific implementation + clearLinkData: PropTypes.func.isRequired, + buildLinkProps: PropTypes.func.isRequired, + updateLinkData: PropTypes.func.isRequired, + selectLinkData: PropTypes.func.isRequired, +}; + +export default AbstractLinkField; diff --git a/client/src/components/AbstractLinkField/linkFieldHOC.js b/client/src/components/AbstractLinkField/linkFieldHOC.js new file mode 100644 index 00000000..4fb7a3d8 --- /dev/null +++ b/client/src/components/AbstractLinkField/linkFieldHOC.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { compose } from 'redux'; +import { withApollo } from '@apollo/client/react/hoc'; +import { injectGraphql } from 'lib/Injector'; +import fieldHolder from 'components/FieldHolder/FieldHolder'; + +/** + * When getting data from entwine, we might get it in a plain JSON string. + * This method rewrites tho data to a normalise format. + */ +const stringifyData = (Component) => (({ data, value, ...props }) => { + let dataValue = value || data; + if (typeof dataValue === 'string') { + dataValue = JSON.parse(dataValue); + } + return ; +}); + + +/** + * Wires a Link field into GraphQL normalise the initial data to a proper objects + */ +const linkFieldHOC = compose( + stringifyData, + injectGraphql('readLinkTypes'), + injectGraphql('readLinkDescription'), + withApollo, + fieldHolder +); + +export default linkFieldHOC; diff --git a/client/src/components/LinkBox/LinkBox.js b/client/src/components/LinkBox/LinkBox.js new file mode 100644 index 00000000..5d2e5696 --- /dev/null +++ b/client/src/components/LinkBox/LinkBox.js @@ -0,0 +1,18 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +/** + * Wraps children in a bok with rounder corners and a form control style. + */ +const LinkBox = ({ className, children }) => ( +
+ { children } +
+); + +LinkBox.propTypes = { + className: PropTypes.string, +}; + +export default LinkBox; diff --git a/client/src/components/LinkBox/LinkBox.scss b/client/src/components/LinkBox/LinkBox.scss new file mode 100644 index 00000000..e74665b1 --- /dev/null +++ b/client/src/components/LinkBox/LinkBox.scss @@ -0,0 +1,14 @@ +.link-box { + display: flex; + height: 54px; + background: white; + width: 100%; + align-items: stretch; + cursor: pointer; + padding: 0; + box-shadow: none; + + &.font-icon-link::before { + margin: $spacer-xs; + } +} diff --git a/client/src/components/LinkField/LinkField.js b/client/src/components/LinkField/LinkField.js index 5571450f..84d1babd 100644 --- a/client/src/components/LinkField/LinkField.js +++ b/client/src/components/LinkField/LinkField.js @@ -1,90 +1,51 @@ -import React, { Fragment, useState } from 'react'; +import React from 'react'; import { compose } from 'redux'; -import { inject, injectGraphql, loadComponent } from 'lib/Injector'; -import fieldHolder from 'components/FieldHolder/FieldHolder'; - -const LinkField = ({ id, loading, Loading, data, LinkPicker, onChange, types, linkDescription, ...props }) => { - if (loading) { - return ; - } - - const [editing, setEditing] = useState(false); - const [newTypeKey, setNewTypeKey] = useState(''); - - const onClear = (event) => { - if (typeof onChange !== 'function') { - return; - } - - onChange(event, { id, value: {} }); - }; - - const { typeKey } = data; - const type = types[typeKey]; - const modalType = newTypeKey ? types[newTypeKey] : type; - - let title = data ? data.Title : ''; - - if (!title) { - title = data ? data.TitleRelField : ''; - } - - const linkProps = { - title, - link: type ? { type, title, description: linkDescription } : undefined, - onEdit: () => { setEditing(true); }, - onClear, - onSelect: (key) => { - setNewTypeKey(key); - setEditing(true); +import { inject } from 'lib/Injector'; +import PropTypes from 'prop-types'; +import LinkData from '../../types/LinkData'; +import AbstractLinkField, { linkFieldPropTypes } from '../AbstractLinkField/AbstractLinkField'; +import linkFieldHOC from '../AbstractLinkField/linkFieldHOC'; + +/** + * Renders a Field allowing the selection of a single link. + */ +const LinkField = (props) => { + const staticProps = { + buildLinkProps: () => { + const { data, linkDescriptions, types } = props; + + // Try to read the link type from the link data or use newTypeKey + const { typeKey } = data; + const type = types[typeKey]; + + // Read link title and description + const linkDescription = linkDescriptions.length > 0 ? linkDescriptions[0] : {}; + const { title, description } = linkDescription; + return { + title, + description, + type: type || undefined, + }; }, - types: Object.values(types) + clearLinkData: () => ({}), + updateLinkData: newLinkData => newLinkData, + selectLinkData: () => (props.data), }; - const onModalSubmit = (modalData, action, submitFn) => { - const { SecurityID, action_insert: actionInsert, ...value } = modalData; - - if (typeof onChange === 'function') { - onChange(event, { id, value }); - } - - setEditing(false); - setNewTypeKey(''); - - return Promise.resolve(); - }; - - const modalProps = { - type: modalType, - editing, - onSubmit: onModalSubmit, - onClosed: () => { - setEditing(false); - }, - data - }; - - const handlerName = modalType ? modalType.handlerName : 'FormBuilderModal'; - const LinkModal = loadComponent(`LinkModal.${handlerName}`); + return ; +}; - return - - - ; +LinkField.propTypes = { + ...linkFieldPropTypes, + data: LinkData }; -const stringifyData = (Component) => (({ data, value, ...props }) => { - let dataValue = value || data; - if (typeof dataValue === 'string') { - dataValue = JSON.parse(dataValue); - } - return ; -}); +export { LinkField as Component }; export default compose( - inject(['LinkPicker', 'Loading']), - injectGraphql('readLinkTypes'), - stringifyData, - injectGraphql('readLinkDescription'), - fieldHolder + inject( + ['LinkPicker', 'Loading'], + (LinkPicker, Loading) => ({ Picker: LinkPicker, Loading }) + ), + linkFieldHOC )(LinkField); diff --git a/client/src/components/LinkModal/FileLinkModal.js b/client/src/components/LinkModal/FileLinkModal.js index a5d7631f..b9799d1e 100644 --- a/client/src/components/LinkModal/FileLinkModal.js +++ b/client/src/components/LinkModal/FileLinkModal.js @@ -1,12 +1,11 @@ +/* eslint-disable import/extensions */ +/* eslint-disable import/no-unresolved */ /* global tinymce, editorIdentifier, ss */ -/* eslint-disable */ -import i18n from 'i18n'; -import React, {useEffect} from 'react'; +import React, { useEffect } from 'react'; import InsertMediaModal from 'containers/InsertMediaModal/InsertMediaModal'; -import {connect} from "react-redux"; - -const FileLinkModal = ({type, editing, data, actions, onSubmit, ...props}) => { +import { connect } from 'react-redux'; +const FileLinkModal = ({ type, editing, data, actions, onSubmit, ...props }) => { if (!type) { return false; } @@ -17,34 +16,37 @@ const FileLinkModal = ({type, editing, data, actions, onSubmit, ...props}) => { } else { actions.reset(); } - }, [editing]) + }, [editing]); const attrs = data ? { ID: data.FileID, Description: data.Title, - TargetBlank: data.OpenInNew ? true : false, + TargetBlank: !!data.OpenInNew, } : {}; - const onInsert = ({ID, Description, TargetBlank}) => { - return onSubmit({ + const onInsert = ({ ID, Description, TargetBlank }) => ( + onSubmit({ FileID: ID, + ID: data ? data.ID : undefined, Title: Description, OpenInNew: TargetBlank, typeKey: type.key - }, '', () => {}); - }; + }, '', () => {}) + ); - return ; -} + return ( + + ); +}; function mapStateToProps() { return {}; diff --git a/client/src/components/LinkModal/LinkModal.js b/client/src/components/LinkModal/LinkModal.js index 7e5ce86d..3baaa1fe 100644 --- a/client/src/components/LinkModal/LinkModal.js +++ b/client/src/components/LinkModal/LinkModal.js @@ -1,7 +1,4 @@ -/* eslint-disable */ -import i18n from 'i18n'; import React from 'react'; -import PropTypes from 'prop-types'; import FormBuilderModal from 'components/FormBuilderModal/FormBuilderModal'; import url from 'url'; import qs from 'qs'; @@ -10,8 +7,7 @@ import Config from 'lib/Config'; const leftAndMain = 'SilverStripe\\Admin\\LeftAndMain'; const buildSchemaUrl = (key, data) => { - - const {schemaUrl} = Config.getSection(leftAndMain).form.DynamicLink; + const { schemaUrl } = Config.getSection(leftAndMain).form.DynamicLink; const parsedURL = url.parse(schemaUrl); const parsedQs = qs.parse(parsedURL.query); @@ -19,21 +15,23 @@ const buildSchemaUrl = (key, data) => { if (data) { parsedQs.data = JSON.stringify(data); } - return url.format({ ...parsedURL, search: qs.stringify(parsedQs)}); -} + return url.format({ ...parsedURL, search: qs.stringify(parsedQs) }); +}; -const LinkModal = ({type, editing, data, ...props}) => { +const LinkModal = ({ type, editing, data, ...props }) => { if (!type) { return false; } - return ; -} + return ( + + ); +}; export default LinkModal; diff --git a/client/src/components/LinkPicker/LinkPicker.js b/client/src/components/LinkPicker/LinkPicker.js index 98aab6ec..4afcbf76 100644 --- a/client/src/components/LinkPicker/LinkPicker.js +++ b/client/src/components/LinkPicker/LinkPicker.js @@ -1,30 +1,36 @@ -/* eslint-disable */ -import i18n from 'i18n'; import React from 'react'; -import { inject } from 'lib/Injector'; import PropTypes from 'prop-types'; -import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem, Button } from 'reactstrap'; import classnames from 'classnames'; import LinkPickerMenu from './LinkPickerMenu'; import LinkPickerTitle from './LinkPickerTitle'; -import LinkType from 'types/LinkType'; +import LinkBox from '../LinkBox/LinkBox'; +import LinkType from '../../types/LinkType'; -const LinkPicker = ({ types, onSelect, link, onEdit, onClear }) => ( -
- {link === undefined && } - {link && link && onEdit && onEdit(link)}/>} -
+const LinkPicker = ({ types, onSelect, title, description, type, onEdit, onClear }) => ( + + { type ? + onEdit && onEdit()} + /> : + + } + ); LinkPicker.propTypes = { ...LinkPickerMenu.propTypes, - link: PropTypes.shape(LinkPickerTitle.propTypes), onEdit: PropTypes.func, onClear: PropTypes.func, + title: PropTypes.string, + description: PropTypes.string, + type: LinkType, }; -export {LinkPicker as Component}; +export { LinkPicker as Component }; export default LinkPicker; diff --git a/client/src/components/LinkPicker/LinkPicker.scss b/client/src/components/LinkPicker/LinkPicker.scss index 29ea23e7..d6fb2306 100644 --- a/client/src/components/LinkPicker/LinkPicker.scss +++ b/client/src/components/LinkPicker/LinkPicker.scss @@ -1,13 +1,6 @@ -.link-picker { - display: flex; - height: auto; - min-height: 54px; - background: white; +.link-menu { width: 100%; - align-items: stretch; - cursor: pointer; - padding: 0; - box-shadow: none; + height: 100%; &.font-icon-link::before { margin: $spacer-xs; @@ -17,7 +10,7 @@ flex-grow: 1; } - &__menu-toggle { + &__toggle { width: 100%; height: 100%; text-align: left; @@ -26,9 +19,21 @@ padding: $spacer-xs; } } +} + +.link-title { - &--selected { + display: flex; + align-items: center; + width: 100%; + text-align: left; + border: none; + margin-right: 0; + &:hover, &:focus { + background: $gray-100; + text-decoration: none; + color: inherit; } &__link { @@ -64,7 +69,7 @@ } } - &__link-detail { + &__detail { flex-grow: 1; } @@ -75,5 +80,4 @@ &__url { color: $link-color; } - } diff --git a/client/src/components/LinkPicker/LinkPickerMenu.js b/client/src/components/LinkPicker/LinkPickerMenu.js index 036f8392..521ecb34 100644 --- a/client/src/components/LinkPicker/LinkPickerMenu.js +++ b/client/src/components/LinkPicker/LinkPickerMenu.js @@ -1,30 +1,30 @@ /* eslint-disable */ import i18n from 'i18n'; -import React, {useState, setState} from 'react'; -import { inject } from 'lib/Injector'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; -import classnames from 'classnames'; import LinkType from 'types/LinkType'; +/** + * Displays a dropdown menu allowing the user to pick a link type. + */ const LinkPickerMenu = ({ types, onSelect }) => { const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(prevState => !prevState); - return ( - {i18n._t('Link.ADD_LINK', 'Add Link')} + {i18n._t('Link.ADD_LINK', 'Add Link')} - {types.map(({key, title}) => - onSelect(key)}>{title} + {types.map(({ key, title, icon }) => + onSelect(key)}>{title} )} - + ); }; diff --git a/client/src/components/LinkPicker/LinkPickerTitle.js b/client/src/components/LinkPicker/LinkPickerTitle.js index c25c7456..8309b244 100644 --- a/client/src/components/LinkPicker/LinkPickerTitle.js +++ b/client/src/components/LinkPicker/LinkPickerTitle.js @@ -3,29 +3,29 @@ import i18n from 'i18n'; import React from 'react'; import PropTypes from 'prop-types'; import LinkType from 'types/LinkType'; -import {Button} from 'reactstrap'; +import { Button } from 'reactstrap'; const stopPropagation = (fn) => (e) => { e.nativeEvent.stopImmediatePropagation(); e.preventDefault(); e.nativeEvent.preventDefault(); e.stopPropagation(); - fn && fn(); -} + if (fn) { + fn(); + } +}; -const LinkPickerTitle = ({ title, type, description, onClear, onClick }) => ( -
- - -
+ + + ); LinkPickerTitle.propTypes = { @@ -36,4 +36,8 @@ LinkPickerTitle.propTypes = { onClick: PropTypes.func }; +LinkPickerTitle.defaultProps = { + type: {} +}; + export default LinkPickerTitle; diff --git a/client/src/components/LinkPicker/tests/LinkPicker-story.js b/client/src/components/LinkPicker/tests/LinkPicker-story.js index 95f1050e..706ad52e 100644 --- a/client/src/components/LinkPicker/tests/LinkPicker-story.js +++ b/client/src/components/LinkPicker/tests/LinkPicker-story.js @@ -7,10 +7,10 @@ import { action } from '@storybook/addon-actions'; import LinkPicker from '../LinkPicker'; const types = [ - {key: 'cms', title: 'Page on this site'}, - {key: 'asset', title: 'File'}, - {key: 'external', title: 'External URL'}, - {key: 'mailto', title: 'Email address'}, + { key: 'cms', title: 'Page on this site' }, + { key: 'asset', title: 'File' }, + { key: 'external', title: 'External URL' }, + { key: 'mailto', title: 'Email address' }, ]; const link = { @@ -33,12 +33,12 @@ const props = { onSelect, onClear, onEdit -} +}; storiesOf('LinkField/LinkPicker', module) .add('Initial', () => ( )) .add('Selected', () => ( - + )); diff --git a/client/src/components/MultiLinkField/MultiLinkField.js b/client/src/components/MultiLinkField/MultiLinkField.js new file mode 100644 index 00000000..7556e3fe --- /dev/null +++ b/client/src/components/MultiLinkField/MultiLinkField.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { compose } from 'redux'; +import { inject } from 'lib/Injector'; +import PropTypes from 'prop-types'; +import { v4 as uuidv4 } from 'uuid'; +import LinkData from '../../types/LinkData'; +import AbstractLinkField, { linkFieldPropTypes } from '../AbstractLinkField/AbstractLinkField'; +import linkFieldHOC from '../AbstractLinkField/linkFieldHOC'; + +/** + * Helper that matches links to their descriptions + */ +function mergeLinkDataWithDescription(links, descriptions) { + return links.map(link => { + const description = descriptions.find(({ id }) => id.toString() === link.ID.toString()); + return { ...link, ...description }; + }); +} + +/** + * Renders a LinkField allowing the selection of multiple links. + */ +const MultiLinkField = (props) => { + const staticProps = { + buildLinkProps: () => ({ + links: mergeLinkDataWithDescription(props.data, props.linkDescriptions), + }), + clearLinkData: linkId => ( + props.data.filter(({ ID }) => ID !== linkId) + ), + updateLinkData: newLinkData => { + const { data } = props; + return newLinkData.ID ? + data.map(oldLink => (oldLink.ID === newLinkData.ID ? newLinkData : oldLink)) : + [...data, { ...newLinkData, ID: uuidv4(), isNew: true }]; + }, + selectLinkData: (editingId) => ( + props.data.find(({ ID }) => ID === editingId) || undefined + ) + }; + + return ; +}; + +MultiLinkField.propTypes = { + ...linkFieldPropTypes, + data: PropTypes.arrayOf(LinkData), +}; + +export { MultiLinkField as Component }; + +export default compose( + inject( + ['MultiLinkPicker', 'Loading'], + (MultiLinkPicker, Loading) => ({ Picker: MultiLinkPicker, Loading }) + ), + linkFieldHOC +)(MultiLinkField); diff --git a/client/src/components/MultiLinkPicker/MultiLinkPicker.js b/client/src/components/MultiLinkPicker/MultiLinkPicker.js new file mode 100644 index 00000000..8c1a3ec7 --- /dev/null +++ b/client/src/components/MultiLinkPicker/MultiLinkPicker.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import LinkPickerMenu from '../LinkPicker/LinkPickerMenu'; +import LinkPickerTitle from '../LinkPicker/LinkPickerTitle'; +import LinkBox from '../LinkBox/LinkBox'; + +const LinkPicker = ({ types, onSelect, links, onEdit, onClear }) => ( +
+ + + + { links.length > 0 && + { links.map(({ ID, ...link }) => ( + type.key === link.typeKey)} + key={`${ID} ${link.description}`} + onClear={(event) => onClear(event, ID)} + onClick={() => onEdit(ID)} + /> + )) } + } +
+); + +LinkPicker.propTypes = { + ...LinkPickerMenu.propTypes, + links: PropTypes.arrayOf(PropTypes.shape(LinkPickerTitle.propTypes)), + onEdit: PropTypes.func, + onClear: PropTypes.func, +}; + + +export { LinkPicker as Component }; + +export default LinkPicker; diff --git a/client/src/components/MultiLinkPicker/MultiLinkPicker.scss b/client/src/components/MultiLinkPicker/MultiLinkPicker.scss new file mode 100644 index 00000000..0d27ac23 --- /dev/null +++ b/client/src/components/MultiLinkPicker/MultiLinkPicker.scss @@ -0,0 +1,20 @@ +.multi-link-picker { + &__picker { + margin-bottom: $spacer-xs; + } + + &__list { + flex-wrap: wrap; + height: auto; + } + + &__link { + width: 100%; + border-top: 1px solid #ced5e1; + border-radius: 0; + + &:first-child { + border-top: none; + } + } +} diff --git a/client/src/components/MultiLinkPicker/tests/MultiLinkPicker-story.js b/client/src/components/MultiLinkPicker/tests/MultiLinkPicker-story.js new file mode 100644 index 00000000..2820f13c --- /dev/null +++ b/client/src/components/MultiLinkPicker/tests/MultiLinkPicker-story.js @@ -0,0 +1,68 @@ +import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { storiesOf } from '@storybook/react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { action } from '@storybook/addon-actions'; +import MultiLinkPicker from '../MultiLinkPicker'; + +const types = [ + { key: 'cms', title: 'Page on this site', icon: 'page' }, + { key: 'asset', title: 'File', icon: 'menu-files' }, + { key: 'external', title: 'External URL', icon: 'external-link' }, + { key: 'mailto', title: 'Email address', icon: 'block-email' }, +]; + +const links = [ + { + id: '1', + title: 'Our people', + type: types[0], + description: '/about-us/people' + }, + { + id: '2', + title: 'About us', + type: types[0], + description: '/about-us' + }, + { + id: '3', + title: 'My document', + type: types[1], + description: '/my-document.pdf' + }, + { + id: '4', + title: 'Silverstripe', + type: types[2], + description: 'https://www.silverstripe.com/' + }, + { + id: '5', + title: 'john@example.com', + type: types[3], + description: 'john@example.com' + }, +]; + +const onSelect = action('onSelect'); +onSelect.toString = () => 'onSelect'; + +const onEdit = action('onEdit'); +onEdit.toString = () => 'onEdit'; + +const onClear = action('onClear'); +onClear.toString = () => 'onClear'; + +const props = { + types, + onSelect, + onClear, + onEdit, + links +}; + +storiesOf('LinkField/MultiLinkPicker', module) + .add('MultiLinkPicker', () => ( + + )); diff --git a/client/src/entwine/JsonField.js b/client/src/entwine/JsonField.js index ee8f22c4..18451fe4 100644 --- a/client/src/entwine/JsonField.js +++ b/client/src/entwine/JsonField.js @@ -33,9 +33,9 @@ jQuery.entwine('ss', ($) => { Root.render(); }, - handleChange(event, {id, value}) { + handleChange(event, { value }) { const fieldID = $(this).data('field-id'); - $('#' + fieldID).val(JSON.stringify(value)).trigger('change'); + $(`#${fieldID}`).val(JSON.stringify(value)).trigger('change'); this.refresh(); }, @@ -46,7 +46,7 @@ jQuery.entwine('ss', ($) => { */ getProps() { const fieldID = $(this).data('field-id'); - const dataStr = $('#' + fieldID).val(); + const dataStr = $(`#${fieldID}`).val(); const value = dataStr ? JSON.parse(dataStr) : undefined; return { diff --git a/client/src/state/linkDescription/readLinkDescription.js b/client/src/state/linkDescription/readLinkDescription.js index 2301bd26..da79d029 100644 --- a/client/src/state/linkDescription/readLinkDescription.js +++ b/client/src/state/linkDescription/readLinkDescription.js @@ -14,11 +14,10 @@ const apolloConfig = { } = props; const errors = error && error.graphQLErrors && error.graphQLErrors.map((graphQLError) => graphQLError.message); - const linkDescription = readLinkDescription ? readLinkDescription.description : ''; return { loading: networkLoading, - linkDescription, + linkDescriptions: readLinkDescription || [], graphQLErrors: errors, }; }, @@ -38,6 +37,6 @@ const query = { dataStr: 'dataStr' } }, - fields: ['description'], + fields: ['id', 'description', 'title'], }; export default query; diff --git a/client/src/state/linkTypes/readLinkTypes.js b/client/src/state/linkTypes/readLinkTypes.js index 1bb0bb7e..5af96c78 100644 --- a/client/src/state/linkTypes/readLinkTypes.js +++ b/client/src/state/linkTypes/readLinkTypes.js @@ -43,6 +43,6 @@ const query = { keys: 'keys' } }, - fields: ['key', 'title', 'handlerName'], + fields: ['key', 'title', 'handlerName', 'icon'], }; export default query; diff --git a/client/src/styles/bundle.scss b/client/src/styles/bundle.scss index 4e24cdb9..bed5d695 100644 --- a/client/src/styles/bundle.scss +++ b/client/src/styles/bundle.scss @@ -9,3 +9,5 @@ @import "../components/LinkPicker/LinkPicker"; +@import "../components/LinkBox/LinkBox"; +@import "../components/MultiLinkPicker/MultiLinkPicker"; diff --git a/client/src/types/LinkData.js b/client/src/types/LinkData.js new file mode 100644 index 00000000..27784efc --- /dev/null +++ b/client/src/types/LinkData.js @@ -0,0 +1,9 @@ +import PropTypes from 'prop-types'; + +const LinkData = PropTypes.shape({ + typeKey: PropTypes.string, + Title: PropTypes.string, + OpenInNew: PropTypes.bool, +}); + +export default LinkData; diff --git a/client/src/types/LinkSummary.js b/client/src/types/LinkSummary.js new file mode 100644 index 00000000..702c9ead --- /dev/null +++ b/client/src/types/LinkSummary.js @@ -0,0 +1,8 @@ +import PropTypes from 'prop-types'; + +const LinkSummary = PropTypes.shape({ + title: PropTypes.string, + description: PropTypes.string, +}); + +export default LinkSummary; diff --git a/client/src/types/LinkType.js b/client/src/types/LinkType.js index c086ec78..ba091f4f 100644 --- a/client/src/types/LinkType.js +++ b/client/src/types/LinkType.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; const LinkType = PropTypes.shape({ key: PropTypes.string.isRequired, + icon: PropTypes.string, title: PropTypes.string.isRequired, }); diff --git a/package.json b/package.json index 066560f6..3b3c5b87 100644 --- a/package.json +++ b/package.json @@ -51,17 +51,21 @@ "@babel/runtime": "^7.20.0", "@silverstripe/eslint-config": "^1.0.0-alpha6", "@silverstripe/webpack-config": "^2.0.0-alpha9", + "autoprefixer": "^10.4.14", "babel-jest": "^29.2.2", "jest-cli": "^29.2.2", "jest-environment-jsdom": "^29.3.1", + "postcss": "^8.4.25", + "postcss-custom-properties": "^13.2.1", + "postcss-loader": "^7.3.3", "webpack": "^5.74.0", "webpack-cli": "^5.0.0" }, "dependencies": { "@apollo/client": "^3.7.1", "bootstrap": "^4.6.2", - "core-js": "^3.26.0", "classnames": "^2.2.5", + "core-js": "^3.26.0", "prop-types": "^15.8.1", "qs": "^6.11.0", "react": "^18.2.0", @@ -76,9 +80,10 @@ "redux": "^4.2.0", "redux-form": "^8.3.8", "redux-thunk": "^2.4.1", + "uuid": "^8.3.2", "webpack-sources": "^3.2.3" }, "browserslist": [ "defaults" ] -} \ No newline at end of file +} diff --git a/src/Form/JsonField.php b/src/Form/JsonField.php index 172a317c..22da4f4c 100644 --- a/src/Form/JsonField.php +++ b/src/Form/JsonField.php @@ -3,6 +3,7 @@ namespace SilverStripe\LinkField\Form; use InvalidArgumentException; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\FormField; use SilverStripe\LinkField\JsonData; use SilverStripe\ORM\DataObject; @@ -27,7 +28,7 @@ public function setValue($value, $data = null) return parent::setValue($value, $data); } - /** + /** * @param DataObject|DataObjectInterface $record * @return $this */ @@ -57,7 +58,7 @@ public function saveInto(DataObjectInterface $record) $record->{"{$fieldname}ID"} = 0; } } elseif ($value) { - $jsonDataObject = new $class(); + $jsonDataObject = Injector::inst()->create($class); $jsonDataObject = $jsonDataObject->setData($value); $jsonDataObject->write(); $record->{"{$fieldname}ID"} = $jsonDataObject->ID; @@ -87,7 +88,7 @@ protected function parseString(string $value): ?array ); } - if (!$data) { + if (!is_array($data) && empty($data)) { return null; } diff --git a/src/Form/MultiLinkField.php b/src/Form/MultiLinkField.php new file mode 100644 index 00000000..8ae2cf17 --- /dev/null +++ b/src/Form/MultiLinkField.php @@ -0,0 +1,127 @@ +dataList = $dataList; + } + + /** + * Set the data source. + * + * @param SS_List $list + * + * @return $this + */ + public function setList(SS_List $list) + { + $this->dataList = $list; + return $this; + } + + /** + * + */ + public function getList(): ?SS_List + { + return $this->dataList; + } + + public function setValue($value, $data = null) + { + if (empty($value)) { + // If the value is empty, we convert our list to a JSON string with all our link data. + // Scenario: We're about to render the data for the front end + $list = $this->getList(); + if (empty($list) && !empty($data)) { + // If we don't have an explicitly defined list, look up the match filed name on our data object. + // Scenario: We only specified the name of the relation on the data object. + $fieldname = $this->getName(); + $list = $data->$fieldname(); + } + + if (!empty($list)) { + // If we managed to find something matching a sensible list, we json serialize it. + $value = json_encode( + array_map(function (Link $link) { + return $link->jsonSerialize(); + }, $list->toArray()) + ); + } + } + // If value is not falsy, that means we got some JSON data back from the frontend. + + return parent::setValue($value, $data); + } + + /** + * @param DataObject|DataObjectInterface $record + * @return $this + */ + public function saveInto(DataObjectInterface $record) + { + // Check required relation details are available + $fieldname = $this->getName(); + if (!$fieldname) { + return $this; + } + + $dataValue = $this->dataValue(); + $value = is_string($dataValue) ? $this->parseString($dataValue) : $dataValue; + + /** @var HasMany|Link[] $links */ + if ($links = $record->$fieldname()) { + foreach ($links as $linkDO) { + $linkData = $this->shiftLinkDataByID($value, $linkDO->ID); + if ($linkData) { + $linkDO->setData($linkData); + $linkDO->write(); + } else { + $linkDO->delete(); + } + } + + foreach ($value as $linkData) { + unset($linkData['ID']); + $linkDO = Link::create(); + $linkDO = $linkDO->setData($linkData); + $links->add($linkDO); + $linkDO->write(); + } + } + + return $this; + } + + /** + * Find a data entry that matches the given ID, and remove it from the array + */ + private function shiftLinkDataByID(array &$linkData, int $id): ?array + { + foreach ($linkData as $key => $link) { + if ($link['ID'] === $id) { + unset($linkData[$key]); + return $link; + } + } + + return null; + } +} diff --git a/src/GraphQL/LinkDescriptionResolver.php b/src/GraphQL/LinkDescriptionResolver.php index 2259f25d..0b201293 100644 --- a/src/GraphQL/LinkDescriptionResolver.php +++ b/src/GraphQL/LinkDescriptionResolver.php @@ -17,18 +17,37 @@ public static function resolve($obj, $args = [], $context = [], ?ResolveInfo $in throw new InvalidArgumentException('data must be a valid JSON string'); } - $typeKey = $data['typeKey'] ?? null; - - if (!$typeKey) { - return ['description' => '']; + if (self::hasStringKeys($data)) { + $data = [$data]; } - $type = Registry::singleton()->byKey($typeKey); + return array_map([self::class, 'resolveSingleDescription'], $data); + } - if (!$type) { - return ['description' => '']; + /** + * Return a description for a specific link + */ + private static function resolveSingleDescription($data): array + { + $id = isset($data['ID']) ? $data['ID'] : 0; + $description = ['title' => '', 'description' => '']; + + // If we don't have a valid typeKey, we'll return a blank description + if (!empty($data['typeKey'])) { + $type = Registry::singleton()->byKey($data['typeKey']); + if (!empty($type)) { + $description = $type->generateLinkDescription($data); + } } - return ['description' => $type->generateLinkDescription($data)]; + return array_merge(['id' => $id], $description); + } + + /** + * Check if our array is sequential or associative. We are assuming that an array with string key is associative. + */ + private static function hasStringKeys(array $array): bool + { + return count(array_filter(array_keys($array), 'is_string')) > 0; } } diff --git a/src/GraphQL/LinkTypeResolver.php b/src/GraphQL/LinkTypeResolver.php index a61f28f1..544d9ad8 100644 --- a/src/GraphQL/LinkTypeResolver.php +++ b/src/GraphQL/LinkTypeResolver.php @@ -13,7 +13,7 @@ class LinkTypeResolver extends Resolver public static function resolve($obj, $args = [], $context = [], ?ResolveInfo $info = null) { if (isset($args['keys']) && !is_array($args['keys'])) { - throw new InvalidArgumentException('If `keys` is provdied, it must be an array'); + throw new InvalidArgumentException('If `keys` is provided, it must be an array'); } $types = Registry::singleton()->list(); @@ -21,7 +21,8 @@ public static function resolve($obj, $args = [], $context = [], ?ResolveInfo $in return [ 'key' => $key, 'handlerName' => $type->LinkTypeHandlerName(), - 'title' => $type->LinkTypeTile() + 'title' => $type->LinkTypeTitle(), + 'icon' => $type->LinkTypeIcon() ]; }, $types, array_keys($types)); diff --git a/src/Models/EmailLink.php b/src/Models/EmailLink.php index f21db6be..6f1c6a28 100644 --- a/src/Models/EmailLink.php +++ b/src/Models/EmailLink.php @@ -18,9 +18,14 @@ class EmailLink extends Link 'Email' => 'Varchar(255)', ]; - public function generateLinkDescription(array $data): string + public function generateLinkDescription(array $data): array { - return isset($data['Email']) ? $data['Email'] : ''; + $description = isset($data['Email']) ? $data['Email'] : ''; + $title = empty($data['Title']) ? $description : $data['Title']; + return [ + 'title' => $title, + 'description' => $description + ]; } public function getCMSFields(): FieldList @@ -36,4 +41,9 @@ public function getURL(): string { return $this->Email ? sprintf('mailto:%s', $this->Email) : ''; } + + protected function FallbackTitle(): string + { + return $this->Email ?: ''; + } } diff --git a/src/Models/ExternalLink.php b/src/Models/ExternalLink.php index c51fb50c..673aec90 100644 --- a/src/Models/ExternalLink.php +++ b/src/Models/ExternalLink.php @@ -15,13 +15,25 @@ class ExternalLink extends Link 'ExternalUrl' => 'Varchar', ]; - public function generateLinkDescription(array $data): string + private static $icon = 'external-link'; + + public function generateLinkDescription(array $data): array { - return isset($data['ExternalUrl']) ? $data['ExternalUrl'] : ''; + $description = isset($data['ExternalUrl']) ? $data['ExternalUrl'] : ''; + $title = empty($data['Title']) ? $description : $data['Title']; + return [ + 'title' => $title, + 'description' => $description + ]; } public function getURL(): string { return $this->ExternalUrl ?? ''; } + + protected function FallbackTitle(): string + { + return $this->ExternalUrl ?: ''; + } } diff --git a/src/Models/FileLink.php b/src/Models/FileLink.php index 770124dd..b6c7dd2b 100644 --- a/src/Models/FileLink.php +++ b/src/Models/FileLink.php @@ -18,17 +18,27 @@ class FileLink extends Link 'File' => File::class, ]; - public function generateLinkDescription(array $data): string - { - $fileId = $data['FileID'] ?? null; + private static $icon = 'menu-files'; - if (!$fileId) { - return ''; + public function generateLinkDescription(array $data): array + { + $description = ''; + $title = empty($data['Title']) ? '' : $data['Title']; + + if (!empty($data['FileID'])) { + $file = File::get()->byID($data['FileID']); + if ($file) { + $description = $file->getFilename(); + if (empty($title)) { + $title = $file->Title; + } + } } - $file = File::get()->byID($fileId); - - return $file?->getFilename() ?? ''; + return [ + 'title' => $title, + 'description' => $description + ]; } public function LinkTypeHandlerName(): string @@ -40,4 +50,9 @@ public function getURL(): string { return $this->File?->getURL() ?? ''; } + + protected function FallbackTitle(): string + { + return $this->File ? $this->File->Title : ''; + } } diff --git a/src/Models/Link.php b/src/Models/Link.php index 1e582695..f007572a 100644 --- a/src/Models/Link.php +++ b/src/Models/Link.php @@ -9,6 +9,7 @@ use SilverStripe\Forms\CompositeValidator; use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FieldList; +use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\RequiredFields; use SilverStripe\LinkField\JsonData; use SilverStripe\LinkField\Type\Registry; @@ -40,6 +41,12 @@ class Link extends DataObject implements JsonData, Type */ private ?string $linkType = null; + private static $has_one = [ + 'Owner' => DataObject::class + ]; + + private static $icon = 'link'; + public function defineLinkTypeRequirements() { Requirements::add_i18n_javascript('silverstripe/linkfield:client/lang', false, true); @@ -52,16 +59,24 @@ public function LinkTypeHandlerName(): string return 'FormBuilderModal'; } - public function generateLinkDescription(array $data): string + public function generateLinkDescription(array $data): array { - return ''; + return [ + 'title' => '', + 'description' => '' + ]; } - public function LinkTypeTile(): string + public function LinkTypeTitle(): string { return $this->i18n_singular_name(); } + public function LinkTypeIcon(): string + { + return self::config()->get('icon'); + } + public function scaffoldLinkFields(array $data): FieldList { return $this->getCMSFields(); @@ -89,6 +104,8 @@ public function getCMSFields(): FieldList $linkTypeField->setEmptyString('-- select type --'); } + $fields->addFieldToTab('Root.Main', HiddenField::create('ID')); + return $fields; } @@ -192,6 +209,9 @@ public function jsonSerialize(): mixed } $data = $this->toMap(); + + $data['OpenInNew'] = boolval($data['OpenInNew']); + $data['typeKey'] = $typeKey; // Some of our models (SiteTreeLink in particular) have defined getTitle() methods. We *don't* want to override // the 'Title' field (which represent the literal 'Title' Database field) - if we did that, then it would also @@ -258,4 +278,17 @@ private function getLinkTypes(): array return $types; } + + /** + * Title for the link when rendered in the front end + */ + public function FrontendTitle(): string + { + return $this->Title ?: $this->FallbackTitle(); + } + + protected function FallbackTitle(): string + { + return ''; + } } diff --git a/src/Models/PhoneLink.php b/src/Models/PhoneLink.php index 855e3b1c..e4db2914 100644 --- a/src/Models/PhoneLink.php +++ b/src/Models/PhoneLink.php @@ -15,9 +15,15 @@ class PhoneLink extends Link 'Phone' => 'Varchar(255)', ]; - public function generateLinkDescription(array $data): string + public function generateLinkDescription(array $data): array { - return isset($data['Phone']) ? $data['Phone'] : ''; + if (isset($data)) { + return [ + 'title' => $data['Phone'] + ]; + } + + return []; } public function getURL(): string diff --git a/src/Models/SiteTreeLink.php b/src/Models/SiteTreeLink.php index 1312b4f2..0864a2b5 100644 --- a/src/Models/SiteTreeLink.php +++ b/src/Models/SiteTreeLink.php @@ -26,22 +26,27 @@ class SiteTreeLink extends Link 'Page' => SiteTree::class, ]; - public function generateLinkDescription(array $data): string - { - $pageId = $data['PageID'] ?? null; - - if (!$pageId) { - return ''; - } + private static $icon = 'page'; - /** @var SiteTree $page */ - $page = SiteTree::get()->byID($pageId); - - if (!$page?->exists()) { - return ''; + public function generateLinkDescription(array $data): array + { + $description = ''; + $title = empty($data['Title']) ? '' : $data['Title']; + + if (!empty($data['PageID'])) { + $page = SiteTree::get()->byID($data['PageID']); + if ($page) { + $description = $page->URLSegment; + if (empty($title)) { + $title = $page->Title; + } + } } - return $page->URLSegment ?: ''; + return [ + 'title' => $title, + 'description' => $description + ]; } public function getCMSFields(): FieldList @@ -84,28 +89,8 @@ public function getURL(): string return $url; } - /** - * Try to populate link title from page title in case we don't have a title yet - * - * @return string|null - */ - public function getTitle(): ?string + protected function FallbackTitle(): string { - $title = $this->getField('Title'); - - if ($title) { - // If we already have a title, we can just bail out without any changes - return $title; - } - - $page = $this->Page(); - - if (!$page?->exists()) { - // We don't have a page to fall back to - return null; - } - - // Use page title as a default value in case CMS user didn't provide the title - return $page->Title; + return $this->Page ? $this->Page->Title : ''; } } diff --git a/src/Type/Type.php b/src/Type/Type.php index 1d0cb428..94533fb1 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -20,15 +20,20 @@ public function defineLinkTypeRequirements(); */ public function LinkTypeHandlerName(): string; + /** + * Font icon to attach to this link type + */ + public function LinkTypeIcon(): string; + /** * What should be the link description be given this data. */ - public function generateLinkDescription(array $data): string; + public function generateLinkDescription(array $data): array; /** - * Human readbale title of this link type + * Human readable title of this link type */ - public function LinkTypeTile(): string; + public function LinkTypeTitle(): string; /** * Build a list of fields suitable to edit this link type diff --git a/tests/php/Form/MultiLinkFieldTest.php b/tests/php/Form/MultiLinkFieldTest.php new file mode 100644 index 00000000..4c67c7f9 --- /dev/null +++ b/tests/php/Form/MultiLinkFieldTest.php @@ -0,0 +1,122 @@ +setList($links); + $this->assertEquals($links, $field->getList(), 'setList should change the value returned by getList'); + } + + public function testSetValueWithExplicitList() + { + $owner = $this->objFromFixture(LinkOwner::class, 'link-owner-1'); + $links = Link::get(); + $field = new MultiLinkField('Test', 'Test', $links); + $field->setValue(null, $owner); + + $expectedValue = json_encode( + array_map(function (Link $link) { + return $link->jsonSerialize(); + }, $links->toArray()) + ); + + $this->assertEquals( + $expectedValue, + $field->Value(), + 'Value should be deduct from the list when no other data is provided' + ); + } + + public function testSetValueWithImplicitList() + { + $owner = $this->objFromFixture(LinkOwner::class, 'link-owner-1'); + $field = new MultiLinkField('Links'); + $field->setValue(null, $owner); + + $this->assertCount(1, $owner->Links(), 'My owner should only have one link'); + + $expectedValue = json_encode( + array_map(function (Link $link) { + return $link->jsonSerialize(); + }, $owner->Links()->toArray()) + ); + + $this->assertEquals( + $expectedValue, + $field->Value(), + 'Value should be deduct from the list matching the field name when the list is not explicitly set' + ); + } + + public function testSetValueWithJSONString() + { + $owner = $this->objFromFixture(LinkOwner::class, 'link-owner-1'); + $field = new MultiLinkField('Links'); + $field->setValue('[]', $owner); + + $this->assertEquals( + '[]', + $field->Value(), + 'When the value is explicitly set to a JSON string, when don\'t to read it from the data list' + ); + } + + + public function testSaveInto() + { + $owner = $this->objFromFixture(LinkOwner::class, 'link-owner-1'); + $linkID = $this->idFromFixture(ExternalLink::class, 'link-2'); + + $field = new MultiLinkField('Links'); + $submittedData = [ + [ + 'ID' => $linkID, + 'Title' => 'My update link', + 'ExternalUrl' => 'http://www.google.co.nz', + 'typeKey' => 'external', + ], + [ + 'Title' => 'My new email address', + 'OpenInNew' => 1, + 'Email' => 'maxime@example.com', + 'ID' => 'aebc8afd-7fbc-4503-bc8f-3fd459a3f2de', + 'typeKey' => 'email', + 'isNew' => true + ] + ]; + $field->setValue(json_encode($submittedData), $owner); + $field->saveInto($owner); + $owner->write(); + + $owner = $this->objFromFixture(LinkOwner::class, 'link-owner-1'); + $links = $owner->Links()->toArray(); + + $this->assertCount(2, $links, 'There should be two links'); + + $this->assertEquals('My update link', $links[0]->Title, 'The first link should have an updated title'); + $this->assertEquals($linkID, $links[0]->ID, 'The first link should still have the same ID'); + $this->assertEquals('http://www.google.co.nz', $links[0]->ExternalUrl, 'The first link URL should have been updated'); + + $this->assertEquals('My new email address', $links[1]->Title, 'The second link has the expected title'); + $this->assertNotEquals('aebc8afd-7fbc-4503-bc8f-3fd459a3f2de', $links[1]->ID, 'The second link should have a proper ID'); + $this->assertEquals('maxime@example.com', $links[1]->Email, 'The first link URL should have been updated'); + } +} diff --git a/tests/php/GraphQL/LinkDescriptionResolverTest.php b/tests/php/GraphQL/LinkDescriptionResolverTest.php new file mode 100644 index 00000000..b1bd0308 --- /dev/null +++ b/tests/php/GraphQL/LinkDescriptionResolverTest.php @@ -0,0 +1,77 @@ +expectException(\InvalidArgumentException::class); + LinkDescriptionResolver::resolve([], ['dataStr' => 'non-sense'], [], null); + } + + public function testListOfLinks() + { + $links = [ + [ + 'ID' => '1', + 'Title' => 'My update link', + 'ExternalUrl' => 'http://www.google.co.nz', + 'typeKey' => 'external', + ], + [ + 'Title' => 'My new email address', + 'OpenInNew' => 1, + 'Email' => 'maxime@example.com', + 'ID' => 'aebc8afd-7fbc-4503-bc8f-3fd459a3f2de', + 'typeKey' => 'email', + 'isNew' => true + ] + ]; + $expected = [ + [ + 'id' => '1', + 'title' => 'My update link', + 'description' => 'http://www.google.co.nz' + ], + [ + 'id' => 'aebc8afd-7fbc-4503-bc8f-3fd459a3f2de', + 'title' => 'My new email address', + 'description' => 'maxime@example.com' + ] + ]; + $this->assertEquals( + $expected, + LinkDescriptionResolver::resolve([], ['dataStr' => json_encode($links)], [], null), + 'Link list data should have been resolved to the expected description' + ); + } + + public function testSingleLink() + { + $link = [ + 'ID' => '1', + 'Title' => 'My update link', + 'ExternalUrl' => 'http://www.google.co.nz', + 'typeKey' => 'external', + ]; + + $results = LinkDescriptionResolver::resolve([], ['dataStr' => json_encode($link)], [], null); + $expected = [ + [ + 'id' => '1', + 'title' => 'My update link', + 'description' => 'http://www.google.co.nz' + ] + ]; + $this->assertEquals( + $expected, + LinkDescriptionResolver::resolve([], ['dataStr' => json_encode($link)], [], null), + 'Single Link data should have been resolved to the expected description' + ); + } +} diff --git a/tests/php/LinkModelTest.php b/tests/php/LinkModelTest.php new file mode 100644 index 00000000..c9874802 --- /dev/null +++ b/tests/php/LinkModelTest.php @@ -0,0 +1,22 @@ +objFromFixture(ExternalLink::class, 'link-1'); + + $this->assertEquals('FormBuilderModal', $model->LinkTypeHandlerName()); + } +} diff --git a/tests/php/LinkModelTest.yml b/tests/php/LinkModelTest.yml new file mode 100644 index 00000000..d4614449 --- /dev/null +++ b/tests/php/LinkModelTest.yml @@ -0,0 +1,14 @@ +SilverStripe\LinkField\Models\ExternalLink: + link-1: + Title: Link1 + ExternalUrl: 'https://silverstripe.org/' + link-2: + Title: Link1 + ExternalUrl: 'https://google.com/' + # Owner: =>SilverStripe\Link\Tests\LinkOwner.link-owner-1 + +SilverStripe\LinkField\Tests\LinkOwner: + link-owner-1: + Title: A Link owner + Links: + - =>SilverStripe\LinkField\Models\ExternalLink.link-2 diff --git a/tests/php/LinkOwner.php b/tests/php/LinkOwner.php new file mode 100644 index 00000000..92f6d0f2 --- /dev/null +++ b/tests/php/LinkOwner.php @@ -0,0 +1,18 @@ + 'Varchar', + ]; + private static $has_many = [ + 'Links' => Link::class, + ]; +} diff --git a/yarn.lock b/yarn.lock index 04a92b6d..683eec93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1040,6 +1040,21 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== +"@csstools/cascade-layer-name-parser@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.3.tgz#7f049a670c1e071102243ab6c392174844ca6cd7" + integrity sha512-ks9ysPP8012j90EQCCFtDsQIXOTCOpTQFIyyoRku06y8CXtUQ+8bXI8KVm9Q9ovwDUVthWuWKZWJD3u1rwnEfw== + +"@csstools/css-parser-algorithms@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz#0cc3a656dc2d638370ecf6f98358973bfbd00141" + integrity sha512-dTKSIHHWc0zPvcS5cqGP+/TPFUJB0ekJ9dGKvMAFoNuBFhDPBt9OMGNZiIA5vTiNdGHHBeScYPXIGBMnVOahsA== + +"@csstools/css-tokenizer@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz#07ae11a0a06365d7ec686549db7b729bc036528e" + integrity sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA== + "@discoveryjs/json-ext@0.5.7", "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -2414,6 +2429,18 @@ autoprefixer@^10.4.13: picocolors "^1.0.0" postcss-value-parser "^4.2.0" +autoprefixer@^10.4.14: + version "10.4.14" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" + integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== + dependencies: + browserslist "^4.21.5" + caniuse-lite "^1.0.30001464" + fraction.js "^4.2.0" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -2687,6 +2714,11 @@ caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz#871e35866b4654a7d25eccca86864f411825540c" integrity sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w== +caniuse-lite@^1.0.30001464: + version "1.0.30001514" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001514.tgz#e2a7e184a23affc9367b7c8d734e7ec4628c1309" + integrity sha512-ENcIpYBmwAAOm/V2cXgM7rZUrKKaqisZl4ZAI520FIkqGXUxJjmaIssbRW5HVVR5tyV6ygTLIm15aU8LUmQSaQ== + chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -3012,6 +3044,16 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +cosmiconfig@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" + integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ== + dependencies: + import-fresh "^3.2.1" + js-yaml "^4.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3190,9 +3232,9 @@ define-properties@^1.1.2, define-properties@^1.1.3: object-keys "^1.0.12" define-properties@^1.1.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" - integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== dependencies: has-property-descriptors "^1.0.0" object-keys "^1.1.1" @@ -3390,7 +3432,36 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: string.prototype.trimleft "^2.1.1" string.prototype.trimright "^2.1.1" -es-abstract@^1.19.0, es-abstract@^1.20.4: +es-abstract@^1.19.0, es-abstract@^1.19.5: + version "1.20.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" + integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + regexp.prototype.flags "^1.4.3" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-abstract@^1.20.4: version "1.21.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6" integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg== @@ -4187,7 +4258,16 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== @@ -4605,7 +4685,16 @@ inquirer@^0.12.0: strip-ansi "^3.0.0" through "^2.3.6" -internal-slot@^1.0.3, internal-slot@^1.0.4: +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +internal-slot@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== @@ -4695,6 +4784,11 @@ is-callable@^1.1.4, is-callable@^1.1.5: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== +is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + is-cidr@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-4.0.2.tgz#94c7585e4c6c77ceabf920f8cde51b8c0fda8814" @@ -5394,6 +5488,11 @@ jest-worker@^29.4.3: merge-stream "^2.0.0" supports-color "^8.0.0" +jiti@^1.18.2: + version "1.19.1" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.1.tgz#fa99e4b76a23053e0e7cde098efe1704a14c16f1" + integrity sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg== + js-sdsl@^4.1.4: version "4.3.0" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" @@ -6105,6 +6204,11 @@ nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6391,7 +6495,12 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-inspect@^1.12.2, object-inspect@^1.9.0: +object-inspect@^1.12.0, object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-inspect@^1.12.2: version "1.12.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== @@ -6429,7 +6538,17 @@ object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" -object.assign@^4.1.2, object.assign@^4.1.3, object.assign@^4.1.4: +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.assign@^4.1.3, object.assign@^4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== @@ -6714,6 +6833,16 @@ postcss-custom-properties@^12.1.10: dependencies: postcss-value-parser "^4.2.0" +postcss-custom-properties@^13.2.1: + version "13.2.1" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-13.2.1.tgz#82452ea09b796bf0271cc945badcca18ef59df1c" + integrity sha512-Z8UmzwVkRh8aITyeZoZnT4McSSPmS2EFl+OyPspfvx7v+N36V2UseMAODp3oBriZvcf/tQpzag9165x/VcC3kg== + dependencies: + "@csstools/cascade-layer-name-parser" "^1.0.3" + "@csstools/css-parser-algorithms" "^2.3.0" + "@csstools/css-tokenizer" "^2.1.1" + postcss-value-parser "^4.2.0" + postcss-load-config@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd" @@ -6731,15 +6860,24 @@ postcss-loader@^7.0.1: klona "^2.0.5" semver "^7.3.8" +postcss-loader@^7.3.3: + version "7.3.3" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.3.tgz#6da03e71a918ef49df1bb4be4c80401df8e249dd" + integrity sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA== + dependencies: + cosmiconfig "^8.2.0" + jiti "^1.18.2" + semver "^7.3.8" + postcss-modules-extract-imports@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524" + integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA== dependencies: icss-utils "^5.0.0" postcss-selector-parser "^6.0.2" @@ -6759,7 +6897,7 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: +postcss-selector-parser@^6.0.10: version "6.0.11" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== @@ -6767,12 +6905,20 @@ postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selecto cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: + version "6.0.13" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" + integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.2.14, postcss@^8.4.18, postcss@^8.4.19: +postcss@^8.2.14, postcss@^8.4.18: version "8.4.21" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== @@ -6781,6 +6927,15 @@ postcss@^8.2.14, postcss@^8.4.18, postcss@^8.4.19: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.19, postcss@^8.4.25: + version "8.4.25" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.25.tgz#4a133f5e379eda7f61e906c3b1aaa9b81292726f" + integrity sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -7099,9 +7254,9 @@ readable-stream@^2.2.2: util-deprecate "~1.0.1" readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -7411,7 +7566,7 @@ rx-lite@^3.1.2: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= -safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -7421,6 +7576,11 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -7771,6 +7931,15 @@ string.prototype.trimend@^1.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" +string.prototype.trimend@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" + integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + string.prototype.trimend@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" @@ -7806,6 +7975,15 @@ string.prototype.trimstart@^1.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" +string.prototype.trimstart@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" + integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + string.prototype.trimstart@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" @@ -8255,6 +8433,11 @@ util@^0.10.3: dependencies: inherits "2.0.3" +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-to-istanbul@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265"