From 421a19827b4a516e255c218d94c1d1150fe042bd Mon Sep 17 00:00:00 2001 From: Sabina Talipova Date: Tue, 12 Dec 2023 14:29:00 +1300 Subject: [PATCH] NEW Allowed link types --- README.md | 72 +++++++++ _config/graphql.yml | 8 - _config/types.yml | 20 --- _graphql/queries.yml | 3 - _graphql/types.yml | 5 - client/dist/js/bundle.js | 2 +- client/src/boot/index.js | 2 - client/src/boot/registerQueries.js | 8 - client/src/components/LinkField/LinkField.js | 1 - client/src/entwine/LinkField.js | 1 + client/src/state/linkTypes/readLinkTypes.js | 48 ------ src/Controllers/LinkFieldController.php | 9 +- src/Form/LinkField.php | 3 + src/Form/MultiLinkField.php | 3 + src/Form/Traits/AllowedLinkClassesTrait.php | 104 +++++++++++++ src/GraphQL/LinkTypeResolver.php | 30 ---- src/Models/Link.php | 20 ++- src/Services/LinkTypeService.php | 69 +++++++++ src/Type/Registry.php | 141 ------------------ .../SilverStripe/LinkField/Form/LinkField.ss | 2 +- .../LinkField/Form/MultiLinkField.ss | 2 +- .../Controllers/LinkFieldControllerTest.php | 10 -- tests/php/Models/LinkTest.php | 124 +-------------- .../Traits/AllowedLinkClassesTraitTest.php | 86 +++++++++++ 24 files changed, 367 insertions(+), 406 deletions(-) delete mode 100644 _config/graphql.yml delete mode 100644 _config/types.yml delete mode 100644 _graphql/queries.yml delete mode 100644 _graphql/types.yml delete mode 100644 client/src/boot/registerQueries.js delete mode 100644 client/src/state/linkTypes/readLinkTypes.js create mode 100644 src/Form/Traits/AllowedLinkClassesTrait.php delete mode 100644 src/GraphQL/LinkTypeResolver.php create mode 100644 src/Services/LinkTypeService.php delete mode 100644 src/Type/Registry.php create mode 100644 tests/php/Traits/AllowedLinkClassesTraitTest.php diff --git a/README.md b/README.md index 1f45e9b2..db766ed2 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,78 @@ class ExternalLinkExtension extends Extension ``` +The user can control and specify the links allowed for creation in each link field. The `setAllowedTypes` method only includes link types that have been provided as parameters. + +```php +addFieldsToTab( + 'Root.Main', + [ + MultiLinkField::create('HasManyLinks') + ->setAllowedTypes([ SiteTreeLink::class ]), + ], + ); + + return $fields; + } +} +``` + + +Additionally, users have the option to designate links for exclusion from the available choices in the specific field. The setDisabledTypes method omits link types provided as parameters from the complete list of link types. +By default, all types of links are available. + +```php +addFieldsToTab( + 'Root.Main', + [ + LinkField::create('HasOneLink') + ->setDisabledTypes([ FileLink::class ]), + ], + ); + + return $fields; + } +} +``` + ## 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..a567e11f 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..6250bc0a 100644 --- a/client/src/components/LinkField/LinkField.js +++ b/client/src/components/LinkField/LinkField.js @@ -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/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..ba84e843 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; diff --git a/src/Form/MultiLinkField.php b/src/Form/MultiLinkField.php index f4874b95..686569bc 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; diff --git a/src/Form/Traits/AllowedLinkClassesTrait.php b/src/Form/Traits/AllowedLinkClassesTrait.php new file mode 100644 index 00000000..7936bc14 --- /dev/null +++ b/src/Form/Traits/AllowedLinkClassesTrait.php @@ -0,0 +1,104 @@ +validateTypes($types)) { + $this->allowed_types = $this->genarateAllowedTypes(); + } else { + $this->allowed_types = $this->genarateAllowedTypes($types); + } + + return $this; + } + + /** + * Get allowed types for LinkField + */ + public function getAllowedTypes(): array + { + return $this->allowed_types; + } + + /** + * Validate types that they are subclasses of Link + */ + private function validateTypes(array $types): bool + { + $validClasses = []; + foreach ($types as $type) { + if (is_subclass_of($type, Link::class)) { + $validClasses[] = $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 + { + $types = []; + $typeDefinitions = $this->genarateAllowedTypes(); + + foreach ($typeDefinitions as $key => $class) { + $types[$key] = Injector::inst()->get($class); + } + + $typesList = []; + + array_map(function (Link $type, string $key) use (&$typesList) { + $typesList[$key] = [ + 'key' => $key, + 'title' => $type->LinkTypeTile(), + 'handlerName' => $type->LinkTypeHandlerName(), + ]; + }, $types, array_keys($types)); + + return json_encode($typesList); + } + + /** + * Generate allowed types with key => value pair + * Example: ['cms' => SiteTreeLink::class] + */ + private function genarateAllowedTypes(array $types = []): array + { + $typeDefinitions = !empty($types) ? $types : $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 dbebf719..29032c9d 100644 --- a/src/Models/Link.php +++ b/src/Models/Link.php @@ -10,7 +10,8 @@ use SilverStripe\Forms\DropdownField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\RequiredFields; -use SilverStripe\LinkField\Type\Registry; +use SilverStripe\LinkField\Form\LinkField; +use SilverStripe\LinkField\Services\LinkTypeService; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectSchema; use SilverStripe\ORM\FieldType\DBHTMLText; @@ -207,7 +208,7 @@ function setData($data): Link ); } - $type = Registry::singleton()->byKey($typeKey); + $type = LinkTypeService::create()->byKey($typeKey); if (!$type) { throw new InvalidArgumentException( @@ -244,7 +245,7 @@ function setData($data): Link public function jsonSerialize(): mixed { - $typeKey = Registry::singleton()->keyByClassName(static::class); + $typeKey = LinkTypeService::create()->keyByClassName(static::class); if (!$typeKey) { return []; @@ -385,4 +386,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'], '', $this->LinkTypeTile())) ?? ''; + } } 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 7ad54151..0b3b0c85 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], ]; } diff --git a/tests/php/Traits/AllowedLinkClassesTraitTest.php b/tests/php/Traits/AllowedLinkClassesTraitTest.php new file mode 100644 index 00000000..0804eb1f --- /dev/null +++ b/tests/php/Traits/AllowedLinkClassesTraitTest.php @@ -0,0 +1,86 @@ +setAllowedTypes($enabled); + $this->assertEquals($expected, $trait->getAllowedTypes()); + } + + public function allowedTypesDataProvider() : array + { + return [ + 'allow all Link classes' => [ + 'enabled' => [ + SiteTreeLink::class, + ExternalLink::class, + FileLink::class, + EmailLink::class, + PhoneLink::class, + TestPhoneLink::class, + ], + 'expected' => [ + 'sitetree' => SiteTreeLink::class, + 'external' => ExternalLink::class, + 'file' => FileLink::class, + 'email' => EmailLink::class, + 'phone' => PhoneLink::class, + 'testphone' => TestPhoneLink::class, + ], + ], + 'allow only SiteTreeLink class' => [ + 'enabled' => [SiteTreeLink::class], + 'expected' => ['sitetree' => SiteTreeLink::class], + ], + 'allow all with empty array' => [ + 'enabled' => [], + 'expected' => [ + 'sitetree' => SiteTreeLink::class, + 'external' => ExternalLink::class, + 'file' => FileLink::class, + 'email' => EmailLink::class, + 'phone' => PhoneLink::class, + 'testphone' => TestPhoneLink::class, + ], + ], + 'all all non-Link classes' => [ + 'enabled' => [DataObject::class, 'WrongClass', 1, true, ], + 'expected' => [ + 'sitetree' => SiteTreeLink::class, + 'external' => ExternalLink::class, + 'file' => FileLink::class, + 'email' => EmailLink::class, + 'phone' => PhoneLink::class, + 'testphone' => TestPhoneLink::class, + ], + ], + 'allow one PhoneLink and few non-Link classes' => [ + 'enabled' => [PhoneLink::class, 'WrongClass', 1, true, ], + 'expected' => [ + 'phone' => PhoneLink::class + ], + ], + ]; + } +}