Merge pull request #3213 from umbraco/temp8-223-scheduled-publishing

Temp8 223 scheduled publishing
This commit is contained in:
Warren Buckley
2018-10-09 14:09:46 +01:00
committed by GitHub
18 changed files with 902 additions and 294 deletions
+11 -1
View File
@@ -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(
+1
View File
@@ -5,6 +5,7 @@
"build": "gulp"
},
"dependencies": {
"flatpickr": "4.5.2",
"npm": "^6.4.1"
},
"devDependencies": {
@@ -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) {
@@ -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);
}
});
@@ -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;
@@ -700,6 +700,12 @@ Opens an overlay to show a custom YSOD. </br>
};
scope.outSideClick = function() {
if(!scope.model.disableBackdropClick) {
scope.closeOverLay();
}
};
unsubscribe.push(unregisterOverlay);
scope.$on('$destroy', function () {
for (var i = 0; i < unsubscribe.length; i++) {
@@ -0,0 +1,231 @@
/**
@ngdoc directive
@name umbraco.directives.directive:umbFlatpickr
@restrict E
@scope
@description
<b>Added in Umbraco version 8.0</b>
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
<h3>Markup example</h3>
<pre>
<div ng-controller="My.Controller as vm">
<umb-flatpickr
ng-model="vm.date"
options="vm.config"
on-change="vm.datePickerChange(selectedDates, dateStr, instance)">
</umb-flatpickr>
</div>
</pre>
<h3>Controller example</h3>
<pre>
(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);
})();
</pre>
@param {object} ngModel (<code>binding</code>): Config object for the date picker.
@param {object} options (<code>binding</code>): Config object for the date picker.
@param {callback} onSetup (<code>callback</code>): onSetup gets triggered when the date picker is initialized
@param {callback} onChange (<code>callback</code>): onChange gets triggered when the user selects a date, or changes the time on a selected date.
@param {callback} onOpen (<code>callback</code>): onOpen gets triggered when the calendar is opened.
@param {callback} onClose (<code>callback</code>): onClose gets triggered when the calendar is closed.
@param {callback} onMonthChange (<code>callback</code>): onMonthChange gets triggered when the month is changed, either by the user or programmatically.
@param {callback} onYearChange (<code>callback</code>): onMonthChange gets triggered when the year is changed, either by the user or programmatically.
@param {callback} onReady (<code>callback</code>): onReady gets triggered once the calendar is in a ready state.
@param {callback} onValueUpdate (<code>callback</code>): onValueUpdate gets triggered when the input value is updated with a new date string.
@param {callback} onDayCreate (<code>callback</code>): Take full control of every date cell with theonDayCreate()hook.
**/
(function() {
'use strict';
var umbFlatpickr = {
template: '<ng-transclude>' +
'<input type="text" ng-if="!$ctrl.options.inline" ng-model="$ctrl.ngModel" placeholder="Select Date.."></input>' +
'<div ng-if="$ctrl.options.inline"></div>' +
'</ng-transclude>',
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);
})();
@@ -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;
},
@@ -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";
@@ -75,7 +75,7 @@
/* ---------- OVERLAY CENTER ---------- */
.umb-overlay.umb-overlay-center {
position: absolute;
width: 500px;
width: 600px;
height: auto;
top: 50%;
left: 50%;
@@ -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;
}
@@ -48,6 +48,7 @@ h5.-black {
border: none
}
.bootstrap-datetimepicker-widget {
width: auto !important;
td {
&.active, span.active {
background: @turquoise !important;
@@ -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 ------------------
@@ -112,78 +112,7 @@
</div>
<div class="umb-package-details__sidebar">
<umb-box data-element="node-info-scheduled-publishing">
<umb-box-header title-key="general_scheduledPublishing"></umb-box-header>
<umb-box-content class="block-form">
<div class="date-wrapper">
<div class="flex items-center flex-column">
<umb-date-time-picker data-element="node-info-publish"
options="datePickerConfig"
on-change="datePickerChange(event, 'publish')">
<div class="date-container">
<div class="date-container__title">
<localize key="content_releaseDate"></localize>
</div>
<div class="date-container__date" ng-if="node.releaseDate">
<div class="date-wrapper__date">{{node.releaseDateMonth}} {{node.releaseDateYear}}</div>
<div class="date-wrapper__number">{{node.releaseDateDayNumber}}</div>
<div class="date-wrapper__date">{{node.releaseDateDay}} {{node.releaseDateTime}}</div>
</div>
<a href="" ng-if="!node.releaseDate" class="bold" style="color: #00aea2; text-decoration: underline;"><localize key="content_setDate">Set date</localize></a>
</div>
</umb-date-time-picker>
<a ng-if="node.releaseDate" ng-click="clearPublishDate()" href="" style="text-decoration: underline;">
<small><localize key="content_removeDate">Clear date</localize></small>
</a>
</div>
<div class="date-separate"></div>
<div class="flex items-center flex-column">
<umb-date-time-picker data-element="node-info-unpublish"
options="datePickerConfig"
on-change="datePickerChange(event, 'unpublish')">
<div class="date-container">
<div class="date-container__title">
<localize key="content_unpublishDate"></localize>
</div>
<div class="date-container__date" ng-if="node.removeDate">
<div class="date-wrapper__date">{{node.removeDateMonth}} {{node.removeDateYear}}</div>
<div class="date-wrapper__number">{{node.removeDateDayNumber}}</div>
<div class="date-wrapper__date">{{node.removeDateDay}} {{node.removeDateTime}}</div>
</div>
<a href="" ng-if="!node.removeDate" class="bold" style="color: #00aea2; text-decoration: underline;"><localize key="content_setDate">Set date</localize></a>
</div>
</umb-date-time-picker>
<a ng-if="node.removeDate" ng-click="clearUnpublishDate()" href="" style="text-decoration: underline;">
<small><localize key="content_removeDate">Clear date</localize></small>
</a>
</div>
</div>
</umb-box-content>
</umb-box>
<umb-box data-element="node-info-general">
<umb-box-header title-key="general_general"></umb-box-header>
<umb-box-content class="block-form">
@@ -1,4 +1,4 @@
<div data-element="overlay" class="umb-overlay umb-overlay-{{position}}" on-outside-click="closeOverLay()">
<div data-element="overlay" class="umb-overlay umb-overlay-{{position}}" on-outside-click="outSideClick()">
<ng-form class="umb-overlay__form" name="overlayForm" novalidate val-form-manager>
<div data-element="overlay-header" class="umb-overlay-header">
@@ -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);
})();
@@ -0,0 +1,191 @@
<div ng-controller="Umbraco.Overlays.ScheduleContentController as vm">
<!-- invariant nodes -->
<div ng-if="vm.variants.length === 1">
<div style="margin-bottom: 15px;">
<p><localize key="content_languagesToSchedule"></localize></p>
</div>
<div class="date-wrapper">
<div class="date-wrapper__date">
<label class="bold">
<localize key="content_releaseDate"></localize>
</label>
<div class="btn-group flex" style="font-size: 15px;">
<umb-flatpickr
ng-model="vm.variants[0].releaseDate"
options="vm.variants[0].datePickerConfig"
on-setup="vm.datePickerSetup(vm.variants[0], 'publish', fpItem)"
on-change="vm.datePickerChange(vm.variants[0], dateStr, 'publish')"
on-open="vm.datePickerShow(vm.variants[0], 'publish')"
on-close="vm.datePickerClose(vm.variants[0], 'publish')">
<div>
<button ng-if="vm.variants[0].releaseDate" class="btn umb-button--xs" style="outline: none;">
{{vm.variants[0].releaseDateFormatted}}
</button>
<a ng-hide="vm.variants[0].releaseDate" href="" class="bold" style="color: #00aea2; text-decoration: underline;">
<localize key="content_setDate">Set date</localize>
</a>
</div>
</umb-flatpickr>
<a ng-if="vm.variants[0].releaseDate" ng-click="vm.clearPublishDate(vm.variants[0])" class="btn umb-button--xs dropdown-toggle umb-button-group__toggle" style="margin-left: -2px;">
<span class="icon icon-wrong"></span>
</a>
</div>
</div>
<div class="date-wrapper__date">
<label class="bold">
<localize key="content_unpublishDate"></localize>
</label>
<div class="btn-group flex" style="font-size: 15px;">
<umb-flatpickr
ng-model="vm.variants[0].removeDate"
options="vm.variants[0].datePickerConfig"
on-setup="vm.datePickerSetup(vm.variants[0], 'unpublish', fpItem)"
on-change="vm.datePickerChange(vm.variants[0], dateStr, 'unpublish')"
on-open="vm.datePickerShow(vm.variants[0], 'unpublish')"
on-close="vm.datePickerClose(vm.variants[0], 'unpublish')">
<div>
<button ng-if="vm.variants[0].removeDate" class="btn umb-button--xs" style="outline: none;">
{{vm.variants[0].removeDateFormatted}}
</button>
<a ng-hide="vm.variants[0].removeDate" href="" class="bold" style="color: #00aea2; text-decoration: underline;">
<localize key="content_setDate">Set date</localize>
</a>
</div>
</umb-flatpickr>
<a ng-if="vm.variants[0].removeDate" ng-click="vm.clearUnpublishDate(vm.variants[0])" class="btn umb-button--xs dropdown-toggle umb-button-group__toggle" style="margin-left: -2px;">
<span class="icon icon-wrong"></span>
</a>
</div>
</div>
</div>
</div>
<!-- nodes with variants -->
<div ng-if="vm.variants.length > 1">
<div style="margin-bottom: 15px;">
<p><localize key="content_languagesToSchedule"></localize></p>
</div>
<div class="umb-list umb-list--condensed">
<div class="umb-list-item" ng-repeat="variant in vm.variants | filter:vm.dirtyVariantFilter">
<ng-form name="scheduleSelectorForm">
<div class="flex">
<input
id="{{variant.language.culture}}"
name="saveVariantSelector"
type="checkbox"
ng-model="variant.schedule"
ng-change="vm.changeSelection(variant)"
style="margin-right: 8px;" />
<div>
<label for="{{variant.language.culture}}" style="margin-bottom: 2px;">
<span>{{ variant.language.name }}</span>
</label>
<div class="umb-permission__description">
<umb-variant-state variant="variant"></umb-variant-state>
<span ng-if="variant.language.isMandatory"> - <localize key="languages_mandatoryLanguage"></localize></span>
</div>
<div ng-if="variant.schedule" class="flex items-center" style="margin-top: 10px; margin-bottom: 10px;">
<div style="font-size: 13px; margin-right: 5px;">Publish: </div>
<div class="btn-group flex" style="font-size: 14px; margin-right: 10px;">
<umb-flatpickr
ng-model="variant.releaseDate"
options="variant.datePickerConfig"
on-setup="vm.datePickerSetup(variant, 'publish', fpItem)"
on-change="vm.datePickerChange(variant, dateStr, 'publish')"
on-open="vm.datePickerShow(variant, 'publish')"
on-close="vm.datePickerClose(variant, 'publish')">
<div>
<button ng-if="variant.releaseDate" class="btn umb-button--xxs" style="outline: none;">
{{variant.releaseDateFormatted}}
</button>
<a ng-hide="variant.releaseDate" href="" class="bold" style="color: #00aea2; text-decoration: underline;">
<localize key="content_setDate">Set date</localize>
</a>
</div>
</umb-flatpickr>
<a ng-if="variant.releaseDate" ng-click="vm.clearPublishDate(variant)" class="btn umb-button--xxs dropdown-toggle umb-button-group__toggle" style="margin-left: -2px;">
<span class="icon icon-wrong"></span>
</a>
</div>
<div style="font-size: 13px; margin-right: 5px;">Unpublish:</div>
<div class="btn-group flex" style="font-size: 14px;">
<umb-flatpickr
ng-model="variant.removeDate"
options="variant.datePickerConfig"
on-setup="vm.datePickerSetup(variant, 'unpublish', fpItem)"
on-change="vm.datePickerChange(variant, dateStr, 'unpublish')"
on-open="vm.datePickerShow(variant, 'unpublish')"
on-close="vm.datePickerClose(variant, 'unpublish')">
<div>
<button ng-if="variant.removeDate" class="btn umb-button--xxs" style="outline: none;">
{{variant.removeDateFormatted}}
</button>
<a ng-hide="variant.removeDate" href="" class="bold" style="color: #00aea2; text-decoration: underline;">
<localize key="content_setDate">Set date</localize>
</a>
</div>
</umb-flatpickr>
<a ng-if="variant.removeDate" ng-click="vm.clearUnpublishDate(variant)" class="btn umb-button--xxs dropdown-toggle umb-button-group__toggle" style="margin-left: -2px;">
<span class="icon icon-wrong"></span>
</a>
</div>
</div>
</div>
</div>
</ng-form>
</div>
<br/>
</div>
<div class="umb-list umb-list--condensed" ng-if="vm.hasPristineVariants">
<div style="margin-bottom: 15px; font-weight: bold;">
<p><localize key="content_publishedLanguages"></localize></p>
</div>
<div class="umb-list-item" ng-repeat="variant in vm.variants | filter:vm.pristineVariantFilter track by variant.language.culture">
<div>
<div style="margin-bottom: 2px;">
<span>{{ variant.language.name }}</span>
</div>
<div class="umb-permission__description">
<umb-variant-state variant="variant"></umb-variant-state>
<span ng-if="variant.language.isMandatory"> - <localize key="languages_mandatoryLanguage"></localize></span>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -126,6 +126,7 @@
<key alias="saveAndSchedule">Save and schedule</key>
<key alias="saveToPublish">Send for approval</key>
<key alias="saveListView">Save list view</key>
<key alias="schedulePublish">Schedule</key>
<key alias="showPage">Preview</key>
<key alias="showPageDisabled">Preview is disabled because there's no template assigned</key>
<key alias="styleChoose">Choose style</key>
@@ -223,7 +224,47 @@
<key alias="publish">Publish</key>
<key alias="published">Published</key>
<key alias="publishedPendingChanges">Published (pending changes)</key>&gt;
<key alias="publishStatus">Publication Status</key><key alias="releaseDate">Publish at</key><key alias="unpublishDate">Unpublish at</key><key alias="removeDate">Clear Date</key><key alias="setDate">Set date</key><key alias="sortDone">Sortorder is updated</key><key alias="sortHelp">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</key><key alias="statistics">Statistics</key><key alias="titleOptional">Title (optional)</key><key alias="altTextOptional">Alternative text (optional)</key><key alias="type">Type</key><key alias="unpublish">Unpublish</key><key alias="unpublished">Draft</key><key alias="notCreated">Not created</key><key alias="updateDate">Last edited</key><key alias="updateDateDesc" version="7.0">Date/time this document was edited</key><key alias="uploadClear">Remove file(s)</key><key alias="urls">Link to document</key><key alias="memberof">Member of group(s)</key><key alias="notmemberof">Not a member of group(s)</key><key alias="childItems" version="7.0">Child items</key><key alias="target" version="7.0">Target</key><key alias="scheduledPublishServerTime">This translates to the following time on the server:</key><key alias="scheduledPublishDocumentation"><![CDATA[<a href="https://our.umbraco.com/documentation/Getting-Started/Data/Scheduled-Publishing/#timezones" target="_blank">What does this mean?</a>]]></key><key alias="nestedContentDeleteItem">Are you sure you want to delete this item?</key><key alias="nestedContentEditorNotSupported">Property %0% uses editor %1% which is not supported by Nested Content.</key><key alias="addTextBox">Add another text box</key><key alias="removeTextBox">Remove this text box</key><key alias="contentRoot">Content root</key><key alias="isSensitiveValue">This value is hidden. If you need access to view this value please contact your website administrator.</key><key alias="isSensitiveValue_short">This value is hidden.</key><key alias="languagesToPublish">What languages would you like to publish?</key><key alias="languagesToSave">What languages would you like to save?</key><key alias="languagesToSendForApproval">What languages would you like to send for approval?</key><key alias="languagesToUnpublish">Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages.</key><key alias="publishedLanguages">Published Languages</key><key alias="unpublishedLanguages">Unpublished Languages</key><key alias="unmodifiedLanguages">Unmodified Languages</key><key alias="readyToPublish">Ready to Publish?</key><key alias="readyToSave">Ready to Save?</key><key alias="sendForApproval">Send for approval</key></area>
<key alias="publishStatus">Publication Status</key>
<key alias="releaseDate">Publish at</key>
<key alias="unpublishDate">Unpublish at</key>
<key alias="removeDate">Clear Date</key>
<key alias="setDate">Set date</key>
<key alias="sortDone">Sortorder is updated</key>
<key alias="sortHelp">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</key>
<key alias="statistics">Statistics</key>
<key alias="titleOptional">Title (optional)</key>
<key alias="altTextOptional">Alternative text (optional)</key>
<key alias="type">Type</key>
<key alias="unpublish">Unpublish</key>
<key alias="unpublished">Draft</key>
<key alias="notCreated">Not created</key>
<key alias="updateDate">Last edited</key>
<key alias="updateDateDesc" version="7.0">Date/time this document was edited</key>
<key alias="uploadClear">Remove file(s)</key>
<key alias="urls">Link to document</key>
<key alias="memberof">Member of group(s)</key>
<key alias="notmemberof">Not a member of group(s)</key>
<key alias="childItems" version="7.0">Child items</key>
<key alias="target" version="7.0">Target</key>
<key alias="scheduledPublishServerTime">This translates to the following time on the server:</key>
<key alias="scheduledPublishDocumentation"><![CDATA[<a href="https://our.umbraco.com/documentation/Getting-Started/Data/Scheduled-Publishing/#timezones" target="_blank">What does this mean?</a>]]></key><key alias="nestedContentDeleteItem">Are you sure you want to delete this item?</key><key alias="nestedContentEditorNotSupported">Property %0% uses editor %1% which is not supported by Nested Content.</key>
<key alias="addTextBox">Add another text box</key>
<key alias="removeTextBox">Remove this text box</key>
<key alias="contentRoot">Content root</key>
<key alias="isSensitiveValue">This value is hidden. If you need access to view this value please contact your website administrator.</key>
<key alias="isSensitiveValue_short">This value is hidden.</key>
<key alias="languagesToPublish">What languages would you like to publish?</key>
<key alias="languagesToSave">What languages would you like to save?</key>
<key alias="languagesToSendForApproval">What languages would you like to send for approval?</key>
<key alias="languagesToSchedule">What languages would you like to schedule?</key>
<key alias="languagesToUnpublish">Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages.</key>
<key alias="publishedLanguages">Published Languages</key>
<key alias="unpublishedLanguages">Unpublished Languages</key>
<key alias="unmodifiedLanguages">Unmodified Languages</key>
<key alias="readyToPublish">Ready to Publish?</key>
<key alias="readyToSave">Ready to Save?</key>
<key alias="sendForApproval">Send for approval</key>
</area>
<area alias="blueprints">
<key alias="createBlueprintFrom">Create a new Content Template from '%0%'</key>
<key alias="blankBlueprint">Blank</key>