diff --git a/README.md b/README.md index 1f45e9b2..07cd766c 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,22 @@ class ExternalLinkExtension extends Extension ``` +## Controlling what type of links can be created in a LinkField +By default, all `Link` subclasses can be created by a LinkField. This includes any custom `Link` subclasses defined in your projects or via third party module. +Developers can control the link types allowed for individual `LinkField`. The `setAllowedTypes` method only allow link types that have been provided as parameters. + +```php +$fields->addFieldsToTab( + 'Root.Main', + [ + MultiLinkField::create('PageLinkList') + ->setAllowedTypes([ SiteTreeLink::class ]), + Link::create('EmailLink') + ->setAllowedTypes([ EmailLink::class ]), + ], +); +``` + ## Unversioned links The `Link` model has the `Versioned` extension applied to it by default. If you wish for links to not be versioned, then remove the extension from the `Link` model in the projects `app/_config.php` file. diff --git a/_config/graphql.yml b/_config/graphql.yml deleted file mode 100644 index a353b4d1..00000000 --- a/_config/graphql.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -Name: linkgraphql ---- -SilverStripe\GraphQL\Schema\Schema: - schemas: - admin: - src: - link: silverstripe/linkfield:_graphql diff --git a/_config/types.yml b/_config/types.yml deleted file mode 100644 index 84960245..00000000 --- a/_config/types.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -Name: linkfield-types ---- -SilverStripe\LinkField\Type\Registry: - types: - cms: - classname: SilverStripe\LinkField\Models\SiteTreeLink - enabled: true - external: - classname: SilverStripe\LinkField\Models\ExternalLink - enabled: true - file: - classname: SilverStripe\LinkField\Models\FileLink - enabled: true - email: - classname: SilverStripe\LinkField\Models\EmailLink - enabled: true - phone: - classname: SilverStripe\LinkField\Models\PhoneLink - enabled: true diff --git a/_graphql/queries.yml b/_graphql/queries.yml deleted file mode 100644 index 8010e060..00000000 --- a/_graphql/queries.yml +++ /dev/null @@ -1,3 +0,0 @@ -'readLinkTypes(keys: [ID])': - type: '[LinkType]' - resolver: ['SilverStripe\LinkField\GraphQL\LinkTypeResolver', 'resolve'] diff --git a/_graphql/types.yml b/_graphql/types.yml deleted file mode 100644 index 8af9de12..00000000 --- a/_graphql/types.yml +++ /dev/null @@ -1,5 +0,0 @@ -LinkType: - description: Describe a Type of Link that can be managed by a LinkField - fields: - key: ID - title: String! diff --git a/client/dist/js/bundle.js b/client/dist/js/bundle.js index 0a48133f..4e0ce7f1 100644 --- a/client/dist/js/bundle.js +++ b/client/dist/js/bundle.js @@ -1 +1 @@ -!function(){"use strict";var e={274:function(e,t,n){var r=o(n(521)),l=o(n(154));function o(e){return e&&e.__esModule?e:{default:e}}document.addEventListener("DOMContentLoaded",(()=>{(0,r.default)(),(0,l.default)()}))},521:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(648)),l=u(n(809)),o=u(n(852)),a=u(n(117)),i=u(n(606));function u(e){return e&&e.__esModule?e:{default:e}}var s=()=>{r.default.component.registerMany({LinkPicker:l.default,LinkField:o.default,"LinkModal.FormBuilderModal":a.default,"LinkModal.InsertMediaModal":i.default})};t.default=s},154:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=o(n(648)),l=o(n(689));function o(e){return e&&e.__esModule?e:{default:e}}var a=()=>{r.default.query.register("readLinkTypes",l.default)};t.default=a},852:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=g(n(363)),l=n(827),o=n(624),a=n(648),i=m(n(42)),u=m(n(809)),s=m(n(734)),d=m(n(686)),f=m(n(697)),c=g(n(123)),p=m(n(159)),y=m(n(510)),v=m(n(86)),k=m(n(754));function m(e){return e&&e.__esModule?e:{default:e}}function _(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(_=function(e){return e?n:t})(e)}function g(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=_(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var a=l?Object.getOwnPropertyDescriptor(e,o):null;a&&(a.get||a.set)?Object.defineProperty(r,o,a):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}const h="SilverStripe\\LinkField\\Controllers\\LinkFieldController",O=e=>{var t;let{value:n=null,onChange:l,types:o,actions:a,isMulti:i=!1}=e;const[d,c]=(0,r.useState)({}),[v,m]=(0,r.useState)(0);let _=n;Array.isArray(_)||("number"==typeof _&&0!=_&&(_=[_]),_||(_=[])),(0,r.useEffect)((()=>{if(!v&&_.length>0){const e=[];for(const t of _)e.push(`itemIDs[]=${t}`);const t=`${y.default.getSection(h).form.linkForm.dataUrl}?${e.join("&")}`;p.default.get(t).then((e=>e.json())).then((e=>{c(e)}))}}),[v,n&&n.length]);const g=()=>{m(0)},O=e=>{m(0);const t=[..._];t.includes(e)||t.push(e),l(i?t:t[0]),a.toasts.success(k.default._t("LinkField.SAVE_SUCCESS","Saved link"))},b=e=>{const t=`${y.default.getSection(h).form.linkForm.deleteUrl}/${e}`;p.default.delete(t,{},{"X-SecurityID":y.default.get("SecurityID")}).then((()=>{a.toasts.success(k.default._t("LinkField.DELETE_SUCCESS","Deleted link"))})).catch((()=>{a.toasts.error(k.default._t("LinkField.DELETE_ERROR","Failed to delete link"))}));const n={...d};delete n[e],c(n),l(i?Object.keys(n):0)},M=i||0===Object.keys(d).length,j=Boolean(v);return r.default.createElement(r.default.Fragment,null,M&&r.default.createElement(u.default,{onModalSuccess:O,onModalClosed:g,types:o}),r.default.createElement("div",null," ",(()=>{const e=[];for(const u of _){var t,n,l,a,i;if(!d[u])continue;const f=o.hasOwnProperty(null===(t=d[u])||void 0===t?void 0:t.typeKey)?o[null===(n=d[u])||void 0===n?void 0:n.typeKey]:{};e.push(r.default.createElement(s.default,{key:u,id:u,title:null===(l=d[u])||void 0===l?void 0:l.Title,description:null===(a=d[u])||void 0===a?void 0:a.description,versionState:null===(i=d[u])||void 0===i?void 0:i.versionState,typeTitle:f.title||"",onClear:b,onClick:()=>{m(u)}}))}return e})()," "),j&&r.default.createElement(f.default,{types:o,typeKey:null===(t=d[v])||void 0===t?void 0:t.typeKey,isOpen:Boolean(v),onSuccess:O,onClosed:g,linkID:v}))};O.propTypes={value:v.default.oneOfType([v.default.arrayOf(v.default.number),v.default.number]),onChange:v.default.func.isRequired,types:v.default.objectOf(d.default).isRequired,actions:v.default.object.isRequired,isMulti:v.default.bool};var b=(0,l.compose)((0,a.injectGraphql)("readLinkTypes"),i.default,(0,o.connect)(null,(e=>({actions:{toasts:(0,l.bindActionCreators)(c,e)}}))))(O);t.default=b},606:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;s(n(754));var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=u(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var a=l?Object.getOwnPropertyDescriptor(e,o):null;a&&(a.get||a.set)?Object.defineProperty(r,o,a):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),l=s(n(475)),o=n(624),a=s(n(686)),i=s(n(86));function u(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(u=function(e){return e?n:t})(e)}function s(e){return e&&e.__esModule?e:{default:e}}function d(){return d=Object.assign?Object.assign.bind():function(e){for(var t=1;t{let{type:t,editing:n,data:o,actions:a,onSubmit:i,...u}=e;if(!t)return!1;(0,r.useEffect)((()=>{n?a.initModal():a.reset()}),[n]);const s=o?{ID:o.FileID,Description:o.Title,TargetBlank:!!o.OpenInNew}:{};return r.default.createElement(l.default,d({isOpen:n,type:"insert-link",title:!1,bodyClassName:"modal__dialog",className:"insert-link__dialog-wrapper--internal",fileAttributes:s,onInsert:e=>{let{ID:n,Description:r,TargetBlank:l}=e;return i({FileID:n,Title:r,OpenInNew:l,typeKey:t.key},"",(()=>{}))}},u))};f.propTypes={type:a.default.isRequired,editing:i.default.bool.isRequired,data:i.default.object.isRequired,actions:i.default.object.isRequired,onClick:i.default.func.isRequired};var c=(0,o.connect)((function(){return{}}),(function(e){return{actions:{initModal:()=>e({type:"INIT_FORM_SCHEMA_STACK",payload:{formSchema:{type:"insert-link",nextType:"admin"}}}),reset:()=>e({type:"RESET"})}}}))(f);t.default=c},117:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=s(n(363)),l=s(n(912)),o=s(n(872)),a=s(n(902)),i=s(n(510)),u=s(n(86));function s(e){return e&&e.__esModule?e:{default:e}}const d=(e,t)=>{const{schemaUrl:n}=i.default.getSection("SilverStripe\\LinkField\\Controllers\\LinkFieldController").form.linkForm,r=o.default.parse(n),l=a.default.parse(r.query);l.typeKey=e;for(const e of["href","path","pathname"])r[e]=`${r[e]}/${t}`;return o.default.format({...r,search:a.default.stringify(l)})},f=e=>{let{typeTitle:t,typeKey:n,linkID:o=0,isOpen:a,onSuccess:i,onClosed:u}=e;if(!n)return!1;return r.default.createElement(l.default,{title:t,isOpen:a,schemaUrl:d(n,o),identifier:"Link.EditingLinkInfo",onSubmit:async(e,t,n)=>{const r=await n();if(!r.id.match(/\/schema\/linkfield\/([0-9]+)/)){const e=r.id.match(/\/linkForm\/([0-9]+)/),t=parseInt(e[1],10);i(t)}return Promise.resolve()},onClosed:u})};f.propTypes={typeTitle:u.default.string.isRequired,typeKey:u.default.string.isRequired,linkID:u.default.number,isOpen:u.default.bool.isRequired,onSuccess:u.default.func.isRequired,onClosed:u.default.func.isRequired};var c=f;t.default=c},809:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=d(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var a=l?Object.getOwnPropertyDescriptor(e,o):null;a&&(a.get||a.set)?Object.defineProperty(r,o,a):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),l=s(n(86)),o=s(n(820)),a=s(n(97)),i=s(n(686)),u=s(n(697));function s(e){return e&&e.__esModule?e:{default:e}}function d(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(d=function(e){return e?n:t})(e)}const f=e=>{let{types:t,onModalSuccess:n,onModalClosed:l}=e;const[i,s]=(0,r.useState)(""),d=""!==i,f=(0,o.default)("link-picker","form-control"),c=Object.values(t);return r.default.createElement("div",{className:f},r.default.createElement(a.default,{types:c,onSelect:e=>{s(e)}}),d&&r.default.createElement(u.default,{types:t,typeKey:i,isOpen:d,onSuccess:e=>{s(""),n(e)},onClosed:()=>{"function"==typeof l&&l(),s("")}}))};t.Component=f,f.propTypes={types:l.default.objectOf(i.default).isRequired,onModalSuccess:l.default.func.isRequired,onModalClosed:l.default.func};var c=f;t.default=c},97:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=s(n(754)),l=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=u(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var a=l?Object.getOwnPropertyDescriptor(e,o):null;a&&(a.get||a.set)?Object.defineProperty(r,o,a):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),o=s(n(86)),a=n(127),i=s(n(686));function u(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(u=function(e){return e?n:t})(e)}function s(e){return e&&e.__esModule?e:{default:e}}const d=e=>{let{types:t,onSelect:n}=e;const[o,i]=(0,l.useState)(!1);return l.default.createElement(a.Dropdown,{isOpen:o,toggle:()=>i((e=>!e)),className:"link-picker__menu"},l.default.createElement(a.DropdownToggle,{className:"link-picker__menu-toggle font-icon-plus-1",caret:!0},r.default._t("LinkField.ADD_LINK","Add Link")),l.default.createElement(a.DropdownMenu,null,t.map((e=>{let{key:t,title:r}=e;return l.default.createElement(a.DropdownItem,{key:t,onClick:()=>n(t)},r)}))))};d.propTypes={types:o.default.arrayOf(i.default).isRequired,onSelect:o.default.func.isRequired};var f=d;t.default=f},734:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(820)),l=u(n(754)),o=u(n(363)),a=u(n(86)),i=n(127);function u(e){return e&&e.__esModule?e:{default:e}}const s=e=>t=>{t.nativeEvent.stopImmediatePropagation(),t.preventDefault(),t.nativeEvent.preventDefault(),t.stopPropagation(),e&&e()},d=e=>{let{id:t,title:n,description:a,versionState:u,typeTitle:d,onClear:f,onClick:c}=e;const p={"link-picker__link":!0,"form-control":!0};u&&(p[` link-picker__link--${u}`]=!0),n&&n.length>25&&(n=n.substring(0,25)+"...");const y=(0,r.default)(p);return o.default.createElement("div",{className:y},o.default.createElement(i.Button,{className:"link-picker__button font-icon-link",color:"secondary",onClick:s(c)},o.default.createElement("div",{className:"link-picker__link-detail"},o.default.createElement("div",{className:"link-picker__title"},o.default.createElement("span",{className:"link-picker__title-text"},n),(e=>{let t="",n="";if("draft"===e)t=l.default._t("LinkField.LINK_DRAFT_TITLE","Link has draft changes"),n=l.default._t("LinkField.LINK_DRAFT_LABEL","Draft");else{if("modified"!==e)return null;t=l.default._t("LinkField.LINK_MODIFIED_TITLE","Link has unpublished changes"),n=l.default._t("LinkField.LINK_MODIFIED_LABEL","Modified")}const a=(0,r.default)("badge",`status-${e}`);return o.default.createElement("span",{className:a,title:t},n)})(u)),o.default.createElement("small",{className:"link-picker__type"},d,": ",o.default.createElement("span",{className:"link-picker__url"},a)))),o.default.createElement(i.Button,{className:"link-picker__clear",color:"link",onClick:s((()=>f(t)))},l.default._t("LinkField.CLEAR","Clear")))};d.propTypes={id:a.default.number.isRequired,title:a.default.string,description:a.default.string,versionState:a.default.string,typeTitle:a.default.string.isRequired,onClear:a.default.func.isRequired,onClick:a.default.func.isRequired};var f=d;t.default=f},697:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=i(n(363)),l=n(648),o=i(n(86)),a=i(n(686));function i(e){return e&&e.__esModule?e:{default:e}}const u=e=>{let{types:t,typeKey:n,linkID:o=0,isOpen:a,onSuccess:i,onClosed:u}=e;if(!n)return!1;const s=t.hasOwnProperty(n)?t[n]:{},d=s&&s.hasOwnProperty("handlerName")?s.handlerName:"FormBuilderModal",f=(0,l.loadComponent)(`LinkModal.${d}`);return r.default.createElement(f,{typeTitle:s.title||"",typeKey:n,linkID:o,isOpen:a,onSuccess:i,onClosed:u})};u.propTypes={types:o.default.objectOf(a.default).isRequired,typeKey:o.default.string.isRequired,linkID:o.default.number,isOpen:o.default.bool.isRequired,onSuccess:o.default.func.isRequired,onClosed:o.default.func.isRequired};var s=u;t.default=s},41:function(e,t,n){var r=i(n(311)),l=i(n(363)),o=i(n(691)),a=n(648);function i(e){return e&&e.__esModule?e:{default:e}}function u(){return u=Object.assign?Object.assign.bind():function(e){for(var t=1;t{e(".js-injector-boot .entwine-linkfield").entwine({Component:null,Root:null,onmatch(){const e=this.closest(".cms-content").attr("id"),t=e?{context:e}:{},n=this.data("schema-component"),r=(0,a.loadComponent)(n,t);this.setComponent(r),this.setRoot(o.default.createRoot(this[0])),this._super(),this.refresh()},refresh(){const e=this.getProps();this.getInputField().val(e.value);const t=this.getComponent();this.getRoot().render(l.default.createElement(t,u({},e,{noHolder:!0})))},handleChange(e){this.getInputField().data("value",e),this.refresh()},getProps(){return{value:this.getInputField().data("value"),onChange:this.handleChange.bind(this),isMulti:this.data("is-multi")??!1}},getInputField(){const t=this.data("field-id");return e(`#${t}`)},onunmatch(){const e=this.getRoot();e&&e.unmount()}})}))},689:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=n(648);const l={props(e){const{data:{error:t,readLinkTypes:n,loading:r}}=e,l=t&&t.graphQLErrors&&t.graphQLErrors.map((e=>e.message));return{loading:r,types:n?n.reduce(((e,t)=>({...e,[t.key]:t})),{}):{},graphQLErrors:l}}},{READ:o}=r.graphqlTemplates;var a={apolloConfig:l,templateName:o,pluralName:"LinkTypes",pagination:!1,params:{keys:"[ID]"},args:{root:{keys:"keys"}},fields:["key","title"]};t.default=a},686:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,l=(r=n(86))&&r.__esModule?r:{default:r};var o=l.default.shape({key:l.default.string.isRequired,title:l.default.string.isRequired});t.default=o},159:function(e){e.exports=Backend},510:function(e){e.exports=Config},42:function(e){e.exports=FieldHolder},912:function(e){e.exports=FormBuilderModal},648:function(e){e.exports=Injector},475:function(e){e.exports=InsertMediaModal},872:function(e){e.exports=NodeUrl},86:function(e){e.exports=PropTypes},363:function(e){e.exports=React},691:function(e){e.exports=ReactDomClient},624:function(e){e.exports=ReactRedux},127:function(e){e.exports=Reactstrap},827:function(e){e.exports=Redux},123:function(e){e.exports=ToastsActions},820:function(e){e.exports=classnames},754:function(e){e.exports=i18n},311:function(e){e.exports=jQuery},902:function(e){e.exports=qs}},t={};function n(r){var l=t[r];if(void 0!==l)return l.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n(274),n(41)}(); \ No newline at end of file +!function(){"use strict";var e={274:function(e,t,n){var r,l=(r=n(521))&&r.__esModule?r:{default:r};document.addEventListener("DOMContentLoaded",(()=>{(0,l.default)()}))},521:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(648)),l=u(n(809)),o=u(n(852)),i=u(n(117)),a=u(n(606));function u(e){return e&&e.__esModule?e:{default:e}}var s=()=>{r.default.component.registerMany({LinkPicker:l.default,LinkField:o.default,"LinkModal.FormBuilderModal":i.default,"LinkModal.InsertMediaModal":a.default})};t.default=s},852:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=_(n(363)),l=n(827),o=n(624),i=(n(648),m(n(42))),a=m(n(809)),u=m(n(734)),s=m(n(686)),d=m(n(697)),f=_(n(123)),c=m(n(159)),p=m(n(510)),y=m(n(86)),v=m(n(754));function m(e){return e&&e.__esModule?e:{default:e}}function k(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(k=function(e){return e?n:t})(e)}function _(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=k(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=l?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}const h="SilverStripe\\LinkField\\Controllers\\LinkFieldController",O=e=>{var t;let{value:n=null,onChange:l,types:o=[],actions:i,isMulti:s=!1}=e;const[f,y]=(0,r.useState)({}),[m,k]=(0,r.useState)(0);let _=n;Array.isArray(_)||("number"==typeof _&&0!=_&&(_=[_]),_||(_=[])),(0,r.useEffect)((()=>{if(!m&&_.length>0){const e=[];for(const t of _)e.push(`itemIDs[]=${t}`);const t=`${p.default.getSection(h).form.linkForm.dataUrl}?${e.join("&")}`;c.default.get(t).then((e=>e.json())).then((e=>{y(e)}))}}),[m,n&&n.length]);const O=()=>{k(0)},g=e=>{k(0);const t=[..._];t.includes(e)||t.push(e),l(s?t:t[0]),i.toasts.success(v.default._t("LinkField.SAVE_SUCCESS","Saved link"))},b=e=>{const t=`${p.default.getSection(h).form.linkForm.deleteUrl}/${e}`;c.default.delete(t,{},{"X-SecurityID":p.default.get("SecurityID")}).then((()=>{i.toasts.success(v.default._t("LinkField.DELETE_SUCCESS","Deleted link"))})).catch((()=>{i.toasts.error(v.default._t("LinkField.DELETE_ERROR","Failed to delete link"))}));const n={...f};delete n[e],y(n),l(s?Object.keys(n):0)},M=s||0===Object.keys(f).length,j=Boolean(m);return r.default.createElement(r.default.Fragment,null,M&&r.default.createElement(a.default,{onModalSuccess:g,onModalClosed:O,types:o}),r.default.createElement("div",null," ",(()=>{const e=[];for(const s of _){var t,n,l,i,a;if(!f[s])continue;const d=o.hasOwnProperty(null===(t=f[s])||void 0===t?void 0:t.typeKey)?o[null===(n=f[s])||void 0===n?void 0:n.typeKey]:{};e.push(r.default.createElement(u.default,{key:s,id:s,title:null===(l=f[s])||void 0===l?void 0:l.Title,description:null===(i=f[s])||void 0===i?void 0:i.description,versionState:null===(a=f[s])||void 0===a?void 0:a.versionState,typeTitle:d.title||"",onClear:b,onClick:()=>{k(s)}}))}return e})()," "),j&&r.default.createElement(d.default,{types:o,typeKey:null===(t=f[m])||void 0===t?void 0:t.typeKey,isOpen:Boolean(m),onSuccess:g,onClosed:O,linkID:m}))};O.propTypes={value:y.default.oneOfType([y.default.arrayOf(y.default.number),y.default.number]),onChange:y.default.func.isRequired,types:y.default.objectOf(s.default).isRequired,actions:y.default.object.isRequired,isMulti:y.default.bool};var g=(0,l.compose)(i.default,(0,o.connect)(null,(e=>({actions:{toasts:(0,l.bindActionCreators)(f,e)}}))))(O);t.default=g},606:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;s(n(754));var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=u(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=l?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),l=s(n(475)),o=n(624),i=s(n(686)),a=s(n(86));function u(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(u=function(e){return e?n:t})(e)}function s(e){return e&&e.__esModule?e:{default:e}}function d(){return d=Object.assign?Object.assign.bind():function(e){for(var t=1;t{let{type:t,editing:n,data:o,actions:i,onSubmit:a,...u}=e;if(!t)return!1;(0,r.useEffect)((()=>{n?i.initModal():i.reset()}),[n]);const s=o?{ID:o.FileID,Description:o.Title,TargetBlank:!!o.OpenInNew}:{};return r.default.createElement(l.default,d({isOpen:n,type:"insert-link",title:!1,bodyClassName:"modal__dialog",className:"insert-link__dialog-wrapper--internal",fileAttributes:s,onInsert:e=>{let{ID:n,Description:r,TargetBlank:l}=e;return a({FileID:n,Title:r,OpenInNew:l,typeKey:t.key},"",(()=>{}))}},u))};f.propTypes={type:i.default.isRequired,editing:a.default.bool.isRequired,data:a.default.object.isRequired,actions:a.default.object.isRequired,onClick:a.default.func.isRequired};var c=(0,o.connect)((function(){return{}}),(function(e){return{actions:{initModal:()=>e({type:"INIT_FORM_SCHEMA_STACK",payload:{formSchema:{type:"insert-link",nextType:"admin"}}}),reset:()=>e({type:"RESET"})}}}))(f);t.default=c},117:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=s(n(363)),l=s(n(912)),o=s(n(872)),i=s(n(902)),a=s(n(510)),u=s(n(86));function s(e){return e&&e.__esModule?e:{default:e}}const d=(e,t)=>{const{schemaUrl:n}=a.default.getSection("SilverStripe\\LinkField\\Controllers\\LinkFieldController").form.linkForm,r=o.default.parse(n),l=i.default.parse(r.query);l.typeKey=e;for(const e of["href","path","pathname"])r[e]=`${r[e]}/${t}`;return o.default.format({...r,search:i.default.stringify(l)})},f=e=>{let{typeTitle:t,typeKey:n,linkID:o=0,isOpen:i,onSuccess:a,onClosed:u}=e;if(!n)return!1;return r.default.createElement(l.default,{title:t,isOpen:i,schemaUrl:d(n,o),identifier:"Link.EditingLinkInfo",onSubmit:async(e,t,n)=>{const r=await n();if(!r.id.match(/\/schema\/linkfield\/([0-9]+)/)){const e=r.id.match(/\/linkForm\/([0-9]+)/),t=parseInt(e[1],10);a(t)}return Promise.resolve()},onClosed:u})};f.propTypes={typeTitle:u.default.string.isRequired,typeKey:u.default.string.isRequired,linkID:u.default.number,isOpen:u.default.bool.isRequired,onSuccess:u.default.func.isRequired,onClosed:u.default.func.isRequired};var c=f;t.default=c},809:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=d(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=l?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),l=s(n(86)),o=s(n(820)),i=s(n(97)),a=s(n(686)),u=s(n(697));function s(e){return e&&e.__esModule?e:{default:e}}function d(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(d=function(e){return e?n:t})(e)}const f=e=>{let{types:t,onModalSuccess:n,onModalClosed:l}=e;const[a,s]=(0,r.useState)(""),d=""!==a,f=(0,o.default)("link-picker","form-control"),c=Object.values(t);return r.default.createElement("div",{className:f},r.default.createElement(i.default,{types:c,onSelect:e=>{s(e)}}),d&&r.default.createElement(u.default,{types:t,typeKey:a,isOpen:d,onSuccess:e=>{s(""),n(e)},onClosed:()=>{"function"==typeof l&&l(),s("")}}))};t.Component=f,f.propTypes={types:l.default.objectOf(a.default).isRequired,onModalSuccess:l.default.func.isRequired,onModalClosed:l.default.func};var c=f;t.default=c},97:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=s(n(754)),l=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=u(t);if(n&&n.has(e))return n.get(e);var r={},l=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&Object.prototype.hasOwnProperty.call(e,o)){var i=l?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,n&&n.set(e,r);return r}(n(363)),o=s(n(86)),i=n(127),a=s(n(686));function u(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(u=function(e){return e?n:t})(e)}function s(e){return e&&e.__esModule?e:{default:e}}const d=e=>{let{types:t,onSelect:n}=e;const[o,a]=(0,l.useState)(!1);return l.default.createElement(i.Dropdown,{isOpen:o,toggle:()=>a((e=>!e)),className:"link-picker__menu"},l.default.createElement(i.DropdownToggle,{className:"link-picker__menu-toggle font-icon-plus-1",caret:!0},r.default._t("LinkField.ADD_LINK","Add Link")),l.default.createElement(i.DropdownMenu,null,t.map((e=>{let{key:t,title:r}=e;return l.default.createElement(i.DropdownItem,{key:t,onClick:()=>n(t)},r)}))))};d.propTypes={types:o.default.arrayOf(a.default).isRequired,onSelect:o.default.func.isRequired};var f=d;t.default=f},734:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=u(n(820)),l=u(n(754)),o=u(n(363)),i=u(n(86)),a=n(127);function u(e){return e&&e.__esModule?e:{default:e}}const s=e=>t=>{t.nativeEvent.stopImmediatePropagation(),t.preventDefault(),t.nativeEvent.preventDefault(),t.stopPropagation(),e&&e()},d=e=>{let{id:t,title:n,description:i,versionState:u,typeTitle:d,onClear:f,onClick:c}=e;const p={"link-picker__link":!0,"form-control":!0};u&&(p[` link-picker__link--${u}`]=!0),n&&n.length>25&&(n=n.substring(0,25)+"...");const y=(0,r.default)(p);return o.default.createElement("div",{className:y},o.default.createElement(a.Button,{className:"link-picker__button font-icon-link",color:"secondary",onClick:s(c)},o.default.createElement("div",{className:"link-picker__link-detail"},o.default.createElement("div",{className:"link-picker__title"},o.default.createElement("span",{className:"link-picker__title-text"},n),(e=>{let t="",n="";if("draft"===e)t=l.default._t("LinkField.LINK_DRAFT_TITLE","Link has draft changes"),n=l.default._t("LinkField.LINK_DRAFT_LABEL","Draft");else{if("modified"!==e)return null;t=l.default._t("LinkField.LINK_MODIFIED_TITLE","Link has unpublished changes"),n=l.default._t("LinkField.LINK_MODIFIED_LABEL","Modified")}const i=(0,r.default)("badge",`status-${e}`);return o.default.createElement("span",{className:i,title:t},n)})(u)),o.default.createElement("small",{className:"link-picker__type"},d,": ",o.default.createElement("span",{className:"link-picker__url"},i)))),o.default.createElement(a.Button,{className:"link-picker__clear",color:"link",onClick:s((()=>f(t)))},l.default._t("LinkField.CLEAR","Clear")))};d.propTypes={id:i.default.number.isRequired,title:i.default.string,description:i.default.string,versionState:i.default.string,typeTitle:i.default.string.isRequired,onClear:i.default.func.isRequired,onClick:i.default.func.isRequired};var f=d;t.default=f},697:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=a(n(363)),l=n(648),o=a(n(86)),i=a(n(686));function a(e){return e&&e.__esModule?e:{default:e}}const u=e=>{let{types:t,typeKey:n,linkID:o=0,isOpen:i,onSuccess:a,onClosed:u}=e;if(!n)return!1;const s=t.hasOwnProperty(n)?t[n]:{},d=s&&s.hasOwnProperty("handlerName")?s.handlerName:"FormBuilderModal",f=(0,l.loadComponent)(`LinkModal.${d}`);return r.default.createElement(f,{typeTitle:s.title||"",typeKey:n,linkID:o,isOpen:i,onSuccess:a,onClosed:u})};u.propTypes={types:o.default.objectOf(i.default).isRequired,typeKey:o.default.string.isRequired,linkID:o.default.number,isOpen:o.default.bool.isRequired,onSuccess:o.default.func.isRequired,onClosed:o.default.func.isRequired};var s=u;t.default=s},41:function(e,t,n){var r=a(n(311)),l=a(n(363)),o=a(n(691)),i=n(648);function a(e){return e&&e.__esModule?e:{default:e}}function u(){return u=Object.assign?Object.assign.bind():function(e){for(var t=1;t{e(".js-injector-boot .entwine-linkfield").entwine({Component:null,Root:null,onmatch(){const e=this.closest(".cms-content").attr("id"),t=e?{context:e}:{},n=this.data("schema-component"),r=(0,i.loadComponent)(n,t);this.setComponent(r),this.setRoot(o.default.createRoot(this[0])),this._super(),this.refresh()},refresh(){const e=this.getProps();this.getInputField().val(e.value);const t=this.getComponent();this.getRoot().render(l.default.createElement(t,u({},e,{noHolder:!0})))},handleChange(e){this.getInputField().data("value",e),this.refresh()},getProps(){return{value:this.getInputField().data("value"),onChange:this.handleChange.bind(this),isMulti:this.data("is-multi")??!1,types:this.data("types")??[]}},getInputField(){const t=this.data("field-id");return e(`#${t}`)},onunmatch(){const e=this.getRoot();e&&e.unmount()}})}))},686:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,l=(r=n(86))&&r.__esModule?r:{default:r};var o=l.default.shape({key:l.default.string.isRequired,title:l.default.string.isRequired});t.default=o},159:function(e){e.exports=Backend},510:function(e){e.exports=Config},42:function(e){e.exports=FieldHolder},912:function(e){e.exports=FormBuilderModal},648:function(e){e.exports=Injector},475:function(e){e.exports=InsertMediaModal},872:function(e){e.exports=NodeUrl},86:function(e){e.exports=PropTypes},363:function(e){e.exports=React},691:function(e){e.exports=ReactDomClient},624:function(e){e.exports=ReactRedux},127:function(e){e.exports=Reactstrap},827:function(e){e.exports=Redux},123:function(e){e.exports=ToastsActions},820:function(e){e.exports=classnames},754:function(e){e.exports=i18n},311:function(e){e.exports=jQuery},902:function(e){e.exports=qs}},t={};function n(r){var l=t[r];if(void 0!==l)return l.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n(274),n(41)}(); \ No newline at end of file diff --git a/client/src/boot/index.js b/client/src/boot/index.js index 7fac11d0..3dbbb99b 100644 --- a/client/src/boot/index.js +++ b/client/src/boot/index.js @@ -1,9 +1,7 @@ /* global document */ /* eslint-disable */ import registerComponents from './registerComponents'; -import registerQueries from './registerQueries'; document.addEventListener('DOMContentLoaded', () => { registerComponents(); - registerQueries(); }); diff --git a/client/src/boot/registerQueries.js b/client/src/boot/registerQueries.js deleted file mode 100644 index 229ae7d9..00000000 --- a/client/src/boot/registerQueries.js +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable */ -import Injector from 'lib/Injector'; -import readLinkTypes from 'state/linkTypes/readLinkTypes'; - -const registerQueries = () => { - Injector.query.register('readLinkTypes', readLinkTypes); -}; -export default registerQueries; diff --git a/client/src/components/LinkField/LinkField.js b/client/src/components/LinkField/LinkField.js index a336a9a8..d4b493dc 100644 --- a/client/src/components/LinkField/LinkField.js +++ b/client/src/components/LinkField/LinkField.js @@ -18,13 +18,13 @@ import i18n from 'i18n'; const section = 'SilverStripe\\LinkField\\Controllers\\LinkFieldController'; /** - * value - ID of the Link passed from JsonField - * onChange - callback function passed from JsonField - used to update the underlying form field - * types - injected by the GraphQL query + * value - ID of the Link passed from LinkField entwine + * onChange - callback function passed from LinkField entwine - used to update the underlying form field + * types - types of the Link passed from LinkField entwine * actions - object of redux actions * isMulti - whether this field handles multiple links or not */ -const LinkField = ({ value = null, onChange, types, actions, isMulti = false }) => { +const LinkField = ({ value = null, onChange, types = [], actions, isMulti = false }) => { const [data, setData] = useState({}); const [editingID, setEditingID] = useState(0); @@ -184,7 +184,6 @@ const mapDispatchToProps = (dispatch) => ({ }); export default compose( - injectGraphql('readLinkTypes'), fieldHolder, connect(null, mapDispatchToProps) )(LinkField); diff --git a/client/src/entwine/LinkField.js b/client/src/entwine/LinkField.js index 0c8b789b..ccd45f9f 100644 --- a/client/src/entwine/LinkField.js +++ b/client/src/entwine/LinkField.js @@ -50,6 +50,7 @@ jQuery.entwine('ss', ($) => { value, onChange: this.handleChange.bind(this), isMulti: this.data('is-multi') ?? false, + types: this.data('types') ?? [], }; }, diff --git a/client/src/state/linkTypes/readLinkTypes.js b/client/src/state/linkTypes/readLinkTypes.js deleted file mode 100644 index 932032d0..00000000 --- a/client/src/state/linkTypes/readLinkTypes.js +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable */ -import { graphqlTemplates } from 'lib/Injector'; - -const apolloConfig = { - props( - props - ) { - const { - data: { - error, - readLinkTypes, - loading: networkLoading, - }, - } = props; - const errors = error && error.graphQLErrors && - error.graphQLErrors.map((graphQLError) => graphQLError.message); - - const types = readLinkTypes ? - readLinkTypes.reduce((accumulator, type) => ( - { ...accumulator, [type.key]: type } - ), {}) : - {}; - - return { - loading: networkLoading, - types, - graphQLErrors: errors, - }; - }, -}; - -const { READ } = graphqlTemplates; -const query = { - apolloConfig, - templateName: READ, - pluralName: 'LinkTypes', - pagination: false, - params: { - keys: '[ID]' - }, - args: { - root: { - keys: 'keys' - } - }, - fields: ['key', 'title'], -}; -export default query; diff --git a/lang/en.yml b/lang/en.yml index b860a7f1..abbba2b1 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -39,3 +39,6 @@ en: MISSING_DEFAULT_TITLE: 'Page missing' SilverStripe\LinkField\Models\FileLink: MISSING_DEFAULT_TITLE: 'File missing' + SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait: + INVALID_TYPECLASS: '"{class}": {typeclass} is not a valid Link Type' + INVALID_TYPECLASS_EMPTY: '"{class}": Allowed types cannot be empty' diff --git a/src/Controllers/LinkFieldController.php b/src/Controllers/LinkFieldController.php index 5340a077..27ff9db5 100644 --- a/src/Controllers/LinkFieldController.php +++ b/src/Controllers/LinkFieldController.php @@ -8,7 +8,6 @@ use SilverStripe\Forms\DefaultFormFactory; use SilverStripe\Forms\Form; use SilverStripe\LinkField\Models\Link; -use SilverStripe\LinkField\Type\Registry; use SilverStripe\Security\SecurityToken; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FormAction; @@ -19,6 +18,8 @@ use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\HiddenField; +use SilverStripe\LinkField\Form\LinkField; +use SilverStripe\LinkField\Services\LinkTypeService; use SilverStripe\ORM\DataList; class LinkFieldController extends LeftAndMain @@ -74,7 +75,7 @@ public function linkForm(): Form } } else { $typeKey = $this->typeKeyFromRequest(); - $link = Registry::create()->byKey($typeKey); + $link = LinkTypeService::create()->byKey($typeKey); if (!$link) { $this->jsonError(404, _t('LinkField.INVALID_TYPEKEY', 'Invalid typeKey')); } @@ -175,7 +176,7 @@ public function save(array $data, Form $form): HTTPResponse // Creating a new Link $operation = 'create'; $typeKey = $this->typeKeyFromRequest(); - $className = Registry::create()->list()[$typeKey] ?? ''; + $className = LinkTypeService::create()->byKey($typeKey) ?? ''; if (!$className) { $this->jsonError(404, _t('LinkField.INVALID_TYPEKEY', 'Invalid typeKey')); } @@ -240,7 +241,7 @@ private function createLinkForm(Link $link, string $operation): Form $form = $formFactory->getForm($this, $name, ['Record' => $link]); // Set where the form is submitted to - $typeKey = Registry::create()->keyByClassName($link->ClassName); + $typeKey = LinkTypeService::create()->keyByClassName($link->ClassName); $form->setFormAction($this->Link("linkForm/$id?typeKey=$typeKey")); // Add save action button diff --git a/src/Form/LinkField.php b/src/Form/LinkField.php index e02a3d98..62ffbe12 100644 --- a/src/Form/LinkField.php +++ b/src/Form/LinkField.php @@ -7,12 +7,15 @@ use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\LinkField\Models\Link; +use SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait; /** * Allows CMS users to edit a Link object. */ class LinkField extends FormField { + use AllowedLinkClassesTrait; + protected $schemaComponent = 'LinkField'; protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_CUSTOM; @@ -63,4 +66,11 @@ protected function getDefaultAttributes(): array $attributes['data-value'] = $this->Value(); return $attributes; } + + public function getSchemaDataDefaults() + { + $data = parent::getSchemaDataDefaults(); + $data['types'] = json_decode($this->getTypesProps()); + return $data; + } } diff --git a/src/Form/MultiLinkField.php b/src/Form/MultiLinkField.php index f4874b95..76632909 100644 --- a/src/Form/MultiLinkField.php +++ b/src/Form/MultiLinkField.php @@ -4,6 +4,7 @@ use LogicException; use SilverStripe\Forms\FormField; +use SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\Relation; @@ -16,6 +17,8 @@ */ class MultiLinkField extends FormField { + use AllowedLinkClassesTrait; + protected $schemaComponent = 'LinkField'; protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_CUSTOM; @@ -61,6 +64,7 @@ public function getSchemaDataDefaults() { $data = parent::getSchemaDataDefaults(); $data['isMulti'] = true; + $data['types'] = json_decode($this->getTypesProps()); return $data; } diff --git a/src/Form/Traits/AllowedLinkClassesTrait.php b/src/Form/Traits/AllowedLinkClassesTrait.php new file mode 100644 index 00000000..d704a81f --- /dev/null +++ b/src/Form/Traits/AllowedLinkClassesTrait.php @@ -0,0 +1,124 @@ +validateTypes($types)) { + $this->allowed_types = $types; + } + + return $this; + } + + /** + * Get allowed types for LinkField + */ + public function getAllowedTypes(): array + { + return $this->allowed_types; + } + + /** + * Validate types that they are subclasses of Link + * @param string[] $types + * @throws InvalidArgumentException + */ + private function validateTypes(array $types): bool + { + if (empty($types)) { + throw new InvalidArgumentException( + _t( + __CLASS__ . '.INVALID_TYPECLASS_EMPTY', + '"{class}": Allowed types cannot be empty', + ['class' => static::class], + ), + ); + } + + $validClasses = []; + foreach ($types as $type) { + if (is_subclass_of($type, Link::class)) { + $validClasses[] = $type; + } else { + throw new InvalidArgumentException( + _t( + __CLASS__ . '.INVALID_TYPECLASS', + '"{class}": {typeclass} is not a valid Link Type', + ['class' => static::class, 'typeclass' => $type], + sprintf( + '"%s": %s is not a valid Link Type', + static::class, + $type, + ), + ), + ); + } + } + + return count($validClasses) > 0; + } + + /** + * The method returns an associational array converted to a JSON string, + * of available link types with additional parameters necessary + * for full-fledged work on the client side. + * @throws InvalidArgumentException + */ + public function getTypesProps(): string + { + $typesList = []; + $typeDefinitions = $this->genarateAllowedTypes(); + foreach ($typeDefinitions as $key => $class) { + $type = Injector::inst()->get($class); + $typesList[$key] = [ + 'key' => $key, + 'title' => $type->i18n_singular_name(), + 'handlerName' => $type->LinkTypeHandlerName(), + ]; + } + + return json_encode($typesList); + } + + /** + * Generate allowed types with key => value pair + * Example: ['cms' => SiteTreeLink::class] + * @param string[] $types + */ + private function genarateAllowedTypes(): array + { + $typeDefinitions = $this->getAllowedTypes() ?? []; + + if (empty($typeDefinitions)) { + return LinkTypeService::create()->generateAllLinkTypes(); + } + + $result = array(); + foreach ($typeDefinitions as $class) { + if (is_subclass_of($class, Link::class)) { + $type = Injector::inst()->get($class)->getShortCode(); + $result[$type] = $class; + } + } + + return $result; + } +} diff --git a/src/GraphQL/LinkTypeResolver.php b/src/GraphQL/LinkTypeResolver.php deleted file mode 100644 index b03fe343..00000000 --- a/src/GraphQL/LinkTypeResolver.php +++ /dev/null @@ -1,30 +0,0 @@ -list(); - $flattenType = array_map(function (Link $type, string $key) { - return [ - 'key' => $key, - 'title' => $type->LinkTypeTile(), - 'handlerName' => $type->LinkTypeHandlerName(), - ]; - }, $types, array_keys($types)); - - return $flattenType; - } -} diff --git a/src/Models/Link.php b/src/Models/Link.php index 02532bd9..7185965d 100644 --- a/src/Models/Link.php +++ b/src/Models/Link.php @@ -10,7 +10,7 @@ use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\RequiredFields; -use SilverStripe\LinkField\Type\Registry; +use SilverStripe\LinkField\Services\LinkTypeService; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectSchema; use SilverStripe\ORM\FieldType\DBHTMLText; @@ -62,11 +62,6 @@ public function getDescription(): string return ''; } - public function LinkTypeTile(): string - { - return $this->i18n_singular_name(); - } - public function scaffoldLinkFields(array $data): FieldList { return $this->getCMSFields(); @@ -207,7 +202,7 @@ function setData($data): Link ); } - $type = Registry::singleton()->byKey($typeKey); + $type = LinkTypeService::create()->byKey($typeKey); if (!$type) { throw new InvalidArgumentException( @@ -244,7 +239,7 @@ function setData($data): Link public function jsonSerialize(): mixed { - $typeKey = Registry::singleton()->keyByClassName(static::class); + $typeKey = LinkTypeService::create()->keyByClassName(static::class); if (!$typeKey) { return []; @@ -446,4 +441,17 @@ public function getDefaultTitle(): string } return $default; } + + /** + * This method process the defined singular_name of Link class + * to get the short code of the Link class name. + * Or If the name is not defined (by redefining $singular_name in the subclass), + * this use the class name. The Link prefix is removed from the class name + * and the resulting name is converted to lowercase. + * Example: Link => link, EmailLink => email, FileLink => file, SiteTreeLink => sitetree + */ + public function getShortCode(): string + { + return strtolower(str_replace([' ', 'Link'], '', ClassInfo::shortName($this))) ?? ''; + } } diff --git a/src/Services/LinkTypeService.php b/src/Services/LinkTypeService.php new file mode 100644 index 00000000..245a2ae0 --- /dev/null +++ b/src/Services/LinkTypeService.php @@ -0,0 +1,69 @@ +get($class)->getShortCode(); + $result[$type] = $class; + } + } + + return $result; + } + + /** + * Return a Link instance by key + * @throws InvalidArgumentException + */ + public function byKey(string $key): ?Link + { + $typeDefinitions = $this->generateAllLinkTypes(); + $className = $typeDefinitions[$key] ?? null; + + if (!$className) { + return null; + } + + return Injector::inst()->get($className); + } + + /** + * Return a key for link type by classname + * @throws InvalidArgumentException + */ + public function keyByClassName(string $classname): ?string + { + $typeDefinitions = $this->generateAllLinkTypes(); + + foreach ($typeDefinitions as $key => $class) { + if ($class === $classname) { + return $key; + } + } + + return null; + } +} diff --git a/src/Type/Registry.php b/src/Type/Registry.php deleted file mode 100644 index 2f94a7dc..00000000 --- a/src/Type/Registry.php +++ /dev/null @@ -1,141 +0,0 @@ -get('types'); - $definition = $typeDefinitions[$key] ?? null; - - if (!$definition) { - return null; - } - - return $this->definitionToType($definition); - } - - /** - * @return Link[] - * @throws InvalidArgumentException - */ - public function list(): array - { - /** @var Link[] $types */ - $types = []; - - /** @var array $types */ - $typeDefinitions = self::config()->get('types'); - - foreach ($typeDefinitions as $key => $def) { - // This link type is disabled, so we can skip it - if (!array_key_exists('enabled', $def) || !$def['enabled']) { - continue; - } - - $types[$key] = $this->definitionToType($def); - } - - return $types; - } - - /** - * @return string[] - */ - public function keys(): array - { - return []; - } - - /** - * @return string[] - */ - public function keysEnabledByDefault(): array - { - return []; - } - - public function init() - { - foreach ($this->list() as $type) { - $type->defineLinkTypeRequirements(); - } - } - - /** - * @param array $def - * @throws LogicException - */ - private function definitionToType(array $def): Link - { - $className = $def['classname'] ?? null; - - if (!$className) { - throw new LogicException( - _t( - 'LinkField.NO_CLASSNAME', - '"{class}": All types should reference a valid classname', - ['class' => static::class], - sprintf('%s: All types should reference a valid classname', static::class), - ), - ); - } - - /** @var Link $type */ - $type = Injector::inst()->get($className); - - if (!$type instanceof Link) { - throw new LogicException( - _t( - 'LinkField.INVALID_TYPENAME', - '"{class}": {typename} is not a valid link type', - [ - 'class' => static::class, - 'typename' => $className, - ], - sprintf('%s: %s is not a valid link type', static::class, $className), - ), - ); - } - - return $type; - } - - public function keyByClassName(string $classname): ?string - { - $typeDefinitions = self::config()->get('types'); - - foreach ($typeDefinitions as $key => $def) { - if ($def['classname'] === $classname) { - return $key; - } - } - - return null; - } -} diff --git a/templates/SilverStripe/LinkField/Form/LinkField.ss b/templates/SilverStripe/LinkField/Form/LinkField.ss index c63fb16a..1710aab3 100644 --- a/templates/SilverStripe/LinkField/Form/LinkField.ss +++ b/templates/SilverStripe/LinkField/Form/LinkField.ss @@ -1,2 +1,2 @@ -
+
\ No newline at end of file diff --git a/templates/SilverStripe/LinkField/Form/MultiLinkField.ss b/templates/SilverStripe/LinkField/Form/MultiLinkField.ss index 6a3ef5d7..2103cefe 100644 --- a/templates/SilverStripe/LinkField/Form/MultiLinkField.ss +++ b/templates/SilverStripe/LinkField/Form/MultiLinkField.ss @@ -1,2 +1,2 @@ -
+
diff --git a/tests/php/Controllers/LinkFieldControllerTest.php b/tests/php/Controllers/LinkFieldControllerTest.php index 03300a3c..93e49e1b 100644 --- a/tests/php/Controllers/LinkFieldControllerTest.php +++ b/tests/php/Controllers/LinkFieldControllerTest.php @@ -4,7 +4,6 @@ use SilverStripe\Dev\FunctionalTest; use SilverStripe\LinkField\Tests\Controllers\LinkFieldControllerTest\TestPhoneLink; -use SilverStripe\LinkField\Type\Registry; use SilverStripe\Core\Config\Config; use SilverStripe\Security\SecurityToken; @@ -21,12 +20,6 @@ class LinkFieldControllerTest extends FunctionalTest protected function setUp(): void { parent::setUp(); - $types = Config::inst()->get(Registry::class, 'types'); - $types['testphone'] = [ - 'classname' => TestPhoneLink::class, - 'enabled' => true, - ]; - Config::modify()->set(Registry::class, 'types', $types); $this->logInWithPermission('ADMIN'); // CSRF token check is normally disabled for unit-tests $this->securityTokenWasEnabled = SecurityToken::is_enabled(); @@ -39,9 +32,6 @@ protected function setUp(): void protected function tearDown(): void { parent::tearDown(); - $types = Config::inst()->get(Registry::class, 'types'); - unset($types['testphone']); - Config::modify()->set(Registry::class, 'types', $types); if (!$this->securityTokenWasEnabled) { SecurityToken::disable(); } diff --git a/tests/php/Models/LinkTest.php b/tests/php/Models/LinkTest.php index 4ca853c3..ae7b0dee 100644 --- a/tests/php/Models/LinkTest.php +++ b/tests/php/Models/LinkTest.php @@ -16,12 +16,14 @@ use SilverStripe\LinkField\Models\Link; use SilverStripe\LinkField\Models\PhoneLink; use SilverStripe\LinkField\Models\SiteTreeLink; -use SilverStripe\LinkField\Type\Registry; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\ValidationException; use SilverStripe\Versioned\Versioned; use SilverStripe\LinkField\Tests\Extensions\ExternalLinkExtension; use SilverStripe\LinkField\Tests\Models\LinkTest\LinkOwner; +use SilverStripe\LinkField\Services\LinkTypeService; +use SilverStripe\Core\Injector\Injector; +use SilverStripe\LinkField\Tests\Controllers\LinkFieldControllerTest\TestPhoneLink; class LinkTest extends SapphireTest { @@ -124,125 +126,7 @@ public function linkTypeProvider(): array [FileLink::class, false], [PhoneLink::class, false], [SiteTreeLink::class, false], - [Link::class, true], - ]; - } - - /** - * @param array $types - * @param array $expected - * @return void - * @dataProvider linkTypeEnabledProvider - */ - public function testLinkTypeEnabled(array $types, array $expected): void - { - Config::withConfig(function (MutableConfigCollectionInterface $config) use ($types, $expected): void { - $config->set(Registry::class, 'types', $types); - - $enabledTypes = Registry::singleton()->list(); - $enabledTypes = array_map(static function (Link $link): string { - return $link->LinkTypeTile(); - }, $enabledTypes); - $enabledTypes = array_values($enabledTypes); - sort($enabledTypes, SORT_STRING); - - $this->assertSame($expected, $enabledTypes, 'We expect specific enabled link types'); - }); - } - - public function linkTypeEnabledProvider(): array - { - return [ - 'all types enabled' => [ - [ - 'cms' => [ - 'classname' => SiteTreeLink::class, - 'enabled' => true, - ], - 'external' => [ - 'classname' => ExternalLink::class, - 'enabled' => true, - ], - 'file' => [ - 'classname' => FileLink::class, - 'enabled' => true, - ], - 'email' => [ - 'classname' => EmailLink::class, - 'enabled' => true, - ], - 'phone' => [ - 'classname' => PhoneLink::class, - 'enabled' => true, - ], - ], - [ - 'Email Link', - 'External Link', - 'File Link', - 'Phone Link', - 'Site Tree Link', - ], - ], - 'file type disabled' => [ - [ - 'cms' => [ - 'classname' => SiteTreeLink::class, - 'enabled' => true, - ], - 'external' => [ - 'classname' => ExternalLink::class, - 'enabled' => true, - ], - 'file' => [ - 'classname' => FileLink::class, - 'enabled' => false, - ], - 'email' => [ - 'classname' => EmailLink::class, - 'enabled' => true, - ], - 'phone' => [ - 'classname' => PhoneLink::class, - 'enabled' => true, - ], - ], - [ - 'Email Link', - 'External Link', - 'Phone Link', - 'Site Tree Link', - ], - ], - 'phone and email types disabled' => [ - [ - 'cms' => [ - 'classname' => SiteTreeLink::class, - 'enabled' => true, - ], - 'external' => [ - 'classname' => ExternalLink::class, - 'enabled' => true, - ], - 'file' => [ - 'classname' => FileLink::class, - 'enabled' => true, - ], - 'email' => [ - 'classname' => EmailLink::class, - 'enabled' => false, - ], - 'phone' => [ - 'classname' => PhoneLink::class, - 'enabled' => false, - ], - ], - [ - 'External Link', - 'File Link', - 'Site Tree Link', - ], - ], + [TestPhoneLink::class, false], ]; } @@ -561,4 +445,43 @@ public function testCanCreate() $this->assertTrue($link->canCreate()); $this->assertTrue($link->can('Create')); } + + public function provideLinkType(): array + { + return [ + 'email_link_type' => [ + 'class' => EmailLink::class, + 'expected' => 'email', + ], + 'external_link_type' => [ + 'class' => ExternalLink::class, + 'expected' => 'external', + ], + 'file_link_type' => [ + 'class' => FileLink::class, + 'expected' => 'file', + ], + 'phone_link_type' => [ + 'class' => PhoneLink::class, + 'expected' => 'phone', + ], + 'sitetree_link_type' => [ + 'class' => SiteTreeLink::class, + 'expected' => 'sitetree', + ], + 'testphone_link_type' => [ + 'class' => TestPhoneLink::class, + 'expected' => 'testphone', + ], + ]; + } + + /** + * @dataProvider provideLinkType + */ + public function testGetShortCode($class, $expected): void + { + $linkClass = Injector::inst()->get($class); + $this->assertSame($expected, $linkClass->getShortCode()); + } } diff --git a/tests/php/Services/LinkTypeServiceTest.php b/tests/php/Services/LinkTypeServiceTest.php new file mode 100644 index 00000000..76ca5094 --- /dev/null +++ b/tests/php/Services/LinkTypeServiceTest.php @@ -0,0 +1,116 @@ + [ + 'expected' => [ + 'email' => EmailLink::class, + 'external' => ExternalLink::class, + 'file' => FileLink::class, + 'phone' => PhoneLink::class, + 'sitetree' => SiteTreeLink::class, + 'testphone' => TestPhoneLink::class, + ], + ], + ]; + } + + /** + * @dataProvider allLinkTypesDataProvider + */ + public function testGenerateAllLinkTypes(array $expected) + { + $service = new LinkTypeService(); + + // Get all unknown Link types + $diff = array_diff($service->generateAllLinkTypes(), $this->link_types); + // Leave only known Link subclasses + $types = array_diff($service->generateAllLinkTypes(), $diff); + + $this->assertSame($expected, $types); + } + + public function keyClassDataProvider(): array + { + return [ + 'sitetree_key' => [ + 'sitetree', + SiteTreeLink::class + ], + 'email_key' => [ + 'email', + EmailLink::class, + + ], + 'external_key' => [ + 'external', + ExternalLink::class, + ], + 'file_key' => [ + 'file', + FileLink::class, + ], + 'phone_key' => [ + 'phone', + PhoneLink::class, + 'testphone' => TestPhoneLink::class, + ], + 'testphone_key' => [ + 'testphone', + TestPhoneLink::class, + ], + ]; + } + + /** + * @dataProvider keyClassDataProvider + */ + public function testByKey($key, $class) + { + $service = new LinkTypeService(); + $keyType = $service->byKey($key); + $linkClass = Injector::inst()->get($class); + + $this->assertEquals($linkClass, $keyType); + } + + /** + * @dataProvider keyClassDataProvider + */ + public function testKeyByClassName($key, $class) + { + $service = new LinkTypeService(); + $type = $service->keyByClassName($class); + + $this->assertEquals($key, $type); + } +} diff --git a/tests/php/Traits/AllowedLinkClassesTraitTest.php b/tests/php/Traits/AllowedLinkClassesTraitTest.php new file mode 100644 index 00000000..c1945ed8 --- /dev/null +++ b/tests/php/Traits/AllowedLinkClassesTraitTest.php @@ -0,0 +1,101 @@ +setAllowedTypes($enabled); + // Get all unknown Link types + $diff = array_diff($trait->getAllowedTypes(), $this->link_types); + // Leave only known Link subclasses + $result = array_diff($trait->getAllowedTypes(), $diff); + $this->assertEquals($expected, $result); + } + + public function allowedTypesDataProvider() : array + { + return [ + 'allow all Link classes' => [ + 'enabled' => [ + SiteTreeLink::class, + ExternalLink::class, + FileLink::class, + EmailLink::class, + PhoneLink::class, + TestPhoneLink::class, + ], + 'expected' => [ + SiteTreeLink::class, + ExternalLink::class, + FileLink::class, + EmailLink::class, + PhoneLink::class, + TestPhoneLink::class, + ], + ], + 'allow only SiteTreeLink class' => [ + 'enabled' => [SiteTreeLink::class], + 'expected' => [SiteTreeLink::class], + ], + ]; + } + + /** + * @dataProvider allowedTypesExceptionDataProvider + */ + public function testSetAllowedTypesException(array $enabled) + { + $trait = LinkField::create('LinkField'); + $this->expectException(InvalidArgumentException::class); + $trait->setAllowedTypes($enabled); + } + + public function allowedTypesExceptionDataProvider() : array + { + return [ + 'allow all with empty array' => [ + 'enabled' => [], + ], + 'all all non-Link classes' => [ + 'enabled' => [DataObject::class, 'WrongClass', 1, true], + ], + 'allow one PhoneLink and few non-Link classes' => [ + 'enabled' => [PhoneLink::class, 'WrongClass', 1, true], + ], + ]; + } +}