Compare commits

...

2 Commits

Author SHA1 Message Date
Guy Sartorelli
7221223567
Merge 91cfcb8229 into bd48b04731 2024-10-17 03:40:21 +00:00
Guy Sartorelli
91cfcb8229
API Make CMSMain more generic
Remove hardcoded references to pages and SiteTree
Remove assumption that records are versioned
Remove or validate assumptions about methods on the model class
Improve general architecture of CMSMain
2024-10-17 16:40:06 +13:00
30 changed files with 833 additions and 806 deletions

View File

@ -2,7 +2,6 @@
use SilverStripe\Admin\CMSMenu;
use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\CMS\Controllers\CMSPageAddController;
use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Controllers\CMSPageSettingsController;
use SilverStripe\CMS\Model\SiteTree;
@ -35,4 +34,3 @@ ShortcodeParser::get('default')->register(
CMSMenu::remove_menu_class(CMSMain::class);
CMSMenu::remove_menu_class(CMSPageEditController::class);
CMSMenu::remove_menu_class(CMSPageSettingsController::class);
CMSMenu::remove_menu_class(CMSPageAddController::class);

View File

@ -4,10 +4,10 @@ After:
- '#corecache'
---
SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.CMSMain_SiteTreeHints:
Psr\SimpleCache\CacheInterface.CMSMain_TreeHints:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "CMSMain_SiteTreeHints"
namespace: "CMSMain_TreeHints"
Psr\SimpleCache\CacheInterface.SiteTree_CreatableChildren:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
@ -15,4 +15,4 @@ SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.SiteTree_PageIcons:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "SiteTree_PageIcons"
namespace: "SiteTree_PageIcons"

View File

@ -1 +1 @@
!function(){"use strict";var e={38:function(e,t,n){var a=i(n(420)),o=i(n(121));function i(e){return e&&e.__esModule?e:{default:e}}window.document.addEventListener("DOMContentLoaded",(()=>{(0,o.default)(),(0,a.default)()}))},121:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=i(n(207)),o=i(n(269));function i(e){return e&&e.__esModule?e:{default:e}}t.default=()=>{a.default.component.register("AnchorSelectorField",o.default)}},420:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=r(n(207)),o=n(367),i=r(n(796));function r(e){return e&&e.__esModule?e:{default:e}}t.default=()=>{a.default.reducer.register("cms",(0,o.combineReducers)({anchorSelector:i.default}))}},269:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.ConnectedAnchorSelectorField=t.Component=void 0;var a=C(n(815)),o=C(n(594)),i=C(n(950)),r=n(40),s=n(367),l=n(381),d=C(n(898)),u=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=_(t);if(n&&n.has(e))return n.get(e);var a={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var i in e)if("default"!==i&&{}.hasOwnProperty.call(e,i)){var r=o?Object.getOwnPropertyDescriptor(e,i):null;r&&(r.get||r.set)?Object.defineProperty(a,i,r):a[i]=e[i]}return a.default=e,n&&n.set(e,a),a}(n(979)),c=C(n(996)),f=C(n(623)),h=C(n(315)),p=C(n(657)),m=C(n(432)),g=C(n(304)),v=C(n(935));function _(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(_=function(e){return e?n:t})(e)}function C(e){return e&&e.__esModule?e:{default:e}}const b=()=>null;class w extends d.default{constructor(e){super(e),this.handleChange=this.handleChange.bind(this),this.handleLoadingError=this.handleLoadingError.bind(this)}componentDidMount(){this.ensurePagesLoaded()}componentDidUpdate(e){this.props.pageId!==e.pageId&&this.ensurePagesLoaded()}ensurePagesLoaded(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.props;if(e.loadingState===c.default.UPDATING||e.loadingState===c.default.SUCCESS||!e.pageId)return Promise.resolve();let t=[];e.loadingState===c.default.FIELD_ONLY&&(t=this.props.anchors),e.actions.anchorSelector.beginUpdating(e.pageId);const n=e.data.endpoint.replace(/:id/,e.pageId);return(0,i.default)(n,{credentials:"same-origin"}).then((e=>e.json())).then((n=>{const a=[...new Set([...n,...t])];return e.actions.anchorSelector.updated(e.pageId,a),a})).catch((t=>{e.actions.anchorSelector.updateFailed(e.pageId),this.handleLoadingError(t,e)}))}getDropdownOptions(){const e=this.props.anchors.map((e=>({value:e})));return this.props.value&&!this.props.anchors.find((e=>e===this.props.value))&&e.unshift({value:this.props.value}),e}handleChange(e){"function"==typeof this.props.onChange&&this.props.onChange(e?e.value:"")}handleLoadingError(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.props;if(t.onLoadingError===b)throw e;return t.onLoadingError({errors:[{value:e.message,type:"error"}]})}render(){const{extraClass:e,CreatableSelectComponent:t}=this.props,n=(0,g.default)("anchorselectorfield",e),i=this.getDropdownOptions(),r=this.props.value||"",s=a.default._t("CMS.ANCHOR_SELECT_OR_TYPE","Select or enter anchor");return o.default.createElement(p.default,null,o.default.createElement(t,{isSearchable:!0,isClearable:!0,options:i,className:n,name:this.props.name,onChange:this.handleChange,value:{value:r},noOptionsMessage:()=>a.default._t("CMS.ANCHOR_NO_OPTIONS","No options"),placeholder:s,getOptionLabel:e=>{let{value:t}=e;return t},classNamePrefix:"anchorselectorfield"}))}}t.Component=w,w.propTypes={extraClass:v.default.string,id:v.default.string,name:v.default.string.isRequired,onChange:v.default.func,value:v.default.string,attributes:v.default.oneOfType([v.default.object,v.default.array]),pageId:v.default.number,anchors:v.default.array,loadingState:v.default.oneOf(Object.keys(c.default).map((e=>c.default[e]))),onLoadingError:v.default.func,data:v.default.shape({endpoint:v.default.string,targetFieldName:v.default.string})},w.defaultProps={value:"",extraClass:"",onLoadingError:b,attributes:{},CreatableSelectComponent:h.default};const S=t.ConnectedAnchorSelectorField=(0,r.connect)((function(e,t){const n=(0,l.formValueSelector)(t.formid,m.default),a=t&&t.data&&t.data.targetFieldName||"PageID",o=Number(n(e,a)||0);let i=[];const r=o?e.cms.anchorSelector.pages.find((e=>e.id===o)):null;!r||r.loadingState!==c.default.SUCCESS&&r.loadingState!==c.default.DIRTY&&r.loadingState!==c.default.FIELD_ONLY||(i=r.anchors);let s=null;return s=r?r.loadingState:o?c.default.DIRTY:c.default.SUCCESS,{pageId:o,anchors:i,loadingState:s}}),(function(e){return{actions:{anchorSelector:(0,s.bindActionCreators)(u,e)}}}))(w);t.default=(0,f.default)(S)},586:function(e,t,n){var a;((a=n(669))&&a.__esModule?a:{default:a}).default.entwine("ss",(function(e){e(".TreeDropdownField").entwine({OldValue:null}),e("#Form_AddForm_ParentID_Holder .treedropdownfield").entwine({onmatch(){this._super(),e(".cms-add-form").updateTypeList()}}),e(".cms-add-form .parent-mode :input").entwine({onclick:function(e){var t=this.closest("form").find("#Form_AddForm_ParentID_Holder .TreeDropdownField");"top"==this.val()?(t.setOldValue(t.getValue()),t.setValue(0)):(t.setValue(t.getOldValue()||0),t.setOldValue(null)),t.refresh(),t.trigger("change")}}),e(".cms-add-form").entwine({ParentCache:{},onadd:function(){var t=this;this.find("#Form_AddForm_ParentID_Holder .TreeDropdownField").on("change",(function(){t.updateTypeList()})),this.find(".SelectionGroup.parent-mode").on("change",(function(){t.updateTypeList()})),"top"==e(".cms-add-form .parent-mode :input").val()&&this.updateTypeList()},loadCachedChildren:function(e){var t=this.getParentCache();return void 0!==t[e]?t[e]:null},saveCachedChildren:function(e,t){var n=this.getParentCache();n[e]=t,this.setParentCache(n)},updateTypeList:function(){var t=this.data("hints"),n=this.find("#Form_AddForm_ParentID"),a=this.find("input[name=ParentModeField]:checked").val(),o=n.data("metadata"),i="child"===a?n.getValue():null,r=o?o.ClassName:null,s=r&&"child"===a&&i?r:"Root",l=void 0!==t[s]?t[s]:null,d=this,u=l&&void 0!==l.defaultChild?l.defaultChild:null,c=[];if(i){if(this.hasClass("loading"))return;return this.addClass("loading"),null!==(c=this.loadCachedChildren(i))?(this.updateSelectionFilter(c,u),void this.removeClass("loading")):(e.ajax({url:d.data("childfilter"),data:{ParentID:i},success:function(e){d.saveCachedChildren(i,e),d.updateSelectionFilter(e,u)},complete:function(){d.removeClass("loading")}}),!1)}c=l&&void 0!==l.disallowedChildren?l.disallowedChildren:[],this.updateSelectionFilter(c,u)},updateSelectionFilter:function(t,n){var a=this.find("#Form_AddForm_PageType div.radio.selected")[0],o=!1,i=null;if(this.find("#Form_AddForm_PageType div.radio").each((function(n,r){var s=e(this).find("input").val(),l=-1===e.inArray(s,t);r===a&&l&&(o=!0),e(this).setEnabled(l),l||e(this).setSelected(!1),i=(null===i||i)&&l})),o)var r=e(a).parents("li:first");else if(n)r=this.find("#Form_AddForm_PageType div.radio input[value="+n+"]").parents("li:first");else r=this.find("#Form_AddForm_PageType div.radio:not(.disabled):first");r.setSelected(!0),r.siblings().setSelected(!1),this.find("#Form_AddForm_PageType div.radio:not(.disabled)").length?this.find("button[name=action_doAdd]").removeAttr("disabled"):this.find("button[name=action_doAdd]").attr("disabled","disabled"),this.find(".message-restricted")[i?"hide":"show"]()}}),e(".cms-add-form #Form_AddForm_PageType div.radio").entwine({onclick:function(e){this.setSelected(!0)},setSelected:function(e){var t=this.find("input");e&&!t.is(":disabled")?(this.siblings().setSelected(!1),this.toggleClass("selected",!0),t.prop("checked",!0)):(this.toggleClass("selected",!1),t.prop("checked",!1))},setEnabled:function(t){e(this).toggleClass("disabled",!t),t?e(this).find("input").removeAttr("disabled"):e(this).find("input").attr("disabled","disabled").removeAttr("checked")}}),e(".cms-content-addpage-button").entwine({onclick:function(t){var n,a=e(".cms-tree"),o=e(".cms-list"),i=0;if(a.is(":visible")){var r=a.jstree("get_selected");i=r?e(r[0]).data("id"):null}else{var s=o.find('input[name="Page[GridState]"]').val();s&&(i=parseInt(JSON.parse(s).ParentID,10))}var l,d={selector:this.data("targetPanel"),pjax:this.data("pjax")};i?(n=this.data("extraParams")?this.data("extraParams"):"",l=e.path.addSearchParams(i18n.sprintf(this.data("urlAddpage"),i),n)):l=this.attr("href"),e(".cms-container").loadPanel(l,null,d),t.preventDefault(),this.blur()}})}))},677:function(e,t,n){var a=r(n(669)),o=r(n(815)),i=r(n(216));function r(e){return e&&e.__esModule?e:{default:e}}a.default.entwine("ss",(function(e){e(".cms-edit-form :input#Form_EditForm_ClassName").entwine({onchange:function(){alert(o.default._t("CMS.ALERTCLASSNAME"))}}),e(".cms-edit-form input[name=Title]").entwine({onmatch:function(){var t=this;t.data("OrigVal",t.val());var n=t.closest("form"),a=e("input:text[name=URLSegment]",n),o=e("input[name=LiveLink]",n);a.length>0&&(t._addActions(),this.on("change",(function(n){var i=t.data("OrigVal"),r=t.val();t.data("OrigVal",r),0===a.val().indexOf(a.data("defaultUrl"))&&""==o.val()?t.updateURLSegment(r):e(".update",t.parent()).show().parent(".form__field-holder").addClass("input-group"),t.updateRelatedFields(r,i),t.updateBreadcrumbLabel(r)}))),this._super()},onunmatch:function(){this._super()},updateRelatedFields:function(t,n){this.parents("form").find("input[name=MetaTitle], input[name=MenuTitle]").each((function(){var a=e(this);a.val()==n&&(a.val(t),a.updatedRelatedFields&&a.updatedRelatedFields())}))},updateURLSegment:function(t){var n=e("input:text[name=URLSegment]",this.closest("form")).closest(".field.urlsegment"),a=e(".update",this.parent());n.update(t),a.is(":visible")&&a.hide().parent(".form__field-holder").removeClass("input-group")},updateBreadcrumbLabel:function(t){e(".cms-edit-form input[name=ID]").val();var n=e("span.cms-panel-link.crumb");t&&""!=t&&n.text(t)},_addActions:function(){var t,n=this;(t=e("<button />",{class:"update btn btn-outline-secondary form__field-update-url",text:o.default._t("CMS.UpdateURL"),type:"button",click:function(e){e.preventDefault(),n.updateURLSegment(n.val())}})).insertAfter(n),t.hide()}}),e(".cms-edit-form .parentTypeSelector").entwine({onmatch:function(){var e=this;this.find(":input[name=ParentType]").on("click",(function(t){e._toggleSelection(t)})),this.find(".TreeDropdownField").on("change",(function(t){e._changeParentId(t)})),this._changeParentId(),this._toggleSelection(),this._super()},onunmatch:function(){this._super()},_toggleSelection:function(t){var n=this.find(":input[name=ParentType]:checked").val(),a=this.find("#Form_EditForm_ParentID_Holder");"root"==n?this.find(":input[name=ParentID]").val(0):this.find(":input[name=ParentID]").val(this.find("#Form_EditForm_ParentType_subpage").data("parentIdValue")),"root"!=n?a.slideDown(400,(function(){e(this).css("overflow","visible")})):a.slideUp()},_changeParentId:function(e){var t=this.find(":input[name=ParentID]").val();this.find("#Form_EditForm_ParentType_subpage").data("parentIdValue",t)}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_doRollback, .cms-edit-form .btn-toolbar #Form_EditForm_action_rollback").entwine({onclick:function(e){if(this.is(":disabled"))return e.preventDefault(),!1;const t=this.parents("form:first").find(":input[name=Version]").val(),n=t?o.default.sprintf(o.default._t("CMS.RollbackToVersion","Do you really want to roll back to version #%s of this page?"),t):o.default._t("CMS.ConfirmRestoreFromLive","Are you sure you want to revert draft to when the page was last published?");return confirm(n)?(this.parents("form:first").addClass("loading"),this._super(e)):(e.preventDefault(),!1)}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_archive:not(.homepage-warning)").entwine({onclick:function(e){var t;return t=this.parents("form:first").find("input[name=ArchiveWarningMessage]").val().replace(/\\n/g,"\n"),!!confirm(t)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_restore").entwine({onclick:function(e){var t,n=this.parents("form:first").find(":input[name=Version]").val(),a=this.data("toRoot");return t=o.default.sprintf(o.default._t(a?"CMS.RestoreToRoot":"CMS.Restore"),n),!!confirm(t)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_unpublish:not(.homepage-warning)").entwine({onclick:function(e){var t,n=this.parents("form:first").find(":input[name=Version]").val();return t=o.default.sprintf(o.default._t("CMS.Unpublish"),n),!!confirm(t)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form.changed").entwine({onmatch:function(t){this.find("button[data-text-alternate]").each((function(){const t=e(this),n=t.find(".btn__title"),a=t.data("textAlternate");a&&(t.data("textStandard",n.text()),n.text(a));const o=t.data("btnAlternate");o&&(t.data("btnStandard",t.attr("class")),t.attr("class",o),t.removeClass("btn-outline-secondary").addClass("btn-primary"));const i=t.data("btnAlternateAdd");i&&t.addClass(i);const r=t.data("btnAlternateRemove");r&&t.removeClass(r)})),this._super(t)},onunmatch:function(t){this.find("button[data-text-alternate]").each((function(){const t=e(this),n=t.find(".btn__title"),a=t.data("textStandard");a&&n.text(a);const o=t.data("btnStandard");o&&(t.attr("class",o),t.addClass("btn-outline-secondary").removeClass("btn-primary"));const i=t.data("btnAlternateAdd");i&&t.removeClass(i);const r=t.data("btnAlternateRemove");r&&t.addClass(r)})),this._super(t)}}),e(".cms-edit-form .btn-toolbar button[name=action_publish]").entwine({onbuttonafterrefreshalternate:function(){this.data("showingAlternate")?(this.addClass("btn-primary"),this.removeClass("btn-secondary")):(this.removeClass("btn-primary"),this.addClass("btn-secondary"))}}),e(".cms-edit-form .btn-toolbar button[name=action_save]").entwine({onbuttonafterrefreshalternate:function(){this.data("showingAlternate")?(this.addClass("btn-primary"),this.removeClass("btn-secondary")):(this.removeClass("btn-primary"),this.addClass("btn-secondary"))}}),e('.cms-edit-form.CMSPageSettingsController input[name="ParentType"]:checked').entwine({onmatch:function(){this.redraw(),this._super()},onunmatch:function(){this._super()},redraw:function(){var t=e(".cms-edit-form.CMSPageSettingsController #Form_EditForm_ParentID_Holder");"Form_EditForm_ParentType_root"==e(this).attr("id")?t.slideUp():t.slideDown()},onclick:function(){this.redraw()}}),"Form_EditForm_ParentType_root"==e('.cms-edit-form.CMSPageSettingsController input[name="ParentType"]:checked').attr("id")&&e(".cms-edit-form.CMSPageSettingsController #Form_EditForm_ParentID_Holder").hide();var t=!1;e(".cms-edit-form .btn-toolbar #Form_EditForm_action_unpublish.homepage-warning,.cms-edit-form .btn-toolbar #Form_EditForm_action_archive.homepage-warning,#Form_EditForm_URLSegment_Holder.homepage-warning .btn.update").entwine({onclick:async function(e){if(t)return this._super(e);e.stopPropagation();var n=o.default._t("CMS.RemoveHomePageWarningMessage","Warning: This page is the home page. By changing the URL segment visitors will not be able to view it.");return await(0,i.default)({title:o.default._t("CMS.RemoveHomePageWarningTitle","Remove your home page?"),message:n,confirmText:o.default._t("CMS.RemoveHomePageWarningLabel","Remove"),confirmColor:"danger"})&&(t=!0,this.trigger("click"),t=!1),!1}})}))},55:function(e,t,n){var a=s(n(669)),o=s(n(815)),i=s(n(216)),r=n(125);function s(e){return e&&e.__esModule?e:{default:e}}a.default.entwine("ss.tree",(function(e){e(".cms-tree").entwine({fromDocument:{"oncontext_show.vakata":function(e){this.adjustContextClass()}},adjustContextClass:function(){var t=e("#vakata-contextmenu").find("ul ul");t.each((function(n){var a="1",o=e(t[n]).find("li").length;o>20?a="3":o>10&&(a="2"),e(t[n]).addClass("vakata-col-"+a).removeClass("right"),e(t[n]).find("li").on("mouseenter",(function(t){e(this).parent("ul").removeClass("right")}))}))},showListViewFor:function(t){localStorage.setItem("ss.pages-view-type","listview");const n=this.closest(".cms-content-view").data("url-listviewroot"),a=e.path.addSearchParams(n,{ParentID:t}),o=e("base").attr("href")||"";window.location.assign((0,r.joinUrlPaths)(o,a))},getTreeConfig:function(){var t=this,n=this._super();this.getHints();return n.plugins.push("contextmenu"),n.contextmenu={items:function(n){var a={edit:{label:n.hasClass("edit-disabled")?o.default._t("CMS.EditPage","Edit page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"):o.default._t("CMS.ViewPage","View page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(o.default.sprintf(t.data("urlEditpage"),n.data("id")))}}};n.hasClass("nochildren")||(a.showaslist={label:o.default._t("CMS.ShowAsList"),action:function(e){t.showListViewFor(e.data("id"))}});n.data("pagetype");var i=n.data("id"),r=n.find(">a .item").data("allowedchildren"),s={},l=!1;return e.each(r,(function(n,a){l=!0,s["allowedchildren-"+a.ClassName]={label:'<span class="jstree-pageicon '+a.IconClass+'"></span>'+a.Title,_class:"class-"+a.ClassName.replace(/[^a-zA-Z0-9\-_:.]+/g,"_"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(o.default.sprintf(t.data("urlAddpage"),i,a.ClassName),t.data("extraParams")))}}})),l&&(a.addsubpage={label:o.default._t("CMS.AddSubPage","Add page under this page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"),submenu:s}),n.hasClass("edit-disabled")||(a.duplicate={label:o.default._t("CMS.Duplicate"),submenu:[{label:o.default._t("CMS.ThisPageOnly"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(o.default.sprintf(t.data("urlDuplicate"),n.data("id")),t.data("extraParams")))}},{label:o.default._t("CMS.ThisPageAndSubpages"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(o.default.sprintf(t.data("urlDuplicatewithchildren"),n.data("id")),t.data("extraParams")))}}]}),a}},n},canMove:async function(e){if(!(e.rslt.o.find(".homepage").first().length>0))return!0;if(e.rslt.op.data("id")===e.rslt.np.data("id"))return!0;var t=o.default._t("CMS.RemoveHomePageWarningMessage","Warning: This page is the home page. By changing the URL segment visitors will not be able to view it.");return await(0,i.default)({title:o.default._t("CMS.RemoveHomePageWarningTitle","Remove your home page?"),message:t,confirmText:o.default._t("CMS.RemoveHomePageWarningLabel","Remove"),confirmColor:"danger"})}}),e(".cms-tree a.jstree-clicked").entwine({onmatch:function(){var e=this,t=e.parents(".cms-tree-view-sidebar");if(e.offset().top<0||e.offset().top>t.height()-e.height()){var n=e.parent();n.prev().length&&(n=n.prev()),n.get(0).scrollIntoView()}}}),e(".cms-tree-filtered .clear-filter").entwine({onclick:function(){window.location=location.protocol+"//"+location.host+location.pathname}}),e(".cms-tree .subtree-list-link").entwine({onclick:function(e){e.preventDefault(),this.closest(".cms-tree").showListViewFor(this.data("id"))}})}))},881:function(e,t,n){var a,o=(a=n(669))&&a.__esModule?a:{default:a},i=n(125);o.default.entwine("ss",(function(e){const t="treeview",n="listview";e(".cms-content-header-info").entwine({"from .cms-panel":{ontoggle:function(e){var t=this.closest(".cms-content").find(e.target);0!==t.length&&this.parent()[t.hasClass("collapsed")?"addClass":"removeClass"]("collapsed")}}}),e(".cms-panel-deferred.cms-content-view").entwine({onadd:function(){if(this.data("no-ajax"))return;var e=localStorage.getItem("ss.pages-view-type")||t;this.closest(".cms-content-tools").length>0&&(e=t);const a=this.data(`url-${e}`);let o=localStorage.getItem("ss.pages-view-filtered");"string"==typeof o&&"false"===o.toLowerCase()&&(o=!1),localStorage.setItem("ss.pages-view-filtered",!1),this.data("deferredNoCache",o||e===n),this.data("url",a+location.search),this._super()}}),e(".js-injector-boot .search-holder--cms").entwine({search(e){localStorage.setItem("ss.pages-view-filtered",!0),this._super(e)}}),e(".cms .page-view-link").entwine({onclick:function(t){t.preventDefault();const a=e(this).data("view"),o=this.closest(".cms-content-view"),r=o.data(`url-${a}`),s=0!==o.closest(".cms-content-tools").length;if(localStorage.setItem("ss.pages-view-type",a),s&&a===n){const t=e("base").attr("href")||"";window.location.assign((0,i.joinUrlPaths)(t,o.data("url-listviewroot")))}else o.data("url",r+location.search),o.redraw()}}),e(".cms .cms-clear-filter").entwine({onclick:function(t){t.preventDefault(),window.location=e(this).prop("href")}}),e(".cms-content-toolbar").entwine({onmatch:function(){var t=this;this._super(),e.each(this.find(".cms-actions-buttons-row .tool-button"),(function(){var n=e(this),a=n.data("toolid");n.hasClass("active");void 0!==a&&(n.data("active",!1).removeClass("active"),e("#"+a).hide(),t.bindActionButtonEvents(n))}))},onunmatch:function(){var t=this;this._super(),e.each(this.find(".cms-actions-buttons-row .tool-button"),(function(){var n=e(this);t.unbindActionButtonEvents(n)}))},bindActionButtonEvents:function(e){var t=this;e.on("click.cmsContentToolbar",(function(n){t.showHideTool(e)}))},unbindActionButtonEvents:function(e){e.off(".cmsContentToolbar")},showHideTool:function(t){var n=t.data("active"),a=t.data("toolid"),o=e("#"+a);e.each(this.find(".cms-actions-buttons-row .tool-button"),(function(){var t=e(this),n=e("#"+t.data("toolid"));t.data("toolid")!==a&&(n.hide(),t.data("active",!1))})),t[n?"removeClass":"addClass"]("active"),o[n?"hide":"show"](),t.data("active",!n)}})}))},739:function(e,t,n){var a;((a=n(669))&&a.__esModule?a:{default:a}).default.entwine("ss",(function(e){e("#Form_EditForm_RedirectionType input").entwine({onmatch:function(){e(this).attr("checked")&&this.toggle(),this._super()},onunmatch:function(){this._super()},onclick:function(){this.toggle()},toggle:function(){"Internal"==e(this).attr("value")?(e("#Form_EditForm_ExternalURL_Holder").hide(),e("#Form_EditForm_LinkToID_Holder").show(),e("#Form_EditForm_LinkToFile_Holder").hide()):"External"==e(this).attr("value")?(e("#Form_EditForm_ExternalURL_Holder").show(),e("#Form_EditForm_LinkToID_Holder").hide(),e("#Form_EditForm_LinkToFile_Holder").hide()):(e("#Form_EditForm_LinkToFile_Holder").show(),e("#Form_EditForm_ExternalURL_Holder").hide(),e("#Form_EditForm_LinkToID_Holder").hide())}})}))},978:function(e,t,n){var a;((a=n(669))&&a.__esModule?a:{default:a}).default.entwine("ss",(function(e){e(".field.urlsegment:not(.readonly)").entwine({MaxPreviewLength:55,Ellipsis:"...",onmatch:function(){this.find(":text").length&&this.toggleEdit(!1),this.redraw(),this._super()},redraw:function(){var e=this.find(":text"),t=decodeURI(e.data("prefix")+e.val()),n=t;t.length>this.getMaxPreviewLength()&&(n=this.getEllipsis()+t.substr(t.length-this.getMaxPreviewLength(),t.length)),this.find(".URL-link").attr("href",encodeURI(t+e.data("suffix"))).text(n)},toggleEdit:function(e){var t=this.find(":text");this.find(".preview-holder")[e?"hide":"show"](),this.find(".edit-holder")[e?"show":"hide"](),e&&(t.data("origval",t.val()),t.focus())},update:function(){var e=this,t=this.find(":text"),n=t.data("origval"),a=arguments[0],o=a&&""!==a?a:t.val();n!=o?(this.addClass("loading"),this.suggest(o,(function(n){t.val(decodeURIComponent(n.value)),e.toggleEdit(!1),e.removeClass("loading"),e.redraw()}))):(this.toggleEdit(!1),this.redraw())},cancel:function(){var e=this.find(":text");e.val(e.data("origval")),this.toggleEdit(!1)},suggest:function(t,n){var a=this,o=a.find(":text"),i=e.path.parseUrl(a.closest("form").attr("action")),r=i.hrefNoSearch+"/field/"+o.attr("name")+"/suggest/?value="+encodeURIComponent(t);i.search&&(r+="&"+i.search.replace(/^\?/,"")),e.ajax({url:r,success:function(e){n.apply(this,arguments)},error:function(e,t){e.statusText=e.responseText},complete:function(){a.removeClass("loading")}})}}),e(".field.urlsegment .text").entwine({onkeydown:function(e){13===e.keyCode&&(e.preventDefault(),this.closest(".field").update())}}),e(".field.urlsegment .edit").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").toggleEdit(!0)}}),e(".field.urlsegment .update").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").update()}}),e(".field.urlsegment .cancel").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").cancel()}})}))},803:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default={ANCHORSELECTOR_CURRENT_FIELD:"ANCHORSELECTOR_CURRENT_FIELD",ANCHORSELECTOR_UPDATED:"ANCHORSELECTOR_UPDATED",ANCHORSELECTOR_UPDATING:"ANCHORSELECTOR_UPDATING",ANCHORSELECTOR_UPDATE_FAILED:"ANCHORSELECTOR_UPDATE_FAILED"}},979:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.beginUpdating=function(e){return{type:o.default.ANCHORSELECTOR_UPDATING,payload:{pageId:e}}},t.updateFailed=function(e){return{type:o.default.ANCHORSELECTOR_UPDATE_FAILED,payload:{pageId:e}}},t.updated=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return{type:o.default.ANCHORSELECTOR_UPDATED,payload:{pageId:e,anchors:t,cacheResult:n}}},t.updatedCurrentField=function(e,t,n){return{type:o.default.ANCHORSELECTOR_CURRENT_FIELD,payload:{pageId:e,anchors:t,fieldID:n}}};var a,o=(a=n(803))&&a.__esModule?a:{default:a}},796:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:s,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const n=(n,o)=>{const i=t.payload.pageId;return(0,a.default)({pages:[...e.pages.filter((e=>e.id!==i)),{id:i,loadingState:n,anchors:o}].sort(((e,t)=>e.id-t.id))})};switch(t.type){case o.default.ANCHORSELECTOR_UPDATING:return n(i.default.UPDATING,[]);case o.default.ANCHORSELECTOR_UPDATED:{const{anchors:e,cacheResult:a}=t.payload,{SUCCESS:o,DIRTY:r}=i.default;return n(a?o:r,e)}case o.default.ANCHORSELECTOR_CURRENT_FIELD:{const{anchors:e}=t.payload;return n(i.default.FIELD_ONLY,e)}case o.default.ANCHORSELECTOR_UPDATE_FAILED:return n(i.default.FAILED,[]);default:return e}};var a=r(n(923)),o=r(n(803)),i=r(n(996));function r(e){return e&&e.__esModule?e:{default:e}}const s=(0,a.default)({pages:[]})},996:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default={SUCCESS:"SUCCESS",DIRTY:"DIRTY",FIELD_ONLY:"FIELD_ONLY",UPDATING:"UPDATING",FAILED:"FAILED"}},560:function(e,t,n){function a(e){return a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a(e)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var o,i=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==a(e)&&"function"!=typeof e)return{default:e};var n=l(t);if(n&&n.has(e))return n.get(e);var o={},i=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var r in e)if("default"!==r&&Object.prototype.hasOwnProperty.call(e,r)){var s=i?Object.getOwnPropertyDescriptor(e,r):null;s&&(s.get||s.set)?Object.defineProperty(o,r,s):o[r]=e[r]}o.default=e,n&&n.set(e,o);return o}(n(594)),r=(o=n(935))&&o.__esModule?o:{default:o},s=n(556);function l(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(l=function(e){return e?n:t})(e)}function d(){return d=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&(e[a]=n[a])}return e},d.apply(this,arguments)}var u=function(e){var t=e.onClose,n=e.message,a=e.title,o=e.confirmText,r=e.cancelText,l=e.confirmColor,u=e.cancelColor,c=e.className,f=e.buttonsComponent,h=e.size,p=e.bodyComponent,m=e.modalProps,g=i.default.createElement(i.Fragment,null,r&&i.default.createElement(s.Button,{color:u,onClick:function(){return t(!1)}},r)," ",i.default.createElement(s.Button,{color:l,onClick:function(){return t(!0)}},o));if(f){var v=f;g=i.default.createElement(v,{onClose:t})}var _=p;return i.default.createElement(s.Modal,d({size:h,isOpen:!0,toggle:function(){return t(!1)},className:"reactstrap-confirm ".concat(c)},m),a&&i.default.createElement(s.ModalHeader,{toggle:function(){return t(!1)}},a||null),i.default.createElement(s.ModalBody,null,p?i.default.createElement(_,null):n),i.default.createElement(s.ModalFooter,null,g))};u.defaultProps={message:"Are you sure?",title:"Warning!",confirmText:"Ok",cancelText:"Cancel",confirmColor:"primary",cancelColor:"",className:"",buttonsComponent:null,size:null,bodyComponent:null,modalProps:{}},u.propTypes={onClose:r.default.func.isRequired,message:r.default.node,title:r.default.node,confirmText:r.default.node,cancelText:r.default.node,confirmColor:r.default.string,cancelColor:r.default.string,className:r.default.string,size:r.default.string,buttonsComponent:r.default.func,bodyComponent:r.default.func,modalProps:r.default.object};var c=u;t.default=c},216:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=r(n(594)),o=n(518),i=r(n(560));function r(e){return e&&e.__esModule?e:{default:e}}function s(){return s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&(e[a]=n[a])}return e},s.apply(this,arguments)}var l=function(e){return new Promise((function(t){var n=document.createElement("div");(0,o.render)(a.default.createElement(i.default,s({},e,{onClose:function(e){(0,o.unmountComponentAtNode)(n),n=null,t(e)}})),n)}))};t.default=l},923:function(e){e.exports=DeepFreezeStrict},657:function(e){e.exports=EmotionCssCacheProvider},623:function(e){e.exports=FieldHolder},207:function(e){e.exports=Injector},950:function(e){e.exports=IsomorphicFetch},935:function(e){e.exports=PropTypes},594:function(e){e.exports=React},518:function(e){e.exports=ReactDom},40:function(e){e.exports=ReactRedux},315:function(e){e.exports=ReactSelectCreatable},556:function(e){e.exports=Reactstrap},367:function(e){e.exports=Redux},381:function(e){e.exports=ReduxForm},898:function(e){e.exports=SilverStripeComponent},304:function(e){e.exports=classnames},432:function(e){e.exports=getFormState},815:function(e){e.exports=i18n},669:function(e){e.exports=jQuery},125:function(e){e.exports=ssUrlLib}},t={};function n(a){var o=t[a];if(void 0!==o)return o.exports;var i=t[a]={exports:{}};return e[a](i,i.exports,n),i.exports}n(586),n(677),n(881),n(55),n(739),n(978),n(38)}();
!function(){"use strict";var e={38:function(e,t,n){var a=i(n(420)),o=i(n(121));function i(e){return e&&e.__esModule?e:{default:e}}window.document.addEventListener("DOMContentLoaded",(()=>{(0,o.default)(),(0,a.default)()}))},121:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=i(n(207)),o=i(n(269));function i(e){return e&&e.__esModule?e:{default:e}}t.default=()=>{a.default.component.register("AnchorSelectorField",o.default)}},420:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=r(n(207)),o=n(367),i=r(n(796));function r(e){return e&&e.__esModule?e:{default:e}}t.default=()=>{a.default.reducer.register("cms",(0,o.combineReducers)({anchorSelector:i.default}))}},269:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.ConnectedAnchorSelectorField=t.Component=void 0;var a=C(n(815)),o=C(n(594)),i=C(n(950)),r=n(40),s=n(367),l=n(381),d=C(n(898)),c=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var n=_(t);if(n&&n.has(e))return n.get(e);var a={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var i in e)if("default"!==i&&{}.hasOwnProperty.call(e,i)){var r=o?Object.getOwnPropertyDescriptor(e,i):null;r&&(r.get||r.set)?Object.defineProperty(a,i,r):a[i]=e[i]}return a.default=e,n&&n.set(e,a),a}(n(979)),u=C(n(996)),f=C(n(623)),h=C(n(315)),p=C(n(657)),m=C(n(432)),g=C(n(304)),v=C(n(935));function _(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(_=function(e){return e?n:t})(e)}function C(e){return e&&e.__esModule?e:{default:e}}const b=()=>null;class w extends d.default{constructor(e){super(e),this.handleChange=this.handleChange.bind(this),this.handleLoadingError=this.handleLoadingError.bind(this)}componentDidMount(){this.ensurePagesLoaded()}componentDidUpdate(e){this.props.pageId!==e.pageId&&this.ensurePagesLoaded()}ensurePagesLoaded(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.props;if(e.loadingState===u.default.UPDATING||e.loadingState===u.default.SUCCESS||!e.pageId)return Promise.resolve();let t=[];e.loadingState===u.default.FIELD_ONLY&&(t=this.props.anchors),e.actions.anchorSelector.beginUpdating(e.pageId);const n=e.data.endpoint.replace(/:id/,e.pageId);return(0,i.default)(n,{credentials:"same-origin"}).then((e=>e.json())).then((n=>{const a=[...new Set([...n,...t])];return e.actions.anchorSelector.updated(e.pageId,a),a})).catch((t=>{e.actions.anchorSelector.updateFailed(e.pageId),this.handleLoadingError(t,e)}))}getDropdownOptions(){const e=this.props.anchors.map((e=>({value:e})));return this.props.value&&!this.props.anchors.find((e=>e===this.props.value))&&e.unshift({value:this.props.value}),e}handleChange(e){"function"==typeof this.props.onChange&&this.props.onChange(e?e.value:"")}handleLoadingError(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.props;if(t.onLoadingError===b)throw e;return t.onLoadingError({errors:[{value:e.message,type:"error"}]})}render(){const{extraClass:e,CreatableSelectComponent:t}=this.props,n=(0,g.default)("anchorselectorfield",e),i=this.getDropdownOptions(),r=this.props.value||"",s=a.default._t("CMS.ANCHOR_SELECT_OR_TYPE","Select or enter anchor");return o.default.createElement(p.default,null,o.default.createElement(t,{isSearchable:!0,isClearable:!0,options:i,className:n,name:this.props.name,onChange:this.handleChange,value:{value:r},noOptionsMessage:()=>a.default._t("CMS.ANCHOR_NO_OPTIONS","No options"),placeholder:s,getOptionLabel:e=>{let{value:t}=e;return t},classNamePrefix:"anchorselectorfield"}))}}t.Component=w,w.propTypes={extraClass:v.default.string,id:v.default.string,name:v.default.string.isRequired,onChange:v.default.func,value:v.default.string,attributes:v.default.oneOfType([v.default.object,v.default.array]),pageId:v.default.number,anchors:v.default.array,loadingState:v.default.oneOf(Object.keys(u.default).map((e=>u.default[e]))),onLoadingError:v.default.func,data:v.default.shape({endpoint:v.default.string,targetFieldName:v.default.string})},w.defaultProps={value:"",extraClass:"",onLoadingError:b,attributes:{},CreatableSelectComponent:h.default};const S=t.ConnectedAnchorSelectorField=(0,r.connect)((function(e,t){const n=(0,l.formValueSelector)(t.formid,m.default),a=t&&t.data&&t.data.targetFieldName||"PageID",o=Number(n(e,a)||0);let i=[];const r=o?e.cms.anchorSelector.pages.find((e=>e.id===o)):null;!r||r.loadingState!==u.default.SUCCESS&&r.loadingState!==u.default.DIRTY&&r.loadingState!==u.default.FIELD_ONLY||(i=r.anchors);let s=null;return s=r?r.loadingState:o?u.default.DIRTY:u.default.SUCCESS,{pageId:o,anchors:i,loadingState:s}}),(function(e){return{actions:{anchorSelector:(0,s.bindActionCreators)(c,e)}}}))(w);t.default=(0,f.default)(S)},586:function(e,t,n){var a;((a=n(669))&&a.__esModule?a:{default:a}).default.entwine("ss",(function(e){e(".TreeDropdownField").entwine({OldValue:null}),e("#Form_AddForm_ParentID_Holder .treedropdownfield").entwine({onmatch(){this._super(),e(".cms-add-form").updateTypeList()}}),e(".cms-add-form .parent-mode :input").entwine({onclick:function(e){var t=this.closest("form").find("#Form_AddForm_ParentID_Holder .TreeDropdownField");"top"==this.val()?(t.setOldValue(t.getValue()),t.setValue(0)):(t.setValue(t.getOldValue()||0),t.setOldValue(null)),t.refresh(),t.trigger("change")}}),e(".cms-add-form").entwine({ParentCache:{},onadd:function(){var t=this;this.find("#Form_AddForm_ParentID_Holder .TreeDropdownField").on("change",(function(){t.updateTypeList()})),this.find(".SelectionGroup.parent-mode").on("change",(function(){t.updateTypeList()})),"top"==e(".cms-add-form .parent-mode :input").val()&&this.updateTypeList()},loadCachedChildren:function(e){var t=this.getParentCache();return void 0!==t[e]?t[e]:null},saveCachedChildren:function(e,t){var n=this.getParentCache();n[e]=t,this.setParentCache(n)},updateTypeList:function(){var t=this.data("hints"),n=this.find("#Form_AddForm_ParentID"),a=this.find("input[name=ParentModeField]:checked").val(),o=n.data("metadata"),i="child"===a?n.getValue():null,r=o?o.ClassName:null,s=r&&"child"===a&&i?r:"Root",l=void 0!==t[s]?t[s]:null,d=this,c=l&&void 0!==l.defaultChild?l.defaultChild:null,u=[];if(i){if(this.hasClass("loading"))return;return this.addClass("loading"),null!==(u=this.loadCachedChildren(i))?(this.updateSelectionFilter(u,c),void this.removeClass("loading")):(e.ajax({url:d.data("childfilter"),data:{ParentID:i},success:function(e){d.saveCachedChildren(i,e),d.updateSelectionFilter(e,c)},complete:function(){d.removeClass("loading")}}),!1)}u=l&&void 0!==l.disallowedChildren?l.disallowedChildren:[],this.updateSelectionFilter(u,c)},updateSelectionFilter:function(t,n){var a=this.find("#Form_AddForm_RecordType div.radio.selected")[0],o=!1,i=null;if(this.find("#Form_AddForm_RecordType div.radio").each((function(n,r){var s=e(this).find("input").val(),l=-1===e.inArray(s,t);r===a&&l&&(o=!0),e(this).setEnabled(l),l||e(this).setSelected(!1),i=(null===i||i)&&l})),o)var r=e(a).parents("li:first");else if(n)r=this.find("#Form_AddForm_RecordType div.radio input[value="+n+"]").parents("li:first");else r=this.find("#Form_AddForm_RecordType div.radio:not(.disabled):first");r.setSelected(!0),r.siblings().setSelected(!1),this.find("#Form_AddForm_RecordType div.radio:not(.disabled)").length?this.find("button[name=action_doAdd]").removeAttr("disabled"):this.find("button[name=action_doAdd]").attr("disabled","disabled"),this.find(".message-restricted")[i?"hide":"show"]()}}),e(".cms-add-form #Form_AddForm_RecordType div.radio").entwine({onclick:function(e){this.setSelected(!0)},setSelected:function(e){var t=this.find("input");e&&!t.is(":disabled")?(this.siblings().setSelected(!1),this.toggleClass("selected",!0),t.prop("checked",!0)):(this.toggleClass("selected",!1),t.prop("checked",!1))},setEnabled:function(t){e(this).toggleClass("disabled",!t),t?e(this).find("input").removeAttr("disabled"):e(this).find("input").attr("disabled","disabled").removeAttr("checked")}}),e(".cms-content-addpage-button").entwine({onclick:function(t){var n,a=e(".cms-tree"),o=e(".cms-list"),i=0;if(a.is(":visible")){var r=a.jstree("get_selected");i=r?e(r[0]).data("id"):null}else{var s=o.find('input[name="Page[GridState]"]').val();s&&(i=parseInt(JSON.parse(s).ParentID,10))}var l,d={selector:this.data("targetPanel"),pjax:this.data("pjax")};i?(n=this.data("extraParams")?this.data("extraParams"):"",l=e.path.addSearchParams(i18n.sprintf(this.data("urlAddpage"),i),n)):l=this.attr("href"),e(".cms-container").loadPanel(l,null,d),t.preventDefault(),this.blur()}})}))},677:function(e,t,n){var a=r(n(669)),o=r(n(815)),i=r(n(216));function r(e){return e&&e.__esModule?e:{default:e}}a.default.entwine("ss",(function(e){e(".cms-edit-form :input#Form_EditForm_ClassName").entwine({onchange:function(){alert(o.default._t("CMS.ALERTCLASSNAME"))}}),e(".cms-edit-form input[name=Title]").entwine({onmatch:function(){var t=this;t.data("OrigVal",t.val());var n=t.closest("form"),a=e("input:text[name=URLSegment]",n),o=e("input[name=LiveLink]",n);a.length>0&&(t._addActions(),this.on("change",(function(n){var i=t.data("OrigVal"),r=t.val();t.data("OrigVal",r),0===a.val().indexOf(a.data("defaultUrl"))&&""==o.val()?t.updateURLSegment(r):e(".update",t.parent()).show().parent(".form__field-holder").addClass("input-group"),t.updateRelatedFields(r,i),t.updateBreadcrumbLabel(r)}))),this._super()},onunmatch:function(){this._super()},updateRelatedFields:function(t,n){this.parents("form").find("input[name=MetaTitle], input[name=MenuTitle]").each((function(){var a=e(this);a.val()==n&&(a.val(t),a.updatedRelatedFields&&a.updatedRelatedFields())}))},updateURLSegment:function(t){var n=e("input:text[name=URLSegment]",this.closest("form")).closest(".field.urlsegment"),a=e(".update",this.parent());n.update(t),a.is(":visible")&&a.hide().parent(".form__field-holder").removeClass("input-group")},updateBreadcrumbLabel:function(t){e(".cms-edit-form input[name=ID]").val();var n=e("span.cms-panel-link.crumb");t&&""!=t&&n.text(t)},_addActions:function(){var t,n=this;(t=e("<button />",{class:"update btn btn-outline-secondary form__field-update-url",text:o.default._t("CMS.UpdateURL"),type:"button",click:function(e){e.preventDefault(),n.updateURLSegment(n.val())}})).insertAfter(n),t.hide()}}),e(".cms-edit-form .parentTypeSelector").entwine({onmatch:function(){var e=this;this.find(":input[name=ParentType]").on("click",(function(t){e._toggleSelection(t)})),this.find(".TreeDropdownField").on("change",(function(t){e._changeParentId(t)})),this._changeParentId(),this._toggleSelection(),this._super()},onunmatch:function(){this._super()},_toggleSelection:function(t){var n=this.find(":input[name=ParentType]:checked").val(),a=this.find("#Form_EditForm_ParentID_Holder");"root"==n?this.find(":input[name=ParentID]").val(0):this.find(":input[name=ParentID]").val(this.find("#Form_EditForm_ParentType_subpage").data("parentIdValue")),"root"!=n?a.slideDown(400,(function(){e(this).css("overflow","visible")})):a.slideUp()},_changeParentId:function(e){var t=this.find(":input[name=ParentID]").val();this.find("#Form_EditForm_ParentType_subpage").data("parentIdValue",t)}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_doRollback, .cms-edit-form .btn-toolbar #Form_EditForm_action_rollback").entwine({onclick:function(e){if(this.is(":disabled"))return e.preventDefault(),!1;const t=this.parents("form:first").find(":input[name=Version]").val(),n=t?o.default.sprintf(o.default._t("CMS.RollbackToVersion","Do you really want to roll back to version #%s of this page?"),t):o.default._t("CMS.ConfirmRestoreFromLive","Are you sure you want to revert draft to when the page was last published?");return confirm(n)?(this.parents("form:first").addClass("loading"),this._super(e)):(e.preventDefault(),!1)}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_archive:not(.homepage-warning)").entwine({onclick:function(e){var t;return t=this.parents("form:first").find("input[name=ArchiveWarningMessage]").val().replace(/\\n/g,"\n"),!!confirm(t)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_restore").entwine({onclick:function(e){var t,n=this.parents("form:first").find(":input[name=Version]").val(),a=this.data("toRoot");return t=o.default.sprintf(o.default._t(a?"CMS.RestoreToRoot":"CMS.Restore"),n),!!confirm(t)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form .btn-toolbar #Form_EditForm_action_unpublish:not(.homepage-warning)").entwine({onclick:function(e){var t,n=this.parents("form:first").find(":input[name=Version]").val();return t=o.default.sprintf(o.default._t("CMS.Unpublish"),n),!!confirm(t)&&(this.parents("form:first").addClass("loading"),this._super(e))}}),e(".cms-edit-form.changed").entwine({onmatch:function(t){this.find("button[data-text-alternate]").each((function(){const t=e(this),n=t.find(".btn__title"),a=t.data("textAlternate");a&&(t.data("textStandard",n.text()),n.text(a));const o=t.data("btnAlternate");o&&(t.data("btnStandard",t.attr("class")),t.attr("class",o),t.removeClass("btn-outline-secondary").addClass("btn-primary"));const i=t.data("btnAlternateAdd");i&&t.addClass(i);const r=t.data("btnAlternateRemove");r&&t.removeClass(r)})),this._super(t)},onunmatch:function(t){this.find("button[data-text-alternate]").each((function(){const t=e(this),n=t.find(".btn__title"),a=t.data("textStandard");a&&n.text(a);const o=t.data("btnStandard");o&&(t.attr("class",o),t.addClass("btn-outline-secondary").removeClass("btn-primary"));const i=t.data("btnAlternateAdd");i&&t.removeClass(i);const r=t.data("btnAlternateRemove");r&&t.addClass(r)})),this._super(t)}}),e(".cms-edit-form .btn-toolbar button[name=action_publish]").entwine({onbuttonafterrefreshalternate:function(){this.data("showingAlternate")?(this.addClass("btn-primary"),this.removeClass("btn-secondary")):(this.removeClass("btn-primary"),this.addClass("btn-secondary"))}}),e(".cms-edit-form .btn-toolbar button[name=action_save]").entwine({onbuttonafterrefreshalternate:function(){this.data("showingAlternate")?(this.addClass("btn-primary"),this.removeClass("btn-secondary")):(this.removeClass("btn-primary"),this.addClass("btn-secondary"))}}),e('.cms-edit-form.CMSPageSettingsController input[name="ParentType"]:checked').entwine({onmatch:function(){this.redraw(),this._super()},onunmatch:function(){this._super()},redraw:function(){var t=e(".cms-edit-form.CMSPageSettingsController #Form_EditForm_ParentID_Holder");"Form_EditForm_ParentType_root"==e(this).attr("id")?t.slideUp():t.slideDown()},onclick:function(){this.redraw()}}),"Form_EditForm_ParentType_root"==e('.cms-edit-form.CMSPageSettingsController input[name="ParentType"]:checked').attr("id")&&e(".cms-edit-form.CMSPageSettingsController #Form_EditForm_ParentID_Holder").hide();var t=!1;e(".cms-edit-form .btn-toolbar #Form_EditForm_action_unpublish.homepage-warning,.cms-edit-form .btn-toolbar #Form_EditForm_action_archive.homepage-warning,#Form_EditForm_URLSegment_Holder.homepage-warning .btn.update").entwine({onclick:async function(e){if(t)return this._super(e);e.stopPropagation();var n=o.default._t("CMS.RemoveHomePageWarningMessage","Warning: This page is the home page. By changing the URL segment visitors will not be able to view it.");return await(0,i.default)({title:o.default._t("CMS.RemoveHomePageWarningTitle","Remove your home page?"),message:n,confirmText:o.default._t("CMS.RemoveHomePageWarningLabel","Remove"),confirmColor:"danger"})&&(t=!0,this.trigger("click"),t=!1),!1}})}))},55:function(e,t,n){var a=s(n(669)),o=s(n(815)),i=s(n(216)),r=n(125);function s(e){return e&&e.__esModule?e:{default:e}}a.default.entwine("ss.tree",(function(e){e(".cms-tree").entwine({fromDocument:{"oncontext_show.vakata":function(e){this.adjustContextClass()}},adjustContextClass:function(){var t=e("#vakata-contextmenu").find("ul ul");t.each((function(n){var a="1",o=e(t[n]).find("li").length;o>20?a="3":o>10&&(a="2"),e(t[n]).addClass("vakata-col-"+a).removeClass("right"),e(t[n]).find("li").on("mouseenter",(function(t){e(this).parent("ul").removeClass("right")}))}))},showListViewFor:function(t){localStorage.setItem("ss.pages-view-type","listview");const n=this.closest(".cms-content-view").data("url-listviewroot"),a=e.path.addSearchParams(n,{ParentID:t}),o=e("base").attr("href")||"";window.location.assign((0,r.joinUrlPaths)(o,a))},getTreeConfig:function(){var t=this,n=this._super();this.getHints();return n.plugins.push("contextmenu"),n.contextmenu={items:function(n){var a={edit:{label:n.hasClass("edit-disabled")?o.default._t("CMS.EditPage","Edit page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"):o.default._t("CMS.ViewPage","View page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(o.default.sprintf(t.data("urlEditpage"),n.data("id")))}}};n.hasClass("nochildren")||(a.showaslist={label:o.default._t("CMS.ShowAsList"),action:function(e){t.showListViewFor(e.data("id"))}});n.data("pagetype");var i=n.data("id"),r=n.find(">a .item").data("allowedchildren"),s={},l=!1;return e.each(r,(function(n,a){l=!0,s["allowedchildren-"+a.ClassName]={label:'<span class="jstree-pageicon '+a.IconClass+'"></span>'+a.Title,_class:"class-"+a.ClassName.replace(/[^a-zA-Z0-9\-_:.]+/g,"_"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(o.default.sprintf(t.data("urlAddpage"),i,a.ClassName),t.data("extraParams")))}}})),l&&(a.addsubpage={label:o.default._t("CMS.AddSubPage","Add page under this page",100,"Used in the context menu when right-clicking on a page node in the CMS tree"),submenu:s}),n.hasClass("edit-disabled")||(a.duplicate={label:o.default._t("CMS.Duplicate"),submenu:[{label:o.default._t("CMS.ThisPageOnly"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(o.default.sprintf(t.data("urlDuplicate"),n.data("id")),t.data("extraParams")))}},{label:o.default._t("CMS.ThisPageAndSubpages"),action:function(n){e(".cms-container").entwine(".ss").loadPanel(e.path.addSearchParams(o.default.sprintf(t.data("urlDuplicatewithchildren"),n.data("id")),t.data("extraParams")))}}]}),a}},n},canMove:async function(e){if(!(e.rslt.o.find(".homepage").first().length>0))return!0;if(e.rslt.op.data("id")===e.rslt.np.data("id"))return!0;var t=o.default._t("CMS.RemoveHomePageWarningMessage","Warning: This page is the home page. By changing the URL segment visitors will not be able to view it.");return await(0,i.default)({title:o.default._t("CMS.RemoveHomePageWarningTitle","Remove your home page?"),message:t,confirmText:o.default._t("CMS.RemoveHomePageWarningLabel","Remove"),confirmColor:"danger"})}}),e(".cms-tree a.jstree-clicked").entwine({onmatch:function(){var e=this,t=e.parents(".cms-tree-view-sidebar");if(e.offset().top<0||e.offset().top>t.height()-e.height()){var n=e.parent();n.prev().length&&(n=n.prev()),n.get(0).scrollIntoView()}}}),e(".cms-tree-filtered .clear-filter").entwine({onclick:function(){window.location=location.protocol+"//"+location.host+location.pathname}}),e(".cms-tree .subtree-list-link").entwine({onclick:function(e){e.preventDefault(),this.closest(".cms-tree").showListViewFor(this.data("id"))}})}))},881:function(e,t,n){var a,o=(a=n(669))&&a.__esModule?a:{default:a},i=n(125);o.default.entwine("ss",(function(e){const t="treeview",n="listview";e(".cms-content-header-info").entwine({"from .cms-panel":{ontoggle:function(e){var t=this.closest(".cms-content").find(e.target);0!==t.length&&this.parent()[t.hasClass("collapsed")?"addClass":"removeClass"]("collapsed")}}}),e(".cms-panel-deferred.cms-content-view").entwine({onadd:function(){if(this.data("no-ajax"))return;var e=localStorage.getItem("ss.pages-view-type")||t;this.closest(".cms-content-tools").length>0&&(e=t);const a=this.data(`url-${e}`);let o=localStorage.getItem("ss.pages-view-filtered");"string"==typeof o&&"false"===o.toLowerCase()&&(o=!1),localStorage.setItem("ss.pages-view-filtered",!1),this.data("deferredNoCache",o||e===n),this.data("url",a+location.search),this._super()}}),e(".js-injector-boot .search-holder--cms").entwine({search(e){localStorage.setItem("ss.pages-view-filtered",!0),this._super(e)}}),e(".cms .page-view-link").entwine({onclick:function(t){t.preventDefault();const a=e(this).data("view"),o=this.closest(".cms-content-view"),r=o.data(`url-${a}`),s=0!==o.closest(".cms-content-tools").length;if(localStorage.setItem("ss.pages-view-type",a),s&&a===n){const t=e("base").attr("href")||"";window.location.assign((0,i.joinUrlPaths)(t,o.data("url-listviewroot")))}else o.data("url",r+location.search),o.redraw()}}),e(".cms .cms-clear-filter").entwine({onclick:function(t){t.preventDefault(),window.location=e(this).prop("href")}}),e(".cms-content-toolbar").entwine({onmatch:function(){var t=this;this._super(),e.each(this.find(".cms-actions-buttons-row .tool-button"),(function(){var n=e(this),a=n.data("toolid");n.hasClass("active");void 0!==a&&(n.data("active",!1).removeClass("active"),e("#"+a).hide(),t.bindActionButtonEvents(n))}))},onunmatch:function(){var t=this;this._super(),e.each(this.find(".cms-actions-buttons-row .tool-button"),(function(){var n=e(this);t.unbindActionButtonEvents(n)}))},bindActionButtonEvents:function(e){var t=this;e.on("click.cmsContentToolbar",(function(n){t.showHideTool(e)}))},unbindActionButtonEvents:function(e){e.off(".cmsContentToolbar")},showHideTool:function(t){var n=t.data("active"),a=t.data("toolid"),o=e("#"+a);e.each(this.find(".cms-actions-buttons-row .tool-button"),(function(){var t=e(this),n=e("#"+t.data("toolid"));t.data("toolid")!==a&&(n.hide(),t.data("active",!1))})),t[n?"removeClass":"addClass"]("active"),o[n?"hide":"show"](),t.data("active",!n)}})}))},739:function(e,t,n){var a;((a=n(669))&&a.__esModule?a:{default:a}).default.entwine("ss",(function(e){e("#Form_EditForm_RedirectionType input").entwine({onmatch:function(){e(this).attr("checked")&&this.toggle(),this._super()},onunmatch:function(){this._super()},onclick:function(){this.toggle()},toggle:function(){"Internal"==e(this).attr("value")?(e("#Form_EditForm_ExternalURL_Holder").hide(),e("#Form_EditForm_LinkToID_Holder").show(),e("#Form_EditForm_LinkToFile_Holder").hide()):"External"==e(this).attr("value")?(e("#Form_EditForm_ExternalURL_Holder").show(),e("#Form_EditForm_LinkToID_Holder").hide(),e("#Form_EditForm_LinkToFile_Holder").hide()):(e("#Form_EditForm_LinkToFile_Holder").show(),e("#Form_EditForm_ExternalURL_Holder").hide(),e("#Form_EditForm_LinkToID_Holder").hide())}})}))},978:function(e,t,n){var a;((a=n(669))&&a.__esModule?a:{default:a}).default.entwine("ss",(function(e){e(".field.urlsegment:not(.readonly)").entwine({MaxPreviewLength:55,Ellipsis:"...",onmatch:function(){this.find(":text").length&&this.toggleEdit(!1),this.redraw(),this._super()},redraw:function(){var e=this.find(":text"),t=decodeURI(e.data("prefix")+e.val()),n=t;t.length>this.getMaxPreviewLength()&&(n=this.getEllipsis()+t.substr(t.length-this.getMaxPreviewLength(),t.length)),this.find(".URL-link").attr("href",encodeURI(t+e.data("suffix"))).text(n)},toggleEdit:function(e){var t=this.find(":text");this.find(".preview-holder")[e?"hide":"show"](),this.find(".edit-holder")[e?"show":"hide"](),e&&(t.data("origval",t.val()),t.focus())},update:function(){var e=this,t=this.find(":text"),n=t.data("origval"),a=arguments[0],o=a&&""!==a?a:t.val();n!=o?(this.addClass("loading"),this.suggest(o,(function(n){t.val(decodeURIComponent(n.value)),e.toggleEdit(!1),e.removeClass("loading"),e.redraw()}))):(this.toggleEdit(!1),this.redraw())},cancel:function(){var e=this.find(":text");e.val(e.data("origval")),this.toggleEdit(!1)},suggest:function(t,n){var a=this,o=a.find(":text"),i=e.path.parseUrl(a.closest("form").attr("action")),r=i.hrefNoSearch+"/field/"+o.attr("name")+"/suggest/?value="+encodeURIComponent(t);i.search&&(r+="&"+i.search.replace(/^\?/,"")),e.ajax({url:r,success:function(e){n.apply(this,arguments)},error:function(e,t){e.statusText=e.responseText},complete:function(){a.removeClass("loading")}})}}),e(".field.urlsegment .text").entwine({onkeydown:function(e){13===e.keyCode&&(e.preventDefault(),this.closest(".field").update())}}),e(".field.urlsegment .edit").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").toggleEdit(!0)}}),e(".field.urlsegment .update").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").update()}}),e(".field.urlsegment .cancel").entwine({onclick:function(e){e.preventDefault(),this.closest(".field").cancel()}})}))},803:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default={ANCHORSELECTOR_CURRENT_FIELD:"ANCHORSELECTOR_CURRENT_FIELD",ANCHORSELECTOR_UPDATED:"ANCHORSELECTOR_UPDATED",ANCHORSELECTOR_UPDATING:"ANCHORSELECTOR_UPDATING",ANCHORSELECTOR_UPDATE_FAILED:"ANCHORSELECTOR_UPDATE_FAILED"}},979:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.beginUpdating=function(e){return{type:o.default.ANCHORSELECTOR_UPDATING,payload:{pageId:e}}},t.updateFailed=function(e){return{type:o.default.ANCHORSELECTOR_UPDATE_FAILED,payload:{pageId:e}}},t.updated=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return{type:o.default.ANCHORSELECTOR_UPDATED,payload:{pageId:e,anchors:t,cacheResult:n}}},t.updatedCurrentField=function(e,t,n){return{type:o.default.ANCHORSELECTOR_CURRENT_FIELD,payload:{pageId:e,anchors:t,fieldID:n}}};var a,o=(a=n(803))&&a.__esModule?a:{default:a}},796:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:s,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const n=(n,o)=>{const i=t.payload.pageId;return(0,a.default)({pages:[...e.pages.filter((e=>e.id!==i)),{id:i,loadingState:n,anchors:o}].sort(((e,t)=>e.id-t.id))})};switch(t.type){case o.default.ANCHORSELECTOR_UPDATING:return n(i.default.UPDATING,[]);case o.default.ANCHORSELECTOR_UPDATED:{const{anchors:e,cacheResult:a}=t.payload,{SUCCESS:o,DIRTY:r}=i.default;return n(a?o:r,e)}case o.default.ANCHORSELECTOR_CURRENT_FIELD:{const{anchors:e}=t.payload;return n(i.default.FIELD_ONLY,e)}case o.default.ANCHORSELECTOR_UPDATE_FAILED:return n(i.default.FAILED,[]);default:return e}};var a=r(n(923)),o=r(n(803)),i=r(n(996));function r(e){return e&&e.__esModule?e:{default:e}}const s=(0,a.default)({pages:[]})},996:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default={SUCCESS:"SUCCESS",DIRTY:"DIRTY",FIELD_ONLY:"FIELD_ONLY",UPDATING:"UPDATING",FAILED:"FAILED"}},560:function(e,t,n){function a(e){return a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a(e)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var o,i=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==a(e)&&"function"!=typeof e)return{default:e};var n=l(t);if(n&&n.has(e))return n.get(e);var o={},i=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var r in e)if("default"!==r&&Object.prototype.hasOwnProperty.call(e,r)){var s=i?Object.getOwnPropertyDescriptor(e,r):null;s&&(s.get||s.set)?Object.defineProperty(o,r,s):o[r]=e[r]}o.default=e,n&&n.set(e,o);return o}(n(594)),r=(o=n(935))&&o.__esModule?o:{default:o},s=n(556);function l(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,n=new WeakMap;return(l=function(e){return e?n:t})(e)}function d(){return d=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&(e[a]=n[a])}return e},d.apply(this,arguments)}var c=function(e){var t=e.onClose,n=e.message,a=e.title,o=e.confirmText,r=e.cancelText,l=e.confirmColor,c=e.cancelColor,u=e.className,f=e.buttonsComponent,h=e.size,p=e.bodyComponent,m=e.modalProps,g=i.default.createElement(i.Fragment,null,r&&i.default.createElement(s.Button,{color:c,onClick:function(){return t(!1)}},r)," ",i.default.createElement(s.Button,{color:l,onClick:function(){return t(!0)}},o));if(f){var v=f;g=i.default.createElement(v,{onClose:t})}var _=p;return i.default.createElement(s.Modal,d({size:h,isOpen:!0,toggle:function(){return t(!1)},className:"reactstrap-confirm ".concat(u)},m),a&&i.default.createElement(s.ModalHeader,{toggle:function(){return t(!1)}},a||null),i.default.createElement(s.ModalBody,null,p?i.default.createElement(_,null):n),i.default.createElement(s.ModalFooter,null,g))};c.defaultProps={message:"Are you sure?",title:"Warning!",confirmText:"Ok",cancelText:"Cancel",confirmColor:"primary",cancelColor:"",className:"",buttonsComponent:null,size:null,bodyComponent:null,modalProps:{}},c.propTypes={onClose:r.default.func.isRequired,message:r.default.node,title:r.default.node,confirmText:r.default.node,cancelText:r.default.node,confirmColor:r.default.string,cancelColor:r.default.string,className:r.default.string,size:r.default.string,buttonsComponent:r.default.func,bodyComponent:r.default.func,modalProps:r.default.object};var u=c;t.default=u},216:function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=r(n(594)),o=n(518),i=r(n(560));function r(e){return e&&e.__esModule?e:{default:e}}function s(){return s=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&(e[a]=n[a])}return e},s.apply(this,arguments)}var l=function(e){return new Promise((function(t){var n=document.createElement("div");(0,o.render)(a.default.createElement(i.default,s({},e,{onClose:function(e){(0,o.unmountComponentAtNode)(n),n=null,t(e)}})),n)}))};t.default=l},923:function(e){e.exports=DeepFreezeStrict},657:function(e){e.exports=EmotionCssCacheProvider},623:function(e){e.exports=FieldHolder},207:function(e){e.exports=Injector},950:function(e){e.exports=IsomorphicFetch},935:function(e){e.exports=PropTypes},594:function(e){e.exports=React},518:function(e){e.exports=ReactDom},40:function(e){e.exports=ReactRedux},315:function(e){e.exports=ReactSelectCreatable},556:function(e){e.exports=Reactstrap},367:function(e){e.exports=Redux},381:function(e){e.exports=ReduxForm},898:function(e){e.exports=SilverStripeComponent},304:function(e){e.exports=classnames},432:function(e){e.exports=getFormState},815:function(e){e.exports=i18n},669:function(e){e.exports=jQuery},125:function(e){e.exports=ssUrlLib}},t={};function n(a){var o=t[a];if(void 0!==o)return o.exports;var i=t[a]={exports:{}};return e[a](i,i.exports,n),i.exports}n(586),n(677),n(881),n(55),n(739),n(978),n(38)}();

View File

@ -123,16 +123,16 @@ $.entwine('ss', function($){
* @param {string} defaultChildClass
*/
updateSelectionFilter: function(disallowedChildren, defaultChildClass) {
var currentSelection = this.find('#Form_AddForm_PageType div.radio.selected')[0];
var currentSelection = this.find('#Form_AddForm_RecordType div.radio.selected')[0];
var keepSelection = false;
// Limit selection
var allAllowed = null; // troolian
this.find('#Form_AddForm_PageType div.radio').each(function (i, el) {
this.find('#Form_AddForm_RecordType div.radio').each(function (i, el) {
var className = $(this).find('input').val(),
isAllowed = ($.inArray(className, disallowedChildren) === -1);
// Avoid changing the selected pagetype if still allowed
// Avoid changing the selected record type if still allowed
if (el === currentSelection && isAllowed) {
keepSelection = true;
}
@ -153,16 +153,16 @@ $.entwine('ss', function($){
var selectedEl = $(currentSelection).parents('li:first');
} else if (defaultChildClass) {
var selectedEl = this
.find('#Form_AddForm_PageType div.radio input[value=' + defaultChildClass + ']')
.find('#Form_AddForm_RecordType div.radio input[value=' + defaultChildClass + ']')
.parents('li:first');
} else {
var selectedEl = this.find('#Form_AddForm_PageType div.radio:not(.disabled):first');
var selectedEl = this.find('#Form_AddForm_RecordType div.radio:not(.disabled):first');
}
selectedEl.setSelected(true);
selectedEl.siblings().setSelected(false);
// Disable the "Create" button if none of the pagetypes are available
if(this.find('#Form_AddForm_PageType div.radio:not(.disabled)').length) {
// Disable the "Create" button if none of the record types are available
if(this.find('#Form_AddForm_RecordType div.radio:not(.disabled)').length) {
this.find('button[name=action_doAdd]').removeAttr('disabled');
} else {
this.find('button[name=action_doAdd]').attr('disabled', 'disabled');
@ -172,7 +172,7 @@ $.entwine('ss', function($){
}
});
$(".cms-add-form #Form_AddForm_PageType div.radio").entwine({
$(".cms-add-form #Form_AddForm_RecordType div.radio").entwine({
onclick: function(e) {
this.setSelected(true);
},

View File

@ -13,10 +13,12 @@ use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish;
use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish;
use SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search;
use SilverStripe\CMS\Model\CurrentPageIdentifier;
use SilverStripe\CMS\Forms\CMSMainAddForm;
use SilverStripe\CMS\Model\CurrentRecordIdentifier;
use SilverStripe\CMS\Model\RedirectorPage;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\CMS\Model\VirtualPage;
use SilverStripe\CMS\Search\SearchForm;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
@ -25,7 +27,6 @@ use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\PjaxResponseNegotiator;
use SilverStripe\Core\Cache\MemberCacheFlusher;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Flushable;
use SilverStripe\Core\Injector\Injector;
@ -47,10 +48,8 @@ use SilverStripe\Forms\LabelField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextField;
use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\FieldType\DBHTMLText;
@ -60,7 +59,6 @@ use SilverStripe\ORM\Hierarchy\MarkedSet;
use SilverStripe\Model\List\SS_List;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Security\InheritedPermissions;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use SilverStripe\Security\PermissionProvider;
use SilverStripe\Security\Security;
@ -71,6 +69,7 @@ use SilverStripe\Versioned\ChangeSetItem;
use SilverStripe\Versioned\Versioned;
use SilverStripe\VersionedAdmin\Controllers\CMSPageHistoryViewerController;
use SilverStripe\Model\ArrayData;
use SilverStripe\Versioned\RecursivePublishable;
use SilverStripe\View\Requirements;
/**
@ -78,53 +77,47 @@ use SilverStripe\View\Requirements;
*
* This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
* admin menu.
*
* @mixin LeftAndMainPageIconsExtension
*/
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider, Flushable, MemberCacheFlusher
class CMSMain extends LeftAndMain implements CurrentRecordIdentifier, PermissionProvider, Flushable, MemberCacheFlusher
{
/**
* Unique ID for page icons CSS block
*/
const PAGE_ICONS_ID = 'PageIcons';
public const PAGE_ICONS_ID = 'PageIcons'; // @TODO AHHHH!!!
private static $url_segment = 'pages';
private static string $url_segment = 'pages';
private static $url_rule = '/$Action/$ID/$OtherID';
private static string $url_rule = '/$Action/$ID/$OtherID';
// Maintain a lower priority than other administration sections
// so that Director does not think they are actions of CMSMain
private static $url_priority = 39;
private static int $url_priority = 39;
private static $menu_title = 'Edit Page';
private static $menu_title = 'Pages';
private static $menu_icon_class = 'font-icon-sitemap';
private static string $menu_icon_class = 'font-icon-sitemap';
private static $menu_priority = 10;
private static int $menu_priority = 10;
private static $tree_class = SiteTree::class;
private static string $tree_class = SiteTree::class;
private static $session_namespace = CMSMain::class;
private static string $session_namespace = CMSMain::class;
private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
private static string|array $required_permission_codes = 'CMS_ACCESS_CMSMain';
/**
* Should the archive warning message be dynamic based on the specific content? This is slow on larger sites and can be disabled.
*
* @config
* @var bool
*/
private static $enable_dynamic_archive_warning_message = true;
private static bool $enable_dynamic_archive_warning_message = true;
/**
* Amount of results showing on a single page.
*
* @config
* @var int
*/
private static $page_length = 15;
private static int $page_length = 15;
private static $allowed_actions = [
private static array $allowed_actions = [
'add',
'AddForm',
'archive',
'deleteitems',
'DeleteItemsForm',
@ -138,7 +131,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'EditForm',
'schema',
'SearchForm',
'SiteTreeAsUL',
'TreeAsUL',
'getshowdeletedsubtree',
'savetreenode',
'getsubtree',
@ -150,26 +143,26 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'childfilter',
];
private static $url_handlers = [
private static array $url_handlers = [
'EditForm/$ID' => 'EditForm',
];
private static $casting = [
private static array $casting = [
'TreeIsFiltered' => 'Boolean',
'AddForm' => 'HTMLFragment',
'LinkPages' => 'Text',
'LinkRecords' => 'Text',
'Link' => 'Text',
'ListViewForm' => 'HTMLFragment',
'ExtraTreeTools' => 'HTMLFragment',
'PageList' => 'HTMLFragment',
'PageListSidebar' => 'HTMLFragment',
'SiteTreeHints' => 'HTMLFragment',
'RecordList' => 'HTMLFragment',
'RecordListSidebar' => 'HTMLFragment',
'TreeHints' => 'HTMLFragment',
'SecurityID' => 'Text',
'SiteTreeAsUL' => 'HTMLFragment',
'TreeAsUL' => 'HTMLFragment',
];
private static $dependencies = [
'HintsCache' => '%$' . CacheInterface::class . '.CMSMain_SiteTreeHints',
private static array $dependencies = [
'HintsCache' => '%$' . CacheInterface::class . '.CMSMain_TreeHints',
];
/**
@ -197,7 +190,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// In case we're not showing a specific record, explicitly remove any session state,
// to avoid it being highlighted in the tree, and causing an edit form to show.
if (!$request->param('Action')) {
$this->setCurrentPageID(null);
$this->setCurrentRecordID(null);
}
return parent::index($request);
@ -216,51 +209,42 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Get pages listing area
*
* @return DBHTMLText
* Get record listing area
*/
public function PageList()
public function RecordList(): DBHTMLText
{
return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
return $this->renderWith($this->getTemplatesWithSuffix('_RecordList'));
}
/**
* Page list view for edit-form
*
* @return DBHTMLText
* Record list view for edit-form
*/
public function PageListSidebar()
public function RecordListSidebar(): DBHTMLText
{
return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
return $this->renderWith($this->getTemplatesWithSuffix('_RecordList_Sidebar'));
}
/**
* If this is set to true, the "switchView" context in the
* template is shown, with links to the staging and publish site.
*
* @return boolean
*/
public function ShowSwitchView()
public function ShowSwitchView(): bool
{
return true;
}
/**
* Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
* Overloads the LeftAndMain::ShowView. Allows to pass a record as a parameter, so we are able
* to switch view also for archived versions.
*
* @param SiteTree $page
* @return array
*/
public function SwitchView($page = null)
public function SwitchView(?DataObject $record = null): array
{
if (!$page) {
$page = $this->currentPage();
if (!$record) {
$record = $this->currentRecord();
}
if ($page) {
$nav = SilverStripeNavigator::get_for_record($page);
if ($record) {
$nav = SilverStripeNavigator::get_for_record($record);
return $nav['items'];
}
}
@ -289,14 +273,14 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $link;
}
public function LinkPages()
public function LinkRecords()
{
return CMSPagesController::singleton()->Link();
}
public function LinkPagesWithSearch()
public function LinkRecordsWithSearch()
{
return $this->LinkWithSearch($this->LinkPages());
return $this->LinkWithSearch($this->LinkRecords());
}
/**
@ -306,7 +290,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/
public function LinkTreeView()
{
// Tree view is just default link to main pages section (no /treeview suffix)
// Tree view is just default link to main section (no /treeview suffix)
return CMSMain::singleton()->Link();
}
@ -317,12 +301,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/
public function LinkListView()
{
// Note : Force redirect to top level page controller (no parentid)
// Note : Force redirect to top level record controller (no parentid)
return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
}
/**
* Link to list view for children of a parent page
* Link to list view for children of a parent record
*
* @param int|string $parentID Literal parentID, or placeholder (e.g. '%d') for
* client side substitution
@ -366,28 +350,31 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Get the link for editing a page.
* Get the link for editing a record.
*
* @see CMSEditLinkExtension::getCMSEditLinkForManagedDataObject()
*/
public function getCMSEditLinkForManagedDataObject(SiteTree $obj): string
public function getCMSEditLinkForManagedDataObject(DataObject $obj): string
{
return Controller::join_links(CMSPageEditController::singleton()->Link('show'), $obj->ID);
}
public function LinkPageEdit($id = null)
public function LinkRecordEdit($id = null)
{
if (!$id) {
$id = $this->currentPageID();
$id = $this->currentRecordID();
}
return $this->LinkWithSearch(
Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
);
}
public function LinkPageSettings()
public function LinkRecordSettings()
{
if ($id = $this->currentPageID()) {
if (!DataObject::singleton($this->getModelClass())->hasMethod('getSettingsFields')) { // @TODO This is awful, I'd much rather it just be part of the main form.
return null;
}
if ($id = $this->currentRecordID()) {
return $this->LinkWithSearch(
Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
);
@ -396,10 +383,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
}
public function LinkPageHistory()
public function LinkRecordHistory()
{
$controller = Injector::inst()->get(CMSPageHistoryViewerController::class);
if (($id = $this->currentPageID()) && $controller) {
if (($id = $this->currentRecordID()) && $controller) {
if ($controller) {
return $this->LinkWithSearch(
Controller::join_links($controller->Link('show'), $id)
@ -463,10 +450,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $link;
}
public function LinkPageAdd($extra = null, $placeholders = null)
public function LinkRecordAdd($extra = null, $placeholders = null)
{
$link = CMSPageAddController::singleton()->Link();
$this->extend('updateLinkPageAdd', $link);
$link = $this->Link('add');
$this->extend('updateLinkRecordAdd', $link);
if ($extra) {
$link = Controller::join_links($link, $extra);
@ -484,10 +471,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/
public function LinkPreview()
{
$record = $this->getRecord($this->currentPageID());
$record = $this->getRecord($this->currentRecordID());
$baseLink = Director::absoluteBaseURL();
if ($record && $record instanceof SiteTree) {
// if we are an external redirector don't show a link
if ($record && $record->hasMethod('Link')) {
// if we are an external redirector don't show a link //@TODO generalise this!!!!!
if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
$baseLink = false;
} else {
@ -497,12 +484,27 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $baseLink;
}
public function add()
{
if ($this->getRequest()->isAjax()) {
return $this->AddForm()->forTemplate();
}
return $this->render([
'Content' => DBHTMLText::create()->setValue($this->AddForm()->forTemplate()),
]);
}
public function AddForm(): Form
{
return CMSMainAddForm::create($this);
}
/**
* Return the entire site tree as a nested set of ULs
*/
public function SiteTreeAsUL()
public function TreeAsUL()
{
$treeClass = $this->config()->get('tree_class');
$treeClass = $this->getModelClass();
$filter = $this->getSearchFilter();
DataObject::singleton($treeClass)->prepopulateTreeDataCache(null, [
@ -510,9 +512,9 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'numChildrenMethod' => $filter ? $filter->getNumChildrenMethod() : 'numChildren',
]);
$html = $this->getSiteTreeFor($treeClass);
$html = $this->getTreeFor($treeClass);
$this->extend('updateSiteTreeAsUL', $html);
$this->extend('updateTreeAsUL', $html);
return $html;
}
@ -528,9 +530,9 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
* @param string $numChildrenMethod
* @param callable $filterFunction
* @param int $nodeCountThreshold
* @return string Nested unordered list with links to each page
* @return string Nested unordered list with links to each record
*/
public function getSiteTreeFor(
public function getTreeFor(
$className,
$rootID = null,
$childrenMethod = null,
@ -550,7 +552,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
if (!$filterFunction) {
$filterFunction = function ($node) use ($filter) {
return $filter->isPageIncluded($node);
return $filter->isRecordIncluded($node);
};
}
}
@ -568,20 +570,23 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Mark tree from this node
$markingSet->markPartialTree();
// Ensure current page is exposed
$currentPage = $this->currentPage();
if ($currentPage) {
$markingSet->markToExpose($currentPage);
// Ensure current record is exposed
$currentRecord = $this->currentRecord();
if ($currentRecord) {
$markingSet->markToExpose($currentRecord);
}
// Pre-cache permissions
$checker = SiteTree::getPermissionChecker();
$modelClass = $this->getModelClass();
$singleton = DataObject::singleton($modelClass);
$checker = $singleton->hasMethod('getPermissionChecker') ? $modelClass::getPermissionChecker() : null; // @TODO eww why is it static?
if ($checker instanceof InheritedPermissions) {
$checker->prePopulatePermissionCache(
InheritedPermissions::EDIT,
$markingSet->markedNodeIDs()
);
}
// @TODO if we don't have inherited permissions, make sure we still DO do permission checks where needed!!
// Render using full-subtree template
return $markingSet->renderChildren(
@ -590,7 +595,6 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
);
}
/**
* Get callback to determine template customisations for nodes
*
@ -599,14 +603,14 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
protected function getTreeNodeCustomisations()
{
$rootTitle = $this->getCMSTreeTitle();
return function (SiteTree $node) use ($rootTitle) {
return function (DataObject $node) use ($rootTitle) {
return [
'listViewLink' => $this->LinkListViewChildren($node->ID),
'rootTitle' => $rootTitle,
'extraClass' => $this->getTreeNodeClasses($node),
'Title' => _t(
CMSMain::class . '.PAGETYPE_TITLE',
'(Page type: {type}) {title}',
CMSMain::class . '.RECORD_TYPE_TITLE',
'(Record type: {type}) {title}',
[
'type' => $node->i18n_singular_name(),
'title' => $node->Title,
@ -617,15 +621,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Get extra CSS classes for a page's tree node
*
* @param SiteTree $node
* @return string
* Get extra CSS classes for a record's tree node
*/
public function getTreeNodeClasses(SiteTree $node)
public function getTreeNodeClasses(DataObject $node): string
{
// Get classes from object
$classes = $node->CMSTreeClasses();
$classes = $node->CMSTreeClasses(); // @TODO that's obviously not a thing.
// Get status flag classes
$flags = $node->getStatusFlags();
@ -638,7 +639,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Get additional filter classes
$filter = $this->getSearchFilter();
if ($filter && ($filterClasses = $filter->getPageClasses($node))) {
if ($filter && ($filterClasses = $filter->getRecordClasses($node))) { // @TODO rename getRecordClasses or similar (though this is probably part of https://github.com/silverstripe/silverstripe-cms/issues/2949)
if (is_array($filterClasses)) {
$filterClasses = implode(' ', $filterClasses);
}
@ -654,8 +655,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/
public function getsubtree(HTTPRequest $request): HTTPResponse
{
$html = $this->getSiteTreeFor(
$this->config()->get('tree_class'),
$html = $this->getTreeFor(
$this->getModelClass(),
$request->getVar('ID'),
null,
null,
@ -687,7 +688,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$record = $this->getRecord($id);
if (!$record) {
continue; // In case a page is no longer available
continue; // In case a record is no longer available
}
// Create marking set with sole marked root
@ -700,7 +701,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
$prev = null;
$className = $this->config()->get('tree_class');
$className = $this->getModelClass();
$next = DataObject::get($className)
->filter('ParentID', $record->ParentID)
->filter('Sort:GreaterThan', $record->Sort)
@ -750,17 +751,17 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
if (!SecurityToken::inst()->checkRequest($request)) {
$this->httpError(400);
}
if (!$this->CanOrganiseSitetree()) {
if (!$this->CanOrganiseTree()) {
$this->httpError(
403,
_t(
__CLASS__.'.CANT_REORGANISE',
"You do not have permission to rearange the site tree. Your change was not saved."
__CLASS__.'.CANT_REORGANISE2',
"You do not have permission to rearange the tree. Your change was not saved.",
)
);
}
$className = $this->config()->get('tree_class');
$className = $this->getModelClass();
$id = $request->requestVar('ID');
$parentID = $request->requestVar('ParentID');
if (!is_numeric($id) || !is_numeric($parentID)) {
@ -768,26 +769,25 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
// Check record exists in the DB
/** @var SiteTree $node */
$node = DataObject::get_by_id($className, $id);
if (!$node) {
$this->httpError(
500,
_t(
__CLASS__.'.PLEASESAVE',
"Please Save Page: This page could not be updated because it hasn't been saved yet."
__CLASS__.'.PLEASESAVE2',
"Please Save Record: This record could not be updated because it hasn't been saved yet."
)
);
}
// Check top level permissions
$root = $node->getParentType();
$root = $node->getParentType(); // @TODO generalise. POC has `$node->ParentID == 0 ? 'root' : 'subpage';`
if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
$this->httpError(
403,
_t(
__CLASS__.'.CANT_REORGANISE',
"You do not have permission to alter Top level pages. Your change was not saved."
__CLASS__.'.CANT_REORGANISE_TOPLEVEL',
'You do not have permission to alter Top level records. Your change was not saved.'
)
);
}
@ -805,20 +805,20 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$node->write();
$statusUpdates['modified'][$node->ID] = [
'TreeTitle' => $node->TreeTitle
'TreeTitle' => $node->TreeTitle // @TODO generalise.
];
// Update all dependent pages
// Update all dependent pages // @TODO generalise!! We shouldn't reference explicitly page types here.
$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
foreach ($virtualPages as $virtualPage) {
$statusUpdates['modified'][$virtualPage->ID] = [
'TreeTitle' => $virtualPage->TreeTitle
'TreeTitle' => $virtualPage->TreeTitle // @TODO generalise.
];
}
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.') ?? '')
rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL2', 'Reorganised the tree successfully.') ?? '')
);
}
@ -830,7 +830,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$node->Sort = ++$counter;
$node->write();
$statusUpdates['modified'][$node->ID] = [
'TreeTitle' => $node->TreeTitle
'TreeTitle' => $node->TreeTitle // @TODO generalise.
];
} elseif (is_numeric($id)) {
// Nodes that weren't "actually moved" shouldn't be registered as
@ -846,7 +846,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.') ?? '')
rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL2', 'Reorganised the tree successfully.') ?? '')
);
}
@ -857,64 +857,43 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Whether the current member has the permission to reorganise SiteTree objects.
* @return bool
* Whether the current member has the permission to reorganise records.
*/
public function CanOrganiseSitetree()
public function CanOrganiseTree(): bool
{
return Permission::check('SITETREE_REORGANISE');
return (bool) Permission::check('SITETREE_REORGANISE'); // @TODO model needs a method or config to say "this is the permission for that!!"
}
/**
* @return boolean
* Whether the tree has been filtered in this request or not.
*/
public function TreeIsFiltered()
public function TreeIsFiltered(): bool
{
$query = $this->getRequest()->getVar('q');
return !empty($query);
}
public function ExtraTreeTools()
public function ExtraTreeTools(): string
{
$html = '';
$this->extend('updateExtraTreeTools', $html);
return $html;
}
/**
* This provides information required to generate the search form
* and can be modified on extensions through updateSearchContext
*
* @return \SilverStripe\ORM\Search\SearchContext
*/
public function getSearchContext()
{
$context = SiteTree::singleton()->getDefaultSearchContext();
$this->extend('updateSearchContext', $context);
return $context;
}
/**
* Returns the search form schema for the current model
*
* @return string
*/
public function getSearchFieldSchema()
public function getSearchFieldSchema(): string
{
$schemaUrl = $this->Link('schema/SearchForm');
$context = $this->getSearchContext();
$singleton = DataObject::singleton($this->getModelClass());
$context = $singleton->getDefaultSearchContext();
$params = $this->getRequest()->requestVar('q') ?: [];
$context->setSearchParams($params);
$placeholder = _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERLABELTEXT', 'Search') . ' "' .
SiteTree::singleton()->i18n_plural_name() . '"';
$placeholder = _t(SearchForm::class . '.FILTERLABELTEXT2', 'Search "{model}"', ['model' => $singleton->i18n_plural_name()]);
$searchParams = $context->getSearchParams();
$searchParams = array_combine(array_map(function ($key) {
return 'Search__' . $key;
}, array_keys($searchParams ?? [])), $searchParams ?? []);
@ -930,50 +909,59 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Returns a Form for page searching for use in templates.
* Returns a Form for record searching for use in templates.
*
* Can be modified from a decorator by a 'updateSearchForm' method
*
* @return Form
*/
public function getSearchForm()
public function getSearchForm(): Form
{
$modelClass = $this->getModelClass();
$singleton = DataObject::singleton($modelClass);
// Create the fields
$dateFrom = DateField::create(
'Search__LastEditedFrom',
_t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATEFROM', 'From')
_t(SearchForm::class . '.FILTERDATEFROM', 'From')
)->setLocale(Security::getCurrentUser()->Locale);
$dateTo = DateField::create(
'Search__LastEditedTo',
_t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATETO', 'To')
_t(SearchForm::class . '.FILTERDATETO', 'To')
)->setLocale(Security::getCurrentUser()->Locale);
$filters = CMSSiteTreeFilter::get_all_filters();
// Remove 'All pages' as we set that to empty/default value
// Remove 'All records' as we set that to empty/default value
unset($filters[CMSSiteTreeFilter_Search::class]);
$pageFilter = DropdownField::create(
$recordFilter = DropdownField::create(
'Search__FilterClass',
_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGES', 'Page status'),
_t(SearchForm::class . '.RECORD_STATUS', '{model} status', ['model' => $singleton->i18n_singular_name()]),
$filters
);
$pageFilter->setEmptyString(_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGESALLOPT', 'All pages'));
$pageClasses = DropdownField::create(
$recordFilter->setEmptyString(_t(
SearchForm::class . '.RECORDS_ALLOPT',
'All {model}',
['model' => mb_strtolower($singleton->i18n_plural_name())]
));
$classes = DropdownField::create(
'Search__ClassName',
_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
$this->getPageTypes()
_t(
SearchForm::class . '.RECORD_TYPEOPT',
'{model} type',
'Dropdown for limiting search to a record type',
['model' => $singleton->i18n_singular_name()]
),
$this->getRecordTypes()
);
$pageClasses->setEmptyString(_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEANYOPT', 'Any'));
$classes->setEmptyString(_t(SearchForm::class . '.RECORD_TYPEANYOPT', 'Any'));
// Group the Datefields
$dateGroup = FieldGroup::create(
_t('SilverStripe\\CMS\\Search\\SearchForm.PAGEFILTERDATEHEADING', 'Last edited'),
_t(SearchForm::class . '.RECORD_FILTERDATEHEADING', 'Last edited'),
[$dateFrom, $dateTo]
)->setName('Search__LastEdited')
->addExtraClass('fieldgroup--fill-width');
// Create the Field list
$fields = new FieldList(
$pageFilter,
$pageClasses,
$recordFilter,
$classes,
$dateGroup
);
@ -1000,18 +988,16 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Returns a sorted array suitable for a dropdown with pagetypes and their translated name
*
* @return array
* Returns a sorted array suitable for a dropdown with classes and their localised name
*/
protected function getPageTypes()
protected function getRecordTypes(): array
{
$pageTypes = [];
foreach (SiteTree::page_type_classes() as $pageTypeClass) {
$pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
$types = [];
foreach (SiteTree::page_type_classes() as $class) { // @TODO generalise!! Not fully into the POC way.
$types[$class] = DataObject::singleton($class)->i18n_singular_name();
}
asort($pageTypes);
return $pageTypes;
asort($types);
return $types;
}
public function doSearch(array $data, Form $form): HTTPResponse
@ -1028,7 +1014,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
{
$breadcrumbs = $this->Breadcrumbs();
if ($breadcrumbs->count() < 2) {
return $this->LinkPages();
return $this->LinkRecords();
}
// Get second from end breadcrumb
return $breadcrumbs
@ -1040,15 +1026,15 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
{
$items = ArrayList::create();
if (($this->getAction() !== 'index') && ($record = $this->currentPage())) {
// The page is being edited
if (($this->getAction() !== 'index') && ($record = $this->currentRecord())) {
// The record is being edited
$this->buildEditFormBreadcrumb($items, $record, $unlinked);
} else {
// Ensure we always have the "Pages" crumb first
// Ensure we always have the admin section crumb first
$this->pushCrumb(
$items,
CMSPagesController::menu_title(),
$unlinked ? false : $this->LinkPages()
$unlinked ? false : $this->LinkRecords()
);
if ($this->TreeIsFiltered()) {
@ -1056,13 +1042,13 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->pushCrumb(
$items,
_t(CMSMain::class . '.SEARCHRESULTS', 'Search results'),
($unlinked) ? false : $this->LinkPages()
($unlinked) ? false : $this->LinkRecords()
);
} elseif ($parentID = $this->getRequest()->getVar('ParentID')) {
// We're navigating the listview. ParentID is the page whose
// We're navigating the listview. ParentID is the record whose
// children are currently displayed.
if ($page = SiteTree::get()->byID($parentID)) {
$this->buildListViewBreadcrumb($items, $page);
if ($record = DataObject::get($this->getModelClass())->byID($parentID)) {
$this->buildListViewBreadcrumb($items, $record);
}
}
}
@ -1084,30 +1070,32 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Build Breadcrumb for the Edit page form. Each crumb links back to its own edit form.
* Build Breadcrumb for the Edit form. Each crumb links back to its own edit form.
*/
private function buildEditFormBreadcrumb(ArrayList $items, SiteTree $page, bool $unlinked): void
private function buildEditFormBreadcrumb(ArrayList $items, DataObject $record, bool $unlinked): void
{
// Find all ancestors of the provided page
$ancestors = $page->getAncestors(true);
// Find all ancestors of the provided record
/** @var DataObject&Hierarchy $record */
$ancestors = $record->getAncestors(true);
$ancestors = array_reverse($ancestors->toArray() ?? []);
foreach ($ancestors as $ancestor) {
// Link to the ancestor's edit form
$this->pushCrumb(
$items,
$ancestor->getMenuTitle(),
$ancestor->getMenuTitle(), // @TODO generalise!!
$unlinked ? false : $ancestor->getCMSEditLink()
);
}
}
/**
* Build Breadcrumb for the List view. Each crumb links to the list view for that page.
* Build Breadcrumb for the List view. Each crumb links to the list view for that record.
*/
private function buildListViewBreadcrumb(ArrayList $items, SiteTree $page): void
private function buildListViewBreadcrumb(ArrayList $items, DataObject $record): void
{
// Find all ancestors of the provided page
$ancestors = $page->getAncestors(true);
// Find all ancestors of the provided record
/** @var DataObject&Hierarchy $record */
$ancestors = $record->getAncestors(true);
$ancestors = array_reverse($ancestors->toArray() ?? []);
//turns the title and link of the breadcrumbs into template-friendly variables
@ -1121,7 +1109,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$params['ParentID'] = $ancestor->ID;
$this->pushCrumb(
$items,
$ancestor->getMenuTitle(),
$ancestor->getMenuTitle(), // @TODO generalise!!
Controller::join_links($this->Link(), '?' . http_build_query($params ?? []))
);
}
@ -1130,13 +1118,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* Create serialized JSON string with site tree hints data to be injected into
* 'data-hints' attribute of root node of jsTree.
*
* @return string Serialized JSON
*/
public function SiteTreeHints()
public function TreeHints(): string // @TODO rename
{
$classes = SiteTree::page_type_classes();
$memberID = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
$classes = SiteTree::page_type_classes(); // @TODO generalise!!
$memberID = Security::getCurrentUser()?->ID ?? 0;
$cache = $this->getHintsCache();
$cacheKey = $this->generateHintsCacheKey($memberID);
$json = $cache->get($cacheKey);
@ -1154,12 +1140,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$def['Root']['disallowedChildren'] = [];
// Contains all possible classes to support UI controls listing them all,
// such as the "add page here" context menu.
// such as the "add record here" context menu.
$def['All'] = [];
// Identify disallows and set globals
foreach ($classes as $class) {
$obj = singleton($class);
$obj = DataObject::singleton($class);
if ($obj instanceof HiddenClass) {
continue;
}
@ -1170,8 +1156,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
];
// Check if can be created at the root
$needsPerm = $obj->config()->get('need_permission');
if (!$obj->config()->get('can_be_root')
$needsPerm = $obj::config()->get('need_permission');
if ($obj::config()->get('can_be_root') === false
|| (!array_key_exists($class, $canCreate ?? []) || !$canCreate[$class])
|| ($needsPerm && !$this->can($needsPerm))
) {
@ -1181,18 +1167,18 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Hint data specific to the class
$def[$class] = [];
$defaultChild = $obj->defaultChild();
if ($defaultChild !== 'Page' && $defaultChild !== null) {
$defaultChild = $obj->defaultChild(); // @TODO generalise!!
if ($defaultChild !== 'Page' && $defaultChild !== null) { // @TODO Find out where that 'Page' string comes from and fix it
$def[$class]['defaultChild'] = $defaultChild;
}
$defaultParent = $obj->defaultParent();
$defaultParent = $obj->defaultParent(); // @TODO generalise!!
if ($defaultParent !== 1 && $defaultParent !== null) {
$def[$class]['defaultParent'] = $defaultParent;
}
}
$this->extend('updateSiteTreeHints', $def);
$this->extend('updateTreeHints', $def);
$json = json_encode($def);
$cache->set($cacheKey, $json);
@ -1202,24 +1188,22 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* Populates an array of classes in the CMS
* which allows the user to change the page type.
*
* @return SS_List
* which allows the user to change the record's ClassName field.
*/
public function PageTypes()
public function RecordTypes(): SS_List
{
$classes = SiteTree::page_type_classes();
$classes = SiteTree::page_type_classes(); // @TODO generalise!!
$result = new ArrayList();
foreach ($classes as $class) {
$instance = SiteTree::singleton($class);
$instance = DataObject::singleton($class);
if ($instance instanceof HiddenClass) {
continue;
}
// skip this type if it is restricted
$needPermissions = $instance->config()->get('need_permission');
$needPermissions = $instance::config()->get('need_permission'); // @TODO consider renaming that since it's so vague
if ($needPermissions && !$this->can($needPermissions)) {
continue;
}
@ -1227,8 +1211,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$result->push(new ArrayData([
'ClassName' => $class,
'AddAction' => $instance->i18n_singular_name(),
'Description' => $instance->i18n_classDescription(),
'IconURL' => $instance->getPageIconURL(),
'Description' => $instance->i18n_classDescription(), // @TODO generalise!!
'IconURL' => $instance->getPageIconURL(), // @TODO generalise!!
'Title' => $instance->i18n_singular_name(),
]));
}
@ -1243,20 +1227,22 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*
* @param int $id Record ID
* @param int $versionID optional Version id of the given record
* @return SiteTree
*/
public function getRecord($id, $versionID = null)
public function getRecord($id, ?int $versionID = null): ?DataObject
{
if (!$id) {
return null;
}
$treeClass = $this->config()->get('tree_class');
if ($id instanceof $treeClass) {
$modelClass = $this->getModelClass();
if ($id instanceof $modelClass) {
return $id;
}
if (substr($id ?? '', 0, 3) == 'new') {
return $this->getNewItem($id);
}
if ($id === 'singleton') {
return DataObject::singleton($modelClass);
}
if (!is_numeric($id)) {
return null;
}
@ -1267,25 +1253,28 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$versionID = (int) $this->getRequest()->getVar('Version');
}
/** @var SiteTree $record */
$isVersioned = $modelClass::has_extension(Versioned::class);
if ($versionID) {
$record = Versioned::get_version($treeClass, $id, $versionID);
if (!$isVersioned) {
throw new HTTPResponse_Exception("Cannot get a version of non-versioned $modelClass record", 400);
}
$record = Versioned::get_version($modelClass, $id, $versionID);
} else {
$record = DataObject::get_by_id($treeClass, $id);
$record = DataObject::get_by_id($modelClass, $id);
}
// Then, try getting a record from the live site
if (!$record) {
// $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
if (!$record && $isVersioned) {
// $record = Versioned::get_one_by_stage($modelClass, "Live", "\"$modelClass\".\"ID\" = $id");
Versioned::set_stage(Versioned::LIVE);
singleton($treeClass)->flushCache();
DataObject::singleton($modelClass)->flushCache();
$record = DataObject::get_by_id($treeClass, $id);
$record = DataObject::get_by_id($modelClass, $id);
}
// Then, try getting a deleted record
if (!$record) {
$record = Versioned::get_latest_version($treeClass, $id);
if (!$record && $isVersioned) {
$record = Versioned::get_latest_version($modelClass, $id);
}
// Set the reading mode back to what it was.
@ -1298,11 +1287,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
* {@inheritdoc}
*
* @param HTTPRequest $request
* @return Form
*/
public function EditForm($request = null)
public function EditForm($request = null): Form
{
// set page ID from request
// set record ID from request
if ($request) {
// Validate id is present
$id = $request->param('ID');
@ -1310,7 +1298,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->httpError(400);
return null;
}
$this->setCurrentPageID($id);
$this->setCurrentRecordID($id);
}
return $this->getEditForm();
}
@ -1318,13 +1306,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* @param int $id
* @param FieldList $fields
* @return Form
*/
public function getEditForm($id = null, $fields = null)
public function getEditForm($id = null, $fields = null): Form
{
// Get record
if (!$id) {
$id = $this->currentPageID();
$id = $this->currentRecordID();
}
$record = $this->getRecord($id);
@ -1340,18 +1327,18 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Add extra fields
$deletedFromStage = !$record->isOnDraft();
$fields->push($idField = new HiddenField("ID", false, $id));
$fields->push(new HiddenField("ID", false, $id));
// Necessary for different subsites
$fields->push($liveLinkField = new HiddenField("AbsoluteLink", false, $record->AbsoluteLink()));
$fields->push($liveLinkField = new HiddenField("AbsoluteLink", false, $record->AbsoluteLink())); // @TODO generalise!!
$fields->push($liveLinkField = new HiddenField("LiveLink"));
$fields->push($stageLinkField = new HiddenField("StageLink"));
$fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage"));
$fields->push(new HiddenField("TreeTitle", false, $record->getTreeTitle()));
$fields->push(new HiddenField("TreeTitle", false, $record->getTreeTitle())); // @TODO generalise!!
$archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
// Build preview / live links
$liveLink = $record->getAbsoluteLiveLink();
$liveLink = $record->getAbsoluteLiveLink(); // @TODO generalise!!
if ($liveLink) {
$liveLinkField->setValue($liveLink);
}
@ -1437,10 +1424,10 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $form;
}
public function EmptyForm()
public function EmptyForm(): Form
{
$fields = new FieldList(
new LabelField('PageDoesntExistLabel', _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
new LabelField('RecordDoesntExistLabel', _t(__CLASS__ . '.RECORDNOTEXISTS', "This record doesn't exist"))
);
$form = parent::EmptyForm();
$form->setFields($fields);
@ -1449,39 +1436,32 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Build an archive warning message based on the page's children
*
* @param SiteTree $record
* @return string
* Build an archive warning message based on the record's children
*/
/**
* Build an archive warning message based on the page's children
*
* @param SiteTree $record
* @return string
*/
protected function getArchiveWarningMessage($record)
protected function getArchiveWarningMessage(DataObject $record): string
{
$defaultMessage = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithChildren', 'Warning: This page and all of its child pages will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
$defaultMessage = _t(
LeftAndMain::class . '.ArchiveWarningWithChildren',
'Warning: This record and all of its child records will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?'
);
// Option to disable this feature as it is slow on large sites
if (!$this->config()->enable_dynamic_archive_warning_message) {
if (!static::config()->get('enable_dynamic_archive_warning_message')) {
return $defaultMessage;
}
// Get all page's descendants
// Get all record's descendants
$descendants = [];
$this->collateDescendants([$record->ID], $descendants);
if (!$descendants) {
$descendants = [];
}
// Get the IDs of all changeset including at least one of the pages.
// Get the IDs of all changeset including at least one of the records.
$descendants[] = $record->ID;
$inChangeSetIDs = ChangeSetItem::get()->filter([
'ObjectID' => $descendants,
'ObjectClass' => SiteTree::class
'ObjectClass' => $this->getModelClass(),
])->column('ChangeSetID');
// Count number of affected change set
@ -1496,20 +1476,31 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$numCampaigns = mb_strtolower($numCampaigns ?? '');
if (count($descendants ?? []) > 0 && $affectedChangeSetCount > 0) {
$archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithChildrenAndCampaigns', 'Warning: This page and all of its child pages will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
$archiveWarningMsg = _t(
LeftAndMain::class . '.ArchiveWarningWithChildrenAndCampaigns',
'Warning: This record and all of its child records will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?',
[ 'NumCampaigns' => $numCampaigns ]
);
} elseif (count($descendants ?? []) > 0) {
$archiveWarningMsg = $defaultMessage;
} elseif ($affectedChangeSetCount > 0) {
$archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithCampaigns', 'Warning: This page will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
$archiveWarningMsg = _t(
LeftAndMain::class . '.ArchiveWarningWithCampaigns',
'Warning: This record will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?',
[ 'NumCampaigns' => $numCampaigns ]
);
} else {
$archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarning', 'Warning: This page will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
$archiveWarningMsg = _t(
LeftAndMain::class . '.ArchiveWarning',
'Warning: This record will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?'
);
}
return $archiveWarningMsg;
}
/**
* Find IDs of all descendant pages for the provided ID lists.
* Find IDs of all descendant records for the provided ID lists.
* @param int[] $recordIDs
* @param array $collator
* @return bool
@ -1517,7 +1508,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
protected function collateDescendants($recordIDs, &$collator)
{
$children = SiteTree::get()->filter(['ParentID' => $recordIDs])->column();
$children = DataObject::get($this->getModelClass())->filter(['ParentID' => $recordIDs])->column();
if ($children) {
foreach ($children as $item) {
$collator[] = $item;
@ -1528,10 +1519,9 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return false;
}
/**
* This method exclusively handles deferred ajax requests to render the
* pages tree deferred handler (no pjax-fragment)
* records tree deferred handler (no pjax-fragment)
*
* @return DBHTMLText HTML response with the rendered treeview
*/
@ -1569,21 +1559,21 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Callback to request the list of page types allowed under a given page instance.
* Provides a slower but more precise response over SiteTreeHints
* Callback to request the list of record types allowed under a given record instance.
* Provides a slower but more precise response over TreeHints
*/
public function childfilter(HTTPRequest $request): HTTPResponse
{
// Check valid parent specified
$parentID = $request->requestVar('ParentID');
$parent = SiteTree::get()->byID($parentID);
$parent = DataObject::get($this->getModelClass())->byID($parentID);
if (!$parent || !$parent->exists()) {
$this->httpError(404);
}
// Build hints specific to this class
// Identify disallows and set globals
$classes = SiteTree::page_type_classes();
$classes = SiteTree::page_type_classes(); // @TODO generalise!!
$disallowedChildren = [];
foreach ($classes as $class) {
$obj = singleton($class);
@ -1623,8 +1613,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
* defaulting to no filter and show all pages in first level.
* Returns the records meet a certain criteria as {@see CMSSiteTreeFilter} or the subrecords of a parent record
* defaulting to no filter and show all records in first level.
* Doubles as search results, if any search parameters are set through {@link SearchForm()}.
*
* @param array $params Search filter criteria
@ -1637,7 +1627,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
if ($filter = $this->getQueryFilter($params)) {
return $filter->getFilteredPages();
} else {
$list = DataList::create($this->config()->get('tree_class'));
$list = DataObject::get($this->getModelClass());
$parentID = is_numeric($parentID) ? $parentID : 0;
return $list->filter("ParentID", $parentID);
}
@ -1658,7 +1648,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$gridFieldConfig = GridFieldConfig::create()->addComponents(
Injector::inst()->create(GridFieldSortableHeader::class),
Injector::inst()->create(GridFieldDataColumns::class),
Injector::inst()->createWithArgs(GridFieldPaginator::class, [$this->config()->get('page_length')])
Injector::inst()->createWithArgs(GridFieldPaginator::class, [static::config()->get('page_length')])
);
if ($parentID) {
$linkSpec = $this->LinkListViewChildren('%d');
@ -1667,17 +1657,18 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
->setLinkSpec($linkSpec)
->setAttributes(['data-pjax-target' => 'ListViewForm,Breadcrumbs'])
);
$this->setCurrentPageID($parentID);
$this->setCurrentRecordID($parentID);
}
$gridField = GridField::create('Page', 'Pages', $list, $gridFieldConfig);
$gridField = GridField::create('Record', 'Records', $list, $gridFieldConfig); // @TODO make title string i18n
$gridField->setAttribute('cms-loading-ignore-url-params', true);
$columns = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class);
// Don't allow navigating into children nodes on filtered lists
$modelClass = $this->getModelClass();
$fields = [
'getTreeTitle' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETITLE', 'Page Title'),
'i18n_singular_name' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETYPE', 'Page Type'),
'LastEdited' => _t('SilverStripe\\CMS\\Model\\SiteTree.LASTUPDATED', 'Last Updated'),
'getTreeTitle' => _t($modelClass . '.TREETITLE', 'Title'),
'i18n_singular_name' => _t($modelClass . '.TREETYPE', 'Record Type'),
'LastEdited' => _t($modelClass . '.LASTUPDATED', 'Last Updated'),
];
$sortableHeader = $gridField->getConfig()->getComponentByType(GridFieldSortableHeader::class);
$sortableHeader->setFieldSorting(['getTreeTitle' => 'Title']);
@ -1696,24 +1687,24 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$columns->setFieldFormatting([
'listChildrenLink' => function ($value, &$item) {
/** @var SiteTree $item */
$num = $item ? $item->numChildren() : null;
/** @var DataObject&Hierarchy $item */
$num = $item?->numChildren();
if ($num) {
return sprintf(
'<a class="btn btn-secondary btn--no-text btn--icon-large font-icon-right-dir cms-panel-link list-children-link" data-pjax-target="ListViewForm,Breadcrumbs" href="%s"><span class="sr-only">%s child pages</span></a>',
'<a class="btn btn-secondary btn--no-text btn--icon-large font-icon-right-dir cms-panel-link list-children-link" data-pjax-target="ListViewForm,Breadcrumbs" href="%s"><span class="sr-only">%s child pages</span></a>', // @TODO generalise and i18n-ify
$this->LinkListViewChildren((int)$item->ID),
$num
);
}
},
'getTreeTitle' => function ($value, &$item) {
/** @var SiteTree $item */
/** @var DataObject $item */
$title = sprintf(
'<a class="action-detail" href="%s">%s</a>',
$item->getCMSEditLink(),
$item->TreeTitle // returns HTML, does its own escaping
$item->TreeTitle // returns HTML, does its own escaping // @TODO generalise!!
);
$breadcrumbs = $item->Breadcrumbs(20, true, false, true, '/');
$breadcrumbs = $item->Breadcrumbs(20, true, false, true, '/'); // @TODO generalise!!
// Remove item's tile
$breadcrumbs = preg_replace('/[^\/]+$/', '', trim($breadcrumbs ?? ''));
// Trim spaces around delimiters
@ -1748,11 +1739,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $listview;
}
public function currentPageID()
public function currentRecordID()
{
$id = parent::currentPageID();
$id = parent::currentRecordID();
$this->extend('updateCurrentPageID', $id);
$this->extend('updateCurrentRecordID', $id);
return $id;
}
@ -1761,18 +1752,17 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Data saving handlers
/**
* Save and Publish page handler
* Save and Publish record handler
*
* @throws HTTPResponse_Exception
*/
public function save(array $data, Form $form): HTTPResponse
{
$className = $this->config()->get('tree_class');
$className = $this->getModelClass();
// Existing or new record?
$id = $data['ID'];
if (substr($id ?? '', 0, 3) != 'new') {
/** @var SiteTree $record */
$record = DataObject::get_by_id($className, $id);
// Check edit permissions
if ($record && !$record->canEdit()) {
@ -1790,14 +1780,15 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Check publishing permissions
$doPublish = !empty($data['publish']);
if ($record && $doPublish && !$record->canPublish()) {
$isVersioned = $record->hasExtension(Versioned::class);
if ($isVersioned && $doPublish && !$record->canPublish()) {
return Security::permissionFailure($this);
}
$record->HasBrokenLink = 0;
$record->HasBrokenFile = 0;
$record->HasBrokenLink = 0; // @TODO generalise!!
$record->HasBrokenFile = 0; // @TODO generalise!!
if (!$record->ObsoleteClassName) {
if ($isVersioned && !$record->ObsoleteClassName) { // @TODO I think that's specific to SiteTree??
$record->writeWithoutVersion();
}
@ -1812,19 +1803,28 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$form->saveInto($record);
$record->write();
// If the 'Publish' button was clicked, also publish the page
// If the 'Publish' button was clicked, also publish the record
if ($doPublish) {
if (!$record->hasExtension(RecursivePublishable::class)) {
throw new HTTPResponse_Exception(get_class($record) . ' record is not publishable.', 400);
}
$record->publishRecursive();
$message = _t(
__CLASS__ . '.PUBLISHED',
"Published '{title}' successfully.",
['title' => $record->Title]
LeftAndMain::class . '.PUBLISHED_RECORD',
'Published {name} "{title}"',
[
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
);
} else {
$message = _t(
__CLASS__ . '.SAVED',
"Saved '{title}' successfully.",
['title' => $record->Title]
LeftAndMain::class . '.SAVED_RECORD',
'Saved {name} "{title}"',
[
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
);
}
@ -1835,12 +1835,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
/**
* @param int|string $id
* @param bool $setID
* @return mixed|DataObject
* @throws HTTPResponse_Exception
*/
public function getNewItem($id, $setID = true)
public function getNewItem($id, $setID = true): DataObject
{
$parentClass = $this->config()->get('tree_class');
$parentClass = $this->getModelClass();
list(, $className, $parentID) = array_pad(explode('-', $id ?? ''), 3, null);
if (!is_a($className, $parentClass ?? '', true)) {
@ -1851,23 +1850,20 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
throw new HTTPResponse_Exception($response);
}
/** @var SiteTree $newItem */
/** @var DataObject $newItem */
$newItem = Injector::inst()->create($className);
$newItem->Title = _t(
__CLASS__ . '.NEWPAGE',
"New {pagetype}",
'followed by a page type title',
['pagetype' => singleton($className)->i18n_singular_name()]
LeftAndMain::class . '.NEW_RECORD',
'New {recordtype}',
['recordtype' => DataObject::singleton($className)->i18n_singular_name()]
);
$newItem->ClassName = $className;
$newItem->ParentID = $parentID;
// DataObject::fieldExists only checks the current class, not the hierarchy
// This allows the CMS to set the correct sort value
if ($newItem->castingHelper('Sort')) {
$table = DataObject::singleton(SiteTree::class)->baseTable();
if ($newItem->hasDatabaseField('Sort')) { // @TODO Let the field name be configurable
$table = DataObject::singleton($parentClass)->baseTable();
$maxSort = DB::prepared_query(
"SELECT MAX(\"Sort\") FROM \"$table\" WHERE \"ParentID\" = ?",
"SELECT MAX(\"Sort\") FROM \"$table\" WHERE \"ParentID\" = ?", // @TODO this won't work if sort is on a subclass! Use ORM instead
[$parentID]
)->value();
$newItem->Sort = (int)$maxSort + 1;
@ -1878,55 +1874,43 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
# Some modules like subsites add extra fields that need to be set when the new item is created
$this->extend('augmentNewSiteTreeItem', $newItem);
$this->extend('updateNewItem', $newItem); // @TODO rename
return $newItem;
}
/**
* Actually perform the publication step
*
* @param Versioned|DataObject $record
* @return mixed
*/
public function performPublish($record)
{
if ($record && !$record->canPublish()) {
return Security::permissionFailure($this);
}
$record->publishRecursive();
}
/**
* Reverts a page by publishing it to live.
* Use {@link restorepage()} if you want to restore a page
* Reverts a record by publishing it to live.
* Use {@link restoreRecord()} if you want to restore a record
* which was deleted from draft without publishing.
*
* @uses SiteTree->doRevertToLive()
*
* @throws HTTPResponse_Exception
*/
public function revert(array $data, Form $form): HTTPResponse
{
$modelClass = $this->getModelClass();
if (!isset($data['ID'])) {
throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
}
$id = (int) $data['ID'];
$restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
if (!$restoredPage) {
throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
if (!$modelClass::has_extension(Versioned::class)) {
throw new HTTPResponse_Exception("$modelClass record cannot be reverted", 400);
}
$table = DataObject::singleton(SiteTree::class)->baseTable();
$liveTable = DataObject::singleton(SiteTree::class)->stageTable($table, Versioned::LIVE);
$record = Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, [
$id = (int) $data['ID'];
$restoredRecord = Versioned::get_latest_version($modelClass, $id);
if (!$restoredRecord) {
throw new HTTPResponse_Exception("Record #$id not found", 400);
}
$table = DataObject::singleton($modelClass)->baseTable();
$liveTable = DataObject::singleton($modelClass)->stageTable($table, Versioned::LIVE);
$record = Versioned::get_one_by_stage($modelClass, Versioned::LIVE, [
"\"$liveTable\".\"ID\"" => $id
]);
// a user can restore a page without publication rights, as it just adds a new draft state
// (this action should just be available when page has been "deleted from draft")
// a user can restore a record without publication rights, as it just adds a new draft state
// (this action should just be available when the record has been "deleted from draft")
if ($record && !$record->canEdit()) {
return Security::permissionFailure($this);
}
@ -1939,27 +1923,27 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(_t(
__CLASS__ . '.RESTORED',
"Restored '{title}' successfully",
'Param {title} is a title',
['title' => $record->Title]
) ?? '')
LeftAndMain::class . '.RESTORED_RECORD',
'Restored {name} "{title}"',
[
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
))
);
return $this->getResponseNegotiator()->respond($this->getRequest());
}
/**
* Delete the current page from draft stage.
*
* @see deletefromlive()
* Delete the current record from draft stage.
*
* @throws HTTPResponse_Exception
*/
public function delete(array $data, Form $form): HTTPResponse
{
$id = $data['ID'];
$record = SiteTree::get()->byID($id);
$record = DataObject::get($this->getModelClass())->byID($id);
if ($record && !$record->canDelete()) {
return Security::permissionFailure();
}
@ -1970,13 +1954,28 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
// Delete record
$record->delete();
if ($record->hasExtension(Versioned::class) && $record->hasStages()) {
$message = _t(
LeftAndMain::class . '.ARCHIVED_RECORD',
'Archived {name} "{title}"',
[
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
);
} else {
$message = _t(
LeftAndMain::class . '.DELETED_RECORD',
'Deleted {name} "{title}"',
[
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
);
}
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(_t(
__CLASS__ . '.REMOVEDPAGEFROMDRAFT',
"Removed '{title}' from the draft site",
['title' => $record->Title]
) ?? '')
rawurlencode($message)
);
// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
@ -1984,14 +1983,14 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Delete this page from both live and stage
* Delete this record from both live and stage
*
* @throws HTTPResponse_Exception
*/
public function archive(array $data, Form $form): HTTPResponse
{
$id = $data['ID'];
$record = SiteTree::get()->byID($id);
$record = DataObject::get($this->getModelClass())->byID($id);
if (!$record || !$record->exists()) {
throw new HTTPResponse_Exception("Bad record ID #$id", 404);
}
@ -2005,10 +2004,13 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(_t(
__CLASS__ . '.ARCHIVEDPAGE',
"Archived page '{title}'",
['title' => $record->Title]
) ?? '')
LeftAndMain::class . '.ARCHIVED_RECORD',
'Archived {name} "{title}"',
[
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
))
);
// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
@ -2024,10 +2026,13 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
public function unpublish(array $data, Form $form): HTTPResponse
{
$className = $this->config()->get('tree_class');
/** @var SiteTree $record */
$className = $this->getModelClass();
$record = DataObject::get_by_id($className, $data['ID']);
if (!$record->hasExtension(Versioned::class)) {
throw new HTTPResponse_Exception(get_class($record) . ' record cannot be unpublished.', 400);
}
if ($record && !$record->canUnpublish()) {
return Security::permissionFailure($this);
}
@ -2040,10 +2045,13 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(_t(
__CLASS__ . '.REMOVEDPAGE',
"Removed '{title}' from the published site",
['title' => $record->Title]
) ?? '')
LeftAndMain::class . '.UNPUBLISHED_RECORD',
'Unpublished {name} "{title}"',
[
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
))
);
return $this->getResponseNegotiator()->respond($this->getRequest());
@ -2055,7 +2063,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
public function rollback()
{
return $this->doRollback([
'ID' => $this->currentPageID(),
'ID' => $this->currentRecordID(),
'Version' => $this->getRequest()->param('VersionID')
], null);
}
@ -2074,8 +2082,13 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$id = (isset($data['ID'])) ? (int) $data['ID'] : null;
$version = (isset($data['Version'])) ? (int) $data['Version'] : null;
/** @var SiteTree|Versioned $record */
$record = Versioned::get_latest_version($this->config()->get('tree_class'), $id);
$modelClass = $this->getModelClass();
if (!$modelClass::has_extension(Versioned::class)) {
throw new HTTPResponse_Exception("$modelClass record cannot be rolled back", 400);
}
/** @var DataObject&Versioned $record */
$record = Versioned::get_latest_version($modelClass, $id);
if ($record && !$record->canEdit()) {
return Security::permissionFailure($this);
}
@ -2083,22 +2096,22 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
if ($version) {
$record->rollbackRecursive($version);
$message = _t(
__CLASS__ . '.ROLLEDBACKVERSIONv2',
"Rolled back to version #{version}.",
LeftAndMain::class . '.ROLLEDBACK_VERSION',
'Rolled back to version #{version}.',
['version' => $data['Version']]
);
} else {
$record->doRevertToLive();
$record->publishRecursive();
$message = _t(
__CLASS__ . '.ROLLEDBACKPUBv2',
"Rolled back to published version."
LeftAndMain::class . '.ROLLEDBACK_PUBLISHED',
'Rolled back to published version.'
);
}
$this->getResponse()->addHeader('X-Status', rawurlencode($message ?? ''));
// Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
// Can be used in different contexts: In normal record edit view, in which case the redirect won't have any effect.
// Or in history view, in which case a revert causes the CMS to re-load the edit view.
// The X-Pjax header forces a "full" content refresh on redirect.
$url = $record->getCMSEditLink();
@ -2142,11 +2155,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$forms[$urlSegment] = $formHtml;
}
}
$pageHtml = '';
$recordHtml = '';
foreach ($forms as $urlSegment => $html) {
$pageHtml .= '<div class="params" id="BatchActionParameters_' . $urlSegment . '" style="display:none">' . $html . '</div>';
$recordHtml .= '<div class="params" id="BatchActionParameters_' . $urlSegment . '" style="display:none">' . $html . '</div>';
}
return new LiteralField('BatchActionParameters', '<div id="BatchActionParameters" class="action-parameters" style="display:none">' . $pageHtml . '</div>');
return new LiteralField('BatchActionParameters', '<div id="BatchActionParameters" class="action-parameters" style="display:none">' . $recordHtml . '</div>');
}
/**
@ -2158,7 +2171,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Restore a completely deleted page from the SiteTree_versions table.
* Restore a completely deleted record from the *_versions table.
*/
public function restore(array $data, Form $form): HTTPResponse
{
@ -2166,21 +2179,29 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return new HTTPResponse("Please pass an ID in the form content", 400);
}
$id = (int)$data['ID'];
$restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
if (!$restoredPage) {
return new HTTPResponse("SiteTree #$id not found", 400);
$modelClass = $this->getModelClass();
if (!$modelClass::has_extension(Versioned::class)) {
throw new HTTPResponse_Exception("$modelClass record cannot be restored", 400);
}
$restoredPage = $restoredPage->doRestoreToStage();
$id = (int)$data['ID'];
$restoredRecord = Versioned::get_latest_version($modelClass, $id);
if (!$restoredRecord) {
return new HTTPResponse("Record #$id not found", 400);
}
$restoredRecord = $restoredRecord->doRestoreToStage();
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(_t(
__CLASS__ . '.RESTORED',
"Restored '{title}' successfully",
['title' => $restoredPage->Title]
) ?? '')
LeftAndMain::class . '.RESTORED_RECORD',
'Restored {name} "{title}"',
[
'name' => $restoredRecord->i18n_singular_name(),
'title' => $restoredRecord->Title,
]
))
);
return $this->getResponseNegotiator()->respond($this->getRequest());
@ -2194,31 +2215,34 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
if (($id = $this->urlParams['ID']) && is_numeric($id)) {
$page = SiteTree::get()->byID($id);
if ($page && !$page->canCreate(null, ['Parent' => $page->Parent()])) {
$record = DataObject::get($this->getModelClass())->byID($id);
if ($record && !$record->canCreate(null, ['Parent' => $record->Parent()])) {
return Security::permissionFailure($this);
}
if (!$page || !$page->ID) {
if (!$record || !$record->ID) {
throw new HTTPResponse_Exception("Bad record ID #$id", 404);
}
$newPage = $page->duplicate();
$newRecord = $record->duplicate();
// ParentID can be hard-set in the URL. This is useful for pages with multiple parents
if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
$newPage->ParentID = $_GET['parentID'];
$newPage->write();
$newRecord->ParentID = $_GET['parentID'];
$newRecord->write();
}
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(_t(
__CLASS__ . '.DUPLICATED',
"Duplicated '{title}' successfully",
['title' => $newPage->Title]
) ?? '')
LeftAndMain::class . '.DUPLICATED_RECORD',
'Duplicated {name} "{title}"',
[
'name' => $newRecord->i18n_singular_name(),
'title' => $newRecord->Title,
]
))
);
$url = $newPage->getCMSEditLink();
$url = $newRecord->getCMSEditLink();
$this->getResponse()->addHeader('X-ControllerURL', $url);
$this->getRequest()->addHeader('X-Pjax', 'Content');
$this->getResponse()->addHeader('X-Pjax', 'Content');
@ -2236,25 +2260,28 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
Environment::increaseTimeLimitTo();
if (($id = $this->urlParams['ID']) && is_numeric($id)) {
$page = SiteTree::get()->byID($id);
if ($page && !$page->canCreate(null, ['Parent' => $page->Parent()])) {
$record = DataObject::get($this->getModelClass())->byID($id);
if ($record && !$record->canCreate(null, ['Parent' => $record->Parent()])) {
return Security::permissionFailure($this);
}
if (!$page || !$page->ID) {
if (!$record || !$record->ID) {
throw new HTTPResponse_Exception("Bad record ID #$id", 404);
}
$newPage = $page->duplicateWithChildren();
$newRecord = $record->duplicateWithChildren();
$this->getResponse()->addHeader(
'X-Status',
rawurlencode(_t(
__CLASS__ . '.DUPLICATEDWITHCHILDREN',
"Duplicated '{title}' and children successfully",
['title' => $newPage->Title]
LeftAndMain::class . '.DUPLICATED_RECORD_WITH_CHILDREN',
'Duplicated {name} "{title}" and children',
[
'name' => $newRecord->i18n_singular_name(),
'title' => $newRecord->Title,
]
) ?? '')
);
$url = $newPage->getCMSEditLink();
$url = $newRecord->getCMSEditLink();
$this->getResponse()->addHeader('X-ControllerURL', $url);
$this->getRequest()->addHeader('X-Pjax', 'Content');
$this->getResponse()->addHeader('X-Pjax', 'Content');
@ -2269,8 +2296,8 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$title = CMSPagesController::menu_title();
return [
"CMS_ACCESS_CMSMain" => [
'name' => _t(__CLASS__ . '.ACCESS', "Access to '{title}' section", ['title' => $title]),
'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
'name' => _t(LeftAndMain::class . '.ACCESS', "Access to '{title}' section", ['title' => $title]),
'category' => _t(LeftAndMain::class . '.CMS_ACCESS_CATEGORY', 'CMS Access'),
'help' => _t(
__CLASS__ . '.ACCESS_HELP',
'Allow viewing of the section containing page tree and content. View and edit permissions can be handled through page specific dropdowns, as well as the separate "Content permissions".'
@ -2293,7 +2320,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
* Cache key for SiteTreeHints() method
* Cache key for TreeHints() method
*
* @param $memberID
* @return string

View File

@ -1,239 +0,0 @@
<?php
namespace SilverStripe\CMS\Controllers;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Session;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\SelectionGroup;
use SilverStripe\Forms\SelectionGroup_Item;
use SilverStripe\Forms\TreeDropdownField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\SiteConfig\SiteConfig;
class CMSPageAddController extends CMSPageEditController
{
private static $url_segment = 'pages/add';
private static $url_rule = '/$Action/$ID/$OtherID';
private static $url_priority = 42;
private static $menu_title = 'Add page';
private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
private static $allowed_actions = [
'AddForm',
'doAdd',
'doCancel'
];
/**
* @return Form
*/
public function AddForm()
{
$pageTypes = [];
$defaultIcon = Config::inst()->get(SiteTree::class, 'icon_class');
foreach ($this->PageTypes() as $type) {
$class = $type->getField('ClassName');
$icon = Config::inst()->get($class, 'icon_class') ?: $defaultIcon;
// If the icon is the SiteTree default and there's some specific icon being provided by `getPageIconURL`
// then we don't need to add the icon class. Otherwise the class take precedence.
if ($icon === $defaultIcon && !empty(singleton($class)->getPageIconURL())) {
$icon = '';
}
$html = sprintf(
'<span class="page-icon %s class-%s"></span><span class="title">%s</span><span class="form__field-description">%s</span>',
$icon,
Convert::raw2htmlid($class),
$type->getField('AddAction'),
$type->getField('Description')
);
$pageTypes[$class] = DBField::create_field('HTMLFragment', $html);
}
// Ensure generic page type shows on top
if (isset($pageTypes['Page'])) {
$pageTitle = $pageTypes['Page'];
$pageTypes = array_merge(['Page' => $pageTitle], $pageTypes);
}
$numericLabelTmpl = '<span class="step-label"><span class="flyout">Step %d. </span><span class="title">%s</span></span>';
$topTitle = _t('SilverStripe\\CMS\\Controllers\\CMSPageAddController.ParentMode_top', 'Top level');
$childTitle = _t('SilverStripe\\CMS\\Controllers\\CMSPageAddController.ParentMode_child', 'Under another page');
$fields = new FieldList(
$parentModeField = new SelectionGroup(
"ParentModeField",
[
$topField = new SelectionGroup_Item(
"top",
null,
$topTitle
),
new SelectionGroup_Item(
'child',
$parentField = new TreeDropdownField(
"ParentID",
"",
SiteTree::class,
'ID',
'TreeTitle'
),
$childTitle
)
]
),
new LiteralField(
'RestrictedNote',
sprintf(
'<p class="alert alert-info message-restricted">%s</p>',
_t(
'SilverStripe\\CMS\\Controllers\\CMSMain.AddPageRestriction',
'Note: Some page types are not allowed for this selection'
)
)
),
$typeField = new OptionsetField(
"PageType",
DBField::create_field(
'HTMLFragment',
sprintf($numericLabelTmpl ?? '', 2, _t('SilverStripe\\CMS\\Controllers\\CMSMain.ChoosePageType', 'Choose page type'))
),
$pageTypes,
'Page'
)
);
$parentModeField->setTitle(DBField::create_field(
'HTMLFragment',
sprintf($numericLabelTmpl ?? '', 1, _t('SilverStripe\\CMS\\Controllers\\CMSMain.ChoosePageParentMode', 'Choose where to create this page'))
));
$parentField->setSearchFunction(function ($sourceObject, $labelField, $search) {
return DataObject::get($sourceObject)
->filterAny([
'MenuTitle:PartialMatch' => $search,
'Title:PartialMatch' => $search,
]);
});
$parentModeField->addExtraClass('parent-mode');
// CMSMain->currentPageID() automatically sets the homepage,
// which we need to counteract in the default selection (which should default to root, ID=0)
if ($parentID = $this->getRequest()->getVar('ParentID')) {
$parentModeField->setValue('child');
$parentField->setValue((int)$parentID);
} else {
$parentModeField->setValue('top');
}
// Check if the current user has enough permissions to create top level pages
// If not, then disable the option to do that
if (!SiteConfig::current_site_config()->canCreateTopLevel()) {
$topField->setDisabled(true);
$parentModeField->setValue('child');
}
$actions = new FieldList(
FormAction::create("doAdd", _t('SilverStripe\\CMS\\Controllers\\CMSMain.Create', "Create"))
->addExtraClass('btn-primary font-icon-plus-circled')
->setUseButtonTag(true),
FormAction::create("doCancel", _t('SilverStripe\\CMS\\Controllers\\CMSMain.Cancel', "Cancel"))
->addExtraClass('btn-secondary')
->setUseButtonTag(true)
);
$this->extend('updatePageOptions', $fields);
$negotiator = $this->getResponseNegotiator();
$form = Form::create(
$this,
"AddForm",
$fields,
$actions
)->setHTMLID('Form_AddForm')->setStrictFormMethodCheck(false);
$form->setAttribute('data-hints', $this->SiteTreeHints());
$form->setAttribute('data-childfilter', $this->Link('childfilter'));
$form->setValidationResponseCallback(function (ValidationResult $errors) use ($negotiator, $form) {
$request = $this->getRequest();
if ($request->isAjax() && $negotiator) {
$result = $form->forTemplate();
return $negotiator->respond($request, [
'CurrentForm' => function () use ($result) {
return $result;
}
]);
}
return null;
});
$form->addExtraClass('flexbox-area-grow fill-height cms-add-form cms-content cms-edit-form ' . $this->BaseCSSClasses());
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
return $form;
}
public function doAdd(array $data, Form $form): HTTPResponse
{
$className = isset($data['PageType']) ? $data['PageType'] : "Page";
$parentID = isset($data['ParentID']) ? (int)$data['ParentID'] : 0;
if (!$parentID && isset($data['Parent'])) {
$page = SiteTree::get_by_link($data['Parent']);
if ($page) {
$parentID = $page->ID;
}
}
if (is_numeric($parentID) && $parentID > 0) {
$parentObj = SiteTree::get()->byID($parentID);
} else {
$parentObj = null;
}
if (!$parentObj || !$parentObj->ID) {
$parentID = 0;
}
if (!singleton($className)->canCreate(Security::getCurrentUser(), ['Parent' => $parentObj])) {
return Security::permissionFailure($this);
}
$record = $this->getNewItem("new-$className-$parentID", false);
$this->extend('updateDoAdd', $record, $form);
$record->write();
$editController = CMSPageEditController::singleton();
$editController->setRequest($this->getRequest());
$editController->setCurrentPageID($record->ID);
$session = $this->getRequest()->getSession();
$session->set(
"FormInfo.Form_EditForm.formError.message",
_t('SilverStripe\\CMS\\Controllers\\CMSMain.PageAdded', 'Successfully created page')
);
$session->set("FormInfo.Form_EditForm.formError.type", 'good');
return $this->redirect(Controller::join_links($editController->Link('show'), $record->ID));
}
public function doCancel(array $data, Form $form): HTTPResponse
{
return $this->redirect(CMSMain::singleton()->Link());
}
}

View File

@ -2,17 +2,15 @@
namespace SilverStripe\CMS\Controllers;
use Page;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\CampaignAdmin\AddToCampaignHandler;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Forms\Form;
use SilverStripe\Core\ArrayLib;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\ORM\DataObject;
/**
* @package cms
@ -57,7 +55,7 @@ class CMSPageEditController extends CMSMain
public function addtocampaign(array $data, Form $form): HTTPResponse
{
$id = $data['ID'];
$record = \Page::get()->byID($id);
$record = DataObject::get($this->getModelClass())->byID($id);
$handler = AddToCampaignHandler::create($this, $record);
$response = $handler->addToCampaign($record, $data);
@ -95,15 +93,16 @@ class CMSPageEditController extends CMSMain
*/
public function getAddToCampaignForm($id)
{
$modelClass = $this->getModelClass();
// Get record-specific fields
$record = SiteTree::get()->byID($id);
$record = DataObject::get($modelClass)->byID($id);
if (!$record) {
$this->httpError(404, _t(
__CLASS__ . '.ErrorNotFound',
'That {Type} couldn\'t be found',
'',
['Type' => Page::singleton()->i18n_singular_name()]
['Type' => DataObject::singleton($modelClass)->i18n_singular_name()]
));
return null;
}
@ -112,7 +111,7 @@ class CMSPageEditController extends CMSMain
__CLASS__.'.ErrorItemPermissionDenied',
'It seems you don\'t have the necessary permissions to add {ObjectTitle} to a campaign',
'',
['ObjectTitle' => Page::singleton()->i18n_singular_name()]
['ObjectTitle' => DataObject::singleton($modelClass)->i18n_singular_name()]
));
return null;
}

View File

@ -2,6 +2,7 @@
namespace SilverStripe\CMS\Controllers;
use SilverStripe\Forms\Form;
use SilverStripe\Model\ArrayData;
class CMSPageSettingsController extends CMSMain
@ -15,11 +16,19 @@ class CMSPageSettingsController extends CMSMain
private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
public function getEditForm($id = null, $fields = null)
public function getEditForm($id = null, $fields = null): Form
{
$record = $this->getRecord($id ?: $this->currentPageID());
$record = $this->getRecord($id ?: $this->currentRecordID());
return parent::getEditForm($id, ($record) ? $record->getSettingsFields() : null);
// @TODO ideally settings isn't its own special thing...
// can we refactor this so it's just another tab in the main form? And just have it lazyload or something?
// At the very least this tab must NOT appear if there are no fields for it.
if ($record && $record->hasMethod('getSettingsFields')) {
$fields = $record->getSettingsFields();
} else {
$fields = null;
}
return parent::getEditForm($id, $fields);
}
public function getTabIdentifier()

View File

@ -2,16 +2,11 @@
namespace SilverStripe\CMS\Controllers;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Controller;
use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\Model\ArrayData;
use stdClass;
// @TODO What a pointless class!!!!!!
class CMSPagesController extends CMSMain
{
private static $url_segment = 'pages';
private static $url_rule = '/$Action/$ID/$OtherID';
@ -27,7 +22,7 @@ class CMSPagesController extends CMSMain
return false;
}
public function isCurrentPage(DataObject $record)
public function isCurrentRecord(DataObject $record)
{
return false;
}

View File

@ -113,7 +113,7 @@ abstract class CMSSiteTreeFilter implements LeftAndMain_SearchFilter
return $this->numChildrenMethod;
}
public function getPageClasses($page)
public function getRecordClasses($page)
{
if ($this->_cache_ids === null) {
$this->populateIDs();
@ -178,7 +178,7 @@ abstract class CMSSiteTreeFilter implements LeftAndMain_SearchFilter
}
}
public function isPageIncluded($page)
public function isRecordIncluded($page)
{
if ($this->_cache_ids === null) {
$this->populateIDs();

View File

@ -18,6 +18,8 @@ use SilverStripe\View\Requirements;
/**
* Extension to include custom page icons
*
* @TODO AAAHHHHHHHHHHH
*
* @extends Extension<LeftAndMain>
*/
class LeftAndMainPageIconsExtension extends Extension implements Flushable

View File

@ -0,0 +1,232 @@
<?php
namespace SilverStripe\CMS\Forms;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\SelectionGroup;
use SilverStripe\Forms\SelectionGroup_Item;
use SilverStripe\Forms\TreeDropdownField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\Security\Security;
use SilverStripe\SiteConfig\SiteConfig;
class CMSMainAddForm extends Form
{
public function __construct(CMSMain $controller)
{
$modelClass = $controller->getModelClass();
$singleton = DataObject::singleton($modelClass);
$recordTypes = [];
$defaultIcon = Config::inst()->get($modelClass, 'icon_class'); // @TODO need a better place for default - maybe try default on class, and fallback to default on cmsmain?
$defaultRecordType = 'Page'; // @TODO GENERICALIFICATE THIS! Note it's not necessarily the same as $modelClass, 'cause SiteTree aint Page
foreach ($controller->RecordTypes() as $type) {
$class = $type->getField('ClassName');
$icon = Config::inst()->get($class, 'icon_class') ?: $defaultIcon;
// If the icon is the default and there's some specific icon being provided by `getPageIconURL`
// then we don't need to add the icon class. Otherwise the class take precedence.
if ($icon === $defaultIcon && !empty(DataObject::singleton($class)->getPageIconURL())) { // @TODO AHHH
$icon = '';
}
$html = sprintf(
'<span class="page-icon %s class-%s"></span><span class="title">%s</span><span class="form__field-description">%s</span>',
$icon,
Convert::raw2htmlid($class),
$type->getField('AddAction'),
$type->getField('Description')
);
$recordTypes[$class] = DBField::create_field('HTMLFragment', $html);
}
// Ensure default record type shows on top
if (isset($recordTypes[$defaultRecordType])) {
$typeName = $recordTypes[$defaultRecordType];
$recordTypes = array_merge([$defaultRecordType => $typeName], $recordTypes);
}
$numericLabelTmpl = '<span class="step-label"><span class="flyout">Step %d. </span><span class="title">%s</span></span>';
$topTitle = _t(__CLASS__ . '.ParentMode_top', 'Top level');
$childTitle = _t(
__CLASS__ . '.ParentMode_child',
'Under another {type}',
['type' => mb_strtolower($singleton->i18n_singular_name())]
);
$fields = FieldList::create(
$parentModeField = SelectionGroup::create(
'ParentModeField',
[
$topField = SelectionGroup_Item::create(
'top',
null,
$topTitle
),
SelectionGroup_Item::create(
'child',
$parentField = TreeDropdownField::create(
'ParentID',
'',
$modelClass,
'ID',
'TreeTitle'
),
$childTitle
)
]
),
LiteralField::create(
'RestrictedNote',
sprintf(
'<p class="alert alert-info message-restricted">%s</p>',
_t(
__CLASS__ . '.AddRecordRestriction',
'Note: Some {model} types are not allowed for this selection',
['model' => mb_strtolower($singleton->i18n_singular_name())]
)
)
),
OptionsetField::create(
'RecordType',
DBField::create_field(
'HTMLFragment',
sprintf($numericLabelTmpl, 2, _t(
__CLASS__ . '.ChooseRecordType',
'Choose {model} type',
['model' => mb_strtolower($singleton->i18n_singular_name())]
))
),
$recordTypes,
$defaultRecordType
)
);
$parentModeField->setTitle(DBField::create_field(
'HTMLFragment',
sprintf($numericLabelTmpl, 1, _t(__CLASS__ . '.ChooseParentMode', 'Choose where to create this record'))
));
$parentField->setSearchFunction(function ($sourceObject, $labelField, $search) {
return DataObject::get($sourceObject)
->filterAny([
'MenuTitle:PartialMatch' => $search,
'Title:PartialMatch' => $search,
]);
});
$parentModeField->addExtraClass('parent-mode');
// CMSMain->currentRecordID() automatically sets the homepage, // @TODO find out what this is about
// which we need to counteract in the default selection (which should default to root, ID=0)
if ($parentID = $controller->getRequest()->getVar('ParentID')) {
$parentModeField->setValue('child');
$parentField->setValue((int)$parentID);
} else {
$parentModeField->setValue('top');
}
// Check if the current user has enough permissions to create top level records
// If not, then disable the option to do that
if (is_a($modelClass, SiteTree::class, true) && !SiteConfig::current_site_config()->canCreateTopLevel()) { // @TODO probably need to make this generic
$topField->setDisabled(true);
$parentModeField->setValue('child');
}
$actions = FieldList::create(
FormAction::create('doAdd', _t(CMSMain::class . '.Create', 'Create'))
->addExtraClass('btn-primary font-icon-plus-circled')
->setUseButtonTag(true),
FormAction::create('doCancel', _t(CMSMain::class . '.Cancel', 'Cancel'))
->addExtraClass('btn-secondary')
->setUseButtonTag(true)
);
$this->extend('updateFields', $fields);
parent::__construct($controller, 'AddForm', $fields, $actions);
$negotiator = $controller->getResponseNegotiator();
$this->setHTMLID('Form_AddForm')->setStrictFormMethodCheck(false);
$this->setAttribute('data-hints', $controller->TreeHints());
$this->setAttribute('data-childfilter', $controller->Link('childfilter'));
$this->setValidationResponseCallback(function () use ($negotiator, $controller) {
$request = $controller->getRequest();
if ($request->isAjax() && $negotiator) {
$result = $this->forTemplate();
return $negotiator->respond($request, [
'CurrentForm' => function () use ($result) {
return $result;
}
]);
}
return null;
});
$this->addExtraClass('flexbox-area-grow fill-height cms-add-form cms-content cms-edit-form ' . $controller->BaseCSSClasses());
$this->setTemplate($controller->getTemplatesWithSuffix('_AddForm'));
}
public function doAdd(array $data, Form $form): HTTPResponse
{
$defaultRecordType = 'Page'; // @TODO GENERICALIFICATE THIS
$controller = $this->getController();
$modelClass = $controller->getModelClass();
$className = isset($data['RecordType']) ? $data['RecordType'] : $defaultRecordType; // @TODO shouldn't this throw an error??
$parentID = isset($data['ParentID']) ? (int)$data['ParentID'] : 0;
if (!$parentID && isset($data['Parent'])) {
$parentRecord = $modelClass::get_by_link($data['Parent']); // @TODO Obviously no good
if ($parentRecord) {
$parentID = $parentRecord->ID;
}
}
if (is_numeric($parentID) && $parentID > 0) {
$parentObj = DataObject::get($modelClass)->byID($parentID);
} else {
$parentObj = null;
}
if (!$parentObj || !$parentObj->ID) {
$parentID = 0;
}
if (!DataObject::singleton($className)->canCreate(Security::getCurrentUser(), ['Parent' => $parentObj])) {
return Security::permissionFailure($controller);
}
$record = $controller->getNewItem("new-$className-$parentID", false);
$controller->extend('updateDoAdd', $record, $form);
$record->write();
$editController = CMSPageEditController::singleton();
$editController->setRequest($controller->getRequest());
$editController->setCurrentRecordID($record->ID);
$controller->getResponse()->addHeader('X-Status', rawurlencode(_t(
LeftAndMain::class . '.CREATED_RECORD',
'Created {name} "{title}"',
[
'name' => $record->i18n_singular_name(),
'title' => $record->Title,
]
)));
return $controller->redirect($editController->Link('show/' . $record->ID));
}
public function doCancel(): HTTPResponse
{
return $this->getController()->redirect(CMSMain::singleton()->Link()); // @TODO when there's no CMSPageEditController anymore, change this to $this->getController()->Link()
}
}

View File

@ -7,19 +7,19 @@ use SilverStripe\ORM\DataObject;
/**
* This interface lets us set up objects that will tell us what the current page is.
*/
interface CurrentPageIdentifier
interface CurrentRecordIdentifier
{
/**
* Get the current page ID.
* @return int
*/
public function currentPageID();
public function currentRecordID();
/**
* Check if the given DataObject is the current page.
* @param DataObject $page The page to check.
* @return boolean
*/
public function isCurrentPage(DataObject $page);
public function isCurrentRecord(DataObject $page);
}

View File

@ -783,12 +783,12 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/
public function isCurrent()
{
$currentPage = Director::get_current_page();
if ($currentPage instanceof ContentController) {
$currentPage = $currentPage->data();
$currentRecord = Director::get_current_page();
if ($currentRecord instanceof ContentController) {
$currentRecord = $currentRecord->data();
}
if ($currentPage instanceof SiteTree) {
return $currentPage === $this || $currentPage->ID === $this->ID;
if ($currentRecord instanceof SiteTree) {
return $currentRecord === $this || $currentRecord->ID === $this->ID;
}
return false;
}
@ -2778,9 +2778,9 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
// Sort alphabetically, and put current on top
asort($result);
if (isset($result[$this->ClassName])) {
$currentPageTypeName = $result[$this->ClassName];
$currentRecordTypeName = $result[$this->ClassName];
unset($result[$this->ClassName]);
$result = [$this->ClassName => $currentPageTypeName] + $result;
$result = [$this->ClassName => $currentRecordTypeName] + $result;
}
return $result;

View File

@ -256,6 +256,8 @@ en:
TABCONTENT: 'Main content'
TABDEPENDENT: 'Dependent pages'
TOPLEVEL: 'Site Content (Top Level)'
TREETITLE: 'Page name'
TREETYPE: 'Page type'
UNTITLED: 'Untitled {pagetype}'
URLSegment: 'URL segment'
UntitledDependentObject: 'Untitled {instanceType}'

View File

@ -0,0 +1,47 @@
<div class="flexbox-area-grow cms-content $Controller.BaseCSSClasses" data-layout-type="border" data-pjax-fragment="Content">
<form $FormAttributes data-layout-type="border">
<div class="toolbar toolbar--north">
<div class="toolbar__navigation">
<ol class="breadcrumb">
<li class="breadcrumb__item">
<% if $Controller.SectionTitle %>
$Controller.SectionTitle
<% else %>
<%t SilverStripe\CMS\Controllers\CMSMain.Title 'Data Models'%>
<% end_if %>
</li>
<li class="breadcrumb__item breadcrumb__item--last breadcrumb__item--no-crumb">
<h2 class="breadcrumb__item-title breadcrumb__item-title--last">
<%t SilverStripe\Admin\LeftAndMain.NewRecord 'New {name}' name=$Controller.getRecord('singleton').i18n_singular_name() %>
</h2>
</li>
</ol>
</div>
</div>
<div class="panel panel--padded panel--scrollable flexbox-area-grow">
<% if $Message %>
<p id="{$FormName}_error" class="alert $AlertType">$Message</p>
<% else %>
<p id="{$FormName}_error" class="alert $AlertType" style="display: none"></p>
<% end_if %>
<fieldset>
<% if $Legend %><legend>$Legend</legend><% end_if %>
<% loop $Fields %>
$FieldHolder
<% end_loop %>
</fieldset>
</div>
<div class="toolbar--south">
<% if $Actions %>
<div class="btn-toolbar">
<% loop $Actions %>
$Field
<% end_loop %>
</div>
<% end_if %>
</div>
</form>
</div>

View File

@ -4,24 +4,24 @@
<div class="fill-height flexbox-area-grow">
<div class="cms-content-header north">
<div class="cms-content-header-info flexbox-area-grow vertical-align-items">
<a href="$BreadcrumbsBackLink" class="btn btn-secondary btn--no-text font-icon-left-open-big hidden-lg-up toolbar__back-button"></a>
<% include SilverStripe\\Admin\\BackLink_Button Backlink=$BreadcrumbsBacklink %>
<% include SilverStripe\\Admin\\CMSBreadcrumbs %>
</div>
<div class="cms-content-header-tabs cms-tabset">
<ul class="cms-tabset-nav-primary nav nav-tabs">
<li class="nav-item content-treeview<% if $TabIdentifier == 'edit' %> ui-tabs-active<% end_if %>">
<a href="$LinkPageEdit" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkPageEdit">
<a href="$LinkRecordEdit" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkRecordEdit">
<%t SilverStripe\\CMS\\Controllers\\CMSMain.TabContent 'Content' %>
</a>
</li>
<li class="nav-item content-listview<% if $TabIdentifier == 'settings' %> ui-tabs-active<% end_if %>">
<a href="$LinkPageSettings" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkPageSettings">
<a href="$LinkRecordSettings" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkRecordSettings">
<%t SilverStripe\\CMS\\Controllers\\CMSMain.TabSettings 'Settings' %>
</a>
</li>
<li class="nav-item content-listview<% if $TabIdentifier == 'history' %> ui-tabs-active<% end_if %>">
<a href="$LinkPageHistory" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkPageHistory">
<a href="$LinkRecordHistory" class="nav-link cms-panel-link" title="Form_EditForm" data-href="$LinkRecordHistory">
<%t SilverStripe\\CMS\\Controllers\\CMSMain.TabHistory 'History' %>
</a>
</li>

View File

@ -1,6 +1,6 @@
<% include SilverStripe\\CMS\\Controllers\\CMSPagesController_ContentToolActions %>
<div class="ss-dialog cms-page-add-form-dialog cms-dialog-content" id="cms-page-add-form" title="<%t SilverStripe\\CMS\\Controllers\\CMSMain.AddNew 'Add new page' %>">
<div class="ss-dialog cms-page-add-form-dialog cms-dialog-content" id="cms-page-add-form" title="<%t SilverStripe\Admin\\LeftAndMain.AddNew 'Add new {name}' name=$getRecord('singleton').i18n_singular_name().lowercase %>">
$AddForm
</div>

View File

@ -1 +0,0 @@
<% include SilverStripe\\CMS\\Controllers\\CMSMain_PageList %>

View File

@ -0,0 +1 @@
<% include SilverStripe\\CMS\\Controllers\\CMSMain_RecordList %>

View File

@ -4,7 +4,7 @@
<% if $limited %>
<ul><li class="readonly">
<span class="item">
<%t SilverStripe\\CMS\\Controllers\\CMSMain.TOO_MANY_PAGES 'Too many pages' %>
<%t SilverStripe\\CMS\\Controllers\\CMSMain.TOO_MANY_RECORDS 'Too many records' %>
(<a href="{$listViewLink.ATT}" class="subtree-list-link" data-id="$node.ID" data-pjax-target="Content"><%t SilverStripe\\CMS\\Controllers\\CMSMain.SHOW_AS_LIST 'show as list' %></a>)
</span>
</li></ul>

View File

@ -2,7 +2,7 @@
<div class="cms-content-header north vertical-align-items">
<div class="cms-content-header-info vertical-align-items fill-width">
<div class="section-heading flexbox-area-grow">
<span class="section-label"><a href="$LinkPages">{$MenuCurrentItem.Title}</a></span>
<span class="section-label"><a href="$LinkRecords">{$MenuCurrentItem.Title}</a></span>
</div>
<% include SilverStripe\\CMS\\Controllers\\CMSMain_Filter %>
</div>
@ -14,7 +14,7 @@
data-schema="$SearchFieldSchema"
></div>
</div>
$PageListSidebar
$RecordListSidebar
</div>
<div class="cms-panel-content-collapsed">
<h3 class="cms-panel-header">$SiteConfig.Title</h3>

View File

@ -1,6 +1,6 @@
<% include SilverStripe\\CMS\\Controllers\\CMSPagesController_ContentToolActions View='Tree' %>
<div class="ss-dialog cms-page-add-form-dialog cms-dialog-content" id="cms-page-add-form" title="<%t SilverStripe\CMS\Controllers\CMSMain.AddNew 'Add new page' %>">
<div class="ss-dialog cms-page-add-form-dialog cms-dialog-content" id="cms-page-add-form" title="<%t SilverStripe\Admin\LeftAndMain.AddNew 'Add new {name}' name=$getRecord('singleton').i18n_singular_name().lowercase %>">
$AddForm
</div>
@ -17,15 +17,15 @@ $ExtraTreeTools
data-url-tree="$LinkWithSearch($Link('getsubtree')).ATT"
data-url-savetreenode="$Link('savetreenode').ATT"
data-url-updatetreenodes="$Link('updatetreenodes').ATT"
data-url-addpage="{$LinkPageAdd('AddForm/?action_doAdd=1', 'ParentID=%s&PageType=%s').ATT}"
data-url-editpage="$LinkPageEdit('%s').ATT"
data-url-addpage="{$LinkRecordAdd('AddForm/?action_doAdd=1', 'ParentID=%s&RecordType=%s').ATT}"
data-url-editpage="$LinkRecordEdit('%s').ATT"
data-url-duplicate="{$Link('duplicate/%s').ATT}"
data-url-duplicatewithchildren="{$Link('duplicatewithchildren/%s').ATT}"
data-url-listview="{$Link('?view=list').ATT}"
data-hints="$SiteTreeHints.ATT"
data-hints="$TreeHints.ATT"
data-childfilter="$Link('childfilter').ATT"
data-extra-params="SecurityID=$SecurityID.ATT">
$SiteTreeAsUL
$TreeAsUL
</div>
</div>
<% else %>
@ -33,14 +33,14 @@ $ExtraTreeTools
data-url-tree="$LinkWithSearch($Link('getsubtree')).ATT"
data-url-savetreenode="$Link('savetreenode').ATT"
data-url-updatetreenodes="$Link('updatetreenodes').ATT"
data-url-addpage="{$LinkPageAdd('AddForm/?action_doAdd=1', 'ParentID=%s&PageType=%s').ATT}"
data-url-editpage="$LinkPageEdit('%s').ATT"
data-url-addpage="{$LinkRecordAdd('AddForm/?action_doAdd=1', 'ParentID=%s&RecordType=%s').ATT}"
data-url-editpage="$LinkRecordEdit('%s').ATT"
data-url-duplicate="{$Link('duplicate/%s').ATT}"
data-url-duplicatewithchildren="{$Link('duplicatewithchildren/%s').ATT}"
data-url-listview="{$Link('?view=list').ATT}"
data-hints="$SiteTreeHints.ATT"
data-hints="$TreeHints.ATT"
data-childfilter="$Link('childfilter').ATT"
data-extra-params="SecurityID=$SecurityID.ATT">
$SiteTreeAsUL
$TreeAsUL
</div>
<% end_if %>

View File

@ -1,6 +1,6 @@
<div class="view-controls view-controls--{$ViewState}">
<% if not $TreeIsFiltered %>
<%-- Change to data-pjax-target="Content-PageList" to enable in-edit listview --%>
<%-- Change to data-pjax-target="Content-RecordList" to enable in-edit listview --%>
<a class="page-view-link btn btn-secondary btn--icon-sm btn--no-text font-icon-tree"
href="$LinkTreeView.ATT"
data-view="treeview"

View File

@ -1,45 +0,0 @@
<div class="flexbox-area-grow cms-content $BaseCSSClasses" data-layout-type="border" data-pjax-fragment="Content">
<% with $AddForm %>
<form $FormAttributes data-layout-type="border">
<div class="toolbar toolbar--north">
<div class="toolbar__navigation">
<ol class="breadcrumb">
<li class="breadcrumb__item">
<%t SilverStripe\CMS\Controllers\CMSPagesController.MENUTITLE 'Pages'%>
</li>
<li class="breadcrumb__item breadcrumb__item--last breadcrumb__item--no-crumb">
<h2 class="breadcrumb__item-title breadcrumb__item-title--last">
<%t SilverStripe\CMS\Controllers\CMSPageAddController.Title 'Add page' %>
</h2>
</li>
</ol>
</div>
</div>
<div class="panel panel--padded panel--scrollable flexbox-area-grow">
<% if $Message %>
<p id="{$FormName}_error" class="alert $AlertType">$Message</p>
<% else %>
<p id="{$FormName}_error" class="alert $AlertType" style="display: none"></p>
<% end_if %>
<fieldset>
<% if $Legend %><legend>$Legend</legend><% end_if %>
<% loop $Fields %>
$FieldHolder
<% end_loop %>
</fieldset>
</div>
<div class="toolbar--south">
<% if $Actions %>
<div class="btn-toolbar">
<% loop $Actions %>
$Field
<% end_loop %>
</div>
<% end_if %>
</div>
</form>
<% end_with %>
</div>

View File

@ -2,9 +2,7 @@
<div class="cms-content-header north vertical-align-items">
<div class="cms-content-header-info fill-width vertical-align-items">
<% if $TreeIsFiltered %>
<a class="btn btn-secondary font-icon-left-open-big toolbar__back-button btn--no-text" href="$BreadcrumbsBacklink">
<span class="sr-only"><%t SilverStripe\Admin\LeftAndMain.NavigateUp "Return to Pages" %></span>
</a>
<% include SilverStripe\\Admin\\BackLink_Button Backlink=$BreadcrumbsBacklink %>
<% end_if %>
<% include SilverStripe\\Admin\\CMSBreadcrumbs %>
<% include SilverStripe\\CMS\\Controllers\\CMSMain_Filter %>
@ -13,6 +11,6 @@
<div class="flexbox-area-grow fill-height cms-content-fields ui-widget-content cms-panel-padded">
$Tools
$PageList
$RecordList
</div>
</div>

View File

@ -1,7 +1,9 @@
<div class="toolbar toolbar--content cms-content-toolbar">
<div class="btn-toolbar cms-actions-buttons-row">
<% if not $TreeIsFiltered %>
<a class="btn btn-primary cms-content-addpage-button tool-button font-icon-plus" href="$LinkPageAdd" data-url-addpage="{$LinkPageAdd('', 'ParentID=%s')}"><%t SilverStripe\CMS\Controllers\CMSMain.AddNewButton 'Add new' %></a>
<a class="btn btn-primary cms-content-addpage-button tool-button font-icon-plus" href="$LinkRecordAdd" data-url-addpage="{$LinkRecordAdd('', 'ParentID=%s')}">
<%t SilverStripe\Admin\\LeftAndMain.AddNew 'Add new {name}' name=$getRecord('singleton').i18n_singular_name().lowercase %>
</a>
<% if $View == 'Tree' %>
<button type="button" class="cms-content-batchactions-button btn btn-secondary tool-button font-icon-check-mark-2 btn--last" data-toolid="batch-actions">
@ -10,7 +12,7 @@
<% end_if %>
<% end_if %>
<% include SilverStripe\\CMS\\Controllers\\CMSMain_ViewControls PJAXTarget='Content-PageList' %>
<% include SilverStripe\\CMS\\Controllers\\CMSMain_ViewControls PJAXTarget='Content-RecordList' %>
</div>

View File

@ -44,15 +44,15 @@ class CMSMainTest extends FunctionalTest
}
}
public function testSiteTreeHints()
public function testTreeHints()
{
$cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_SiteTreeHints');
$cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_TreeHints');
// Login as user with root creation privileges
$user = $this->objFromFixture(Member::class, 'rootedituser');
Security::setCurrentUser($user);
$cache->clear();
$rawHints = singleton(CMSMain::class)->SiteTreeHints();
$rawHints = singleton(CMSMain::class)->TreeHints();
$this->assertNotNull($rawHints);
$rawHints = preg_replace('/^"(.*)"$/', '$1', Convert::xml2raw($rawHints) ?? '');
@ -423,7 +423,7 @@ class CMSMainTest extends FunctionalTest
);
$jsonStr = $response->getBody();
$data = json_decode($jsonStr, true);
$parser = new CSSContentParser($data['Breadcrumbs']);
$crumbs = $parser->getBySelector('.breadcrumbs-wrapper .crumb');
@ -611,7 +611,7 @@ class CMSMainTest extends FunctionalTest
$this->assertEquals('Class A', $newPage->Title);
}
public function testSiteTreeHintsCache()
public function testTreeHintsCache()
{
$cms = CMSMain::create();
/** @var Member $user */
@ -635,31 +635,31 @@ class CMSMainTest extends FunctionalTest
// Initially, cache misses (1)
Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$hints = $cms->SiteTreeHints();
$hints = $cms->TreeHints();
$this->assertNotNull($hints);
// Now it hits
Injector::inst()->registerService($mockPageHitsCache, $pageClass);
$hints = $cms->SiteTreeHints();
$hints = $cms->TreeHints();
$this->assertNotNull($hints);
// Mutating member record invalidates cache. Misses (2)
$user->FirstName = 'changed';
$user->write();
Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$hints = $cms->SiteTreeHints();
$hints = $cms->TreeHints();
$this->assertNotNull($hints);
// Now it hits again
Injector::inst()->registerService($mockPageHitsCache, $pageClass);
$hints = $cms->SiteTreeHints();
$hints = $cms->TreeHints();
$this->assertNotNull($hints);
// Different user. Misses. (3)
$user = $this->objFromFixture(Member::class, 'allcmssectionsuser');
Security::setCurrentUser($user);
Injector::inst()->registerService($mockPageMissesCache, $pageClass);
$hints = $cms->SiteTreeHints();
$hints = $cms->TreeHints();
$this->assertNotNull($hints);
}
@ -703,21 +703,21 @@ class CMSMainTest extends FunctionalTest
);
}
public function testCanOrganiseSitetree()
public function testCanOrganiseTree()
{
$cms = CMSMain::create();
$this->assertFalse($cms->CanOrganiseSitetree());
$this->assertFalse($cms->CanOrganiseTree());
$this->logInWithPermission('CMS_ACCESS_CMSMain');
$this->assertFalse($cms->CanOrganiseSitetree());
$this->assertFalse($cms->CanOrganiseTree());
$this->logOut();
$this->logInWithPermission('SITETREE_REORGANISE');
$this->assertTrue($cms->CanOrganiseSitetree());
$this->assertTrue($cms->CanOrganiseTree());
$this->logOut();
$this->logInWithPermission('ADMIN');
$this->assertTrue($cms->CanOrganiseSitetree());
$this->assertTrue($cms->CanOrganiseTree());
}
}

View File

@ -24,8 +24,8 @@ class CMSSiteTreeFilterTest extends SapphireTest
$f = new CMSSiteTreeFilter_Search();
$results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($page1));
$this->assertTrue($f->isPageIncluded($page2));
$this->assertTrue($f->isRecordIncluded($page1));
$this->assertTrue($f->isRecordIncluded($page2));
}
public function testSearchFilterByTitle()
@ -36,8 +36,8 @@ class CMSSiteTreeFilterTest extends SapphireTest
$f = new CMSSiteTreeFilter_Search(['Title' => 'Page 1']);
$results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($page1));
$this->assertFalse($f->isPageIncluded($page2));
$this->assertTrue($f->isRecordIncluded($page1));
$this->assertFalse($f->isRecordIncluded($page2));
$this->assertEquals(1, count($results ?? []));
$this->assertEquals(
['ID' => $page1->ID, 'ParentID' => 0],
@ -50,10 +50,10 @@ class CMSSiteTreeFilterTest extends SapphireTest
$page = $this->objFromFixture(SiteTree::class, 'page8');
$filter = CMSSiteTreeFilter_Search::create(['Term' => 'lake-wanaka+adventure']);
$this->assertTrue($filter->isPageIncluded($page));
$this->assertTrue($filter->isRecordIncluded($page));
$filter = CMSSiteTreeFilter_Search::create(['URLSegment' => 'lake-wanaka+adventure']);
$this->assertTrue($filter->isPageIncluded($page));
$this->assertTrue($filter->isRecordIncluded($page));
}
public function testIncludesParentsForNestedMatches()
@ -64,8 +64,8 @@ class CMSSiteTreeFilterTest extends SapphireTest
$f = new CMSSiteTreeFilter_Search(['Title' => 'Page 3b']);
$results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($parent));
$this->assertTrue($f->isPageIncluded($child));
$this->assertTrue($f->isRecordIncluded($parent));
$this->assertTrue($f->isRecordIncluded($child));
$this->assertEquals(1, count($results ?? []));
$this->assertEquals(
['ID' => $child->ID, 'ParentID' => $parent->ID],
@ -91,8 +91,8 @@ class CMSSiteTreeFilterTest extends SapphireTest
$f = new CMSSiteTreeFilter_ChangedPages(['Term' => 'Changed']);
$results = $f->pagesIncluded();
$this->assertTrue($f->isPageIncluded($changedPage));
$this->assertFalse($f->isPageIncluded($unchangedPage));
$this->assertTrue($f->isRecordIncluded($changedPage));
$this->assertFalse($f->isRecordIncluded($unchangedPage));
$this->assertEquals(1, count($results ?? []));
$this->assertEquals(
['ID' => $changedPage->ID, 'ParentID' => 0],
@ -130,11 +130,11 @@ class CMSSiteTreeFilterTest extends SapphireTest
);
$f = new CMSSiteTreeFilter_DeletedPages(['Term' => 'Page']);
$this->assertTrue($f->isPageIncluded($deletedPage));
$this->assertTrue($f->isRecordIncluded($deletedPage));
// Check that only changed pages are returned
$f = new CMSSiteTreeFilter_DeletedPages(['Term' => 'No Matches']);
$this->assertFalse($f->isPageIncluded($deletedPage));
$this->assertFalse($f->isRecordIncluded($deletedPage));
}
public function testStatusDraftPagesFilter()
@ -148,16 +148,16 @@ class CMSSiteTreeFilterTest extends SapphireTest
// Check draft page is shown
$f = new CMSSiteTreeFilter_StatusDraftPages(['Term' => 'Page']);
$this->assertTrue($f->isPageIncluded($draftPage));
$this->assertTrue($f->isRecordIncluded($draftPage));
// Check filter respects parameters
$f = new CMSSiteTreeFilter_StatusDraftPages(['Term' => 'No Match']);
$this->assertEmpty($f->isPageIncluded($draftPage));
$this->assertEmpty($f->isRecordIncluded($draftPage));
// Ensures empty array returned if no data to show
$f = new CMSSiteTreeFilter_StatusDraftPages();
$draftPage->delete();
$this->assertEmpty($f->isPageIncluded($draftPage));
$this->assertEmpty($f->isRecordIncluded($draftPage));
}
public function testDateFromToLastSameDate()
@ -171,7 +171,7 @@ class CMSSiteTreeFilterTest extends SapphireTest
'LastEditedTo' => $date,
]);
$this->assertTrue(
$filter->isPageIncluded($draftPage),
$filter->isRecordIncluded($draftPage),
'Using the same date for from and to should show find that page'
);
}
@ -189,16 +189,16 @@ class CMSSiteTreeFilterTest extends SapphireTest
// Check live-only page is included
$f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages(['LastEditedFrom' => '2000-01-01 00:00']);
$this->assertTrue($f->isPageIncluded($removedDraftPage));
$this->assertTrue($f->isRecordIncluded($removedDraftPage));
// Check filter is respected
$f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages(['LastEditedTo' => '1999-01-01 00:00']);
$this->assertEmpty($f->isPageIncluded($removedDraftPage));
$this->assertEmpty($f->isRecordIncluded($removedDraftPage));
// Ensures empty array returned if no data to show
$f = new CMSSiteTreeFilter_StatusRemovedFromDraftPages();
$removedDraftPage->delete();
$this->assertEmpty($f->isPageIncluded($removedDraftPage));
$this->assertEmpty($f->isRecordIncluded($removedDraftPage));
}
public function testStatusDeletedFilter()
@ -214,10 +214,10 @@ class CMSSiteTreeFilterTest extends SapphireTest
// Check deleted page is included
$f = new CMSSiteTreeFilter_StatusDeletedPages(['Title' => 'Page']);
$this->assertTrue($f->isPageIncluded($checkParentExists));
$this->assertTrue($f->isRecordIncluded($checkParentExists));
// Check filter is respected
$f = new CMSSiteTreeFilter_StatusDeletedPages(['Title' => 'Bobby']);
$this->assertFalse($f->isPageIncluded($checkParentExists));
$this->assertFalse($f->isRecordIncluded($checkParentExists));
}
}