This commit is contained in:
Guy Sartorelli 2024-10-17 03:40:21 +00:00 committed by GitHub
commit 7221223567
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 833 additions and 806 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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