diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index 33dabe1b2435..92d397520e2c 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -134,15 +134,16 @@ public static IEnumerable BuildDtos(ContentVariation contentVar // publishing = deal with edit and published values foreach (var propertyValue in property.Values) { - var isInvariantValue = propertyValue.Culture == null; - var isCultureValue = propertyValue.Culture != null && propertyValue.Segment == null; + var isInvariantValue = propertyValue.Culture == null && propertyValue.Segment == null; + var isCultureValue = propertyValue.Culture != null; + var isSegmentValue = propertyValue.Segment != null; // deal with published value - if (propertyValue.PublishedValue != null && publishedVersionId > 0) + if ((propertyValue.PublishedValue != null || isSegmentValue) && publishedVersionId > 0) propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue)); // deal with edit value - if (propertyValue.EditedValue != null) + if (propertyValue.EditedValue != null || isSegmentValue) propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the diff --git a/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs b/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs index 3ce43b5fc221..8d285e0375f8 100644 --- a/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs +++ b/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs @@ -63,7 +63,7 @@ public void Add_Invariant_Property_Error() ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null); //invariant property - Assert.AreEqual("_Properties.headerImage.invariant", ms.Keys.First()); + Assert.AreEqual("_Properties.headerImage.invariant.null", ms.Keys.First()); } [Test] @@ -73,9 +73,57 @@ public void Add_Variant_Property_Error() var localizationService = new Mock(); localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US"); - ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", "en-US"); //invariant property + ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", "en-US"); //variant property - Assert.AreEqual("_Properties.headerImage.en-US", ms.Keys.First()); + Assert.AreEqual("_Properties.headerImage.en-US.null", ms.Keys.First()); + } + + [Test] + public void Add_Invariant_Segment_Property_Error() + { + var ms = new ModelStateDictionary(); + var localizationService = new Mock(); + localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US"); + + ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null, "mySegment"); //invariant/segment property + + Assert.AreEqual("_Properties.headerImage.invariant.mySegment", ms.Keys.First()); + } + + [Test] + public void Add_Variant_Segment_Property_Error() + { + var ms = new ModelStateDictionary(); + var localizationService = new Mock(); + localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US"); + + ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", "en-US", "mySegment"); //variant/segment property + + Assert.AreEqual("_Properties.headerImage.en-US.mySegment", ms.Keys.First()); + } + + [Test] + public void Add_Invariant_Segment_Field_Property_Error() + { + var ms = new ModelStateDictionary(); + var localizationService = new Mock(); + localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US"); + + ms.AddPropertyError(new ValidationResult("no header image", new[] { "myField" }), "headerImage", null, "mySegment"); //invariant/segment property + + Assert.AreEqual("_Properties.headerImage.invariant.mySegment.myField", ms.Keys.First()); + } + + [Test] + public void Add_Variant_Segment_Field_Property_Error() + { + var ms = new ModelStateDictionary(); + var localizationService = new Mock(); + localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US"); + + ms.AddPropertyError(new ValidationResult("no header image", new[] { "myField" }), "headerImage", "en-US", "mySegment"); //variant/segment property + + Assert.AreEqual("_Properties.headerImage.en-US.mySegment.myField", ms.Keys.First()); } } } 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 d674b3d1e8c3..0b137a5fbed4 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 @@ -31,7 +31,7 @@ $scope.page.hideActionsMenu = infiniteMode ? true : false; $scope.page.hideChangeVariant = false; $scope.allowOpen = true; - $scope.app = null; + $scope.activeApp = null; //initializes any watches function startWatches(content) { @@ -74,31 +74,23 @@ var isAppPresent = false; // on first init, we dont have any apps. but if we are re-initializing, we do, but ... - if ($scope.app) { + if ($scope.activeApp) { - // lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.) _.forEach(content.apps, function (app) { - if (app === $scope.app) { + if (app.alias === $scope.activeApp.alias) { isAppPresent = true; + $scope.appChanged(app); } }); - // if we did reload our DocType, but still have the same app we will try to find it by the alias. if (isAppPresent === false) { - _.forEach(content.apps, function (app) { - if (app.alias === $scope.app.alias) { - isAppPresent = true; - app.active = true; - $scope.appChanged(app); - } - }); + // active app does not exist anymore. + $scope.activeApp = null; } - } // if we still dont have a app, lets show the first one: - if (isAppPresent === false && content.apps.length) { - content.apps[0].active = true; + if ($scope.activeApp === null && content.apps.length) { $scope.appChanged(content.apps[0]); } // otherwise make sure the save options are up to date with the current content state @@ -151,8 +143,8 @@ } /** Returns true if the content item varies by culture */ - function isContentCultureVariant() { - return $scope.content.variants.length > 1; + function hasVariants(content) { + return content.variants.length > 1; } function reload() { @@ -215,6 +207,13 @@ })); } + function appendRuntimeData() { + $scope.content.variants.forEach((variant) => { + variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); + variant.htmlId = "_content_variant_" + variant.compositeId; + }); + } + /** * This does the content loading and initializes everything, called on first load */ @@ -226,6 +225,7 @@ $scope.content = data; + appendRuntimeData(); init(); syncTreeNode($scope.content, $scope.content.path, true); @@ -251,6 +251,7 @@ $scope.content = data; + appendRuntimeData(); init(); startWatches($scope.content); @@ -274,7 +275,7 @@ $scope.page.saveButtonStyle = content.trashed || content.isElement || content.isBlueprint ? "primary" : "info"; // only create the save/publish/preview buttons if the // content app is "Conent" - if ($scope.app && $scope.app.alias !== "umbContent" && $scope.app.alias !== "umbInfo" && $scope.app.alias !== "umbListView") { + if ($scope.activeApp && $scope.activeApp.alias !== "umbContent" && $scope.activeApp.alias !== "umbInfo" && $scope.activeApp.alias !== "umbListView") { $scope.defaultButton = null; $scope.subButtons = null; $scope.page.showSaveButton = false; @@ -589,7 +590,7 @@ $scope.sendToPublish = function () { clearNotifications($scope.content); - if (isContentCultureVariant()) { + if (hasVariants($scope.content)) { //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "publish" })) { @@ -649,7 +650,7 @@ $scope.saveAndPublish = function () { clearNotifications($scope.content); - if (isContentCultureVariant()) { + if (hasVariants($scope.content)) { //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "publish" })) { var dialog = { @@ -711,7 +712,7 @@ $scope.save = function () { clearNotifications($scope.content); // TODO: Add "..." to save button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant - if (isContentCultureVariant()) { + if (hasVariants($scope.content)) { //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "openSaveDialog" })) { @@ -776,7 +777,7 @@ clearNotifications($scope.content); //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "schedule" })) { - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { //ensure the flags are set $scope.content.variants[0].save = true; } @@ -813,7 +814,7 @@ }, function (err) { clearDirtyState($scope.content.variants); //if this is invariant, show the notification errors, else they'll be shown inline with the variant - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { formHelper.showNotifications(err.data); } model.submitButtonState = "error"; @@ -840,7 +841,7 @@ //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "publishDescendants" })) { - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { //ensure the flags are set $scope.content.variants[0].save = true; $scope.content.variants[0].publish = true; @@ -873,7 +874,7 @@ }, function (err) { clearDirtyState($scope.content.variants); //if this is invariant, show the notification errors, else they'll be shown inline with the variant - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { formHelper.showNotifications(err.data); } model.submitButtonState = "error"; @@ -963,11 +964,18 @@ * Call back when a content app changes * @param {any} app */ - $scope.appChanged = function (app) { - - $scope.app = app; + $scope.appChanged = function (activeApp) { + + $scope.activeApp = activeApp; + + _.forEach($scope.content.apps, function (app) { + app.active = false; + if (app.alias === $scope.activeApp.alias) { + app.active = true; + } + }); - $scope.$broadcast("editors.apps.appChanged", { app: app }); + $scope.$broadcast("editors.apps.appChanged", { app: activeApp }); createButtons($scope.content); @@ -1029,6 +1037,7 @@ getMethod: "&", getScaffoldMethod: "&?", culture: "=?", + segment: "=?", infiniteModel: "=?" } }; 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 95b2a520d140..1e929af6e9bb 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 @@ -323,7 +323,7 @@ // find the urls for the currently selected language if (scope.node.variants.length > 1) { // nodes with variants - scope.currentUrls = _.filter(scope.node.urls, (url) => scope.currentVariant.language.culture === url.culture); + scope.currentUrls = _.filter(scope.node.urls, (url) => (scope.currentVariant.language && scope.currentVariant.language.culture === url.culture)); } else { // invariant nodes scope.currentUrls = scope.node.urls; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js index 06f426889f11..3aa0470262b0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js @@ -4,7 +4,7 @@ /** This directive is used to render out the current variant tabs and properties and exposes an API for other directives to consume */ function tabbedContentDirective($timeout) { - function link($scope, $element, $attrs) { + function link($scope, $element) { var appRootNode = $element[0]; @@ -115,21 +115,18 @@ } - function controller($scope, $element, $attrs) { - + function controller($scope) { //expose the property/methods for other directives to use this.content = $scope.content; - this.activeVariant = _.find(this.content.variants, variant => { - return variant.active; - }); - - $scope.activeVariant = this.activeVariant; - - $scope.defaultVariant = _.find(this.content.variants, variant => { - return variant.language.isDefault; - }); - + + if($scope.contentNodeModel) { + $scope.defaultVariant = _.find($scope.contentNodeModel.variants, variant => { + // defaultVariant will never have segment. Wether it has a language or not depends on the setup. + return !variant.segment && ((variant.language && variant.language.isDefault) || (!variant.language)); + }); + } + $scope.unlockInvariantValue = function(property) { property.unlockInvariantValue = !property.unlockInvariantValue; }; @@ -141,6 +138,24 @@ } } ); + + $scope.propertyEditorDisabled = function (property) { + if (property.unlockInvariantValue) { + return false; + } + + var contentLanguage = $scope.content.language; + + var canEditCulture = !contentLanguage || + // If the property culture equals the content culture it can be edited + property.culture === contentLanguage.culture || + // A culture-invariant property can only be edited by the default language variant + (property.culture == null && contentLanguage.isDefault); + + var canEditSegment = property.segment === $scope.content.segment; + + return !canEditCulture || !canEditSegment; + } } var directive = { @@ -150,7 +165,8 @@ controller: controller, link: link, scope: { - content: "=" + content: "=", // in this context the content is the variant model. + contentNodeModel: "=?" //contentNodeModel is the content model for the node, } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js index 6fec20b256c6..2d3a8e223859 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js @@ -12,7 +12,6 @@ editor: "<", editorIndex: "<", editorCount: "<", - openVariants: "<", onCloseSplitView: "&", onSelectVariant: "&", onOpenSplitView: "&", @@ -25,7 +24,7 @@ controller: umbVariantContentController }; - function umbVariantContentController($scope, $element, $location) { + function umbVariantContentController($scope) { var unsubscribe = []; @@ -42,13 +41,14 @@ vm.showBackButton = showBackButton; function onInit() { - // disable the name field if the active content app is not "Content" - vm.nameDisabled = false; - angular.forEach(vm.editor.content.apps, function(app){ - if(app.active && app.alias !== "umbContent" && app.alias !== "umbInfo" && app.alias !== "umbListView") { - vm.nameDisabled = true; - } - }); + + // Make copy of apps, so we can have a variant specific model for the App. (needed for validation etc.) + vm.editor.variantApps = Utilities.copy(vm.content.apps); + + var activeApp = vm.content.apps.find((app) => app.active); + + onAppChanged(activeApp); + } function showBackButton() { @@ -94,14 +94,23 @@ } $scope.$on("editors.apps.appChanged", function($event, $args) { - var app = $args.app; - // disable the name field if the active content app is not "Content" or "Info" - vm.nameDisabled = false; - if(app && app.alias !== "umbContent" && app.alias !== "umbInfo" && app.alias !== "umbListView") { - vm.nameDisabled = true; - } + var activeApp = $args.app; + + // sync varaintApps active with new active. + _.forEach(vm.editor.variantApps, function (app) { + app.active = (app.alias === activeApp.alias); + }); + + onAppChanged(activeApp); }); + function onAppChanged(activeApp) { + + // disable the name field if the active content app is not "Content" or "Info" + vm.nameDisabled = (activeApp && activeApp.alias !== "umbContent" && activeApp.alias !== "umbInfo" && activeApp.alias !== "umbListView"); + + } + /** * Used to proxy a callback * @param {any} item diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index 6aba1e675880..a188a83d83de 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -8,8 +8,9 @@ templateUrl: 'views/components/content/umb-variant-content-editors.html', bindings: { page: "<", - content: "<", // TODO: Not sure if this should be = since we are changing the 'active' property of a variant + content: "<", culture: "<", + segment: "<", onSelectApp: "&?", onSelectAppAnchor: "&?", onBack: "&?", @@ -19,12 +20,11 @@ controller: umbVariantContentEditorsController }; - function umbVariantContentEditorsController($scope, $location, $timeout) { + function umbVariantContentEditorsController($scope, $location, contentEditingHelper) { var prevContentDateUpdated = null; var vm = this; - var activeAppAlias = null; vm.$onInit = onInit; vm.$onChanges = onChanges; @@ -39,13 +39,11 @@ //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 - vm.openVariants = []; /** Called when the component initializes */ function onInit() { prevContentDateUpdated = Utilities.copy(vm.content.updateDate); - setActiveCulture(); + setActiveVariant(); } /** Called when the component has linked all elements, this is when the form controller is available */ @@ -60,14 +58,16 @@ function onChanges(changes) { if (changes.culture && !changes.culture.isFirstChange() && changes.culture.currentValue !== changes.culture.previousValue) { - setActiveCulture(); + setActiveVariant(); + } else if (changes.segment && !changes.segment.isFirstChange() && changes.segment.currentValue !== changes.segment.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 = Utilities.copy(vm.content.updateDate); } } @@ -79,37 +79,32 @@ } /** - * Set the active variant based on the current culture (query string) + * Set the active variant based on the current culture or segment (query string) */ - function setActiveCulture() { + function setActiveVariant() { // set the active variant var activeVariant = null; _.each(vm.content.variants, function (v) { - if (v.language && v.language.culture === vm.culture) { - v.active = true; + if ((vm.culture === "invariant" || v.language && v.language.culture === vm.culture) && v.segment === vm.segment) { activeVariant = v; } - else { - v.active = false; - } }); if (!activeVariant) { // Set the first variant to active if we can't find it. // If the content item is invariant, then only one item exists in the array. - vm.content.variants[0].active = true; activeVariant = vm.content.variants[0]; } - insertVariantEditor(0, initVariant(activeVariant, 0)); + insertVariantEditor(0, activeVariant); 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++) { //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 (!v.language || v.language.culture === vm.editors[s].content.language.culture) && v.segment === vm.editors[s].content.segment; }); - vm.editors[s].content = initVariant(variant, s); + vm.editors[s].content = variant; } } @@ -122,157 +117,84 @@ */ function insertVariantEditor(index, variant) { + if (vm.editors[index]) { + if (vm.editors[index].content === variant) { + // This variant is already the content of the editor in this index. + return; + } + vm.editors[index].content.active = false; + } + variant.active = true; + var variantCulture = variant.language ? variant.language.culture : "invariant"; + var variantSegment = variant.segment; - //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; + var currentCulture = index < vm.editors.length ? vm.editors[index].culture : null; + var currentSegment = index < vm.editors.length ? vm.editors[index].segment : null; + + // if index not already exists or if the culture or segment isnt identical then we do a replacement. + if (index >= vm.editors.length || currentCulture !== variantCulture || currentSegment !== variantSegment) { - if (currentCulture !== variantCulture) { - //Not the current culture which means we need to modify the array. + //Not the current culture or segment 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, { + compositeId: variant.compositeId, content: variant, - //used for "track-by" ng-repeat - culture: variantCulture + culture: variantCulture, + segment: variantSegment }); } else { - //replace the editor for the same culture + //replace the content of the editor, since the culture and segment is the same. vm.editors[index].content = variant; } + } - - function initVariant(variant, editorIndex) { - //The model that is assigned to the editor contains the current content variant along - //with a copy of the contentApps. This is required because each editor renders it's own - //header and content apps section and the content apps contains the view for editing content itself - //and we need to assign a view model to the subView so that it is scoped to the current - //editor so that split views work. - - //copy the apps from the main model if not assigned yet to the variant - if (!variant.apps) { - variant.apps = Utilities.copy(vm.content.apps); - } - - //if this is a variant has a culture/language than we need to assign the language drop down info - if (variant.language) { - //if the variant list that defines the header drop down isn't assigned to the variant then assign it now - if (!variant.variants) { - variant.variants = _.map(vm.content.variants, - function (v) { - return _.pick(v, "active", "language", "state"); - }); - } - else { - //merge the scope variants on top of the header variants collection (handy when needing to refresh) - angular.extend(variant.variants, - _.map(vm.content.variants, - function (v) { - return _.pick(v, "active", "language", "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) { - variant.variants[i].active = true; - } - else { - variant.variants[i].active = false; - } - } - - // 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); - } else { - vm.openVariants[editorIndex] = variant.language.culture; - } - - } - - //then assign the variant to a view model to the content app - var contentApp = _.find(variant.apps, function (a) { - return a.alias === "umbContent"; - }); - - if (contentApp) { - //The view model for the content app is simply the index of the variant being edited - var variantIndex = vm.content.variants.indexOf(variant); - contentApp.viewModel = variantIndex; - } - - // make sure the same app it set to active in the new variant - if (activeAppAlias) { - angular.forEach(variant.apps, function (app) { - app.active = false; - if (app.alias === activeAppAlias) { - app.active = true; - } - }); - } - - return variant; - } + /** * Adds a new editor to the editors array to show content in a split view * @param {any} selectedVariant */ function openSplitView(selectedVariant) { - var selectedCulture = selectedVariant.language.culture; + // enforce content contentApp in splitview. + var contentApp = vm.content.apps.find((app) => app.alias === "umbContent"); + if(contentApp) { + selectApp(contentApp); + } + + insertVariantEditor(vm.editors.length, selectedVariant); + + splitViewChanged(); + + } + + $scope.$on("editors.content.splitViewRequest", function(event, args) {requestSplitView(args);}); + vm.requestSplitView = requestSplitView; + function requestSplitView(args) { + var culture = args.culture; + var segment = args.segment; - //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 (!v.language || v.language.culture === culture) && v.segment === segment; }); - insertVariantEditor(vm.editors.length, initVariant(variant, vm.editors.length)); - - //only the content app can be selected since no other apps are shown, and because we copy all of these apps - //to the "editors" we need to update this across all editors - for (var e = 0; e < vm.editors.length; e++) { - var editor = vm.editors[e]; - for (var i = 0; i < editor.content.apps.length; i++) { - var app = editor.content.apps[i]; - if (app.alias === "umbContent") { - app.active = true; - // tell the world that the app has changed (but do it only once) - if (e === 0) { - selectApp(app); - } - } - else { - app.active = false; - } - } + if (variant != null) { + openSplitView(variant); } - - // TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular - editor.collapsed = true; - editor.loading = true; - $timeout(function () { - editor.collapsed = false; - editor.loading = false; - splitViewChanged(); - }, 100); } /** Closes the split view */ function closeSplitView(editorIndex) { // TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular var editor = vm.editors[editorIndex]; - editor.loading = true; - editor.collapsed = true; - $timeout(function () { - vm.editors.splice(editorIndex, 1); - //remove variant from open variants - vm.openVariants.splice(editorIndex, 1); - //update the current culture to reflect the last open variant (closing the split view corresponds to selecting the other variant) - $location.search("cculture", vm.openVariants[0]); - splitViewChanged(); - }, 400); + vm.editors.splice(editorIndex, 1); + editor.content.active = false; + + //update the current culture to reflect the last open variant (closing the split view corresponds to selecting the other variant) + + $location.search({"cculture": vm.editors[0].content.language ? vm.editors[0].content.language.culture : null, "csegment": vm.editors[0].content.segment}); + splitViewChanged(); } /** @@ -282,38 +204,26 @@ */ function selectVariant(variant, editorIndex) { - // prevent variants already open in a split view to be opened - if (vm.openVariants.indexOf(variant.language.culture) !== -1) { + var variantCulture = variant.language ? variant.language.culture : "invariant"; + var variantSegment = variant.segment || null; + + // Check if we already have this editor open, if so, do nothing. + if (vm.editors.find((editor) => (!editor.content.language || editor.content.language.culture === variantCulture) && editor.content.segment === variantSegment)) { return; } - + //if the editor index is zero, then update the query string to track the lang selection, otherwise if it's part //of a 2nd split view editor then update the model directly. 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", variantCulture).search("csegment", variantSegment); } else { - //Update the 'active' variant for this editor - var editor = vm.editors[editorIndex]; - //set all variant drop down items as inactive for this editor and then set the selected one as active - for (var i = 0; i < editor.content.variants.length; i++) { - editor.content.variants[i].active = false; - } - variant.active = true; - - //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; - }); - editor.content = initVariant(contentVariant, editorIndex); - //update the editors collection - insertVariantEditor(editorIndex, contentVariant); - + insertVariantEditor(editorIndex, variant); + } } @@ -322,25 +232,17 @@ * @param {any} app This is the model of the selected app */ function selectApp(app) { - if (vm.onSelectApp) { - vm.onSelectApp({ "app": app }); + if(vm.onSelectApp) { + vm.onSelectApp({"app": app}); } } - + function selectAppAnchor(app, anchor) { - if (vm.onSelectAppAnchor) { - vm.onSelectAppAnchor({ "app": app, "anchor": anchor }); + if(vm.onSelectAppAnchor) { + vm.onSelectAppAnchor({"app": app, "anchor": anchor}); } } - - $scope.$on("editors.apps.appChanged", function ($event, $args) { - var app = $args.app; - if (app && app.alias) { - activeAppAlias = app.alias; - } - }); - } angular.module('umbraco.directives').component('umbVariantContentEditors', umbVariantContentEditors); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js index 7755d9d63b74..f80b3ceb3ec3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js @@ -73,7 +73,7 @@ Use this directive to generate a list of breadcrumbs. var path = scope.pathTo(ancestor); $location.path(path); - navigationService.clearSearch(["cculture"]); + navigationService.clearSearch(["cculture", "csegment"]); } scope.pathTo = function (ancestor) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index fe2a6aa40ad0..7bd812e32112 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -2,11 +2,10 @@ 'use strict'; function EditorContentHeader(serverValidationManager, localizationService, editorState) { + function link(scope) { - - function link(scope, el, attr, ctrl) { var unsubscribe = []; - + if (!scope.serverValidationNameField) { scope.serverValidationNameField = "Name"; } @@ -14,19 +13,20 @@ scope.serverValidationAliasField = "Alias"; } - 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) { + scope.isNew = scope.editor.content.state == "NotCreated"; + localizationService.localizeMany( + [ + scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit", + "placeholders_a11yName", + scope.isNew ? "general_new" : "general_edit" + ] + ).then(function (data) { scope.a11yMessage = data[0]; scope.a11yName = data[1]; var title = data[2] + ": "; if (!scope.isNew) { - scope.a11yMessage += " " + scope.content.name; + scope.a11yMessage += " " + scope.editor.content.name; title += scope.content.name; } else { var name = editorState.current.contentTypeName; @@ -34,199 +34,212 @@ scope.a11yName = name + " " + scope.a11yName; title += name; } - + scope.$emit("$changeTitle", title); }); scope.vm = {}; + scope.vm.hasVariants = false; + scope.vm.hasSubVariants = false; + scope.vm.hasCulture = false; + scope.vm.hasSegments = false; 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 updateVaraintErrors() { + scope.content.variants.forEach( function (variant) { + variant.hasError = scope.variantHasError(variant); + + }); + checkErrorsOnOtherVariants(); + } function checkErrorsOnOtherVariants() { var check = false; - angular.forEach(scope.content.variants, function (variant) { - if (scope.openVariants.indexOf(variant.language.culture) === -1 && scope.variantHasError(variant.language.culture)) { + scope.content.variants.forEach( function (variant) { + if (variant.active !== true && variant.hasError) { check = true; } }); scope.vm.errorsOnOtherVariants = check; } + + function onVariantValidation(valid, errors, allErrors, culture, segment) { + + // only want to react to property errors: + if(errors.findIndex(error => {return error.propertyAlias !== null;}) === -1) { + // we dont have any errors for properties, meaning we will back out. + return; + } + + // If error coming back is invariant, we will assign the error to the default variant by picking the defaultVariant language. + if(culture === "invariant") { + culture = scope.vm.defaultVariant.language.culture; + } - function onCultureValidation(valid, errors, allErrors, culture) { - var index = scope.vm.variantsWithError.indexOf(culture); - if (valid === true) { + var index = scope.vm.variantsWithError.findIndex((item) => item.culture === culture && item.segment === segment) + if(valid === true) { if (index !== -1) { scope.vm.variantsWithError.splice(index, 1); } } else { if (index === -1) { - scope.vm.variantsWithError.push(culture); + scope.vm.variantsWithError.push({"culture": culture, "segment": segment}); } } - checkErrorsOnOtherVariants(); + scope.$evalAsync(updateVaraintErrors); } - + function onInit() { - - // find default. - angular.forEach(scope.content.variants, function (variant) { - if (variant.language.isDefault) { + + // find default + check if we have variants. + scope.content.variants.forEach( function (variant) { + if (variant.language !== null && variant.language.isDefault) { scope.vm.defaultVariant = variant; } + if (variant.language !== null) { + scope.vm.hasCulture = true; + } + if (variant.segment !== null) { + scope.vm.hasSegments = true; + } }); - setCurrentVariant(); - - angular.forEach(scope.content.apps, (app) => { + scope.vm.hasVariants = (scope.vm.hasCulture || scope.vm.hasSegments); + scope.vm.hasSubVariants = (scope.vm.hasCulture && scope.vm.hasSegments); + + updateVaraintErrors(); + + scope.vm.variantMenu = []; + if (scope.vm.hasCulture) { + scope.content.variants.forEach( (v) => { + if (v.language !== null && v.segment === null) { + var variantMenuEntry = { + key: String.CreateGuid(), + open: v.language && v.language.culture === scope.editor.culture, + variant: v, + subVariants: scope.content.variants.filter( (subVariant) => subVariant.language.culture === v.language.culture && subVariant.segment !== null) + }; + scope.vm.variantMenu.push(variantMenuEntry); + } + }); + } else { + scope.content.variants.forEach( (v) => { + scope.vm.variantMenu.push({ + key: String.CreateGuid(), + variant: v + }); + }); + } + + scope.editor.variantApps.forEach( (app) => { if (app.alias === "umbContent") { - app.anchors = scope.content.tabs; + app.anchors = scope.editor.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(); - } - }); - } - - scope.goBack = function () { - if (scope.onBack) { - scope.onBack(); + scope.content.variants.forEach( function (variant) { + + // if we are looking for the variant with default language then we also want to check for invariant variant. + if (variant.language && variant.language.culture === scope.vm.defaultVariant.language.culture && variant.segment === null) { + unsubscribe.push(serverValidationManager.subscribe(null, "invariant", null, onVariantValidation, null)); + } + unsubscribe.push(serverValidationManager.subscribe(null, variant.language !== null ? variant.language.culture : null, null, onVariantValidation, variant.segment)); + }); + } - }; - scope.selectVariant = function (event, variant) { + scope.goBack = function () { + if (scope.onBack) { + scope.onBack(); + } + }; - if (scope.onSelectVariant) { - scope.vm.dropdownOpen = false; - scope.onSelectVariant({ "variant": variant }); - } - }; + scope.selectVariant = function (event, variant) { - scope.selectNavigationItem = function (item) { - if (scope.onSelectNavigationItem) { - scope.onSelectNavigationItem({ "item": item }); - } - } + if (scope.onSelectVariant) { + scope.vm.dropdownOpen = false; + scope.onSelectVariant({ "variant": variant }); + } + }; - scope.selectAnchorItem = function (item, anchor) { - if (scope.onSelectAnchorItem) { - scope.onSelectAnchorItem({ "item": item, "anchor": anchor }); + scope.selectNavigationItem = function(item) { + if(scope.onSelectNavigationItem) { + scope.onSelectNavigationItem({"item": item}); + } } - } - scope.closeSplitView = function () { - if (scope.onCloseSplitView) { - scope.onCloseSplitView(); - } - }; - - scope.openInSplitView = function (event, variant) { - if (scope.onOpenInSplitView) { - scope.vm.dropdownOpen = false; - scope.onOpenInSplitView({ "variant": variant }); + scope.selectAnchorItem = function(item, anchor) { + if(scope.onSelectAnchorItem) { + scope.onSelectAnchorItem({"item": item, "anchor": anchor}); + } } - }; - /** - * 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); - } + scope.closeSplitView = function () { + if (scope.onCloseSplitView) { + scope.onCloseSplitView(); + } + }; - /** - * 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) { + scope.openInSplitView = function (event, variant) { + if (scope.onOpenInSplitView) { + scope.vm.dropdownOpen = false; + scope.onOpenInSplitView({ "variant": variant }); + } + }; + + /** + * Check whether a variant has a error, used to display errors in variant switcher. + * @param {any} culture + */ + scope.variantHasError = function(variant) { + if(scope.vm.variantsWithError.find((item) => (!variant.language || item.culture === variant.language.culture) && item.segment === variant.segment) !== undefined) { return true; } + return false; } - 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(); + onInit(); + + 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: " 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) { + if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { //This property is locked cause its a invariant property shown on a non-default language. //Therefor do not validate this field. return; @@ -70,7 +71,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { //this can be null if no property was assigned if (scope.currentProperty) { //first try to get the error msg from the server collection - var err = serverValidationManager.getPropertyError(scope.currentProperty.alias, null, ""); + var err = serverValidationManager.getPropertyError(scope.currentProperty.alias, null, "", null); //if there's an error message use it if (err && err.errorMsg) { return err.errorMsg; @@ -221,25 +222,31 @@ function valPropertyMsg(serverValidationManager, localizationService) { // the correct field validation in their property editors. if (scope.currentProperty) { //this can be null if no property was assigned + + function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { + hasError = !isValid; + if (hasError) { + //set the error message to the server message + scope.errorMsg = propertyErrors[0].errorMsg; + //flag that the current validator is invalid + formCtrl.$setValidity('valPropertyMsg', false); + startWatch(); + } + else { + scope.errorMsg = ""; + //flag that the current validator is valid + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); + } + } + unsubscribe.push(serverValidationManager.subscribe(scope.currentProperty.alias, currentCulture, "", - function(isValid, propertyErrors, allErrors) { - hasError = !isValid; - if (hasError) { - //set the error message to the server message - scope.errorMsg = propertyErrors[0].errorMsg; - //flag that the current validator is invalid - formCtrl.$setValidity('valPropertyMsg', false); - startWatch(); - } - else { - scope.errorMsg = ""; - //flag that the current validator is valid - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); - } - })); + serverValidationManagerCallback, + currentSegment + ) + ); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index a0cc7e303348..3fa9220f7b52 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -24,6 +24,7 @@ function valServer(serverValidationManager) { var currentProperty = umbPropCtrl.property; var currentCulture = currentProperty.culture; + var currentSegment = currentProperty.segment; if (umbVariantCtrl) { //if we are inside of an umbVariantContent directive @@ -31,7 +32,7 @@ function valServer(serverValidationManager) { var currentVariant = umbVariantCtrl.editor.content; // Lets check if we have variants and we are on the default language then ... - if (umbVariantCtrl.content.variants.length > 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) { + if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { //This property is locked cause its a invariant property shown on a non-default language. //Therefor do not validate this field. return; @@ -75,7 +76,7 @@ function valServer(serverValidationManager) { if (modelCtrl.$invalid) { modelCtrl.$setValidity('valServer', true); //clear the server validation entry - serverValidationManager.removePropertyError(currentProperty.alias, currentCulture, fieldName); + serverValidationManager.removePropertyError(currentProperty.alias, currentCulture, fieldName, currentSegment); stopWatch(); } }, true); @@ -90,23 +91,26 @@ function valServer(serverValidationManager) { } //subscribe to the server validation changes + function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { + if (!isValid) { + modelCtrl.$setValidity('valServer', false); + //assign an error msg property to the current validator + modelCtrl.errorMsg = propertyErrors[0].errorMsg; + startWatch(); + } + else { + modelCtrl.$setValidity('valServer', true); + //reset the error message + modelCtrl.errorMsg = ""; + stopWatch(); + } + } unsubscribe.push(serverValidationManager.subscribe(currentProperty.alias, currentCulture, fieldName, - function(isValid, propertyErrors, allErrors) { - if (!isValid) { - modelCtrl.$setValidity('valServer', false); - //assign an error msg property to the current validator - modelCtrl.errorMsg = propertyErrors[0].errorMsg; - startWatch(); - } - else { - modelCtrl.$setValidity('valServer', true); - //reset the error message - modelCtrl.errorMsg = ""; - stopWatch(); - } - })); + serverValidationManagerCallback, + currentSegment) + ); scope.$on('$destroy', function () { stopWatch(); diff --git a/src/Umbraco.Web.UI.Client/src/common/interceptors/culturerequest.interceptor.js b/src/Umbraco.Web.UI.Client/src/common/interceptors/culturerequest.interceptor.js index 186f3accf0a5..283a2a7ae98c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/interceptors/culturerequest.interceptor.js +++ b/src/Umbraco.Web.UI.Client/src/common/interceptors/culturerequest.interceptor.js @@ -24,6 +24,7 @@ if ($routeParams) { // it's an API request, add the current client culture as a header value config.headers["X-UMB-CULTURE"] = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + config.headers["X-UMB-SEGMENT"] = $routeParams.csegment ? $routeParams.csegment : null; } return config; 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 5cf54ad6a0c3..8f2aa1d22b46 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 @@ -698,7 +698,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt // /belle/#/content/edit/9876 (where 9876 is the new id) //clear the query strings - navigationService.clearSearch(["cculture"]); + navigationService.clearSearch(["cculture", "csegment"]); if (softRedirect) { navigationService.setSoftRedirect(); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index 9d84c3c616f7..90fbd76ec9d8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -159,9 +159,16 @@ function formHelper(angularHelper, serverValidationManager, notificationsService //the alias in model state can be in dot notation which indicates // * the first part is the content property alias // * the second part is the field to which the valiation msg is associated with - //There will always be at least 3 parts for content properties since all model errors for properties are prefixed with "_Properties" + //There will always be at least 4 parts for content properties since all model errors for properties are prefixed with "_Properties" //If it is not prefixed with "_Properties" that means the error is for a field of the object directly. + // Example: "_Properties.headerImage.en-US.mySegment.myField" + // * it's for a property since it has a _Properties prefix + // * it's for the headerImage property type + // * it's for the en-US culture + // * it's for the mySegment segment + // * it's for the myField html field (optional) + var parts = e.split("."); //Check if this is for content properties - specific to content/media/member editors because those are special @@ -179,16 +186,23 @@ function formHelper(angularHelper, serverValidationManager, notificationsService } } - //if it contains 3 '.' then we will wire it up to a property's html field + var segment = null; if (parts.length > 3) { - //add an error with a reference to the field for which the validation belongs too - serverValidationManager.addPropertyError(propertyAlias, culture, parts[3], modelState[e][0]); + segment = parts[3]; + //special check in case the string is formatted this way + if (segment === "null") { + segment = null; + } } - else { - //add a generic error for the property, no reference to a specific html field - serverValidationManager.addPropertyError(propertyAlias, culture, "", modelState[e][0]); + + var htmlFieldReference = ""; + if (parts.length > 4) { + htmlFieldReference = parts[4] || ""; } + // add a generic error for the property + serverValidationManager.addPropertyError(propertyAlias, culture, htmlFieldReference, modelState[e][0], segment); + } else { //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example: diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index 019890711901..da784a1f9e83 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -29,7 +29,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService //A list of query strings defined that when changed will not cause a reload of the route - var nonRoutingQueryStrings = ["mculture", "cculture", "lq", "sr"]; + var nonRoutingQueryStrings = ["mculture", "cculture", "csegment", "lq", "sr"]; var retainedQueryStrings = ["mculture"]; function setMode(mode) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index e2e51a6c2838..718e44d66e45 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -13,13 +13,14 @@ function serverValidationManager($timeout) { var callbacks = []; /** calls the callback specified with the errors specified, used internally */ - function executeCallback(self, errorsForCallback, callback, culture) { + function executeCallback(self, errorsForCallback, callback, culture, segment) { callback.apply(self, [ false, // pass in a value indicating it is invalid errorsForCallback, // pass in the errors for this item self.items, // pass in all errors in total - culture // pass the culture that we are listing for. + culture, // pass the culture that we are listing for. + segment // pass the segment that we are listing for. ] ); } @@ -35,7 +36,8 @@ function serverValidationManager($timeout) { }); } - function getPropertyErrors(self, propertyAlias, culture, fieldName) { + + function getPropertyErrors(self, propertyAlias, culture, segment, fieldName) { if (!Utilities.isString(propertyAlias)) { throw "propertyAlias must be a string"; } @@ -46,22 +48,28 @@ function serverValidationManager($timeout) { if (!culture) { culture = "invariant"; } + if (!segment) { + segment = null; + } //find all errors for this property return _.filter(self.items, function (item) { - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); } - function getCultureErrors(self, culture) { + function getVariantErrors(self, culture, segment) { if (!culture) { culture = "invariant"; } + if (!segment) { + segment = null; + } //find all errors for this property return _.filter(self.items, function (item) { - return (item.culture === culture); + return (item.culture === culture && item.segment === segment); }); } @@ -71,21 +79,21 @@ function serverValidationManager($timeout) { //its a field error callback var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName); if (fieldErrors.length > 0) { - executeCallback(self, fieldErrors, callbacks[cb].callback, callbacks[cb].culture); + executeCallback(self, fieldErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); } } else if (callbacks[cb].propertyAlias != null) { //its a property error - var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].culture, callbacks[cb].fieldName); + var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].culture, callbacks[cb].segment, callbacks[cb].fieldName); if (propErrors.length > 0) { - executeCallback(self, propErrors, callbacks[cb].callback, callbacks[cb].culture); + executeCallback(self, propErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); } } else { - //its a culture error - var cultureErrors = getCultureErrors(self, callbacks[cb].culture); - if (cultureErrors.length > 0) { - executeCallback(self, cultureErrors, callbacks[cb].callback, callbacks[cb].culture); + //its a variant error + var variantErrors = getVariantErrors(self, callbacks[cb].culture, callbacks[cb].segment); + if (variantErrors.length > 0) { + executeCallback(self, variantErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); } } } @@ -150,20 +158,27 @@ function serverValidationManager($timeout) { * field alias to listen for. * If propertyAlias is null, then this subscription is for a field property (not a user defined property). */ - subscribe: function (propertyAlias, culture, fieldName, callback) { + subscribe: function (propertyAlias, culture, fieldName, callback, segment) { if (!callback) { return; } var id = String.CreateGuid(); + + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } if (propertyAlias === null) { callbacks.push({ propertyAlias: null, culture: culture, + segment: segment, fieldName: fieldName, callback: callback, id: id @@ -175,6 +190,7 @@ function serverValidationManager($timeout) { callbacks.push({ propertyAlias: propertyAlias, culture: culture, + segment: segment, fieldName: fieldName, callback: callback, id: id @@ -199,25 +215,29 @@ function serverValidationManager($timeout) { * @param {} fieldName * @returns {} */ - unsubscribe: function (propertyAlias, culture, fieldName) { + unsubscribe: function (propertyAlias, culture, fieldName, segment) { - //normalize culture to null + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } if (propertyAlias === null) { //remove all callbacks for the content field callbacks = _.reject(callbacks, function (item) { - return item.propertyAlias === null && item.culture === culture && item.fieldName === fieldName; + return item.propertyAlias === null && item.culture === culture && item.segment === segment && item.fieldName === fieldName; }); } else if (propertyAlias !== undefined) { //remove all callbacks for the content property callbacks = _.reject(callbacks, function (item) { - return item.propertyAlias === propertyAlias && item.culture === culture && + return item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || ((item.fieldName === undefined || item.fieldName === "") && (fieldName === undefined || fieldName === ""))); }); @@ -236,16 +256,20 @@ function serverValidationManager($timeout) { * This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an * explicit field name set. */ - getPropertyCallbacks: function (propertyAlias, culture, fieldName) { + getPropertyCallbacks: function (propertyAlias, culture, fieldName, segment) { - //normalize culture to null + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } var found = _.filter(callbacks, function (item) { //returns any callback that have been registered directly against the field and for only the property - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === ""))); }); return found; }, @@ -262,7 +286,7 @@ function serverValidationManager($timeout) { getFieldCallbacks: function (fieldName) { var found = _.filter(callbacks, function (item) { //returns any callback that have been registered directly against the field - return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName); + return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); return found; }, @@ -274,12 +298,29 @@ function serverValidationManager($timeout) { * @function * * @description - * Gets all callbacks that has been registered using the subscribe method for the culture. + * Gets all callbacks that has been registered using the subscribe method for the culture. Not including segments. */ getCultureCallbacks: function (culture) { var found = _.filter(callbacks, function (item) { //returns any callback that have been registered directly/ONLY against the culture - return (item.culture === culture && item.propertyAlias === null && item.fieldName === null); + return (item.culture === culture && item.segment === null && item.propertyAlias === null && item.fieldName === null); + }); + return found; + }, + + /** + * @ngdoc function + * @name getVariantCallbacks + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Gets all callbacks that has been registered using the subscribe method for the culture and segment. + */ + getVariantCallbacks: function (culture, segment) { + var found = _.filter(callbacks, function (item) { + //returns any callback that have been registered directly against the given culture and given segment. + return (item.culture === culture && item.segment === segment && item.propertyAlias === null && item.fieldName === null); }); return found; }, @@ -303,6 +344,7 @@ function serverValidationManager($timeout) { this.items.push({ propertyAlias: null, culture: "invariant", + segment: null, fieldName: fieldName, errorMsg: errorMsg }); @@ -314,7 +356,7 @@ function serverValidationManager($timeout) { var cbs = this.getFieldCallbacks(fieldName); //call each callback for this error for (var cb in cbs) { - executeCallback(this, errorsForCallback, cbs[cb].callback, null); + executeCallback(this, errorsForCallback, cbs[cb].callback, null, null); } }, @@ -327,7 +369,7 @@ function serverValidationManager($timeout) { * @description * Adds an error message for the content property */ - addPropertyError: function (propertyAlias, culture, fieldName, errorMsg) { + addPropertyError: function (propertyAlias, culture, fieldName, errorMsg, segment) { if (!propertyAlias) { return; } @@ -336,31 +378,36 @@ function serverValidationManager($timeout) { if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } //only add the item if it doesn't exist - if (!this.hasPropertyError(propertyAlias, culture, fieldName)) { + if (!this.hasPropertyError(propertyAlias, culture, fieldName, segment)) { this.items.push({ propertyAlias: propertyAlias, culture: culture, + segment: segment, fieldName: fieldName, errorMsg: errorMsg }); } //find all errors for this item - var errorsForCallback = getPropertyErrors(this, propertyAlias, culture, fieldName); + var errorsForCallback = getPropertyErrors(this, propertyAlias, culture, segment, fieldName); //we should now call all of the call backs registered for this error - var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName); + var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName, segment); //call each callback for this error for (var cb in cbs) { - executeCallback(this, errorsForCallback, cbs[cb].callback, culture); + executeCallback(this, errorsForCallback, cbs[cb].callback, culture, segment); } - //execute culture specific callbacks here too when a propery error is added - var cultureCbs = this.getCultureCallbacks(culture); + //execute variant specific callbacks here too when a propery error is added + var variantCbs = this.getVariantCallbacks(culture, segment); //call each callback for this error - for (var cb in cultureCbs) { - executeCallback(this, errorsForCallback, cultureCbs[cb].callback, culture); + for (var cb in variantCbs) { + executeCallback(this, errorsForCallback, variantCbs[cb].callback, culture, segment); } }, @@ -373,7 +420,7 @@ function serverValidationManager($timeout) { * @description * Removes an error message for the content property */ - removePropertyError: function (propertyAlias, culture, fieldName) { + removePropertyError: function (propertyAlias, culture, fieldName, segment) { if (!propertyAlias) { return; @@ -383,10 +430,14 @@ function serverValidationManager($timeout) { if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } //remove the item this.items = _.reject(this.items, function (item) { - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); }, @@ -405,7 +456,10 @@ function serverValidationManager($timeout) { callbacks[cb].callback.apply(this, [ true, //pass in a value indicating it is VALID [], //pass in empty collection - []]); //pass in empty collection + [], + null, + null] + ); } }, @@ -431,16 +485,20 @@ function serverValidationManager($timeout) { * @description * Gets the error message for the content property */ - getPropertyError: function (propertyAlias, culture, fieldName) { + getPropertyError: function (propertyAlias, culture, fieldName, segment) { - //normalize culture to null + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); return err; }, @@ -457,7 +515,7 @@ function serverValidationManager($timeout) { getFieldError: function (fieldName) { var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName); + return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); return err; }, @@ -471,16 +529,20 @@ function serverValidationManager($timeout) { * @description * Checks if the content property + culture + field name combo has an error */ - hasPropertyError: function (propertyAlias, culture, fieldName) { + hasPropertyError: function (propertyAlias, culture, fieldName, segment) { //normalize culture to null if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); return err ? true : false; }, @@ -497,12 +559,11 @@ function serverValidationManager($timeout) { hasFieldError: function (fieldName) { var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName); + return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); return err ? true : false; }, - /** * @ngdoc function * @name hasCultureError @@ -513,14 +574,40 @@ function serverValidationManager($timeout) { * Checks if the given culture has an error */ hasCultureError: function (culture) { + + //normalize culture to "invariant" + if (!culture) { + culture = "invariant"; + } + + var err = _.find(this.items, function (item) { + return (item.culture === culture && item.segment === null); + }); + return err ? true : false; + }, + + /** + * @ngdoc function + * @name hasVariantError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Checks if the given culture has an error + */ + hasVariantError: function (culture, segment) { - //normalize culture to null + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } var err = _.find(this.items, function (item) { - return item.culture === culture; + return (item.culture === culture && item.segment === segment); }); return err ? true : false; }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index bfcf4d353280..109fff0919dd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -64,7 +64,7 @@ var saveModel = _.pick(displayModel, 'compositeContentTypes', 'isContainer', 'allowAsRoot', 'allowedTemplates', 'allowedContentTypes', 'alias', 'description', 'thumbnail', 'name', 'id', 'icon', 'trashed', - 'key', 'parentId', 'alias', 'path', 'allowCultureVariant', 'isElement'); + 'key', 'parentId', 'alias', 'path', 'allowCultureVariant', 'allowSegmentVariant', 'isElement'); // TODO: Map these saveModel.allowedTemplates = _.map(displayModel.allowedTemplates, function (t) { return t.alias; }); @@ -83,7 +83,7 @@ }); var saveProperties = _.map(realProperties, function (p) { - var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant'); + var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant', 'allowSegmentVariant'); return saveProperty; }); @@ -367,6 +367,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, @@ -393,38 +394,59 @@ */ formatContentGetData: function(displayModel) { - //We need to check for invariant properties among the variant variants. - //When we detect this, we want to make sure that the property object instance is the - //same reference object between all variants instead of a copy (which it will be when - //return from the JSON structure). + // We need to check for invariant properties among the variant variants, + // as the value of an invariant property is shared between different variants. + // A property can be culture invariant, segment invariant, or both. + // When we detect this, we want to make sure that the property object instance is the + // same reference object between all variants instead of a copy (which it will be when + // return from the JSON structure). if (displayModel.variants && displayModel.variants.length > 1) { + // Collect all invariant properties from the variants that are either the + // default language variant or the default segment variant. + var defaultVariants = _.filter(displayModel.variants, function (variant) { + var isDefaultLanguage = variant.language && variant.language.isDefault; + var isDefaultSegment = variant.segment == null; - var invariantProperties = []; - - //collect all invariant properties on the first first variant - var firstVariant = displayModel.variants[0]; - _.each(firstVariant.tabs, function(tab, tabIndex) { - _.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) { - invariantProperties.push({ - tabIndex: tabIndex, - propIndex: propIndex, - property: property - }); - } - }); + return isDefaultLanguage || isDefaultSegment; }); + if (defaultVariants.length > 0) { + _.each(defaultVariants, function (defaultVariant) { + var invariantProps = []; + + _.each(defaultVariant.tabs, function (tab, tabIndex) { + _.each(tab.properties, function (property, propIndex) { + // culture == null -> property is culture invariant + // segment == null -> property is *possibly* segment invariant + if (!property.culture || !property.segment) { + invariantProps.push({ + tabIndex: tabIndex, + propIndex: propIndex, + property: property + }); + } + }); + }); - //now assign this same invariant property instance to the same index of the other variants property array - for (var j = 1; j < displayModel.variants.length; j++) { - var variant = displayModel.variants[j]; + var otherVariants = _.filter(displayModel.variants, function (variant) { + return variant !== defaultVariant; + }); - _.each(invariantProperties, function (invProp) { - variant.tabs[invProp.tabIndex].properties[invProp.propIndex] = invProp.property; + // now assign this same invariant property instance to the same index of the other variants property array + _.each(otherVariants, function (variant) { + _.each(invariantProps, function (invProp) { + var tab = variant.tabs[invProp.tabIndex]; + var prop = tab.properties[invProp.propIndex]; + + var inheritsCulture = prop.culture === invProp.property.culture && prop.segment == null && invProp.property.segment == null; + var inheritsSegment = prop.segment === invProp.property.segment && !prop.culture; + + if (inheritsCulture || inheritsSegment) { + tab.properties[invProp.propIndex] = invProp.property; + } + }); + }); }); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 0921f46aac13..174f9f41d714 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -107,6 +107,7 @@ @import "components/overlays.less"; @import "components/card.less"; @import "components/editor/umb-editor.less"; +@import "components/editor/umb-variant-switcher.less"; @import "components/umb-sub-views.less"; @import "components/umb-editor-navigation.less"; @import "components/umb-editor-navigation-item.less"; @@ -137,6 +138,7 @@ @import "components/tooltip/umb-tooltip-list.less"; @import "components/overlays/umb-overlay-backdrop.less"; @import "components/overlays/umb-itempicker.less"; +@import "components/overlays/umb-variant-selector-overlay"; @import "components/umb-grid.less"; @import "components/umb-empty-state.less"; @import "components/umb-property-editor.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less index bc84b0d35e20..ac55c6ffb13e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -162,161 +162,6 @@ a.umb-editor-header__close-split-view:hover { } } -/* variant switcher */ -.umb-variant-switcher__toggle { - position: relative; - display: flex; - align-items: center; - padding: 0 10px; - margin: 1px 1px; - right: 0; - height: 30px; - text-decoration: none !important; - font-size: 13px; - color: @ui-action-discreet-type; - background: transparent; - border: none; - - max-width: 50%; - white-space: nowrap; - - user-select: none; - - span { - text-overflow: ellipsis; - overflow: hidden; - } -} - -button.umb-variant-switcher__toggle { - transition: color 0.2s ease-in-out; - &:hover { - //background-color: @gray-10; - color: @ui-action-discreet-type-hover; - .umb-variant-switcher__expand { - color: @ui-action-discreet-type-hover; - } - } - - &.--error { - &::before { - content: '!'; - position: absolute; - top: -8px; - right: -10px; - display: inline-flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border-radius: 10px; - text-align: center; - font-weight: bold; - background-color: @errorBackground; - color: @errorText; - } - } -} - -.umb-variant-switcher__expand { - color: @ui-action-discreet-type; - margin-top: 3px; - margin-left: 5px; - margin-right: -5px; - transition: color 0.2s ease-in-out; -} - -.umb-variant-switcher__item { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid @gray-9; - position: relative; -} - -.umb-variant-switcher__item:last-child { - border-bottom: none; -} - -.umb-variant-switcher__item.--current { - color: @ui-light-active-type; -} -.umb-variant-switcher__item.--current .umb-variant-switcher__name-wrapper { - border-left: 4px solid @ui-active; -} - -.umb-variant-switcher__item:hover { - outline: none; -} - -.umb-variant-switcher__item.--not-allowed:not(.--current) .umb-variant-switcher__name-wrapper:hover { - //background-color: @white !important; - cursor: default; -} - -.umb-variant-switcher__item:hover .umb-variant-switcher__split-view { - display: block; - cursor: pointer; -} - -.umb-variant-switcher__item.--error { - .umb-variant-switcher__name { - color: @red; - &::after { - content: '!'; - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - margin-left: 5px; - top: -3px; - width: 14px; - height: 14px; - border-radius: 7px; - font-size: 8px; - text-align: center; - font-weight: bold; - background-color: @errorBackground; - color: @errorText; - } - } -} - -.umb-variant-switcher__name-wrapper { - font-size: 14px; - flex: 1; - cursor: pointer; - padding-top: 6px !important; - padding-bottom: 6px !important; - background-color: transparent; - border: none; - border-left: 2px solid transparent; -} - -.umb-variant-switcher__name { - display: block; -} - -.umb-variant-switcher__state { - font-size: 13px; - color: @gray-4; -} - -.umb-variant-switcher__split-view { - font-size: 13px; - display: none; - padding: 16px 20px; - position: absolute; - right: 0; - top: 0; - bottom: 0; - background-color: @white; - - &:hover { - background-color: @ui-option-hover; - color: @ui-option-type-hover; - } -} // container diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less new file mode 100644 index 000000000000..8dbc070856f1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -0,0 +1,332 @@ +/* variant switcher */ +.umb-variant-switcher__toggle { + position: relative; + display: flex; + align-items: center; + padding: 0 10px; + margin: 1px 1px; + right: 0; + height: 30px; + text-decoration: none !important; + font-size: 13px; + color: @ui-action-discreet-type; + background: transparent; + border: none; + + max-width: 50%; + white-space: nowrap; + + user-select: none; + + span { + text-overflow: ellipsis; + overflow: hidden; + } +} + +button.umb-variant-switcher__toggle { + transition: color 0.2s ease-in-out; + &:hover { + //background-color: @gray-10; + color: @ui-action-discreet-type-hover; + .umb-variant-switcher__expand { + color: @ui-action-discreet-type-hover; + } + } + + &.--error { + &::before { + content: '!'; + position: absolute; + top: -8px; + right: -10px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 10px; + text-align: center; + font-weight: bold; + background-color: @errorBackground; + color: @errorText; + } + } +} + +.umb-variant-switcher__expand { + color: @ui-action-discreet-type; + margin-top: 3px; + margin-left: 5px; + margin-right: -5px; + transition: color 0.2s ease-in-out; +} + + +.umb-variant-switcher { + min-width: 100%; + max-height: 80vh; + overflow-y: auto; + margin-top: 5px; + user-select: none; +} + +.umb-variant-switcher__item { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid @gray-9; + position: relative; + .umb-variant-switcher__name-wrapper:hover { + .umb-variant-switcher__name { + color: @blueMid; + } + .umb-variant-switcher__state { + color: @blueMid; + } + } +} +.umb-variant-switcher__item.--state-notCreated:not(.--active) { + .umb-variant-switcher__name-wrapper::before { + content: "+"; + display: block; + float: left; + font-size: 15px; + font-weight: 900; + padding: 8px 16px 8px 6px; + color: @gray-5; + } + .umb-variant-switcher__item-expand-button + .umb-variant-switcher__name-wrapper::before { + padding: 8px 16px 8px 20px; + } + .umb-variant-switcher__name { + color: @gray-5; + } + .umb-variant-switcher__state { + color: @gray-6; + } + .umb-variant-switcher__name-wrapper::after { + content: ""; + position: absolute; + z-index: 1; + border: 1px dashed @gray-9; + top: 7px; + bottom: 7px; + left: 7px; + right: 7px; + border-radius: 3px; + pointer-events: none; + } + + .umb-variant-switcher__name-wrapper:hover { + &::before { + color: @blueMid; + } + .umb-variant-switcher__name { + color: @blueMid; + } + .umb-variant-switcher__state { + color: @blueMid; + } + } +} +/* +.umb-variant-switcher__item.--state-draft { + .umb-variant-switcher__name { + color: @gray-5; + } + &:hover { + .umb-variant-switcher__name { + color: @blueMid; + } + } +} +*/ + +.umb-variant-switcher.--has-sub-variants { + .umb-variant-switcher__item { + + } +} + +.umb-variant-switcher__item-expand-button { + text-decoration: none; + display: inline-block; + flex: 0; + align-self: stretch; + + padding-left: 22px !important; + padding-right: 14px !important; + + font-size: 12px; + + * { + pointer-events: none; + } +} + +.umb-variant-switcher__item:last-child { + border-bottom: none; +} + +.umb-variant-switcher__item.--current { + //color: @ui-light-active-type; + //background-color: @pinkExtraLight; + .umb-variant-switcher__name { + //color: @ui-light-active-type; + font-weight: 700; + } + &::before { + content: ''; + position: absolute; + border-radius: 0 4px 4px 0; + background-color: @ui-active-border; + width: 4px; + top:8px; + bottom: 8px; + left:0; + z-index:1; + pointer-events: none; + } +} + +.umb-variant-switcher__item:hover { + outline: none; +} + +.umb-variant-switcher__item.--active:not(.--current) .umb-variant-switcher__name-wrapper:hover { + //background-color: @white !important; + cursor: default; +} + +.umb-variant-switcher__item:focus .umb-variant-switcher__split-view, +.umb-variant-switcher__item:focus-within .umb-variant-switcher__split-view, +.umb-variant-switcher__item:hover .umb-variant-switcher__split-view, +.umb-variant-switcher__split-view:focus { + display: block; + cursor: pointer; +} + +.umb-variant-switcher__item.--error { + .umb-variant-switcher__name { + color: @red; + &::after { + content: '!'; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 5px; + top: -3px; + width: 14px; + height: 14px; + border-radius: 7px; + font-size: 8px; + text-align: center; + font-weight: bold; + background-color: @errorBackground; + color: @errorText; + } + } +} + +.umb-variant-switcher__name-wrapper { + font-size: 14px; + text-align: left; + flex: 1; + cursor: pointer; + background-color: transparent; + border: none; +} +.dropdown-menu>li { + > .umb-variant-switcher__name-wrapper { + padding-top: 10px; + padding-bottom: 10px; + } + + > .umb-variant-switcher__item-expand-button + .umb-variant-switcher__name-wrapper { + padding-left: 5px; + } +} + + +.umb-variant-switcher__name { + display: block; + font-weight: 600; + margin-bottom: -2px; +} + +.umb-variant-switcher__state { + font-size: 12px; + color: @gray-4; +} + +.umb-variant-switcher__split-view { + font-size: 12px; + display: none; + padding: 20px 20px; + position: absolute; + right: 0; + top: 0; + bottom: 0; + background-color: @white; + + &:hover { + background-color: @ui-option-hover; + color: @ui-option-type-hover; + } +} + + +.umb-variant-switcher__sub-variants { + + position: relative; + border-bottom: 1px solid @gray-9; + background-color: @gray-13; + /* + &::before { + content: ""; + position: absolute; + z-index: 1; + top: 0px; + left: 20px; + width: 4px; + bottom: 14px; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + background-color: @gray-8; + } + */ + .umb-variant-switcher__item { + border-bottom-color: @gray-10; + } + + .umb-variant-switcher__item.--state-notCreated:not(.--active) { + .umb-variant-switcher__name-wrapper::after { + left: 55px;// overwrite left to achieve same indentation on the dashed border as language. + } + } + + .umb-variant-switcher__name-wrapper { + + margin-left: 48px; + padding-left: 20px; + + padding-top: 10px; + padding-bottom: 10px; + + &:hover { + color: @ui-option-type-hover; + background-color: @ui-option-hover; + } + + .umb-variant-switcher__name { + //margin-right: 20px; + } + .umb-variant-switcher__state { + //flex: 0 0 200px; + } + + + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less new file mode 100644 index 000000000000..b50a622f9817 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less @@ -0,0 +1,26 @@ +.umb-variant-selector-overlay { + + + .umb-variant-selector-entry { + .umb-form-check { + .umb-form-check__symbol { + margin-top: 2px; + } + } + } + .umb-variant-selector-entry__title { + font-weight: 600; + font-size: 14px; + .__secondarytitle { + font-weight: normal; + color: @gray-5; + } + } + .umb-variant-selector-entry__description { + display: block; + font-size: 12px; + color: @gray-4; + } + + +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less index 2cca77661471..f3c41dbc339a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less @@ -178,11 +178,11 @@ } // Validation -.umb-sub-views-nav-item__action.-has-error, +.show-validation .umb-sub-views-nav-item__action.-has-error, .show-validation .umb-sub-views-nav-item > a.-has-error { color: @red; - &::after { + &::before { background-color: @red; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less index 94cfa6f62c1d..44955e8f8e21 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less @@ -1,15 +1,18 @@ .umb-list--condensed { .umb-list-item { - padding-top: 5px; - padding-bottom: 5px; + padding-top: 7px; + padding-bottom: 7px; } } .umb-list-item { - border-bottom: 1px solid @gray-9; + border-bottom: 1px solid @gray-11; padding-top: 15px; padding-bottom: 15px; display: flex; + &:last-of-type { + border-bottom: none; + } } a.umb-list-item:hover, diff --git a/src/Umbraco.Web.UI.Client/src/less/properties.less b/src/Umbraco.Web.UI.Client/src/less/properties.less index 8523fe9300d2..9e951feb1a79 100644 --- a/src/Umbraco.Web.UI.Client/src/less/properties.less +++ b/src/Umbraco.Web.UI.Client/src/less/properties.less @@ -49,7 +49,7 @@ } .date-wrapper-mini--checkbox{ - margin: 0 0 0 26px; + margin: 0 0 0 28px; } .date-wrapper-mini__date { @@ -62,6 +62,10 @@ &:first-of-type { margin-left: 0; } + .flatpickr-input > button:first-of-type { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } } .date-wrapper-mini__date .flatpickr-input > a { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js index 9640f2eba2f5..24152370df84 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js @@ -30,6 +30,7 @@ vm.close = close; vm.toggleAllowCultureVariants = toggleAllowCultureVariants; + vm.toggleAllowSegmentVariants = toggleAllowSegmentVariants; vm.toggleValidation = toggleValidation; vm.toggleShowOnMemberProfile = toggleShowOnMemberProfile; vm.toggleMemberCanEdit = toggleMemberCanEdit; @@ -248,6 +249,10 @@ $scope.model.property.allowCultureVariant = toggleValue($scope.model.property.allowCultureVariant); } + function toggleAllowSegmentVariants() { + $scope.model.property.allowSegmentVariant = toggleValue($scope.model.property.allowSegmentVariant); + } + function toggleValidation() { $scope.model.property.validation.mandatory = toggleValue($scope.model.property.validation.mandatory); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 77ee276e3e9e..2a1446669fec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -40,7 +40,7 @@ - +
- + - +
@@ -133,9 +133,9 @@
- -
- + +
+
- + +
+ +
+ + + + +
+
@@ -177,7 +189,7 @@
checked="model.property.isSensitiveData" on-click="vm.toggleIsSensitiveData()"> - +
@@ -207,7 +219,7 @@
label-key="general_submit" action="vm.submit(model)"> - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html index 9b230410b0b1..e292a946065d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html @@ -9,9 +9,9 @@ hide-icon="true" hide-description="true"> - + - + @@ -21,21 +21,21 @@
-
- +
- +

{{vm.currentVersion.name}} (Created: {{vm.currentVersion.createDate}})

- +
-