From 8b8b8e36200fa55473a2f180054ecbfe4cbe46ef Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Mon, 11 Sep 2017 12:01:41 +1200 Subject: [PATCH 1/3] API Add React modal popup for reviewing content in SiteTree * Add i18n javascript source file and Transifex configuration * Add npm requirements, React + entwine wrapper for CMS * Overload LeftAndMain::getSchema... methods in extension so they can be used * Refactor CSS to only the content review button --- .tx/config | 6 + _config/config.yml | 25 ++- client/dist/js/contentreview.js | 2 +- client/dist/js/contentreview.js.map | 2 +- client/dist/styles/contentreview.css | 2 +- client/dist/styles/contentreview.css.map | 2 +- client/lang/src/en.json | 3 + client/src/bundles/ContentReviewForm.js | 101 ++++++++++++ client/src/bundles/ContentReviewPopup.js | 11 -- client/src/bundles/ContentReviewSettings.js | 4 +- client/src/bundles/PagesDueForReview.js | 4 +- client/src/bundles/bundle.js | 4 +- client/src/styles/ContentReviewForm.scss | 17 ++ client/src/styles/bundle.scss | 2 +- client/src/styles/contentreview.scss | 50 ------ lang/en.yml | 8 + package.json | 10 +- src/Extensions/ContentReviewCMSExtension.php | 153 +++++++++++++----- .../ContentReviewLeftAndMainExtension.php | 20 +++ src/Extensions/SiteTreeContentReview.php | 29 +--- src/Forms/ReviewContentHandler.php | 139 ++++++++++++++++ src/Forms/ReviewContentHandlerFormAction.php | 19 +++ .../SiteTreeContentReview_button.ss | 3 + webpack.config.js | 4 +- yarn.lock | 118 +++++++++++++- 25 files changed, 586 insertions(+), 152 deletions(-) create mode 100644 client/lang/src/en.json create mode 100644 client/src/bundles/ContentReviewForm.js delete mode 100644 client/src/bundles/ContentReviewPopup.js create mode 100644 client/src/styles/ContentReviewForm.scss delete mode 100644 client/src/styles/contentreview.scss create mode 100644 src/Extensions/ContentReviewLeftAndMainExtension.php create mode 100644 src/Forms/ReviewContentHandler.php create mode 100644 src/Forms/ReviewContentHandlerFormAction.php create mode 100644 templates/SilverStripe/ContentReview/Extensions/SiteTreeContentReview_button.ss diff --git a/.tx/config b/.tx/config index b4ac4e5..75ad0ea 100644 --- a/.tx/config +++ b/.tx/config @@ -6,3 +6,9 @@ file_filter = lang/.yml source_file = lang/en.yml source_lang = en type = YML + +[silverstripe-asset-admin.master-js] +file_filter = client/lang/src/.json +source_file = client/lang/src/en.json +source_lang = en +type = KEYVALUEJSON diff --git a/_config/config.yml b/_config/config.yml index 120f2eb..df7e28c 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -1,21 +1,30 @@ --- Name: contentreviewextensions --- -SilverStripe\CMS\Model\SiteTree: +SilverStripe\Admin\LeftAndMain: extensions: - - SilverStripe\ContentReview\Extensions\SiteTreeContentReview -SilverStripe\Security\Group: - extensions: - - SilverStripe\ContentReview\Extensions\ContentReviewOwner -SilverStripe\Security\Member: - extensions: - - SilverStripe\ContentReview\Extensions\ContentReviewOwner + - SilverStripe\ContentReview\Extensions\ContentReviewLeftAndMainExtension + SilverStripe\CMS\Controllers\CMSPageEditController: extensions: - SilverStripe\ContentReview\Extensions\ContentReviewCMSExtension + SilverStripe\CMS\Controllers\CMSPageSettingsController: extensions: - SilverStripe\ContentReview\Extensions\ContentReviewCMSExtension + +SilverStripe\CMS\Model\SiteTree: + extensions: + - SilverStripe\ContentReview\Extensions\SiteTreeContentReview + +SilverStripe\Security\Group: + extensions: + - SilverStripe\ContentReview\Extensions\ContentReviewOwner + +SilverStripe\Security\Member: + extensions: + - SilverStripe\ContentReview\Extensions\ContentReviewOwner + SilverStripe\SiteConfig\SiteConfig: extensions: - SilverStripe\ContentReview\Extensions\ContentReviewDefaultSettings diff --git a/client/dist/js/contentreview.js b/client/dist/js/contentreview.js index 2a5e95f..6f24145 100644 --- a/client/dist/js/contentreview.js +++ b/client/dist/js/contentreview.js @@ -1 +1 @@ -!function(n){function t(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return n[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var e={};t.m=n,t.c=e,t.i=function(n){return n},t.d=function(n,e,i){t.o(n,e)||Object.defineProperty(n,e,{configurable:!1,enumerable:!0,get:i})},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,"a",e),e},t.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},t.p="",t(t.s=3)}([function(n,t){window.jQuery.entwine("ss",function(n){n('.review-notes input[name="action_savereview"]').entwine({onclick:function(){this._super(),n(".contentreview-tab .nav-link").trigger("click")}})})},function(n,t){window.jQuery.entwine("ss",function(n){n(".cms-edit-form #Form_EditForm_ContentReviewType_Holder").entwine({onmatch:function(){var n=this;this.find(".optionset :input").bind("change",function(t){n.show_option(t.target.value)});var t=this.find("input[name=ContentReviewType]:checked").val();this.show_option(t),this._super()},onunmatch:function(){return this._super()},show_option:function(n){"Custom"===n?this._custom():"Inherit"===n?this._inherited():this._disabled()},_custom:function(){n(".review-settings").show(),n(".field.custom-setting").show()},_inherited:function(){n(".review-settings").show(),n(".field.custom-setting").hide()},_disabled:function(){n(".review-settings").hide()}})})},function(n,t){window.jQuery.entwine("ss",function(n){function t(t){var e="ContentReviewOwnerID"+t,i=n("div.subsiteSpecificOwnerID"),o=0;for(o=0;o'),e("body").append(t)),t.open(),!1}}),e(".content-review-modal .content-review-modal__nav-link").entwine({onclick:function(n){n.preventDefault();var t=e(n.target);window.location=t.attr("href")}}),e("#content-review__dialog-wrapper").entwine({onunmatch:function(){this._clearModal()},open:function(){this._renderModal(!0)},close:function(){this._renderModal(!1)},_renderModal:function(n){var t=this,o=function(){return t.close()},r=function(){return t._handleSubmitModal.apply(t,arguments)},s=e("form.cms-edit-form :input[name=ID]").val(),c=window.ss.store,u=c.getState().config.sections.find(function(e){return"SilverStripe\\CMS\\Controllers\\CMSPageEditController"===e.name}),f=u.form.ReviewContentForm.schemaUrl+"/"+s,_=i.a._t("ContentReview.CONTENT_DUE_FOR_REVIEW","Content due for review");d.a.render(a.a.createElement(l.Provider,{store:c},a.a.createElement(h,{title:_,show:n,handleSubmit:r,handleHide:o,schemaUrl:f,bodyClassName:"modal__dialog",className:"content-review-modal",responseClassBad:"modal__response modal__response--error",responseClassGood:"modal__response modal__response--good",identifier:"ContentReview.CONTENT_DUE_FOR_REVIEW"})),this[0])},_clearModal:function(){d.a.unmountComponentAtNode(this[0])},_handleSubmitModal:function(e,n,t){return t()}})})},function(e,n,t){"use strict";var o=t(0);t.n(o).a.entwine("ss",function(e){e(".cms-edit-form #Form_EditForm_ContentReviewType_Holder").entwine({onmatch:function(){var e=this;this.find(".optionset :input").bind("change",function(n){e.show_option(n.target.value)});var n=this.find("input[name=ContentReviewType]:checked").val();this.show_option(n),this._super()},onunmatch:function(){return this._super()},show_option:function(e){"Custom"===e?this._custom():"Inherit"===e?this._inherited():this._disabled()},_custom:function(){e(".review-settings").show(),e(".field.custom-setting").show()},_inherited:function(){e(".review-settings").show(),e(".field.custom-setting").hide()},_disabled:function(){e(".review-settings").hide()}})})},function(e,n,t){"use strict";var o=t(0);t.n(o).a.entwine("ss",function(e){function n(n){var t="ContentReviewOwnerID"+n,o=e("div.subsiteSpecificOwnerID"),i=0;for(i=0;i {\n $('.review-notes input[name=\"action_savereview\"]').entwine({\n /**\n * Close the review popup when the form is submitted\n */\n onclick() {\n this._super();\n $('.contentreview-tab .nav-link').trigger('click');\n }\n });\n});\n\n\n\n// WEBPACK FOOTER //\n// ./client/src/bundles/ContentReviewPopup.js","window.jQuery.entwine('ss', ($) => {\n /**\n * Class: .cms-edit-form #Form_EditForm_ContentReviewType_Holder\n *\n * Toggle display of group dropdown in \"access\" tab,\n * based on selection of radiobuttons.\n */\n $('.cms-edit-form #Form_EditForm_ContentReviewType_Holder').entwine({\n // Constructor: onmatch\n onmatch() {\n const self = this;\n this.find('.optionset :input').bind('change', function (e) {\n self.show_option(e.target.value);\n });\n\n // initial state\n const currentVal = this.find(`input[name=${this.attr('id')}]:checked`).val();\n this.show_option(currentVal);\n this._super();\n },\n\n onunmatch() {\n return this._super();\n },\n\n show_option(value) {\n if (value === 'Custom') {\n this._custom();\n } else if (value === 'Inherit') {\n this._inherited();\n } else {\n this._disabled();\n }\n },\n\n _custom() {\n $('.review-settings').show();\n $('.field.custom-setting').show();\n },\n\n _inherited() {\n $('.review-settings').show();\n $('.field.custom-setting').hide();\n },\n\n _disabled() {\n $('.review-settings').hide();\n },\n });\n});\n\n\n\n// WEBPACK FOOTER //\n// ./client/src/bundles/ContentReviewSettings.js","/**\n * @todo Re-validate this with Subsites\n */\nwindow.jQuery.entwine('ss', ($) => {\n // Hide all owner dropdowns except the one for the current subsite\n function showCorrectSubsiteIDDropdown(value) {\n const domid = `ContentReviewOwnerID${value}`;\n\n const ownerIDDropdowns = $('div.subsiteSpecificOwnerID');\n let i = 0;\n for (i = 0; i < ownerIDDropdowns.length; i++) {\n if (ownerIDDropdowns[i].id === domid) {\n $(ownerIDDropdowns[i]).show();\n } else {\n $(ownerIDDropdowns[i]).hide();\n }\n }\n }\n\n $('#Form_EditForm_SubsiteIDWithOwner').entwine({\n // Call method to show on report load\n onmatch() {\n showCorrectSubsiteIDDropdown(this.value);\n },\n\n // Call method to show on dropdown change\n change() {\n showCorrectSubsiteIDDropdown(this.value);\n },\n });\n});\n\n\n\n// WEBPACK FOOTER //\n// ./client/src/bundles/PagesDueForReview.js","import 'bundles/PagesDueForReview.js';\nimport 'bundles/ContentReviewPopup.js';\nimport 'bundles/ContentReviewSettings.js';\n\n\n\n// WEBPACK FOOTER //\n// ./client/src/bundles/bundle.js"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack:///webpack/bootstrap 2e84be8a35a52c0a303b","webpack:///external \"jQuery\"","webpack:///./client/src/bundles/ContentReviewForm.js","webpack:///./client/src/bundles/ContentReviewSettings.js","webpack:///./client/src/bundles/PagesDueForReview.js","webpack:///./client/src/bundles/bundle.js","webpack:///external \"FormBuilderModal\"","webpack:///external \"Injector\"","webpack:///external \"React\"","webpack:///external \"ReactDom\"","webpack:///external \"ReactRedux\"","webpack:///external \"i18n\""],"names":["InjectableFormBuilderModal","provideInjector","jQuery","entwine","$","onclick","e","preventDefault","dialog","length","append","open","$link","target","window","location","attr","onunmatch","_clearModal","_renderModal","close","show","handleHide","handleSubmit","_handleSubmitModal","id","val","store","ss","sectionConfigKey","sectionConfig","getState","config","sections","find","section","name","modalSchemaUrl","form","ReviewContentForm","schemaUrl","title","i18n","_t","ReactDOM","render","unmountComponentAtNode","data","action","submitFn","onmatch","self","bind","show_option","value","currentVal","_super","_custom","_inherited","_disabled","hide","showCorrectSubsiteIDDropdown","domid","ownerIDDropdowns","i","change"],"mappings":"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","file":"js/contentreview.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 4);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 2e84be8a35a52c0a303b","module.exports = jQuery;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"jQuery\"\n// module id = 0\n// module chunks = 0","import i18n from 'i18n';\nimport jQuery from 'jquery';\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Provider } from 'react-redux';\nimport { provideInjector } from 'lib/Injector';\nimport FormBuilderModal from 'components/FormBuilderModal/FormBuilderModal';\n\nconst InjectableFormBuilderModal = provideInjector(FormBuilderModal);\n\n/**\n * \"Content due for review\" modal popup. See AddToCampaignForm.js in\n * silverstripe/admin for reference.\n */\njQuery.entwine('ss', ($) => {\n\t/**\n * Kick off a \"content due for review\" dialog from the CMS actions.\n */\n $('.cms-content-actions .content-review__button').entwine({\n onclick(e) {\n e.preventDefault();\n\n let dialog = $('#content-review__dialog-wrapper');\n\n if (!dialog.length) {\n dialog = $('
');\n $('body').append(dialog);\n }\n\n dialog.open();\n\n return false;\n },\n });\n\n // This is required because the React version of e.preventDefault() doesn't work\n // this is to prevent PJAX request to occur when clicking a link the modal\n $('.content-review-modal .content-review-modal__nav-link').entwine({\n onclick: (e) => {\n e.preventDefault();\n const $link = $(e.target);\n window.location = $link.attr('href');\n },\n });\n\n\t/**\n * Uses React-Bootstrap in order to replicate the bootstrap styling and JavaScript behaviour.\n */\n $('#content-review__dialog-wrapper').entwine({\n onunmatch() {\n // solves errors given by ReactDOM \"no matched root found\" error.\n this._clearModal();\n },\n\n open() {\n this._renderModal(true);\n },\n\n close() {\n this._renderModal(false);\n },\n\n _renderModal(show) {\n const handleHide = () => this.close();\n const handleSubmit = (...args) => this._handleSubmitModal(...args);\n const id = $('form.cms-edit-form :input[name=ID]').val();\n const store = window.ss.store;\n const sectionConfigKey = 'SilverStripe\\\\CMS\\\\Controllers\\\\CMSPageEditController';\n const sectionConfig = store.getState().config.sections\n .find((section) => section.name === sectionConfigKey);\n const modalSchemaUrl = `${sectionConfig.form.ReviewContentForm.schemaUrl}/${id}`;\n const title = i18n._t(\n 'SilverStripe\\\\ContentReview\\\\Models\\\\ContentReviewLog.CONTENT_DUE_FOR_REVIEW',\n 'Content due for review'\n );\n\n ReactDOM.render(\n \n \n ,\n this[0]\n );\n },\n\n _clearModal() {\n ReactDOM.unmountComponentAtNode(this[0]);\n // this.empty();\n },\n\n _handleSubmitModal(data, action, submitFn) {\n return submitFn();\n },\n });\n});\n\n\n\n// WEBPACK FOOTER //\n// ./client/src/bundles/ContentReviewForm.js","import jQuery from 'jquery';\n\njQuery.entwine('ss', ($) => {\n /**\n * Class: .cms-edit-form #Form_EditForm_ContentReviewType_Holder\n *\n * Toggle display of group dropdown in \"access\" tab,\n * based on selection of radiobuttons.\n */\n $('.cms-edit-form #Form_EditForm_ContentReviewType_Holder').entwine({\n // Constructor: onmatch\n onmatch() {\n const self = this;\n this.find('.optionset :input').bind('change', (e) => {\n self.show_option(e.target.value);\n });\n\n // initial state\n const currentVal = this.find('input[name=ContentReviewType]:checked').val();\n this.show_option(currentVal);\n this._super();\n },\n\n onunmatch() {\n return this._super();\n },\n\n show_option(value) {\n if (value === 'Custom') {\n this._custom();\n } else if (value === 'Inherit') {\n this._inherited();\n } else {\n this._disabled();\n }\n },\n\n _custom() {\n $('.review-settings').show();\n $('.field.custom-setting').show();\n },\n\n _inherited() {\n $('.review-settings').show();\n $('.field.custom-setting').hide();\n },\n\n _disabled() {\n $('.review-settings').hide();\n },\n });\n});\n\n\n\n// WEBPACK FOOTER //\n// ./client/src/bundles/ContentReviewSettings.js","import jQuery from 'jquery';\n\n/**\n * @todo Re-validate this with Subsites\n */\njQuery.entwine('ss', ($) => {\n // Hide all owner dropdowns except the one for the current subsite\n function showCorrectSubsiteIDDropdown(value) {\n const domid = `ContentReviewOwnerID${value}`;\n\n const ownerIDDropdowns = $('div.subsiteSpecificOwnerID');\n let i = 0;\n for (i = 0; i < ownerIDDropdowns.length; i++) {\n if (ownerIDDropdowns[i].id === domid) {\n $(ownerIDDropdowns[i]).show();\n } else {\n $(ownerIDDropdowns[i]).hide();\n }\n }\n }\n\n $('#Form_EditForm_SubsiteIDWithOwner').entwine({\n // Call method to show on report load\n onmatch() {\n showCorrectSubsiteIDDropdown(this.value);\n },\n\n // Call method to show on dropdown change\n change() {\n showCorrectSubsiteIDDropdown(this.value);\n },\n });\n});\n\n\n\n// WEBPACK FOOTER //\n// ./client/src/bundles/PagesDueForReview.js","import 'bundles/ContentReviewForm.js';\nimport 'bundles/ContentReviewSettings.js';\nimport 'bundles/PagesDueForReview.js';\n\n\n\n// WEBPACK FOOTER //\n// ./client/src/bundles/bundle.js","module.exports = FormBuilderModal;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"FormBuilderModal\"\n// module id = 5\n// module chunks = 0","module.exports = Injector;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"Injector\"\n// module id = 6\n// module chunks = 0","module.exports = React;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"React\"\n// module id = 7\n// module chunks = 0","module.exports = ReactDom;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"ReactDom\"\n// module id = 8\n// module chunks = 0","module.exports = ReactRedux;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"ReactRedux\"\n// module id = 9\n// module chunks = 0","module.exports = i18n;\n\n\n//////////////////\n// WEBPACK FOOTER\n// external \"i18n\"\n// module id = 10\n// module chunks = 0"],"sourceRoot":""} \ No newline at end of file diff --git a/client/dist/styles/contentreview.css b/client/dist/styles/contentreview.css index 60cb873..3ff8498 100644 --- a/client/dist/styles/contentreview.css +++ b/client/dist/styles/contentreview.css @@ -1 +1 @@ -.cms .review-notes textarea{-webkit-box-sizing:border-box;box-sizing:border-box;height:60px;margin:4px 0;resize:none}.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel .review-notes label.left{margin:0;padding:0}.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel .review-notes .btn{width:auto}.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-nav .contentreview-tab a{width:20px;height:20px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAoCAYAAAD+MdrbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAPlJREFUeNpi/P//PwM1AeOAGPg1h3EBiOae8j+BkFoWIi22pIoLgS5rAFIFQMwPFfoIxBNAGOjaDyQZCDTsApDSx2HXRSB2wGYoEx6X6ePxmT7U5QxEGYhLMTFqMLwMdJ0AkHpPZBwYAr19gZALDUhIdgLEeplsQBcDBUjQr4A3UoARQlbGBkYMI828TN/SBkeaFMSVjwl6GaqxEUmoEZ9hA1fAjkADL8dC6hTdxYOtTgG6DGedAnQtaXUK0DCCdQo2Q5nwuGyQ1ilA15FUpwC9PVqn0LJOAUYIWRkbGDHDpU7BkSYFceVjgl6GakSpU/AZNjRKbIAAAwDhCJTckjfR5wAAAABJRU5ErkJggg==) 50% no-repeat;background-position:0 0;text-indent:-9999px;padding:0;margin:2px 4px 0 12px}.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-nav .contentreview-tab a:focus,.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-nav .contentreview-tab a:hover{background-position:0 -20px}.cms .ui-tabs-panel.contentreview-tab .form-group .form__fieldgroup{margin:0;min-width:250px}.cms .ui-tabs-panel.contentreview-tab .form-group:after{border-bottom:0} \ No newline at end of file +.content-review__button{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAoCAYAAAD+MdrbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAPlJREFUeNpi/P//PwM1AeOAGPg1h3EBiOae8j+BkFoWIi22pIoLgS5rAFIFQMwPFfoIxBNAGOjaDyQZCDTsApDSx2HXRSB2wGYoEx6X6ePxmT7U5QxEGYhLMTFqMLwMdJ0AkHpPZBwYAr19gZALDUhIdgLEeplsQBcDBUjQr4A3UoARQlbGBkYMI828TN/SBkeaFMSVjwl6GaqxEUmoEZ9hA1fAjkADL8dC6hTdxYOtTgG6DGedAnQtaXUK0DCCdQo2Q5nwuGyQ1ilA15FUpwC9PVqn0LJOAUYIWRkbGDHDpU7BkSYFceVjgl6GakSpU/AZNjRKbIAAAwDhCJTckjfR5wAAAABJRU5ErkJggg==) 50% no-repeat;background-position:0 0;display:inline-block;height:20px;margin:6px 4px 0 12px;padding:0;text-indent:-9999px;width:20px}.content-review__button:focus,.content-review__button:hover{background-position:0 -20px} \ No newline at end of file diff --git a/client/dist/styles/contentreview.css.map b/client/dist/styles/contentreview.css.map index 1c5e84e..6a08807 100644 --- a/client/dist/styles/contentreview.css.map +++ b/client/dist/styles/contentreview.css.map @@ -1 +1 @@ -{"version":3,"sources":["webpack:///./client/src/styles/contentreview.scss?f47b","webpack:///./bundle.scss?6663"],"names":[],"mappings":"AAAA,4BAKI,oDACA,YACA,aACA,YCMH,yFDAO,SACA,UCUP,mFDNO,WCUP,mFDFK,WACA,YACA,uDACA,wBACA,oBACA,UACA,sBCML,kLDFO,4BCOP,oEDGK,SACA,gBCCL,wDDGK,gBCCL","file":"styles/contentreview.css","sourcesContent":[".cms {\n /**\n * The textarea for entering review comments\n */\n .review-notes textarea {\n box-sizing: border-box;\n height: 60px;\n margin: 4px 0;\n resize: none;\n }\n\n .ss-ui-action-tabset.action-menus.ss-tabset {\n .ui-tabs-panel .review-notes {\n label.left {\n margin: 0;\n padding: 0;\n }\n\n .btn {\n width: auto;\n }\n }\n\n /**\n * The icon showing that reviews are required (shows next to actions dropdown)\n */\n .ui-tabs-nav .contentreview-tab a {\n width: 20px;\n height: 20px;\n background: url('images/icon-bell.png') center center no-repeat;\n background-position: 0 0;\n text-indent: -9999px;\n padding: 0;\n margin: 2px 4px 0 12px;\n\n &:hover,\n &:focus {\n background-position: 0 -20px;\n }\n }\n }\n\n /**\n * The box container for the popup\n */\n .ui-tabs-panel.contentreview-tab .form-group {\n .form__fieldgroup {\n margin: 0;\n min-width: 250px;\n }\n\n &:after {\n border-bottom: 0;\n }\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// ./client/src/styles/contentreview.scss",".cms {\n /**\n * The textarea for entering review comments\n */\n /**\n * The box container for the popup\n */\n}\n\n.cms .review-notes textarea {\n box-sizing: border-box;\n height: 60px;\n margin: 4px 0;\n resize: none;\n}\n\n.cms .ss-ui-action-tabset.action-menus.ss-tabset {\n /**\n * The icon showing that reviews are required (shows next to actions dropdown)\n */\n}\n\n.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel .review-notes label.left {\n margin: 0;\n padding: 0;\n}\n\n.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-panel .review-notes .btn {\n width: auto;\n}\n\n.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-nav .contentreview-tab a {\n width: 20px;\n height: 20px;\n background: url(\"../images/icon-bell.png\") center center no-repeat;\n background-position: 0 0;\n text-indent: -9999px;\n padding: 0;\n margin: 2px 4px 0 12px;\n}\n\n.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-nav .contentreview-tab a:hover,\n.cms .ss-ui-action-tabset.action-menus.ss-tabset .ui-tabs-nav .contentreview-tab a:focus {\n background-position: 0 -20px;\n}\n\n.cms .ui-tabs-panel.contentreview-tab .form-group .form__fieldgroup {\n margin: 0;\n min-width: 250px;\n}\n\n.cms .ui-tabs-panel.contentreview-tab .form-group:after {\n border-bottom: 0;\n}\n\n\n\n\n// WEBPACK FOOTER //\n// ./bundle.scss"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack:///./client/src/styles/ContentReviewForm.scss?a3bd","webpack:///./bundle.scss?6663"],"names":[],"mappings":"AAEA,wBACE,uDACA,wBACA,qBACA,YACA,sBACA,UACA,oBACA,WCDD,4DDKG,4BCAH","file":"styles/contentreview.css","sourcesContent":["// The bell button, shows up next to the major actions (save, publish, etc) when\n// viewing a page in the CMS\n.content-review__button {\n background: url(\"images/icon-bell.png\") center center no-repeat;\n background-position: 0 0;\n display: inline-block;\n height: 20px;\n margin: 6px 4px 0 12px;\n padding: 0;\n text-indent: -9999px;\n width: 20px;\n\n &:hover,\n &:focus {\n background-position: 0 -20px;\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// ./client/src/styles/ContentReviewForm.scss",".content-review__button {\n background: url(\"../images/icon-bell.png\") center center no-repeat;\n background-position: 0 0;\n display: inline-block;\n height: 20px;\n margin: 6px 4px 0 12px;\n padding: 0;\n text-indent: -9999px;\n width: 20px;\n}\n\n.content-review__button:hover,\n.content-review__button:focus {\n background-position: 0 -20px;\n}\n\n\n\n\n// WEBPACK FOOTER //\n// ./bundle.scss"],"sourceRoot":""} \ No newline at end of file diff --git a/client/lang/src/en.json b/client/lang/src/en.json new file mode 100644 index 0000000..c4a20b4 --- /dev/null +++ b/client/lang/src/en.json @@ -0,0 +1,3 @@ +{ + "ContentReview.CONTENT_DUE_FOR_REVIEW": "Content due for review" +} diff --git a/client/src/bundles/ContentReviewForm.js b/client/src/bundles/ContentReviewForm.js new file mode 100644 index 0000000..9ad4895 --- /dev/null +++ b/client/src/bundles/ContentReviewForm.js @@ -0,0 +1,101 @@ +import i18n from 'i18n'; +import jQuery from 'jquery'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import { provideInjector } from 'lib/Injector'; +import FormBuilderModal from 'components/FormBuilderModal/FormBuilderModal'; + +const InjectableFormBuilderModal = provideInjector(FormBuilderModal); + +/** + * "Content due for review" modal popup. See AddToCampaignForm.js in + * silverstripe/admin for reference. + */ +jQuery.entwine('ss', ($) => { + /** + * Kick off a "content due for review" dialog from the CMS actions. + */ + $('.cms-content-actions .content-review__button').entwine({ + onclick(e) { + e.preventDefault(); + + let dialog = $('#content-review__dialog-wrapper'); + + if (!dialog.length) { + dialog = $('
'); + $('body').append(dialog); + } + + dialog.open(); + + return false; + }, + }); + + // This is required because the React version of e.preventDefault() doesn't work + // this is to prevent PJAX request to occur when clicking a link the modal + $('.content-review-modal .content-review-modal__nav-link').entwine({ + onclick: (e) => { + e.preventDefault(); + const $link = $(e.target); + window.location = $link.attr('href'); + }, + }); + + /** + * Uses React-Bootstrap in order to replicate the bootstrap styling and JavaScript behaviour. + */ + $('#content-review__dialog-wrapper').entwine({ + onunmatch() { + // solves errors given by ReactDOM "no matched root found" error. + this._clearModal(); + }, + + open() { + this._renderModal(true); + }, + + close() { + this._renderModal(false); + }, + + _renderModal(show) { + const handleHide = () => this.close(); + const handleSubmit = (...args) => this._handleSubmitModal(...args); + const id = $('form.cms-edit-form :input[name=ID]').val(); + const store = window.ss.store; + const sectionConfigKey = 'SilverStripe\\CMS\\Controllers\\CMSPageEditController'; + const sectionConfig = store.getState().config.sections + .find((section) => section.name === sectionConfigKey); + const modalSchemaUrl = `${sectionConfig.form.ReviewContentForm.schemaUrl}/${id}`; + const title = i18n._t('ContentReview.CONTENT_DUE_FOR_REVIEW', 'Content due for review'); + + ReactDOM.render( + + + , + this[0] + ); + }, + + _clearModal() { + ReactDOM.unmountComponentAtNode(this[0]); + }, + + _handleSubmitModal(data, action, submitFn) { + return submitFn(); + }, + }); +}); diff --git a/client/src/bundles/ContentReviewPopup.js b/client/src/bundles/ContentReviewPopup.js deleted file mode 100644 index 4e595f8..0000000 --- a/client/src/bundles/ContentReviewPopup.js +++ /dev/null @@ -1,11 +0,0 @@ -window.jQuery.entwine('ss', ($) => { - $('.review-notes input[name="action_savereview"]').entwine({ - /** - * Close the review popup when the form is submitted - */ - onclick() { - this._super(); - $('.contentreview-tab .nav-link').trigger('click'); - }, - }); -}); diff --git a/client/src/bundles/ContentReviewSettings.js b/client/src/bundles/ContentReviewSettings.js index dbab394..11300d6 100644 --- a/client/src/bundles/ContentReviewSettings.js +++ b/client/src/bundles/ContentReviewSettings.js @@ -1,4 +1,6 @@ -window.jQuery.entwine('ss', ($) => { +import jQuery from 'jquery'; + +jQuery.entwine('ss', ($) => { /** * Class: .cms-edit-form #Form_EditForm_ContentReviewType_Holder * diff --git a/client/src/bundles/PagesDueForReview.js b/client/src/bundles/PagesDueForReview.js index 4fa98fb..f8cec4e 100644 --- a/client/src/bundles/PagesDueForReview.js +++ b/client/src/bundles/PagesDueForReview.js @@ -1,7 +1,9 @@ +import jQuery from 'jquery'; + /** * @todo Re-validate this with Subsites */ -window.jQuery.entwine('ss', ($) => { +jQuery.entwine('ss', ($) => { // Hide all owner dropdowns except the one for the current subsite function showCorrectSubsiteIDDropdown(value) { const domid = `ContentReviewOwnerID${value}`; diff --git a/client/src/bundles/bundle.js b/client/src/bundles/bundle.js index 5c51988..cf5a1d3 100644 --- a/client/src/bundles/bundle.js +++ b/client/src/bundles/bundle.js @@ -1,3 +1,3 @@ -import 'bundles/PagesDueForReview.js'; -import 'bundles/ContentReviewPopup.js'; +import 'bundles/ContentReviewForm.js'; import 'bundles/ContentReviewSettings.js'; +import 'bundles/PagesDueForReview.js'; diff --git a/client/src/styles/ContentReviewForm.scss b/client/src/styles/ContentReviewForm.scss new file mode 100644 index 0000000..4eaec1b --- /dev/null +++ b/client/src/styles/ContentReviewForm.scss @@ -0,0 +1,17 @@ +// The bell button, shows up next to the major actions (save, publish, etc) when +// viewing a page in the CMS +.content-review__button { + background: url("images/icon-bell.png") center center no-repeat; + background-position: 0 0; + display: inline-block; + height: 20px; + margin: 6px 4px 0 12px; + padding: 0; + text-indent: -9999px; + width: 20px; + + &:hover, + &:focus { + background-position: 0 -20px; + } +} diff --git a/client/src/styles/bundle.scss b/client/src/styles/bundle.scss index 556ba47..ae52d95 100644 --- a/client/src/styles/bundle.scss +++ b/client/src/styles/bundle.scss @@ -1,4 +1,4 @@ // See includePaths in webpack.config.js @import "variables"; -@import "contentreview"; +@import "ContentReviewForm"; diff --git a/client/src/styles/contentreview.scss b/client/src/styles/contentreview.scss deleted file mode 100644 index abdfb18..0000000 --- a/client/src/styles/contentreview.scss +++ /dev/null @@ -1,50 +0,0 @@ -.cms { - // The textarea for entering review comments - .review-notes textarea { - box-sizing: border-box; - height: 60px; - margin: 4px 0; - resize: none; - } - - .ss-ui-action-tabset.action-menus.ss-tabset { - .ui-tabs-panel .review-notes { - label.left { - margin: 0; - padding: 0; - } - - .btn { - width: auto; - } - } - - // The icon showing that reviews are required (shows next to actions dropdown) - .ui-tabs-nav .contentreview-tab a { - width: 20px; - height: 20px; - background: url("images/icon-bell.png") center center no-repeat; - background-position: 0 0; - text-indent: -9999px; - padding: 0; - margin: 2px 4px 0 12px; - - &:hover, - &:focus { - background-position: 0 -20px; - } - } - } - - // The box container for the popup - .ui-tabs-panel.contentreview-tab .form-group { - .form__fieldgroup { - margin: 0; - min-width: 250px; - } - - &:after { - border-bottom: 0; - } - } -} diff --git a/lang/en.yml b/lang/en.yml index 8bf285c..447ba37 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -46,3 +46,11 @@ en: TITLE: 'Pages without a scheduled review.' Review: EMAILFROM_RIGHTTITLE: 'e.g: do-not-reply@site.com' + + SilverStripe\ContentReview\Forms\ReviewContentHandler: + ContentDueForReview: Content due for review + ErrorReviewPermissionDenied: It seems you don't have the necessary permissions to submit a content review + MarkAsReviewedAction: Mark as reviewed + NoComments: (no comments) + ObjectDoesntExist: That object doesn't exist + Success: Review successfully added diff --git a/package.json b/package.json index 0c88574..9bde7b7 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,13 @@ "url": "https://github.com/silverstripe/silverstripe-contentreview/issues" }, "homepage": "https://github.com/silverstripe/silverstripe-contentreview#readme", + "dependencies": { + "jquery": "^3.2.1", + "react": "15.3.1", + "react-dom": "15.3.1", + "react-redux": "^4.4.1", + "redux": "https://registry.npmjs.org/redux/-/redux-3.0.5.tgz" + }, "devDependencies": { "@silverstripe/webpack-config": "^0.2.5", "babel-core": "^6.24.1", @@ -31,9 +38,6 @@ "eslint": "^2.7.0", "eslint-config-airbnb": "^6.2.0" }, - "dependencies": { - "jquery": "^3.2.1" - }, "engines": { "node": "^6.x" } diff --git a/src/Extensions/ContentReviewCMSExtension.php b/src/Extensions/ContentReviewCMSExtension.php index 9b2095c..8210bb3 100644 --- a/src/Extensions/ContentReviewCMSExtension.php +++ b/src/Extensions/ContentReviewCMSExtension.php @@ -2,11 +2,15 @@ namespace SilverStripe\ContentReview\Extensions; +use SilverStripe\Admin\LeftAndMain; use SilverStripe\Admin\LeftAndMainExtension; use SilverStripe\CMS\Model\SiteTree; -use SilverStripe\Control\HTTPResponse_Exception; +use SilverStripe\ContentReview\Forms\ReviewContentHandler; +use SilverStripe\Control\HTTPRequest; +use SilverStripe\Control\HTTPResponse; +use SilverStripe\Core\Convert; use SilverStripe\Forms\Form; -use SilverStripe\Security\Member; +use SilverStripe\ORM\ValidationResult; use SilverStripe\Security\Security; /** @@ -15,64 +19,127 @@ use SilverStripe\Security\Security; */ class ContentReviewCMSExtension extends LeftAndMainExtension { - /** - * @var array - */ - private static $allowed_actions = array( - "savereview" - ); + private static $allowed_actions = [ + 'ReviewContentForm', + 'submitReview', + ]; /** - * Save the review notes and redirect back to the page edit form. + * URL handler for the "content due for review" form + * + * @param HTTPRequest $request + * @return Form|null + */ + public function ReviewContentForm(HTTPRequest $request) + { + // Get ID either from posted back value, or url parameter + $id = $request->param('ID') ?: $request->postVar('ID'); + return $this->getReviewContentForm($id); + } + + /** + * Return a handler for "content due for review" forms, according to the given object ID + * + * @param int $id + * @return Form|null + */ + public function getReviewContentForm($id) + { + $record = SiteTree::get()->byID($id); + + if (!$record) { + $this->owner->httpError(404, _t(__CLASS__ . '.ErrorNotFound', 'That object couldn\'t be found')); + return null; + } + + $user = Security::getCurrentUser(); + if (!$record->canEdit() || ($record->hasMethod('canBeReviewedBy') && !$record->canBeReviewedBy($user))) { + $this->owner->httpError(403, _t( + __CLASS__.'.ErrorItemPermissionDenied', + 'It seems you don\'t have the necessary permissions to review this content' + )); + return null; + } + + $handler = ReviewContentHandler::create($this->owner, $record); + $form = $handler->Form($record); + + $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id) { + $schemaId = $this->owner->join_links($this->owner->Link('schema/ReviewContentForm'), $id); + return $this->owner->getSchemaResponse($schemaId, $form, $errors); + }); + + return $form; + } + + /** + * Action handler for processing the submitted content review * * @param array $data - * @param Form $form - * - * @return string - * - * @throws HTTPResponse_Exception + * @param Form $form + * @return DBHTMLText|HTTPResponse */ - public function savereview($data, Form $form) + public function submitReview($data = '', $form = '') { - $page = $this->findRecord($data); - if (!$page->canEdit()) { - return Security::permissionFailure($this->owner); + $id = $data['ID']; + $record = SiteTree::get()->byID($id); + + $handler = ReviewContentHandler::create($this->owner, $record); + $form = $handler->Form($record); + $results = $handler->submitReview($record, $data); + if (is_null($results)) { + return null; } - $notes = (!empty($data["ReviewNotes"]) - ? $data["ReviewNotes"] - : _t("ContentReview.NOCOMMENTS", "(no comments)")); - $page->addReviewNote(Member::currentUser(), $notes); - $page->advanceReviewDate(); + if ($this->getSchemaRequested()) { + // Send extra "message" data with schema response + $extraData = ['message' => $results]; + $schemaId = $this->owner->join_links($this->owner->Link('schema/ReviewContentForm'), $id); + return $this->getSchemaResponse($schemaId, $form, null, $extraData); + } - $this->owner->getResponse() - ->addHeader("X-Status", _t("ContentReview.REVIEWSUCCESSFUL", "Content reviewed successfully")); - return $this->owner->redirectBack(); + return $results; } /** - * Find the page this form is updating + * Check if the current request has a X-Formschema-Request header set. + * Used by conditional logic that responds to validation results * - * @param array $data Form data - * @return SiteTree Record - * @throws SS_HTTPResponse_Exception + * @todo Remove duplication. See https://github.com/silverstripe/silverstripe-admin/issues/240 + * + * @return bool */ - protected function findRecord($data) + protected function getSchemaRequested() { - if (empty($data["ID"])) { - throw new HTTPResponse_Exception("No record ID", 404); + $parts = $this->owner->getRequest()->getHeader(LeftAndMain::SCHEMA_HEADER); + return !empty($parts); + } + + /** + * Generate schema for the given form based on the X-Formschema-Request header value + * + * @todo Remove duplication. See https://github.com/silverstripe/silverstripe-admin/issues/240 + * + * @param string $schemaID ID for this schema. Required. + * @param Form $form Required for 'state' or 'schema' response + * @param ValidationResult $errors Required for 'error' response + * @param array $extraData Any extra data to be merged with the schema response + * @return HTTPResponse + */ + protected function getSchemaResponse($schemaID, $form = null, ValidationResult $errors = null, $extraData = []) + { + $parts = $this->owner->getRequest()->getHeader(LeftAndMain::SCHEMA_HEADER); + $data = $this->owner + ->getFormSchema() + ->getMultipartSchema($parts, $schemaID, $form, $errors); + + if ($extraData) { + $data = array_merge($data, $extraData); } - $page = null; - $id = $data["ID"]; - if (is_numeric($id)) { - $page = SiteTree::get()->byID($id); - } + $response = HTTPResponse::create(Convert::raw2json($data)); + $response->addHeader('Content-Type', 'application/json'); - if (!$page || !$page->ID) { - throw new HTTPResponse_Exception("Bad record ID #{$id}", 404); - } - - return $page; + return $response; } } diff --git a/src/Extensions/ContentReviewLeftAndMainExtension.php b/src/Extensions/ContentReviewLeftAndMainExtension.php new file mode 100644 index 0000000..03ce9d1 --- /dev/null +++ b/src/Extensions/ContentReviewLeftAndMainExtension.php @@ -0,0 +1,20 @@ + $this->owner->Link('schema/ReviewContentForm') + ]; + } +} diff --git a/src/Extensions/SiteTreeContentReview.php b/src/Extensions/SiteTreeContentReview.php index 337a1c9..0ebd77d 100644 --- a/src/Extensions/SiteTreeContentReview.php +++ b/src/Extensions/SiteTreeContentReview.php @@ -161,28 +161,15 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider */ public function updateCMSActions(FieldList $actions) { - if ($this->canBeReviewedBy(Security::getCurrentUser())) { - Requirements::css('silverstripe/contentreview:client/dist/styles/contentreview.css'); - Requirements::javascript('silverstripe/contentreview:client/dist/js/contentreview.js'); - - $reviewTitle = LiteralField::create( - "ReviewContentNotesLabel", - "" - ); - - $ReviewNotes = LiteralField::create("ReviewNotes", ""); - - $quickReviewAction = FormAction::create("savereview", _t("ContentReview.MARKREVIEWED", "Mark as reviewed")) - ->addExtraClass('btn btn-primary'); - - $allFields = CompositeField::create($reviewTitle, $ReviewNotes, $quickReviewAction) - ->addExtraClass('review-notes field'); - - $reviewTab = Tab::create('ReviewContent', $allFields); - $reviewTab->addExtraClass('contentreview-tab'); - - $actions->fieldByName('ActionMenus')->insertBefore($reviewTab, 'MoreOptions'); + if (!$this->canBeReviewedBy(Security::getCurrentUser())) { + return; } + + Requirements::css('silverstripe/contentreview:client/dist/styles/contentreview.css'); + Requirements::javascript('silverstripe/contentreview:client/dist/js/contentreview.js'); + + $reviewTab = LiteralField::create('ContentReviewButton', $this->owner->renderWith(__CLASS__ . '_button')); + $actions->insertAfter('MajorActions', $reviewTab); } /** diff --git a/src/Forms/ReviewContentHandler.php b/src/Forms/ReviewContentHandler.php new file mode 100644 index 0000000..bb09164 --- /dev/null +++ b/src/Forms/ReviewContentHandler.php @@ -0,0 +1,139 @@ +controller = $controller; + if ($data instanceof DataObject) { + $data = $data->toMap(); + } + $this->data = $data; + $this->name = $name; + } + + /** + * Bootstrap the form fields for the content review modal + * + * @param DataObject $object + * @return Form + */ + public function Form($object) + { + $placeholder = 'Add comments (optional)'; + + $fields = FieldList::create([ + HiddenField::create('ID', null, $object->ID), + HiddenField::create('ClassName', null, $object->baseClass()), + TextareaField::create('Review', '') + ->setAttribute('placeholder', $placeholder) + ->setSchemaData(['attributes' => ['placeholder' => $placeholder]]) + ]); + + $actions = FieldList::create([ + ReviewContentHandlerFormAction::create() + ->setTitle(_t(__CLASS__ . '.MarkAsReviewedAction', 'Mark as reviewed')) + ->addExtraClass('review-content__action') + ]); + + $form = Form::create($this->controller, $this->name, $fields, $actions); + + $form->setHTMLID('Form_EditForm_ReviewContent'); + $form->addExtraClass('form--no-dividers review-content__form'); + + return $form; + } + + /** + * Validate, and save the submitted form's review + * + * @param DataObject $record + * @param array $data + * @return HTTPResponse|string + */ + public function submitReview($record, $data) + { + if (!$record || !$record->exists()) { + throw new ValidationException(_t(__CLASS__ . '.ObjectDoesntExist', 'That object doesn\'t exist')); + } + + if (!$record->canEdit() + || !$record->hasMethod('canBeReviewedBy') + || !$record->canBeReviewedBy(Security::getCurrentUser()) + ) { + throw new ValidationException(_t( + __CLASS__ . '.ErrorReviewPermissionDenied', + 'It seems you don\'t have the necessary permissions to submit a content review' + )); + } + + $this->saveRecord($record, $data); + + $request = $this->controller->getRequest(); + $message = _t(__CLASS__ . '.Success', 'Review successfully added'); + + if ($request->getHeader('X-Formschema-Request')) { + return $message; + } elseif (Director::is_ajax()) { + $response = HTTPResponse::create($message, 200); + $response->addHeader('Content-Type', 'text/html; charset=utf-8'); + return $response; + } else { + return $this->controller->redirectBack(); + } + } + + /** + * Save the review provided in $data to the $record + * + * @param DataObject $record + * @param array $data + */ + protected function saveRecord($record, $data) + { + $notes = (!empty($data['Review']) ? $data['Review'] : _t(__CLASS__ . '.NoComments', '(no comments)')); + $record->addReviewNote(Security::getCurrentUser(), $notes); + $record->advanceReviewDate(); + } +} diff --git a/src/Forms/ReviewContentHandlerFormAction.php b/src/Forms/ReviewContentHandlerFormAction.php new file mode 100644 index 0000000..321abfd --- /dev/null +++ b/src/Forms/ReviewContentHandlerFormAction.php @@ -0,0 +1,19 @@ +setUseButtonTag(false) + ->addExtraClass('review-content-action btn btn-primary'); + } +} diff --git a/templates/SilverStripe/ContentReview/Extensions/SiteTreeContentReview_button.ss b/templates/SilverStripe/ContentReview/Extensions/SiteTreeContentReview_button.ss new file mode 100644 index 0000000..ba5d93b --- /dev/null +++ b/templates/SilverStripe/ContentReview/Extensions/SiteTreeContentReview_button.ss @@ -0,0 +1,3 @@ + diff --git a/webpack.config.js b/webpack.config.js index f7b3e4e..fa7090e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,4 @@ const Path = require('path'); -const webpack = require('webpack'); // Import the core config const webpackConfig = require('@silverstripe/webpack-config'); const { @@ -14,11 +13,11 @@ const { const ENV = process.env.NODE_ENV; const PATHS = { MODULES: 'node_modules', + THIRDPARTY: 'thirdparty', FILES_PATH: '../', ROOT: Path.resolve(), SRC: Path.resolve('client/src'), DIST: Path.resolve('client/dist'), - THIRDPARTY: Path.resolve('thirdparty'), }; const config = [ @@ -33,6 +32,7 @@ const config = [ }, devtool: (ENV !== 'production') ? 'source-map' : '', resolve: resolveJS(ENV, PATHS), + externals: externalJS(ENV, PATHS), module: moduleJS(ENV, PATHS), plugins: pluginJS(ENV, PATHS), }, diff --git a/yarn.lock b/yarn.lock index 9d246ed..b7d1001 100644 --- a/yarn.lock +++ b/yarn.lock @@ -197,6 +197,10 @@ arrify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + asn1.js@^4.0.0: version "4.9.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" @@ -1171,6 +1175,10 @@ convert-source-map@^1.1.1, convert-source-map@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + core-js@^2.4.0, core-js@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" @@ -1218,6 +1226,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +create-react-class@^15.5.1: + version "15.6.0" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.0.tgz#ab448497c26566e1e29413e883207d57cfe7bed4" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + object-assign "^4.1.1" + cross-spawn@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" @@ -1463,6 +1479,12 @@ emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + enhanced-resolve@^3.3.0: version "3.4.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" @@ -1708,6 +1730,18 @@ fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" +fbjs@^0.8.4, fbjs@^0.8.9: + version "0.8.15" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.15.tgz#4f0695fdfcc16c37c0b07facec8cb4c4091685b9" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + figures@^1.3.5: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -2046,6 +2080,10 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoist-non-react-statics@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -2073,6 +2111,10 @@ https-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" +iconv-lite@~0.4.13: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -2161,7 +2203,7 @@ interpret@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" -invariant@^2.2.2: +invariant@^2.0.0, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -2304,6 +2346,10 @@ is-resolvable@^1.0.0: dependencies: tryit "^1.0.1" +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + is-svg@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" @@ -2336,6 +2382,13 @@ isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -2621,7 +2674,7 @@ lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -lodash@4.17.4, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.3.0, lodash@~4.17.4: +lodash@4.17.4, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.4: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -2629,7 +2682,7 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -2807,6 +2860,13 @@ nan@^2.3.0, nan@^2.3.2: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-gyp@^3.3.1: version "3.6.2" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" @@ -2952,7 +3012,7 @@ oauth-sign@~0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -3440,6 +3500,19 @@ progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + dependencies: + asap "~2.0.3" + +prop-types@^15.5.4: + version "15.5.10" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" @@ -3515,6 +3588,29 @@ rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-dom@15.3.1: + version "15.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.3.1.tgz#6d42cd2b64c8c5e0b693f3ffaec301e6e627e24e" + +react-redux@^4.4.1: + version "4.4.8" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-4.4.8.tgz#e7bc1dd100e8b64e96ac8212db113239b9e2e08f" + dependencies: + create-react-class "^15.5.1" + hoist-non-react-statics "^1.0.3" + invariant "^2.0.0" + lodash "^4.2.0" + loose-envify "^1.1.0" + prop-types "^15.5.4" + +react@15.3.1: + version "15.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-15.3.1.tgz#f78501ed8c2ec6e6e31c3223652e97f1369d2bd6" + dependencies: + fbjs "^0.8.4" + loose-envify "^1.1.0" + object-assign "^4.1.0" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -3580,6 +3676,10 @@ reduce-function-call@^1.0.1: dependencies: balanced-match "^0.4.2" +"redux@https://registry.npmjs.org/redux/-/redux-3.0.5.tgz": + version "3.0.5" + resolved "https://registry.npmjs.org/redux/-/redux-3.0.5.tgz#f3f23f780b98c8dd7f84b9187ab5f86fe90199b8" + regenerate@^1.2.1: version "1.3.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" @@ -3857,7 +3957,7 @@ set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" -setimmediate@^1.0.4: +setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -4191,6 +4291,10 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" +ua-parser-js@^0.7.9: + version "0.7.14" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca" + uglify-js@^2.8.27: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" @@ -4338,6 +4442,10 @@ webpack@^2.6.1: webpack-sources "^1.0.1" yargs "^6.0.0" +whatwg-fetch@>=0.10.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + whet.extend@~0.9.9: version "0.9.9" resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" From 31bcd0d43946d88c94bed7040841b80fb3603d76 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Mon, 11 Sep 2017 17:53:59 +1200 Subject: [PATCH 2/3] NEW Add behat tests to cover content review configuration and review modal * Update existing test for review button field name, remove obsolete save review test * Move test classes to tests/php to differentiate from behat * Add behat build to Travis configuration --- .scrutinizer.yml | 2 +- .travis.yml | 20 +++- behat.yml | 27 ++++++ composer.json | 7 +- lang/en.yml | 3 +- phpunit.xml.dist | 2 +- src/Extensions/ContentReviewCMSExtension.php | 66 ++++++++----- src/Forms/ReviewContentHandler.php | 69 ++++++-------- src/Forms/ReviewContentHandlerFormAction.php | 19 ---- tests/behat/_manifest_exclude | 0 tests/behat/features/set-up-reviews.feature | 41 ++++++++ tests/{ => php}/ContentReviewBaseTest.php | 0 ...ContentReviewCMSPageEditControllerTest.php | 28 ------ .../ContentReviewNotificationTest.php | 0 tests/{ => php}/ContentReviewReportTest.php | 0 tests/{ => php}/ContentReviewSettingsTest.php | 0 tests/{ => php}/ContentReviewSettingsTest.yml | 0 tests/{ => php}/ContentReviewTest.yml | 0 .../ContentReviewCMSExtensionTest.php | 93 +++++++++++++++++++ tests/php/Forms/ReviewContentHandlerTest.php | 84 +++++++++++++++++ tests/{ => php}/SiteTreeContentReviewTest.php | 9 +- 21 files changed, 350 insertions(+), 120 deletions(-) create mode 100644 behat.yml delete mode 100644 src/Forms/ReviewContentHandlerFormAction.php create mode 100644 tests/behat/_manifest_exclude create mode 100644 tests/behat/features/set-up-reviews.feature rename tests/{ => php}/ContentReviewBaseTest.php (100%) rename tests/{ => php}/ContentReviewCMSPageEditControllerTest.php (78%) rename tests/{ => php}/ContentReviewNotificationTest.php (100%) rename tests/{ => php}/ContentReviewReportTest.php (100%) rename tests/{ => php}/ContentReviewSettingsTest.php (100%) rename tests/{ => php}/ContentReviewSettingsTest.yml (100%) rename tests/{ => php}/ContentReviewTest.yml (100%) create mode 100644 tests/php/Extensions/ContentReviewCMSExtensionTest.php create mode 100644 tests/php/Forms/ReviewContentHandlerTest.php rename tests/{ => php}/SiteTreeContentReviewTest.php (96%) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 61b0c9f..de09355 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -6,4 +6,4 @@ checks: duplication: true filter: - paths: [code/*, tests/*] + paths: [src/*, tests/*] diff --git a/.travis.yml b/.travis.yml index 0a95617..839210c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,15 @@ language: php -dist: trusty +dist: precise + +addons: + firefox: "31.0" env: global: - COMPOSER_ROOT_VERSION=4.0.x-dev + - DISPLAY=":99" + - XVFBARGS=":99 -ac -screen 0 1024x768x16" matrix: include: @@ -14,6 +19,8 @@ matrix: env: DB=PGSQL PHPUNIT_TEST=1 - php: 7.1 env: DB=MYSQL PHPUNIT_COVERAGE_TEST=1 + - php: 7.1 + env: DB=MYSQL BEHAT_TEST=1 before_script: # Init PHP @@ -26,10 +33,21 @@ before_script: - if [[ $DB == PGSQL ]]; then composer require --prefer-dist --no-update silverstripe/postgresql:2.0.x-dev; fi - composer update + # Start behat services + - if [[ $BEHAT_TEST ]]; then echo 'SS_BASE_URL=http://localhost:8080/' >> .env; fi + - if [[ $BEHAT_TEST ]]; then mkdir artifacts; fi + - if [[ $BEHAT_TEST ]]; then sh -e /etc/init.d/xvfb start; sleep 3; fi + - if [[ $BEHAT_TEST ]]; then (vendor/bin/selenium-server-standalone > artifacts/selenium.log 2>&1 &); fi + - if [[ $BEHAT_TEST ]]; then (vendor/bin/serve --bootstrap-file cms/tests/behat/serve-bootstrap.php &> artifacts/serve.log &); fi + script: - if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit; fi - if [[ $PHPUNIT_COVERAGE_TEST ]]; then phpdbg -qrr vendor/bin/phpunit --coverage-clover=coverage.xml; fi - if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs --standard=framework/phpcs.xml.dist src/ tests/; fi + - if [[ $BEHAT_TEST ]]; then vendor/bin/behat @contentreview; fi after_success: - if [[ $PHPUNIT_COVERAGE_TEST ]]; then bash <(curl -s https://codecov.io/bash) -f coverage.xml; fi + +after_failure: + - php ./framework/tests/behat/travis-upload-artifacts.php --if-env BEHAT_TEST,ARTIFACTS_BUCKET,ARTIFACTS_KEY,ARTIFACTS_SECRET --target-path $TRAVIS_REPO_SLUG/$TRAVIS_BUILD_ID/$TRAVIS_JOB_ID --artifacts-base-url https://s3.amazonaws.com/$ARTIFACTS_BUCKET/ --artifacts-path ./artifacts/ diff --git a/behat.yml b/behat.yml new file mode 100644 index 0000000..f4dd94e --- /dev/null +++ b/behat.yml @@ -0,0 +1,27 @@ +default: + suites: + contentreview: + paths: + - %paths.modules.contentreview%/tests/behat/features + contexts: + - SilverStripe\Framework\Tests\Behaviour\FeatureContext + - SilverStripe\Framework\Tests\Behaviour\CmsFormsContext + - SilverStripe\Framework\Tests\Behaviour\CmsUiContext + - SilverStripe\BehatExtension\Context\BasicContext + - SilverStripe\BehatExtension\Context\EmailContext + - SilverStripe\CMS\Tests\Behaviour\LoginContext + - SilverStripe\CMS\Tests\Behaviour\ThemeContext + - SilverStripe\CMS\Tests\Behaviour\FixtureContext: + # Note: double indent for args is intentional + - %paths.modules.contentreview%/tests/behat/features/files/ + + extensions: + SilverStripe\BehatExtension\MinkExtension: + default_session: selenium2 + javascript_session: selenium2 + selenium2: + browser: firefox + + SilverStripe\BehatExtension\Extension: + screenshot_path: %paths.base%/artifacts/screenshots + bootstrap_file: "cms/tests/behat/serve-bootstrap.php" diff --git a/composer.json b/composer.json index 0966918..9a226eb 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,10 @@ }, "require-dev": { "phpunit/phpunit": "^5.7", - "squizlabs/php_codesniffer": "^3.0" + "squizlabs/php_codesniffer": "^3.0", + "silverstripe/behat-extension": "^3@dev", + "silverstripe/serve": "dev-master", + "se/selenium-server-standalone": "2.41.0" }, "suggest": { "symbiote/silverstripe-queuedjobs": "Automatically schedules content review emails to be sent, only requiring one crontask to be created" @@ -40,7 +43,7 @@ "autoload": { "psr-4": { "SilverStripe\\ContentReview\\": "src/", - "SilverStripe\\ContentReview\\Tests\\": "tests/" + "SilverStripe\\ContentReview\\Tests\\": "tests/php/" } }, "minimum-stability": "dev", diff --git a/lang/en.yml b/lang/en.yml index 447ba37..9feec6d 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -49,8 +49,7 @@ en: SilverStripe\ContentReview\Forms\ReviewContentHandler: ContentDueForReview: Content due for review - ErrorReviewPermissionDenied: It seems you don't have the necessary permissions to submit a content review MarkAsReviewedAction: Mark as reviewed NoComments: (no comments) - ObjectDoesntExist: That object doesn't exist + Placeholder: Add comments (optional) Success: Review successfully added diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 130037b..110f952 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ - tests/ + tests/php/ diff --git a/src/Extensions/ContentReviewCMSExtension.php b/src/Extensions/ContentReviewCMSExtension.php index 8210bb3..b8cf389 100644 --- a/src/Extensions/ContentReviewCMSExtension.php +++ b/src/Extensions/ContentReviewCMSExtension.php @@ -8,6 +8,7 @@ use SilverStripe\CMS\Model\SiteTree; use SilverStripe\ContentReview\Forms\ReviewContentHandler; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; +use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Core\Convert; use SilverStripe\Forms\Form; use SilverStripe\ORM\ValidationResult; @@ -21,7 +22,7 @@ class ContentReviewCMSExtension extends LeftAndMainExtension { private static $allowed_actions = [ 'ReviewContentForm', - 'submitReview', + 'savereview', ]; /** @@ -45,15 +46,9 @@ class ContentReviewCMSExtension extends LeftAndMainExtension */ public function getReviewContentForm($id) { - $record = SiteTree::get()->byID($id); - - if (!$record) { - $this->owner->httpError(404, _t(__CLASS__ . '.ErrorNotFound', 'That object couldn\'t be found')); - return null; - } - + $page = $this->findRecord(['ID' => $id]); $user = Security::getCurrentUser(); - if (!$record->canEdit() || ($record->hasMethod('canBeReviewedBy') && !$record->canBeReviewedBy($user))) { + if (!$page->canEdit() || ($page->hasMethod('canBeReviewedBy') && !$page->canBeReviewedBy($user))) { $this->owner->httpError(403, _t( __CLASS__.'.ErrorItemPermissionDenied', 'It seems you don\'t have the necessary permissions to review this content' @@ -61,12 +56,10 @@ class ContentReviewCMSExtension extends LeftAndMainExtension return null; } - $handler = ReviewContentHandler::create($this->owner, $record); - $form = $handler->Form($record); - + $form = $this->getReviewContentHandler()->Form($page); $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id) { $schemaId = $this->owner->join_links($this->owner->Link('schema/ReviewContentForm'), $id); - return $this->owner->getSchemaResponse($schemaId, $form, $errors); + return $this->getSchemaResponse($schemaId, $form, $errors); }); return $form; @@ -77,30 +70,59 @@ class ContentReviewCMSExtension extends LeftAndMainExtension * * @param array $data * @param Form $form - * @return DBHTMLText|HTTPResponse + * @return DBHTMLText|HTTPResponse|null */ - public function submitReview($data = '', $form = '') + public function savereview($data, Form $form) { - $id = $data['ID']; - $record = SiteTree::get()->byID($id); + $page = $this->findRecord($data); - $handler = ReviewContentHandler::create($this->owner, $record); - $form = $handler->Form($record); - $results = $handler->submitReview($record, $data); + $results = $this->getReviewContentHandler()->submitReview($page, $data); if (is_null($results)) { return null; } - if ($this->getSchemaRequested()) { // Send extra "message" data with schema response $extraData = ['message' => $results]; - $schemaId = $this->owner->join_links($this->owner->Link('schema/ReviewContentForm'), $id); + $schemaId = $this->owner->join_links($this->owner->Link('schema/ReviewContentForm'), $page->ID); return $this->getSchemaResponse($schemaId, $form, null, $extraData); } return $results; } + /** + * Return a handler or reviewing content + * + * @return ReviewContentHandler + */ + protected function getReviewContentHandler() + { + return ReviewContentHandler::create($this->owner); + } + + /** + * Find the page this form is updating + * + * @param array $data Form data + * @return SiteTree Record + * @throws HTTPResponse_Exception + */ + protected function findRecord($data) + { + if (empty($data["ID"])) { + throw new HTTPResponse_Exception("No record ID", 404); + } + $page = null; + $id = $data["ID"]; + if (is_numeric($id)) { + $page = SiteTree::get()->byID($id); + } + if (!$page || !$page->ID) { + throw new HTTPResponse_Exception("Bad record ID #{$id}", 404); + } + return $page; + } + /** * Check if the current request has a X-Formschema-Request header set. * Used by conditional logic that responds to validation results diff --git a/src/Forms/ReviewContentHandler.php b/src/Forms/ReviewContentHandler.php index bb09164..bea06bd 100644 --- a/src/Forms/ReviewContentHandler.php +++ b/src/Forms/ReviewContentHandler.php @@ -7,6 +7,7 @@ use SilverStripe\Control\HTTPResponse; use SilverStripe\Core\Injector\Injectable; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\Form; +use SilverStripe\Forms\FormAction; use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\TextareaField; use SilverStripe\ORM\DataObject; @@ -24,13 +25,6 @@ class ReviewContentHandler */ protected $controller; - /** - * The submitted form data - * - * @var array - */ - protected $data; - /** * Form name to use * @@ -40,16 +34,11 @@ class ReviewContentHandler /** * @param Controller $controller - * @param array|DataObject $data * @param string $name */ - public function __construct($controller = null, $data = [], $name = 'ReviewContentForm') + public function __construct($controller = null, $name = 'ReviewContentForm') { $this->controller = $controller; - if ($data instanceof DataObject) { - $data = $data->toMap(); - } - $this->data = $data; $this->name = $name; } @@ -61,7 +50,8 @@ class ReviewContentHandler */ public function Form($object) { - $placeholder = 'Add comments (optional)'; + $placeholder = _t(__CLASS__ . '.Placeholder', 'Add comments (optional)'); + $title = _t(__CLASS__ . '.MarkAsReviewedAction', 'Mark as reviewed'); $fields = FieldList::create([ HiddenField::create('ID', null, $object->ID), @@ -71,16 +61,15 @@ class ReviewContentHandler ->setSchemaData(['attributes' => ['placeholder' => $placeholder]]) ]); - $actions = FieldList::create([ - ReviewContentHandlerFormAction::create() - ->setTitle(_t(__CLASS__ . '.MarkAsReviewedAction', 'Mark as reviewed')) - ->addExtraClass('review-content__action') - ]); + $action = FormAction::create('savereview', $title) + ->setTitle($title) + ->setUseButtonTag(false) + ->addExtraClass('review-content__action btn btn-primary'); + $actions = FieldList::create([$action]); - $form = Form::create($this->controller, $this->name, $fields, $actions); - - $form->setHTMLID('Form_EditForm_ReviewContent'); - $form->addExtraClass('form--no-dividers review-content__form'); + $form = Form::create($this->controller, $this->name, $fields, $actions) + ->setHTMLID('Form_EditForm_ReviewContent') + ->addExtraClass('form--no-dividers review-content__form'); return $form; } @@ -91,24 +80,20 @@ class ReviewContentHandler * @param DataObject $record * @param array $data * @return HTTPResponse|string + * @throws ValidationException If the user cannot submit the review */ public function submitReview($record, $data) { - if (!$record || !$record->exists()) { - throw new ValidationException(_t(__CLASS__ . '.ObjectDoesntExist', 'That object doesn\'t exist')); - } - - if (!$record->canEdit() - || !$record->hasMethod('canBeReviewedBy') - || !$record->canBeReviewedBy(Security::getCurrentUser()) - ) { + if (!$this->canSubmitReview($record)) { throw new ValidationException(_t( __CLASS__ . '.ErrorReviewPermissionDenied', 'It seems you don\'t have the necessary permissions to submit a content review' )); } - $this->saveRecord($record, $data); + $notes = (!empty($data['Review']) ? $data['Review'] : _t(__CLASS__ . '.NoComments', '(no comments)')); + $record->addReviewNote(Security::getCurrentUser(), $notes); + $record->advanceReviewDate(); $request = $this->controller->getRequest(); $message = _t(__CLASS__ . '.Success', 'Review successfully added'); @@ -119,21 +104,25 @@ class ReviewContentHandler $response = HTTPResponse::create($message, 200); $response->addHeader('Content-Type', 'text/html; charset=utf-8'); return $response; - } else { - return $this->controller->redirectBack(); } + + return $this->controller->redirectBack(); } /** - * Save the review provided in $data to the $record + * Determine whether the user can submit a review * * @param DataObject $record - * @param array $data + * @return bool */ - protected function saveRecord($record, $data) + public function canSubmitReview($record) { - $notes = (!empty($data['Review']) ? $data['Review'] : _t(__CLASS__ . '.NoComments', '(no comments)')); - $record->addReviewNote(Security::getCurrentUser(), $notes); - $record->advanceReviewDate(); + if (!$record->canEdit() + || !$record->hasMethod('canBeReviewedBy') + || !$record->canBeReviewedBy(Security::getCurrentUser()) + ) { + return false; + } + return true; } } diff --git a/src/Forms/ReviewContentHandlerFormAction.php b/src/Forms/ReviewContentHandlerFormAction.php deleted file mode 100644 index 321abfd..0000000 --- a/src/Forms/ReviewContentHandlerFormAction.php +++ /dev/null @@ -1,19 +0,0 @@ -setUseButtonTag(false) - ->addExtraClass('review-content-action btn btn-primary'); - } -} diff --git a/tests/behat/_manifest_exclude b/tests/behat/_manifest_exclude new file mode 100644 index 0000000..e69de29 diff --git a/tests/behat/features/set-up-reviews.feature b/tests/behat/features/set-up-reviews.feature new file mode 100644 index 0000000..c6036ee --- /dev/null +++ b/tests/behat/features/set-up-reviews.feature @@ -0,0 +1,41 @@ +Feature: Set up reviews + As a CMS user + I can set up content reviews for my content + In order to ensure my content gets reviewed regularly + + Background: + # Note: the review date is deliberately in the past + Given a "page" "Home" with "Content"="

Welcome

", "NextReviewDate"="01/01/2017", "ReviewPeriodDays"="1" + And I am logged in with "ADMIN" permissions + And I go to "admin/pages" + + @javascript + Scenario: I can set content review options + When I click on "Home" in the tree + And I click the "Settings" CMS tab + Then I should see a "Content Review" button + + When I click the "Content Review" CMS tab + And I select "Custom settings" from "Options" input group + And I wait for 1 second + And I select "ADMIN group" from "Groups" + And I press "Save" + Then I should see a "Content due for review" button + + @javascript + Scenario: I can enter a review in the modal + When I click on "Home" in the tree + And I click the "Settings" CMS tab + And I click the "Content Review" CMS tab + And I select "Custom settings" from "Options" input group + And I wait for 1 seconds + And I select "ADMIN group" from "Groups" + And I press "Save" + And I follow "Content due for review" + And I wait for 3 seconds + Then I should see a "Mark as reviewed" button + + When I fill in "Review" with "LGTM" + And I press "Mark as reviewed" + And I wait for 3 seconds + Then I should see "Review successfully added" diff --git a/tests/ContentReviewBaseTest.php b/tests/php/ContentReviewBaseTest.php similarity index 100% rename from tests/ContentReviewBaseTest.php rename to tests/php/ContentReviewBaseTest.php diff --git a/tests/ContentReviewCMSPageEditControllerTest.php b/tests/php/ContentReviewCMSPageEditControllerTest.php similarity index 78% rename from tests/ContentReviewCMSPageEditControllerTest.php rename to tests/php/ContentReviewCMSPageEditControllerTest.php index cd0f0bc..ab84c64 100644 --- a/tests/ContentReviewCMSPageEditControllerTest.php +++ b/tests/php/ContentReviewCMSPageEditControllerTest.php @@ -90,34 +90,6 @@ class ContentReviewCMSPageEditControllerTest extends ContentReviewBaseTest $this->assertEquals(200, $response->getStatusCode()); } - public function testSaveReview() - { - /** @var Member $author */ - $author = $this->objFromFixture(Member::class, "author"); - - $this->logInAs($author); - - /** @var Page|SiteTreeContentReview $page */ - $page = $this->objFromFixture(Page::class, "home"); - - $data = array( - "action_savereview" => 1, - "ID" => $page->ID, - "ReviewNotes" => "This is the best page ever", - ); - - $this->get('admin/pages/edit/show/' . $page->ID); - $response = $this->post($this->getFormAction($page), $data); - - $this->assertEquals("OK", $response->getStatusDescription()); - $this->assertEquals(200, $response->getStatusCode()); - $this->assertEquals(1, $page->ReviewLogs()->count()); - - $reviewLog = $page->ReviewLogs()->first(); - - $this->assertEquals($data["ReviewNotes"], $reviewLog->Note); - } - /** * Return a CMS page edit form action via using a dummy request and session * diff --git a/tests/ContentReviewNotificationTest.php b/tests/php/ContentReviewNotificationTest.php similarity index 100% rename from tests/ContentReviewNotificationTest.php rename to tests/php/ContentReviewNotificationTest.php diff --git a/tests/ContentReviewReportTest.php b/tests/php/ContentReviewReportTest.php similarity index 100% rename from tests/ContentReviewReportTest.php rename to tests/php/ContentReviewReportTest.php diff --git a/tests/ContentReviewSettingsTest.php b/tests/php/ContentReviewSettingsTest.php similarity index 100% rename from tests/ContentReviewSettingsTest.php rename to tests/php/ContentReviewSettingsTest.php diff --git a/tests/ContentReviewSettingsTest.yml b/tests/php/ContentReviewSettingsTest.yml similarity index 100% rename from tests/ContentReviewSettingsTest.yml rename to tests/php/ContentReviewSettingsTest.yml diff --git a/tests/ContentReviewTest.yml b/tests/php/ContentReviewTest.yml similarity index 100% rename from tests/ContentReviewTest.yml rename to tests/php/ContentReviewTest.yml diff --git a/tests/php/Extensions/ContentReviewCMSExtensionTest.php b/tests/php/Extensions/ContentReviewCMSExtensionTest.php new file mode 100644 index 0000000..b060a90 --- /dev/null +++ b/tests/php/Extensions/ContentReviewCMSExtensionTest.php @@ -0,0 +1,93 @@ +getMockBuilder(ContentReviewCMSExtension::class) + ->setMethods(['getReviewContentForm']) + ->getMock(); + + $mock->expects($this->once())->method('getReviewContentForm')->with(123)->willReturn(true); + + $request = new HTTPRequest('GET', '/', [], ['ID' => 123]); + $result = $mock->ReviewContentForm($request); + $this->assertTrue($result); + } + + /** + * @expectedException SilverStripe\Control\HTTPResponse_Exception + * @expectedExceptionMessage Bad record ID #1234 + */ + public function testGetReviewContentFormThrowsExceptionWhenPageNotFound() + { + (new ContentReviewCMSExtension)->getReviewContentForm(1234); + } + + /** + * @expectedException SilverStripe\Control\HTTPResponse_Exception + * @expectedExceptionMessage It seems you don't have the necessary permissions to review this content + */ + public function testGetReviewContentFormThrowsExceptionWhenObjectCannotBeReviewed() + { + $this->logOut(); + + $mock = $this->getMockBuilder(ContentReviewCMSExtension::class) + ->setMethods(['findRecord']) + ->getMock(); + + $mock->setOwner(new Controller); + + // Return a DataObject without the content review extension applied + $mock->expects($this->once())->method('findRecord')->with(['ID' => 123])->willReturn(new Member); + + $mock->getReviewContentForm(123); + } + + /** + * Ensure that savereview() calls the ReviewContentHandler and passes the data to it + */ + public function testSaveReviewCallsHandler() + { + $mock = $this->getMockBuilder(ContentReviewCMSExtension::class) + ->setMethods(['findRecord', 'getReviewContentHandler']) + ->getMock(); + + $mock->setOwner(new Controller); + + $mockPage = (object) ['ID' => 123]; + $mock->expects($this->once())->method('findRecord')->willReturn($mockPage); + + $mockHandler = $this->getMockBuilder(ReviewContentHandler::class) + ->setMethods(['submitReview']) + ->getMock(); + + $mockHandler->expects($this->once()) + ->method('submitReview') + ->with($mockPage, ['foo']) + ->willReturn('Success'); + + $mock->expects($this->once())->method('getReviewContentHandler')->willReturn($mockHandler); + + $form = $this->getMockBuilder(Form::class) + ->disableOriginalConstructor() + ->getMock(); + + $result = $mock->savereview(['foo'], $form); + $this->assertSame('Success', $result); + } +} diff --git a/tests/php/Forms/ReviewContentHandlerTest.php b/tests/php/Forms/ReviewContentHandlerTest.php new file mode 100644 index 0000000..7561224 --- /dev/null +++ b/tests/php/Forms/ReviewContentHandlerTest.php @@ -0,0 +1,84 @@ +Title = 'Test'; + $page->write(); + + $form = ReviewContentHandler::create()->Form($page); + + $this->assertInstanceOf(Form::class, $form); + $this->assertSame('ReviewContentForm', $form->getName()); + + $this->assertInstanceOf(HiddenField::class, $form->Fields()->fieldByName('ID')); + $this->assertInstanceOf(HiddenField::class, $form->Fields()->fieldByName('ClassName')); + $this->assertInstanceOf(TextareaField::class, $form->Fields()->fieldByName('Review')); + + $saveAction = $form->Actions()->first(); + $this->assertNotNull($saveAction); + $this->assertTrue($saveAction->hasClass('review-content__action')); + } + + /** + * @expectedException SilverStripe\ORM\ValidationException + * @expectedExceptionMessage It seems you don't have the necessary permissions to submit a content review + */ + public function testExceptionThrownWhenSubmittingReviewForInvalidObject() + { + ReviewContentHandler::create()->submitReview(new Member, ['foo' => 'bar']); + } + + public function testAddReviewNoteCalledWhenSubmittingReview() + { + $this->logInWithPermission('ADMIN'); + + $controller = new Controller; + $request = new HTTPRequest('GET', '/'); + $controller->setRequest($request); + Injector::inst()->registerservice($request); + + $mock = $this->getMockBuilder(ReviewContentHandler::class) + ->setConstructorArgs([$controller]) + ->setMethods(['canSubmitReview']) + ->getMock(); + + $mock->expects($this->exactly(3))->method('canSubmitReview')->willReturn(true); + + // Via CMS + $request->addHeader('X-Formschema-Request', true); + $result = $mock->submitReview(new SiteTree, ['Review' => 'testing']); + $this->assertSame('Review successfully added', $result); + $request->removeHeader('X-Formschema-Request'); + + // Via AJAX + $request->addHeader('X-Requested-With', 'XMLHttpRequest'); + $result = $mock->submitReview(new SiteTree, ['Review' => 'testing']); + $this->assertInstanceOf(HTTPResponse::class, $result); + $this->assertSame(200, $result->getStatusCode()); + $this->assertSame('Review successfully added', $result->getBody()); + $request->removeHeader('X-Requested-With'); + + // Default + $result = $mock->submitReview(new SiteTree, ['Review' => 'testing']); + $this->assertInstanceOf(HTTPResponse::class, $result); + $this->assertSame(302, $result->getStatusCode()); + } +} diff --git a/tests/SiteTreeContentReviewTest.php b/tests/php/SiteTreeContentReviewTest.php similarity index 96% rename from tests/SiteTreeContentReviewTest.php rename to tests/php/SiteTreeContentReviewTest.php index 098ccea..8960e97 100644 --- a/tests/SiteTreeContentReviewTest.php +++ b/tests/php/SiteTreeContentReviewTest.php @@ -9,6 +9,7 @@ use SilverStripe\ContentReview\Extensions\SiteTreeContentReview; use SilverStripe\ContentReview\Extensions\ContentReviewOwner; use SilverStripe\ContentReview\Extensions\ContentReviewCMSExtension; use SilverStripe\ContentReview\Extensions\ContentReviewDefaultSettings; +use SilverStripe\Forms\LiteralField; use SilverStripe\Security\Group; use SilverStripe\Security\Member; use SilverStripe\SiteConfig\SiteConfig; @@ -294,19 +295,19 @@ class SiteTreeContentReviewTest extends ContentReviewBaseTest public function testReviewActionVisibleForAuthor() { - DBDatetime::set_mock_now("2020-03-01 12:00:00"); + DBDatetime::set_mock_now('2020-03-01 12:00:00'); /** @var Page|SiteTreeContentReview $page */ - $page = $this->objFromFixture(Page::class, "contact"); + $page = $this->objFromFixture(Page::class, 'contact'); /** @var Member $author */ - $author = $this->objFromFixture(Member::class, "author"); + $author = $this->objFromFixture(Member::class, 'author'); $this->logInAs($author); $fields = $page->getCMSActions(); - $this->assertNotNull($fields->fieldByName("ActionMenus.ReviewContent")); + $this->assertInstanceOf(LiteralField::class, $fields->fieldByName('ContentReviewButton')); DBDatetime::clear_mock_now(); } From 4e7792a13235365e64afc1bed7f69880ca5ce991 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Wed, 13 Sep 2017 13:37:13 +1200 Subject: [PATCH 3/3] FIX Use ModuleLoader for requirements, and use path resolver for thirdparty dir --- src/Extensions/SiteTreeContentReview.php | 9 ++++++--- webpack.config.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Extensions/SiteTreeContentReview.php b/src/Extensions/SiteTreeContentReview.php index 0ebd77d..5370a40 100644 --- a/src/Extensions/SiteTreeContentReview.php +++ b/src/Extensions/SiteTreeContentReview.php @@ -8,6 +8,7 @@ use SilverStripe\ContentReview\Jobs\ContentReviewNotificationJob; use SilverStripe\ContentReview\Models\ContentReviewLog; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\FormAction; @@ -165,8 +166,9 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider return; } - Requirements::css('silverstripe/contentreview:client/dist/styles/contentreview.css'); - Requirements::javascript('silverstripe/contentreview:client/dist/js/contentreview.js'); + $module = ModuleLoader::getModule('silverstripe/contentreview'); + Requirements::css($module->getRelativeResourcePath('client/dist/styles/contentreview.css')); + Requirements::javascript($module->getRelativeResourcePath('client/dist/js/contentreview.js')); $reviewTab = LiteralField::create('ContentReviewButton', $this->owner->renderWith(__CLASS__ . '_button')); $actions->insertAfter('MajorActions', $reviewTab); @@ -327,7 +329,8 @@ class SiteTreeContentReview extends DataExtension implements PermissionProvider */ public function updateSettingsFields(FieldList $fields) { - Requirements::javascript("silverstripe/contentreview:client/dist/js/contentreview.js"); + $module = ModuleLoader::getModule('silverstripe/contentreview'); + Requirements::javascript($module->getRelativeResourcePath('client/dist/js/contentreview.js')); // Display read-only version only if (!Permission::check("EDIT_CONTENT_REVIEW_FIELDS")) { diff --git a/webpack.config.js b/webpack.config.js index fa7090e..591f8da 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,11 +13,11 @@ const { const ENV = process.env.NODE_ENV; const PATHS = { MODULES: 'node_modules', - THIRDPARTY: 'thirdparty', FILES_PATH: '../', ROOT: Path.resolve(), SRC: Path.resolve('client/src'), DIST: Path.resolve('client/dist'), + THIRDPARTY: Path.resolve('thirdparty'), }; const config = [