Merge remote-tracking branch 'PerplexDaniel/feature/backoffice-segment-support-client' into v8/feature/AB4550-segments-ui

# Conflicts:
#	src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js
#	src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html
This commit is contained in:
Niels Lyngsø
2020-01-22 09:22:39 +01:00
10 changed files with 268 additions and 192 deletions

View File

@@ -9,7 +9,7 @@
bindings: {
page: "<",
content: "<", // TODO: Not sure if this should be = since we are changing the 'active' property of a variant
culture: "<",
variantId: "<",
onSelectApp: "&?",
onSelectAppAnchor: "&?",
onBack: "&?",
@@ -19,7 +19,7 @@
controller: umbVariantContentEditorsController
};
function umbVariantContentEditorsController($scope, $location, $timeout) {
function umbVariantContentEditorsController($scope, $location, $timeout, variantHelper) {
var prevContentDateUpdated = null;
@@ -40,12 +40,14 @@
//Used to track how many content views there are (for split view there will be 2, it could support more in theory)
vm.editors = [];
//Used to track the open variants across the split views
// The values are the variant ids of the currently open variants.
// See variantHelper.getId() for the current format.
vm.openVariants = [];
/** Called when the component initializes */
function onInit() {
prevContentDateUpdated = angular.copy(vm.content.updateDate);
setActiveCulture();
setActiveVariant();
}
/** Called when the component has linked all elements, this is when the form controller is available */
@@ -59,15 +61,15 @@
*/
function onChanges(changes) {
if (changes.culture && !changes.culture.isFirstChange() && changes.culture.currentValue !== changes.culture.previousValue) {
setActiveCulture();
if (changes.variantId && !changes.variantId.isFirstChange() && changes.variantId.currentValue !== changes.variantId.previousValue) {
setActiveVariant();
}
}
/** Allows us to deep watch whatever we want - executes on every digest cycle */
function doCheck() {
if (!angular.equals(vm.content.updateDate, prevContentDateUpdated)) {
setActiveCulture();
setActiveVariant();
prevContentDateUpdated = angular.copy(vm.content.updateDate);
}
}
@@ -79,13 +81,13 @@
}
/**
* Set the active variant based on the current culture (query string)
* Set the active variant based on the current culture + segment (query string)
*/
function setActiveCulture() {
function setActiveVariant() {
// set the active variant
var activeVariant = null;
var activeVariant = null;
_.each(vm.content.variants, function (v) {
if (v.language && v.language.culture === vm.culture) {
if (variantHelper.getId(v) === vm.variantId) {
v.active = true;
activeVariant = v;
}
@@ -105,9 +107,10 @@
if (vm.editors.length > 1) {
//now re-sync any other editor content (i.e. if split view is open)
for (var s = 1; s < vm.editors.length; s++) {
var editorVariantId = variantHelper.getId(vm.editors[s].content);
//get the variant from the scope model
var variant = _.find(vm.content.variants, function (v) {
return v.language.culture === vm.editors[s].content.language.culture;
return variantHelper.getId(v) === editorVariantId;
});
vm.editors[s].content = initVariant(variant, s);
}
@@ -122,19 +125,19 @@
*/
function insertVariantEditor(index, variant) {
var variantCulture = variant.language ? variant.language.culture : "invariant";
var variantId = variantHelper.getId(variant);
//check if the culture at the index is the same, if it's null an editor will be added
var currentCulture = vm.editors.length === 0 || vm.editors.length <= index ? null : vm.editors[index].culture;
//check if the variant at the index is the same, if it's null an editor will be added
var currentVariantId = vm.editors.length === 0 || vm.editors.length <= index ? null : vm.editors[index].variantId;
if (currentCulture !== variantCulture) {
if (currentVariantId !== variantId) {
//Not the current culture which means we need to modify the array.
//NOTE: It is not good enough to just replace the `content` object at a given index in the array
// since that would mean that directives are not re-initialized.
vm.editors.splice(index, 1, {
content: variant,
//used for "track-by" ng-repeat
culture: variantCulture
variantId: variantId
});
}
else {
@@ -161,7 +164,7 @@
if (!variant.variants) {
variant.variants = _.map(vm.content.variants,
function (v) {
return _.pick(v, "active", "language", "state");
return _.pick(v, "active", "language", "segment", "state");
});
}
else {
@@ -169,13 +172,14 @@
angular.extend(variant.variants,
_.map(vm.content.variants,
function (v) {
return _.pick(v, "active", "language", "state");
return _.pick(v, "active", "language", "segment", "state");
}));
}
//ensure the current culture is set as the active one
for (var i = 0; i < variant.variants.length; i++) {
if (variant.variants[i].language.culture === variant.language.culture) {
if (variant.variants[i].language.culture === variant.language.culture &&
variant.variants[i].segment === variant.segment) {
variant.variants[i].active = true;
}
else {
@@ -183,12 +187,13 @@
}
}
var variantId = variantHelper.getId(variant);
// keep track of the open variants across the different split views
// push the first variant then update the variant index based on the editor index
if(vm.openVariants && vm.openVariants.length === 0) {
vm.openVariants.push(variant.language.culture);
if (vm.openVariants && vm.openVariants.length === 0) {
vm.openVariants.push(variantId);
} else {
vm.openVariants[editorIndex] = variant.language.culture;
vm.openVariants[editorIndex] = variantId;
}
}
@@ -221,11 +226,11 @@
* @param {any} selectedVariant
*/
function openSplitView(selectedVariant) {
var selectedCulture = selectedVariant.language.culture;
var variant = variantHelper.getId(selectedVariant);
//Find the whole variant model based on the culture that was chosen
var variant = _.find(vm.content.variants, function (v) {
return v.language.culture === selectedCulture;
return variantHelper.getId(v) === variant;
});
insertVariantEditor(vm.editors.length, initVariant(variant, vm.editors.length));
@@ -282,8 +287,10 @@
*/
function selectVariant(variant, editorIndex) {
var variantId = variantHelper.getId(variant);
// prevent variants already open in a split view to be opened
if(vm.openVariants.indexOf(variant.language.culture) !== -1) {
if (vm.openVariants.indexOf(variantId) !== -1) {
return;
}
@@ -292,7 +299,7 @@
if (editorIndex === 0) {
//If we've made it this far, then update the query string.
//The editor will respond to this query string changing.
$location.search("cculture", variant.language.culture);
$location.search("cculture", variantId);
}
else {
@@ -307,7 +314,7 @@
//get the variant content model and initialize the editor with that
var contentVariant = _.find(vm.content.variants,
function (v) {
return v.language.culture === variant.language.culture;
return variantHelper.getId(v) === variantId;
});
editor.content = initVariant(contentVariant, editorIndex);

View File

@@ -1,12 +1,12 @@
(function () {
'use strict';
function EditorContentHeader(serverValidationManager, localizationService, editorState) {
function EditorContentHeader(serverValidationManager, localizationService, editorState, variantHelper) {
function link(scope, el, attr, ctrl) {
var unsubscribe = [];
if (!scope.serverValidationNameField) {
scope.serverValidationNameField = "Name";
}
@@ -15,36 +15,30 @@
}
scope.isNew = scope.content.state == "NotCreated";
localizationService.localizeMany([
scope.isNew ? "visuallyHiddenTexts_createItem" : "visuallyHiddenTexts_edit",
"visuallyHiddenTexts_name",
scope.isNew ? "general_new" : "general_edit"]
).then(function (data) {
localizationService.localizeMany([
scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit",
"placeholders_a11yName"]
).then(function (data) {
scope.a11yMessage = data[0];
scope.a11yName = data[1];
var title = data[2] + ": ";
if (!scope.isNew) {
scope.a11yMessage += " " + scope.content.name;
title += scope.content.name;
} else {
var name = editorState.current.contentTypeName;
scope.a11yMessage += " " + name;
scope.a11yName = name + " " + scope.a11yName;
title += name;
}
scope.$emit("$changeTitle", title);
});
scope.vm = {};
scope.vm.dropdownOpen = false;
scope.vm.currentVariant = "";
scope.vm.variantsWithError = [];
scope.vm.defaultVariant = null;
scope.vm.errorsOnOtherVariants = false;// indicating wether to show that other variants, than the current, have errors.
function checkErrorsOnOtherVariants() {
var check = false;
angular.forEach(scope.content.variants, function (variant) {
@@ -54,10 +48,10 @@
});
scope.vm.errorsOnOtherVariants = check;
}
function onCultureValidation(valid, errors, allErrors, culture) {
var index = scope.vm.variantsWithError.indexOf(culture);
if (valid === true) {
if(valid === true) {
if (index !== -1) {
scope.vm.variantsWithError.splice(index, 1);
}
@@ -68,165 +62,168 @@
}
checkErrorsOnOtherVariants();
}
function onInit() {
// find default.
angular.forEach(scope.content.variants, function (variant) {
if (variant.language.isDefault) {
scope.vm.defaultVariant = variant;
}
});
setCurrentVariant();
angular.forEach(scope.content.apps, (app) => {
if (app.alias === "umbContent") {
app.anchors = scope.content.tabs;
}
});
angular.forEach(scope.content.variants, function (variant) {
unsubscribe.push(serverValidationManager.subscribe(null, variant.language.culture, null, onCultureValidation));
});
unsubscribe.push(serverValidationManager.subscribe(null, null, null, onCultureValidation));
}
function setCurrentVariant() {
angular.forEach(scope.content.variants, function (variant) {
if (variant.active) {
scope.vm.currentVariant = variant;
checkErrorsOnOtherVariants();
}
});
}
angular.forEach(scope.content.variants, function (variant) {
unsubscribe.push(serverValidationManager.subscribe(null, variant.language.culture, null, onCultureValidation));
});
scope.getVariantDisplayName = variantHelper.getDisplayName;
unsubscribe.push(serverValidationManager.subscribe(null, null, null, onCultureValidation));
}
function setCurrentVariant() {
angular.forEach(scope.content.variants, function (variant) {
if (variant.active) {
scope.vm.currentVariant = variant;
checkErrorsOnOtherVariants();
scope.goBack = function () {
if (scope.onBack) {
scope.onBack();
}
});
}
};
scope.goBack = function () {
if (scope.onBack) {
scope.onBack();
}
};
scope.selectVariant = function (event, variant) {
scope.selectVariant = function (event, variant) {
if (scope.onSelectVariant) {
scope.vm.dropdownOpen = false;
scope.onSelectVariant({ "variant": variant });
}
};
if (scope.onSelectVariant) {
scope.vm.dropdownOpen = false;
scope.onSelectVariant({ "variant": variant });
}
};
scope.selectNavigationItem = function (item) {
if (scope.onSelectNavigationItem) {
scope.onSelectNavigationItem({ "item": item });
}
}
scope.selectAnchorItem = function (item, anchor) {
if (scope.onSelectAnchorItem) {
scope.onSelectAnchorItem({ "item": item, "anchor": anchor });
}
}
scope.closeSplitView = function () {
if (scope.onCloseSplitView) {
scope.onCloseSplitView();
}
};
scope.openInSplitView = function (event, variant) {
if (scope.onOpenInSplitView) {
scope.vm.dropdownOpen = false;
scope.onOpenInSplitView({ "variant": variant });
}
};
/**
* keep track of open variants - this is used to prevent the same variant to be open in more than one split view
* @param {any} culture
*/
scope.variantIsOpen = function (culture) {
return (scope.openVariants.indexOf(culture) !== -1);
}
/**
* Check whether a variant has a error, used to display errors in variant switcher.
* @param {any} culture
*/
scope.variantHasError = function (culture) {
// if we are looking for the default language we also want to check for invariant.
if (culture === scope.vm.defaultVariant.language.culture) {
if (scope.vm.variantsWithError.indexOf("invariant") !== -1) {
return true;
scope.selectNavigationItem = function(item) {
if(scope.onSelectNavigationItem) {
scope.onSelectNavigationItem({"item": item});
}
}
if (scope.vm.variantsWithError.indexOf(culture) !== -1) {
return true;
scope.selectAnchorItem = function(item, anchor) {
if(scope.onSelectAnchorItem) {
scope.onSelectAnchorItem({"item": item, "anchor": anchor});
}
}
return false;
}
onInit();
scope.closeSplitView = function () {
if (scope.onCloseSplitView) {
scope.onCloseSplitView();
}
};
//watch for the active culture changing, if it changes, update the current variant
if (scope.content.variants) {
scope.$watch(function () {
for (var i = 0; i < scope.content.variants.length; i++) {
var v = scope.content.variants[i];
if (v.active) {
return v.language.culture;
scope.openInSplitView = function (event, variant) {
if (scope.onOpenInSplitView) {
scope.vm.dropdownOpen = false;
scope.onOpenInSplitView({ "variant": variant });
}
};
/**
* keep track of open variants - this is used to prevent the same variant to be open in more than one split view
* @param {any} culture
*/
scope.variantIsOpen = function (variant) {
var variantId = variantHelper.getId(variant);
return (scope.openVariants.indexOf(variantId) !== -1);
}
/**
* Check whether a variant has a error, used to display errors in variant switcher.
* @param {any} culture
*/
scope.variantHasError = function(culture) {
// if we are looking for the default language we also want to check for invariant.
if (culture === scope.vm.defaultVariant.language.culture) {
if(scope.vm.variantsWithError.indexOf("invariant") !== -1) {
return true;
}
}
return scope.vm.currentVariant.language.culture; //should never get here
}, function (newValue, oldValue) {
if (newValue !== scope.vm.currentVariant.language.culture) {
setCurrentVariant();
if(scope.vm.variantsWithError.indexOf(culture) !== -1) {
return true;
}
return false;
}
onInit();
//watch for the active culture changing, if it changes, update the current variant
if (scope.content.variants) {
scope.$watch(function () {
for (var i = 0; i < scope.content.variants.length; i++) {
var v = scope.content.variants[i];
if (v.active) {
return v.language.culture;
}
}
return scope.vm.currentVariant.language.culture; //should never get here
}, function (newValue, oldValue) {
if (newValue !== scope.vm.currentVariant.language.culture) {
setCurrentVariant();
}
});
}
scope.$on('$destroy', function () {
for (var u in unsubscribe) {
unsubscribe[u]();
}
});
}
scope.$on('$destroy', function () {
for (var u in unsubscribe) {
unsubscribe[u]();
}
});
var directive = {
transclude: true,
restrict: 'E',
replace: true,
templateUrl: 'views/components/editor/umb-editor-content-header.html',
scope: {
name: "=",
nameDisabled: "<?",
menu: "=",
hideActionsMenu: "<?",
content: "=",
openVariants: "<",
hideChangeVariant: "<?",
onSelectNavigationItem: "&?",
onSelectAnchorItem: "&?",
showBackButton: "<?",
onBack: "&?",
splitViewOpen: "=?",
onOpenInSplitView: "&?",
onCloseSplitView: "&?",
onSelectVariant: "&?",
serverValidationNameField: "@?",
serverValidationAliasField: "@?"
},
link: link
};
return directive;
}
var directive = {
transclude: true,
restrict: 'E',
replace: true,
templateUrl: 'views/components/editor/umb-editor-content-header.html',
scope: {
name: "=",
nameDisabled: "<?",
menu: "=",
hideActionsMenu: "<?",
content: "=",
openVariants: "<",
hideChangeVariant: "<?",
onSelectNavigationItem: "&?",
onSelectAnchorItem: "&?",
showBackButton: "<?",
onBack: "&?",
splitViewOpen: "=?",
onOpenInSplitView: "&?",
onCloseSplitView: "&?",
onSelectVariant: "&?",
serverValidationNameField: "@?",
serverValidationAliasField: "@?"
},
link: link
};
return directive;
}
angular.module('umbraco.directives').directive('umbEditorContentHeader', EditorContentHeader);
}) ();
})();

View File

@@ -368,6 +368,7 @@
name: v.name || "", //if its null/empty,we must pass up an empty string else we get json converter errors
properties: getContentProperties(v.tabs),
culture: v.language ? v.language.culture : null,
segment: v.segment,
publish: v.publish,
save: v.save,
releaseDate: v.releaseDate,
@@ -409,7 +410,7 @@
_.each(tab.properties, function (property, propIndex) {
//in theory if there's more than 1 variant, that means they would all have a language
//but we'll do our safety checks anyways here
if (firstVariant.language && !property.culture) {
if (firstVariant.language && !property.culture && !property.segment) {
invariantProperties.push({
tabIndex: tabIndex,
propIndex: propIndex,
@@ -425,7 +426,15 @@
var variant = displayModel.variants[j];
_.each(invariantProperties, function (invProp) {
variant.tabs[invProp.tabIndex].properties[invProp.propIndex] = invProp.property;
var tab = variant.tabs[invProp.tabIndex];
var prop = tab.properties[invProp.propIndex];
if (prop.segment) {
// Do not touch segmented properties
return;
}
tab.properties[invProp.propIndex] = invProp.property;
});
}
}

View File

@@ -0,0 +1,61 @@
/**
* @ngdoc service
* @name umbraco.services.variantHelper
* @description A helper service for dealing with variants
**/
function variantHelper() {
/**
* Returns the id for this variant
* @param {any} variant
*/
function getId(variant) {
var hasLanguage = variant.language && !!variant.language.culture;
var hasSegment = !!variant.segment;
var sep = ";";
if (!hasLanguage && !hasSegment) {
// Invariant
return "";
} else if (hasLanguage && !hasSegment) {
// Culture only
return variant.language.culture;
} else if (!hasLanguage && hasSegment) {
// Segment only
return sep + variant.segment;
} else {
// Culture and Segment
return variant.language.culture + sep + variant.segment;
}
}
function getDisplayName(variant) {
if (variant == null) {
return "";
}
var parts = [];
if (variant.language && variant.language.name) {
parts.push(variant.language.name);
}
if (variant.segment) {
var capitalized = variant.segment.split(" ").map(p => p[0].toUpperCase() + p.substring(1)).join(" ");
parts.push(capitalized);
}
if (parts.length === 0) {
// Invariant
parts.push("Default");
}
return parts.join(" - ");
}
return {
getId,
getDisplayName
}
}
angular.module('umbraco.services').factory('variantHelper', variantHelper);

View File

@@ -9,7 +9,7 @@
<umb-variant-content-editors
page="page"
content="content"
culture="culture"
variant-id="culture"
on-select-app="appChanged(app)"
on-select-app-anchor="appAnchorChanged(app, anchor)"
on-back="onBack()"

View File

@@ -11,13 +11,13 @@
data-element="property-{{property.alias}}"
ng-repeat="property in group.properties track by property.alias"
property="property"
show-inherit="content.variants.length > 1 && !property.culture && !activeVariant.language.isDefault"
show-inherit="content.variants.length > 1 && ((!activeVariant.language.isDefault && !property.culture) || (activeVariant.segment && !property.segment)) && !property.unlockInvariantValue"
inherits-from="defaultVariant.language.name">
<div ng-class="{'o-40 cursor-not-allowed': content.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue}">
<div ng-class="{'o-40 cursor-not-allowed': content.variants.length > 1 && ((!activeVariant.language.isDefault && !property.culture) || (activeVariant.segment && !property.segment)) && !property.unlockInvariantValue}">
<umb-property-editor
model="property"
preview="content.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue">
preview="content.variants.length > 1 && ((!activeVariant.language.isDefault && !property.culture) || (activeVariant.segment && !property.segment)) && !property.unlockInvariantValue">
</umb-property-editor>
</div>

View File

@@ -1,6 +1,6 @@
<div class="umb-split-views">
<div class="umb-split-view"
ng-repeat="editor in vm.editors track by editor.culture"
ng-repeat="editor in vm.editors track by editor.variantId"
ng-class="{'umb-split-view--collapsed': editor.collapsed}">
<umb-variant-content

View File

@@ -39,21 +39,21 @@
autocomplete="off" maxlength="255" />
</ng-form>
<button type="button" ng-if="content.variants.length > 0 && hideChangeVariant !== true" class="umb-variant-switcher__toggle umb-outline" href="" ng-click="vm.dropdownOpen = !vm.dropdownOpen" ng-class="{'--error': vm.errorsOnOtherVariants}">
<span>{{vm.currentVariant.language.name}}</span>
<a ng-if="content.variants.length > 0 && hideChangeVariant !== true" class="umb-variant-switcher__toggle" href="" ng-click="vm.dropdownOpen = !vm.dropdownOpen" ng-class="{'--error': vm.errorsOnOtherVariants}">
<span ng-bind="getVariantDisplayName(vm.currentVariant)"></span>
<ins class="umb-variant-switcher__expand" ng-class="{'icon-navigation-down': !vm.dropdownOpen, 'icon-navigation-up': vm.dropdownOpen}">&nbsp;</ins>
</button>
</a>
<span ng-if="hideChangeVariant" class="umb-variant-switcher__toggle">
<span>{{vm.currentVariant.language.name}}</span>
<span ng-bind="getVariantDisplayName(variant)"></span>
</span>
<umb-dropdown ng-if="vm.dropdownOpen" style="min-width: 100%; max-height: 250px; overflow-y: auto; margin-top: 5px;" on-close="vm.dropdownOpen = false" umb-keyboard-list>
<umb-dropdown-item class="umb-variant-switcher__item" ng-class="{'--current': variant.active, '--not-allowed': variantIsOpen(variant.language.culture), '--error': variantHasError(variant.language.culture)}" ng-repeat="variant in content.variants">
<button class="umb-variant-switcher__name-wrapper umb-outline umb-outline--thin" ng-click="selectVariant($event, variant)" prevent-default>
<span class="umb-variant-switcher__name">{{variant.language.name}}</span>
<umb-dropdown-item class="umb-variant-switcher__item" ng-class="{'--current': variant.active, '--not-allowed': variantIsOpen(variant), '--error': variantHasError(variant.language.culture)}" ng-repeat="variant in content.variants">
<a href="" class="umb-variant-switcher__name-wrapper" ng-click="selectVariant($event, variant)" prevent-default>
<span class="umb-variant-switcher__name" ng-bind="getVariantDisplayName(variant)"></span>
<umb-variant-state variant="variant" class="umb-variant-switcher__state"></umb-variant-state>
</button>
</a>
<div ng-if="splitViewOpen !== true && !variant.active" class="umb-variant-switcher__split-view" ng-click="openInSplitView($event, variant)">Open in split view</div>
</umb-dropdown-item>
</umb-dropdown>

View File

@@ -1,7 +1,7 @@
(function () {
"use strict";
function PublishController($scope, localizationService, contentEditingHelper) {
function PublishController($scope, localizationService, contentEditingHelper, variantHelper) {
var vm = this;
vm.loading = true;
@@ -12,6 +12,8 @@
vm.dirtyVariantFilter = dirtyVariantFilter;
vm.pristineVariantFilter = pristineVariantFilter;
$scope.getVariantDisplayName = variantHelper.getDisplayName;
/** Returns true if publishing is possible based on if there are un-published mandatory languages */
function canPublish() {

View File

@@ -20,8 +20,8 @@
model="variant.publish"
on-change="vm.changeSelection(variant)"
disabled="(variant.canPublish === false)"
server-validation-field="{{variant.htmlId}}"
text="{{ variant.language.name }}"
server-validation-field="{{variant.htmlId}}"
text="{{getVariantDisplayName(variant)}}"
/>
<div>
<span class="db umb-list-item__description umb-list-item__description--checkbox" ng-if="!publishVariantSelectorForm.publishVariantSelector.$invalid && !(variant.notifications && variant.notifications.length > 0)">
@@ -51,7 +51,7 @@
<div class="umb-list-item" ng-repeat="variant in vm.variants | filter:vm.pristineVariantFilter">
<div>
<div style="margin-bottom: 2px;">
<span>{{variant.language.name}}</span>
<span ng-bind="getVariantDisplayName(variant)"></span>
<strong ng-if="variant.language.isMandatory" class="umb-control-required">*</strong>
</div>