diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 2d34e8ecb3..1a2942ec2b 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -142,7 +142,7 @@ gulp.task('docserve', function(cb) { **************************/ gulp.task('dependencies', function () { - //bower component specific copy rules + //bower component/npm specific copy rules //this is to patch the sometimes wonky rules these libs are distrbuted under //as we do multiple things in this task, we merge the multiple streams @@ -199,6 +199,16 @@ gulp.task('dependencies', function () { .pipe(gulp.dest(root + targets.lib + "/codemirror")) ); + // npm dependencies + // flatpickr + stream.add( + gulp.src([ + "./node_modules/flatpickr/dist/flatpickr.js", + "./node_modules/flatpickr/dist/flatpickr.css"], + { base: "./node_modules/flatpickr/dist" }) + .pipe(gulp.dest(root + targets.lib + "/flatpickr")) + ); + //copy over libs which are not on bower (/lib) and //libraries that have been managed by bower-installer (/lib-bower) stream.add( diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index c05401a05c..8e4587c973 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -5,6 +5,7 @@ "build": "gulp" }, "dependencies": { + "flatpickr": "4.5.2", "npm": "^6.4.1" }, "devDependencies": { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 69c871f5ce..aec667077c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -97,14 +97,6 @@ eventsService.unsubscribe(evts[e]); } - evts.push(eventsService.on("editors.content.changePublishDate", function (event, args) { - createButtons(args.node); - })); - - evts.push(eventsService.on("editors.content.changeUnpublishDate", function (event, args) { - createButtons(args.node); - })); - evts.push(eventsService.on("editors.documentType.saved", function (name, args) { // if this content item uses the updated doc type we need to reload the content item if (args && args.documentType && args.documentType.key === content.documentType.key) { @@ -177,7 +169,8 @@ methods: { saveAndPublish: $scope.saveAndPublish, sendToPublish: $scope.sendToPublish, - unpublish: $scope.unpublish + unpublish: $scope.unpublish, + schedulePublish: $scope.schedule } }); @@ -611,6 +604,30 @@ }; + $scope.schedule = function() { + clearNotifications($scope.content); + //before we launch the dialog we want to execute all client side validations first + if (formHelper.submitForm({ scope: $scope, action: "schedule" })) { + + var dialog = { + parentScope: $scope, + view: "views/content/overlays/schedule.html", + variants: $scope.content.variants, //set a model property for the dialog + skipFormValidation: true, //when submitting the overlay form, skip any client side validation + submitButtonLabel: "Schedule", + submit: function (model) { + model.submitButtonState = "busy"; + clearNotifications($scope.content); + model.submitButtonState = "success"; + }, + close: function () { + overlayService.close(); + } + }; + overlayService.open(dialog); + } + }; + $scope.preview = function (content) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index 4831ecea8c..b2e64983d6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -46,19 +46,6 @@ }); - scope.datePickerConfig = { - pickDate: true, - pickTime: true, - useSeconds: false, - format: "YYYY-MM-DD HH:mm", - icons: { - time: "icon-time", - date: "icon-calendar", - up: "icon-chevron-up", - down: "icon-chevron-down" - } - }; - scope.auditTrailOptions = { "id": scope.node.id }; @@ -69,9 +56,6 @@ // get document type details scope.documentType = scope.node.documentType; - // make sure dates are formatted to the user's locale - formatDatesToLocal(); - //default setting for redirect url management scope.urlTrackerDisabled = false; @@ -117,22 +101,6 @@ scope.node.template = templateAlias; }; - scope.datePickerChange = function (event, type) { - if (type === 'publish') { - setPublishDate(event.date.format("YYYY-MM-DD HH:mm")); - } else if (type === 'unpublish') { - setUnpublishDate(event.date.format("YYYY-MM-DD HH:mm")); - } - }; - - scope.clearPublishDate = function () { - clearPublishDate(); - }; - - scope.clearUnpublishDate = function () { - clearUnpublishDate(); - }; - function loadAuditTrail() { scope.loadingAuditTrail = true; @@ -243,97 +211,6 @@ } } - function setPublishDate(date) { - - if (!date) { - return; - } - - //The date being passed in here is the user's local date/time that they have selected - //we need to convert this date back to the server date on the model. - - var serverTime = dateHelper.convertToServerStringTime(moment(date), Umbraco.Sys.ServerVariables.application.serverTimeOffset); - - // update publish value - scope.node.releaseDate = serverTime; - - // make sure dates are formatted to the user's locale - formatDatesToLocal(); - - // emit event - var args = { node: scope.node, date: date }; - eventsService.emit("editors.content.changePublishDate", args); - - } - - function clearPublishDate() { - - // update publish value - scope.node.releaseDate = null; - - // emit event - var args = { node: scope.node, date: null }; - eventsService.emit("editors.content.changePublishDate", args); - - } - - function setUnpublishDate(date) { - - if (!date) { - return; - } - - //The date being passed in here is the user's local date/time that they have selected - //we need to convert this date back to the server date on the model. - - var serverTime = dateHelper.convertToServerStringTime(moment(date), Umbraco.Sys.ServerVariables.application.serverTimeOffset); - - // update publish value - scope.node.removeDate = serverTime; - - // make sure dates are formatted to the user's locale - formatDatesToLocal(); - - // emit event - var args = { node: scope.node, date: date }; - eventsService.emit("editors.content.changeUnpublishDate", args); - - } - - function clearUnpublishDate() { - - // update publish value - scope.node.removeDate = null; - - // emit event - var args = { node: scope.node, date: null }; - eventsService.emit("editors.content.changeUnpublishDate", args); - - } - - function ucfirst(string) { - return string.charAt(0).toUpperCase() + string.slice(1); - } - - function formatDatesToLocal() { - // get current backoffice user and format dates - userService.getCurrentUser().then(function (currentUser) { - scope.node.createDateFormatted = dateHelper.getLocalDate(scope.node.createDate, currentUser.locale, 'LLL'); - - scope.node.releaseDateYear = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'YYYY')) : null; - scope.node.releaseDateMonth = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'MMMM')) : null; - scope.node.releaseDateDayNumber = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'DD')) : null; - scope.node.releaseDateDay = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'dddd')) : null; - scope.node.releaseDateTime = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'HH:mm')) : null; - - scope.node.removeDateYear = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'YYYY')) : null; - scope.node.removeDateMonth = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'MMMM')) : null; - scope.node.removeDateDayNumber = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'DD')) : null; - scope.node.removeDateDay = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'dddd')) : null; - scope.node.removeDateTime = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'HH:mm')) : null; - }); - } - // load audit trail and redirects when on the info tab evts.push(eventsService.on("app.tabChange", function (event, args) { $timeout(function () { @@ -356,7 +233,6 @@ if(isInfoTab) { loadAuditTrail(); loadRedirectUrls(); - formatDatesToLocal(); setNodePublishStatus(scope.node); } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js index 3a467807a3..21980be925 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js @@ -173,6 +173,12 @@ angular.module('umbraco.directives') return; } + // ignore clicks in flatpickr datepicker + var flatpickr = $(event.target).closest(".flatpickr-calendar"); + if (flatpickr.length === 1) { + return; + } + //ignore clicks inside this element if( $(element).has( $(event.target) ).length > 0 ){ return; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js index 6655bef4ca..07cfbb9848 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js @@ -700,6 +700,12 @@ Opens an overlay to show a custom YSOD.
}; + scope.outSideClick = function() { + if(!scope.model.disableBackdropClick) { + scope.closeOverLay(); + } + }; + unsubscribe.push(unregisterOverlay); scope.$on('$destroy', function () { for (var i = 0; i < unsubscribe.length; i++) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbflatpickr.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbflatpickr.directive.js new file mode 100644 index 0000000000..1f396ab6c2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbflatpickr.directive.js @@ -0,0 +1,231 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbFlatpickr +@restrict E +@scope + +@description +Added in Umbraco version 8.0 +This directive is a wrapper of the flatpickr library. Use it to render a date time picker. +For extra details about options and events take a look here: https://flatpickr.js.org/ + +Use this directive to render a date time picker + +

Markup example

+
+	
+ + + + +
+
+ +

Controller example

+
+	(function () {
+		"use strict";
+
+		function Controller() {
+
+            var vm = this;
+
+            vm.date = "2018-10-10 10:00";
+
+            vm.config = {
+				enableTime: true,
+				dateFormat: "Y-m-d H:i",
+				time_24hr: true
+            };
+
+            vm.datePickerChange = datePickerChange;
+
+            function datePickerChange(selectedDates, dateStr, instance) {
+            	// handle change
+            }
+
+        }
+
+		angular.module("umbraco").controller("My.Controller", Controller);
+
+	})();
+
+ +@param {object} ngModel (binding): Config object for the date picker. +@param {object} options (binding): Config object for the date picker. +@param {callback} onSetup (callback): onSetup gets triggered when the date picker is initialized +@param {callback} onChange (callback): onChange gets triggered when the user selects a date, or changes the time on a selected date. +@param {callback} onOpen (callback): onOpen gets triggered when the calendar is opened. +@param {callback} onClose (callback): onClose gets triggered when the calendar is closed. +@param {callback} onMonthChange (callback): onMonthChange gets triggered when the month is changed, either by the user or programmatically. +@param {callback} onYearChange (callback): onMonthChange gets triggered when the year is changed, either by the user or programmatically. +@param {callback} onReady (callback): onReady gets triggered once the calendar is in a ready state. +@param {callback} onValueUpdate (callback): onValueUpdate gets triggered when the input value is updated with a new date string. +@param {callback} onDayCreate (callback): Take full control of every date cell with theonDayCreate()hook. +**/ + +(function() { + 'use strict'; + + var umbFlatpickr = { + template: '' + + '' + + '
' + + '
', + controller: umbFlatpickrCtrl, + transclude: true, + bindings: { + ngModel: '<', + options: '<', + onSetup: '&?', + onChange: '&?', + onOpen: '&?', + onClose: '&?', + onMonthChange: '&?', + onYearChange: '&?', + onReady: '&?', + onValueUpdate: '&?', + onDayCreate: '&?' + } + }; + + function umbFlatpickrCtrl($element, $timeout, $scope, assetsService) { + var ctrl = this; + var loaded = false; + + ctrl.$onInit = function() { + + // load css file for the date picker + assetsService.loadCss('lib/flatpickr/flatpickr.css', $scope); + + // load the js file for the date picker + assetsService.loadJs('lib/flatpickr/flatpickr.js', $scope).then(function () { + // init date picker + loaded = true; + grabElementAndRunFlatpickr(); + }); + + }; + + function grabElementAndRunFlatpickr() { + $timeout(function() { + var transcludeEl = $element.find('ng-transclude')[0]; + var element = transcludeEl.children[0]; + + setDatepicker(element); + }, 0, true); + } + + function setDatepicker(element) { + var fpLib = flatpickr ? flatpickr : FlatpickrInstance; + + if (!fpLib) { + return console.warn('Unable to find any flatpickr installation'); + } + + setUpCallbacks(); + + var fpInstance = new fpLib(element, ctrl.options); + + if (ctrl.onSetup) { + ctrl.onSetup({ + fpItem: fpInstance + }); + } + + // If has ngModel set the date + if (ctrl.ngModel) { + fpInstance.setDate(ctrl.ngModel); + } + + // destroy the flatpickr instance when the dom element is removed + angular.element(element).on('$destroy', function() { + fpInstance.destroy(); + }); + + // Refresh the scope + $scope.$applyAsync(); + } + + function setUpCallbacks() { + // bind hook for onChange + if(ctrl.options && ctrl.onChange) { + ctrl.options.onChange = function(selectedDates, dateStr, instance) { + $timeout(function() { + ctrl.onChange({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); + }); + }; + } + + // bind hook for onOpen + if(ctrl.options && ctrl.onOpen) { + ctrl.options.onOpen = function(selectedDates, dateStr, instance) { + $timeout(function() { + ctrl.onOpen({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); + }); + }; + } + + // bind hook for onOpen + if(ctrl.options && ctrl.onClose) { + ctrl.options.onClose = function(selectedDates, dateStr, instance) { + $timeout(function() { + ctrl.onClose({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); + }); + }; + } + + // bind hook for onMonthChange + if(ctrl.options && ctrl.onMonthChange) { + ctrl.options.onMonthChange = function(selectedDates, dateStr, instance) { + $timeout(function() { + ctrl.onMonthChange({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); + }); + }; + } + + // bind hook for onYearChange + if(ctrl.options && ctrl.onYearChange) { + ctrl.options.onYearChange = function(selectedDates, dateStr, instance) { + $timeout(function() { + ctrl.onYearChange({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); + }); + }; + } + + // bind hook for onReady + if(ctrl.options && ctrl.onReady) { + ctrl.options.onReady = function(selectedDates, dateStr, instance) { + $timeout(function() { + ctrl.onReady({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); + }); + }; + } + + // bind hook for onValueUpdate + if(ctrl.onValueUpdate) { + ctrl.options.onValueUpdate = function(selectedDates, dateStr, instance) { + $timeout(function() { + ctrl.onValueUpdate({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); + }); + }; + } + + // bind hook for onDayCreate + if(ctrl.onDayCreate) { + ctrl.options.onDayCreate = function(selectedDates, dateStr, instance) { + $timeout(function() { + ctrl.onDayCreate({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); + }); + }; + } + + } + } + + angular.module('umbraco.directives').component('umbFlatpickr', umbFlatpickr); + +})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index ba2df1a68a..777b336447 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -146,7 +146,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica if (!args.methods) { throw "args.methods is not defined"; } - if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.unpublish) { + if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.unpublish || !args.methods.schedulePublish) { throw "args.methods does not contain all required defined methods"; } @@ -190,6 +190,16 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica alias: "unpublish", addEllipsis: args.content.variants && args.content.variants.length > 1 ? "true" : "false" }; + case "SCHEDULE": + //schedule publish - schedule doesn't have a permission letter so + // the button letter is made unique so it doesn't collide with anything else + return { + letter: ch, + labelKey: "buttons_schedulePublish", + handler: args.methods.schedulePublish, + alias: "schedulePublish", + addEllipsis: "true" + }; default: return null; } @@ -200,7 +210,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //This is the ideal button order but depends on circumstance, we'll use this array to create the button list // Publish, SendToPublish - var buttonOrder = ["U", "H"]; + var buttonOrder = ["U", "H", "SCHEDULE"]; //Create the first button (primary button) //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item. @@ -215,6 +225,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica break; } } + //Here's the special check, if the button still isn't set and we are creating and they have create access //we need to add the Save button if (!buttons.defaultButton && args.create && _.contains(args.content.allowedActions, "C")) { @@ -237,6 +248,12 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } } + // if publishing is allowed also allow schedule publish + // we add this manually becuase it doesn't have a permission so it wont + // get picked up by the loop through permissions + if( _.contains(args.content.allowedActions, "U")) { + buttons.subButtons.push(createButtonDefinition("SCHEDULE")); + } // if we are not creating, then we should add unpublish too, // so long as it's already published and if the user has access to publish @@ -249,40 +266,6 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } } - // If we have a scheduled publish or unpublish date change the default button to - // "save" and update the label to "save and schedule - if (args.content.releaseDate || args.content.removeDate) { - - // if save button is alread the default don't change it just update the label - if (buttons.defaultButton && buttons.defaultButton.letter === "A") { - buttons.defaultButton.labelKey = "buttons_saveAndSchedule"; - return buttons; - } - - if (buttons.defaultButton && buttons.subButtons && buttons.subButtons.length > 0) { - // save a copy of the default so we can push it to the sub buttons later - var defaultButtonCopy = angular.copy(buttons.defaultButton); - var newSubButtons = []; - - // if save button is not the default button - find it and make it the default - angular.forEach(buttons.subButtons, function (subButton) { - - if (subButton.letter === "A") { - buttons.defaultButton = subButton; - buttons.defaultButton.labelKey = "buttons_saveAndSchedule"; - } else { - newSubButtons.push(subButton); - } - - }); - - // push old default button into subbuttons - newSubButtons.push(defaultButtonCopy); - buttons.subButtons = newSubButtons; - } - - } - return buttons; }, diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index cc98ec306b..ac8279e3ab 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -96,6 +96,7 @@ @import "components/tree/umb-actions.less"; @import "components/tree/umb-tree-item.less"; + @import "components/editor.less"; @import "components/overlays.less"; @import "components/card.less"; @@ -104,6 +105,7 @@ @import "components/umb-editor-navigation.less"; @import "components/umb-editor-sub-views.less"; @import "components/editor/subheader/umb-editor-sub-header.less"; +@import "components/umb-flatpickr.less"; @import "components/umb-grid-selector.less"; @import "components/umb-child-selector.less"; @import "components/umb-group-builder.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index 3fbe32134b..6538a751f9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -75,7 +75,7 @@ /* ---------- OVERLAY CENTER ---------- */ .umb-overlay.umb-overlay-center { position: absolute; - width: 500px; + width: 600px; height: auto; top: 50%; left: 50%; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less new file mode 100644 index 0000000000..1848d55d96 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less @@ -0,0 +1,21 @@ +.flatpickr-calendar.flatpickr-calendar { + border-radius: @baseBorderRadius; + box-shadow: 0 5px 10px 0 rgba(0,0,0,0.16); +} + +span.flatpickr-day { + border-radius: @baseBorderRadius; + border: none; +} + +span.flatpickr-day:hover { + background-color: @gray-10; +} + +span.flatpickr-day.selected { + background-color: @turquoise; +} + +span.flatpickr-day.selected:hover { + background-color: @turquoise-d1; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 6d567c5fb3..0284a79865 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -48,6 +48,7 @@ h5.-black { border: none } .bootstrap-datetimepicker-widget { + width: auto !important; td { &.active, span.active { background: @turquoise !important; diff --git a/src/Umbraco.Web.UI.Client/src/less/properties.less b/src/Umbraco.Web.UI.Client/src/less/properties.less index 06f05ab0aa..916f1b5a3a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/properties.less +++ b/src/Umbraco.Web.UI.Client/src/less/properties.less @@ -1,61 +1,19 @@ //----- SCHEDULED PUBLISH ------ -.place-holder { - height: 60px; - width: 60px; - margin: 15px auto; - background-color: @gray-8; -} - .date-wrapper { display: flex; - justify-content: space-around; flex-direction: row; + border-top: 1px solid @gray-10; + border-bottom: 1px solid @gray-10; } -.date-container { - text-align: center; +.date-wrapper__date { + padding: 10px; + flex: 1 1 50%; } -.date-wrapper__number{ - font-size: 40px; - line-height: 50px; - color: @gray-2; - font-weight: 900; -} - -.date-container__title { - font-size: 16px; - font-weight: bold; - color: @gray-3; - margin-bottom: 5px; -} - -.date-container__date { - padding: 0 10px; -} -.date-container__date:hover { - background-color: @gray-10; - cursor: pointer; -} - -.date-wrapper__date{ - font-size: 13px; - color: @gray-6; - margin: 0; -} - -.data-wrapper__add{ - font-size: 18px; - line-height: 10px; - color: @gray-8; - font-weight: 900; - margin: 0; -} - -.date-separate { - width: 1px; - background-color: @gray-8; +.date-wrapper__date:last-of-type { + border-left: 1px solid @gray-10; } //------------------- HISTORY ------------------ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index c2e5df6f30..03d4318ba2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -112,78 +112,7 @@
- - - - -
- -
- - - - -
- -
- -
- -
-
{{node.releaseDateMonth}} {{node.releaseDateYear}}
-
{{node.releaseDateDayNumber}}
-
{{node.releaseDateDay}} {{node.releaseDateTime}}
-
- - Set date - -
- -
- - - Clear date - - -
- -
- -
- - -
- -
- -
- -
-
{{node.removeDateMonth}} {{node.removeDateYear}}
-
{{node.removeDateDayNumber}}
-
{{node.removeDateDay}} {{node.removeDateTime}}
-
- - Set date - -
- -
- - - Clear date - -
- -
- - -
-
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html index feacf3adf8..40b0e181bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js new file mode 100644 index 0000000000..6921644a1b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js @@ -0,0 +1,335 @@ +(function () { + "use strict"; + + function ScheduleContentController($scope, $timeout, localizationService, dateHelper, userService) { + + var vm = this; + + vm.datePickerSetup = datePickerSetup; + vm.datePickerChange = datePickerChange; + vm.datePickerShow = datePickerShow; + vm.datePickerClose = datePickerClose; + vm.clearPublishDate = clearPublishDate; + vm.clearUnpublishDate = clearUnpublishDate; + vm.dirtyVariantFilter = dirtyVariantFilter; + vm.pristineVariantFilter = pristineVariantFilter; + vm.changeSelection = changeSelection; + + vm.firstSelectedDates = {}; + vm.currentUser = null; + + function onInit() { + + vm.variants = $scope.model.variants; + vm.hasPristineVariants = false; + + if(!$scope.model.title) { + localizationService.localize("general_scheduledPublishing").then(function(value){ + $scope.model.title = value; + }); + } + + // Check for variants: if a node is invariant it will still have the default language in variants + // so we have to check for length > 1 + if (vm.variants.length > 1) { + + _.each(vm.variants, + function (variant) { + variant.compositeId = variant.language.culture + "_" + (variant.segment ? variant.segment : ""); + variant.htmlId = "_content_variant_" + variant.compositeId; + + //check for pristine variants + if (!vm.hasPristineVariants) { + vm.hasPristineVariants = pristineVariantFilter(variant); + } + }); + + //now sort it so that the current one is at the top + vm.variants = _.sortBy(vm.variants, function (v) { + return v.active ? 0 : 1; + }); + + var active = _.find(vm.variants, function (v) { + return v.active; + }); + + if (active) { + //ensure that the current one is selected + active.schedule = true; + active.save = true; + } + + $scope.model.disableSubmitButton = !canSchedule(); + + } + + // get current backoffice user and format dates + userService.getCurrentUser().then(function (currentUser) { + + vm.currentUser = currentUser; + + angular.forEach(vm.variants, function(variant) { + + // prevent selecting publish/unpublish date before today + var now = new Date(); + var nowFormatted = moment(now).format("YYYY-MM-DD HH:mm"); + + var datePickerConfig = { + enableTime: true, + dateFormat: "Y-m-d H:i", + time_24hr: true, + minDate: nowFormatted, + defaultDate: nowFormatted + }; + + variant.datePickerConfig = datePickerConfig; + + // format all dates to local + if(variant.releaseDate || variant.removeDate) { + formatDatesToLocal(variant); + } + }); + + }); + + } + + /** + * Callback when date is set up + * @param {any} variant + * @param {any} type publish or unpublish + * @param {any} datePickerInstance The date picker instance + */ + function datePickerSetup(variant, type, datePickerInstance) { + // store a date picker instance for publish and unpublish picker + // so we can change the settings independently. + if (type === 'publish') { + variant.releaseDatePickerInstance = datePickerInstance; + } else if (type === 'unpublish') { + variant.removeDatePickerInstance = datePickerInstance; + } + }; + + /** + * Callback when date picker date changes + * @param {any} variant + * @param {any} dateStr Date string from the date picker + * @param {any} type publish or unpublish + */ + function datePickerChange(variant, dateStr, type) { + if (type === 'publish') { + setPublishDate(variant, dateStr); + } else if (type === 'unpublish') { + setUnpublishDate(variant, dateStr); + } + } + + /** + * Add flag when a date picker opens is we can prevent the overlay from closing + * @param {any} variant + * @param {any} type publish or unpublish + */ + function datePickerShow(variant, type) { + if (type === 'publish') { + variant.releaseDatePickerOpen = true; + } else if (type === 'unpublish') { + variant.removeDatePickerOpen = true; + } + checkForBackdropClick(); + } + + /** + * Remove flag when a date picker closes so the overlay can be closed again + * @param {any} variant + * @param {any} type publish or unpublish + */ + function datePickerClose(variant, type) { + $timeout(function(){ + if (type === 'publish') { + variant.releaseDatePickerOpen = false; + } else if (type === 'unpublish') { + variant.removeDatePickerOpen = false; + } + checkForBackdropClick(); + }, 200); + } + + /** + * Prevent the overlay from closing if any date pickers are open + */ + function checkForBackdropClick() { + + var open = _.find(vm.variants, function (variant) { + return variant.releaseDatePickerOpen || variant.removeDatePickerOpen; + }); + + if(open) { + $scope.model.disableBackdropClick = true; + } else { + $scope.model.disableBackdropClick = false; + } + } + + /** + * Sets the selected publish date + * @param {any} variant + * @param {any} date The selected date + */ + function setPublishDate(variant, date) { + + if (!date) { + return; + } + + //The date being passed in here is the user's local date/time that they have selected + //we need to convert this date back to the server date on the model. + var serverTime = dateHelper.convertToServerStringTime(moment(date), Umbraco.Sys.ServerVariables.application.serverTimeOffset); + + // update publish value + variant.releaseDate = serverTime; + + // make sure dates are formatted to the user's locale + formatDatesToLocal(variant); + + // make sure the unpublish date can't be before the publish date + variant.removeDatePickerInstance.set("minDate", moment(variant.releaseDate).format("YYYY-MM-DD HH:mm")); + + } + + /** + * Sets the selected unpublish date + * @param {any} variant + * @param {any} date The selected date + */ + function setUnpublishDate(variant, date) { + + if (!date) { + return; + } + + //The date being passed in here is the user's local date/time that they have selected + //we need to convert this date back to the server date on the model. + var serverTime = dateHelper.convertToServerStringTime(moment(date), Umbraco.Sys.ServerVariables.application.serverTimeOffset); + + // update publish value + variant.removeDate = serverTime; + + // make sure dates are formatted to the user's locale + formatDatesToLocal(variant); + + // make sure the publish date can't be after the publish date + variant.releaseDatePickerInstance.set("maxDate", moment(variant.removeDate).format("YYYY-MM-DD HH:mm")); + + } + + /** + * Clears the publish date + * @param {any} variant + */ + function clearPublishDate(variant) { + if(variant && variant.releaseDate) { + variant.releaseDate = null; + // we don't have a publish date anymore so we can clear the min date for unpublish + var now = new Date(); + var nowFormatted = moment(now).format("YYYY-MM-DD HH:mm"); + variant.removeDatePickerInstance.set("minDate", nowFormatted); + } + } + + /** + * Clears the unpublish date + * @param {any} variant + */ + function clearUnpublishDate(variant) { + if(variant && variant.removeDate) { + variant.removeDate = null; + // we don't have a unpublish date anymore so we can clear the max date for publish + variant.releaseDatePickerInstance.set("maxDate", null); + } + } + + /** + * Formates the selected dates to fit the user culture + * @param {any} variant + */ + function formatDatesToLocal(variant) { + if(variant && variant.releaseDate) { + variant.releaseDateFormatted = dateHelper.getLocalDate(variant.releaseDate, vm.currentUser.locale, "MMM Do YYYY, HH:mm"); + } + if(variant && variant.removeDate) { + variant.removeDateFormatted = dateHelper.getLocalDate(variant.removeDate, vm.currentUser.locale, "MMM Do YYYY, HH:mm"); + } + } + + /** + * Called when new variants are selected or deselected + * @param {any} variant + */ + function changeSelection(variant) { + $scope.model.disableSubmitButton = !canSchedule(); + //need to set the Save state to true if publish is true + variant.save = variant.schedule; + } + + function dirtyVariantFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * the active one + // * it's editor is in a $dirty state + // * it has pending saves + // * it is unpublished + // * it is in NotCreated state + return (variant.active || variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges" || variant.state === "NotCreated"); + } + + function pristineVariantFilter(variant) { + return !(dirtyVariantFilter(variant)); + } + + /** Returns true if publishing is possible based on if there are un-published mandatory languages */ + function canSchedule() { + var selected = []; + for (var i = 0; i < vm.variants.length; i++) { + var variant = vm.variants[i]; + + //if this variant will show up in the publish-able list + var publishable = dirtyVariantFilter(variant); + + if ((variant.language.isMandatory && (variant.state === "NotCreated" || variant.state === "Draft")) + && (!publishable || !variant.schedule)) { + //if a mandatory variant isn't published and it's not publishable or not selected to be published + //then we cannot publish anything + + //TODO: Show a message when this occurs + return false; + } + + if (variant.schedule) { + selected.push(variant.schedule); + } + } + return selected.length > 0; + } + + onInit(); + + //when this dialog is closed, clean up + $scope.$on('$destroy', function () { + for (var i = 0; i < vm.variants.length; i++) { + vm.variants[i].save = false; + vm.variants[i].schedule = false; + // remove properties only needed for this dialog + delete vm.variants[i].releaseDateFormatted; + delete vm.variants[i].removeDateFormatted; + delete vm.variants[i].datePickerConfig; + delete vm.variants[i].releaseDatePickerInstance; + delete vm.variants[i].removeDatePickerInstance; + delete vm.variants[i].releaseDatePickerOpen; + delete vm.variants[i].removeDatePickerOpen; + } + }); + + } + + angular.module("umbraco").controller("Umbraco.Overlays.ScheduleContentController", ScheduleContentController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html new file mode 100644 index 0000000000..0486bf68dd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html @@ -0,0 +1,191 @@ +
+ + +
+ +
+

+
+ +
+
+ + +
+ + + +
+ + + + Set date + +
+ +
+ + + + + +
+
+ +
+ + +
+ + +
+ + + + Set date + +
+ +
+ + + + + +
+
+
+
+ + +
+ +
+

+
+ +
+ +
+ +
+ +
+ + + +
+ + - +
+ +
+
Publish:
+ +
+ +
+ + + + Set date + +
+
+ + + +
+ +
Unpublish:
+ +
+ +
+ + + + Set date + +
+
+ + + +
+ +
+ +
+
+ +
+
+
+
+ +
+
+

+
+ +
+
+
+ {{ variant.language.name }} +
+
+ + - +
+
+
+
+ +
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 3c95ebf9da..ae75d7af43 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -126,6 +126,7 @@ Save and schedule Send for approval Save list view + Schedule Preview Preview is disabled because there's no template assigned Choose style @@ -223,7 +224,47 @@ Publish Published Published (pending changes)> - Publication StatusPublish atUnpublish atClear DateSet dateSortorder is updatedTo sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selectingStatisticsTitle (optional)Alternative text (optional)TypeUnpublishDraftNot createdLast editedDate/time this document was editedRemove file(s)Link to documentMember of group(s)Not a member of group(s)Child itemsTargetThis translates to the following time on the server:What does this mean?]]>Are you sure you want to delete this item?Property %0% uses editor %1% which is not supported by Nested Content.Add another text boxRemove this text boxContent rootThis value is hidden. If you need access to view this value please contact your website administrator.This value is hidden.What languages would you like to publish?What languages would you like to save?What languages would you like to send for approval?Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages.Published LanguagesUnpublished LanguagesUnmodified LanguagesReady to Publish?Ready to Save?Send for approval + Publication Status + Publish at + Unpublish at + Clear Date + Set date + Sortorder is updated + To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting + Statistics + Title (optional) + Alternative text (optional) + Type + Unpublish + Draft + Not created + Last edited + Date/time this document was edited + Remove file(s) + Link to document + Member of group(s) + Not a member of group(s) + Child items + Target + This translates to the following time on the server: + What does this mean?]]>Are you sure you want to delete this item?Property %0% uses editor %1% which is not supported by Nested Content. + Add another text box + Remove this text box + Content root + This value is hidden. If you need access to view this value please contact your website administrator. + This value is hidden. + What languages would you like to publish? + What languages would you like to save? + What languages would you like to send for approval? + What languages would you like to schedule? + Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. + Published Languages + Unpublished Languages + Unmodified Languages + Ready to Publish? + Ready to Save? + Send for approval + Create a new Content Template from '%0%' Blank