diff --git a/_config.php b/_config.php
index 0fb43491..75c19579 100644
--- a/_config.php
+++ b/_config.php
@@ -1,8 +1,6 @@
register(
[SiteTree::class, 'link_shortcode_handler']
);
-CMSMenu::remove_menu_class(CMSMain::class);
CMSMenu::remove_menu_class(CMSPageEditController::class);
CMSMenu::remove_menu_class(CMSPageSettingsController::class);
-CMSMenu::remove_menu_class(CMSPageAddController::class);
diff --git a/_config/adminpanels.yml b/_config/adminpanels.yml
index 0c13ba40..3704d101 100644
--- a/_config/adminpanels.yml
+++ b/_config/adminpanels.yml
@@ -2,4 +2,4 @@
Name: cmsdefaultadmin
---
SilverStripe\Admin\AdminRootController:
- default_panel: SilverStripe\CMS\Controllers\CMSPagesController
+ default_panel: SilverStripe\CMS\Controllers\CMSMain
diff --git a/_config/cache.yml b/_config/cache.yml
index 65c7b080..0237deb2 100644
--- a/_config/cache.yml
+++ b/_config/cache.yml
@@ -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"
\ No newline at end of file
+ namespace: "SiteTree_PageIcons"
diff --git a/_config/permissions.yml b/_config/permissions.yml
index e1401cf6..00bb9048 100644
--- a/_config/permissions.yml
+++ b/_config/permissions.yml
@@ -17,4 +17,3 @@ SilverStripe\Core\Injector\Injector:
Services:
- '%$SilverStripe\Security\PermissionChecker.sitetree'
- '%$SilverStripe\CMS\Controllers\CMSMain'
- - '%$SilverStripe\CMS\Model\SiteTree'
diff --git a/client/dist/js/bundle.js b/client/dist/js/bundle.js
index 45acbd6d..2477f0aa 100644
--- a/client/dist/js/bundle.js
+++ b/client/dist/js/bundle.js
@@ -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("",{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:''+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{(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("",{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:''+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' + child.Title,
+ 'label': '' + child.Title,
'_class': 'class-' + child.ClassName.replace(/[^a-zA-Z0-9\-_:.]+/g, '_'),
'action': function(obj) {
$('.cms-container').entwine('.ss').loadPanel(
diff --git a/code/Controllers/CMSMain.php b/code/Controllers/CMSMain.php
index 61f3635a..28d8eca2 100644
--- a/code/Controllers/CMSMain.php
+++ b/code/Controllers/CMSMain.php
@@ -13,10 +13,11 @@ 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\Model\RedirectorPage;
+use SilverStripe\CMS\Forms\CMSMainAddForm;
+use SilverStripe\CMS\Model\CurrentRecordIdentifier;
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;
@@ -24,11 +25,14 @@ use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Control\PjaxResponseNegotiator;
use SilverStripe\Core\Cache\MemberCacheFlusher;
+use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Flushable;
use SilverStripe\Core\Injector\Injector;
+use SilverStripe\Core\Manifest\ModuleResource;
+use SilverStripe\Core\Manifest\ModuleResourceLoader;
use SilverStripe\Forms\DateField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldGroup;
@@ -47,10 +51,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 +62,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,60 +72,56 @@ 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;
+use SilverStripe\View\ThemeResourceLoader;
/**
* The main "content" area of the CMS.
*
* 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 +135,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
'EditForm',
'schema',
'SearchForm',
- 'SiteTreeAsUL',
+ 'TreeAsUL',
'getshowdeletedsubtree',
'savetreenode',
'getsubtree',
@@ -150,32 +147,32 @@ 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',
+ 'TreeHints' => 'HTMLFragment',
'SecurityID' => 'Text',
- 'SiteTreeAsUL' => 'HTMLFragment',
+ 'TreeAsUL' => 'HTMLFragment',
+ 'RecordTreeMarkup' => 'HTMLFragment',
];
- private static $dependencies = [
- 'HintsCache' => '%$' . CacheInterface::class . '.CMSMain_SiteTreeHints',
+ private static array $dependencies = [
+ 'HintsCache' => '%$' . CacheInterface::class . '.CMSMain_TreeHints',
+ 'creatableChildrenCache' => '%$' . CacheInterface::class . '.SiteTree_CreatableChildren',
];
- /**
- * @var CacheInterface
- */
- protected $hintsCache;
+ protected ?CacheInterface $hintsCache = null;
+
+ private ?CacheInterface $creatableChildrenCache = null;
protected function init()
{
@@ -197,7 +194,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 +213,34 @@ 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'));
- }
-
- /**
- * Page list view for edit-form
- *
- * @return DBHTMLText
- */
- public function PageListSidebar()
- {
- return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
+ return $this->renderWith($this->getTemplatesWithSuffix('_RecordList'));
}
/**
* 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 +269,19 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $link;
}
- public function LinkPages()
+ public function LinkRecords()
{
- return CMSPagesController::singleton()->Link();
+ // @TODO remove this and replace with just Link() when other CMSMain subclasses are gone
+ $controller = $this;
+ if (static::class !== CMSMain::class) {
+ $controller = CMSMain::singleton();
+ }
+ return $controller->Link();
}
- public function LinkPagesWithSearch()
+ public function LinkRecordsWithSearch()
{
- return $this->LinkWithSearch($this->LinkPages());
+ return $this->LinkWithSearch($this->LinkRecords());
}
/**
@@ -306,7 +291,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 +302,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 +351,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 +384,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)
@@ -421,31 +409,35 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return 'edit';
}
- /**
- * @param CacheInterface $cache
- * @return $this
- */
- public function setHintsCache(CacheInterface $cache)
+ public function setHintsCache(CacheInterface $cache): static
{
$this->hintsCache = $cache;
-
return $this;
}
- /**
- * @return CacheInterface $cache
- */
- public function getHintsCache()
+ public function getHintsCache(): ?CacheInterface
{
return $this->hintsCache;
}
+ public function setCreatableChildrenCache(CacheInterface $cache): static
+ {
+ $this->creatableChildrenCache = $cache;
+ return $this;
+ }
+
+ public function getCreatableChildrenCache(): ?CacheInterface
+ {
+ return $this->creatableChildrenCache;
+ }
+
/**
* Clears all dependent cache backends
*/
public function clearCache()
{
$this->getHintsCache()->clear();
+ $this->getCreatableChildrenCache()->clear();
}
public function LinkWithSearch($link)
@@ -463,10 +455,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);
@@ -479,30 +471,27 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $link;
}
- /**
- * @return string
- */
- public function LinkPreview()
+ public function add()
{
- $record = $this->getRecord($this->currentPageID());
- $baseLink = Director::absoluteBaseURL();
- if ($record && $record instanceof SiteTree) {
- // if we are an external redirector don't show a link
- if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
- $baseLink = false;
- } else {
- $baseLink = $record->Link('?stage=Stage');
- }
+ if ($this->getRequest()->isAjax()) {
+ return $this->AddForm()->forTemplate();
}
- return $baseLink;
+ 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 +499,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 +517,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 +539,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 +557,22 @@ 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();
+ $checker = ClassInfo::hasMethod($modelClass, '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 +581,6 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
);
}
-
/**
* Get callback to determine template customisations for nodes
*
@@ -599,14 +589,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 +607,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 +625,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 +641,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 +674,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,10 +687,10 @@ 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)
+ ->filter('Sort:GreaterThan', $record->Sort) // @TODO here's an assumed field
->first();
if (!$next) {
@@ -750,17 +737,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 +755,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 +791,20 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$node->write();
$statusUpdates['modified'][$node->ID] = [
- 'TreeTitle' => $node->TreeTitle
+ 'TreeTitle' => $this->getRecordTreeMarkup($node),
];
- // 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' => $this->getRecordTreeMarkup($virtualPage),
];
}
$this->getResponse()->addHeader(
'X-Status',
- rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.') ?? '')
+ rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL2', 'Reorganised the tree successfully.') ?? '')
);
}
@@ -830,7 +816,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$node->Sort = ++$counter;
$node->write();
$statusUpdates['modified'][$node->ID] = [
- 'TreeTitle' => $node->TreeTitle
+ 'TreeTitle' => $this->getRecordTreeMarkup($node),
];
} elseif (is_numeric($id)) {
// Nodes that weren't "actually moved" shouldn't be registered as
@@ -846,7 +832,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 +843,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 +895,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 +974,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 +1000,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 +1012,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()
+ CMSMain::menu_title(), // @TODO make static for subclasses to have their own
+ $unlinked ? false : $this->LinkRecords()
);
if ($this->TreeIsFiltered()) {
@@ -1056,13 +1028,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 +1056,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 +1095,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 +1104,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 +1126,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 +1142,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 +1153,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 +1174,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 +1197,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' => $this->getRecordIconUrl($instance),
'Title' => $instance->i18n_singular_name(),
]));
}
@@ -1238,25 +1208,115 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $result;
}
+ /**
+ * Get the URL to the icon for this record, if there is one
+ */
+ public function getRecordIconUrl(DataObject|string $recordOrClass): ?string
+ {
+ if (is_string($recordOrClass)) {
+ return Config::inst()->get($recordOrClass, 'cms_icon');
+ } else {
+ $icon = $recordOrClass::config()->get('cms_icon');
+ }
+ if (!$icon) {
+ return null;
+ }
+ if (strpos($icon ?? '', 'data:image/') !== false) {
+ return $icon;
+ }
+
+ // Icon is relative resource
+ $iconResource = ModuleResourceLoader::singleton()->resolveResource($icon);
+ if ($iconResource instanceof ModuleResource) {
+ return $iconResource->getURL();
+ }
+
+ // Icon is themed resource
+ $iconThemedUrl = ThemeResourceLoader::themedResourceURL($icon);
+ if ($iconThemedUrl) {
+ return $iconThemedUrl;
+ }
+
+ // Full path to file
+ if (Director::fileExists($icon)) {
+ return ModuleResourceLoader::resourceURL($icon);
+ }
+
+ // Skip invalid files
+ return null;
+ }
+
+ /**
+ * Get the CSS class for the record's icon, if there is one
+ */
+ public function getRecordIconCssClass(DataObject|string $recordOrClass): ?string
+ {
+ if (is_string($recordOrClass)) {
+ return Config::inst()->get($recordOrClass, 'cms_icon_class');
+ }
+ return $recordOrClass::config()->get('cms_icon_class');
+ }
+
+ /**
+ * Get the HTML markup to represent the record in a jstree structure.
+ *
+ * Returns three html elements, an empty with the class 'jstree-recordicon' in
+ * front, following by a wrapping around its MenuTitle, then following by a indicating its
+ * publication status.
+ */
+ public function getRecordTreeMarkup(DataObject $record): string
+ {
+ $children = $this->creatableChildPages($record);
+ $flags = $record->getStatusFlags();
+ $iconClasses = [
+ 'jstree-recordicon',
+ 'record-icon',
+ $this->getRecordIconCssClass($record), // @TODO used to use cms_icon too, but that can hold non-css classes!! See what happens now with custom icon file
+ 'class-' . Convert::raw2htmlid(get_class($record)),
+ ];
+ $record->invokeWithExtensions('updateTreeIconClasses', $iconClasses);
+ $treeTitle = sprintf(
+ '%s',
+ implode(' ', $iconClasses),
+ Convert::raw2att(json_encode($children)),
+ Convert::raw2xml(str_replace(["\n","\r"], "", $record->getTreeTitle()))
+ );
+ foreach ($flags as $class => $data) {
+ if (is_string($data)) {
+ $data = ['text' => $data];
+ }
+ $treeTitle .= sprintf(
+ '%s',
+ 'status-' . Convert::raw2xml($class),
+ (isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
+ Convert::raw2xml($data['text'])
+ );
+ }
+
+ return $treeTitle;
+ }
+
/**
* Get a database record to be managed by the CMS.
*
* @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 +1327,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 +1361,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 +1372,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$this->httpError(400);
return null;
}
- $this->setCurrentPageID($id);
+ $this->setCurrentRecordID($id);
}
return $this->getEditForm();
}
@@ -1318,13 +1380,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);
@@ -1339,29 +1400,12 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
// Add extra fields
- $deletedFromStage = !$record->isOnDraft();
- $fields->push($idField = new HiddenField("ID", false, $id));
- // Necessary for different subsites
- $fields->push($liveLinkField = new HiddenField("AbsoluteLink", false, $record->AbsoluteLink()));
- $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('ID', false, $id));
+ $fields->push($archiveWarningMsgField = new HiddenField('ArchiveWarningMessage'));
+ $fields->push(new HiddenField('TreeTitle', false, $this->getRecordTreeMarkup($record))); // @TODO do we need this?
$archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
- // Build preview / live links
- $liveLink = $record->getAbsoluteLiveLink();
- if ($liveLink) {
- $liveLinkField->setValue($liveLink);
- }
- if (!$deletedFromStage) {
- $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
- if ($stageLink) {
- $stageLinkField->setValue($stageLink);
- }
- }
-
// Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
if (($record instanceof CMSPreviewable || $record->has_extension(CMSPreviewable::class))
&& !$fields->fieldByName('SilverStripeNavigator')
@@ -1420,7 +1464,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
$form->addExtraClass('fill-height flexbox-area-grow');
- if (!$record->canEdit() || $deletedFromStage) {
+ if (!$record->canEdit() || ($record->hasExtension(Versioned::class) && $record->hasStages() && !$record->isOnDraft())) {
$readonlyFields = $form->Fields()->makeReadonly();
$form->setFields($readonlyFields);
}
@@ -1437,10 +1481,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 +1493,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 +1533,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 +1565,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 +1576,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 +1616,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 +1670,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 +1684,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 +1705,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 +1714,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
+ // Set up columns and sorting for list view GridField
+ $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 +1744,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(
- '%s child pages',
+ '%s child pages', // @TODO generalise and i18n-ify
$this->LinkListViewChildren((int)$item->ID),
$num
);
}
},
'getTreeTitle' => function ($value, &$item) {
- /** @var SiteTree $item */
+ /** @var DataObject $item */
$title = sprintf(
'%s',
$item->getCMSEditLink(),
- $item->TreeTitle // returns HTML, does its own escaping
+ $this->getRecordTreeMarkup($item) // returns HTML, does its own escaping
);
- $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,31 +1796,69 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
return $listview;
}
- public function currentPageID()
+ public function currentRecordID()
{
- $id = parent::currentPageID();
-
- $this->extend('updateCurrentPageID', $id);
-
+ $id = parent::currentRecordID();
+ $this->extend('updateCurrentRecordID', $id);
return $id;
}
+ /**
+ * Gets a list of the classes that can be created under this specific record.
+ *
+ * @return array Array of associative arrays with FQCNs, localised model names, and icon CSS classes.
+ */
+ protected function creatableChildPages(DataObject $record): array
+ {
+ // Build the list of candidate children
+ $cache = $this->getCreatableChildrenCache();
+ $cacheKey = $this->generateChildrenCacheKey();
+ $children = $cache->get($cacheKey, []);
+ $modelClass = $this->getModelClass();
+
+ if (!$children || !isset($children[$modelClass][$record->ID])) {
+ $children[$modelClass][$record->ID] = [];
+ $candidates = $this->getValidSubClasses();
+
+ foreach ($candidates as $childClass) {
+ $child = DataObject::singleton($childClass);
+ if ($child->canCreate(context: ['Parent' => $record])) {
+ $children[$modelClass][$record->ID][] = [
+ 'ClassName' => $childClass,
+ 'Title' => $child->i18n_singular_name(),
+ 'IconClass' => $this->getRecordIconCssClass($child),
+ ];
+ }
+ }
+ $cache->set($cacheKey, $children);
+ }
+
+ return $children[$modelClass][$record->ID];
+ }
+
+ protected function getValidSubClasses(): array
+ {
+ $modelClass = $this->getModelClass();
+ $classes = ClassInfo::getValidSubClasses($modelClass);
+ DataObject::singleton()->invokeWithExtensions('updateValidSubClasses', $classes);
+ return $classes;
+ }
+
//------------------------------------------------------------------------------------------//
// 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 +1876,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 +1899,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 +1931,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 +1946,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 +1970,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 +2019,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 +2050,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 +2079,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 +2100,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 +2122,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 +2141,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 +2159,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 +2178,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 +2192,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 +2251,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
$forms[$urlSegment] = $formHtml;
}
}
- $pageHtml = '';
+ $recordHtml = '';
foreach ($forms as $urlSegment => $html) {
- $pageHtml .= '
' . $html . '
';
+ $recordHtml .= '
' . $html . '
';
}
- return new LiteralField('BatchActionParameters', '
' . $pageHtml . '
');
+ return new LiteralField('BatchActionParameters', '
' . $recordHtml . '
');
}
/**
@@ -2158,7 +2267,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 +2275,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 +2311,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 +2356,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');
@@ -2266,11 +2389,11 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
public function providePermissions()
{
- $title = CMSPagesController::menu_title();
+ $title = CMSMain::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".'
@@ -2285,7 +2408,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*
* @return string
*/
- protected function getCMSTreeTitle()
+ public function getCMSTreeTitle()
{
$rootTitle = SiteConfig::current_site_config()->Title;
$this->extend('updateCMSTreeTitle', $rootTitle);
@@ -2293,7 +2416,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
}
/**
- * Cache key for SiteTreeHints() method
+ * Cache key for TreeHints() method
*
* @param $memberID
* @return string
@@ -2301,9 +2424,7 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
protected function generateHintsCacheKey($memberID)
{
$baseKey = $memberID . '_' . __CLASS__;
-
$this->extend('updateHintsCacheKey', $baseKey);
-
return md5($baseKey ?? '');
}
@@ -2322,16 +2443,28 @@ class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionPr
*/
public function flushMemberCache($memberIDs = null)
{
- $cache = $this->getHintsCache();
+ $hintsCache = $this->getHintsCache();
+ $childrenCache = $this->getCreatableChildrenCache();
if (!$memberIDs) {
- $cache->clear();
+ $hintsCache->clear();
+ $childrenCache->clear();
return;
}
foreach ($memberIDs as $memberID) {
- $key = $this->generateHintsCacheKey($memberID);
- $cache->delete($key);
+ $hintsKey = $this->generateHintsCacheKey($memberID);
+ $hintsCache->delete($hintsKey);
+ $childrenKey = $this->generateChildrenCacheKey($memberID);
+ $childrenCache->delete($childrenKey);
}
}
+
+ private function generateChildrenCacheKey(?int $memberID = null)
+ {
+ if ($memberID === null) {
+ $memberID = Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0;
+ }
+ return md5($memberID . '_' . __CLASS__);
+ }
}
diff --git a/code/Controllers/CMSPageAddController.php b/code/Controllers/CMSPageAddController.php
deleted file mode 100644
index 37f1e221..00000000
--- a/code/Controllers/CMSPageAddController.php
+++ /dev/null
@@ -1,239 +0,0 @@
-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(
- '%s%s',
- $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 = 'Step %d. %s';
-
- $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(
- '
%s
',
- _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());
- }
-}
diff --git a/code/Controllers/CMSPageEditController.php b/code/Controllers/CMSPageEditController.php
index 271d5929..36f8e567 100644
--- a/code/Controllers/CMSPageEditController.php
+++ b/code/Controllers/CMSPageEditController.php
@@ -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;
}
diff --git a/code/Controllers/CMSPageSettingsController.php b/code/Controllers/CMSPageSettingsController.php
index 34353c7c..68f987bf 100644
--- a/code/Controllers/CMSPageSettingsController.php
+++ b/code/Controllers/CMSPageSettingsController.php
@@ -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()
diff --git a/code/Controllers/CMSPagesController.php b/code/Controllers/CMSPagesController.php
deleted file mode 100644
index 98e6c630..00000000
--- a/code/Controllers/CMSPagesController.php
+++ /dev/null
@@ -1,34 +0,0 @@
-numChildrenMethod;
}
- public function getPageClasses($page)
+ public function getRecordClasses($page)
{
if ($this->_cache_ids === null) {
$this->populateIDs();
@@ -130,7 +130,7 @@ abstract class CMSSiteTreeFilter implements LeftAndMain_SearchFilter
/**
* Gets the list of filtered pages
*
- * @see {@link SiteTree::getStatusFlags()}
+ * @see {@link ModelData::getStatusFlags()}
* @return SS_List
*/
abstract public function getFilteredPages();
@@ -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();
diff --git a/code/Controllers/CMSSiteTreeFilter_PublishedPages.php b/code/Controllers/CMSSiteTreeFilter_PublishedPages.php
index 1e76cae0..5c120aeb 100644
--- a/code/Controllers/CMSSiteTreeFilter_PublishedPages.php
+++ b/code/Controllers/CMSSiteTreeFilter_PublishedPages.php
@@ -36,7 +36,7 @@ class CMSSiteTreeFilter_PublishedPages extends CMSSiteTreeFilter
/**
* Filters out all pages who's status who's status that doesn't exist on live
*
- * @see {@link SiteTree::getStatusFlags()}
+ * @see {@link ModelData::getStatusFlags()}
* @return SS_List
*/
public function getFilteredPages()
diff --git a/code/Controllers/CMSSiteTreeFilter_StatusDeletedPages.php b/code/Controllers/CMSSiteTreeFilter_StatusDeletedPages.php
index a541011f..bf9b58a6 100644
--- a/code/Controllers/CMSSiteTreeFilter_StatusDeletedPages.php
+++ b/code/Controllers/CMSSiteTreeFilter_StatusDeletedPages.php
@@ -30,7 +30,7 @@ class CMSSiteTreeFilter_StatusDeletedPages extends CMSSiteTreeFilter
/**
* Filters out all pages who's status is set to "Deleted".
*
- * @see {@link SiteTree::getStatusFlags()}
+ * @see {@link ModelData::getStatusFlags()}
* @return SS_List
*/
public function getFilteredPages()
diff --git a/code/Controllers/CMSSiteTreeFilter_StatusDraftPages.php b/code/Controllers/CMSSiteTreeFilter_StatusDraftPages.php
index 95cc5d89..dbed7234 100644
--- a/code/Controllers/CMSSiteTreeFilter_StatusDraftPages.php
+++ b/code/Controllers/CMSSiteTreeFilter_StatusDraftPages.php
@@ -20,7 +20,7 @@ class CMSSiteTreeFilter_StatusDraftPages extends CMSSiteTreeFilter
/**
* Filters out all pages who's status is set to "Draft".
*
- * @see {@link SiteTree::getStatusFlags()}
+ * @see {@link ModelData::getStatusFlags()}
* @return SS_List
*/
public function getFilteredPages()
diff --git a/code/Controllers/LeftAndMainPageIconsExtension.php b/code/Controllers/LeftAndMainPageIconsExtension.php
index ed0e7e69..93160ada 100644
--- a/code/Controllers/LeftAndMainPageIconsExtension.php
+++ b/code/Controllers/LeftAndMainPageIconsExtension.php
@@ -18,6 +18,8 @@ use SilverStripe\View\Requirements;
/**
* Extension to include custom page icons
*
+ * @TODO AAAHHHHHHHHHHH
+ *
* @extends Extension
*/
class LeftAndMainPageIconsExtension extends Extension implements Flushable
@@ -60,13 +62,13 @@ class LeftAndMainPageIconsExtension extends Extension implements Flushable
$css = '';
$classes = ClassInfo::subclassesFor(SiteTree::class);
foreach ($classes as $class) {
- if (!empty(Config::inst()->get($class, 'icon_class', Config::UNINHERITED))) {
+ if (!empty(Config::inst()->get($class, 'cms_icon_class', Config::UNINHERITED))) {
continue;
}
- $iconURL = SiteTree::singleton($class)->getPageIconURL();
+ $iconURL = CMSMain::singleton()->getRecordIconUrl(SiteTree::class);
if ($iconURL) {
$cssClass = Convert::raw2htmlid($class);
- $selector = sprintf('.page-icon.class-%1$s, li.class-%1$s > a .jstree-pageicon', $cssClass);
+ $selector = sprintf('.record-icon.class-%1$s, li.class-%1$s > a .jstree-recordicon', $cssClass);
$css .= sprintf('%s { background: transparent url(\'%s\') 0 0 no-repeat; }', $selector, $iconURL);
}
}
diff --git a/code/Forms/CMSMainAddForm.php b/code/Forms/CMSMainAddForm.php
new file mode 100644
index 00000000..bdc2a91b
--- /dev/null
+++ b/code/Forms/CMSMainAddForm.php
@@ -0,0 +1,230 @@
+getModelClass();
+ $singleton = DataObject::singleton($modelClass);
+ $recordTypes = [];
+ $defaultIcon = $controller->getRecordIconCssClass($singleton);
+ $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');
+ $typeSingleton = DataObject::singleton($class);
+ $icon = $controller->getRecordIconCssClass($typeSingleton) ?: $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($controller->getRecordIconUrl($typeSingleton))) {
+ $icon = '';
+ }
+
+ $html = sprintf(
+ '%s%s',
+ $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 = 'Step %d. %s';
+
+ $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
+ ),
+ $childTitle
+ )
+ ]
+ ),
+ LiteralField::create(
+ 'RestrictedNote',
+ sprintf(
+ '
%s
',
+ _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()
+ }
+}
diff --git a/code/Forms/InternalLinkFormFactory.php b/code/Forms/InternalLinkFormFactory.php
index 045c695a..37781e16 100644
--- a/code/Forms/InternalLinkFormFactory.php
+++ b/code/Forms/InternalLinkFormFactory.php
@@ -28,9 +28,7 @@ class InternalLinkFormFactory extends LinkFormFactory
TreeDropdownField::create(
'PageID',
_t(__CLASS__.'.SELECT_PAGE', 'Select a page'),
- SiteTree::class,
- 'ID',
- 'TreeTitle'
+ SiteTree::class
)
->setTitleField('MenuTitle')
->setHasEmptyDefault(true),
diff --git a/code/Model/CurrentPageIdentifier.php b/code/Model/CurrentRecordIdentifier.php
similarity index 75%
rename from code/Model/CurrentPageIdentifier.php
rename to code/Model/CurrentRecordIdentifier.php
index bee76b82..9c004fa7 100644
--- a/code/Model/CurrentPageIdentifier.php
+++ b/code/Model/CurrentRecordIdentifier.php
@@ -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);
}
diff --git a/code/Model/RedirectorPage.php b/code/Model/RedirectorPage.php
index 6eaec826..d7e7a2df 100644
--- a/code/Model/RedirectorPage.php
+++ b/code/Model/RedirectorPage.php
@@ -23,7 +23,7 @@ class RedirectorPage extends Page
{
private static $description = 'Redirects requests to another location';
- private static $icon_class = 'font-icon-p-redirect';
+ private static $cms_icon_class = 'font-icon-p-redirect';
private static $show_stage_link = false;
diff --git a/code/Model/SiteTree.php b/code/Model/SiteTree.php
index 2ee8e6b1..87c4397d 100755
--- a/code/Model/SiteTree.php
+++ b/code/Model/SiteTree.php
@@ -7,7 +7,6 @@ use Psr\SimpleCache\CacheInterface;
use SilverStripe\Admin\CMSEditLinkExtension;
use SilverStripe\Assets\Shortcodes\FileLinkTracking;
use SilverStripe\CMS\Controllers\CMSMain;
-use SilverStripe\CMS\Controllers\CMSPageEditController;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\CMS\Controllers\ModelAsController;
use SilverStripe\CMS\Controllers\RootURLController;
@@ -15,14 +14,11 @@ use SilverStripe\CMS\Forms\SiteTreeURLSegmentField;
use SilverStripe\Control\ContentNegotiator;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
-use SilverStripe\Core\Cache\MemberCacheFlusher;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Flushable;
use SilverStripe\Core\Injector\Injector;
-use SilverStripe\Core\Manifest\ModuleResource;
-use SilverStripe\Core\Manifest\ModuleResourceLoader;
use SilverStripe\Core\Manifest\VersionProvider;
use SilverStripe\Core\Resettable;
use SilverStripe\Forms\CheckboxField;
@@ -35,19 +31,15 @@ use SilverStripe\Forms\FormField;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldDataColumns;
use SilverStripe\Forms\GridField\GridFieldLazyLoader;
-use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
-use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\SearchableMultiDropdownField;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextareaField;
-use SilverStripe\Forms\TextField;
use SilverStripe\Forms\ToggleCompositeField;
use SilverStripe\Forms\TreeDropdownField;
use SilverStripe\Forms\TreeMultiselectField;
-use SilverStripe\i18n\i18n;
use SilverStripe\i18n\i18nEntityProvider;
use SilverStripe\Model\List\ArrayList;
use SilverStripe\ORM\CMSPreviewable;
@@ -57,8 +49,8 @@ use SilverStripe\ORM\DB;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\HiddenClass;
use SilverStripe\ORM\Hierarchy\Hierarchy;
-use SilverStripe\ORM\ManyManyList;
use SilverStripe\Core\Validation\ValidationResult;
+use SilverStripe\Forms\HiddenField;
use SilverStripe\Security\Group;
use SilverStripe\Security\InheritedPermissions;
use SilverStripe\Security\InheritedPermissionsExtension;
@@ -112,7 +104,7 @@ use SilverStripe\View\SSViewer;
* @mixin InheritedPermissionsExtension
* @method HasManyList BackLinks()
*/
-class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvider, CMSPreviewable, Resettable, Flushable, MemberCacheFlusher
+class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvider, CMSPreviewable, Resettable, Flushable
{
/**
* Indicates what kind of children this page type can have.
@@ -285,7 +277,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
'Link' => 'Text',
'RelativeLink' => 'Text',
'AbsoluteLink' => 'Text',
- 'TreeTitle' => 'HTMLFragment',
'MetaTags' => 'HTMLFragment',
];
@@ -338,21 +329,29 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
private static $can_create = true;
/**
- * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
- * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
+ * Icon to use in the CMS page tree.
+ *
+ * This should be one of the following:
+ * - the full filename, relative to the webroot
+ * - inline "data:image/" url string
+ * - a themed resource reference
+ * - a module resource reference
+ *
+ * If cms_icon_class has a value, cms_icon is ignored for most purposes.
*
* @see LeftAndMainPageIconsExtension::generatePageIconsCss()
* @config
* @var string
*/
- private static $icon = null;
+ private static $cms_icon = null;
/**
* Class attached to page icons in the CMS page tree. Also supports font-icon set.
+ * Overrides cms_icon for most purposes
* @config
* @var string
*/
- private static $icon_class = 'font-icon-page';
+ private static $cms_icon_class = 'font-icon-page';
private static $extensions = [
Hierarchy::class,
@@ -410,8 +409,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/
private static $show_meta_generator_version = true;
- protected $_cache_statusFlags = null;
-
/**
* Plural form for SiteTree / Page classes. Not inherited by subclasses.
*
@@ -452,18 +449,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
*/
private static $base_description = 'Generic content page';
- /**
- * @var array
- */
- private static $dependencies = [
- 'creatableChildrenCache' => '%$' . CacheInterface::class . '.SiteTree_CreatableChildren'
- ];
-
- /**
- * @var CacheInterface
- */
- protected $creatableChildrenCache;
-
/**
* @var VersionProvider
*/
@@ -579,8 +564,17 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
public static function page_type_classes()
{
$classes = ClassInfo::getValidSubClasses();
+ SiteTree::singleton()->updateValidSubClasses($classes);
+ return $classes;
+ }
- $baseClassIndex = array_search(SiteTree::class, $classes ?? []);
+ /**
+ * Update a list of classes to exclude page types that should be hidden through {@link SiteTree::$hide_pagetypes}
+ * {@see CMSMain::getValidSubClasses}
+ */
+ public function updateValidSubClasses(array &$classes): void
+ {
+ $baseClassIndex = array_search(SiteTree::class, $classes);
if ($baseClassIndex !== false) {
unset($classes[$baseClassIndex]);
}
@@ -601,17 +595,26 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
// If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
// requirements
if ($kill_ancestors) {
- $kill_ancestors = array_unique($kill_ancestors ?? []);
+ $kill_ancestors = array_unique($kill_ancestors);
foreach ($kill_ancestors as $mark) {
// unset from $classes
- $idx = array_search($mark, $classes ?? [], true);
+ $idx = array_search($mark, $classes, true);
if ($idx !== false) {
unset($classes[$idx]);
}
}
}
+ }
- return $classes;
+ /**
+ * Update CSS classes for the icon used in the CMS site tree
+ * {@see CMSMain::getRecordTreeMarkup}
+ */
+ public function updateTreeIconClasses(array &$classes): void
+ {
+ if ($this->isHomePage()) {
+ $classes[] = 'homepage';
+ }
}
/**
@@ -783,12 +786,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;
}
@@ -1015,25 +1018,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
return null;
}
- /**
- * @param CacheInterface $cache
- * @return $this
- */
- public function setCreatableChildrenCache(CacheInterface $cache)
- {
- $this->creatableChildrenCache = $cache;
-
- return $this;
- }
-
- /**
- * @return CacheInterface $cache
- */
- public function getCreatableChildrenCache()
- {
- return $this->creatableChildrenCache;
- }
-
/**
* Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
*
@@ -1812,32 +1796,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
parent::onAfterDelete();
}
- public function flushCache($persistent = true)
- {
- parent::flushCache($persistent);
- $this->_cache_statusFlags = null;
- }
-
- /**
- * Flushes the member specific cache for creatable children
- *
- * @param array $memberIDs
- */
- public function flushMemberCache($memberIDs = null)
- {
- $cache = SiteTree::singleton()->getCreatableChildrenCache();
-
- if (!$memberIDs) {
- $cache->clear();
- return;
- }
-
- foreach ($memberIDs as $memberID) {
- $key = $this->generateChildrenCacheKey($memberID);
- $cache->delete($key);
- }
- }
-
public function validate()
{
$result = parent::validate();
@@ -2241,6 +2199,14 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
"Title"
);
}
+
+ // Necessary for updating URLSegment when changing the default page title.
+ // LiveLink is (for better or for worse) used as part of the "is this page new?" detection.
+ $fields->push($liveLinkField = HiddenField::create('LiveLink'));
+ $liveLink = $this->getAbsoluteLiveLink();
+ if ($liveLink) {
+ $liveLinkField->setValue($liveLink);
+ }
});
return parent::getCMSFields();
@@ -2778,9 +2744,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;
@@ -2834,41 +2800,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
return $allowedChildren;
}
- /**
- *
- * Gets a list of the page types that can be created under this specific page, including font icons
- *
- * @return array
- */
- public function creatableChildPages()
- {
- // Build the list of candidate children
- $cache = SiteTree::singleton()->getCreatableChildrenCache();
- $cacheKey = $this->generateChildrenCacheKey(Security::getCurrentUser() ? Security::getCurrentUser()->ID : 0);
- $children = $cache->get($cacheKey, []);
-
- if (!$children || !isset($children[$this->ID])) {
- $children[$this->ID] = [];
- $candidates = static::page_type_classes();
-
- foreach ($candidates as $childClass) {
- $child = singleton($childClass);
-
- if ($child->canCreate(null, ['Parent' => $this])) {
- $children[$this->ID][] = [
- 'ClassName' => $childClass,
- 'Title' => $child->i18n_singular_name(),
- 'IconClass' => $child->getIconClass(),
- ];
- }
- }
-
- $cache->set($cacheKey, $children);
- }
-
- return $children[$this->ID];
- }
-
/**
* Returns the class name of the default class for children of this page.
*
@@ -2927,102 +2858,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
}
}
- /**
- * A flag provides the user with additional data about the current page status, for example a "removed from draft"
- * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
- * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
- * the flags.
- *
- * Example (simple):
- * "deletedonlive" => "Deleted"
- *
- * Example (with optional title attribute):
- * "deletedonlive" => ['text' => "Deleted", 'title' => 'This page has been deleted']
- *
- * @param bool $cached Whether to serve the fields from cache; false regenerate them
- * @return array
- */
- public function getStatusFlags($cached = true)
- {
- if (!$this->_cache_statusFlags || !$cached) {
- $flags = [];
- if ($this->isOnLiveOnly()) {
- $flags['removedfromdraft'] = [
- 'text' => _t(__CLASS__.'.ONLIVEONLYSHORT', 'On live only'),
- 'title' => _t(__CLASS__.'.ONLIVEONLYSHORTHELP', 'Page is published, but has been deleted from draft'),
- ];
- } elseif ($this->isArchived()) {
- $flags['archived'] = [
- 'text' => _t(__CLASS__.'.ARCHIVEDPAGESHORT', 'Archived'),
- 'title' => _t(__CLASS__.'.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
- ];
- } elseif ($this->isOnDraftOnly()) {
- $flags['addedtodraft'] = [
- 'text' => _t(__CLASS__.'.ADDEDTODRAFTSHORT', 'Draft'),
- 'title' => _t(__CLASS__.'.ADDEDTODRAFTHELP', "Page has not been published yet")
- ];
- } elseif ($this->isModifiedOnDraft()) {
- $flags['modified'] = [
- 'text' => _t(__CLASS__.'.MODIFIEDONDRAFTSHORT', 'Modified'),
- 'title' => _t(__CLASS__.'.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
- ];
- }
-
- $this->extend('updateStatusFlags', $flags);
-
- $this->_cache_statusFlags = $flags;
- }
-
- return $this->_cache_statusFlags;
- }
-
- /**
- * Returns the CSS class used for the page icon in the site tree.
- *
- * @return string
- */
- public function getIconClass()
- {
- if ($this->config()->get('icon')) {
- return '';
- }
- return $this->config()->get('icon_class');
- }
-
- /**
- * getTreeTitle will return three html DOM elements, an empty with the class 'jstree-pageicon' in
- * front, following by a wrapping around its MenuTitle, then following by a indicating its
- * publication status.
- *
- * @return string An HTML string ready to be directly used in a template
- */
- public function getTreeTitle()
- {
- $children = $this->creatableChildPages();
- $flags = $this->getStatusFlags();
- $treeTitle = sprintf(
- '%s',
- $this->getIconClass(),
- Convert::raw2htmlid(static::class),
- $this->isHomePage() ? ' homepage' : '',
- Convert::raw2att(json_encode($children)),
- Convert::raw2xml(str_replace(["\n","\r"], "", $this->MenuTitle ?? ''))
- );
- foreach ($flags as $class => $data) {
- if (is_string($data)) {
- $data = ['text' => $data];
- }
- $treeTitle .= sprintf(
- "%s",
- 'status-' . Convert::raw2xml($class),
- (isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
- Convert::raw2xml($data['text'])
- );
- }
-
- return $treeTitle;
- }
-
/**
* Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
* we're currently inside, etc.
@@ -3214,36 +3049,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
return parent::plural_name();
}
- /**
- * Generate link to this page's icon
- *
- * @return string
- */
- public function getPageIconURL()
- {
- $icon = $this->config()->get('icon');
- if (!$icon) {
- return null;
- }
- if (strpos($icon ?? '', 'data:image/') !== false) {
- return $icon;
- }
-
- // Icon is relative resource
- $iconResource = ModuleResourceLoader::singleton()->resolveResource($icon);
- if ($iconResource instanceof ModuleResource) {
- return $iconResource->getURL();
- }
-
- // Full path to file
- if (Director::fileExists($icon)) {
- return ModuleResourceLoader::resourceURL($icon);
- }
-
- // Skip invalid files
- return null;
- }
-
/**
* Get description for this page type
*
@@ -3343,17 +3148,6 @@ class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvi
}
}
- /**
- * Cache key for creatableChildPages() method
- *
- * @param int $memberID
- * @return string
- */
- protected function generateChildrenCacheKey($memberID)
- {
- return md5($memberID . '_' . __CLASS__);
- }
-
/**
* Get the list of excluded root URL segments
*
diff --git a/code/Model/VirtualPage.php b/code/Model/VirtualPage.php
index 1237d3ee..44ac2f74 100644
--- a/code/Model/VirtualPage.php
+++ b/code/Model/VirtualPage.php
@@ -30,7 +30,7 @@ class VirtualPage extends Page
{
private static $description = 'Displays the content of another page';
- private static $icon_class = 'font-icon-p-virtual';
+ private static $cms_icon_class = 'font-icon-p-virtual';
public static $virtualFields;
diff --git a/code/Reports/BrokenLinksReport.php b/code/Reports/BrokenLinksReport.php
index c74c990b..a9b99a0c 100644
--- a/code/Reports/BrokenLinksReport.php
+++ b/code/Reports/BrokenLinksReport.php
@@ -140,7 +140,7 @@ class BrokenLinksReport extends Report
'title' => _t(__CLASS__ . '.ColumnURL', 'URL'),
'formatting' => function ($value, $item) {
/** @var SiteTree $item */
- $liveLink = $item->AbsoluteLiveLink;
+ $liveLink = $item->getAbsoluteLiveLink();
$stageLink = $item->AbsoluteLink();
return sprintf(
'%s %s',
diff --git a/lang/en.yml b/lang/en.yml
index 3c012c9c..95b78778 100644
--- a/lang/en.yml
+++ b/lang/en.yml
@@ -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}'
diff --git a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_AddForm.ss b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_AddForm.ss
new file mode 100644
index 00000000..707848d4
--- /dev/null
+++ b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_AddForm.ss
@@ -0,0 +1,47 @@
+
+
+
diff --git a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_Content.ss b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_Content.ss
index 0dbb1653..48400472 100644
--- a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_Content.ss
+++ b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_Content.ss
@@ -1,27 +1,28 @@
+<% if $CurrentRecord %>
+<%-- Left and right panel panel only --%>
$Tools
-
-
+ <% include SilverStripe\\Admin\\BackLink_Button Backlink=$BreadcrumbsBacklink %>
<% include SilverStripe\\Admin\\CMSBreadcrumbs %>
diff --git a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_LeftPanel.ss b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_LeftPanel.ss
new file mode 100644
index 00000000..12e1feba
--- /dev/null
+++ b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_LeftPanel.ss
@@ -0,0 +1,24 @@
+
+
+ <% if $TreeIsFiltered %>
+ <% include SilverStripe\\Admin\\BackLink_Button Backlink=$BreadcrumbsBacklink %>
+ <% end_if %>
+ <% if $CurrentRecord %>
+ <%-- Explicit breadcrumb item for this menu section --%>
+
+ $MenuCurrentItem.Title
+
+ <% else %>
+ <%-- Full breadcrumbs (useful for tree view which isn't available when viewing an edit form) --%>
+ <% include SilverStripe\\Admin\\CMSBreadcrumbs %>
+ <% end_if %>
+ <% include SilverStripe\\CMS\\Controllers\\CMSMain_Filter %>
+
+
+
+
+
+
+
+ $RecordList
+
diff --git a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_ListView.ss b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_ListView.ss
index aed42dfe..e9257f35 100644
--- a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_ListView.ss
+++ b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_ListView.ss
@@ -1,6 +1,6 @@
-<% include SilverStripe\\CMS\\Controllers\\CMSPagesController_ContentToolActions %>
+<% include SilverStripe\\CMS\\Controllers\\CMSMain_ContentToolActions %>
-
+
$AddForm
diff --git a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_PageList_Sidebar.ss b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_PageList_Sidebar.ss
deleted file mode 100644
index 296f8a6e..00000000
--- a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_PageList_Sidebar.ss
+++ /dev/null
@@ -1 +0,0 @@
-<% include SilverStripe\\CMS\\Controllers\\CMSMain_PageList %>
diff --git a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_PageList.ss b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_RecordList.ss
similarity index 100%
rename from templates/SilverStripe/CMS/Controllers/Includes/CMSMain_PageList.ss
rename to templates/SilverStripe/CMS/Controllers/Includes/CMSMain_RecordList.ss
diff --git a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_SubTree.ss b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_SubTree.ss
index 34140b29..740124ae 100644
--- a/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_SubTree.ss
+++ b/templates/SilverStripe/CMS/Controllers/Includes/CMSMain_SubTree.ss
@@ -4,7 +4,7 @@
<% if $limited %>
- <% include SilverStripe\\CMS\\Controllers\\CMSMain_Filter %>
+<%-- If we're editing a record, include the left panel and allow it to be collapsed --%>
+<% if $CurrentRecord %>
+
+ <% include SilverStripe\\CMS\\Controllers\\CMSMain_LeftPanel %>
+
<% 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 --%>
<% end_if %>