From 81a055d452476f8d949b512e9517fc6fc74b66ae Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Wed, 21 Aug 2024 18:34:56 +1200 Subject: [PATCH] API Remove GraphQL --- _config/graphql.yml | 10 - _graphql/config.yml | 5 - _graphql/models.yml | 41 - _graphql/mutations.yml | 9 - _graphql/scalars.yml | 4 - client/dist/js/bundle.js | 110 +-- client/src/boot/registerTransforms.js | 47 +- .../ElementActions/ArchiveAction.js | 35 +- .../ElementActions/DuplicateAction.js | 29 +- .../ElementActions/UnpublishAction.js | 87 +-- .../tests/ArchiveAction-test.js | 140 +++- .../tests/DuplicateAction-test.js | 44 +- .../tests/PublishAction-test.js | 99 ++- .../ElementActions/tests/SaveAction-test.js | 97 ++- .../tests/UnpublishAction-test.js | 100 ++- .../ElementEditor/AddElementPopover.js | 32 +- .../src/components/ElementEditor/Element.js | 47 +- .../components/ElementEditor/ElementEditor.js | 119 ++- .../components/ElementEditor/ElementList.js | 73 +- .../ElementEditor/tests/Element-test.js | 105 +-- .../ElementEditor/tests/ElementEditor-test.js | 27 +- .../ElementEditor/tests/ElementList-test.js | 12 +- client/src/legacy/ElementEditor/entwine.js | 9 +- client/src/state/editor/addElementMutation.js | 42 - .../src/state/editor/archiveBlockMutation.js | 36 - .../state/editor/duplicateBlockMutation.js | 38 - .../src/state/editor/publishBlockMutation.js | 9 - .../state/editor/readBlocksForAreaQuery.js | 65 -- client/src/state/editor/sortBlockMutation.js | 92 --- .../state/editor/unpublishBlockMutation.js | 45 -- client/src/state/history/readOneBlockQuery.js | 96 --- .../history/revertToBlockVersionMutation.js | 43 -- .../history/revertToBlockVersionRequest.js | 25 + client/src/types/elementTypeType.js | 2 +- composer.json | 1 - package.json | 5 +- src/Controllers/ElementalAreaController.php | 258 ++++++- src/GraphQL/Resolvers/Resolver.php | 209 ----- src/Models/BaseElement.php | 37 +- src/ORM/FieldType/DBObjectType.php | 33 - src/Services/ElementTypeRegistry.php | 5 +- tests/Blocks/TestElementContent.php | 24 +- tests/Blocks/TestElementalArea.php | 23 + .../ElementalAreaControllerTest.php | 727 +++++++++++++++++- .../ElementalAreaControllerTest.yml | 25 +- .../GraphQL/AddElementToAreaMutationTest.php | 95 --- .../GraphQL/AddElementToAreaMutationTest.yml | 2 - .../GraphQL/DuplicateElementMutationTest.php | 72 -- tests/GraphQL/FakeResolveInfo.php | 28 - .../GraphQL/SortBlockMutationCreatorTest.php | 64 -- .../GraphQL/SortBlockMutationCreatorTest.yml | 13 - tests/Reports/ElementsInUseReportTest.php | 21 - yarn.lock | 143 +--- 53 files changed, 1760 insertions(+), 1799 deletions(-) delete mode 100644 _config/graphql.yml delete mode 100644 _graphql/config.yml delete mode 100644 _graphql/models.yml delete mode 100644 _graphql/mutations.yml delete mode 100644 _graphql/scalars.yml delete mode 100644 client/src/state/editor/addElementMutation.js delete mode 100644 client/src/state/editor/archiveBlockMutation.js delete mode 100644 client/src/state/editor/duplicateBlockMutation.js delete mode 100644 client/src/state/editor/publishBlockMutation.js delete mode 100644 client/src/state/editor/readBlocksForAreaQuery.js delete mode 100644 client/src/state/editor/sortBlockMutation.js delete mode 100644 client/src/state/editor/unpublishBlockMutation.js delete mode 100644 client/src/state/history/readOneBlockQuery.js delete mode 100644 client/src/state/history/revertToBlockVersionMutation.js create mode 100644 client/src/state/history/revertToBlockVersionRequest.js delete mode 100644 src/GraphQL/Resolvers/Resolver.php delete mode 100644 src/ORM/FieldType/DBObjectType.php create mode 100644 tests/Blocks/TestElementalArea.php delete mode 100644 tests/GraphQL/AddElementToAreaMutationTest.php delete mode 100644 tests/GraphQL/AddElementToAreaMutationTest.yml delete mode 100644 tests/GraphQL/DuplicateElementMutationTest.php delete mode 100644 tests/GraphQL/FakeResolveInfo.php delete mode 100644 tests/GraphQL/SortBlockMutationCreatorTest.php delete mode 100644 tests/GraphQL/SortBlockMutationCreatorTest.yml diff --git a/_config/graphql.yml b/_config/graphql.yml deleted file mode 100644 index 13b2b076..00000000 --- a/_config/graphql.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -Name: elemental-graphql -Only: - classexists: 'SilverStripe\GraphQL\Schema\Schema' ---- -SilverStripe\GraphQL\Schema\Schema: - schemas: - admin: - src: - elemental: 'dnadesign/silverstripe-elemental: _graphql' diff --git a/_graphql/config.yml b/_graphql/config.yml deleted file mode 100644 index 08e7c7be..00000000 --- a/_graphql/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -resolvers: - - 'DNADesign\Elemental\GraphQL\Resolvers\Resolver' -typeMapping: - DNADesign\Elemental\Models\BaseElement: Block - DNADesign\Elemental\Models\ElementalArea: ElementalArea diff --git a/_graphql/models.yml b/_graphql/models.yml deleted file mode 100644 index 77e585aa..00000000 --- a/_graphql/models.yml +++ /dev/null @@ -1,41 +0,0 @@ -# Expose common static fields for the ElementEditor component to use for preview summaries -DNADesign\Elemental\Models\BaseElement: - fields: - id: true - lastEdited: true - absoluteLink: String - title: true - showTitle: true - sort: true - blockSchema: ObjectType - obsoleteClassName: String - isPublished: Boolean - isLiveVersion: Boolean - canCreate: Boolean - canPublish: Boolean - canUnpublish: Boolean - canDelete: Boolean - operations: - copyToStage: true - readOne: true - delete: true - publish: true - unpublish: true -# Expose access to Elements via Page -> ElementalArea -> Elements (see resolver) -DNADesign\Elemental\Models\ElementalArea: - fields: - id: true - elements: - plugins: - paginateList: false - operations: - readOne: true - -# Basic member information is required for history views -SilverStripe\Security\Member: - fields: - id: true - firstName: true - surname: true - operations: - readOne: true diff --git a/_graphql/mutations.yml b/_graphql/mutations.yml deleted file mode 100644 index 48ecba4d..00000000 --- a/_graphql/mutations.yml +++ /dev/null @@ -1,9 +0,0 @@ -'addElementToArea(className: String!, elementalAreaID: ID!, afterElementID: ID)': - model: DNADesign\Elemental\Models\BaseElement - description: Adds an Element to an ElementalArea, optionally after another Element -'duplicateBlock(id: ID!)': - model: DNADesign\Elemental\Models\BaseElement - description: Duplicate an Element in this ElementalArea -'sortBlock(id: ID!, afterBlockID: ID!)': - model: DNADesign\Elemental\Models\BaseElement - description: Changes the sort position of an element diff --git a/_graphql/scalars.yml b/_graphql/scalars.yml deleted file mode 100644 index 1bffd517..00000000 --- a/_graphql/scalars.yml +++ /dev/null @@ -1,4 +0,0 @@ -ObjectType: - serialiser: [DNADesign\Elemental\GraphQL\Resolvers\Resolver, 'serialiseObjectType'] - literalParser: [DNADesign\Elemental\GraphQL\Resolvers\Resolver, 'parseLiteralObjectType'] - valueParser: [DNADesign\Elemental\GraphQL\Resolvers\Resolver, 'parseValueObjectType'] diff --git a/client/dist/js/bundle.js b/client/dist/js/bundle.js index df1b50b3..fbfc05a0 100644 --- a/client/dist/js/bundle.js +++ b/client/dist/js/bundle.js @@ -1,109 +1 @@ -!function(){"use strict";var e={2038:function(e,t,n){var r=a(n(4121)),o=a(n(1240));function a(e){return e&&e.__esModule?e:{default:e}}window.document.addEventListener("DOMContentLoaded",(()=>{(0,r.default)(),(0,o.default)()}))},4121:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=v(n(5207)),o=v(n(584)),a=v(n(7105)),l=v(n(6721)),i=v(n(2512)),s=v(n(5563)),d=v(n(5297)),u=v(n(7651)),c=v(n(1305)),f=v(n(7660)),p=v(n(7333)),m=v(n(2382)),g=v(n(8953)),h=v(n(1950)),b=v(n(6573));function v(e){return e&&e.__esModule?e:{default:e}}t.default=()=>{r.default.component.registerMany({ElementEditor:l.default,ElementToolbar:s.default,ElementAddNewButton:d.default,ElementList:i.default,Element:o.default,ElementActions:a.default,ElementHeader:u.default,ElementContent:c.default,ElementSummary:f.default,ElementInlineEditForm:p.default,AddElementPopover:m.default,HoverBar:g.default,DragPositionIndicator:h.default,TextCheckboxGroupField:b.default})}},1240:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=m(n(5207)),o=m(n(5071)),a=m(n(2977)),l=m(n(8633)),i=m(n(3521)),s=m(n(6283)),d=m(n(4)),u=m(n(9283)),c=m(n(8501)),f=m(n(2221)),p=m(n(5904));function m(e){return e&&e.__esModule?e:{default:e}}t.default=()=>{r.default.transform("elemental-fieldgroup",(e=>{e.component("FieldGroup.HistoryViewer.VersionDetail",a.default,"HistoricElement")}),{after:"field-holders"}),r.default.transform("elements-history",(e=>{e.component("HistoryViewer.Form_ItemEditForm",o.default,"ElementHistoryViewer")})),r.default.transform("blocks-history-revert",(e=>{e.component("HistoryViewerToolbar.VersionedAdmin.HistoryViewer.Element.HistoryViewerVersionDetail",l.default,"BlockRevertMutation")})),r.default.transform("cms-element-editor",(e=>{e.component("ElementList",i.default,"PageElements")})),r.default.transform("cms-element-adder",(e=>{e.component("AddElementPopover",s.default,"ElementAddButton")})),r.default.transform("element-actions",(e=>{e.component("ElementActions",c.default,"ElementActionsWithSave"),e.component("ElementActions",f.default,"ElementActionsWithPublish"),e.component("ElementActions",p.default,"ElementActionsWithUnpublish"),e.component("ElementActions",u.default,"ElementActionsWithDuplicate"),e.component("ElementActions",d.default,"ElementActionsWithArchive")}))}},26:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=s(n(1594)),o=s(n(6935)),a=s(n(6923)),l=n(3556),i=n(9791);function s(e){return e&&e.__esModule?e:{default:e}}const d=e=>{const{className:t,title:n,label:o}=e,i={className:(0,a.default)(t,"dropdown-item"),...e};return r.default.createElement(l.DropdownItem,i,o||n)};d.propTypes={disabled:o.default.bool,className:o.default.string,onClick:o.default.func,title:o.default.string,name:o.default.string,type:i.elementTypeType,active:o.default.bool,label:o.default.string},d.defaultProps={disabled:!1};t.default=d},4:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=s(n(1594)),o=n(9367),a=s(n(26)),l=s(n(1429)),i=s(n(5815));function s(e){return e&&e.__esModule?e:{default:e}}const d=e=>t=>{const n=void 0!==t.element.canDelete&&!t.element.canDelete,o=i.default._t("ElementArchiveAction.ARCHIVE","Archive"),l={label:o,title:n?i.default._t("ElementArchiveAction.ARCHIVE_PERMISSION_DENY","Archive, insufficient permissions"):o,disabled:n,className:"element-editor__actions-archive",onClick:e=>{e.stopPropagation();const{element:{id:n},isPublished:r,actions:{handleArchiveBlock:o}}=t;let a=i.default._t("ElementArchiveAction.CONFIRM_DELETE","Are you sure you want to send this block to the archive?");r&&(a=i.default._t("ElementArchiveAction.CONFIRM_DELETE_AND_UNPUBLISH","Warning: This block will be unpublished before being sent to the archive. Are you sure you want to proceed?")),o&&window.confirm(a)&&o(n).then((()=>{const e=window.jQuery(".cms-preview");e&&"function"==typeof e.entwine&&e.entwine("ss.preview")._loadUrl(e.find("iframe").attr("src"))}))},toggle:t.toggle};return r.default.createElement(e,t,t.children,r.default.createElement(a.default,l))};t.Component=d;t.default=(0,o.compose)(l.default,d)},9283:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=s(n(1594)),o=n(9367),a=s(n(26)),l=s(n(9324)),i=s(n(5815));function s(e){return e&&e.__esModule?e:{default:e}}const d=e=>t=>{if(t.type.broken)return r.default.createElement(e,t);const n=void 0!==t.element.canCreate&&!t.element.canCreate,o=i.default._t("ElementArchiveAction.DUPLICATE","Duplicate"),l={label:o,title:n?i.default._t("ElementArchiveAction.DUPLICATE_PERMISSION_DENY","Duplicate, insufficient permissions"):o,disabled:n,className:"element-editor__actions-duplicate",onClick:e=>{e.stopPropagation();const{element:{id:n},actions:{handleDuplicateBlock:r}}=t;r&&r(n).then((()=>{const e=window.jQuery(".cms-preview");e.entwine("ss.preview")._loadUrl(e.find("iframe").attr("src"))}))},toggle:t.toggle};return r.default.createElement(e,t,t.children,r.default.createElement(a.default,l))};t.Component=d;t.default=(0,o.compose)(l.default,d)},2221: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=s(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=i(n(26)),a=i(n(5815)),l=n(584);function i(e){return e&&e.__esModule?e:{default:e}}function s(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(s=function(e){return e?n:t})(e)}const d=e=>t=>{const{formDirty:n,onPublishButtonClick:i}=(0,r.useContext)(l.ElementContext),{element:s}=t,d=void 0!==t.element.canPublish&&!t.element.canPublish,u=a.default._t("ElementArchiveAction.PUBLISH","Publish"),c={label:u,title:d?a.default._t("ElementArchiveAction.PUBLISH_PERMISSION_DENY","Publish, insufficient permissions"):u,disabled:d,className:"element-editor__actions-publish",onClick:e=>{e.stopPropagation(),i()},toggle:t.toggle};return t.type.broken?r.default.createElement(e,t):r.default.createElement(e,t,t.children,(n||!s.isLiveVersion)&&r.default.createElement(o.default,c))};t.Component=d;t.default=d},8501: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=s(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=i(n(26)),a=i(n(5815)),l=n(584);function i(e){return e&&e.__esModule?e:{default:e}}function s(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(s=function(e){return e?n:t})(e)}const d=e=>t=>{const{onSaveButtonClick:n,formDirty:i}=(0,r.useContext)(l.ElementContext),s={title:a.default._t("ElementSaveAction.SAVE","Save"),className:"element-editor__actions-save",onClick:e=>{e.stopPropagation(),n()},toggle:t.toggle};return!t.expandable||t.type.broken?r.default.createElement(e,t):r.default.createElement(e,t,t.children,i&&r.default.createElement(o.default,s))};t.Component=d;t.default=d},5904:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r=s(n(1594)),o=n(9367),a=s(n(26)),l=s(n(2393)),i=s(n(5815));function s(e){return e&&e.__esModule?e:{default:e}}const d=e=>t=>{if(t.type.broken)return r.default.createElement(e,t);const{element:n,type:o,actions:{handleUnpublishBlock:l}}=t,s=void 0!==t.element.canUnpublish&&!t.element.canUnpublish,d=i.default._t("ElementArchiveAction.UNPUBLISH","Unpublish"),u={label:d,title:s?i.default._t("ElementArchiveAction.UNPUBLISH_PERMISSION_DENY","Unpublish, insufficient permissions"):d,disabled:s,className:"element-editor__actions-unpublish",onClick:e=>{e.stopPropagation();const{jQuery:t}=window,r=i.default.inject(i.default._t("ElementHeader.NOTITLE","Untitled {type} block"),{type:o.title});l&&l(n.id).then((()=>{const e=t(".cms-preview");e.entwine("ss.preview")._loadUrl(e.find("iframe").attr("src")),t.noticeAdd({text:i.default.inject(i.default._t("ElementUnpublishAction.SUCCESS_NOTIFICATION","Removed '{title}' from the published page"),{title:n.title||r}),stay:!1,type:"success"})})).catch((()=>{t.noticeAdd({text:i.default.inject(i.default._t("ElementUnpublishAction.ERROR_NOTIFICATION","Error unpublishing '{title}'"),{title:n.title||r}),stay:!1,type:"error"})}))},toggle:t.toggle};return r.default.createElement(e,t,t.children,n.isPublished&&r.default.createElement(a.default,u))};t.Component=d;t.default=(0,o.compose)(l.default,d)},2382:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=u(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=d(n(6935)),a=d(n(6923)),l=n(5207),i=n(9791),s=d(n(5815));function d(e){return e&&e.__esModule?e:{default:e}}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)}class c extends r.Component{constructor(e){super(e),this.handleToggle=this.handleToggle.bind(this)}getElementButtonClickHandler(e){return t=>{const{actions:{handleAddElementToArea:n},insertAfterElement:r}=this.props;t.preventDefault(),n(e.class,r).then((()=>{const e=window.jQuery(".cms-preview");e.entwine("ss.preview")._loadUrl(e.find("iframe").attr("src"))})),this.handleToggle()}}handleToggle(){const{toggle:e}=this.props;e()}render(){const{PopoverOptionSetComponent:e,elementTypes:t,container:n,extraClass:o,isOpen:l,placement:i,target:d}=this.props,u=(0,a.default)("element-editor-add-element",o),c=t.map((e=>({content:e.title,key:e.name,className:(0,a.default)(e.icon,"btn--icon-xl","element-editor-add-element__button"),onClick:this.getElementButtonClickHandler(e)})));return r.default.createElement(e,{buttons:c,searchPlaceholder:s.default._t("ElementAddElementPopover.SEARCH_BLOCKS","Search blocks"),extraClass:u,container:n,isOpen:l,placement:i,target:d,toggle:this.handleToggle})}}c.propTypes={container:o.default.oneOfType([o.default.string,o.default.func,o.default.object]),elementTypes:o.default.arrayOf(i.elementTypeType).isRequired,extraClass:o.default.oneOfType([o.default.string,o.default.array,o.default.object]),isOpen:o.default.bool.isRequired,placement:o.default.string,target:o.default.oneOfType([o.default.string,o.default.func,o.default.object]).isRequired,toggle:o.default.func.isRequired,areaId:o.default.number.isRequired,insertAfterElement:o.default.oneOfType([o.default.number,o.default.string])};t.default=(0,l.inject)(["PopoverOptionSet"],(e=>({PopoverOptionSetComponent:e})),(()=>"ElementEditor"))(c)},5297: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=u(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=d(n(6935)),a=n(3556),l=d(n(5815)),i=n(9791),s=n(5207);function d(e){return e&&e.__esModule?e:{default:e}}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)}class c extends r.Component{constructor(e){super(e),this.toggle=this.toggle.bind(this),this.state={popoverOpen:!1}}toggle(){this.setState((e=>({popoverOpen:!e.popoverOpen})))}render(){const{AddElementPopoverComponent:e,elementTypes:t,areaId:n}=this.props,o={id:`ElementalArea${n}_AddButton`,color:"primary",onClick:this.toggle,className:"font-icon-plus"};return r.default.createElement("div",null,r.default.createElement(a.Button,o,l.default._t("ElementAddNewButton.ADD_BLOCK","Add block")),r.default.createElement(e,{placement:"bottom-start",target:o.id,isOpen:this.state.popoverOpen,elementTypes:t,toggle:this.toggle,areaId:n,insertAfterElement:0}))}}t.Component=c,c.defaultProps={},c.propTypes={elementTypes:o.default.arrayOf(i.elementTypeType).isRequired,areaId:o.default.number.isRequired};t.default=(0,s.inject)(["AddElementPopover"],(e=>({AddElementPopoverComponent:e})),(()=>"ElementEditor.ElementList.AddNewButton"))(c)},1305:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r,o=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=s(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=(r=n(6935))&&r.__esModule?r:{default:r},l=n(5207),i=n(9367);function s(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(s=function(e){return e?n:t})(e)}class d extends o.PureComponent{render(){const{id:e,fileUrl:t,fileTitle:n,content:r,previewExpanded:a,InlineEditFormComponent:l,SummaryComponent:i,activeTab:s,onFormInit:d,handleLoadingError:u,formDirty:c,broken:f,onFormSchemaSubmitResponse:p,ensureFormRendered:m,formHasRendered:g}=this.props,h=!a&&(m||g),b={"element-editor-editform--collapsed":!a,"element-editor-editform--rendered-not-visible":h};return o.default.createElement("div",{className:"element-editor-content"},!a&&o.default.createElement(i,{content:r,fileUrl:t,fileTitle:n,broken:f}),(a||m||g)&&o.default.createElement(l,{extraClass:b,onClick:e=>e.stopPropagation(),elementId:e,activeTab:s,onFormInit:d,handleLoadingError:u,onFormSchemaSubmitResponse:p,notVisible:h}),c&&o.default.createElement("input",{type:"hidden",name:"change-tracker",className:"element-form-dirty-state",value:"1"}))}}t.Component=d,d.propTypes={id:a.default.string,content:a.default.string,fileUrl:a.default.string,fileTitle:a.default.string,previewExpanded:a.default.bool,SummaryComponent:a.default.elementType,InlineEditFormComponent:a.default.elementType,handleLoadingError:a.default.func,broken:a.default.bool,onFormSchemaSubmitResponse:a.default.func,onFormInit:a.default.func,ensureFormRendered:a.default.bool,formHasRendered:a.default.bool,formDirty:a.default.object},d.defaultProps={};t.default=(0,i.compose)((0,l.inject)(["ElementSummary","ElementInlineEditForm"],((e,t)=>({SummaryComponent:e,InlineEditFormComponent:t})),(()=>"ElementEditor.ElementList.Element")))(d)},1950:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=o(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&{}.hasOwnProperty.call(e,l)){var i=a?Object.getOwnPropertyDescriptor(e,l):null;i&&(i.get||i.set)?Object.defineProperty(r,l,i):r[l]=e[l]}return r.default=e,n&&n.set(e,r),r}(n(1594));function o(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(o=function(e){return e?n:t})(e)}class a extends r.PureComponent{render(){return r.default.createElement("div",{className:"elemental-editor-drag-indicator"},r.default.createElement("div",{className:"elemental-editor-drag-indicator__ball"}))}}t.default=a},584:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.ElementContext=t.Component=void 0;var r=I(n(1594)),o=n(8695),a=D(n(6935)),l=n(455),i=n(9791),s=n(9367),d=n(5207),u=D(n(5815)),c=D(n(6923)),f=n(9040),p=n(5381),m=n(2153),g=n(7785),h=n(4242),b=n(3521),v=I(n(5034)),y=n(1820),_=n(8724),E=n(9306),O=I(n(8918)),T=n(9077);function D(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 I(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={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}const C=t.ElementContext=(0,r.createContext)(null),P=e=>{const[t,n]=(0,r.useState)(!1),[a,l]=(0,r.useState)(""),[i,s]=(0,r.useState)(!1),[d,f]=(0,r.useState)(e.element.title),[p,m]=(0,r.useState)(!1),[g,v]=(0,r.useState)(!1),[y,E]=(0,r.useState)(!1),[O,T]=(0,r.useState)(!1),[D,k]=(0,r.useState)(!1),[I,P]=(0,r.useState)(!1),[S,w]=(0,r.useState)(!1),[j,A]=(0,r.useState)(!1),[M]=(0,o.useMutation)(h.publishBlockMutation);(0,r.useEffect)((()=>{void 0!==e.formDirty&&!j&&A(!0)}),[e.formDirty]),(0,r.useEffect)((()=>{e.onChangeHasUnsavedChanges(j)}),[j]),(0,r.useEffect)((()=>{e.saveElement&&j&&!g&&v(!0)}),[e.saveElement,j,e.increment]),(0,r.useEffect)((()=>{e.connectDragPreview&&e.connectDragPreview((0,_.getEmptyImage)(),{captureDraggingState:!0})}),[]),(0,r.useEffect)((()=>{p&&I&&(m(!1),j?(e.submitForm(),T(!0)):E(!0))}),[p,I]),(0,r.useEffect)((()=>{S&&(w(!1),e.dispatchAddFormChanged())}),[S]);const R=()=>u.default.inject(u.default._t("ElementHeader.NOTITLE","Untitled {type} block"),{type:e.type.title}),N=()=>window.ss.apolloClient.queryManager.refetchQueries({include:[{query:b.query,variables:{id:e.areaId}}]}),B=t=>{(t=>{const n=d||R();if(t){const t=u.default.inject(u.default._t("ElementPublishAction.ERROR_NOTIFICATION","Error publishing '{title}'"),{title:n});e.actions.toasts.error(t)}else{const t=u.default.inject(u.default._t("ElementPublishAction.SUCCESS_NOTIFICATION","Published '{title}' successfully"),{title:n});e.actions.toasts.success(t)}})(t),E(!1),T(!1),Promise.all(N()).then((()=>{setTimeout((()=>e.dispatchRemoveFormChanged()),250)}))};(0,r.useEffect)((()=>{I&&g&&(e.submitForm(),v(!1))}),[I,g]),(0,r.useEffect)((()=>{I&&y&&M({variables:{blockId:e.element.id}}).then((()=>B(!1))).catch((()=>B(!0)))}),[I,y]);const x=t=>{const{tabSetName:n,onActivateTab:r}=e;if(a||l(t),t||a)r(n,t||a);else{r(n,"Main")}},q=r=>{const{type:o,link:a}=e;o.broken||("button"!==r.target.type?!o.inlineEditable||i?window.location=a:n(!t):r.stopPropagation())},{element:L,type:F,areaId:$,HeaderComponent:U,ContentComponent:H,link:W,activeTab:V,connectDragSource:Q,connectDropTarget:K,isDragging:G,isOver:Y,onDragEnd:J,formDirty:X}=e;if(!L.id)return null;const z=(0,c.default)("element-editor__element",{"element-editor__element--broken":F.broken,"element-editor__element--expandable":F.inlineEditable&&!F.broken,"element-editor__element--dragging":G,"element-editor__element--dragged-over":Y},(()=>{const{element:t}=e,n="element-editor__element";return t.isPublished?t.isPublished&&!t.isLiveVersion?`${n}--modified`:`${n}--published`:`${n}--draft`})()),Z={formDirty:X,onPublishButtonClick:()=>{m(!0),k(!0)},onSaveButtonClick:()=>{k(!0),v(!0)}},ee=K(r.default.createElement("div",{className:z,onClick:q,onKeyUp:e=>{const{nodeName:t}=e.target;" "!==e.key&&"Enter"!==e.key||["input","textarea"].includes(t.toLowerCase())||q(e)},role:"button",tabIndex:0,title:(e=>e.broken?u.default._t("ElementalElement.ARCHIVE_BROKEN","Archive this block"):u.default.inject(u.default._t("ElementalElement.TITLE","Edit this {type} block"),{type:e.title}))(F),key:L.id},r.default.createElement(C.Provider,{value:Z},r.default.createElement(U,{element:L,type:F,areaId:$,expandable:F.inlineEditable,link:W,previewExpanded:t,handleEditTabsClick:t=>{const{activeTab:r}=e;t===r||i||(n(!0),x(t))},activeTab:V,disableTooltip:G,onDragEnd:J}),r.default.createElement(H,{id:L.id,fileUrl:L.blockSchema.fileURL,fileTitle:L.blockSchema.fileTitle,content:((e,t)=>t.broken?e.title?u.default.inject(u.default._t("ElementalElement.BROKEN_DESCRIPTION_TITLE",'This block had the title "{title}". It is broken and will not display on the front-end. You can archive it to remove it from this elemental area.'),{title:e.title}):u.default._t("ElementalElement.BROKEN_DESCRIPTION","This block is broken and will not display on the front-end. You can archive it to remove it from this elemental area."):e.blockSchema.content)(L,F),previewExpanded:t&&!G,ensureFormRendered:D,formHasRendered:I,activeTab:V,handleLoadingError:()=>{s(!0)},broken:F.broken,onFormSchemaSubmitResponse:(t,r)=>{if(t.id.match(/\/schema\/elemental-area\/([0-9]+)/))return e.type.inlineEditable&&n(!0),w(!0),O&&T(!1),void e.onAfterSubmitResponse(!1);A(!1),f(r),O&&(T(!1),E(!0)),y||O||(t=>{const n=t||R(),r=u.default.inject(u.default._t("ElementSaveAction.SUCCESS_NOTIFICATION","Saved '{title}' successfully"),{title:n});e.actions.toasts.success(r)})(r),N(),e.onAfterSubmitResponse(!0)},onFormInit:()=>(e=>{x(e),P(!0)})(V)}))));return t?ee:Q(ee)};t.Component=P,P.propTypes={element:l.elementType,type:i.elementTypeType.isRequired,areaId:a.default.number.isRequired,link:a.default.string.isRequired,activeTab:a.default.string,tabSetName:a.default.string,onActivateTab:a.default.func,connectDragSource:a.default.func.isRequired,connectDragPreview:a.default.func.isRequired,connectDropTarget:a.default.func.isRequired,isDragging:a.default.bool.isRequired,isOver:a.default.bool.isRequired,onDragOver:a.default.func,onDragEnd:a.default.func,onDragStart:a.default.func,saveElement:a.default.bool.isRequired,onBeforeSubmitForm:a.default.func.isRequired,onAfterSubmitResponse:a.default.func.isRequired,increment:a.default.number.isRequired},P.defaultProps={element:null};const S={drop(e,t,n){const{element:r}=e;return{target:r.id,dropSpot:(0,E.isOverTop)(t,n)?"top":"bottom"}},hover(e,t,n){const{element:r,onDragOver:o}=e;o&&o(r,(0,E.isOverTop)(t,n))}};t.default=(0,s.compose)((0,y.DropTarget)("element",S,((e,t)=>({connectDropTarget:e.dropTarget(),isOver:t.isOver()}))),(0,y.DragSource)("element",E.elementDragSource,((e,t)=>({connectDragSource:e.dragSource(),connectDragPreview:e.dragPreview(),isDragging:t.isDragging()}))),(0,f.connect)((function(e,t){const n=t.element.id,r=(0,m.loadElementFormStateName)(n),o=(0,g.loadElementSchemaValue)("schemaUrl",n),a=e.form&&e.form.formSchemas[o]&&e.form.formSchemas[o].schema&&e.form.formSchemas[o].schema.fields.find((e=>"Tabs"===e.component)),l=a&&a.id,i=`element.${r}__${l}`,s=e.unsavedForms.find((e=>e.name===`element.${r}`));return{tabSetName:l,activeTab:e.tabs&&e.tabs.fields&&e.tabs.fields[i]&&e.tabs.fields[i].activeTab,formDirty:s}}),(function(e,t){const n=(0,m.loadElementFormStateName)(t.element.id);return{onActivateTab(t,r){e(v.activateTab(`element.${n}__${t}`,r))},submitForm(){t.onBeforeSubmitForm(t.element.id),e((0,p.submit)(`element.${n}`))},dispatchAddFormChanged(){e((0,T.addFormChanged)(`element.${n}`))},dispatchRemoveFormChanged(){e((0,T.removeFormChanged)(`element.${n}`))},actions:{toasts:(0,s.bindActionCreators)(O,e)}}})),(0,d.inject)(["ElementHeader","ElementContent"],((e,t)=>({HeaderComponent:e,ContentComponent:t})),(()=>"ElementEditor.ElementList.Element")))(P)},7105: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=f(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=c(n(6935)),a=n(9367),l=n(3556),i=n(5207),s=n(455),d=n(9791),u=c(n(26));function c(e){return e&&e.__esModule?e:{default:e}}function f(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(f=function(e){return e?n:t})(e)}class p extends r.Component{constructor(e){super(e),this.handleEditTabsClick=this.handleEditTabsClick.bind(this)}handleEditTabsClick(e){const{handleEditTabsClick:t}=this.props;t(e.target.name)}renderEditTabs(){const{editTabs:e,activeTab:t,type:n,expandable:o}=this.props;return!n.broken&&o&&e&&e.length?e.map((e=>{let{name:o,title:a}=e;return r.default.createElement(u.default,{key:o,name:o,title:a,type:n,onClick:this.handleEditTabsClick,active:o===t})})):null}renderDivider(){const{children:e,editTabs:t,expandable:n}=this.props;return n&&t&&t.length&&0!==r.default.Children.count(e)?r.default.createElement(l.DropdownItem,{divider:!0,role:"separator"}):null}render(){const{children:e,id:t,ActionMenuComponent:n}=this.props;return r.default.createElement(n,{id:`element-editor-actions-${t}`,className:"element-editor-header__actions-dropdown",dropdownMenuProps:{right:!0},dropdownToggleClassNames:["element-editor-header__actions-toggle","btn","btn-sm","btn--no-text","font-icon-dot-3"]},this.renderEditTabs(),this.renderDivider(),e)}}t.Component=p,p.propTypes={element:s.elementType,type:d.elementTypeType.isRequired,areaId:o.default.number.isRequired,activeTab:o.default.string,editTabs:o.default.arrayOf(o.default.shape({title:o.default.string,name:o.default.string})),handleEditTabsClick:o.default.func.isRequired,expandable:o.default.bool},p.defaultProps={editTabs:[],expandable:!0};t.default=(0,a.compose)((0,i.inject)(["ActionMenu"],(e=>({ActionMenuComponent:e})),(()=>"ElementEditor.ElementList.Element")))(p)},920:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=c(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=u(n(6935)),a=u(n(7651)),l=n(1820),i=n(455),s=n(9791),d=n(1135);function u(e){return e&&e.__esModule?e:{default:e}}function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(c=function(e){return e?n:t})(e)}class f extends r.Component{render(){const{isDragging:e,element:t,elementTypes:n,currentOffset:o}=this.props;if(!e||!o)return null;const{x:l,y:i}=o,s=`translate(${l}px, ${i}px)`,u={transform:s,WebkitTransform:s},c=(0,d.getElementTypeConfig)(t,n);return r.default.createElement("div",{className:"element-editor-drag-preview",style:u},r.default.createElement(a.default,{element:t,type:c,simple:!0}))}}f.propTypes={element:i.elementType,elementTypes:o.default.arrayOf(s.elementTypeType),isDragging:o.default.bool,currentOffset:o.default.shape({x:o.default.number.isRequired,y:o.default.number.isRequired})};t.default=(0,l.DragLayer)((e=>({element:e.getItem(),currentOffset:e.getSourceClientOffset(),isDragging:e.isDragging()})))(f)},6721: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=p(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=f(n(6935)),a=n(5207),l=n(9367),i=n(9791),s=n(1820),d=f(n(3695)),u=f(n(920)),c=f(n(5799));function f(e){return e&&e.__esModule?e:{default:e}}function p(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(p=function(e){return e?n:t})(e)}class m extends r.PureComponent{constructor(e){super(e),this.state={dragTargetElementId:null,dragSpot:null},this.handleDragOver=this.handleDragOver.bind(this),this.handleDragEnd=this.handleDragEnd.bind(this)}handleDragOver(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const n=!!e&&e.id;this.setState({dragTargetElementId:n,dragSpot:!1===t?"bottom":"top"})}handleDragEnd(e,t){const{actions:{handleSortBlock:n},areaId:r}=this.props;n(e,t,r).then((()=>{const e=window.jQuery(".cms-preview");e.entwine("ss.preview")._loadUrl(e.find("iframe").attr("src"))})),this.setState({dragTargetElementId:null,dragSpot:null})}render(){const{ToolbarComponent:e,ListComponent:t,areaId:n,elementTypes:o,isDraggingOver:a,connectDropTarget:l,allowedElements:i,sharedObject:s}=this.props,{dragTargetElementId:d,dragSpot:c}=this.state,f=i.map((e=>o.find((t=>t.class===e))));return l(r.default.createElement("div",{className:"element-editor"},r.default.createElement(e,{elementTypes:f,areaId:n,onDragOver:this.handleDragOver}),r.default.createElement(t,{allowedElementTypes:f,elementTypes:o,areaId:n,onDragOver:this.handleDragOver,onDragStart:this.handleDragStart,onDragEnd:this.handleDragEnd,dragSpot:c,isDraggingOver:a,dragTargetElementId:d,sharedObject:s}),r.default.createElement(u.default,{elementTypes:o})))}}t.Component=m,m.propTypes={elementTypes:o.default.arrayOf(i.elementTypeType).isRequired,allowedElements:o.default.arrayOf(o.default.string).isRequired,areaId:o.default.number.isRequired,actions:o.default.shape({handleSortBlock:o.default.func})};t.default=(0,l.compose)(c.default,(0,s.DropTarget)("element",{},((e,t)=>({connectDropTarget:e.dropTarget(),isDraggingOver:t.isOver()}))),(0,a.inject)(["ElementToolbar","ElementList"],((e,t)=>({ToolbarComponent:e,ListComponent:t})),(()=>"ElementEditor")),d.default)(m)},2512: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=g(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=m(n(6935)),a=n(455),l=n(9791),i=n(9367),s=n(5207),d=m(n(6923)),u=m(n(5815)),c=n(1820),f=n(9306),p=n(1135);function m(e){return e&&e.__esModule?e:{default:e}}function g(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(g=function(e){return e?n:t})(e)}class h extends r.Component{constructor(e){super(e),this.resetState=this.resetState.bind(this),this.handleBeforeSubmitForm=this.handleBeforeSubmitForm.bind(this),this.handleAfterSubmitResponse=this.handleAfterSubmitResponse.bind(this),this.state={saveAllElements:!1,increment:0,hasUnsavedChangesBlockIDs:{},validBlockIDs:{}},this.props.sharedObject.setState=this.setState.bind(this)}componentDidUpdate(e,t){if(this.props.blocks===e.blocks){if(this.state.saveAllElements){const e=this.props.blocks.map((e=>parseInt(e.id,10))).filter((e=>this.state.hasUnsavedChangesBlockIDs[e]));if(e.every((e=>null!==this.state.validBlockIDs[e]))){const n=e.every((e=>this.state.validBlockIDs[e])),r={success:n,reason:n?"":"invalid"};this.props.sharedObject.entwineResolve(r),this.resetState(t,n),this.setState({saveAllElements:!1})}}}else this.resetState(t,!1)}resetState(e,t){const n={},r={};(this.props.blocks||[]).forEach((o=>{const a=parseInt(o.id,10);t?n[a]=!1:e.hasUnsavedChangesBlockIDs.hasOwnProperty(a)?n[a]=e.hasUnsavedChangesBlockIDs[a]:n[a]=!1,r[a]=null})),this.setState({hasUnsavedChangesBlockIDs:n,validBlockIDs:r})}handleChangeHasUnsavedChanges(e,t){this.setState((n=>({hasUnsavedChangesBlockIDs:{...n.hasUnsavedChangesBlockIDs,[e]:t}})))}handleBeforeSubmitForm(e){this.setState((t=>({validBlockIDs:{...t.validBlockIDs,[e]:null}})))}handleAfterSubmitResponse(e,t){this.setState((n=>({hasUnsavedChangesBlockIDs:{...n.hasUnsavedChangesBlockIDs,[e]:!t},validBlockIDs:{...n.validBlockIDs,[e]:t}})))}getDragIndicatorIndex(){const{dragTargetElementId:e,draggedItem:t,blocks:n,dragSpot:r}=this.props;return(0,f.getDragIndicatorIndex)(n.map((e=>e.id)),e,t&&t.id,r)}renderBlocks(){const{ElementComponent:e,HoverBarComponent:t,DragIndicatorComponent:n,blocks:o,allowedElementTypes:a,elementTypes:l,areaId:i,onDragEnd:s,onDragOver:d,onDragStart:c,isDraggingOver:f}=this.props;if(!o)return null;if(o&&!o.length)return r.default.createElement("div",null,u.default._t("ElementList.ADD_BLOCKS","Add blocks to place your content"));let m=o.map((n=>{const o=this.state.saveAllElements&&this.state.hasUnsavedChangesBlockIDs[n.id]&&null===this.state.validBlockIDs[n.id];return r.default.createElement("div",{key:n.id},r.default.createElement(e,{element:n,areaId:i,type:(0,p.getElementTypeConfig)(n,l),link:n.blockSchema.actions.edit,onDragOver:d,onDragEnd:s,onDragStart:c,saveElement:o,onChangeHasUnsavedChanges:e=>this.handleChangeHasUnsavedChanges(n.id,e),onBeforeSubmitForm:()=>this.handleBeforeSubmitForm(n.id),onAfterSubmitResponse:e=>this.handleAfterSubmitResponse(n.id,e),increment:this.state.increment}),f||r.default.createElement(t,{key:`create-after-${n.id}`,areaId:i,elementId:n.id,elementTypes:a}))}));f||(m=[r.default.createElement(t,{key:0,areaId:i,elementId:0,elementTypes:a})].concat(m));const g=this.getDragIndicatorIndex();return f&&null!==g&&m.splice(g,0,r.default.createElement(n,{key:"DropIndicator"})),m}renderLoading(){const{loading:e,LoadingComponent:t}=this.props;return e?r.default.createElement(t,null):null}render(){const{blocks:e}=this.props,t=(0,d.default)("elemental-editor-list",{"elemental-editor-list--empty":!e||!e.length});return this.props.connectDropTarget(r.default.createElement("div",{className:t},this.renderLoading(),this.renderBlocks()))}}t.Component=h,h.propTypes={blocks:o.default.arrayOf(a.elementType),elementTypes:o.default.arrayOf(l.elementTypeType).isRequired,allowedElementTypes:o.default.arrayOf(l.elementTypeType).isRequired,loading:o.default.bool,areaId:o.default.number.isRequired,dragTargetElementId:o.default.oneOfType([o.default.string,o.default.bool]),onDragOver:o.default.func,onDragStart:o.default.func,onDragEnd:o.default.func,sharedObject:o.default.object.isRequired},h.defaultProps={blocks:[],loading:!1,sharedObject:{entwineResolve:()=>{},setState:null}};const b={drop(e,t){const{blocks:n}=e,r=t.getDropResult();if(!r)return{};const o=(0,f.getDragIndicatorIndex)(n.map((e=>e.id)),r.target,t.getItem(),r.dropSpot),a=n[o-1]?n[o-1].id:"0";return{...r,dropAfterID:a}}};t.default=(0,i.compose)((0,c.DropTarget)("element",b,((e,t)=>({connectDropTarget:e.dropTarget(),draggedItem:t.getItem()}))),(0,s.inject)(["Element","Loading","HoverBar","DragPositionIndicator"],((e,t,n,r)=>({ElementComponent:e,LoadingComponent:t,HoverBarComponent:n,DragIndicatorComponent:r})),(()=>"ElementEditor.ElementList")))(h)},7651: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=h(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=g(n(6935)),a=n(3556),l=n(455),i=n(9791),s=n(9367),d=n(5207),u=g(n(5815)),c=g(n(6923)),f=n(1820),p=n(9306),m=n(8724);function g(e){return e&&e.__esModule?e:{default:e}}function h(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(h=function(e){return e?n:t})(e)}class b extends r.Component{constructor(e){super(e),this.toggle=this.toggle.bind(this),this.state={tooltipOpen:!1}}componentDidMount(){const{connectDragPreview:e}=this.props;e&&e((0,m.getEmptyImage)(),{captureDraggingState:!0})}componentDidUpdate(){const{tooltipOpen:e}=this.state,{disableTooltip:t}=this.props;e&&t&&this.setState({tooltipOpen:!1})}getBlockTitle(e,t){return t.broken?u.default.inject(u.default._t("ElementHeader.BROKEN","This element is of obsolete type {type}."),{type:t.obsoleteClassName}):e.title?e.title:u.default.inject(u.default._t("ElementHeader.NOTITLE","Untitled {type} block"),{type:t.title})}toggle(){this.setState((e=>({tooltipOpen:!e.tooltipOpen})))}renderVersionedStateMessage(){const{element:{isLiveVersion:e,isPublished:t}}=this.props;if(t&&e)return null;let n="";const o=["element-editor-header__version-state"];return t?e||(n=u.default._t("ElementHeader.STATE_MODIFIED","Item has unpublished changes"),o.push("element-editor-header__version-state--modified")):(n=u.default._t("ElementHeader.STATE_DRAFT","Item has not been published yet"),o.push("element-editor-header__version-state--draft")),r.default.createElement("span",{className:(0,c.default)(o),title:n})}renderStatusBadge(){const{element:{isLiveVersion:e,isPublished:t}}=this.props;if(t&&e)return null;let n="",o="";const a=["badge"];return t?e||(n=u.default._t("ElementHeader.BADGE_MODIFIED","Modified"),o=u.default._t("ElementHeader.STATE_MODIFIED","Item has unpublished changes"),a.push("status-modified")):(n=u.default._t("ElementHeader.BADGE_DRAFT","Draft"),o=u.default._t("ElementHeader.STATE_DRAFT","Item has not been published yet"),a.push("status-addedtodraft")),r.default.createElement("span",{className:(0,c.default)(a),title:o},n)}render(){const{connectDragSource:e,element:t,type:n,areaId:o,previewExpanded:l,simple:i,disableTooltip:s,activeTab:d,expandable:f,ElementActionsComponent:p,handleEditTabsClick:m}=this.props,g=this.getBlockTitle(t,n),h=(0,c.default)({"element-editor-header__title":!0,"element-editor-header__title--none":!t.title}),b=u.default._t("ElementHeader.EXPAND","Show editable fields"),v=(0,c.default)("element-editor-header",{"element-editor-header--simple":i}),y=(0,c.default)("element-editor-header__icon-container",{"element-editor-header__icon-container--broken":n.broken}),_=(0,c.default)("element-editor-header__expand",{"font-icon-right-open-big":!f,"font-icon-up-open-big":f&&l,"font-icon-down-open-big":f&&!l}),E=`element-icon-${t.id}`,O=r.default.createElement("div",{className:v},r.default.createElement("div",{className:"element-editor-header__drag-handle"},r.default.createElement("i",{className:"font-icon-drag-handle"})),r.default.createElement("div",{className:"element-editor-header__info"},r.default.createElement("div",{className:y},r.default.createElement("i",{className:n.icon,id:E}),this.renderVersionedStateMessage(),!n.broken&&!i&&r.default.createElement(a.Tooltip,{placement:"top",isOpen:this.state.tooltipOpen&&!s,target:E,toggle:this.toggle},n.title)),r.default.createElement("h3",{className:h},g),this.renderStatusBadge()),!i&&r.default.createElement("div",{className:"element-editor-header__actions"},r.default.createElement("div",{role:"none",onClick:e=>e.stopPropagation()},r.default.createElement(p,{element:t,type:n,areaId:o,activeTab:d,editTabs:n.editTabs,handleEditTabsClick:m,expandable:f})),!n.broken&&r.default.createElement("i",{className:_,title:b})));return l?e(O):O}}t.Component=b,b.propTypes={element:l.elementType.isRequired,type:i.elementTypeType.isRequired,areaId:o.default.number,activeTab:o.default.string,simple:o.default.bool,ElementActionsComponent:o.default.elementType,previewExpanded:o.default.bool,disableTooltip:o.default.bool,connectDragSource:o.default.func.isRequired,connectDragPreview:o.default.func.isRequired,onDragEnd:o.default.func},b.defaultProps={expandable:!0};t.default=(0,s.compose)((0,f.DragSource)("element",p.elementDragSource,(e=>({connectDragSource:e.dragSource(),connectDragPreview:e.dragPreview()}))),(0,d.inject)(["ElementActions"],(e=>({ElementActionsComponent:e})),(()=>"ElementEditor.ElementList.Element")))(b)},8953: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={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=s(n(6935)),a=n(5207),l=n(9791),i=s(n(5815));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 u=(0,s(n(7577)).default)("element-editor__hover-bar");function c(e){let{AddElementPopoverComponent:t,elementTypes:n,elementId:o,areaId:a,popoverOpen:l,onToggle:s}=e;const d=`${u("-line")} font-icon-plus-circled`,c=i.default._t("ElementAddNewButton.ADD_BLOCK","Add block"),f={className:u("-area",{"-area--focus":l}),onClick:s,"aria-label":c,title:c,id:`AddBlockHoverBarArea_${a}_${o}`};return r.default.createElement("div",{className:u(""),id:`AddBlockHoverBar_${a}_${o}`},r.default.createElement("button",f,r.default.createElement("span",{className:u("-area-inner")},r.default.createElement("span",{className:d}))),r.default.createElement(t,{placement:"bottom",target:`AddBlockHoverBarArea_${a}_${o}`,isOpen:l,elementTypes:n,toggle:s,container:`#AddBlockHoverBar_${a}_${o}`,areaId:a,insertAfterElement:o}))}class f extends r.Component{constructor(e){super(e),this.toggle=this.toggle.bind(this),this.state={popoverOpen:!1}}toggle(){this.setState((e=>({popoverOpen:!e.popoverOpen})))}render(){const e={...this.state,...this.props,onToggle:this.toggle};return r.default.createElement(c,e)}}t.Component=f,f.propTypes={elementTypes:o.default.arrayOf(l.elementTypeType).isRequired,elementId:o.default.oneOfType([o.default.string,o.default.number]).isRequired,areaId:o.default.oneOfType([o.default.number,o.default.string]).isRequired};t.default=(0,a.inject)(["AddElementPopover"],(e=>({AddElementPopoverComponent:e})),(()=>"ElementEditor.ElementList.HoverBar"))(f)},7333:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=f(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=c(n(6935)),a=c(n(6923)),l=c(n(55)),i=n(7785),s=c(n(5815)),d=n(2153),u=n(9040);function c(e){return e&&e.__esModule?e:{default:e}}function f(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(f=function(e){return e?n:t})(e)}function p(){return p=Object.assign?Object.assign.bind():function(e){for(var t=1;t{t.match(/PageElements_[0-9]+_Title/)&&(r=e[t])})),n().then((e=>this.props.onFormSchemaSubmitResponse(e,r)))}render(){const{elementId:e,extraClass:t,onClick:n,onFormInit:o,formHasState:s,notVisible:d}=this.props,{loadingError:u}=this.state,c=(0,a.default)("element-editor-editform",t),f={formTag:"form",schemaUrl:(0,i.loadElementSchemaValue)("schemaUrl",e),identifier:"element",refetchSchemaOnMount:!s,onLoadingError:this.handleLoadingError,onSubmit:this.handleSubmit};u&&(f.loading=!1),"function"==typeof o&&(f.onReduxFormInit=o);const m={};return d&&(m["aria-hidden"]="true"),r.default.createElement("div",p({className:c,onClick:n,role:"presentation"},m),r.default.createElement(l.default,f))}}m.propTypes={extraClass:o.default.oneOfType([o.default.string,o.default.object]),onClick:o.default.func,elementId:o.default.string,handleLoadingError:o.default.func,onFormSchemaSubmitResponse:o.default.func,notVisible:o.default.bool};t.default=(0,u.connect)((function(e,t){const n=(0,d.loadElementFormStateName)(t.elementId);return{formHasState:e.form.formState&&e.form.formState.element&&!!e.form.formState.element[n]}}))(m)},7660:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=s(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=i(n(6935)),a=i(n(6923)),l=i(n(5815));function i(e){return e&&e.__esModule?e:{default:e}}function s(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(s=function(e){return e?n:t})(e)}class d extends r.PureComponent{render(){const{fileUrl:e,fileTitle:t,content:n,broken:o}=this.props,i=l.default._t("ElementSummary.NO_PREVIEW","No preview available"),s=(0,a.default)("element-editor-summary__content",{"element-editor-summary__content--broken":o});return r.default.createElement("div",{className:"element-editor-summary"},e&&r.default.createElement("img",{className:"element-editor-summary__thumbnail-image",src:e,alt:t}),(n||!e)&&r.default.createElement("p",{className:s},n||i))}}d.defaultProps={},d.propTypes={content:o.default.string,fileUrl:o.default.string,fileTitle:o.default.string,broken:o.default.bool};t.default=d},5563:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,o=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=d(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&{}.hasOwnProperty.call(e,a)){var l=o?Object.getOwnPropertyDescriptor(e,a):null;l&&(l.get||l.set)?Object.defineProperty(r,a,l):r[a]=e[a]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=(r=n(6935))&&r.__esModule?r:{default:r},l=n(5207),i=n(9791),s=n(1820);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)}class u extends o.PureComponent{render(){const{AddNewButtonComponent:e,elementTypes:t,areaId:n,connectDropTarget:r}=this.props;return r(o.default.createElement("div",{className:"element-editor__toolbar"},o.default.createElement(e,{elementTypes:t,areaId:n})))}}u.defaultProps={},u.propTypes={elementTypes:a.default.arrayOf(i.elementTypeType).isRequired,areaId:a.default.number.isRequired,AddNewButtonComponent:a.default.elementType.isRequired,connectDropTarget:a.default.func.isRequired,onDragOver:a.default.func,onDragDrop:a.default.func};const c={hover(e){const{onDragOver:t}=e;t&&t()}};t.default=(0,s.DropTarget)("element",c,(e=>({connectDropTarget:e.dropTarget()})))((0,l.inject)(["ElementAddNewButton"],(e=>({AddNewButtonComponent:e})),(()=>"ElementEditor.ElementToolbar"))(u))},2977:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=l(n(1594)),o=l(n(5815)),a=l(n(6923));function l(e){return e&&e.__esModule?e:{default:e}}t.default=e=>class extends e{getClassName(){const e=[super.getClassName()];return this.props.data.ElementID&&e.unshift("elemental-area__element--historic-inner"),(0,a.default)(e)}render(){const e=this.getLegend(),t=this.props.data.tag||"div",n=this.getClassName(),{data:a}=this.props;return a.ElementID?r.default.createElement(t,{className:n},e,r.default.createElement("div",{className:"elemental-preview elemental-preview--historic"},a.ElementEditLink&&r.default.createElement("a",{className:"elemental-preview__link",href:a.ElementEditLink},r.default.createElement("span",{className:"elemental-preview__link-text"},o.default._t("HistoricElementView.VIEW_BLOCK_HISTORY","Block history")),r.default.createElement("i",{className:"font-icon-angle-right btn--icon-lg elemental-preview__link-caret"})),r.default.createElement("div",{className:"elemental-preview__icon"},r.default.createElement("i",{className:a.ElementIcon})),r.default.createElement("div",{className:"elemental-preview__detail"},r.default.createElement("h3",null,a.ElementTitle," ",r.default.createElement("small",null,a.ElementType)))),this.props.children):super.render()}}},6573:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=l(n(1594)),o=n(3556),a=l(n(2623));function l(e){return e&&e.__esModule?e:{default:e}}t.default=(0,a.default)((e=>{const{children:t}=e,n=r.default.Children.toArray(r.default.Children.map(t,((t,n)=>{const o={};return 0===n?(o.id=e.id,o.title=null):1===n&&(o.noHolder=!0),r.default.cloneElement(t,o)})));return e.readOnly?r.default.createElement("div",{className:"text-checkbox-group-field--read-only"},n):1===n.length?n[0]:r.default.createElement(o.InputGroup,{className:"text-checkbox-group-field"},n[0],r.default.createElement(o.InputGroupAddon,{addonType:"append"},r.default.createElement(o.InputGroupText,null,n[1])))}))},51:function(e,t,n){var r=d(n(1669)),o=d(n(1594)),a=n(5145),l=n(5207),i=n(1135),s=n(5381);function d(e){return e&&e.__esModule?e:{default:e}}const u=()=>{window.ss.apolloClient.resetStore(),setTimeout((()=>{const{store:e}=window.ss;e&&e.dispatch((0,s.destroy)(...Object.keys(e.getState().form.formState.element||{}).map((e=>`element.${e}`))))}),0)};r.default.entwine("ss",(e=>{e(".js-injector-boot .element-editor__container").entwine({ReactRoot:null,AreaIDsSharedObject:{},Increment:0,onmatch(){const e=(0,l.loadComponent)("ElementEditor",{}),t=this.data("schema"),n=(0,i.getConfig)().elementTypes,r=t["elemental-area-id"],s=this.getAreaIDsSharedObject();s.hasOwnProperty(r)||(s[r]={entwineResolve:null,setState:null});const d={areaId:r,allowedElements:t["allowed-elements"],elementTypes:n,sharedObject:s[r]};let u=this.getReactRoot();u||(u=(0,a.createRoot)(this[0]),this.setReactRoot(u)),u.render(o.default.createElement(e,d))},onunmatch(){e(".cms-edit-form").data("hasValidationErrors")||u(),this.unmountComponent()},unmountComponent(){const e=this.getReactRoot();e&&(e.unmount(),this.setReactRoot(null))},"from .cms-edit-form":{onbeforesubmitform(e,t){if(!t)return;let n;const r=new Promise((e=>{n=e}));t.promises.push(r),t.onAjaxSuccessCallbacks.push(this.unmountComponent.bind(this));const o=this.data("schema")["elemental-area-id"],a=this.getAreaIDsSharedObject()[o],l=this.getIncrement()+1;this.setIncrement(l),a.entwineResolve=n,a.setState({saveAllElements:!0,increment:l})},onaftersubmitform(t,n){const r=JSON.parse(n.xhr.responseText).ValidationResult;JSON.parse(r.replace(/<\/?script[^>]*?>/g,"")).isValid?(e(".cms-edit-form").data("hasValidationErrors",!1),u()):e(".cms-edit-form").data("hasValidationErrors",!0)}}}),e(".js-injector-boot .element-editor__container .element-form-dirty-state").entwine({onmatch(){e(".cms-edit-form").trigger("change")},onunmatch(){e(".cms-edit-form").trigger("change")}}),e(".cms-edit-form").entwine({getChangeTrackerOptions(){const t=void 0===this.entwineData("ChangeTrackerOptions");let n=this._super();return t&&(n=e.extend({},n),n.ignoreFieldSelector+=", .elementalarea :input:not(.element-form-dirty-state)",this.setChangeTrackerOptions(n)),n}})}))},9306:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.isOverTop=t.getDragIndicatorIndex=t.elementDragSource=void 0;var r=n(4518);t.isOverTop=(e,t)=>{const n=e.getClientOffset(),o=(0,r.findDOMNode)(t).getBoundingClientRect();return n.y{if(null===t||!n)return null;let o=t?e.findIndex((e=>e===t)):0;const a=e.findIndex((e=>e===n));return"bottom"===r&&(o+=1),a===o||a+1===o?null:o};t.elementDragSource={beginDrag(e){return e.element},endDrag(e,t){const{onDragEnd:n}=e,r=t.getDropResult();if(!n||!r||!r.dropAfterID)return;const o=t.getItem().id,{dropAfterID:a}=r;o!==a&&n(o,a)}}},7577:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,o=(r=n(6923))&&r.__esModule?r:{default:r};t.default=e=>function(){const t=t=>`${e}${t}`;for(var n=arguments.length,r=new Array(n),a=0;a!(!e&&""!==e)&&("object"==typeof e?Array.isArray(e)?e.map(t):Object.entries(e).reduce(((e,n)=>{let[r,o]=n;return Object.assign({},e,{[t(r)]:o})}),{}):t(e))));return(0,o.default)(...l)}},6283:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.mutation=t.default=t.config=void 0;var r,o=n(708),a=(r=n(7284))&&r.__esModule?r:{default:r},l=n(3521);const i=t.mutation=a.default` -mutation AddElementToArea($className: String!, $elementalAreaID: ID!, $afterElementID: ID) { - addElementToArea( - className: $className, - elementalAreaID: $elementalAreaID, - afterElementID: $afterElementID - ) { - id - } -} -`,s=t.config={props:e=>{let{mutate:t,ownProps:{actions:n,areaId:r}}=e;return{actions:{...n,handleAddElementToArea:(e,n)=>t({variables:{className:e,elementalAreaID:r,afterElementID:n}})}}},options:e=>{let{areaId:t}=e;return{refetchQueries:[{query:l.query,variables:l.config.options({areaId:t}).variables}]}}};t.default=(0,o.graphql)(i,s)},1429:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.mutation=t.default=t.config=void 0;var r,o=n(708),a=(r=n(7284))&&r.__esModule?r:{default:r},l=n(3521);const i=t.mutation=a.default` -mutation ArchiveBlock($blockId: ID!) { - deleteBlocks(ids: [$blockId]) -} -`,s=t.config={props:e=>{let{mutate:t,ownProps:{actions:n}}=e;return{actions:{...n,handleArchiveBlock:e=>t({variables:{blockId:e}})}}},options:e=>{let{areaId:t}=e;return{refetchQueries:[{query:l.query,variables:l.config.options({areaId:t}).variables}]}}};t.default=(0,o.graphql)(i,s)},9324:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.mutation=t.default=t.config=void 0;var r,o=n(708),a=(r=n(7284))&&r.__esModule?r:{default:r},l=n(3521);const i=t.mutation=a.default` -mutation DuplicateBlock($blockId: ID!) { - duplicateBlock(id: $blockId) { - id - } -} -`,s=t.config={props:e=>{let{mutate:t,ownProps:{actions:n}}=e;return{actions:{...n,handleDuplicateBlock:e=>t({variables:{blockId:e}})}}},options:e=>{let{areaId:t}=e;return{refetchQueries:[{query:l.query,variables:l.config.options({areaId:t}).variables}]}}};t.default=(0,o.graphql)(i,s)},1135:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.getElementTypeConfig=t.getConfig=void 0;var r,o=(r=n(6177))&&r.__esModule?r:{default:r};const a=()=>o.default.getSection("DNADesign\\Elemental\\Controllers\\ElementalAreaController");t.getConfig=a;t.getElementTypeConfig=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const n=e.blockSchema.typeName;let r=(Array.isArray(t)?t:a().elementTypes).find((e=>e.class===n||e.name===n));return e.obsoleteClassName&&(r=Object.assign({obsoleteClassName:e.obsoleteClassName},r),Object.preventExtensions(r)),r}},2153:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.loadElementFormStateName=void 0;var r,o=(r=n(6177))&&r.__esModule?r:{default:r};t.loadElementFormStateName=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;const t=o.default.getSection("DNADesign\\Elemental\\Controllers\\ElementalAreaController").form.elementForm.formNameTemplate;return e?t.replace("{id}",e):t}},7785:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.loadElementSchemaValue=void 0;var r,o=(r=n(6177))&&r.__esModule?r:{default:r};t.loadElementSchemaValue=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const n=o.default.getSection("DNADesign\\Elemental\\Controllers\\ElementalAreaController").form.elementForm[e]||"";return t?`${n}/${t}`:n}},4242:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.publishBlockMutation=void 0;var r,o=(r=n(7284))&&r.__esModule?r:{default:r};t.publishBlockMutation=o.default` -mutation PublishBlock($blockId:ID!) { - publishBlock(id: $blockId) { - id - } -} -`},3521:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.query=t.default=t.config=void 0;var r,o=n(708),a=(r=n(7284))&&r.__esModule?r:{default:r};const l=t.query=a.default` -query ReadBlocksForArea($id:ID!) { - readOneElementalArea(filter: { id: { eq: $id } }, versioning: { - mode: DRAFT - }){ - elements { - id - title - blockSchema - obsoleteClassName - isLiveVersion - isPublished - version - canCreate - canPublish - canUnpublish - canDelete - } - } -} -`,i=t.config={options(e){let{areaId:t}=e;return{variables:{id:t}}},props(e){let{data:{error:t,readOneElementalArea:n,loading:r}}=e,o=null;n&&(o=n.elements);return{loading:r||!o,blocks:o,graphQLErrors:t&&t.graphQLErrors&&t.graphQLErrors.map((e=>e.message))}}};t.default=(0,o.graphql)(l,i)},3695:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.mutation=t.default=t.config=void 0;var r,o=n(708),a=(r=n(7284))&&r.__esModule?r:{default:r},l=n(3521);const i=t.mutation=a.default` -mutation SortBlockMutation($blockId:ID!, $afterBlockId:ID!) { - sortBlock( - id: $blockId - afterBlockID: $afterBlockId - ) { - id - isLiveVersion - isPublished - } -} -`,s=t.config={props:e=>{let{mutate:t,ownProps:{actions:n}}=e;return{actions:{...n,handleSortBlock:(e,n,r)=>t({variables:{blockId:e,afterBlockId:n},optimisticResponse:{sortBlock:{id:e,isLiveVersion:!1,isPublished:!1,__typename:"Block"}},update:(t,o)=>{let{data:{sortBlock:a}}=o;const i=l.config.options({areaId:r}).variables,s=t.readQuery({query:l.query,variables:i}),d=JSON.parse(JSON.stringify(s));let u=d.readOneElementalArea.elements;const c=u.findIndex((t=>t.id===e)),f=u[c];if(Object.entries(a).forEach((e=>{let[t,n]=e;"__typename"!==t&&(f[t]=n)})),u.splice(c,1),"0"===n)u.unshift(f);else{let e=u.findIndex((e=>e.id===n));-1===e&&(e=c-1);const t=u.slice(e+1);u=u.slice(0,e+1),u.push(f),u=u.concat(t)}d.readOneElementalArea.elements=u,t.writeQuery({query:l.query,data:d,variables:i})}})}}}};t.default=(0,o.graphql)(i,s)},2393:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.mutation=t.default=t.config=void 0;var r,o=n(708),a=(r=n(7284))&&r.__esModule?r:{default:r},l=n(3521);const i=t.mutation=a.default` -mutation UnpublishBlock($blockId:ID!) { - unpublishBlock( - id: $blockId - ) { - id - } -} -`,s=t.config={props:e=>{let{mutate:t,ownProps:{actions:n}}=e;return{actions:{...n,handleUnpublishBlock:(e,n,r,o)=>t({variables:{blockId:e,fromStage:n,toStage:r,fromVersion:o}})}}},options:e=>{let{areaId:t}=e;return{refetchQueries:[{query:l.query,variables:l.config.options({areaId:t}).variables}]}}};t.default=(0,o.graphql)(i,s)},5071:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.query=t.default=t.config=void 0;var r,o=n(708),a=(r=n(7284))&&r.__esModule?r:{default:r};const l=t.query=a.default` -query ReadHistoryViewerBlock ($block_id: ID!, $limit: Int!, $offset: Int!) { - readOneBlock( - versioning: { - mode: LATEST - }, - filter: { id: { eq: $block_id } } - ) { - id - versions (limit: $limit, offset: $offset, sort: { version: DESC }) { - pageInfo { - totalCount - } - nodes { - version - absoluteLink - author { - firstName - surname - } - publisher { - firstName - surname - } - published - liveVersion - latestDraftVersion - lastEdited - } - } - } -} -`,i=t.config={options(e){let{recordId:t,limit:n,page:r}=e;return{variables:{limit:n,offset:((r||1)-1)*n,block_id:t}}},props(e){let{data:{error:t,refetch:n,readOneBlock:r,loading:o},ownProps:{actions:a={versions:{}},limit:l,recordId:i}}=e;const s=r||null;return{loading:o||!s,versions:s,graphQLErrors:t&&t.graphQLErrors&&t.graphQLErrors.map((e=>e.message)),actions:{...a,versions:{...s,goToPage(e){n({offset:((e||1)-1)*l,limit:l,block_id:i})}}}}}};t.default=(0,o.graphql)(l,i)},8633:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.mutation=t.default=t.config=void 0;var r,o=n(708),a=(r=n(7284))&&r.__esModule?r:{default:r};const l=t.mutation=a.default` -mutation revertBlockToVersion($id:ID!, $fromStage:VersionedStage!, $toStage:VersionedStage!, $fromVersion:Int!) { - copyBlockToStage(input: { - id: $id - fromVersion: $fromVersion - fromStage: $fromStage - toStage: $toStage - }) { - id - } -} -`,i=t.config={props:e=>{let{mutate:t,ownProps:{actions:n}}=e;return{actions:{...n,revertToVersion:(e,n,r,o)=>t({variables:{id:e,fromVersion:n,fromStage:r,toStage:o}})}}},options:{refetchQueries:["ReadHistoryViewerBlock"]}};t.default=(0,o.graphql)(l,i)},455:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.elementType=void 0;var r,o=(r=n(6935))&&r.__esModule?r:{default:r};t.elementType=o.default.shape({id:o.default.string.isRequired,title:o.default.string,blockSchema:o.default.object,inlineEditable:o.default.bool,published:o.default.bool,liveVersion:o.default.bool,version:o.default.number})},9791:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.elementTypeType=void 0;var r,o=(r=n(6935))&&r.__esModule?r:{default:r};t.elementTypeType=o.default.shape({name:o.default.string,title:o.default.string,icon:o.default.string,inlineEditable:o.default.bool,editTabs:o.default.arrayOf(o.default.shape({title:o.default.string,name:o.default.string})),config:o.default.object})},8695:function(e){e.exports=ApolloClient},708:function(e){e.exports=ApolloClientReactHoc},6177:function(e){e.exports=Config},2623:function(e){e.exports=FieldHolder},55:function(e){e.exports=FormBuilderLoader},7284:function(e){e.exports=GraphQLTag},5207:function(e){e.exports=Injector},6935:function(e){e.exports=PropTypes},1594:function(e){e.exports=React},1820:function(e){e.exports=ReactDND},8724:function(e){e.exports=ReactDNDHtml5Backend},4518:function(e){e.exports=ReactDom},5145:function(e){e.exports=ReactDomClient},9040:function(e){e.exports=ReactRedux},3556:function(e){e.exports=Reactstrap},9367:function(e){e.exports=Redux},5381:function(e){e.exports=ReduxForm},5034:function(e){e.exports=TabsActions},8918:function(e){e.exports=ToastsActions},9077:function(e){e.exports=UnsavedFormsActions},6923:function(e){e.exports=classnames},5815:function(e){e.exports=i18n},1669:function(e){e.exports=jQuery},5799:function(e){e.exports=withDragDropContext}},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var a=t[r]={exports:{}};return e[r](a,a.exports,n),a.exports}n(51),n(2038)}(); \ No newline at end of file +!function(){"use strict";var e={2038:function(e,t,n){var r=o(n(4121)),a=o(n(1240));function o(e){return e&&e.__esModule?e:{default:e}}window.document.addEventListener("DOMContentLoaded",(()=>{(0,r.default)(),(0,a.default)()}))},4121:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=v(n(5207)),a=v(n(584)),o=v(n(7105)),l=v(n(6721)),i=v(n(2512)),s=v(n(5563)),d=v(n(5297)),u=v(n(7651)),c=v(n(1305)),f=v(n(7660)),p=v(n(7333)),m=v(n(2382)),g=v(n(8953)),h=v(n(1950)),b=v(n(6573));function v(e){return e&&e.__esModule?e:{default:e}}t.default=()=>{r.default.component.registerMany({ElementEditor:l.default,ElementToolbar:s.default,ElementAddNewButton:d.default,ElementList:i.default,Element:a.default,ElementActions:o.default,ElementHeader:u.default,ElementContent:c.default,ElementSummary:f.default,ElementInlineEditForm:p.default,AddElementPopover:m.default,HoverBar:g.default,DragPositionIndicator:h.default,TextCheckboxGroupField:b.default})}},1240:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=c(n(5207)),a=c(n(2977)),o=c(n(3337)),l=c(n(4)),i=c(n(9283)),s=c(n(8501)),d=c(n(2221)),u=c(n(5904));function c(e){return e&&e.__esModule?e:{default:e}}t.default=()=>{r.default.transform("elemental-fieldgroup",(e=>{e.component("FieldGroup.HistoryViewer.VersionDetail",a.default,"HistoricElement")}),{after:"field-holders"}),r.default.transform("blocks-history-revert",(e=>{e.component("HistoryViewerToolbar.VersionedAdmin.HistoryViewer.Element.HistoryViewerVersionDetail",o.default,"BlockRevertRequest")})),r.default.transform("element-actions",(e=>{e.component("ElementActions",s.default,"ElementActionsWithSave"),e.component("ElementActions",d.default,"ElementActionsWithPublish"),e.component("ElementActions",u.default,"ElementActionsWithUnpublish"),e.component("ElementActions",i.default,"ElementActionsWithDuplicate"),e.component("ElementActions",l.default,"ElementActionsWithArchive")}))}},26:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=s(n(1594)),a=s(n(6935)),o=s(n(6923)),l=n(3556),i=n(9791);function s(e){return e&&e.__esModule?e:{default:e}}const d=e=>{const{className:t,title:n,label:a}=e,i={className:(0,o.default)(t,"dropdown-item"),...e};return r.default.createElement(l.DropdownItem,i,a||n)};d.propTypes={disabled:a.default.bool,className:a.default.string,onClick:a.default.func,title:a.default.string,name:a.default.string,type:i.elementTypeType,active:a.default.bool,label:a.default.string},d.defaultProps={disabled:!1};t.default=d},4: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=c(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=u(n(26)),o=u(n(5815)),l=n(6721),i=u(n(5609)),s=u(n(6177)),d=n(1135);function u(e){return e&&e.__esModule?e:{default:e}}function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(c=function(e){return e?n:t})(e)}const f=e=>t=>{const{fetchElements:n}=(0,r.useContext)(l.ElementEditorContext),u=void 0!==t.element.canDelete&&!t.element.canDelete,c=o.default._t("ElementArchiveAction.ARCHIVE","Archive"),f={label:c,title:u?o.default._t("ElementArchiveAction.ARCHIVE_PERMISSION_DENY","Archive, insufficient permissions"):c,disabled:u,className:"element-editor__actions-archive",onClick:e=>{e.stopPropagation();const r=t.element.isPublished;let a=o.default._t("ElementArchiveAction.CONFIRM_DELETE","Are you sure you want to send this block to the archive?");if(r&&(a=o.default._t("ElementArchiveAction.CONFIRM_DELETE_AND_UNPUBLISH","Warning: This block will be unpublished before being sent to the archive. Are you sure you want to proceed?")),!window.confirm(a))return;const l=t.element.id,u=`${(0,d.getConfig)().controllerLink.replace(/\/$/,"")}/api/delete`;i.default.post(u,{id:l},{"X-SecurityID":s.default.get("SecurityID")}).then((()=>n()))},toggle:t.toggle};return r.default.createElement(e,t,t.children,r.default.createElement(a.default,f))};t.Component=f;t.default=f},9283: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=c(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=u(n(26)),o=u(n(5815)),l=n(6721),i=u(n(5609)),s=u(n(6177)),d=n(1135);function u(e){return e&&e.__esModule?e:{default:e}}function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(c=function(e){return e?n:t})(e)}const f=e=>t=>{const{fetchElements:n}=(0,r.useContext)(l.ElementEditorContext);if(t.type.broken)return r.default.createElement(e,t);const u=void 0!==t.element.canCreate&&!t.element.canCreate,c=o.default._t("ElementArchiveAction.DUPLICATE","Duplicate"),f={label:c,title:u?o.default._t("ElementArchiveAction.DUPLICATE_PERMISSION_DENY","Duplicate, insufficient permissions"):c,disabled:u,className:"element-editor__actions-duplicate",onClick:e=>{e.stopPropagation();const r=t.element.id,a=`${(0,d.getConfig)().controllerLink.replace(/\/$/,"")}/api/duplicate`;i.default.post(a,{id:r},{"X-SecurityID":s.default.get("SecurityID")}).then((()=>n()))},toggle:t.toggle};return r.default.createElement(e,t,t.children,r.default.createElement(a.default,f))};t.Component=f;t.default=f},2221: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=s(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=i(n(26)),o=i(n(5815)),l=n(584);function i(e){return e&&e.__esModule?e:{default:e}}function s(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(s=function(e){return e?n:t})(e)}const d=e=>t=>{const{formDirty:n,onPublishButtonClick:i}=(0,r.useContext)(l.ElementContext),{element:s}=t,d=void 0!==t.element.canPublish&&!t.element.canPublish,u=o.default._t("ElementArchiveAction.PUBLISH","Publish"),c={label:u,title:d?o.default._t("ElementArchiveAction.PUBLISH_PERMISSION_DENY","Publish, insufficient permissions"):u,disabled:d,className:"element-editor__actions-publish",onClick:e=>{e.stopPropagation(),i()},toggle:t.toggle};return t.type.broken?r.default.createElement(e,t):r.default.createElement(e,t,t.children,(n||!s.isLiveVersion)&&r.default.createElement(a.default,c))};t.Component=d;t.default=d},8501: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=s(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=i(n(26)),o=i(n(5815)),l=n(584);function i(e){return e&&e.__esModule?e:{default:e}}function s(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(s=function(e){return e?n:t})(e)}const d=e=>t=>{const{onSaveButtonClick:n,formDirty:i}=(0,r.useContext)(l.ElementContext),s={title:o.default._t("ElementSaveAction.SAVE","Save"),className:"element-editor__actions-save",onClick:e=>{e.stopPropagation(),n()},toggle:t.toggle};return!t.expandable||t.type.broken?r.default.createElement(e,t):r.default.createElement(e,t,t.children,i&&r.default.createElement(a.default,s))};t.Component=d;t.default=d},5904: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=c(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=u(n(26)),o=u(n(5815)),l=u(n(5609)),i=n(6721),s=u(n(6177)),d=n(1135);function u(e){return e&&e.__esModule?e:{default:e}}function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(c=function(e){return e?n:t})(e)}const f=e=>t=>{const{fetchElements:n}=(0,r.useContext)(i.ElementEditorContext);if(t.type.broken)return r.default.createElement(e,t);const u=(e,t,n)=>{const r=o.default.inject(o.default._t("ElementHeader.NOTITLE","Untitled {type} block"),{type:e}),a=o.default.inject(o.default._t("ElementUnpublishAction.SUCCESS_NOTIFICATION","Removed '{title}' from the published page"),{title:t||r}),l=o.default.inject(o.default._t("ElementUnpublishAction.ERROR_NOTIFICATION","Error unpublishing '{title}'"),{title:t||r});window.jQuery.noticeAdd({text:n?a:l,stay:!1,type:n?"success":"error"})},{element:c,type:f}=t,p=void 0!==t.element.canUnpublish&&!t.element.canUnpublish,m=o.default._t("ElementArchiveAction.UNPUBLISH","Unpublish"),g={label:m,title:p?o.default._t("ElementArchiveAction.UNPUBLISH_PERMISSION_DENY","Unpublish, insufficient permissions"):m,disabled:p,className:"element-editor__actions-unpublish",onClick:e=>{e.stopPropagation(),(()=>{const e=t.element.id,r=`${(0,d.getConfig)().controllerLink.replace(/\/$/,"")}/api/unpublish`;return l.default.post(r,{id:e},{"X-SecurityID":s.default.get("SecurityID")}).then((()=>n()))})().then((()=>u(f.title,c.title,!0))).catch((()=>u(f.title,c.title,!1)))},toggle:t.toggle};return r.default.createElement(e,t,t.children,c.isPublished&&r.default.createElement(a.default,g))};t.Component=f;t.default=f},2382:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=p(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=f(n(6935)),o=f(n(6923)),l=n(5207),i=n(9791),s=f(n(5815)),d=f(n(5609)),u=f(n(6177)),c=n(6721);function f(e){return e&&e.__esModule?e:{default:e}}function p(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(p=function(e){return e?n:t})(e)}class m extends r.Component{constructor(e){super(e),this.handleToggle=this.handleToggle.bind(this),m.contextType=c.ElementEditorContext}getElementButtonClickHandler(e){return t=>{t.preventDefault(),d.default.post("/admin/elemental-area/api/create/",{elementClass:e.class,elementalAreaID:this.props.areaId,insertAfterElementID:this.props.insertAfterElement},{"X-SecurityID":u.default.get("SecurityID")}).then((()=>{const{fetchElements:e}=this.context;return e()})).then((()=>{const e=window.jQuery(".cms-preview");e.entwine("ss.preview")._loadUrl(e.find("iframe").attr("src"))})),this.handleToggle()}}handleToggle(){const{toggle:e}=this.props;e()}render(){const{PopoverOptionSetComponent:e,elementTypes:t,container:n,extraClass:a,isOpen:l,placement:i,target:d}=this.props,u=(0,o.default)("element-editor-add-element",a),c=t.map((e=>({content:e.title,key:e.name,className:(0,o.default)(e.icon,"btn--icon-xl","element-editor-add-element__button"),onClick:this.getElementButtonClickHandler(e)})));return r.default.createElement(e,{buttons:c,searchPlaceholder:s.default._t("ElementAddElementPopover.SEARCH_BLOCKS","Search blocks"),extraClass:u,container:n,isOpen:l,placement:i,target:d,toggle:this.handleToggle})}}m.propTypes={container:a.default.oneOfType([a.default.string,a.default.func,a.default.object]),elementTypes:a.default.arrayOf(i.elementTypeType).isRequired,extraClass:a.default.oneOfType([a.default.string,a.default.array,a.default.object]),isOpen:a.default.bool.isRequired,placement:a.default.string,target:a.default.oneOfType([a.default.string,a.default.func,a.default.object]).isRequired,toggle:a.default.func.isRequired,areaId:a.default.number.isRequired,insertAfterElement:a.default.oneOfType([a.default.number,a.default.string])};t.default=(0,l.inject)(["PopoverOptionSet"],(e=>({PopoverOptionSetComponent:e})),(()=>"ElementEditor"))(m)},5297: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=u(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=d(n(6935)),o=n(3556),l=d(n(5815)),i=n(9791),s=n(5207);function d(e){return e&&e.__esModule?e:{default:e}}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)}class c extends r.Component{constructor(e){super(e),this.toggle=this.toggle.bind(this),this.state={popoverOpen:!1}}toggle(){this.setState((e=>({popoverOpen:!e.popoverOpen})))}render(){const{AddElementPopoverComponent:e,elementTypes:t,areaId:n}=this.props,a={id:`ElementalArea${n}_AddButton`,color:"primary",onClick:this.toggle,className:"font-icon-plus"};return r.default.createElement("div",null,r.default.createElement(o.Button,a,l.default._t("ElementAddNewButton.ADD_BLOCK","Add block")),r.default.createElement(e,{placement:"bottom-start",target:a.id,isOpen:this.state.popoverOpen,elementTypes:t,toggle:this.toggle,areaId:n,insertAfterElement:0}))}}t.Component=c,c.defaultProps={},c.propTypes={elementTypes:a.default.arrayOf(i.elementTypeType).isRequired,areaId:a.default.number.isRequired};t.default=(0,s.inject)(["AddElementPopover"],(e=>({AddElementPopoverComponent:e})),(()=>"ElementEditor.ElementList.AddNewButton"))(c)},1305:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.Component=void 0;var r,a=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=s(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=(r=n(6935))&&r.__esModule?r:{default:r},l=n(5207),i=n(9367);function s(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(s=function(e){return e?n:t})(e)}class d extends a.PureComponent{render(){const{id:e,fileUrl:t,fileTitle:n,content:r,previewExpanded:o,InlineEditFormComponent:l,SummaryComponent:i,activeTab:s,onFormInit:d,handleLoadingError:u,formDirty:c,broken:f,onFormSchemaSubmitResponse:p,ensureFormRendered:m,formHasRendered:g}=this.props,h=!o&&(m||g),b={"element-editor-editform--collapsed":!o,"element-editor-editform--rendered-not-visible":h};return a.default.createElement("div",{className:"element-editor-content"},!o&&a.default.createElement(i,{content:r,fileUrl:t,fileTitle:n,broken:f}),(o||m||g)&&a.default.createElement(l,{extraClass:b,onClick:e=>e.stopPropagation(),elementId:e,activeTab:s,onFormInit:d,handleLoadingError:u,onFormSchemaSubmitResponse:p,notVisible:h}),c&&a.default.createElement("input",{type:"hidden",name:"change-tracker",className:"element-form-dirty-state",value:"1"}))}}t.Component=d,d.propTypes={id:o.default.string,content:o.default.string,fileUrl:o.default.string,fileTitle:o.default.string,previewExpanded:o.default.bool,SummaryComponent:o.default.elementType,InlineEditFormComponent:o.default.elementType,handleLoadingError:o.default.func,broken:o.default.bool,onFormSchemaSubmitResponse:o.default.func,onFormInit:o.default.func,ensureFormRendered:o.default.bool,formHasRendered:o.default.bool,formDirty:o.default.object},d.defaultProps={};t.default=(0,i.compose)((0,l.inject)(["ElementSummary","ElementInlineEditForm"],((e,t)=>({SummaryComponent:e,InlineEditFormComponent:t})),(()=>"ElementEditor.ElementList.Element")))(d)},1950:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=a(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&{}.hasOwnProperty.call(e,l)){var i=o?Object.getOwnPropertyDescriptor(e,l):null;i&&(i.get||i.set)?Object.defineProperty(r,l,i):r[l]=e[l]}return r.default=e,n&&n.set(e,r),r}(n(1594));function a(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(a=function(e){return e?n:t})(e)}class o extends r.PureComponent{render(){return r.default.createElement("div",{className:"elemental-editor-drag-indicator"},r.default.createElement("div",{className:"elemental-editor-drag-indicator__ball"}))}}t.default=o},584:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.ElementContext=t.Component=void 0;var r=S(n(1594)),a=C(n(6935)),o=n(455),l=n(9791),i=n(9367),s=n(5207),d=C(n(5815)),u=C(n(6923)),c=n(9040),f=n(5381),p=n(2153),m=n(7785),g=S(n(5034)),h=n(1820),b=n(8724),v=n(9306),y=S(n(8918)),E=n(9077),_=n(6721),O=n(1135),D=C(n(5609)),T=C(n(6177));function C(e){return e&&e.__esModule?e:{default:e}}function P(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(P=function(e){return e?n:t})(e)}function S(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=P(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}const k=t.ElementContext=(0,r.createContext)(null),I=e=>{const[t,n]=(0,r.useState)(!1),[a,o]=(0,r.useState)(""),[l,i]=(0,r.useState)(!1),[s,c]=(0,r.useState)(e.element.title),[f,p]=(0,r.useState)(!1),[m,g]=(0,r.useState)(!1),[h,v]=(0,r.useState)(!1),[y,E]=(0,r.useState)(!1),[C,P]=(0,r.useState)(!1),[S,I]=(0,r.useState)(!1),[w,j]=(0,r.useState)(!1),[M,A]=(0,r.useState)(!1),{fetchElements:R}=(0,r.useContext)(_.ElementEditorContext);(0,r.useEffect)((()=>{void 0!==e.formDirty&&!M&&A(!0)}),[e.formDirty]),(0,r.useEffect)((()=>{e.onChangeHasUnsavedChanges(M)}),[M]),(0,r.useEffect)((()=>{e.saveElement&&M&&!m&&g(!0)}),[e.saveElement,M,e.increment]),(0,r.useEffect)((()=>{e.connectDragPreview&&e.connectDragPreview((0,b.getEmptyImage)(),{captureDraggingState:!0})}),[]),(0,r.useEffect)((()=>{f&&S&&(p(!1),M?(e.submitForm(),E(!0)):v(!0))}),[f,S]),(0,r.useEffect)((()=>{w&&(j(!1),e.dispatchAddFormChanged())}),[w]);const N=()=>d.default.inject(d.default._t("ElementHeader.NOTITLE","Untitled {type} block"),{type:e.type.title}),x=t=>{(t=>{const n=s||N();if(t){const t=d.default.inject(d.default._t("ElementPublishAction.ERROR_NOTIFICATION","Error publishing '{title}'"),{title:n});e.actions.toasts.error(t)}else{const t=d.default.inject(d.default._t("ElementPublishAction.SUCCESS_NOTIFICATION","Published '{title}' successfully"),{title:n});e.actions.toasts.success(t)}})(t),v(!1),E(!1),R().then((()=>{setTimeout((()=>e.dispatchRemoveFormChanged()),250)}))};(0,r.useEffect)((()=>{S&&m&&(e.submitForm(),g(!1))}),[S,m]),(0,r.useEffect)((()=>{if(S&&h){const t=`${(0,O.getConfig)().controllerLink.replace(/\/$/,"")}/api/publish`;D.default.post(t,{id:e.element.id},{"X-SecurityID":T.default.get("SecurityID")}).then((()=>x(!1))).catch((()=>x(!0)))}}),[S,h]);const B=t=>{const{tabSetName:n,onActivateTab:r}=e;if(a||o(t),t||a)r(n,t||a);else{r(n,"Main")}},L=r=>{const{type:a,link:o}=e;a.broken||("button"!==r.target.type?!a.inlineEditable||l?window.location=o:n(!t):r.stopPropagation())},{element:F,type:W,areaId:H,HeaderComponent:U,ContentComponent:$,link:q,activeTab:V,connectDragSource:K,connectDropTarget:X,isDragging:G,isOver:Y,onDragEnd:Q,formDirty:J}=e;if(!F.id)return null;const z=(0,u.default)("element-editor__element",{"element-editor__element--broken":W.broken,"element-editor__element--expandable":W.inlineEditable&&!W.broken,"element-editor__element--dragging":G,"element-editor__element--dragged-over":Y},(()=>{const{element:t}=e,n="element-editor__element";return t.isPublished?t.isPublished&&!t.isLiveVersion?`${n}--modified`:`${n}--published`:`${n}--draft`})()),Z={formDirty:J,onPublishButtonClick:()=>{p(!0),P(!0)},onSaveButtonClick:()=>{P(!0),g(!0)}},ee=X(r.default.createElement("div",{className:z,onClick:L,onKeyUp:e=>{const{nodeName:t}=e.target;" "!==e.key&&"Enter"!==e.key||["input","textarea"].includes(t.toLowerCase())||L(e)},role:"button",tabIndex:0,title:(e=>e.broken?d.default._t("ElementalElement.ARCHIVE_BROKEN","Archive this block"):d.default.inject(d.default._t("ElementalElement.TITLE","Edit this {type} block"),{type:e.title}))(W),key:F.id},r.default.createElement(k.Provider,{value:Z},r.default.createElement(U,{element:F,type:W,areaId:H,expandable:W.inlineEditable,link:q,previewExpanded:t,handleEditTabsClick:t=>{const{activeTab:r}=e;t===r||l||(n(!0),B(t))},activeTab:V,disableTooltip:G,onDragEnd:Q}),r.default.createElement($,{id:F.id,fileUrl:F.blockSchema.fileURL,fileTitle:F.blockSchema.fileTitle,content:((e,t)=>t.broken?e.title?d.default.inject(d.default._t("ElementalElement.BROKEN_DESCRIPTION_TITLE",'This block had the title "{title}". It is broken and will not display on the front-end. You can archive it to remove it from this elemental area.'),{title:e.title}):d.default._t("ElementalElement.BROKEN_DESCRIPTION","This block is broken and will not display on the front-end. You can archive it to remove it from this elemental area."):e.blockSchema.content)(F,W),previewExpanded:t&&!G,ensureFormRendered:C,formHasRendered:S,activeTab:V,handleLoadingError:()=>{i(!0)},broken:W.broken,onFormSchemaSubmitResponse:(t,r)=>{if(t.id.match(/\/schema\/elemental-area\/([0-9]+)/))return e.type.inlineEditable&&n(!0),j(!0),y&&E(!1),void e.onAfterSubmitResponse(!1);A(!1),c(r),y&&(E(!1),v(!0)),h||y||(t=>{const n=t||N(),r=d.default.inject(d.default._t("ElementSaveAction.SUCCESS_NOTIFICATION","Saved '{title}' successfully"),{title:n});e.actions.toasts.success(r)})(r),e.onAfterSubmitResponse(!0),R()},onFormInit:()=>(e=>{B(e),I(!0)})(V)}))));return t?ee:K(ee)};t.Component=I,I.propTypes={element:o.elementType,type:l.elementTypeType.isRequired,areaId:a.default.number.isRequired,link:a.default.string.isRequired,activeTab:a.default.string,tabSetName:a.default.string,onActivateTab:a.default.func,connectDragSource:a.default.func.isRequired,connectDragPreview:a.default.func.isRequired,connectDropTarget:a.default.func.isRequired,isDragging:a.default.bool.isRequired,isOver:a.default.bool.isRequired,onDragOver:a.default.func,onDragEnd:a.default.func,onDragStart:a.default.func,saveElement:a.default.bool.isRequired,onBeforeSubmitForm:a.default.func.isRequired,onAfterSubmitResponse:a.default.func.isRequired,increment:a.default.number.isRequired},I.defaultProps={element:null};const w={drop(e,t,n){const{element:r}=e;return{target:r.id,dropSpot:(0,v.isOverTop)(t,n)?"top":"bottom"}},hover(e,t,n){const{element:r,onDragOver:a}=e;a&&a(r,(0,v.isOverTop)(t,n))}};t.default=(0,i.compose)((0,h.DropTarget)("element",w,((e,t)=>({connectDropTarget:e.dropTarget(),isOver:t.isOver()}))),(0,h.DragSource)("element",v.elementDragSource,((e,t)=>({connectDragSource:e.dragSource(),connectDragPreview:e.dragPreview(),isDragging:t.isDragging()}))),(0,c.connect)((function(e,t){const n=t.element.id,r=(0,p.loadElementFormStateName)(n),a=(0,m.loadElementSchemaValue)("schemaUrl",n),o=e.form&&e.form.formSchemas[a]&&e.form.formSchemas[a].schema&&e.form.formSchemas[a].schema.fields.find((e=>"Tabs"===e.component)),l=o&&o.id,i=`element.${r}__${l}`,s=e.unsavedForms.find((e=>e.name===`element.${r}`));return{tabSetName:l,activeTab:e.tabs&&e.tabs.fields&&e.tabs.fields[i]&&e.tabs.fields[i].activeTab,formDirty:s}}),(function(e,t){const n=(0,p.loadElementFormStateName)(t.element.id);return{onActivateTab(t,r){e(g.activateTab(`element.${n}__${t}`,r))},submitForm(){t.onBeforeSubmitForm(t.element.id),e((0,f.submit)(`element.${n}`))},dispatchAddFormChanged(){e((0,E.addFormChanged)(`element.${n}`))},dispatchRemoveFormChanged(){e((0,E.removeFormChanged)(`element.${n}`))},actions:{toasts:(0,i.bindActionCreators)(y,e)}}})),(0,s.inject)(["ElementHeader","ElementContent"],((e,t)=>({HeaderComponent:e,ContentComponent:t})),(()=>"ElementEditor.ElementList.Element")))(I)},7105: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=f(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=c(n(6935)),o=n(9367),l=n(3556),i=n(5207),s=n(455),d=n(9791),u=c(n(26));function c(e){return e&&e.__esModule?e:{default:e}}function f(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(f=function(e){return e?n:t})(e)}class p extends r.Component{constructor(e){super(e),this.handleEditTabsClick=this.handleEditTabsClick.bind(this)}handleEditTabsClick(e){const{handleEditTabsClick:t}=this.props;t(e.target.name)}renderEditTabs(){const{editTabs:e,activeTab:t,type:n,expandable:a}=this.props;return!n.broken&&a&&e&&e.length?e.map((e=>{let{name:a,title:o}=e;return r.default.createElement(u.default,{key:a,name:a,title:o,type:n,onClick:this.handleEditTabsClick,active:a===t})})):null}renderDivider(){const{children:e,editTabs:t,expandable:n}=this.props;return n&&t&&t.length&&0!==r.default.Children.count(e)?r.default.createElement(l.DropdownItem,{divider:!0,role:"separator"}):null}render(){const{children:e,id:t,ActionMenuComponent:n}=this.props;return r.default.createElement(n,{id:`element-editor-actions-${t}`,className:"element-editor-header__actions-dropdown",dropdownMenuProps:{right:!0},dropdownToggleClassNames:["element-editor-header__actions-toggle","btn","btn-sm","btn--no-text","font-icon-dot-3"]},this.renderEditTabs(),this.renderDivider(),e)}}t.Component=p,p.propTypes={element:s.elementType,type:d.elementTypeType.isRequired,areaId:a.default.number.isRequired,activeTab:a.default.string,editTabs:a.default.arrayOf(a.default.shape({title:a.default.string,name:a.default.string})),handleEditTabsClick:a.default.func.isRequired,expandable:a.default.bool},p.defaultProps={editTabs:[],expandable:!0};t.default=(0,o.compose)((0,i.inject)(["ActionMenu"],(e=>({ActionMenuComponent:e})),(()=>"ElementEditor.ElementList.Element")))(p)},920:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=c(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=u(n(6935)),o=u(n(7651)),l=n(1820),i=n(455),s=n(9791),d=n(1135);function u(e){return e&&e.__esModule?e:{default:e}}function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(c=function(e){return e?n:t})(e)}class f extends r.Component{render(){const{isDragging:e,element:t,elementTypes:n,currentOffset:a}=this.props;if(!e||!a)return null;const{x:l,y:i}=a,s=`translate(${l}px, ${i}px)`,u={transform:s,WebkitTransform:s},c=(0,d.getElementTypeConfig)(t,n);return r.default.createElement("div",{className:"element-editor-drag-preview",style:u},r.default.createElement(o.default,{element:t,type:c,simple:!0}))}}f.propTypes={element:i.elementType,elementTypes:a.default.arrayOf(s.elementTypeType),isDragging:a.default.bool,currentOffset:a.default.shape({x:a.default.number.isRequired,y:a.default.number.isRequired})};t.default=(0,l.DragLayer)((e=>({element:e.getItem(),currentOffset:e.getSourceClientOffset(),isDragging:e.isDragging()})))(f)},6721:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.ElementEditorContext=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=g(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=m(n(6935)),o=n(5207),l=n(9367),i=n(9791),s=n(1820),d=m(n(920)),u=m(n(5799)),c=m(n(5609)),f=m(n(6177)),p=n(1135);function m(e){return e&&e.__esModule?e:{default:e}}function g(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(g=function(e){return e?n:t})(e)}const h=t.ElementEditorContext=(0,r.createContext)(null);class b extends r.PureComponent{constructor(e){super(e),this.state={dragTargetElementId:null,dragSpot:null,elements:null,isLoading:!0},this.handleDragOver=this.handleDragOver.bind(this),this.handleDragEnd=this.handleDragEnd.bind(this),this.fetchElements=this.fetchElements.bind(this)}handleDragOver(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const n=!!e&&e.id;this.setState({dragTargetElementId:n,dragSpot:!1===t?"bottom":"top"})}handleDragEnd(e,t){const n=`${(0,p.getConfig)().controllerLink.replace(/\/$/,"")}/api/sort`;c.default.post(n,{id:e,afterBlockID:t},{"X-SecurityID":f.default.get("SecurityID")}).then((()=>this.fetchElements())),this.setState({dragTargetElementId:null,dragSpot:null})}fetchElements(){(!(arguments.length>0&&void 0!==arguments[0])||arguments[0])&&this.setState((e=>({...e,isLoading:!0})));const e=`${(0,p.getConfig)().controllerLink.replace(/\/$/,"")}/api/readElements/${this.props.areaId}`;return c.default.get(e).then((e=>e.json())).then((e=>{this.setState((t=>({...t,elements:e,isLoading:!1})));const t=window.jQuery(".cms-preview");t&&t.entwine("ss.preview")._loadUrl(t.find("iframe").attr("src"))}))}render(){const{ToolbarComponent:e,ListComponent:t,areaId:n,elementTypes:a,isDraggingOver:o,connectDropTarget:l,allowedElements:i,sharedObject:s,isLoading:u}=this.props,{dragTargetElementId:c,dragSpot:f,elements:p}=this.state;if(null===p)return this.fetchElements(!1),null;const m=i.map((e=>a.find((t=>t.class===e)))),g={fetchElements:this.fetchElements};return l(r.default.createElement("div",{className:"element-editor"},r.default.createElement(h.Provider,{value:g},r.default.createElement(e,{elementTypes:m,areaId:n,onDragOver:this.handleDragOver}),r.default.createElement(t,{allowedElementTypes:m,elementTypes:a,areaId:n,onDragOver:this.handleDragOver,onDragStart:this.handleDragStart,onDragEnd:this.handleDragEnd,dragSpot:f,isDraggingOver:o,dragTargetElementId:c,sharedObject:s,elements:p,isLoading:u}),r.default.createElement(d.default,{elementTypes:a}))))}}t.Component=b,b.propTypes={elementTypes:a.default.arrayOf(i.elementTypeType).isRequired,allowedElements:a.default.arrayOf(a.default.string).isRequired,areaId:a.default.number.isRequired,actions:a.default.shape({handleSortBlock:a.default.func})};const v=[u.default,(0,s.DropTarget)("element",{},((e,t)=>({connectDropTarget:e.dropTarget(),isDraggingOver:t.isOver()}))),(0,o.inject)(["ElementToolbar","ElementList"],((e,t)=>({ToolbarComponent:e,ListComponent:t})),(()=>"ElementEditor"))];t.default=(0,l.compose)(...v)(b)},2512: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=g(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=m(n(6935)),o=n(455),l=n(9791),i=n(9367),s=n(5207),d=m(n(6923)),u=m(n(5815)),c=n(1820),f=n(9306),p=n(1135);function m(e){return e&&e.__esModule?e:{default:e}}function g(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(g=function(e){return e?n:t})(e)}class h extends r.Component{constructor(e){super(e),this.resetState=this.resetState.bind(this),this.handleBeforeSubmitForm=this.handleBeforeSubmitForm.bind(this),this.handleAfterSubmitResponse=this.handleAfterSubmitResponse.bind(this),this.state={saveAllElements:!1,increment:0,hasUnsavedChangesBlockIDs:{},validBlockIDs:{}},this.props.sharedObject.setState=this.setState.bind(this)}componentDidMount(){this.resetState({},!0)}componentDidUpdate(e,t){if(this.props.elements)if(this.props.elements===e.elements){if(this.state.saveAllElements){const e=this.props.elements.map((e=>parseInt(e.id,10))).filter((e=>this.state.hasUnsavedChangesBlockIDs[e]));let n=!0;for(let t=0;tthis.state.validBlockIDs[e])),r={success:n,reason:n?"":"invalid"};this.props.sharedObject.entwineResolve(r),this.resetState(t,n),this.setState({saveAllElements:!1})}}}else this.resetState(t,!1)}resetState(e,t){const n={},r={};(this.props.elements||[]).forEach((a=>{const o=parseInt(a.id,10);t?n[o]=!1:e.hasUnsavedChangesBlockIDs.hasOwnProperty(o)?n[o]=e.hasUnsavedChangesBlockIDs[o]:n[o]=!1,r[o]=null})),this.setState({hasUnsavedChangesBlockIDs:n,validBlockIDs:r})}handleChangeHasUnsavedChanges(e,t){this.setState((n=>({hasUnsavedChangesBlockIDs:{...n.hasUnsavedChangesBlockIDs,[e]:t}})))}handleBeforeSubmitForm(e){this.setState((t=>({validBlockIDs:{...t.validBlockIDs,[e]:null}})))}handleAfterSubmitResponse(e,t){this.setState((n=>({hasUnsavedChangesBlockIDs:{...n.hasUnsavedChangesBlockIDs,[e]:!t},validBlockIDs:{...n.validBlockIDs,[e]:t}})))}getDragIndicatorIndex(){const{dragTargetElementId:e,draggedItem:t,elements:n,dragSpot:r}=this.props;return(0,f.getDragIndicatorIndex)(n.map((e=>e.id)),e,t&&t.id,r)}renderBlocks(){const{ElementComponent:e,HoverBarComponent:t,DragIndicatorComponent:n,elements:a,allowedElementTypes:o,elementTypes:l,areaId:i,onDragEnd:s,onDragOver:d,onDragStart:c,isDraggingOver:f}=this.props;if(0===a.length)return r.default.createElement("div",null,u.default._t("ElementList.ADD_BLOCKS","Add blocks to place your content"));let m=a.map((n=>{const a=this.state.saveAllElements&&this.state.hasUnsavedChangesBlockIDs[n.id]&&null===this.state.validBlockIDs[n.id];return r.default.createElement("div",{key:n.id},r.default.createElement(e,{element:n,areaId:i,type:(0,p.getElementTypeConfig)(n,l),link:n.blockSchema.actions.edit,onDragOver:d,onDragEnd:s,onDragStart:c,saveElement:a,onChangeHasUnsavedChanges:e=>this.handleChangeHasUnsavedChanges(n.id,e),onBeforeSubmitForm:()=>this.handleBeforeSubmitForm(n.id),onAfterSubmitResponse:e=>this.handleAfterSubmitResponse(n.id,e),increment:this.state.increment}),f||r.default.createElement(t,{key:`create-after-${n.id}`,areaId:i,elementId:n.id,elementTypes:o}))}));f||(m=[r.default.createElement(t,{key:0,areaId:i,elementId:0,elementTypes:o})].concat(m));const g=this.getDragIndicatorIndex();return f&&null!==g&&m.splice(g,0,r.default.createElement(n,{key:"DropIndicator"})),m}renderLoading(){const{isLoading:e,LoadingComponent:t}=this.props;return e?r.default.createElement(t,null):null}render(){const{elements:e}=this.props,t=(0,d.default)("elemental-editor-list",{"elemental-editor-list--empty":!e||!e.length});return this.props.connectDropTarget(r.default.createElement("div",{className:t},this.renderLoading(),this.renderBlocks()))}}t.Component=h,h.propTypes={elements:a.default.arrayOf(o.elementType).isRequired,elementTypes:a.default.arrayOf(l.elementTypeType).isRequired,allowedElementTypes:a.default.arrayOf(l.elementTypeType).isRequired,areaId:a.default.number.isRequired,dragTargetElementId:a.default.oneOfType([a.default.string,a.default.bool]),onDragOver:a.default.func,onDragStart:a.default.func,onDragEnd:a.default.func,sharedObject:a.default.object.isRequired},h.defaultProps={sharedObject:{entwineResolve:()=>{},setState:null},elements:[],isLoading:!1};const b={drop(e,t){const{elements:n}=e,r=t.getDropResult();if(!r)return{};const a=(0,f.getDragIndicatorIndex)(n.map((e=>e.id)),r.target,t.getItem(),r.dropSpot),o=n[a-1]?n[a-1].id:"0";return{...r,dropAfterID:o}}};t.default=(0,i.compose)((0,c.DropTarget)("element",b,((e,t)=>({connectDropTarget:e.dropTarget(),draggedItem:t.getItem()}))),(0,s.inject)(["Element","Loading","HoverBar","DragPositionIndicator"],((e,t,n,r)=>({ElementComponent:e,LoadingComponent:t,HoverBarComponent:n,DragIndicatorComponent:r})),(()=>"ElementEditor.ElementList")))(h)},7651: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=h(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=g(n(6935)),o=n(3556),l=n(455),i=n(9791),s=n(9367),d=n(5207),u=g(n(5815)),c=g(n(6923)),f=n(1820),p=n(9306),m=n(8724);function g(e){return e&&e.__esModule?e:{default:e}}function h(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(h=function(e){return e?n:t})(e)}class b extends r.Component{constructor(e){super(e),this.toggle=this.toggle.bind(this),this.state={tooltipOpen:!1}}componentDidMount(){const{connectDragPreview:e}=this.props;e&&e((0,m.getEmptyImage)(),{captureDraggingState:!0})}componentDidUpdate(){const{tooltipOpen:e}=this.state,{disableTooltip:t}=this.props;e&&t&&this.setState({tooltipOpen:!1})}getBlockTitle(e,t){return t.broken?u.default.inject(u.default._t("ElementHeader.BROKEN","This element is of obsolete type {type}."),{type:t.obsoleteClassName}):e.title?e.title:u.default.inject(u.default._t("ElementHeader.NOTITLE","Untitled {type} block"),{type:t.title})}toggle(){this.setState((e=>({tooltipOpen:!e.tooltipOpen})))}renderVersionedStateMessage(){const{element:{isLiveVersion:e,isPublished:t}}=this.props;if(t&&e)return null;let n="";const a=["element-editor-header__version-state"];return t?e||(n=u.default._t("ElementHeader.STATE_MODIFIED","Item has unpublished changes"),a.push("element-editor-header__version-state--modified")):(n=u.default._t("ElementHeader.STATE_DRAFT","Item has not been published yet"),a.push("element-editor-header__version-state--draft")),r.default.createElement("span",{className:(0,c.default)(a),title:n})}renderStatusBadge(){const{element:{isLiveVersion:e,isPublished:t}}=this.props;if(t&&e)return null;let n="",a="";const o=["badge"];return t?e||(n=u.default._t("ElementHeader.BADGE_MODIFIED","Modified"),a=u.default._t("ElementHeader.STATE_MODIFIED","Item has unpublished changes"),o.push("status-modified")):(n=u.default._t("ElementHeader.BADGE_DRAFT","Draft"),a=u.default._t("ElementHeader.STATE_DRAFT","Item has not been published yet"),o.push("status-addedtodraft")),r.default.createElement("span",{className:(0,c.default)(o),title:a},n)}render(){const{connectDragSource:e,element:t,type:n,areaId:a,previewExpanded:l,simple:i,disableTooltip:s,activeTab:d,expandable:f,ElementActionsComponent:p,handleEditTabsClick:m}=this.props,g=this.getBlockTitle(t,n),h=(0,c.default)({"element-editor-header__title":!0,"element-editor-header__title--none":!t.title}),b=u.default._t("ElementHeader.EXPAND","Show editable fields"),v=(0,c.default)("element-editor-header",{"element-editor-header--simple":i}),y=(0,c.default)("element-editor-header__icon-container",{"element-editor-header__icon-container--broken":n.broken}),E=(0,c.default)("element-editor-header__expand",{"font-icon-right-open-big":!f,"font-icon-up-open-big":f&&l,"font-icon-down-open-big":f&&!l}),_=`element-icon-${t.id}`,O=r.default.createElement("div",{className:v},r.default.createElement("div",{className:"element-editor-header__drag-handle"},r.default.createElement("i",{className:"font-icon-drag-handle"})),r.default.createElement("div",{className:"element-editor-header__info"},r.default.createElement("div",{className:y},r.default.createElement("i",{className:n.icon,id:_}),this.renderVersionedStateMessage(),!n.broken&&!i&&r.default.createElement(o.Tooltip,{placement:"top",isOpen:this.state.tooltipOpen&&!s,target:_,toggle:this.toggle},n.title)),r.default.createElement("h3",{className:h},g),this.renderStatusBadge()),!i&&r.default.createElement("div",{className:"element-editor-header__actions"},r.default.createElement("div",{role:"none",onClick:e=>e.stopPropagation()},r.default.createElement(p,{element:t,type:n,areaId:a,activeTab:d,editTabs:n.editTabs,handleEditTabsClick:m,expandable:f})),!n.broken&&r.default.createElement("i",{className:E,title:b})));return l?e(O):O}}t.Component=b,b.propTypes={element:l.elementType.isRequired,type:i.elementTypeType.isRequired,areaId:a.default.number,activeTab:a.default.string,simple:a.default.bool,ElementActionsComponent:a.default.elementType,previewExpanded:a.default.bool,disableTooltip:a.default.bool,connectDragSource:a.default.func.isRequired,connectDragPreview:a.default.func.isRequired,onDragEnd:a.default.func},b.defaultProps={expandable:!0};t.default=(0,s.compose)((0,f.DragSource)("element",p.elementDragSource,(e=>({connectDragSource:e.dragSource(),connectDragPreview:e.dragPreview()}))),(0,d.inject)(["ElementActions"],(e=>({ElementActionsComponent:e})),(()=>"ElementEditor.ElementList.Element")))(b)},8953: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={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=s(n(6935)),o=n(5207),l=n(9791),i=s(n(5815));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 u=(0,s(n(7577)).default)("element-editor__hover-bar");function c(e){let{AddElementPopoverComponent:t,elementTypes:n,elementId:a,areaId:o,popoverOpen:l,onToggle:s}=e;const d=`${u("-line")} font-icon-plus-circled`,c=i.default._t("ElementAddNewButton.ADD_BLOCK","Add block"),f={className:u("-area",{"-area--focus":l}),onClick:s,"aria-label":c,title:c,id:`AddBlockHoverBarArea_${o}_${a}`};return r.default.createElement("div",{className:u(""),id:`AddBlockHoverBar_${o}_${a}`},r.default.createElement("button",f,r.default.createElement("span",{className:u("-area-inner")},r.default.createElement("span",{className:d}))),r.default.createElement(t,{placement:"bottom",target:`AddBlockHoverBarArea_${o}_${a}`,isOpen:l,elementTypes:n,toggle:s,container:`#AddBlockHoverBar_${o}_${a}`,areaId:o,insertAfterElement:a}))}class f extends r.Component{constructor(e){super(e),this.toggle=this.toggle.bind(this),this.state={popoverOpen:!1}}toggle(){this.setState((e=>({popoverOpen:!e.popoverOpen})))}render(){const e={...this.state,...this.props,onToggle:this.toggle};return r.default.createElement(c,e)}}t.Component=f,f.propTypes={elementTypes:a.default.arrayOf(l.elementTypeType).isRequired,elementId:a.default.oneOfType([a.default.string,a.default.number]).isRequired,areaId:a.default.oneOfType([a.default.number,a.default.string]).isRequired};t.default=(0,o.inject)(["AddElementPopover"],(e=>({AddElementPopoverComponent:e})),(()=>"ElementEditor.ElementList.HoverBar"))(f)},7333:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=f(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=c(n(6935)),o=c(n(6923)),l=c(n(55)),i=n(7785),s=c(n(5815)),d=n(2153),u=n(9040);function c(e){return e&&e.__esModule?e:{default:e}}function f(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(f=function(e){return e?n:t})(e)}function p(){return p=Object.assign?Object.assign.bind():function(e){for(var t=1;t{t.match(/PageElements_[0-9]+_Title/)&&(r=e[t])})),n().then((e=>this.props.onFormSchemaSubmitResponse(e,r)))}render(){const{elementId:e,extraClass:t,onClick:n,onFormInit:a,formHasState:s,notVisible:d}=this.props,{loadingError:u}=this.state,c=(0,o.default)("element-editor-editform",t),f={formTag:"form",schemaUrl:(0,i.loadElementSchemaValue)("schemaUrl",e),identifier:"element",refetchSchemaOnMount:!s,onLoadingError:this.handleLoadingError,onSubmit:this.handleSubmit};u&&(f.loading=!1),"function"==typeof a&&(f.onReduxFormInit=a);const m={};return d&&(m["aria-hidden"]="true"),r.default.createElement("div",p({className:c,onClick:n,role:"presentation"},m),r.default.createElement(l.default,f))}}m.propTypes={extraClass:a.default.oneOfType([a.default.string,a.default.object]),onClick:a.default.func,elementId:a.default.string,handleLoadingError:a.default.func,onFormSchemaSubmitResponse:a.default.func,notVisible:a.default.bool};t.default=(0,u.connect)((function(e,t){const n=(0,d.loadElementFormStateName)(t.elementId);return{formHasState:e.form.formState&&e.form.formState.element&&!!e.form.formState.element[n]}}))(m)},7660:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=s(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),a=i(n(6935)),o=i(n(6923)),l=i(n(5815));function i(e){return e&&e.__esModule?e:{default:e}}function s(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(s=function(e){return e?n:t})(e)}class d extends r.PureComponent{render(){const{fileUrl:e,fileTitle:t,content:n,broken:a}=this.props,i=l.default._t("ElementSummary.NO_PREVIEW","No preview available"),s=(0,o.default)("element-editor-summary__content",{"element-editor-summary__content--broken":a});return r.default.createElement("div",{className:"element-editor-summary"},e&&r.default.createElement("img",{className:"element-editor-summary__thumbnail-image",src:e,alt:t}),(n||!e)&&r.default.createElement("p",{className:s},n||i))}}d.defaultProps={},d.propTypes={content:a.default.string,fileUrl:a.default.string,fileTitle:a.default.string,broken:a.default.bool};t.default=d},5563:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,a=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=d(t);if(n&&n.has(e))return n.get(e);var r={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if("default"!==o&&{}.hasOwnProperty.call(e,o)){var l=a?Object.getOwnPropertyDescriptor(e,o):null;l&&(l.get||l.set)?Object.defineProperty(r,o,l):r[o]=e[o]}return r.default=e,n&&n.set(e,r),r}(n(1594)),o=(r=n(6935))&&r.__esModule?r:{default:r},l=n(5207),i=n(9791),s=n(1820);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)}class u extends a.PureComponent{render(){const{AddNewButtonComponent:e,elementTypes:t,areaId:n,connectDropTarget:r}=this.props;return r(a.default.createElement("div",{className:"element-editor__toolbar"},a.default.createElement(e,{elementTypes:t,areaId:n})))}}u.defaultProps={},u.propTypes={elementTypes:o.default.arrayOf(i.elementTypeType).isRequired,areaId:o.default.number.isRequired,AddNewButtonComponent:o.default.elementType.isRequired,connectDropTarget:o.default.func.isRequired,onDragOver:o.default.func,onDragDrop:o.default.func};const c={hover(e){const{onDragOver:t}=e;t&&t()}};t.default=(0,s.DropTarget)("element",c,(e=>({connectDropTarget:e.dropTarget()})))((0,l.inject)(["ElementAddNewButton"],(e=>({AddNewButtonComponent:e})),(()=>"ElementEditor.ElementToolbar"))(u))},2977:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=l(n(1594)),a=l(n(5815)),o=l(n(6923));function l(e){return e&&e.__esModule?e:{default:e}}t.default=e=>class extends e{getClassName(){const e=[super.getClassName()];return this.props.data.ElementID&&e.unshift("elemental-area__element--historic-inner"),(0,o.default)(e)}render(){const e=this.getLegend(),t=this.props.data.tag||"div",n=this.getClassName(),{data:o}=this.props;return o.ElementID?r.default.createElement(t,{className:n},e,r.default.createElement("div",{className:"elemental-preview elemental-preview--historic"},o.ElementEditLink&&r.default.createElement("a",{className:"elemental-preview__link",href:o.ElementEditLink},r.default.createElement("span",{className:"elemental-preview__link-text"},a.default._t("HistoricElementView.VIEW_BLOCK_HISTORY","Block history")),r.default.createElement("i",{className:"font-icon-angle-right btn--icon-lg elemental-preview__link-caret"})),r.default.createElement("div",{className:"elemental-preview__icon"},r.default.createElement("i",{className:o.ElementIcon})),r.default.createElement("div",{className:"elemental-preview__detail"},r.default.createElement("h3",null,o.ElementTitle," ",r.default.createElement("small",null,o.ElementType)))),this.props.children):super.render()}}},6573:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=l(n(1594)),a=n(3556),o=l(n(2623));function l(e){return e&&e.__esModule?e:{default:e}}t.default=(0,o.default)((e=>{const{children:t}=e,n=r.default.Children.toArray(r.default.Children.map(t,((t,n)=>{const a={};return 0===n?(a.id=e.id,a.title=null):1===n&&(a.noHolder=!0),r.default.cloneElement(t,a)})));return e.readOnly?r.default.createElement("div",{className:"text-checkbox-group-field--read-only"},n):1===n.length?n[0]:r.default.createElement(a.InputGroup,{className:"text-checkbox-group-field"},n[0],r.default.createElement(a.InputGroupAddon,{addonType:"append"},r.default.createElement(a.InputGroupText,null,n[1])))}))},51:function(e,t,n){var r=d(n(1669)),a=d(n(1594)),o=n(5145),l=n(5207),i=n(1135),s=n(5381);function d(e){return e&&e.__esModule?e:{default:e}}const u=()=>{setTimeout((()=>{const{store:e}=window.ss;e&&e.dispatch((0,s.destroy)(...Object.keys(e.getState().form.formState.element||{}).map((e=>`element.${e}`))))}),0)};r.default.entwine("ss",(e=>{e(".js-injector-boot .element-editor__container").entwine({ReactRoot:null,AreaIDsSharedObject:{},Increment:0,onmatch(){const e=(0,l.loadComponent)("ElementEditor",{}),t=this.data("schema"),n=(0,i.getConfig)().elementTypes,r=t["elemental-area-id"],s=this.getAreaIDsSharedObject();s.hasOwnProperty(r)||(s[r]={entwineResolve:null,setState:null});const d={areaId:r,allowedElements:t["allowed-elements"],elementTypes:n,sharedObject:s[r]};let u=this.getReactRoot();u||(u=(0,o.createRoot)(this[0]),this.setReactRoot(u)),u.render(a.default.createElement(e,d))},onunmatch(){e(".cms-edit-form").data("hasValidationErrors")||u(),this.unmountComponent()},unmountComponent(){const e=this.getReactRoot();e&&(e.unmount(),this.setReactRoot(null))},"from .cms-edit-form":{onbeforesubmitform(e,t){if(!t)return;let n;const r=new Promise((e=>{n=e}));t.promises.push(r),t.onAjaxSuccessCallbacks.push(this.unmountComponent.bind(this));const a=this.data("schema")["elemental-area-id"],o=this.getAreaIDsSharedObject()[a],l=this.getIncrement()+1;this.setIncrement(l),o.entwineResolve=n,o.setState({saveAllElements:!0,increment:l})},onaftersubmitform(t,n){const r=JSON.parse(n.xhr.responseText).ValidationResult;JSON.parse(r.replace(/<\/?script[^>]*?>/g,"")).isValid?(e(".cms-edit-form").data("hasValidationErrors",!1),u()):e(".cms-edit-form").data("hasValidationErrors",!0)}}}),e(".js-injector-boot .element-editor__container .element-form-dirty-state").entwine({onmatch(){e(".cms-edit-form").trigger("change")},onunmatch(){e(".cms-edit-form").trigger("change")}}),e(".cms-edit-form").entwine({getChangeTrackerOptions(){const t=void 0===this.entwineData("ChangeTrackerOptions");let n=this._super();return t&&(n=e.extend({},n),n.ignoreFieldSelector+=", .elementalarea :input:not(.element-form-dirty-state)",this.setChangeTrackerOptions(n)),n}})}))},9306:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.isOverTop=t.getDragIndicatorIndex=t.elementDragSource=void 0;var r=n(4518);t.isOverTop=(e,t)=>{const n=e.getClientOffset(),a=(0,r.findDOMNode)(t).getBoundingClientRect();return n.y{if(null===t||!n)return null;let a=t?e.findIndex((e=>e===t)):0;const o=e.findIndex((e=>e===n));return"bottom"===r&&(a+=1),o===a||o+1===a?null:a};t.elementDragSource={beginDrag(e){return e.element},endDrag(e,t){const{onDragEnd:n}=e,r=t.getDropResult();if(!n||!r||!r.dropAfterID)return;const a=t.getItem().id,{dropAfterID:o}=r;a!==o&&n(a,o)}}},7577:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,a=(r=n(6923))&&r.__esModule?r:{default:r};t.default=e=>function(){const t=t=>`${e}${t}`;for(var n=arguments.length,r=new Array(n),o=0;o!(!e&&""!==e)&&("object"==typeof e?Array.isArray(e)?e.map(t):Object.entries(e).reduce(((e,n)=>{let[r,a]=n;return Object.assign({},e,{[t(r)]:a})}),{}):t(e))));return(0,a.default)(...l)}},1135:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.getElementTypeConfig=t.getConfig=void 0;var r,a=(r=n(6177))&&r.__esModule?r:{default:r};const o=()=>a.default.getSection("DNADesign\\Elemental\\Controllers\\ElementalAreaController");t.getConfig=o;t.getElementTypeConfig=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const n=e.blockSchema.typeName;let r=(Array.isArray(t)?t:o().elementTypes).find((e=>e.class===n||e.name===n));return e.obsoleteClassName&&(r=Object.assign({obsoleteClassName:e.obsoleteClassName},r),Object.preventExtensions(r)),r}},2153:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.loadElementFormStateName=void 0;var r,a=(r=n(6177))&&r.__esModule?r:{default:r};t.loadElementFormStateName=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;const t=a.default.getSection("DNADesign\\Elemental\\Controllers\\ElementalAreaController").form.elementForm.formNameTemplate;return e?t.replace("{id}",e):t}},7785:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.loadElementSchemaValue=void 0;var r,a=(r=n(6177))&&r.__esModule?r:{default:r};t.loadElementSchemaValue=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const n=a.default.getSection("DNADesign\\Elemental\\Controllers\\ElementalAreaController").form.elementForm[e]||"";return t?`${n}/${t}`:n}},3337:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=i(n(1594)),a=i(n(5609)),o=n(1135),l=i(n(6177));function i(e){return e&&e.__esModule?e:{default:e}}t.default=e=>t=>{const n={...t};return n.hasOwnProperty("actions")||(n.actions={}),n.actions.revertToVersion=(e,t,n,r)=>{const i=`${(0,o.getConfig)().controllerLink.replace(/\/$/,"")}/api/revert`;return a.default.post(i,{id:e,fromVersion:t,fromStage:n,toStage:r},{"X-SecurityID":l.default.get("SecurityID")})},r.default.createElement(e,n)}},455:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.elementType=void 0;var r,a=(r=n(6935))&&r.__esModule?r:{default:r};t.elementType=a.default.shape({id:a.default.string.isRequired,title:a.default.string,blockSchema:a.default.object,inlineEditable:a.default.bool,published:a.default.bool,liveVersion:a.default.bool,version:a.default.number})},9791:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.elementTypeType=void 0;var r,a=(r=n(6935))&&r.__esModule?r:{default:r};t.elementTypeType=a.default.shape({name:a.default.string,title:a.default.string,icon:a.default.string,inlineEditable:a.default.bool,editTabs:a.default.arrayOf(a.default.shape({title:a.default.string,name:a.default.string})),config:a.default.object})},5609:function(e){e.exports=Backend},6177:function(e){e.exports=Config},2623:function(e){e.exports=FieldHolder},55:function(e){e.exports=FormBuilderLoader},5207:function(e){e.exports=Injector},6935:function(e){e.exports=PropTypes},1594:function(e){e.exports=React},1820:function(e){e.exports=ReactDND},8724:function(e){e.exports=ReactDNDHtml5Backend},4518:function(e){e.exports=ReactDom},5145:function(e){e.exports=ReactDomClient},9040:function(e){e.exports=ReactRedux},3556:function(e){e.exports=Reactstrap},9367:function(e){e.exports=Redux},5381:function(e){e.exports=ReduxForm},5034:function(e){e.exports=TabsActions},8918:function(e){e.exports=ToastsActions},9077:function(e){e.exports=UnsavedFormsActions},6923:function(e){e.exports=classnames},5815:function(e){e.exports=i18n},1669:function(e){e.exports=jQuery},5799:function(e){e.exports=withDragDropContext}},t={};function n(r){var a=t[r];if(void 0!==a)return a.exports;var o=t[r]={exports:{}};return e[r](o,o.exports,n),o.exports}n(51),n(2038)}(); \ No newline at end of file diff --git a/client/src/boot/registerTransforms.js b/client/src/boot/registerTransforms.js index 75cfd145..8fd98f43 100644 --- a/client/src/boot/registerTransforms.js +++ b/client/src/boot/registerTransforms.js @@ -1,9 +1,6 @@ import Injector from 'lib/Injector'; -import readOneBlockQuery from 'state/history/readOneBlockQuery'; import HistoricElementViewFactory from 'components/HistoricElementView/HistoricElementView'; -import revertToBlockVersionMutation from 'state/history/revertToBlockVersionMutation'; -import readBlocksForAreaQuery from 'state/editor/readBlocksForAreaQuery'; -import addElementToArea from 'state/editor/addElementMutation'; +import revertToBlockVersionRequest from 'state/history/revertToBlockVersionRequest'; import ArchiveAction from 'components/ElementActions/ArchiveAction'; import DuplicateAction from 'components/ElementActions/DuplicateAction'; import SaveAction from 'components/ElementActions/SaveAction'; @@ -25,50 +22,14 @@ export default () => { } ); - Injector.transform( - 'elements-history', - (updater) => { - // Add content block history to the HistoryViewer - updater.component( - 'HistoryViewer.Form_ItemEditForm', - readOneBlockQuery, - 'ElementHistoryViewer' - ); - } - ); - Injector.transform( 'blocks-history-revert', (updater) => { - // Add block element revert GraphQL mutation to the HistoryViewerToolbar + // Add revertToVersion() to props.actions on HistoryViewerToolbar updater.component( 'HistoryViewerToolbar.VersionedAdmin.HistoryViewer.Element.HistoryViewerVersionDetail', - revertToBlockVersionMutation, - 'BlockRevertMutation' - ); - } - ); - - Injector.transform( - 'cms-element-editor', - (updater) => { - // Add GraphQL query for reading elements on a page for the ElementEditor - updater.component( - 'ElementList', - readBlocksForAreaQuery, - 'PageElements' - ); - } - ); - - Injector.transform( - 'cms-element-adder', - (updater) => { - // Add GraphQL query for adding elements to an ElementEditor (ElementalArea) - updater.component( - 'AddElementPopover', - addElementToArea, - 'ElementAddButton' + revertToBlockVersionRequest, + 'BlockRevertRequest' ); } ); diff --git a/client/src/components/ElementActions/ArchiveAction.js b/client/src/components/ElementActions/ArchiveAction.js index 101198a7..9b991f61 100644 --- a/client/src/components/ElementActions/ArchiveAction.js +++ b/client/src/components/ElementActions/ArchiveAction.js @@ -1,40 +1,43 @@ /* global window */ -import React from 'react'; -import { compose } from 'redux'; +import React, { useContext } from 'react'; import AbstractAction from 'components/ElementActions/AbstractAction'; -import archiveBlockMutation from 'state/editor/archiveBlockMutation'; import i18n from 'i18n'; +import { ElementEditorContext } from 'components/ElementEditor/ElementEditor'; +import backend from 'lib/Backend'; +import Config from 'lib/Config'; +import { getConfig } from 'state/editor/elementConfig'; /** * Adds the elemental menu action to archive a block of any state */ const ArchiveAction = (MenuComponent) => (props) => { + const { fetchElements } = useContext(ElementEditorContext); + const handleClick = (event) => { event.stopPropagation(); - - const { element: { id }, isPublished, actions: { handleArchiveBlock } } = props; - + const isPublished = props.element.isPublished; let archiveMessage = i18n._t( 'ElementArchiveAction.CONFIRM_DELETE', 'Are you sure you want to send this block to the archive?' ); - if (isPublished) { archiveMessage = i18n._t( 'ElementArchiveAction.CONFIRM_DELETE_AND_UNPUBLISH', 'Warning: This block will be unpublished before being sent to the archive. Are you sure you want to proceed?' ); } - // eslint-disable-next-line no-alert - if (handleArchiveBlock && window.confirm(archiveMessage)) { - handleArchiveBlock(id).then(() => { - const preview = window.jQuery('.cms-preview'); - if (preview && typeof preview.entwine === 'function') { - preview.entwine('ss.preview')._loadUrl(preview.find('iframe').attr('src')); - } - }); + if (!window.confirm(archiveMessage)) { + return; } + const id = props.element.id; + const url = `${getConfig().controllerLink.replace(/\/$/, '')}/api/delete`; + backend.post(url, { + id + }, { + 'X-SecurityID': Config.get('SecurityID') + }) + .then(() => fetchElements()); }; const disabled = props.element.canDelete !== undefined && !props.element.canDelete; @@ -61,4 +64,4 @@ const ArchiveAction = (MenuComponent) => (props) => { export { ArchiveAction as Component }; -export default compose(archiveBlockMutation, ArchiveAction); +export default ArchiveAction; diff --git a/client/src/components/ElementActions/DuplicateAction.js b/client/src/components/ElementActions/DuplicateAction.js index f1996aab..518fbce5 100644 --- a/client/src/components/ElementActions/DuplicateAction.js +++ b/client/src/components/ElementActions/DuplicateAction.js @@ -1,14 +1,18 @@ /* global window */ -import React from 'react'; -import { compose } from 'redux'; +import React, { useContext } from 'react'; import AbstractAction from 'components/ElementActions/AbstractAction'; -import duplicateBlockMutation from 'state/editor/duplicateBlockMutation'; import i18n from 'i18n'; +import { ElementEditorContext } from 'components/ElementEditor/ElementEditor'; +import backend from 'lib/Backend'; +import Config from 'lib/Config'; +import { getConfig } from 'state/editor/elementConfig'; /** * Adds the elemental menu action to duplicate a block */ const DuplicateAction = (MenuComponent) => (props) => { + const { fetchElements } = useContext(ElementEditorContext); + if (props.type.broken) { // Don't allow this action for a broken element. return ( @@ -18,15 +22,14 @@ const DuplicateAction = (MenuComponent) => (props) => { const handleClick = (event) => { event.stopPropagation(); - - const { element: { id }, actions: { handleDuplicateBlock } } = props; - - if (handleDuplicateBlock) { - handleDuplicateBlock(id).then(() => { - const preview = window.jQuery('.cms-preview'); - preview.entwine('ss.preview')._loadUrl(preview.find('iframe').attr('src')); - }); - } + const id = props.element.id; + const url = `${getConfig().controllerLink.replace(/\/$/, '')}/api/duplicate`; + backend.post(url, { + id, + }, { + 'X-SecurityID': Config.get('SecurityID') + }) + .then(() => fetchElements()); }; const disabled = props.element.canCreate !== undefined && !props.element.canCreate; @@ -53,4 +56,4 @@ const DuplicateAction = (MenuComponent) => (props) => { export { DuplicateAction as Component }; -export default compose(duplicateBlockMutation, DuplicateAction); +export default DuplicateAction; diff --git a/client/src/components/ElementActions/UnpublishAction.js b/client/src/components/ElementActions/UnpublishAction.js index a8bc506d..87f1e1d4 100644 --- a/client/src/components/ElementActions/UnpublishAction.js +++ b/client/src/components/ElementActions/UnpublishAction.js @@ -1,14 +1,18 @@ /* global window */ -import React from 'react'; -import { compose } from 'redux'; +import React, { useContext } from 'react'; import AbstractAction from 'components/ElementActions/AbstractAction'; -import unpublishBlockMutation from 'state/editor/unpublishBlockMutation'; import i18n from 'i18n'; +import backend from 'lib/Backend'; +import { ElementEditorContext } from 'components/ElementEditor/ElementEditor'; +import Config from 'lib/Config'; +import { getConfig } from 'state/editor/elementConfig'; /** * Adds the elemental menu action to unpublish a published block */ const UnpublishAction = (MenuComponent) => (props) => { + const { fetchElements } = useContext(ElementEditorContext); + if (props.type.broken) { // Don't allow this action for a broken element. return ( @@ -16,51 +20,44 @@ const UnpublishAction = (MenuComponent) => (props) => { ); } - const { element, type, actions: { handleUnpublishBlock } } = props; - - const handleClick = (event) => { - event.stopPropagation(); - const { jQuery: $ } = window; + const reportUnpublicationStatus = (type, title, success) => { const noTitle = i18n.inject( - i18n._t( - 'ElementHeader.NOTITLE', - 'Untitled {type} block' - ), - { type: type.title } + i18n._t('ElementHeader.NOTITLE', 'Untitled {type} block'), + { type } + ); + const successMessage = i18n.inject( + i18n._t('ElementUnpublishAction.SUCCESS_NOTIFICATION', 'Removed \'{title}\' from the published page'), + { title: title || noTitle } + ); + const errorMessage = i18n.inject( + i18n._t('ElementUnpublishAction.ERROR_NOTIFICATION', 'Error unpublishing \'{title}\''), + { title: title || noTitle } ); + window.jQuery.noticeAdd({ + text: success ? successMessage : errorMessage, + stay: false, + type: success ? 'success' : 'error', + }); + }; + + const unpublishElement = () => { + const id = props.element.id; + const url = `${getConfig().controllerLink.replace(/\/$/, '')}/api/unpublish`; + return backend.post(url, { + id, + }, { + 'X-SecurityID': Config.get('SecurityID') + }) + .then(() => fetchElements()); + }; - if (handleUnpublishBlock) { - handleUnpublishBlock(element.id) - .then(() => { - const preview = $('.cms-preview'); - preview.entwine('ss.preview')._loadUrl(preview.find('iframe').attr('src')); + const { element, type } = props; - $.noticeAdd({ - text: i18n.inject( - i18n._t( - 'ElementUnpublishAction.SUCCESS_NOTIFICATION', - 'Removed \'{title}\' from the published page' - ), - { title: element.title || noTitle } - ), - stay: false, - type: 'success' - }); - }) - .catch(() => { - $.noticeAdd({ - text: i18n.inject( - i18n._t( - 'ElementUnpublishAction.ERROR_NOTIFICATION', - 'Error unpublishing \'{title}\'' - ), - { title: element.title || noTitle } - ), - stay: false, - type: 'error' - }); - }); - } + const handleClick = (event) => { + event.stopPropagation(); + unpublishElement() + .then(() => reportUnpublicationStatus(type.title, element.title, true)) + .catch(() => reportUnpublicationStatus(type.title, element.title, false)); }; const disabled = props.element.canUnpublish !== undefined && !props.element.canUnpublish; @@ -87,4 +84,4 @@ const UnpublishAction = (MenuComponent) => (props) => { export { UnpublishAction as Component }; -export default compose(unpublishBlockMutation, UnpublishAction); +export default UnpublishAction; diff --git a/client/src/components/ElementActions/tests/ArchiveAction-test.js b/client/src/components/ElementActions/tests/ArchiveAction-test.js index bd21978d..44336f0c 100644 --- a/client/src/components/ElementActions/tests/ArchiveAction-test.js +++ b/client/src/components/ElementActions/tests/ArchiveAction-test.js @@ -4,6 +4,26 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import { Component as ArchiveAction } from '../ArchiveAction'; +import { ElementEditorContext } from '../../ElementEditor/ElementEditor'; + +let resolveMockPostRequest; + +jest.mock('lib/Backend', () => ({ + post: () => new Promise((resolve) => { + resolveMockPostRequest = resolve; + }) +})); + +const sectionConfigKey = 'DNADesign\\Elemental\\Controllers\\ElementalAreaController'; +window.ss.config = { + SecurityID: 1234567890, + sections: [ + { + name: sectionConfigKey, + controllerLink: 'my/test/endpoint', + }, + ], +}; const WrappedComponent = (props) =>
{props.children}
; const ActionComponent = ArchiveAction(WrappedComponent); @@ -16,9 +36,7 @@ function makeProps(obj = {}) { element: { id: 123, isPublished: true, - blockSchema: { type: 'Test' } }, - isPublished: true, actions: { handleArchiveBlock: () => {} }, @@ -27,44 +45,70 @@ function makeProps(obj = {}) { }; } +function makeProviderProps(obj = {}) { + return { + value: { + fetchElements: () => {}, + ...obj, + } + }; +} + test('ArchiveAction renders the title and class', () => { - const { container } = render(); + const { container } = render( + + + + ); expect(container.querySelector('button.element-editor__actions-archive').textContent).toBe('Archive'); }); -test('ArchiveAction does not archive when declining the confirmation', () => { +test('ArchiveAction does not archive when declining the confirmation', async () => { + resolveMockPostRequest = 'lorem ipsum'; global.confirm = () => false; - const mockMutation = jest.fn(() => new Promise((resolve) => { resolve(); })); - const { container } = render(); + const { container } = render( + + + + ); fireEvent.click(container.querySelector('button.element-editor__actions-archive')); - expect(mockMutation).not.toHaveBeenCalled(); + // mock backend post method was never called, therefore resolveMockPostRequest value unchanged + expect(resolveMockPostRequest).toBe('lorem ipsum'); }); -test('ArchiveAction archives when accepting the confirmation', () => { +test('ArchiveAction archives when accepting the confirmation', async () => { global.confirm = () => true; - const mockMutation = jest.fn(() => new Promise((resolve) => { resolve(); })); - const { container } = render(); + let resolvePromise; + const myPromise = new Promise((resolve) => { + resolvePromise = resolve; + }); + const fetchElements = jest.fn(() => { + resolvePromise(); + }); + const { container } = render( + + + + ); fireEvent.click(container.querySelector('button.element-editor__actions-archive')); - expect(mockMutation).toHaveBeenCalled(); + resolveMockPostRequest(); + await Promise.all([myPromise]); + expect(fetchElements).toHaveBeenCalled(); }); test('ArchiveAction indicates that the block will be sent to archive when it is unpublished', () => { const mockConfirm = jest.fn(); global.confirm = mockConfirm; - const { container } = render(); + const { container } = render( + + + + ); fireEvent.click(container.querySelector('button.element-editor__actions-archive')); expect(mockConfirm).toHaveBeenCalledWith('Are you sure you want to send this block to the archive?'); }); @@ -72,30 +116,44 @@ test('ArchiveAction indicates that the block will be sent to archive when it is test('ArchiveAction indicates that the block will be sent to archive when it is published', () => { const mockConfirm = jest.fn(); global.confirm = mockConfirm; - const { container } = render(); + const { container } = render( + + + + ); fireEvent.click(container.querySelector('button.element-editor__actions-archive')); expect(mockConfirm).toHaveBeenCalledWith('Warning: This block will be unpublished before being sent to the archive. Are you sure you want to proceed?'); }); test('ArchiveAction is disabled when user doesn\'t have correct permissions', () => { - const { container } = render(); + const { container } = render( + + + + ); expect(container.querySelector('button.element-editor__actions-archive').disabled).toBe(true); }); test('ArchiveAction renders a button even when block is broken', () => { - const { container } = render(); + const { container } = render( + + + + ); expect(container.querySelectorAll('button.element-editor__actions-archive')).toHaveLength(1); }); diff --git a/client/src/components/ElementActions/tests/DuplicateAction-test.js b/client/src/components/ElementActions/tests/DuplicateAction-test.js index 4ec0f761..56b75960 100644 --- a/client/src/components/ElementActions/tests/DuplicateAction-test.js +++ b/client/src/components/ElementActions/tests/DuplicateAction-test.js @@ -4,6 +4,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { Component as DuplicateAction } from '../DuplicateAction'; +import { ElementEditorContext } from '../../ElementEditor/ElementEditor'; const WrappedComponent = (props) =>
{props.children}
; const ActionComponent = DuplicateAction(WrappedComponent); @@ -27,30 +28,49 @@ function makeProps(obj = {}) { }; } +function makeProviderProps(obj = {}) { + return { + value: { + fetchElements: () => {}, + ...obj, + } + }; +} + test('DuplicateAction renders a button', () => { - const { container } = render(); + const { container } = render( + + + + ); expect(container.querySelectorAll('button.element-editor__actions-duplicate')).toHaveLength(1); }); test('DuplicateAction is disabled when user doesn\'t have correct permissions', () => { const { container } = render( - + + + ); expect(container.querySelector('button.element-editor__actions-duplicate').disabled).toBe(true); }); test('DuplicateAction does not render a button when block is broken', () => { const { container } = render( - + + + ); expect(container.querySelectorAll('button.element-editor__actions-duplicate')).toHaveLength(0); }); diff --git a/client/src/components/ElementActions/tests/PublishAction-test.js b/client/src/components/ElementActions/tests/PublishAction-test.js index 47b0b3bd..1bf0fa4e 100644 --- a/client/src/components/ElementActions/tests/PublishAction-test.js +++ b/client/src/components/ElementActions/tests/PublishAction-test.js @@ -3,6 +3,7 @@ import React from 'react'; import { fireEvent, render } from '@testing-library/react'; import { Component as PublishAction } from '../PublishAction'; +import { ElementEditorContext } from '../../ElementEditor/ElementEditor'; import { ElementContext } from '../../ElementEditor/Element'; window.jQuery = { @@ -28,61 +29,85 @@ function makeProps(obj = {}) { }; } -function makeProviderValue(obj = {}) { +function makeElementEditorProviderProps(obj = {}) { return { - doPublishElement: false, - formHasRendered: false, - onAfterPublish: () => {}, - onPublishButtonClick: () => {}, - ...obj, + value: { + fetchElements: () => {}, + ...obj, + } + }; +} + +function makeElementProviderProps(obj = {}) { + return { + value: { + doPublishElement: false, + formHasRendered: false, + onAfterPublish: () => {}, + onPublishButtonClick: () => {}, + ...obj, + } }; } const WrappedComponent = (props) =>
{props.children}
; const ActionComponent = PublishAction(WrappedComponent); -const ProvidedActionComponent = (props) => ( - - - -); test('PublishAction renders the title and class', () => { - const { container } = render(); + const { container } = render( + + + + + + ); expect(container.querySelector('button.element-editor__actions-publish').textContent).toBe('Publish'); }); test('PublishAction returns null when is the live version', () => { const { container } = render( - + + + + + ); expect(container.querySelector('button.element-editor__actions-publish')).toBe(null); }); test('PublishAction is disabled when user doesn\'t have correct permissions', () => { const { container } = render( - + + + + + ); expect(container.querySelector('button.element-editor__actions-publish').disabled).toBe(true); }); test('PublishAction does not render a button when block is broken', () => { const { container } = render( - + + + + + ); expect(container.querySelectorAll('button.element-editor__actions-publish')).toHaveLength(0); }); @@ -90,12 +115,14 @@ test('PublishAction does not render a button when block is broken', () => { test('Clicking button calls onPublishButtonClick', () => { const onPublishButtonClick = jest.fn(); const { container } = render( - - - + + + + + ); fireEvent.click(container.querySelector('button.element-editor__actions-publish')); expect(onPublishButtonClick).toHaveBeenCalled(); diff --git a/client/src/components/ElementActions/tests/SaveAction-test.js b/client/src/components/ElementActions/tests/SaveAction-test.js index 528c0b73..89257293 100644 --- a/client/src/components/ElementActions/tests/SaveAction-test.js +++ b/client/src/components/ElementActions/tests/SaveAction-test.js @@ -4,6 +4,7 @@ import React from 'react'; import { fireEvent, render } from '@testing-library/react'; import { Component as SaveAction } from '../SaveAction'; +import { ElementEditorContext } from '../../ElementEditor/ElementEditor'; import { ElementContext } from '../../ElementEditor/Element'; function makeProps(obj = {}) { @@ -26,61 +27,83 @@ function makeProps(obj = {}) { }; } -function makeProviderValue(obj = {}) { +function makeElementEditorProviderProps(obj = {}) { return { - formHasRendered: false, - onAfterSave: () => {}, - doSaveElement: false, - onSaveButtonClick: () => {}, - submitForm: () => {}, - formDirty: true, - ...obj, + value: { + fetchElements: () => {}, + ...obj, + } + }; +} + +function makeElementProviderProps(obj = {}) { + return { + value: { + formHasRendered: false, + onAfterSave: () => {}, + doSaveElement: false, + onSaveButtonClick: () => {}, + submitForm: () => {}, + formDirty: true, + ...obj, + } }; } const WrappedComponent = (props) =>
{props.children}
; const ActionComponent = SaveAction(WrappedComponent); -const ProvidedActionComponent = (props) => ( - - - -); + test('SaveAction does not render a button when block is expandable and not formDirty', () => { const { container } = render( - - - + + + + + ); expect(container.querySelectorAll('button.element-editor__actions-save')).toHaveLength(0); }); + test('SaveAction renders a button when block is expandable and formDirty', () => { const { container } = render( - + + + + + ); expect(container.querySelectorAll('button.element-editor__actions-save')).toHaveLength(1); }); test('SaveAction does not render a button when block is not expandable', () => { const { container } = render( - + + + + + ); expect(container.querySelectorAll('button.element-editor__actions-save')).toHaveLength(0); }); test('SaveAction does not render a button when block is broken', () => { const { container } = render( - + + + + + ); expect(container.querySelectorAll('button.element-editor__actions-save')).toHaveLength(0); }); @@ -88,12 +111,14 @@ test('SaveAction does not render a button when block is broken', () => { test('Clicking button calls onSaveButtonClick', () => { const onSaveButtonClick = jest.fn(); const { container } = render( - - - + + + + + ); fireEvent.click(container.querySelector('button.element-editor__actions-save')); expect(onSaveButtonClick).toHaveBeenCalled(); diff --git a/client/src/components/ElementActions/tests/UnpublishAction-test.js b/client/src/components/ElementActions/tests/UnpublishAction-test.js index edd3dcac..49a95576 100644 --- a/client/src/components/ElementActions/tests/UnpublishAction-test.js +++ b/client/src/components/ElementActions/tests/UnpublishAction-test.js @@ -4,11 +4,31 @@ import React from 'react'; import { fireEvent, render } from '@testing-library/react'; import { Component as UnpublishAction } from '../UnpublishAction'; +import { ElementEditorContext } from '../../ElementEditor/ElementEditor'; window.jQuery = { noticeAdd: jest.fn() }; +let resolveMockPostRequest; + +jest.mock('lib/Backend', () => ({ + post: () => new Promise((resolve) => { + resolveMockPostRequest = resolve; + }) +})); + +const sectionConfigKey = 'DNADesign\\Elemental\\Controllers\\ElementalAreaController'; +window.ss.config = { + SecurityID: 1234567890, + sections: [ + { + name: sectionConfigKey, + controllerLink: 'my/test/endpoint', + }, + ], +}; + function makeProps(obj = {}) { return { title: 'My unpublish action', @@ -26,63 +46,85 @@ function makeProps(obj = {}) { }; } +function makeProviderProps(obj = {}) { + return { + value: { + fetchElements: () => {}, + ...obj, + } + }; +} + const WrappedComponent = (props) =>
{props.children}
; const ActionComponent = UnpublishAction(WrappedComponent); test('UnpublishAction renders the title and class', () => { const { container } = render( - + + + ); expect(container.querySelector('button.element-editor__actions-unpublish').textContent).toBe('Unpublish'); }); test('UnpublishAction returns null when is not published', () => { const { container } = render( - + + + ); expect(container.querySelectorAll('button')).toHaveLength(0); }); -test('UnpublishAction calls the unpublish mutation', () => { - const mockMutation = jest.fn(() => new Promise((resolve) => { resolve(); })); +test('UnpublishAction calls unpublish endpoint and fetchElements', async () => { + let resolvePromise; + const myPromise = new Promise((resolve) => { + resolvePromise = resolve; + }); + const fetchElements = jest.fn(() => { + resolvePromise(); + }); const { container } = render( - + + + ); fireEvent.click(container.querySelector('button.element-editor__actions-unpublish')); - expect(mockMutation).toHaveBeenCalledWith(123); + resolveMockPostRequest(); + await Promise.all([myPromise]); + expect(fetchElements).toHaveBeenCalled(); }); test('UnpublishAction is disabled when user doesn\'t have correct permissions', () => { const { container } = render( - + + + ); expect(container.querySelector('button.element-editor__actions-unpublish').disabled).toBe(true); }); test('UnpublishAction does not render a button when block is broken', () => { const { container } = render( - + + + ); expect(container.querySelectorAll('button.element-editor__actions-unpublish')).toHaveLength(0); }); diff --git a/client/src/components/ElementEditor/AddElementPopover.js b/client/src/components/ElementEditor/AddElementPopover.js index cb6cb312..931315d0 100644 --- a/client/src/components/ElementEditor/AddElementPopover.js +++ b/client/src/components/ElementEditor/AddElementPopover.js @@ -6,6 +6,9 @@ import classNames from 'classnames'; import { inject } from 'lib/Injector'; import { elementTypeType } from 'types/elementTypeType'; import i18n from 'i18n'; +import backend from 'lib/Backend'; +import Config from 'lib/Config'; +import { ElementEditorContext } from 'components/ElementEditor/ElementEditor'; /** * The AddElementPopover component used in the context of an ElementEditor shows the @@ -16,27 +19,32 @@ class AddElementPopover extends Component { super(props); this.handleToggle = this.handleToggle.bind(this); + AddElementPopover.contextType = ElementEditorContext; } /** - * click handler that preserves the details of what was clicked - * @param {object} elementType in the shape of types/elmementTypeType - * @returns {function} + * - Call add element to area endpoint (areaID, elementType, insertAfterElementID) + * - Then call read blocks from area endpoint (areaID) + * - Then update the preview via jquery/entwine */ getElementButtonClickHandler(elementType) { return (event) => { - const { - actions: { handleAddElementToArea }, - insertAfterElement - } = this.props; - event.preventDefault(); - handleAddElementToArea(elementType.class, insertAfterElement).then( - () => { + backend.post('/admin/elemental-area/api/create/', { + elementClass: elementType.class, + elementalAreaID: this.props.areaId, + insertAfterElementID: this.props.insertAfterElement, + }, { + 'X-SecurityID': Config.get('SecurityID') + }) + .then(() => { + const { fetchElements } = this.context; + return fetchElements(); + }) + .then(() => { const preview = window.jQuery('.cms-preview'); preview.entwine('ss.preview')._loadUrl(preview.find('iframe').attr('src')); - } - ); + }); this.handleToggle(); }; } diff --git a/client/src/components/ElementEditor/Element.js b/client/src/components/ElementEditor/Element.js index ee7f5f33..b0db95eb 100644 --- a/client/src/components/ElementEditor/Element.js +++ b/client/src/components/ElementEditor/Element.js @@ -1,7 +1,6 @@ /* global window */ -import React, { useState, useEffect, createContext } from 'react'; -import { useMutation } from '@apollo/client'; +import React, { useState, useEffect, useContext, createContext } from 'react'; import PropTypes from 'prop-types'; import { elementType } from 'types/elementType'; import { elementTypeType } from 'types/elementTypeType'; @@ -13,14 +12,16 @@ import { connect } from 'react-redux'; import { submit } from 'redux-form'; import { loadElementFormStateName } from 'state/editor/loadElementFormStateName'; import { loadElementSchemaValue } from 'state/editor/loadElementSchemaValue'; -import { publishBlockMutation } from 'state/editor/publishBlockMutation'; -import { query as readBlocksForAreaQuery } from 'state/editor/readBlocksForAreaQuery'; import * as TabsActions from 'state/tabs/TabsActions'; import { DragSource, DropTarget } from 'react-dnd'; import { getEmptyImage } from 'react-dnd-html5-backend'; import { elementDragSource, isOverTop } from 'lib/dragHelpers'; import * as toastsActions from 'state/toasts/ToastsActions'; import { addFormChanged, removeFormChanged } from 'state/unsavedForms/UnsavedFormsActions'; +import { ElementEditorContext } from 'components/ElementEditor/ElementEditor'; +import { getConfig } from 'state/editor/elementConfig'; +import backend from 'lib/Backend'; +import Config from 'lib/Config'; export const ElementContext = createContext(null); @@ -41,7 +42,7 @@ const Element = (props) => { const [formHasRendered, setFormHasRendered] = useState(false); const [doDispatchAddFormChanged, setDoDispatchAddFormChanged] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [publishBlock] = useMutation(publishBlockMutation); + const { fetchElements } = useContext(ElementEditorContext); useEffect(() => { // Note that formDirty from redux can be set to undefined after failed validation @@ -128,29 +129,20 @@ const Element = (props) => { } }; - // This will trigger a graphql request that will cause this - // element to re-render including any updated title and versioned badge - const refetchElementalArea = () => window.ss.apolloClient.queryManager.refetchQueries({ - include: [{ - query: readBlocksForAreaQuery, - variables: { id: props.areaId } - }] - }); - const handleAfterPublish = (wasError) => { showPublishedElementToast(wasError); setDoPublishElement(false); setDoPublishElementAfterSave(false); - // Ensure that formDirty becomes falsey after publishing - // We need to call at a later render rather than straight away or redux-form may override this - // and set the form state to dirty under certain conditions - // setTimeout is a hackish way to do this, though I'm not sure how else we can do this - // The core issue is that redux-form will detect changes when a form is hydrated for the first - // time under certain conditions, specifically during a behat test when trying to publish a closed - // block when presumably the apollo cache is empty (or something like that). This happens late and - // there are no hooks/callbacks available after this happens the input onchange handlers are fired - Promise.all(refetchElementalArea()) + fetchElements() .then(() => { + // Ensure that formDirty becomes falsey after publishing + // We need to call at a later render rather than straight away or redux-form may override this + // and set the form state to dirty under certain conditions + // setTimeout is a hackish way to do this, though I'm not sure how else we can do this + // The core issue is that redux-form will detect changes when a form is hydrated for the first + // time under certain conditions, specifically during a behat test when trying to publish a closed + // block when presumably the old apollo cache was empty (or something like that). This happens late and + // there are no hooks/callbacks available after this happens the input onchange handlers are fired setTimeout(() => props.dispatchRemoveFormChanged(), 250); }); }; @@ -166,7 +158,12 @@ const Element = (props) => { // Publish action useEffect(() => { if (formHasRendered && doPublishElement) { - publishBlock({ variables: { blockId: props.element.id } }) + const url = `${getConfig().controllerLink.replace(/\/$/, '')}/api/publish`; + backend.post(url, { + id: props.element.id, + }, { + 'X-SecurityID': Config.get('SecurityID') + }) .then(() => handleAfterPublish(false)) .catch(() => handleAfterPublish(true)); } @@ -354,8 +351,8 @@ const Element = (props) => { if (!doPublishElement && !doPublishElementAfterSave) { showSavedElementToast(title); } - refetchElementalArea(); props.onAfterSubmitResponse(true); + fetchElements(); }; const { diff --git a/client/src/components/ElementEditor/ElementEditor.js b/client/src/components/ElementEditor/ElementEditor.js index 99a7cb5f..8b2f0dbd 100644 --- a/client/src/components/ElementEditor/ElementEditor.js +++ b/client/src/components/ElementEditor/ElementEditor.js @@ -1,13 +1,17 @@ /* global window */ -import React, { PureComponent } from 'react'; +import React, { PureComponent, createContext } from 'react'; import PropTypes from 'prop-types'; import { inject } from 'lib/Injector'; import { compose } from 'redux'; import { elementTypeType } from 'types/elementTypeType'; import { DropTarget } from 'react-dnd'; -import sortBlockMutation from 'state/editor/sortBlockMutation'; import ElementDragPreview from 'components/ElementEditor/ElementDragPreview'; import withDragDropContext from 'lib/withDragDropContext'; +import backend from 'lib/Backend'; +import Config from 'lib/Config'; +import { getConfig } from 'state/editor/elementConfig'; + +export const ElementEditorContext = createContext(null); /** * The ElementEditor is used in the CMS to manage a list or nested lists of @@ -20,10 +24,13 @@ class ElementEditor extends PureComponent { this.state = { dragTargetElementId: null, dragSpot: null, + elements: null, + isLoading: true, }; this.handleDragOver = this.handleDragOver.bind(this); this.handleDragEnd = this.handleDragEnd.bind(this); + this.fetchElements = this.fetchElements.bind(this); } /** @@ -47,18 +54,18 @@ class ElementEditor extends PureComponent { /** * Hook for ReactDND triggered when a drag source is dropped onto a drag target. * - * This will fire the GraphQL mutation for sorting and reset any state updates - * * @param sourceId * @param afterId */ handleDragEnd(sourceId, afterId) { - const { actions: { handleSortBlock }, areaId } = this.props; - - handleSortBlock(sourceId, afterId, areaId).then(() => { - const preview = window.jQuery('.cms-preview'); - preview.entwine('ss.preview')._loadUrl(preview.find('iframe').attr('src')); - }); + const url = `${getConfig().controllerLink.replace(/\/$/, '')}/api/sort`; + backend.post(url, { + id: sourceId, + afterBlockID: afterId, + }, { + 'X-SecurityID': Config.get('SecurityID') + }) + .then(() => this.fetchElements()); this.setState({ dragTargetElementId: null, @@ -66,6 +73,33 @@ class ElementEditor extends PureComponent { }); } + /** + * Make an API call to read all elements endpoint (areaID) + */ + fetchElements(doSetLoadingState = true) { + if (doSetLoadingState) { + this.setState(prevState => ({ + ...prevState, + isLoading: true, + })); + } + const url = `${getConfig().controllerLink.replace(/\/$/, '')}/api/readElements/${this.props.areaId}`; + return backend.get(url) + .then(response => response.json()) + .then(responseJson => { + this.setState(prevState => ({ + ...prevState, + elements: responseJson, + isLoading: false, + })); + // refresh preview + const preview = window.jQuery('.cms-preview'); + if (preview) { + preview.entwine('ss.preview')._loadUrl(preview.find('iframe').attr('src')); + } + }); + } + render() { const { ToolbarComponent, @@ -76,34 +110,51 @@ class ElementEditor extends PureComponent { connectDropTarget, allowedElements, sharedObject, + isLoading, } = this.props; - const { dragTargetElementId, dragSpot } = this.state; + const { dragTargetElementId, dragSpot, elements } = this.state; + + if (elements === null) { + this.fetchElements(false); + return null; + } // Map the allowed elements because we want to retain the sort order provided by that array. const allowedElementTypes = allowedElements.map(className => elementTypes.find(type => type.class === className) ); + // Need to convert this to a functional component in order to resolve the following eslint warning: + // warning The 'providerValue' object (at line 124) passed as the value prop to the Context provider (at line 127) changes every render. To fix this consider wrapping it in a useMemo hook + // eslint-disable-next-line react/jsx-no-constructed-context-values + const providerValue = { + fetchElements: this.fetchElements, + }; + return connectDropTarget(
- - - + + + + +
); } @@ -119,7 +170,8 @@ ElementEditor.propTypes = { }; export { ElementEditor as Component }; -export default compose( + +const params = [ withDragDropContext, DropTarget('element', {}, (connector, monitor) => ({ connectDropTarget: connector.dropTarget(), @@ -132,6 +184,7 @@ export default compose( ListComponent, }), () => 'ElementEditor' - ), - sortBlockMutation -)(ElementEditor); + ) +]; + +export default compose(...params)(ElementEditor); diff --git a/client/src/components/ElementEditor/ElementList.js b/client/src/components/ElementEditor/ElementList.js index 8ee7adb4..95471e4a 100644 --- a/client/src/components/ElementEditor/ElementList.js +++ b/client/src/components/ElementEditor/ElementList.js @@ -29,19 +29,35 @@ class ElementList extends Component { this.props.sharedObject.setState = this.setState.bind(this); } + componentDidMount() { + this.resetState({}, true); + } + componentDidUpdate(prevProps, prevState) { - // Scenario: blocks props just changed after a graphql query response updated it - if (this.props.blocks !== prevProps.blocks) { + // Don't do anything if elements have not yet been recieved from xhr request + if (!this.props.elements) { + return; + } + // Scenario: elements props just changed after an xhr response updated it + if (this.props.elements !== prevProps.elements) { this.resetState(prevState, false); return; } // Scenario Saving all elements and state has just updated because of a formSchema response from // an inline save - see Element.js handleFormSchemaSubmitResponse() if (this.state.saveAllElements) { - const unsavedChangesBlockIDs = this.props.blocks + const unsavedChangesBlockIDs = this.props.elements .map(block => parseInt(block.id, 10)) .filter(blockID => this.state.hasUnsavedChangesBlockIDs[blockID]); - const allValidated = unsavedChangesBlockIDs.every(blockID => this.state.validBlockIDs[blockID] !== null); + // const allValidated = unsavedChangesBlockIDs.every(blockID => this.state.validBlockIDs[blockID] !== null); + let allValidated = true; + for (let i = 0; i < unsavedChangesBlockIDs.length; i++) { + const blockID = unsavedChangesBlockIDs[i]; + if (this.state.validBlockIDs[blockID] === null) { + allValidated = false; + break; + } + } if (allValidated) { const allValid = unsavedChangesBlockIDs.every(blockID => this.state.validBlockIDs[blockID]); // entwineResolve is bound in entwine.js @@ -64,9 +80,9 @@ class ElementList extends Component { // - true: saved, valid // - false: attempted save, invalid const validBlockIDs = {}; - const blocks = this.props.blocks || []; - blocks.forEach(block => { - const blockID = parseInt(block.id, 10); + const elements = this.props.elements || []; + elements.forEach(element => { + const blockID = parseInt(element.id, 10); if (resetHasUnsavedChangesBlockIDs) { hasUnsavedChangesBlockIDs[blockID] = false; } else if (prevState.hasUnsavedChangesBlockIDs.hasOwnProperty(blockID)) { @@ -111,9 +127,9 @@ class ElementList extends Component { } getDragIndicatorIndex() { - const { dragTargetElementId, draggedItem, blocks, dragSpot } = this.props; + const { dragTargetElementId, draggedItem, elements, dragSpot } = this.props; return getDragIndicatorIndex( - blocks.map(element => element.id), + elements.map(element => element.id), dragTargetElementId, draggedItem && draggedItem.id, dragSpot @@ -122,15 +138,14 @@ class ElementList extends Component { /** * Renders a list of Element components, each with an elementType object - * of data mapped into it. The data is provided by a GraphQL HOC registered - * in registerTransforms.js. + * of data mapped into it. */ renderBlocks() { const { ElementComponent, HoverBarComponent, DragIndicatorComponent, - blocks, + elements, allowedElementTypes, elementTypes, areaId, @@ -140,16 +155,11 @@ class ElementList extends Component { isDraggingOver, } = this.props; - // Blocks can be either null or an empty array - if (!blocks) { - return null; - } - - if (blocks && !blocks.length) { + if (elements.length === 0) { return
{i18n._t('ElementList.ADD_BLOCKS', 'Add blocks to place your content')}
; } - let output = blocks.map(element => { + let output = elements.map(element => { const saveElement = this.state.saveAllElements && this.state.hasUnsavedChangesBlockIDs[element.id] && this.state.validBlockIDs[element.id] === null; @@ -203,19 +213,23 @@ class ElementList extends Component { * @returns {LoadingComponent|null} */ renderLoading() { - const { loading, LoadingComponent } = this.props; + const { + isLoading, + LoadingComponent + } = this.props; - if (loading) { + if (isLoading) { return ; } return null; } render() { - const { blocks } = this.props; + const { elements } = this.props; + const listClassNames = classNames( 'elemental-editor-list', - { 'elemental-editor-list--empty': !blocks || !blocks.length } + { 'elemental-editor-list--empty': !elements || !elements.length } ); return this.props.connectDropTarget( @@ -228,10 +242,9 @@ class ElementList extends Component { } ElementList.propTypes = { - blocks: PropTypes.arrayOf(elementType), + elements: PropTypes.arrayOf(elementType).isRequired, elementTypes: PropTypes.arrayOf(elementTypeType).isRequired, allowedElementTypes: PropTypes.arrayOf(elementTypeType).isRequired, - loading: PropTypes.bool, areaId: PropTypes.number.isRequired, dragTargetElementId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), onDragOver: PropTypes.func, @@ -241,19 +254,19 @@ ElementList.propTypes = { }; ElementList.defaultProps = { - blocks: [], - loading: false, sharedObject: { entwineResolve: () => {}, setState: null, }, + elements: [], + isLoading: false, }; export { ElementList as Component }; const elementListTarget = { drop(props, monitor) { - const { blocks } = props; + const { elements } = props; const elementTargetDropResult = monitor.getDropResult(); if (!elementTargetDropResult) { @@ -261,12 +274,12 @@ const elementListTarget = { } const dropIndex = getDragIndicatorIndex( - blocks.map(element => element.id), + elements.map(element => element.id), elementTargetDropResult.target, monitor.getItem(), elementTargetDropResult.dropSpot, ); - const dropAfterID = blocks[dropIndex - 1] ? blocks[dropIndex - 1].id : '0'; + const dropAfterID = elements[dropIndex - 1] ? elements[dropIndex - 1].id : '0'; return { ...elementTargetDropResult, diff --git a/client/src/components/ElementEditor/tests/Element-test.js b/client/src/components/ElementEditor/tests/Element-test.js index ba564b0b..7f6de2d5 100644 --- a/client/src/components/ElementEditor/tests/Element-test.js +++ b/client/src/components/ElementEditor/tests/Element-test.js @@ -2,9 +2,9 @@ /* global jest, test, describe, it, expect */ import React from 'react'; -import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'; import { render } from '@testing-library/react'; import { Component as Element } from '../Element'; +import { ElementEditorContext } from '../ElementEditor'; function makeProps(obj = {}) { return { @@ -45,81 +45,98 @@ function makeProps(obj = {}) { }; } +function makeProviderProps(obj = {}) { + return { + value: { + fetchElements: () => {}, + ...obj, + } + }; +} + test('Element should render the HeaderComponent and the ContentComponent', () => { - const client = new ApolloClient({ cache: new InMemoryCache() }); - const { container } = render(); + const { container } = render( + + + + ); expect(container.querySelectorAll('.element-editor__element .test-header')).toHaveLength(1); expect(container.querySelectorAll('.element-editor__element .test-content')).toHaveLength(1); }); test('Element should not render at all if no ID is given', () => { - const client = new ApolloClient({ cache: new InMemoryCache() }); const { container } = render( - + + + ); expect(container.querySelectorAll('.element-editor__element')).toHaveLength(0); }); test('Element should render even if the element is broken', () => { - const client = new ApolloClient({ cache: new InMemoryCache() }); const { container } = render( - + + + ); expect(container.querySelectorAll('.element-editor__element .test-header')).toHaveLength(1); expect(container.querySelectorAll('.element-editor__element .test-content')).toHaveLength(1); }); test('Element getVersionedStateClassName() should identify draft elements', () => { - const client = new ApolloClient({ cache: new InMemoryCache() }); const { container } = render( - + + + ); expect(container.querySelector('.element-editor__element').classList.contains('element-editor__element--draft')).toBe(true); }); test('Element getVersionedStateClassName() should identify modified elements', () => { - const client = new ApolloClient({ cache: new InMemoryCache() }); const { container } = render( - + + + ); expect(container.querySelectorAll('.element-editor__element--modified')).toHaveLength(1); }); test('Element getVersionedStateClassName() should identify published elements', () => { - const client = new ApolloClient({ cache: new InMemoryCache() }); const { container } = render( - + + + ); expect(container.querySelectorAll('.element-editor__element--published')).toHaveLength(1); }); diff --git a/client/src/components/ElementEditor/tests/ElementEditor-test.js b/client/src/components/ElementEditor/tests/ElementEditor-test.js index 39f9213b..93045074 100644 --- a/client/src/components/ElementEditor/tests/ElementEditor-test.js +++ b/client/src/components/ElementEditor/tests/ElementEditor-test.js @@ -5,6 +5,24 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { Component as ElementEditor } from '../ElementEditor'; +jest.mock('lib/Backend', () => ({ + get: () => Promise.resolve({ json: () => [] }), +})); + +const sectionConfigKey = 'DNADesign\\Elemental\\Controllers\\ElementalAreaController'; +window.ss.config = { + SecurityID: 1234567890, + sections: [ + { + name: sectionConfigKey, + controllerLink: 'my/test/endpoint', + }, + ], +}; + +const jQuery = jest.fn(); +window.jQuery = jQuery; + function makeProps(obj = {}) { return { ToolbarComponent: ({ elementTypes }) =>
type.class).join(',')} />, @@ -53,28 +71,31 @@ function makeProps(obj = {}) { }; } -test('ElementEditor should render ElementList and Toolbar', () => { +test('ElementEditor should render ElementList and Toolbar', async () => { const { container } = render(); + await screen.findByTestId('test-toolbar'); expect(container.querySelectorAll('.test-list')).toHaveLength(1); expect(container.querySelectorAll('[data-testid="test-toolbar"]')).toHaveLength(1); }); -test('ElementEditor should filter all element types by those allowed for this editor', () => { +test('ElementEditor should filter all element types by those allowed for this editor', async () => { render( ); + await screen.findByTestId('test-toolbar'); expect(screen.getByTestId('test-toolbar').getAttribute('data-elementtypes')).toBe('Test\\Class\\Aye'); }); -test('ElementEditor should retain the order specified by the allowed elements config', () => { +test('ElementEditor should retain the order specified by the allowed elements config', async () => { render( ); + await screen.findByTestId('test-toolbar'); expect(screen.getByTestId('test-toolbar').getAttribute('data-elementtypes')).toBe('Test\\Class\\Bee,Test\\Class\\TestElement'); }); diff --git a/client/src/components/ElementEditor/tests/ElementList-test.js b/client/src/components/ElementEditor/tests/ElementList-test.js index 94628c8d..186cc298 100644 --- a/client/src/components/ElementEditor/tests/ElementList-test.js +++ b/client/src/components/ElementEditor/tests/ElementList-test.js @@ -17,7 +17,7 @@ const elementTypes = [ function makeProps(obj = {}) { return { key: '1', - blocks: [ + elements: [ { id: '1', title: 'Title', @@ -53,7 +53,7 @@ function makeProps(obj = {}) { }; } -test('ElementList renders elements when blocks are provided as props', () => { +test('ElementList renders elements when elements are provided as props', () => { const { container } = render(); expect(container.querySelectorAll('.test-element')).toHaveLength(2); expect(container.querySelectorAll('.test-loading')).toHaveLength(0); @@ -63,8 +63,8 @@ test('ElementList renders a loading component', () => { const { container } = render( ); @@ -76,8 +76,8 @@ test('ElementList renders a placeholder message when no elements are provided as const { container } = render( ); diff --git a/client/src/legacy/ElementEditor/entwine.js b/client/src/legacy/ElementEditor/entwine.js index 68fd4024..91123572 100644 --- a/client/src/legacy/ElementEditor/entwine.js +++ b/client/src/legacy/ElementEditor/entwine.js @@ -9,14 +9,9 @@ import { getConfig } from 'state/editor/elementConfig'; import { destroy } from 'redux-form'; /** - * Reset the Apollo and Redux stores holding data relating to elemental inline edit forms + * Reset the Redux store holding data relating to elemental inline edit forms */ const resetStores = () => { - // After page level saves we need to reload all the blocks from the server. We can remove - // this if we can figure out a way to optimistically update the apollo cache. See: - // https://github.com/dnadesign/silverstripe-elemental/pull/439#issuecomment-428773370 - window.ss.apolloClient.resetStore(); - // Defer playing with redux store setTimeout(() => { // After the page submit we want to destroy the form values so it's reloaded. We can't @@ -133,7 +128,7 @@ jQuery.entwine('ss', ($) => { const validationResultPjax = JSON.parse(data.xhr.responseText).ValidationResult; const validationResult = JSON.parse(validationResultPjax.replace(/<\/?script[^>]*?>/g, '')); - // Reset redux store if form is succesfully submitted so apollo to refetches element data + // Reset redux store if form is succesfully submitted // Do not reset if there are any validation errors because we want redux to hydrate the // form, rather than then refetching which will return a value from the database. // Instead the user should still see any modfied value they just entered. diff --git a/client/src/state/editor/addElementMutation.js b/client/src/state/editor/addElementMutation.js deleted file mode 100644 index e61223fc..00000000 --- a/client/src/state/editor/addElementMutation.js +++ /dev/null @@ -1,42 +0,0 @@ -import { graphql } from '@apollo/client/react/hoc'; -import gql from 'graphql-tag'; -import { query as readBlocksQuery, config as readBlocksConfig } from './readBlocksForAreaQuery'; - -// GraphQL query for deleting a specific block -const mutation = gql` -mutation AddElementToArea($className: String!, $elementalAreaID: ID!, $afterElementID: ID) { - addElementToArea( - className: $className, - elementalAreaID: $elementalAreaID, - afterElementID: $afterElementID - ) { - id - } -} -`; - -const config = { - props: ({ mutate, ownProps: { actions, areaId } }) => { - const handleAddElementToArea = (className, afterElementID) => mutate({ - variables: { className, elementalAreaID: areaId, afterElementID }, - }); - - return { - actions: { - ...actions, - handleAddElementToArea, - }, - }; - }, - options: ({ areaId }) => ({ - // Refetch versions after mutation is completed - refetchQueries: [{ - query: readBlocksQuery, - variables: readBlocksConfig.options({ areaId }).variables - }] - }) -}; - -export { mutation, config }; - -export default graphql(mutation, config); diff --git a/client/src/state/editor/archiveBlockMutation.js b/client/src/state/editor/archiveBlockMutation.js deleted file mode 100644 index 2b12fb39..00000000 --- a/client/src/state/editor/archiveBlockMutation.js +++ /dev/null @@ -1,36 +0,0 @@ -import { graphql } from '@apollo/client/react/hoc'; -import gql from 'graphql-tag'; -import { config as readBlocksConfig, query as readBlocksQuery } from './readBlocksForAreaQuery'; - -// GraphQL query for deleting a specific block -const mutation = gql` -mutation ArchiveBlock($blockId: ID!) { - deleteBlocks(ids: [$blockId]) -} -`; - -const config = { - props: ({ mutate, ownProps: { actions } }) => { - const handleArchiveBlock = (blockId) => mutate({ - variables: { blockId }, - }); - - return { - actions: { - ...actions, - handleArchiveBlock, - }, - }; - }, - options: ({ areaId }) => ({ - // Refetch versions after mutation is completed - refetchQueries: [{ - query: readBlocksQuery, - variables: readBlocksConfig.options({ areaId }).variables - }] - }), -}; - -export { mutation, config }; - -export default graphql(mutation, config); diff --git a/client/src/state/editor/duplicateBlockMutation.js b/client/src/state/editor/duplicateBlockMutation.js deleted file mode 100644 index 5e36496a..00000000 --- a/client/src/state/editor/duplicateBlockMutation.js +++ /dev/null @@ -1,38 +0,0 @@ -import { graphql } from '@apollo/client/react/hoc'; -import gql from 'graphql-tag'; -import { config as readBlocksConfig, query as readBlocksQuery } from './readBlocksForAreaQuery'; - -// GraphQL query for duplicating a specific block -const mutation = gql` -mutation DuplicateBlock($blockId: ID!) { - duplicateBlock(id: $blockId) { - id - } -} -`; - -const config = { - props: ({ mutate, ownProps: { actions } }) => { - const handleDuplicateBlock = (blockId) => mutate({ - variables: { blockId }, - }); - - return { - actions: { - ...actions, - handleDuplicateBlock, - }, - }; - }, - options: ({ areaId }) => ({ - // Refetch versions after mutation is completed - refetchQueries: [{ - query: readBlocksQuery, - variables: readBlocksConfig.options({ areaId }).variables - }] - }), -}; - -export { mutation, config }; - -export default graphql(mutation, config); diff --git a/client/src/state/editor/publishBlockMutation.js b/client/src/state/editor/publishBlockMutation.js deleted file mode 100644 index 9f54f2e8..00000000 --- a/client/src/state/editor/publishBlockMutation.js +++ /dev/null @@ -1,9 +0,0 @@ -import gql from 'graphql-tag'; - -export const publishBlockMutation = gql` -mutation PublishBlock($blockId:ID!) { - publishBlock(id: $blockId) { - id - } -} -`; diff --git a/client/src/state/editor/readBlocksForAreaQuery.js b/client/src/state/editor/readBlocksForAreaQuery.js deleted file mode 100644 index 9b9488c3..00000000 --- a/client/src/state/editor/readBlocksForAreaQuery.js +++ /dev/null @@ -1,65 +0,0 @@ -import { graphql } from '@apollo/client/react/hoc'; -import gql from 'graphql-tag'; - -// GraphQL query for retrieving the current state of elements for an area. The results of the query -// must be set to the "blocks" prop on the component that this HOC is applied to for binding -// implementation. -const query = gql` -query ReadBlocksForArea($id:ID!) { - readOneElementalArea(filter: { id: { eq: $id } }, versioning: { - mode: DRAFT - }){ - elements { - id - title - blockSchema - obsoleteClassName - isLiveVersion - isPublished - version - canCreate - canPublish - canUnpublish - canDelete - } - } -} -`; - -const config = { - options({ areaId }) { - return { - variables: { - id: areaId, - } - }; - }, - props( - { - data: { - error, - readOneElementalArea, - loading: networkLoading, - }, - } - ) { - let blocks = null; - if (readOneElementalArea) { - // Remove the GraphQL pagination keys - blocks = readOneElementalArea.elements; - } - - const errors = error && error.graphQLErrors && - error.graphQLErrors.map((graphQLError) => graphQLError.message); - - return { - loading: networkLoading || !blocks, - blocks, - graphQLErrors: errors, - }; - }, -}; - -export { query, config }; - -export default graphql(query, config); diff --git a/client/src/state/editor/sortBlockMutation.js b/client/src/state/editor/sortBlockMutation.js deleted file mode 100644 index 71dedc0b..00000000 --- a/client/src/state/editor/sortBlockMutation.js +++ /dev/null @@ -1,92 +0,0 @@ -import { graphql } from '@apollo/client/react/hoc'; -import gql from 'graphql-tag'; -import { query as readBlocksQuery, config as readBlocksConfig } from './readBlocksForAreaQuery'; - -// GraphQL query for changing the sort order of blocks -const mutation = gql` -mutation SortBlockMutation($blockId:ID!, $afterBlockId:ID!) { - sortBlock( - id: $blockId - afterBlockID: $afterBlockId - ) { - id - isLiveVersion - isPublished - } -} -`; - -const config = { - props: ({ mutate, ownProps: { actions } }) => { - const handleSortBlock = (blockId, afterBlockId, areaId) => mutate({ - variables: { - blockId, - afterBlockId, - }, - optimisticResponse: { - sortBlock: { - id: blockId, - isLiveVersion: false, - isPublished: false, - __typename: 'Block', - }, - }, - update: (store, { data: { sortBlock: updatedElementData } }) => { - const variables = readBlocksConfig.options({ areaId }).variables; - const cachedData = store.readQuery({ query: readBlocksQuery, variables }); - - // Query returns a deeply nested object. Explicit reconstruction via spreads is too verbose. - // This is an alternative, relatively efficient way to deep clone - const newData = JSON.parse(JSON.stringify(cachedData)); - let blocks = newData.readOneElementalArea.elements; - // Find the block we reordered - const movedBlockIndex = blocks.findIndex(block => block.id === blockId); - // Keep it - const movedBlock = blocks[movedBlockIndex]; - // Update the moved block with the new details returned in the GraphQL response - Object.entries(updatedElementData).forEach(([key, value]) => { - // Skip the type name as this is always returned but should never change - if (key === '__typename') { - return; - } - - movedBlock[key] = value; - }); - // Remove the moved block - blocks.splice(movedBlockIndex, 1); - // If the target is 0, it's moving to the start - if (afterBlockId === '0') { - blocks.unshift(movedBlock); - } else { - // Else, find the block we inserted after - let targetBlockIndex = blocks.findIndex(block => block.id === afterBlockId); - - // If we can't find the block, it must be the one we're trying to move - put it back! - if (targetBlockIndex === -1) { - targetBlockIndex = movedBlockIndex - 1; - } - - // Add it back after the target - const end = blocks.slice(targetBlockIndex + 1); - blocks = blocks.slice(0, targetBlockIndex + 1); - blocks.push(movedBlock); - blocks = blocks.concat(end); - } - - // Add it back to the full result - newData.readOneElementalArea.elements = blocks; - store.writeQuery({ query: readBlocksQuery, data: newData, variables }); - }, - }); - return { - actions: { - ...actions, - handleSortBlock, - }, - }; - }, -}; - -export { mutation, config }; - -export default graphql(mutation, config); diff --git a/client/src/state/editor/unpublishBlockMutation.js b/client/src/state/editor/unpublishBlockMutation.js deleted file mode 100644 index 261bcb1f..00000000 --- a/client/src/state/editor/unpublishBlockMutation.js +++ /dev/null @@ -1,45 +0,0 @@ -import { graphql } from '@apollo/client/react/hoc'; -import gql from 'graphql-tag'; -import { config as readBlocksConfig, query as readBlocksQuery } from './readBlocksForAreaQuery'; - -// GraphQL query for unpublishing a specific block -const mutation = gql` -mutation UnpublishBlock($blockId:ID!) { - unpublishBlock( - id: $blockId - ) { - id - } -} -`; - -const config = { - props: ({ mutate, ownProps: { actions } }) => { - const handleUnpublishBlock = (blockId, fromStage, toStage, fromVersion) => mutate({ - variables: { - blockId, - fromStage, - toStage, - fromVersion - }, - }); - - return { - actions: { - ...actions, - handleUnpublishBlock, - }, - }; - }, - options: ({ areaId }) => ({ - // Refetch versions after mutation is completed - refetchQueries: [{ - query: readBlocksQuery, - variables: readBlocksConfig.options({ areaId }).variables - }] - }), -}; - -export { mutation, config }; - -export default graphql(mutation, config); diff --git a/client/src/state/history/readOneBlockQuery.js b/client/src/state/history/readOneBlockQuery.js deleted file mode 100644 index af211a61..00000000 --- a/client/src/state/history/readOneBlockQuery.js +++ /dev/null @@ -1,96 +0,0 @@ -import { graphql } from '@apollo/client/react/hoc'; -import gql from 'graphql-tag'; - -// GraphQL query for retrieving the version history of a specific block. The results of -// the query must be set to the "versions" prop on the component that this HOC is -// applied to for binding implementation. -const query = gql` -query ReadHistoryViewerBlock ($block_id: ID!, $limit: Int!, $offset: Int!) { - readOneBlock( - versioning: { - mode: LATEST - }, - filter: { id: { eq: $block_id } } - ) { - id - versions (limit: $limit, offset: $offset, sort: { version: DESC }) { - pageInfo { - totalCount - } - nodes { - version - absoluteLink - author { - firstName - surname - } - publisher { - firstName - surname - } - published - liveVersion - latestDraftVersion - lastEdited - } - } - } -} -`; - -const config = { - options({ recordId, limit, page }) { - return { - variables: { - limit, - offset: ((page || 1) - 1) * limit, - block_id: recordId, - } - }; - }, - props( - { - data: { - error, - refetch, - readOneBlock, - loading: networkLoading, - }, - ownProps: { - actions = { - versions: {} - }, - limit, - recordId, - }, - } - ) { - const versions = readOneBlock || null; - - const errors = error && error.graphQLErrors && - error.graphQLErrors.map((graphQLError) => graphQLError.message); - - return { - loading: networkLoading || !versions, - versions, - graphQLErrors: errors, - actions: { - ...actions, - versions: { - ...versions, - goToPage(page) { - refetch({ - offset: ((page || 1) - 1) * limit, - limit, - block_id: recordId, - }); - } - }, - }, - }; - }, -}; - -export { query, config }; - -export default graphql(query, config); diff --git a/client/src/state/history/revertToBlockVersionMutation.js b/client/src/state/history/revertToBlockVersionMutation.js deleted file mode 100644 index ef2de656..00000000 --- a/client/src/state/history/revertToBlockVersionMutation.js +++ /dev/null @@ -1,43 +0,0 @@ -import { graphql } from '@apollo/client/react/hoc'; -import gql from 'graphql-tag'; - -const mutation = gql` -mutation revertBlockToVersion($id:ID!, $fromStage:VersionedStage!, $toStage:VersionedStage!, $fromVersion:Int!) { - copyBlockToStage(input: { - id: $id - fromVersion: $fromVersion - fromStage: $fromStage - toStage: $toStage - }) { - id - } -} -`; - -const config = { - props: ({ mutate, ownProps: { actions } }) => { - const revertToVersion = (id, fromVersion, fromStage, toStage) => mutate({ - variables: { - id, - fromVersion, - fromStage, - toStage, - }, - }); - - return { - actions: { - ...actions, - revertToVersion, - }, - }; - }, - options: { - // Refetch versions after mutation is completed - refetchQueries: ['ReadHistoryViewerBlock'] - } -}; - -export { mutation, config }; - -export default graphql(mutation, config); diff --git a/client/src/state/history/revertToBlockVersionRequest.js b/client/src/state/history/revertToBlockVersionRequest.js new file mode 100644 index 00000000..02eb265b --- /dev/null +++ b/client/src/state/history/revertToBlockVersionRequest.js @@ -0,0 +1,25 @@ +import React from 'react'; +import backend from 'lib/Backend'; +import { getConfig } from 'state/editor/elementConfig'; +import Config from 'lib/Config'; + +const revertToBlockVersionRequest = (HistoryViewerVersionDetailComponent) => (props) => { + const newProps = { ...props }; + if (!newProps.hasOwnProperty('actions')) { + newProps.actions = {}; + } + newProps.actions.revertToVersion = (id, fromVersion, fromStage, toStage) => { + const url = `${getConfig().controllerLink.replace(/\/$/, '')}/api/revert`; + return backend.post(url, { + id, + fromVersion, + fromStage, + toStage + }, { + 'X-SecurityID': Config.get('SecurityID') + }); + }; + return ; +}; + +export default revertToBlockVersionRequest; diff --git a/client/src/types/elementTypeType.js b/client/src/types/elementTypeType.js index fcc28308..e80dc1a9 100644 --- a/client/src/types/elementTypeType.js +++ b/client/src/types/elementTypeType.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; // Describes the structure of an element type: const elementTypeType = PropTypes.shape({ - // The GraphQL typeName of the element + // The typeName of the element name: PropTypes.string, // The "name" of the type (eg. "Content") title: PropTypes.string, diff --git a/composer.json b/composer.json index 1d469bfe..1359a63a 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,6 @@ "silverstripe/admin": "^3", "silverstripe/versioned": "^3", "silverstripe/versioned-admin": "^3", - "silverstripe/graphql": "^6", "symbiote/silverstripe-gridfieldextensions": "^5", "silverstripe/vendor-plugin": "^2" }, diff --git a/package.json b/package.json index 8ea978eb..23ff4148 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,9 @@ }, "homepage": "https://github.com/dnadesign/silverstripe-elemental#readme", "dependencies": { - "@apollo/client": "^3.7.1", "@popperjs/core": "^2.11.6", "bootstrap": "^4.6.2", "classnames": "^2.3.2", - "graphql": "^16.8.1", - "graphql-tag": "^2.12.6", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dnd": "^5.0.0", @@ -51,7 +48,7 @@ }, "devDependencies": { "@silverstripe/eslint-config": "^1.3.0", - "@silverstripe/webpack-config": "^2.1.0", + "@silverstripe/webpack-config": "^3.0.0-alpha1", "@testing-library/react": "^14.0.0", "babel-jest": "^29.2.2", "jest-cli": "^29.2.2", diff --git a/src/Controllers/ElementalAreaController.php b/src/Controllers/ElementalAreaController.php index 9cad61e9..16a55579 100644 --- a/src/Controllers/ElementalAreaController.php +++ b/src/Controllers/ElementalAreaController.php @@ -15,6 +15,11 @@ use SilverStripe\Forms\FormAction; use SilverStripe\Forms\FieldList; use SilverStripe\Control\Controller; +use DNADesign\Elemental\Models\ElementalArea; +use DNADesign\Elemental\Services\ReorderElements; +use Exception; +use SilverStripe\Control\HTTPRequest; +use InvalidArgumentException; /** * Controller for "ElementalArea" - handles loading and saving of in-line edit forms in an elemental area in admin @@ -29,16 +34,227 @@ class ElementalAreaController extends CMSMain private static $url_handlers = [ 'elementForm/$ItemID' => 'elementForm', - 'POST api/saveForm/$ID' => 'apiSaveForm', '$FormName/field/$FieldName' => 'formAction', + 'GET api/readElements/$elementalAreaID!' => 'apiReadElements', + 'POST api/create' => 'apiCreate', + 'POST api/delete' => 'apiDelete', + 'POST api/duplicate' => 'apiDuplicate', + 'POST api/publish' => 'apiPublish', + 'POST api/saveForm/$ID' => 'apiSaveForm', + 'POST api/sort' => 'apiSort', + 'POST api/unpublish' => 'apiUnpublish', ]; private static $allowed_actions = [ 'elementForm', - 'apiSaveForm', 'formAction', + 'apiCreate', + 'apiDelete', + 'apiDuplicate', + 'apiPublish', + 'apiReadElements', + 'apiSaveForm', + 'apiSort', + 'apiUnpublish', ]; + /** + * JSON endpoint to create a new element in an ElementalArea + */ + public function apiCreate(HTTPRequest $request): HTTPResponse + { + if (!SecurityToken::inst()->checkRequest($request)) { + $this->jsonError(400); + } + $elementClass = $this->getPostedJsonValue($request, 'elementClass'); + $elementalAreaID = $this->getPostedJsonValue($request, 'elementalAreaID'); + // $afterElementID can be null, so do not error if it's missing + $data = json_decode($request->getBody(), true); + $afterElementID = $data['insertAfterElementID'] ?? null; + if (!is_subclass_of($elementClass, BaseElement::class)) { + $this->jsonError(400); + } + $elementalArea = ElementalArea::get()->byID($elementalAreaID); + if (!$elementalArea) { + $this->jsonError(400); + } + if (!$elementalArea->canEdit()) { + $this->jsonError(403); + } + /** @var BaseElement $newElement */ + $newElement = Injector::inst()->create($elementClass); + if (!$newElement->canCreate()) { + $this->jsonError(403); + } + // Assign the parent ID directly rather than via HasManyList to prevent multiple writes. + // See BaseElement::$has_one for the "Parent" naming. + $newElement->ParentID = $elementalArea->ID; + $newElement->ensureSortSet(); + if ($afterElementID !== null) { + $this->reorderElements($newElement, $afterElementID); + } else { + $newElement->write(); + } + return $this->jsonSuccess(204); + } + + /** + * JSON endpoint to delete an element + */ + public function apiDelete(HTTPRequest $request): HTTPResponse + { + if (!SecurityToken::inst()->checkRequest($request)) { + $this->jsonError(400); + } + $id = $this->getPostedJsonValue($request, 'id'); + $element = BaseElement::get()->byID($id); + if (!$element) { + $this->jsonError(400); + } + if (!$element->canDelete()) { + $this->jsonError(403); + } + // Elemental does not officially support unversioned elements so always call doArchive() + $element->doArchive(); + return $this->jsonSuccess(204); + } + + /** + * JSON endpoint to duplicate an element + */ + public function apiDuplicate(HTTPRequest $request): HTTPResponse + { + if (!SecurityToken::inst()->checkRequest($request)) { + $this->jsonError(400); + } + $id = $this->getPostedJsonValue($request, 'id'); + $element = BaseElement::get()->byID($id); + if (!$element) { + $this->jsonError(400); + } + if (!$element->canCreate()) { + $this->jsonError(403); + } + // check can edit the elemental area + $areaID = $element->ParentID; + $area = ElementalArea::get()->byID($areaID); + if (!$area) { + $this->jsonError(400); + } + if (!$area->canEdit()) { + $this->jsonError(403); + } + // clone element + $clone = $element->duplicate(false); + $clone->Title = $this->getNewTitle($clone->Title ?? ''); + $clone->Sort = 0; // must be zeroed for reorder to work + $area->Elements()->add($clone); + // reorder + $this->reorderElements($clone, $id); + return $this->jsonSuccess(204); + } + + /** + * JSON endpoint to publish an element + */ + public function apiPublish(HTTPRequest $request): HTTPResponse + { + if (!SecurityToken::inst()->checkRequest($request)) { + $this->jsonError(400); + } + $id = $this->getPostedJsonValue($request, 'id'); + $element = BaseElement::get()->byID($id); + if (!$element) { + $this->jsonError(400); + } + if (!$element->canPublish()) { + $this->jsonError(403); + } + $element->publishRecursive(); + return $this->jsonSuccess(204); + } + + /** + * JSON endpoint to read elements on an ElementalArea + */ + public function apiReadElements(HTTPRequest $request): HTTPResponse + { + $request = $this->getRequest(); + $elementalAreaID = $request->param('elementalAreaID'); + $elementalArea = ElementalArea::get()->byID($elementalAreaID); + if (!$elementalArea) { + $this->jsonError(404); + } + if (!$elementalArea->canView()) { + $this->jsonError(403); + } + $data = []; + foreach ($elementalArea->Elements() as $element) { + if (!$element->canView()) { + continue; + } + $data[] = [ + 'id' => $element->ID, + 'title' => $element->Title, + 'blockSchema' => $element->getBlockSchema(), + 'obsoleteClassName' => $element->getObsoleteClassName(), + 'version' => $element->Version, + 'isPublished' => $element->isPublished(), + 'isLiveVersion' => $element->isLiveVersion(), + 'canDelete' => $element->canDelete(), + 'canPublish' => $element->canPublish(), + 'canUnpublish' => $element->canUnpublish(), + 'canCreate' => $element->canCreate(), + ]; + } + $this->extend('updateApiReadElementalArea', $data, $request); + return $this->jsonSuccess(200, $data); + } + + /** + * JSON endpoint to sort elements on an ElementalArea + */ + public function apiSort(HTTPRequest $request): HTTPResponse + { + if (!SecurityToken::inst()->checkRequest($request)) { + $this->jsonError(400); + } + $id = $this->getPostedJsonValue($request, 'id'); + $afterBlockID = $this->getPostedJsonValue($request, 'afterBlockID'); + $element = BaseElement::get()->byID($id); + if (!$element) { + $this->jsonError(400); + } + if (!$element->canEdit()) { + $this->jsonError(403); + } + $this->reorderElements($element, $afterBlockID); + return $this->jsonSuccess(204); + } + + /** + * JSON endpoint to unpublish an element + */ + public function apiUnpublish(HTTPRequest $request): HTTPResponse + { + if (!SecurityToken::inst()->checkRequest($request)) { + $this->jsonError(400); + } + $id = $this->getPostedJsonValue($request, 'id'); + $element = BaseElement::get()->byID($id); + if (!$element) { + $this->jsonError(400); + } + if (!$element->canUnpublish()) { + $this->jsonError(403); + } + $element->doUnpublish(); + return $this->jsonSuccess(204); + } + + /** + * Returns configuration required by the client app + */ public function getClientConfig() { $clientConfig = parent::getClientConfig(); @@ -46,6 +262,8 @@ public function getClientConfig() 'schemaUrl' => $this->Link('schema/elementForm'), 'formNameTemplate' => sprintf(static::FORM_NAME_TEMPLATE, '{id}'), ]; + $clientConfig['controllerLink'] = $this->Link(); + // Configuration that is available per element type $clientConfig['elementTypes'] = ElementTypeRegistry::generate()->getDefinitions(); return $clientConfig; @@ -60,7 +278,6 @@ public function getClientConfig() public function elementForm(): Form { $id = $this->getRequest()->param('ItemID'); - // Note that new elements are added via graphql, so only using this endpoint for editing existing $element = BaseElement::get()->byID($id); if (!$element) { $this->jsonError(404); @@ -89,7 +306,7 @@ public function save(array $data, Form $form): HTTPResponse { $request = $this->getRequest(); - // Check security token for non-view operation + // Check security token for non-view operation - note token is pased in POST body, not headers if (!SecurityToken::inst()->checkRequest($request)) { $this->jsonError(400); } @@ -158,6 +375,39 @@ public static function removeNamespacesFromFields(array $data, $elementID) return $output; } + private function reorderElements(BaseElement $element, int $afterElementID): void + { + try { + /** @var ReorderElements $reorderer */ + $reorderer = Injector::inst()->create(ReorderElements::class, $element); + $reorderer->reorder($afterElementID); + } catch (InvalidArgumentException) { + $this->jsonError(500); + } + } + + private function getNewTitle(string $title = ''): ?string + { + $hasCopyPattern = '/^.*(\scopy($|\s[0-9]+$))/'; + $hasNumPattern = '/^.*(\s[0-9]+$)/'; + $parts = []; + // does $title end with 'copy' (ignoring numbers for now)? + if (preg_match($hasCopyPattern ?? '', $title ?? '', $parts)) { + $copy = $parts[1]; + // does $title end with numbers? + if (preg_match($hasNumPattern ?? '', $copy ?? '', $parts)) { + $num = trim($parts[1] ?? ''); + $len = strlen($num ?? ''); + $inc = (int)$num + 1; + return substr($title ?? '', 0, -$len) . "$inc"; + } else { + return $title . ' 2'; + } + } else { + return $title . ' copy'; + } + } + /** * Create a form for a given element */ diff --git a/src/GraphQL/Resolvers/Resolver.php b/src/GraphQL/Resolvers/Resolver.php deleted file mode 100644 index fd6bbe82..00000000 --- a/src/GraphQL/Resolvers/Resolver.php +++ /dev/null @@ -1,209 +0,0 @@ -value; - } - - /** - * @param $obj - * @param array $args - * @param array $context - * @param ResolveInfo $info - * @return BaseElement - * @throws ValidationException - * @throws InvalidArgumentException - */ - public static function resolveAddElementToArea( - $obj, - array $args, - array $context, - ResolveInfo $info - ): BaseElement { - $elementClass = $args['className']; - $elementalAreaID = $args['elementalAreaID']; - $afterElementID = $args['afterElementID'] ?? null; - - if (!is_subclass_of($elementClass, BaseElement::class)) { - throw new InvalidArgumentException("$elementClass is not a subclass of " . BaseElement::class); - } - - $elementalArea = ElementalArea::get()->byID($elementalAreaID); - - if (!$elementalArea) { - throw new InvalidArgumentException("Invalid ElementalAreaID: $elementalAreaID"); - } - - $member = UserContextProvider::get($context); - if (!$elementalArea->canEdit($member)) { - throw new InvalidArgumentException("The current user has insufficient permission to edit ElementalAreas"); - } - - /** @var BaseElement $newElement */ - $newElement = Injector::inst()->create($elementClass); - - $member = UserContextProvider::get($context); - if (!$newElement->canEdit($member)) { - throw new InvalidArgumentException( - 'The current user has insufficient permission to edit Elements' - ); - } - - // Assign the parent ID directly rather than via HasManyList to prevent multiple writes. - // See BaseElement::$has_one for the "Parent" naming. - $newElement->ParentID = $elementalArea->ID; - // Ensure that a sort order is assigned - $newElement->ensureSortSet(); - - if ($afterElementID !== null) { - /** @var ReorderElements $reorderer */ - $reorderer = Injector::inst()->create(ReorderElements::class, $newElement, true); - $reorderer->reorder($afterElementID); // also writes the element - } else { - $newElement->write(skipValidation: true); - } - - return $newElement; - } - - /** - * @param $object - * @param array $args - * @param $context - * @param ResolveInfo $info - * @return BaseElement - * @throws ValidationException - */ - public static function resolveDuplicateBlock($object, array $args, $context, ResolveInfo $info) - { - // load element to clone - $elementID = $args['id']; - $element = BaseElement::get_by_id($elementID); - if (!$element) { - throw new InvalidArgumentException("Invalid BaseElementID: $elementID"); - } - - // check can edit the elemental area - $areaID = $element->ParentID; - $area = ElementalArea::get_by_id($areaID); - if (!$area) { - throw new InvalidArgumentException("Invalid ParentID on BaseElement: $elementID"); - } - $member = UserContextProvider::get($context); - if (!$area->canEdit($member)) { - throw new InvalidArgumentException( - "The current user has insufficient permission to edit ElementalArea: $areaID" - ); - } - - try { - // clone element - $clone = $element->duplicate(false); - $clone->Title = static::newTitle($clone->Title ?? ''); - $clone->Sort = 0; // must be zeroed for reorder to work - $area->Elements()->add($clone); - - // reorder - $reorderer = Injector::inst()->create(ReorderElements::class, $clone); - $reorderer->reorder($elementID); - - return $clone; - } catch (Exception $e) { - throw new Exception("Something went wrong when duplicating element: $elementID"); - } - } - - public static function newTitle(string $title = ''): ?string - { - $hasCopyPattern = '/^.*(\scopy($|\s[0-9]+$))/'; - $hasNumPattern = '/^.*(\s[0-9]+$)/'; - $parts = []; - - // does $title end with 'copy' (ignoring numbers for now)? - if (preg_match($hasCopyPattern ?? '', $title ?? '', $parts)) { - $copy = $parts[1]; - // does $title end with numbers? - if (preg_match($hasNumPattern ?? '', $copy ?? '', $parts)) { - $num = trim($parts[1] ?? ''); - $len = strlen($num ?? ''); - $inc = (int)$num + 1; - return substr($title ?? '', 0, -$len) . "$inc"; - } else { - return $title . ' 2'; - } - } else { - return $title . ' copy'; - } - } - - public static function resolveSortBlock($object, array $args, $context, ResolveInfo $info) - { - $element = BaseElement::get()->byID($args['id']); - - if (!$element) { - throw new InvalidArgumentException(sprintf( - '%s#%s not found', - BaseElement::class, - $args['ID'] - )); - } - $member = UserContextProvider::get($context); - if (!$element->canEdit($member)) { - throw new InvalidArgumentException( - 'Changing the sort order of blocks is not allowed for the current user' - ); - } - - $reorderingService = Injector::inst()->create(ReorderElements::class, $element); - return $reorderingService->reorder($args['afterBlockID']); - } -} diff --git a/src/Models/BaseElement.php b/src/Models/BaseElement.php index 4f1f520e..8956b790 100644 --- a/src/Models/BaseElement.php +++ b/src/Models/BaseElement.php @@ -4,7 +4,6 @@ use DNADesign\Elemental\Controllers\ElementController; use DNADesign\Elemental\Forms\TextCheckboxGroupField; -use DNADesign\Elemental\ORM\FieldType\DBObjectType; use DNADesign\Elemental\Services\ReorderElements; use DNADesign\Elemental\TopPage\DataExtension; use Exception; @@ -19,8 +18,6 @@ use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\NumericField; use SilverStripe\Forms\TextField; -use SilverStripe\GraphQL\Scaffolding\StaticSchema; -use SilverStripe\GraphQL\Schema\Exception\SchemaBuilderException; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\FieldType\DBBoolean; use SilverStripe\ORM\FieldType\DBField; @@ -35,7 +32,6 @@ use SilverStripe\View\Requirements; use SilverStripe\ORM\CMSPreviewable; use SilverStripe\Core\Config\Config; -use SilverStripe\ORM\DataObjectSchema; use SilverStripe\ORM\ValidationResult; /** @@ -95,7 +91,6 @@ class BaseElement extends DataObject implements CMSPreviewable ]; private static $casting = [ - 'BlockSchema' => DBObjectType::class, 'IsLiveVersion' => DBBoolean::class, 'IsPublished' => DBBoolean::class, 'canCreate' => DBBoolean::class, @@ -1046,7 +1041,7 @@ public function getMimeType() /** * This can be overridden on child elements to create a summary for display in GridFields. * The react Summary component takes `content` (html) and/or `fileUrl` & `fileTitle` (image) props, - * which have to be added to the graphql output in `provideBlockSchema()`. + * which have to be added to the output in `provideBlockSchema()`. * * @return string */ @@ -1067,27 +1062,17 @@ public static function getBlockConfig() } /** - * The block actions is an associative array available for providing data to the client side to be used to describe - * actions that may be performed. This is available as a plain "ObjectType" in the GraphQL schema. - * - * By default the only action is "edit" which is simply the URL where the block may be edited. - * - * To modify the actions, either use the extension point or overload the `provideBlockSchema` method. - * - * @internal This API may change in future. Treat this as a `final` method. - * @return array + * Get the block schema data which will be sent to the editor client */ - public function getBlockSchema() + public function getBlockSchema(): array { $blockSchema = $this->provideBlockSchema(); - $this->extend('updateBlockSchema', $blockSchema); - return $blockSchema; } /** - * Provide block schema data, which will be serialised and sent via GraphQL to the editor client. + * Provide block schema data which will be sent to the editor client. * * Overload this method in child element classes to augment, or use the extension point on `getBlockSchema` * to update it from an `Extension`. @@ -1099,7 +1084,7 @@ public function getBlockSchema() protected function provideBlockSchema() { return [ - 'typeName' => static::getGraphQLTypeName(), + 'typeName' => str_replace('\\', '_', get_class($this)), 'actions' => [ 'edit' => $this->getEditLink(), ], @@ -1107,7 +1092,7 @@ protected function provideBlockSchema() ]; } - /** + /** * Generate markup for element type icons suitable for use in GridFields. * * @return null|DBHTMLText @@ -1270,17 +1255,9 @@ public function EvenOdd() /** * @return string - * @deprecated 5.3.0 Will be replaced with getTypeName() */ - public static function getGraphQLTypeName(): string + public function getTypeName(): string { - Deprecation::withNoReplacement(function () { - Deprecation::notice('5.3.0', 'Will be replaced with getTypeName()'); - }); - // For GraphQL 3, use the static schema type name - except for BaseElement for which this is inconsistent. - if (class_exists(StaticSchema::class) && static::class !== BaseElement::class) { - return StaticSchema::inst()->typeNameForDataObject(static::class); - } return str_replace('\\', '_', static::class); } diff --git a/src/ORM/FieldType/DBObjectType.php b/src/ORM/FieldType/DBObjectType.php deleted file mode 100644 index f4ade846..00000000 --- a/src/ORM/FieldType/DBObjectType.php +++ /dev/null @@ -1,33 +0,0 @@ -getType('ObjectType'); - } -} diff --git a/src/Services/ElementTypeRegistry.php b/src/Services/ElementTypeRegistry.php index 61be6e3a..dec1587e 100644 --- a/src/Services/ElementTypeRegistry.php +++ b/src/Services/ElementTypeRegistry.php @@ -48,11 +48,10 @@ public function registerElement($elementClass) } // Get the GraphQL type name - $typeName = $singleton->getGraphQLTypeName(); $this->elementTypes[] = [ 'icon' => $singleton::config()->get('icon'), - 'name' => $typeName, + 'name' => $singleton->getTypeName(), 'class' => $elementClass, 'title' => $singleton->getType(), 'inlineEditable' => $singleton->inlineEditable(), @@ -70,7 +69,7 @@ private function registerBrokenElement() $singleton = singleton(BaseElement::class); $this->elementTypes[] = [ 'icon' => 'font-icon-block', - 'name' => $singleton->getGraphQLTypeName(), + 'name' => $singleton->getTypeName(), 'class' => BaseElement::class, 'title' => '', 'inlineEditable' => false, diff --git a/tests/Blocks/TestElementContent.php b/tests/Blocks/TestElementContent.php index b2d49b47..014e2304 100644 --- a/tests/Blocks/TestElementContent.php +++ b/tests/Blocks/TestElementContent.php @@ -14,9 +14,14 @@ class TestElementContent extends ElementContent implements TestOnly public static $fail = ''; - public function canView($member = null) + public function canCreate($member = null, $context = []) { - return self::$fail !== 'can-view'; + return self::$fail !== 'can-create'; + } + + public function canDelete($member = null, $context = []) + { + return self::$fail !== 'can-delete'; } public function canEdit($member = null, $context = []) @@ -24,6 +29,21 @@ public function canEdit($member = null, $context = []) return self::$fail !== 'can-edit'; } + public function canView($member = null) + { + return self::$fail !== 'can-view'; + } + + public function canPublish($member = null) + { + return self::$fail !== 'can-publish'; + } + + public function canUnpublish($member = null) + { + return self::$fail !== 'can-unpublish'; + } + public function validate(): ValidationResult { $validationResult = parent::validate(); diff --git a/tests/Blocks/TestElementalArea.php b/tests/Blocks/TestElementalArea.php new file mode 100644 index 00000000..c8d6bd40 --- /dev/null +++ b/tests/Blocks/TestElementalArea.php @@ -0,0 +1,23 @@ +objFromFixture(TestElementContent::class, 'blocks_page_content'); + $element = $this->objFromFixture(TestElementContent::class, 'TestElementContent01'); $id = $element->ID; $url = "/admin/elemental-area/schema/elementForm/$id"; $headers = $this->getFormSchemaHeader(); @@ -81,7 +85,7 @@ public function testElementFormGetSchema( $this->assertSame('action_save', $formSchema['schema']['actions'][0]['name']); // state node is flattened, unlike schema node $this->assertSame('PageElements_' . $id. '_Title', $formSchema['state']['fields'][3]['name']); - $this->assertSame('My title', $formSchema['state']['fields'][3]['value']); + $this->assertSame('TestElementContent01 Title', $formSchema['state']['fields'][3]['value']); $this->assertFalse(array_key_exists('errors', $formSchema)); } @@ -151,7 +155,7 @@ public function provideElementFormPost(): array public function testElementFormPost(string $idType, string $dataType, string $fail, int $expectedCode): void { TestElementContent::$fail = $fail; - $element = $this->objFromFixture(TestElementContent::class, 'blocks_page_content'); + $element = $this->objFromFixture(TestElementContent::class, 'TestElementContent01'); $id = $element->ID; if ($idType === 'non-numeric') { $id = 'fish'; @@ -218,7 +222,7 @@ public function testElementFormPost(string $idType, string $dataType, string $fa $this->assertSame($expected, $formSchema['errors']); // assert database was not updated $element = TestElementContent::get()->byID($id); - $this->assertSame($element->Title, 'My title'); + $this->assertSame($element->Title, 'TestElementContent01 Title'); } else { // assert there are no errors $this->assertFalse(array_key_exists('errors', $formSchema)); @@ -248,7 +252,7 @@ public function provideElementFormReadonly(): array public function testElementFormReadonly(string $fail, bool $expected): void { TestElementContent::$fail = $fail; - $element = $this->objFromFixture(TestElementContent::class, 'blocks_page_content'); + $element = $this->objFromFixture(TestElementContent::class, 'TestElementContent01'); $id = $element->ID; $url = "/admin/elemental-area/schema/elementForm/$id"; $headers = $this->getFormSchemaHeader(); @@ -258,6 +262,719 @@ public function testElementFormReadonly(string $fail, bool $expected): void $this->assertSame($expected, $actual); } + public function provideApiDelete(): array + { + return [ + 'Valid' => [ + 'idType' => 'existing', + 'fail' => '', + 'expectedCode' => 204, + ], + 'Reject fail canDelete()' => [ + 'idType' => 'existing', + 'fail' => 'can-delete', + 'expectedCode' => 403, + ], + 'Reject fail csrf-token' => [ + 'idType' => 'existing', + 'fail' => 'csrf-token', + 'expectedCode' => 400, + ], + 'Reject invalid ID' => [ + 'idType' => 'invalid', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject missing ID' => [ + 'idType' => 'missing', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject non-numeric ID' => [ + 'idType' => 'non-numeric', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject new record ID' => [ + 'idType' => 'new-record', + 'fail' => '', + 'expectedCode' => 400, + ], + ]; + } + + /** + * @dataProvider provideApiDelete + */ + public function testApiDelete( + string $idType, + string $fail, + int $expectedCode + ): void { + TestElementContent::$fail = $fail; + $id = $this->getID($idType); + $fixtureID = $this->getElementFixture()->ID; + $url = "/admin/elemental-area/api/delete"; + $body = json_encode(['id' => $id]); + $headers = []; + if ($fail !== 'csrf-token') { + $headers = array_merge($headers, $this->getCsrfTokenheader()); + } + $response = $this->mainSession->sendRequest('POST', $url, [], $headers, null, $body); + $this->assertSame('application/json', $response->getHeader('Content-type')); + $this->assertSame($expectedCode, $response->getStatusCode()); + if ($expectedCode >= 400) { + $this->assertNotNull(TestElementContent::get()->byID($fixtureID)); + } else { + $this->assertNull(TestElementContent::get()->byID($fixtureID)); + } + } + + public function provideApiCreate(): array + { + return [ + 'Valid' => [ + 'fail' => '', + 'insertAfterElementID' => null, + 'expectedCode' => 204, + ], + 'Valid after ID' => [ + 'fail' => '', + 'insertAfterElementID' => 2, + 'expectedCode' => 204, + ], + 'Reject fail canCreate()' => [ + 'fail' => 'can-create', + 'insertAfterElementID' => null, + 'expectedCode' => 403, + ], + 'Reject fail elemental-area canEdit()' => [ + 'fail' => 'elemental-area-can-edit', + 'insertAfterElementID' => null, + 'expectedCode' => 403, + ], + 'Reject invalid after ID' => [ + 'fail' => '', + 'insertAfterElementID' => -1, + 'expectedCode' => 500, + ], + 'Reject fail csrf-token' => [ + 'fail' => 'csrf-token', + 'insertAfterElementID' => null, + 'expectedCode' => 400, + ], + 'Reject no subclass' => [ + 'fail' => 'no-subclass', + 'insertAfterElementID' => null, + 'expectedCode' => 400, + ], + 'Reject invalid subclass' => [ + 'fail' => 'invalid-subclass', + 'insertAfterElementID' => null, + 'expectedCode' => 400, + ], + 'Reject no area' => [ + 'fail' => 'no-area', + 'insertAfterElementID' => null, + 'expectedCode' => 400, + ], + 'Reject invalid area' => [ + 'fail' => 'invalid-area', + 'insertAfterElementID' => null, + 'expectedCode' => 400, + ], + ]; + } + + /** + * @dataProvider provideApiCreate + */ + public function testApiCreate( + string $fail, + ?int $insertAfterElementID, + int $expectedCode + ): void { + TestElementContent::$fail = $fail; + if (str_contains($fail, 'elemental-area')) { + TestElementalArea::$fail = str_replace('elemental-area-', '', $fail); + } + $url = '/admin/elemental-area/api/create'; + $headers = []; + if ($fail !== 'csrf-token') { + $headers = array_merge($headers, $this->getCsrfTokenheader()); + } + $elementClass = match ($fail) { + 'no-subclass' => '', + 'invalid-subclass' => TestElementalArea::class, + default => TestElementContent::class, + }; + $data = [ + 'elementClass' => $elementClass, + ]; + if ($fail !== 'no-area') { + $data['elementalAreaID'] = match ($fail) { + 'invalid-area' => TestElementalArea::get()->max('ID') + 1, + default => TestElementalArea::get()->first()->ID, + }; + } + if ($insertAfterElementID) { + $data['insertAfterElementID'] = $insertAfterElementID; + } + $body = json_encode($data); + $response = $this->mainSession->sendRequest('POST', $url, [], $headers, null, $body); + $this->assertSame('application/json', $response->getHeader('Content-type')); + $this->assertSame($expectedCode, $response->getStatusCode()); + if ($expectedCode >= 400) { + $expected = [ + 'TestElementContent01 Title' => 1, + 'TestElementContent02 Title' => 2, + 'TestElementContent03 Title' => 3, + ]; + } elseif ($insertAfterElementID === 2) { + $expected = [ + 'TestElementContent01 Title' => 1, + 'TestElementContent02 Title' => 2, + '' => 3, + 'TestElementContent03 Title' => 4, + ]; + } else { + $expected = [ + 'TestElementContent01 Title' => 1, + 'TestElementContent02 Title' => 2, + 'TestElementContent03 Title' => 3, + '' => 4, + ]; + } + $map = TestElementContent::get()->sort('Sort')->map('Title', 'Sort')->toArray(); + $this->assertSame($expected, $map); + } + + public function provideApiDuplicate(): array + { + return [ + 'Valid' => [ + 'idType' => 'existing', + 'fail' => '', + 'expectedCode' => 204, + ], + 'Reject fail canCreate()' => [ + 'idType' => 'existing', + 'fail' => 'can-create', + 'expectedCode' => 403, + ], + 'Reject fail elemental-area canEdit()' => [ + 'idType' => 'existing', + 'fail' => 'elemental-area-can-edit', + 'expectedCode' => 403, + ], + 'Reject fail csrf-token' => [ + 'idType' => 'existing', + 'fail' => 'csrf-token', + 'expectedCode' => 400, + ], + 'Reject invalid ID' => [ + 'idType' => 'invalid', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject missing ID' => [ + 'idType' => 'missing', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject non-numeric ID' => [ + 'idType' => 'non-numeric', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject new record ID' => [ + 'idType' => 'new-record', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Element no parent' => [ + 'idType' => 'existing', + 'fail' => 'no-parent', + 'expectedCode' => 400, + ], + ]; + } + + /** + * @dataProvider provideApiDuplicate + */ + public function testApiDuplicate( + string $idType, + string $fail, + int $expectedCode + ): void { + TestElementContent::$fail = $fail; + if (str_contains($fail, 'elemental-area')) { + TestElementalArea::$fail = str_replace('elemental-area-', '', $fail); + } + $id = $this->getID($idType); + $fixture = $this->getElementFixture(); + if ($fail == 'no-parent') { + $fixture->ParentID = 0; + $fixture->write(); + } + $url = '/admin/elemental-area/api/duplicate'; + $headers = []; + if ($fail !== 'csrf-token') { + $headers = array_merge($headers, $this->getCsrfTokenheader()); + } + $data = []; + if ($id !== -1) { + $data['id'] = $id; + } + $body = json_encode($data); + $response = $this->mainSession->sendRequest('POST', $url, [], $headers, null, $body); + $this->assertSame('application/json', $response->getHeader('Content-type')); + $this->assertSame($expectedCode, $response->getStatusCode()); + $newElement = TestElementContent::get()->filter('Title', 'TestElementContent01 Title copy')->first(); + if ($expectedCode >= 400) { + $this->assertNull($newElement); + } else { + $this->assertNotNull($newElement); + $expected = [ + 'TestElementContent01 Title' => 1, + 'TestElementContent01 Title copy' => 2, + 'TestElementContent02 Title' => 3, + 'TestElementContent03 Title' => 4, + ]; + $map = TestElementContent::get()->sort('Sort')->map('Title', 'Sort')->toArray(); + $this->assertSame($expected, $map); + } + } + + public function provideApiPublish(): array + { + return [ + 'Valid' => [ + 'idType' => 'existing', + 'fail' => '', + 'expectedCode' => 204, + ], + 'Reject fail canPublish()' => [ + 'idType' => 'existing', + 'fail' => 'can-publish', + 'expectedCode' => 403, + ], + 'Reject fail csrf-token' => [ + 'idType' => 'existing', + 'fail' => 'csrf-token', + 'expectedCode' => 400, + ], + 'Reject invalid ID' => [ + 'idType' => 'invalid', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject missing ID' => [ + 'idType' => 'missing', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject non-numeric ID' => [ + 'idType' => 'non-numeric', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject new record ID' => [ + 'idType' => 'new-record', + 'fail' => '', + 'expectedCode' => 400, + ], + ]; + } + + /** + * @dataProvider provideApiPublish + */ + public function testApiPublish( + string $idType, + string $fail, + int $expectedCode + ): void { + $fixture = $this->getElementFixture(); + try { + TestElementContent::$fail = $fail; + $id = $this->getID($idType); + $url = '/admin/elemental-area/api/publish'; + $headers = []; + if ($fail !== 'csrf-token') { + $headers = array_merge($headers, $this->getCsrfTokenheader()); + } + $data = []; + if ($id !== -1) { + $data['id'] = $id; + } + $body = json_encode($data); + $this->assertFalse($fixture->isPublished()); + $response = $this->mainSession->sendRequest('POST', $url, [], $headers, null, $body); + $this->assertSame('application/json', $response->getHeader('Content-type')); + $this->assertSame($expectedCode, $response->getStatusCode()); + if ($expectedCode >= 400) { + $this->assertFalse($fixture->isPublished()); + } else { + $this->assertTrue($fixture->isPublished()); + } + } finally { + // fixtures will remain published between tests, fixtures are normally unpublished + if ($fixture->isPublished()) { + $fixture->doUnpublish(); + } + } + } + + public function provideApiRead(): array + { + return [ + 'Valid' => [ + 'idType' => 'existing', + 'fail' => '', + 'expectedCode' => 200, + ], + 'Valid fail canView()' => [ + 'idType' => 'existing', + 'fail' => 'can-view', + 'expectedCode' => 200, + ], + 'Reject fail elemental-area canView()' => [ + 'idType' => 'existing', + 'fail' => 'elemental-area-can-view', + 'expectedCode' => 403, + ], + 'Reject invalid ID' => [ + 'idType' => 'invalid', + 'fail' => '', + 'expectedCode' => 404, + ], + 'Reject missing ID' => [ + 'idType' => 'missing', + 'fail' => '', + 'expectedCode' => 404, + ], + 'Reject non-numeric ID' => [ + 'idType' => 'non-numeric', + 'fail' => '', + 'expectedCode' => 404, + ], + 'Reject new record ID' => [ + 'idType' => 'new-record', + 'fail' => '', + 'expectedCode' => 404, + ], + ]; + } + + /** + * @dataProvider provideApiRead + */ + public function testApiRead( + string $idType, + string $fail, + int $expectedCode + ): void { + TestElementContent::$fail = $fail; + if (str_contains($fail, 'elemental-area')) { + TestElementalArea::$fail = str_replace('elemental-area-', '', $fail); + } + $id = $this->getID($idType, 'elemental-area'); + $url = "/admin/elemental-area/api/readElements/$id"; + $data = []; + if ($id !== -1) { + $data['id'] = $id; + } + $response = $this->mainSession->sendRequest('GET', $url, []); + $this->assertSame('application/json', $response->getHeader('Content-type')); + $this->assertSame($expectedCode, $response->getStatusCode()); + $body = $response->getBody(); + if ($expectedCode === 200) { + $editLinks = []; + foreach (TestElementContent::get() as $element) { + $editLinks[$element->ID] = $element->getEditLink(); + } + if ($fail == 'can-view') { + $expected = []; + } else { + $expected = [ + [ + 'id' => 1, + 'title' => 'TestElementContent01 Title', + 'blockSchema' => [ + 'typeName' => 'DNADesign_Elemental_Tests_Blocks_TestElementContent', + 'actions' => [ + 'edit' => $editLinks[1], + ], + 'content' => 'Some HTML', + ], + 'obsoleteClassName' => null, + 'version' => 1, + 'isPublished' => false, + 'isLiveVersion' => false, + 'canDelete' => true, + 'canPublish' => true, + 'canUnpublish' => true, + 'canCreate' => true, + ], + [ + 'id' => 2, + 'title' => 'TestElementContent02 Title', + 'blockSchema' => [ + 'typeName' => 'DNADesign_Elemental_Tests_Blocks_TestElementContent', + 'actions' => [ + 'edit' => $editLinks[2], + ], + 'content' => 'Some HTML', + ], + 'obsoleteClassName' => null, + 'version' => 1, + 'isPublished' => false, + 'isLiveVersion' => false, + 'canDelete' => true, + 'canPublish' => true, + 'canUnpublish' => true, + 'canCreate' => true, + ], + [ + 'id' => 3, + 'title' => 'TestElementContent03 Title', + 'blockSchema' => [ + 'typeName' => 'DNADesign_Elemental_Tests_Blocks_TestElementContent', + 'actions' => [ + 'edit' => $editLinks[3], + ], + 'content' => 'Some HTML', + ], + 'obsoleteClassName' => null, + 'version' => 1, + 'isPublished' => false, + 'isLiveVersion' => false, + 'canDelete' => true, + 'canPublish' => true, + 'canUnpublish' => true, + 'canCreate' => true, + ], + ]; + } + $this->assertSame($expected, json_decode($body, true)); + } + } + + public function provideApiSort(): array + { + return [ + 'Valid move from first' => [ + 'idType' => 'existing', + 'fail' => '', + 'afterBlockID' => 2, + 'expectedCode' => 204, + ], + 'Valid move to first' => [ + 'idType' => 'existing', + 'fail' => '', + 'afterBlockID' => 0, + 'expectedCode' => 204, + ], + 'Reject fail canEdit()' => [ + 'idType' => 'existing', + 'fail' => 'can-edit', + 'afterBlockID' => 2, + 'expectedCode' => 403, + ], + 'Reject fail csrf-token' => [ + 'idType' => 'existing', + 'fail' => 'csrf-token', + 'afterBlockID' => 2, + 'expectedCode' => 400, + ], + 'Reject invalid ID' => [ + 'idType' => 'invalid', + 'fail' => '', + 'afterBlockID' => 2, + 'expectedCode' => 400, + ], + 'Reject missing ID' => [ + 'idType' => 'missing', + 'fail' => '', + 'afterBlockID' => 2, + 'expectedCode' => 400, + ], + 'Reject non-numeric ID' => [ + 'idType' => 'non-numeric', + 'fail' => '', + 'afterBlockID' => 2, + 'expectedCode' => 400, + ], + 'Reject new record ID' => [ + 'idType' => 'new-record', + 'fail' => '', + 'afterBlockID' => 2, + 'expectedCode' => 400, + ], + ]; + } + + /** + * @dataProvider provideApiSort + */ + public function testApiSort( + string $idType, + string $fail, + int $afterBlockID, + int $expectedCode + ): void { + TestElementContent::$fail = $fail; + $id = $afterBlockID === 0 ? 2 : $this->getID($idType); + $fixture = $this->getElementFixture(); + $url = '/admin/elemental-area/api/sort'; + $headers = []; + if ($fail !== 'csrf-token') { + $headers = array_merge($headers, $this->getCsrfTokenheader()); + } + $data = [ + 'afterBlockID' => $afterBlockID, + ]; + if ($id !== -1) { + $data['id'] = $id; + } + $body = json_encode($data); + $response = $this->mainSession->sendRequest('POST', $url, [], $headers, null, $body); + $this->assertSame('application/json', $response->getHeader('Content-type')); + $this->assertSame($expectedCode, $response->getStatusCode()); + if ($expectedCode >= 400) { + $expected = [ + 'TestElementContent01 Title' => 1, + 'TestElementContent02 Title' => 2, + 'TestElementContent03 Title' => 3, + ]; + } elseif ($afterBlockID === 0) { + $expected = [ + 'TestElementContent01 Title' => 2, + 'TestElementContent02 Title' => 1, + 'TestElementContent03 Title' => 3, + ]; + } else { + $expected = [ + 'TestElementContent01 Title' => 2, + 'TestElementContent02 Title' => 1, + 'TestElementContent03 Title' => 3, + ]; + } + $map = TestElementContent::get()->sort('ID ASC')->map('Title', 'Sort')->toArray(); + $this->assertSame($expected, $map); + } + + public function provideApiUnpublish(): array + { + return [ + 'Valid' => [ + 'idType' => 'existing', + 'fail' => '', + 'expectedCode' => 204, + ], + 'Reject fail canUnpublish()' => [ + 'idType' => 'existing', + 'fail' => 'can-unpublish', + 'expectedCode' => 403, + ], + 'Reject fail csrf-token' => [ + 'idType' => 'existing', + 'fail' => 'csrf-token', + 'expectedCode' => 400, + ], + 'Reject invalid ID' => [ + 'idType' => 'invalid', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject missing ID' => [ + 'idType' => 'missing', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject non-numeric ID' => [ + 'idType' => 'non-numeric', + 'fail' => '', + 'expectedCode' => 400, + ], + 'Reject new record ID' => [ + 'idType' => 'new-record', + 'fail' => '', + 'expectedCode' => 400, + ], + ]; + } + + /** + * @dataProvider provideApiUnpublish + */ + public function testApiUnpublish( + string $idType, + string $fail, + int $expectedCode + ): void { + $fixture = $this->getElementFixture(); + $fixture->publishSingle(); + try { + TestElementContent::$fail = $fail; + $id = $this->getID($idType); + $url = '/admin/elemental-area/api/unpublish'; + $headers = []; + if ($fail !== 'csrf-token') { + $headers = array_merge($headers, $this->getCsrfTokenheader()); + } + $data = []; + if ($id !== -1) { + $data['id'] = $id; + } + $body = json_encode($data); + $this->assertTrue($fixture->isPublished()); + $response = $this->mainSession->sendRequest('POST', $url, [], $headers, null, $body); + $this->assertSame('application/json', $response->getHeader('Content-type')); + $this->assertSame($expectedCode, $response->getStatusCode()); + if ($expectedCode >= 400) { + $this->assertTrue($fixture->isPublished()); + } else { + $this->assertFalse($fixture->isPublished()); + } + } finally { + // fixtures will remain published between tests, fixtures are normally unpublished + if ($fixture->isPublished()) { + $fixture->doUnpublish(); + } + } + } + + private function getElementFixture(): TestElementContent + { + return $this->objFromFixture(TestElementContent::class, 'TestElementContent01'); + } + + private function getElementalAreaFixture(): TestElementalArea + { + return $this->objFromFixture(TestElementalArea::class, 'blocks_page_area'); + } + + private function getID(string $idType, string $type = 'element'): mixed + { + if ($type == 'element') { + $obj = $this->getElementFixture(); + } elseif ($type == 'elemental-area') { + $obj = $this->getElementalAreaFixture(); + } else { + throw new Exception('Invalid type'); + } + return match ($idType) { + 'existing' => $obj->ID, + 'invalid' => $obj->ID + 99999, + 'missing' => -1, + 'non-numeric' => 'fish', + 'new-record' => 0, + }; + } + private function getFormSchemaHeader(): array { return [ diff --git a/tests/Controllers/ElementalAreaControllerTest.yml b/tests/Controllers/ElementalAreaControllerTest.yml index 5e9b3247..7e31442a 100644 --- a/tests/Controllers/ElementalAreaControllerTest.yml +++ b/tests/Controllers/ElementalAreaControllerTest.yml @@ -1,4 +1,4 @@ -DNADesign\Elemental\Models\ElementalArea: +DNADesign\Elemental\Tests\Blocks\TestElementalArea: blocks_page_area: Title: Area 1 @@ -6,12 +6,27 @@ DNADesign\Elemental\Tests\Src\TestPage: blocks_page: Title: Content blocks page URLSegment: blocks-page - ElementalAreaID: =>DNADesign\Elemental\Models\ElementalArea.blocks_page_area + ElementalAreaID: =>DNADesign\Elemental\Tests\Blocks\TestElementalArea.blocks_page_area DNADesign\Elemental\Tests\Blocks\TestElementContent: - blocks_page_content: - Title: My title + TestElementContent01: + Title: TestElementContent01 Title HTML: Some HTML - ParentID: =>DNADesign\Elemental\Models\ElementalArea.blocks_page_area + ParentID: =>DNADesign\Elemental\Tests\Blocks\TestElementalArea.blocks_page_area MyInt: 123 MyEnum: Cloudy + Sort: 1 + TestElementContent02: + Title: TestElementContent02 Title + HTML: Some HTML + ParentID: =>DNADesign\Elemental\Tests\Blocks\TestElementalArea.blocks_page_area + MyInt: 456 + MyEnum: Cloudy + Sort: 2 + TestElementContent03: + Title: TestElementContent03 Title + HTML: Some HTML + ParentID: =>DNADesign\Elemental\Tests\Blocks\TestElementalArea.blocks_page_area + MyInt: 789 + MyEnum: Cloudy + Sort: 3 diff --git a/tests/GraphQL/AddElementToAreaMutationTest.php b/tests/GraphQL/AddElementToAreaMutationTest.php deleted file mode 100644 index 5504074d..00000000 --- a/tests/GraphQL/AddElementToAreaMutationTest.php +++ /dev/null @@ -1,95 +0,0 @@ -markTestSkipped('Skipped GraphQL 4 test ' . __CLASS__); - } - } - - public function testAddingBlocksInOrder() - { - $className = TestElement::class; - $areaID = $this->objFromFixture(ElementalArea::class, 'one')->ID; - - $one = $this->runMutation($className, $areaID)->ID; - $this->assertIdsInOrder([$one], 'The first element is added'); - - $two = $this->runMutation($className, $areaID, $one)->ID; - $this->assertIdsInOrder([$one, $two], 'The second element is added after the first'); - - $three = $this->runMutation($className, $areaID, $one)->ID; - $this->assertIdsInOrder([$one, $three, $two], 'The third element is added after the first'); - - $four = $this->runMutation($className, $areaID)->ID; - $this->assertIdsInOrder( - [$one, $three, $two, $four], - 'The fourth element is added last, when no after ID parameter is given' - ); - - $five = $this->runMutation($className, $areaID, 0)->ID; - $this->assertIdsInOrder([$five, $one, $three, $two, $four], 'The fifth element is added first, after ID 0'); - } - - public function testBadElementalArea() - { - $this->expectException(InvalidArgumentException::class); - $areaID = $this->objFromFixture(ElementalArea::class, 'one')->ID; - $this->runMutation(TestElement::class, $areaID + 1); - } - - public function testOrderingByWrongElementalArea() - { - $this->expectException(InvalidArgumentException::class); - $firstArea = ElementalArea::get()->first(); - $elementInFirstArea = TestElement::create(); - $firstArea->Elements()->add($elementInFirstArea); - - ElementalArea::create()->write(); - $this->runMutation(TestElement::class, 2, $elementInFirstArea->ID); - } - - protected function assertIdsInOrder($ids, $message = null) - { - $actualIDs = TestElement::get()->sort('Sort')->map()->keys(); - // Ideally this should be assertSame, but sometimes the database - // returns IDs as strings instead of integers (e.g. "1" instead of 1). - $this->assertEquals($ids, $actualIDs, $message); - } - - protected function runMutation($className, $elementalAreaID, $afterElementId = null) - { - $context = ['currentUser' => Security::getCurrentUser()]; - $resolveInfo = new FakeResolveInfo(); - - $args = [ - 'className' => $className, - 'elementalAreaID' => $elementalAreaID, - ]; - - if (!is_null($afterElementId)) { - $args['afterElementID'] = $afterElementId; - } - - return Resolver::resolveAddElementToArea(null, $args, $context, $resolveInfo); - } -} diff --git a/tests/GraphQL/AddElementToAreaMutationTest.yml b/tests/GraphQL/AddElementToAreaMutationTest.yml deleted file mode 100644 index 4f099556..00000000 --- a/tests/GraphQL/AddElementToAreaMutationTest.yml +++ /dev/null @@ -1,2 +0,0 @@ -DNADesign\Elemental\Models\ElementalArea: - one: \ No newline at end of file diff --git a/tests/GraphQL/DuplicateElementMutationTest.php b/tests/GraphQL/DuplicateElementMutationTest.php deleted file mode 100644 index 66a94b2d..00000000 --- a/tests/GraphQL/DuplicateElementMutationTest.php +++ /dev/null @@ -1,72 +0,0 @@ -markTestSkipped('Skipped GraphQL 4 test ' . __CLASS__); - } - } - - public function testResolvePermissions() - { - $cannotEditClass = new class extends BaseElement { - public function canEdit($member = null) - { - return false; - } - public function canCreate($member = null, $context = []) - { - return true; - } - }; - - $cannotCreateClass = new class extends BaseElement { - public function canEdit($member = null) - { - return true; - } - public function canCreate($member = null, $context = []) - { - return false; - } - }; - - $area = new ElementalArea(); - $area->write(); - - $testCases = [ - [$cannotEditClass, 'edit'], - [$cannotCreateClass, 'create'], - ]; - - foreach ($testCases as $data) { - [$class, $operation] = $data; - - $element = new $class(); - $element->ParentID = $area->ID; - $element->write(); - $mutation = new DuplicateElementMutation(); - $object = null; - $args = ['id' => $element->ID]; - $context = ['currentUser' => Security::getCurrentUser()]; - $resolveInfo = new FakeResolveInfo(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessageRegExp("#insufficient permission to {$operation}#"); - $mutation->resolve($object, $args, $context, $resolveInfo); - } - } -} diff --git a/tests/GraphQL/FakeResolveInfo.php b/tests/GraphQL/FakeResolveInfo.php deleted file mode 100644 index 8618ace6..00000000 --- a/tests/GraphQL/FakeResolveInfo.php +++ /dev/null @@ -1,28 +0,0 @@ - 'fake', 'type' => Type::string()]), - new \ArrayObject(), - new ObjectType(['name' => 'fake']), - [], - new Schema([]), - [], - '', - new OperationDefinitionNode([]), - [] - ); - } -} diff --git a/tests/GraphQL/SortBlockMutationCreatorTest.php b/tests/GraphQL/SortBlockMutationCreatorTest.php deleted file mode 100644 index 8c50499c..00000000 --- a/tests/GraphQL/SortBlockMutationCreatorTest.php +++ /dev/null @@ -1,64 +0,0 @@ -markTestSkipped('Skipped GraphQL 4 test ' . __CLASS__); - } - } - - /** - * Reorders blocks by compounding each result into the next test (rather than isolated sorting tests) - */ - public function testSortBlock() - { - $this->runMutation(1, 3); - $this->assertIdsInOrder([2, 3, 1, 4]); - - $this->runMutation(4, 2); - $this->assertIdsInOrder([2, 4, 3, 1]); - - $this->runMutation(1, 0); - $this->assertIdsInOrder([1, 2, 4, 3]); - - $this->runMutation(3, 2); - $this->assertIdsInOrder([1, 2, 3, 4]); - } - - protected function assertIdsInOrder($ids) - { - $actualIDs = TestElement::get()->sort('Sort')->map()->keys(); - - $this->assertSame($ids, $actualIDs); - } - - protected function runMutation($id, $afterBlockId) - { - $member = Security::getCurrentUser(); - - $context = ['currentUser' => $member]; - $resolveInfo = new FakeResolveInfo(); - - Resolver::resolveSortBlock(null, [ - 'id' => $id, - 'afterBlockID' => $afterBlockId, - ], $context, $resolveInfo); - } -} diff --git a/tests/GraphQL/SortBlockMutationCreatorTest.yml b/tests/GraphQL/SortBlockMutationCreatorTest.yml deleted file mode 100644 index d54cb617..00000000 --- a/tests/GraphQL/SortBlockMutationCreatorTest.yml +++ /dev/null @@ -1,13 +0,0 @@ -DNADesign\Elemental\Tests\Src\TestElement: - element1: - ID: 1 - Sort: 1 - element2: - ID: 2 - Sort: 2 - element3: - ID: 3 - Sort: 3 - element4: - ID: 4 - Sort: 4 diff --git a/tests/Reports/ElementsInUseReportTest.php b/tests/Reports/ElementsInUseReportTest.php index 2f22432e..4b8871ff 100644 --- a/tests/Reports/ElementsInUseReportTest.php +++ b/tests/Reports/ElementsInUseReportTest.php @@ -9,7 +9,6 @@ use DNADesign\Elemental\Tests\Src\TestElement; use DNADesign\Elemental\Tests\Src\TestPage; use SilverStripe\Dev\FunctionalTest; -use SilverStripe\GraphQL\Tests\Schema\NaiveSchemaBuilder; use SilverStripe\ORM\DataList; class ElementsInUseReportTest extends FunctionalTest @@ -27,26 +26,6 @@ class ElementsInUseReportTest extends FunctionalTest TestPage::class, ]; - protected function setUp(): void - { - parent::setUp(); - - // GraphQL 4 only - if (class_exists(NaiveSchemaBuilder::class)) { - NaiveSchemaBuilder::activate(); - } - } - - protected function tearDown(): void - { - parent::tearDown(); - - // GraphQL 4 only - if (class_exists(NaiveSchemaBuilder::class)) { - NaiveSchemaBuilder::deactivate(); - } - } - public function testReportShowsElementsInUse() { $this->logInWithPermission('ADMIN'); diff --git a/yarn.lock b/yarn.lock index 8ac31e84..5e290f44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,26 +15,6 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@apollo/client@^3.7.1": - version "3.9.9" - resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.9.9.tgz#38f983a1ad24e2687abfced0a9c1c3bef8d32616" - integrity sha512-/sMecU/M0WK9knrguts1lSLV8xFKzIgOMVb4mi6MOxgJXjliDB8PvOtmXhTqh2cVMMR4TzXgOnb+af/690zlQw== - dependencies: - "@graphql-typed-document-node/core" "^3.1.1" - "@wry/caches" "^1.0.0" - "@wry/equality" "^0.5.6" - "@wry/trie" "^0.5.0" - graphql-tag "^2.12.6" - hoist-non-react-statics "^3.3.2" - optimism "^0.18.0" - prop-types "^15.7.2" - rehackt "0.0.6" - response-iterator "^0.2.6" - symbol-observable "^4.0.0" - ts-invariant "^0.10.3" - tslib "^2.3.0" - zen-observable-ts "^1.2.5" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2": version "7.24.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" @@ -1110,11 +1090,6 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@graphql-typed-document-node/core@^3.1.1": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" - integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== - "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -1622,10 +1597,10 @@ stylelint-config-standard "^36.0.0" stylelint-scss "^6.2.1" -"@silverstripe/webpack-config@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@silverstripe/webpack-config/-/webpack-config-2.1.0.tgz#9d6409498126db766299fa40f209f618515932a4" - integrity sha512-8rE2K3AXkIP6crDTBXdHz/3hiNVp2XpINkWGDBKzvMS6sGRcVTluPs12BdOXYo1aUZEeqDk9QYOLLGGgZxnOdw== +"@silverstripe/webpack-config@^3.0.0-alpha1": + version "3.0.0-alpha1" + resolved "https://registry.yarnpkg.com/@silverstripe/webpack-config/-/webpack-config-3.0.0-alpha1.tgz#9576b8b1deac92ea96636e149b051b513ce14ab6" + integrity sha512-xBsiewBVAUIGdshWSs8ChAg4JXNNpHMwBLhjhYJCPFKe2HpNx3OqFEIqjR9YgSbCVSxkNh4x5yvYklu6+k+YpA== dependencies: "@babel/core" "^7.19.6" "@babel/preset-env" "^7.19.4" @@ -2007,41 +1982,6 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== -"@wry/caches@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@wry/caches/-/caches-1.0.1.tgz#8641fd3b6e09230b86ce8b93558d44cf1ece7e52" - integrity sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA== - dependencies: - tslib "^2.3.0" - -"@wry/context@^0.7.0": - version "0.7.4" - resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.7.4.tgz#e32d750fa075955c4ab2cfb8c48095e1d42d5990" - integrity sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ== - dependencies: - tslib "^2.3.0" - -"@wry/equality@^0.5.6": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.5.7.tgz#72ec1a73760943d439d56b7b1e9985aec5d497bb" - integrity sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw== - dependencies: - tslib "^2.3.0" - -"@wry/trie@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.4.3.tgz#077d52c22365871bf3ffcbab8e95cb8bc5689af4" - integrity sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w== - dependencies: - tslib "^2.3.0" - -"@wry/trie@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.5.0.tgz#11e783f3a53f6e4cd1d42d2d1323f5bc3fa99c94" - integrity sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA== - dependencies: - tslib "^2.3.0" - "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -2673,9 +2613,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001565: - version "1.0.30001636" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78" - integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg== + version "1.0.30001642" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz#6aa6610eb24067c246d30c57f055a9d0a7f8d05f" + integrity sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA== caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599: version "1.0.30001603" @@ -3306,9 +3246,9 @@ duplexer@^0.1.2: integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== electron-to-chromium@^1.4.601: - version "1.4.810" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.810.tgz#7dee01b090b9e048e6db752f7b30921790230654" - integrity sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ== + version "1.4.828" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.828.tgz#a1ee8cd8847448b2898d3f2d9db02113f9c5b35c" + integrity sha512-QOIJiWpQJDHAVO4P58pwb133Cwee0nbvy/MV1CwzZVGpkH1RX33N3vsaWRCpR6bF63AAq366neZrRTu7Qlsbbw== electron-to-chromium@^1.4.668: version "1.4.722" @@ -4196,18 +4136,6 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -graphql-tag@^2.12.6: - version "2.12.6" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" - integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg== - dependencies: - tslib "^2.1.0" - -graphql@^16.8.1: - version "16.8.1" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" - integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== - gud@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" @@ -6266,16 +6194,6 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -optimism@^0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.18.0.tgz#e7bb38b24715f3fdad8a9a7fc18e999144bbfa63" - integrity sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ== - dependencies: - "@wry/caches" "^1.0.0" - "@wry/context" "^0.7.0" - "@wry/trie" "^0.4.3" - tslib "^2.3.0" - optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -6651,7 +6569,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -6972,11 +6890,6 @@ regjsparser@^0.9.1: dependencies: jsesc "~0.5.0" -rehackt@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/rehackt/-/rehackt-0.0.6.tgz#7a0a2247f2295e7548915417e44fbbf03bf004f4" - integrity sha512-l3WEzkt4ntlEc/IB3/mF6SRgNHA6zfQR7BlGOgBTOmx7IJJXojDASav+NsgXHFjHn+6RmwqsGPFgZpabWpeOdw== - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -7053,11 +6966,6 @@ resolve@^2.0.0-next.5: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -response-iterator@^0.2.6: - version "0.2.6" - resolved "https://registry.yarnpkg.com/response-iterator/-/response-iterator-0.2.6.tgz#249005fb14d2e4eeb478a3f735a28fd8b4c9f3da" - integrity sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw== - retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -7635,11 +7543,6 @@ symbol-observable@^1.0.4: resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== -symbol-observable@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" - integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== - symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -7757,13 +7660,6 @@ treeverse@^2.0.0: resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-2.0.0.tgz#036dcef04bc3fd79a9b79a68d4da03e882d8a9ca" integrity sha512-N5gJCkLu1aXccpOTtqV6ddSEi6ZmGkh3hjmbu1IjcavJK4qyOVQmi0myQKM7z5jVGmD68SJoliaVrMmVObhj6A== -ts-invariant@^0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.10.3.tgz#3e048ff96e91459ffca01304dbc7f61c1f642f6c" - integrity sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ== - dependencies: - tslib "^2.1.0" - tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" @@ -7774,11 +7670,6 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.1.0, tslib@^2.3.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -8344,15 +8235,3 @@ yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== - -zen-observable-ts@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58" - integrity sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg== - dependencies: - zen-observable "0.8.15" - -zen-observable@0.8.15: - version "0.8.15" - resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" - integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==