diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs deleted file mode 100644 index fa5a29fece24..000000000000 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.Serialization; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.Core.Models.Blocks -{ - /// - /// The base class for any strongly typed model for a Block editor implementation - /// - public abstract class BlockEditorModel - { - protected BlockEditorModel(IEnumerable contentData, IEnumerable settingsData) - { - ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData)); - SettingsData = settingsData ?? new List(); - } - - public BlockEditorModel() - { - } - - - /// - /// The content data items of the Block List editor - /// - [DataMember(Name = "contentData")] - public IEnumerable ContentData { get; set; } = new List(); - - /// - /// The settings data items of the Block List editor - /// - [DataMember(Name = "settingsData")] - public IEnumerable SettingsData { get; set; } = new List(); - } -} diff --git a/src/Umbraco.Core/Models/Blocks/BlockItemData.cs b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs index 12a636771e43..02432766b065 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockItemData.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs @@ -49,8 +49,14 @@ public class BlockItemData /// public class BlockPropertyValue { - public object Value { get; set; } - public PropertyType PropertyType { get; set; } + public BlockPropertyValue(object value, PropertyType propertyType) + { + Value = value; + PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); + } + + public object Value { get; } + public PropertyType PropertyType { get; } } } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs similarity index 65% rename from src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs rename to src/Umbraco.Core/Models/Blocks/BlockListItem.cs index f576bd927fc3..f4b5c489e7f0 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs @@ -7,10 +7,10 @@ namespace Umbraco.Core.Models.Blocks /// /// Represents a layout item for the Block List editor /// - [DataContract(Name = "blockListLayout", Namespace = "")] - public class BlockListLayoutReference : IBlockReference + [DataContract(Name = "block", Namespace = "")] + public class BlockListItem : IBlockReference { - public BlockListLayoutReference(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) + public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) { ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); Content = content ?? throw new ArgumentNullException(nameof(content)); @@ -33,19 +33,13 @@ public BlockListLayoutReference(Udi contentUdi, IPublishedElement content, Udi s /// /// The content data item referenced /// - /// - /// This is ignored from serialization since it is just a reference to the actual data element - /// - [IgnoreDataMember] + [DataMember(Name = "content")] public IPublishedElement Content { get; } /// /// The settings data item referenced /// - /// - /// This is ignored from serialization since it is just a reference to the actual data element - /// - [IgnoreDataMember] + [DataMember(Name = "settings")] public IPublishedElement Settings { get; } } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs index 0492cf0d7328..9a5a3af22aad 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Runtime.Serialization; using Umbraco.Core.Models.PublishedContent; @@ -8,26 +11,54 @@ namespace Umbraco.Core.Models.Blocks /// The strongly typed model for the Block List editor /// [DataContract(Name = "blockList", Namespace = "")] - public class BlockListModel : BlockEditorModel + public class BlockListModel : IReadOnlyList { + private readonly IReadOnlyList _layout = new List(); + public static BlockListModel Empty { get; } = new BlockListModel(); private BlockListModel() { } - public BlockListModel(IEnumerable contentData, IEnumerable settingsData, IEnumerable layout) - : base(contentData, settingsData) + public BlockListModel(IEnumerable layout) { - Layout = layout; + _layout = layout.ToList(); } + public int Count => _layout.Count; + + /// + /// Get the block by index + /// + /// + /// + public BlockListItem this[int index] => _layout[index]; + + /// + /// Get the block by content Guid + /// + /// + /// + public BlockListItem this[Guid contentKey] => _layout.FirstOrDefault(x => x.Content.Key == contentKey); + /// - /// The layout items of the Block List editor + /// Get the block by content element Udi /// - [DataMember(Name = "layout")] - public IEnumerable Layout { get; } = new List(); + /// + /// + public BlockListItem this[Udi contentUdi] + { + get + { + if (!(contentUdi is GuidUdi guidUdi)) return null; + return _layout.FirstOrDefault(x => x.Content.Key == guidUdi.Guid); + } + } + + public IEnumerator GetEnumerator() => _layout.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e8180d31162f..19f9d25369ae 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -150,8 +150,7 @@ - - + diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/documentTypes.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/documentTypes.ts index e0a651731ac8..c40d65d54120 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/documentTypes.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/documentTypes.ts @@ -10,6 +10,7 @@ context('Document Types', () => { const name = "Test document type"; cy.umbracoEnsureDocumentTypeNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); cy.umbracoSection('settings'); cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); @@ -44,6 +45,7 @@ context('Document Types', () => { //Assert cy.umbracoSuccessNotification().should('be.visible'); + cy.umbracoEnsureTemplateNameNotExists(name); //Clean up cy.umbracoEnsureDocumentTypeNameNotExists(name); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViewMacroFiles.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViewMacroFiles.ts index f4c976de08f7..563ff77658ef 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViewMacroFiles.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViewMacroFiles.ts @@ -1,23 +1,34 @@ /// +import { PartialViewMacroBuilder } from "umbraco-cypress-testhelpers"; + context('Partial View Macro Files', () => { beforeEach(() => { cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); }); - it('Create new partial view macro', () => { - const name = "TestPartialViewMacro"; - const fileName = name + ".cshtml"; - - cy.umbracoEnsurePartialViewMacroFileNameNotExists(fileName); - cy.umbracoEnsureMacroNameNotExists(name); - + function openPartialViewMacroCreatePanel() { cy.umbracoSection('settings'); cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); cy.umbracoTreeItem("settings", ["Partial View Macro Files"]).rightclick(); - cy.umbracoContextMenuAction("action-create").click(); + } + + function cleanup(name, extension = ".cshtml") { + const fileName = name + extension; + + cy.umbracoEnsureMacroNameNotExists(name); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(fileName); + } + + it('Create new partial view macro', () => { + const name = "TestPartialViewMacro"; + + cleanup(name); + + openPartialViewMacroCreatePanel(); + cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-label").click(); //Type name @@ -28,10 +39,117 @@ context('Partial View Macro Files', () => { //Assert cy.umbracoSuccessNotification().should('be.visible'); + cy.umbracoMacroExists(name).then(exists => { expect(exists).to.be.true; }); //Clean up - cy.umbracoEnsurePartialViewMacroFileNameNotExists(fileName); - cy.umbracoEnsureMacroNameNotExists(name); - }); + cleanup(name); + }); + + it('Create new partial view macro without macro', () => { + const name = "TestPartialMacrolessMacro"; + + cleanup(name); + + openPartialViewMacroCreatePanel(); + + cy.get('.menu-label').eq(1).click(); + + // Type name + cy.umbracoEditorHeaderName(name); + + // Save + cy.get('.btn-success').click(); + + // Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.umbracoMacroExists(name).then(exists => { expect(exists).to.be.false; }); + + // Clean + cleanup(name); + }); + + it('Create new partial view macro from snippet', () => { + const name = "TestPartialFromSnippet"; + + cleanup(name); + + openPartialViewMacroCreatePanel(); + + cy.get('.menu-label').eq(2).click(); + + // Select snippet + cy.get('.menu-label').eq(1).click(); + + // Type name + cy.umbracoEditorHeaderName(name); + + // Save + cy.get('.btn-success').click(); + + // Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.umbracoMacroExists(name).then(exists => { expect(exists).to.be.true; }); + + // Clean + cleanup(name); + }); + + it('Delete partial view macro', () => { + const name = "TestDeletePartialViewMacro"; + const fullName = name + ".cshtml" + + cleanup(name); + + const partialViewMacro = new PartialViewMacroBuilder() + .withName(name) + .withContent("@inherits Umbraco.Web.Macros.PartialViewMacroPage") + .build(); + + cy.savePartialViewMacro(partialViewMacro); + + // Navigate to settings + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + // Delete partialViewMacro + cy.umbracoTreeItem("settings", ["Partial View Macro Files", fullName]).rightclick(); + cy.umbracoContextMenuAction("action-delete").click(); + cy.umbracoButtonByLabelKey("general_ok").click(); + + // Assert + cy.contains(fullName).should('not.exist'); + + // Clean + cleanup(name); + }); + + it('Edit partial view macro', () => { + const name = "TestPartialViewMacroEditable"; + const fullName = name + ".cshtml"; + + cleanup(name); + + const partialViewMacro = new PartialViewMacroBuilder() + .withName(name) + .withContent("@inherits Umbraco.Web.Macros.PartialViewMacroPage") + .build(); + + cy.savePartialViewMacro(partialViewMacro); + + // Navigate to settings + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + cy.umbracoTreeItem("settings", ["Partial View Macro Files", fullName]).click(); + + // Type an edit + cy.get('.ace_text-input').type(" // test", {force:true} ); + // Save + cy.get('.btn-success').click(); + + // Assert + cy.umbracoSuccessNotification().should('be.visible'); + + cleanup(name); + }); }); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts index b644c6642b51..068338f8fa14 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts @@ -1,35 +1,145 @@ /// +import { PartialViewBuilder } from "umbraco-cypress-testhelpers"; + context('Partial Views', () => { - beforeEach(() => { - cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); - }); + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + function navigateToSettings() { + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + } + + function openPartialViewsCreatePanel() { + navigateToSettings(); + cy.umbracoTreeItem("settings", ["Partial Views"]).rightclick(); + } + + it('Create new empty partial view', () => { + const name = "TestPartialView"; + const fileName = name + ".cshtml"; + + cy.umbracoEnsurePartialViewNameNotExists(fileName); + + openPartialViewsCreatePanel(); + + cy.umbracoContextMenuAction("action-create").click(); + cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click(); + + //Type name + cy.umbracoEditorHeaderName(name); + + //Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.umbracoPartialViewExists(fileName).then(exists => { expect(exists).to.be.true; }); + + //Clean up + cy.umbracoEnsurePartialViewNameNotExists(fileName); + }); + + it('Create partial view from snippet', () => { + const name = "TestPartialViewFromSnippet"; + const fileName = name + ".cshtml"; + + cy.umbracoEnsurePartialViewNameNotExists(fileName); + + openPartialViewsCreatePanel(); + + cy.umbracoContextMenuAction("action-create").click(); + cy.get('.menu-label').eq(1).click(); + // Select snippet + cy.get('.menu-label').eq(2).click(); + + // Type name + cy.umbracoEditorHeaderName(name); + + // Save + cy.get('.btn-success').click(); + + // Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.umbracoPartialViewExists(fileName).then(exists => { expect(exists).to.be.true; }); + + // Clean up + cy.umbracoEnsurePartialViewNameNotExists(fileName); + }); + + it('Partial view with no name', () => { + openPartialViewsCreatePanel(); + + cy.umbracoContextMenuAction("action-create").click(); + cy.get('.menu-label').first().click(); + + // The test would fail intermittently, most likely because the editor didn't have time to load + // This should ensure that the editor is loaded and the test should no longer fail unexpectedly. + cy.get('.ace_content', {timeout: 5000}).should('exist'); + + // Click save + cy.get('.btn-success').click(); + + // Asserts + cy.umbracoErrorNotification().should('be.visible'); + }); + + it('Delete partial view', () => { + const name = "TestDeletePartialView"; + const fileName = name + ".cshtml"; + + cy.umbracoEnsurePartialViewNameNotExists(fileName); + + // Build and save partial view + const partialView = new PartialViewBuilder() + .withName(name) + .withContent("@inherits Umbraco.Web.Mvc.UmbracoViewPage") + .build(); + + cy.savePartialView(partialView); + + navigateToSettings(); + + // Delete partial view + cy.umbracoTreeItem("settings", ["Partial Views", fileName]).rightclick(); + cy.umbracoContextMenuAction("action-delete").click(); + cy.umbracoButtonByLabelKey("general_ok").click(); - it('Create new empty partial view', () => { - const name = "TestPartialView"; - const fileName = name + ".cshtml"; + // Assert + cy.contains(fileName).should('not.exist'); + cy.umbracoPartialViewExists(fileName).then(exists => { expect(exists).to.be.false; }); - cy.umbracoEnsurePartialViewNameNotExists(fileName); + // Clean + cy.umbracoEnsurePartialViewNameNotExists(fileName); + }); - cy.umbracoSection('settings'); - cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + it('Edit partial view', () => { + const name = 'EditPartialView'; + const fileName = name + ".cshtml"; - cy.umbracoTreeItem("settings", ["Partial Views"]).rightclick(); + cy.umbracoEnsurePartialViewNameNotExists(fileName); - cy.umbracoContextMenuAction("action-create").click(); - cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click(); + const partialView = new PartialViewBuilder() + .withName(name) + .withContent("@inherits Umbraco.Web.Mvc.UmbracoViewPage\n") + .build(); - //Type name - cy.umbracoEditorHeaderName(name); + cy.savePartialView(partialView); - //Save - cy.get('.btn-success').click(); + navigateToSettings(); + // Open partial view + cy.umbracoTreeItem("settings", ["Partial Views", fileName]).click(); + // Edit + cy.get('.ace_text-input').type("var num = 5;", {force:true} ); + cy.get('.btn-success').click(); - //Assert - cy.umbracoSuccessNotification().should('be.visible'); + // Assert + cy.umbracoSuccessNotification().should('be.visible'); + // Clean + cy.umbracoEnsurePartialViewNameNotExists(fileName); + }); - //Clean up - cy.umbracoEnsurePartialViewNameNotExists(fileName); - }); }); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/scripts.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/scripts.ts index 8cffd3e59bea..cce8a45da6af 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/scripts.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/scripts.ts @@ -14,7 +14,7 @@ context('Scripts', () => { cy.umbracoSection('settings'); cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); - cy.umbracoTreeItem("settings", ["Stylesheets"]).rightclick(); + cy.umbracoTreeItem("settings", ["Scripts"]).rightclick(); cy.umbracoContextMenuAction("action-create").click(); cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click(); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts index 6871db7ffed2..aff1c380935a 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts @@ -1,57 +1,168 @@ /// -import {DocumentTypeBuilder, TemplateBuilder} from "umbraco-cypress-testhelpers"; +import { TemplateBuilder } from 'umbraco-cypress-testhelpers'; context('Templates', () => { - beforeEach(() => { - cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); - }); - - it('Create template', () => { - const name = "Test template"; - - cy.umbracoEnsureTemplateNameNotExists(name); + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + function navigateToSettings() { cy.umbracoSection('settings'); cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + } + function createTemplate() { + navigateToSettings(); cy.umbracoTreeItem("settings", ["Templates"]).rightclick(); - cy.umbracoContextMenuAction("action-create").click(); + } + + + it('Create template', () => { + const name = "Test template test"; + cy.umbracoEnsureTemplateNameNotExists(name); + + createTemplate(); //Type name cy.umbracoEditorHeaderName(name); + /* Make an edit, if you don't the file will be create twice, + only happens in testing though, probably because the test is too fast + Certifiably mega wonk regardless */ + cy.get('.ace_text-input').type("var num = 5;", {force:true} ); //Save - cy.get("form[name='contentForm']").submit(); + cy.get('.btn-success').click(); //Assert cy.umbracoSuccessNotification().should('be.visible'); //Clean up cy.umbracoEnsureTemplateNameNotExists(name); - }); + }); + + it('Unsaved changes stay', () => { + const name = "Templates Unsaved Changes Stay test"; + const edit = "var num = 5;"; + cy.umbracoEnsureTemplateNameNotExists(name); + + const template = new TemplateBuilder() + .withName(name) + .withContent('@inherits Umbraco.Web.Mvc.UmbracoViewPage\n') + .build(); + + cy.saveTemplate(template); + + navigateToSettings(); + + // Open partial view + cy.umbracoTreeItem("settings", ["Templates", name]).click(); + // Edit + cy.get('.ace_text-input').type(edit, {force:true} ); + + // Navigate away + cy.umbracoSection('content'); + // Click stay button + cy.get('umb-button[label="Stay"] button:enabled').click(); + + // Assert + // That the same document is open + cy.get('#headerName').should('have.value', name); + cy.get('.ace_content').contains(edit); + + cy.umbracoEnsureTemplateNameNotExists(name); + }); + + it('Discard unsaved changes', () => { + const name = "Discard changes test"; + const edit = "var num = 5;"; - it('Delete template', () => { - const name = "Test template"; cy.umbracoEnsureTemplateNameNotExists(name); const template = new TemplateBuilder() .withName(name) + .withContent('@inherits Umbraco.Web.Mvc.UmbracoViewPage\n') .build(); cy.saveTemplate(template); + navigateToSettings(); + + // Open partial view + cy.umbracoTreeItem("settings", ["Templates", name]).click(); + // Edit + cy.get('.ace_text-input').type(edit, {force:true} ); + + // Navigate away + cy.umbracoSection('content'); + // Click discard + cy.get('umb-button[label="Discard changes"] button:enabled').click(); + // Navigate back cy.umbracoSection('settings'); - cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); - cy.umbracoTreeItem("settings", ["Templates", name]).rightclick(); - cy.umbracoContextMenuAction("action-delete").click(); + // Asserts + cy.get('.ace_content').should('not.contain', edit); + // cy.umbracoPartialViewExists(fileName).then(exists => { expect(exists).to.be.false; }); TODO: Switch to template + cy.umbracoEnsureTemplateNameNotExists(name); + }); + + it('Insert macro', () => { + const name = 'InsertMacroTest'; + + cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureMacroNameNotExists(name); + + const template = new TemplateBuilder() + .withName(name) + .withContent('') + .build(); - cy.umbracoButtonByLabelKey("general_ok").click(); + cy.saveTemplate(template); - cy.contains(name).should('not.exist'); + cy.saveMacro(name); + navigateToSettings(); + cy.umbracoTreeItem("settings", ["Templates", name]).click(); + // Insert macro + cy.umbracoButtonByLabelKey('general_insert').click(); + cy.get('.umb-insert-code-box__title').contains('Macro').click(); + cy.get('.umb-card-grid-item').contains(name).click(); + + // Assert + cy.get('.ace_content').contains('@Umbraco.RenderMacro("' + name + '")').should('exist'); + + // Clean cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureMacroNameNotExists(name); }); + + it('Insert value', () => { + const name = 'Insert Value Test'; + + cy.umbracoEnsureTemplateNameNotExists(name); + + const partialView = new TemplateBuilder() + .withName(name) + .withContent('') + .build(); + + cy.saveTemplate(partialView); + + navigateToSettings(); + cy.umbracoTreeItem("settings", ["Templates", name]).click(); + + // Insert value + cy.umbracoButtonByLabelKey('general_insert').click(); + cy.get('.umb-insert-code-box__title').contains('Value').click(); + cy.get('select').select('umbracoBytes'); + cy.umbracoButtonByLabelKey('general_submit').click(); + + // assert + cy.get('.ace_content').contains('@Model.Value("umbracoBytes")').should('exist'); + + // Clean + cy.umbracoEnsureTemplateNameNotExists(name); + }); + }); diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index 3b4177ce3f8c..867b7f5cf389 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -5,9 +5,9 @@ }, "devDependencies": { "cross-env": "^7.0.2", + "cypress": "^4.12.1", "ncp": "^2.0.0", - "cypress": "^4.9.0", - "umbraco-cypress-testhelpers": "1.0.0-beta-44" + "umbraco-cypress-testhelpers": "^1.0.0-beta-48" }, "dependencies": { "typescript": "^3.9.2" diff --git a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs index 23cc78210680..959e059d59d0 100644 --- a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -154,15 +154,13 @@ public void Convert_Null_Empty() var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; Assert.IsNotNull(converted); - Assert.AreEqual(0, converted.ContentData.Count()); - Assert.AreEqual(0, converted.Layout.Count()); + Assert.AreEqual(0, converted.Count); json = string.Empty; converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; Assert.IsNotNull(converted); - Assert.AreEqual(0, converted.ContentData.Count()); - Assert.AreEqual(0, converted.Layout.Count()); + Assert.AreEqual(0, converted.Count); } [Test] @@ -177,8 +175,7 @@ public void Convert_Valid_Empty_Json() var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; Assert.IsNotNull(converted); - Assert.AreEqual(0, converted.ContentData.Count()); - Assert.AreEqual(0, converted.Layout.Count()); + Assert.AreEqual(0, converted.Count); json = @"{ layout: {}, @@ -186,8 +183,7 @@ public void Convert_Valid_Empty_Json() converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; Assert.IsNotNull(converted); - Assert.AreEqual(0, converted.ContentData.Count()); - Assert.AreEqual(0, converted.Layout.Count()); + Assert.AreEqual(0, converted.Count); // Even though there is a layout, there is no data, so the conversion will result in zero elements in total json = @" @@ -205,8 +201,7 @@ public void Convert_Valid_Empty_Json() converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; Assert.IsNotNull(converted); - Assert.AreEqual(0, converted.ContentData.Count()); - Assert.AreEqual(0, converted.Layout.Count()); + Assert.AreEqual(0, converted.Count); // Even though there is a layout and data, the data is invalid (missing required keys) so the conversion will result in zero elements in total json = @" @@ -228,8 +223,7 @@ public void Convert_Valid_Empty_Json() converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; Assert.IsNotNull(converted); - Assert.AreEqual(0, converted.ContentData.Count()); - Assert.AreEqual(0, converted.Layout.Count()); + Assert.AreEqual(0, converted.Count); // Everthing is ok except the udi reference in the layout doesn't match the data so it will be empty json = @" @@ -252,8 +246,7 @@ public void Convert_Valid_Empty_Json() converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; Assert.IsNotNull(converted); - Assert.AreEqual(1, converted.ContentData.Count()); - Assert.AreEqual(0, converted.Layout.Count()); + Assert.AreEqual(0, converted.Count); } [Test] @@ -283,14 +276,12 @@ public void Convert_Valid_Json() var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; Assert.IsNotNull(converted); - Assert.AreEqual(1, converted.ContentData.Count()); - var item0 = converted.ContentData.ElementAt(0); + Assert.AreEqual(1, converted.Count); + var item0 = converted[0].Content; Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key); Assert.AreEqual("Test1", item0.ContentType.Alias); - Assert.AreEqual(1, converted.Layout.Count()); - var layout0 = converted.Layout.ElementAt(0); - Assert.IsNull(layout0.Settings); - Assert.AreEqual(Udi.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), layout0.ContentUdi); + Assert.IsNull(converted[0].Settings); + Assert.AreEqual(Udi.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), converted[0].ContentUdi); } [Test] @@ -348,17 +339,15 @@ public void Get_Data_From_Layout_Item() var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; Assert.IsNotNull(converted); - Assert.AreEqual(3, converted.ContentData.Count()); - Assert.AreEqual(3, converted.SettingsData.Count()); - Assert.AreEqual(2, converted.Layout.Count()); + Assert.AreEqual(2, converted.Count); - var item0 = converted.Layout.ElementAt(0); + var item0 = converted[0]; Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Content.Key); Assert.AreEqual("Test1", item0.Content.ContentType.Alias); Assert.AreEqual(Guid.Parse("1F613E26CE274898908A561437AF5100"), item0.Settings.Key); Assert.AreEqual("Setting2", item0.Settings.ContentType.Alias); - var item1 = converted.Layout.ElementAt(1); + var item1 = converted[1]; Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Content.Key); Assert.AreEqual("Test2", item1.Content.ContentType.Alias); Assert.AreEqual(Guid.Parse("63027539B0DB45E7B70459762D4E83DD"), item1.Settings.Key); @@ -434,11 +423,9 @@ public void Data_Item_Removed_If_Removed_From_Config() var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; Assert.IsNotNull(converted); - Assert.AreEqual(2, converted.ContentData.Count()); - Assert.AreEqual(0, converted.SettingsData.Count()); - Assert.AreEqual(1, converted.Layout.Count()); + Assert.AreEqual(1, converted.Count); - var item0 = converted.Layout.ElementAt(0); + var item0 = converted[0]; Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item0.Content.Key); Assert.AreEqual("Test2", item0.Content.ContentType.Alias); Assert.IsNull(item0.Settings); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index c2b298ad2409..43598593f441 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -168,6 +168,7 @@ vm.inviteStep = 2; }, function (err) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); formHelper.handleError(err); vm.invitedUserPasswordModel.buttonState = "error"; }); 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 937c7467e4fa..826d6b87fc21 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 @@ -588,6 +588,7 @@ eventsService.emit("content.unpublished", { content: $scope.content }); overlayService.close(); }, function (err) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); $scope.page.buttonGroupState = 'error'; handleHttpException(err); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js index 2cfc6257a205..6e4a388276b4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -188,6 +188,7 @@ Use this directive to construct a header inside the main editor window. @param {string} name The content name. +@param {boolean=} nameRequired Require name to be defined. (True by default) @param {array=} tabs Array of tabs. See example above. @param {array=} navigation Array of sub views. See example above. @param {boolean=} nameLocked Set to true to lock the name. @@ -199,7 +200,7 @@ Use this directive to construct a header inside the main editor window. @param {boolean=} hideAlias Set to true to hide alias. @param {string=} description Add a description to the content. @param {boolean=} hideDescription Set to true to hide description. -@param {boolean=} setpagetitle If true the page title will be set to reflect the type of data the header is working with +@param {boolean=} setpagetitle If true the page title will be set to reflect the type of data the header is working with @param {string=} editorfor The localization to use to aid accessibility on the edit and create screen **/ @@ -207,7 +208,7 @@ Use this directive to construct a header inside the main editor window. 'use strict'; function EditorHeaderDirective(editorService, localizationService, editorState, $rootScope) { - + function link(scope, $injector) { scope.vm = {}; @@ -341,11 +342,11 @@ Use this directive to construct a header inside the main editor window. } scope.accessibility.a11yMessageVisible = !isEmptyOrSpaces(scope.accessibility.a11yMessage); scope.accessibility.a11yNameVisible = !isEmptyOrSpaces(scope.accessibility.a11yName); - + }); } - + function isEmptyOrSpaces(str) { return str === null || str===undefined || str.trim ===''; @@ -360,7 +361,7 @@ Use this directive to construct a header inside the main editor window. }); } - + var directive = { transclude: true, @@ -370,6 +371,7 @@ Use this directive to construct a header inside the main editor window. scope: { name: "=", nameLocked: "=", + nameRequired: "=?", menu: "=", hideActionsMenu: "Added in Umbraco 8.7. Model Object for dealing with data of Block Editors. - * + * * Block Editor Model Object provides the basic features for editing data of a block editor.
* Use the Block Editor Service to instantiate the Model Object.
* See {@link umbraco.services.blockEditorService blockEditorService} - * + * */ (function () { 'use strict'; - function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService) { + function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper) { /** * Simple mapping from property model content entry to editing model, @@ -236,7 +236,7 @@ /** - * Formats the content apps and ensures unsupported property's have the notsupported view (returns a promise) + * Formats the content apps and ensures unsupported property's have the notsupported view * @param {any} scaffold */ function formatScaffoldData(scaffold) { @@ -255,7 +255,7 @@ // could be empty in tests if (!scaffold.apps) { console.warn("No content apps found in scaffold"); - return $q.resolve(scaffold); + return scaffold; } // replace view of content app @@ -271,22 +271,27 @@ scaffold.apps.splice(infoAppIndex, 1); } - // add the settings app - return localizationService.localize("blockEditor_tabBlockSettings").then( - function (settingsName) { - - var settingsTab = { - "name": settingsName, - "alias": "settings", - "icon": "icon-settings", - "view": "views/common/infiniteeditors/blockeditor/blockeditor.settings.html", - "hasError": false - }; - scaffold.apps.push(settingsTab); + return scaffold; + } - return scaffold; - } - ); + /** + * Creates a settings content app, we only want to do this if settings is present on the specific block. + * @param {any} contentModel + */ + function appendSettingsContentApp(contentModel, settingsName) { + if (!contentModel.apps) { + return + } + + // add the settings app + var settingsTab = { + "name": settingsName, + "alias": "settings", + "icon": "icon-settings", + "view": "views/common/infiniteeditors/blockeditor/blockeditor.settings.html", + "hasError": false + }; + contentModel.apps.push(settingsTab); } /** @@ -309,6 +314,8 @@ this.__watchers = []; + this.__labels = {}; + // ensure basic part of data-structure is in place: this.value = propertyModelValue; this.value.layout = this.value.layout || {}; @@ -318,13 +325,25 @@ this.propertyEditorAlias = propertyEditorAlias; this.blockConfigurations = blockConfigurations; + this.blockConfigurations.forEach(blockConfiguration => { + if (blockConfiguration.view != null && blockConfiguration.view !== "") { + blockConfiguration.view = umbRequestHelper.convertVirtualToAbsolutePath(blockConfiguration.view); + } + if (blockConfiguration.stylesheet != null && blockConfiguration.stylesheet !== "") { + blockConfiguration.stylesheet = umbRequestHelper.convertVirtualToAbsolutePath(blockConfiguration.stylesheet); + } + if (blockConfiguration.thumbnail != null && blockConfiguration.thumbnail !== "") { + blockConfiguration.thumbnail = umbRequestHelper.convertVirtualToAbsolutePath(blockConfiguration.thumbnail); + } + }); + this.scaffolds = []; this.isolatedScope = scopeOfExistance.$new(true); this.isolatedScope.blockObjects = {}; this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this))); - this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this))); + this.__watchers.push(propertyEditorScope.$on("formSubmittingFinalPhase", this.sync.bind(this))); }; @@ -344,24 +363,25 @@ // update our values this.value = propertyModelValue; this.value.layout = this.value.layout || {}; - this.value.data = this.value.data || []; + this.value.contentData = this.value.contentData || []; + this.value.settingsData = this.value.settingsData || []; // re-create the watchers this.__watchers = []; this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this))); - this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this))); + this.__watchers.push(propertyEditorScope.$on("formSubmittingFinalPhase", this.sync.bind(this))); }, /** * @ngdoc method * @name getBlockConfiguration * @methodOf umbraco.services.blockEditorModelObject - * @description Get block configuration object for a given contentTypeKey. - * @param {string} key contentTypeKey to recive the configuration model for. - * @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentTypeKey isnt available in the current block configurations. + * @description Get block configuration object for a given contentElementTypeKey. + * @param {string} key contentElementTypeKey to recive the configuration model for. + * @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentElementTypeKey isnt available in the current block configurations. */ getBlockConfiguration: function (key) { - return this.blockConfigurations.find(bc => bc.contentTypeKey === key) || null; + return this.blockConfigurations.find(bc => bc.contentElementTypeKey === key) || null; }, /** @@ -373,12 +393,24 @@ * @returns {Promise} A Promise object which resolves when all scaffold models are loaded. */ load: function () { + + var self = this; + var tasks = []; + tasks.push(localizationService.localize("blockEditor_tabBlockSettings").then( + function (settingsName) { + // self.__labels might not exists anymore, this happens if this instance has been destroyed before the load is complete. + if(self.__labels) { + self.__labels.settingsName = settingsName; + } + } + )); + var scaffoldKeys = []; this.blockConfigurations.forEach(blockConfiguration => { - scaffoldKeys.push(blockConfiguration.contentTypeKey); + scaffoldKeys.push(blockConfiguration.contentElementTypeKey); if (blockConfiguration.settingsElementTypeKey != null) { scaffoldKeys.push(blockConfiguration.settingsElementTypeKey); } @@ -387,19 +419,11 @@ // removing duplicates. scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index); - var self = this; - scaffoldKeys.forEach(contentTypeKey => { tasks.push(contentResource.getScaffoldByKey(-20, contentTypeKey).then(scaffold => { // self.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete. if (self.scaffolds) { - return formatScaffoldData(scaffold).then(s => { - self.scaffolds.push(s); - return s; - }); - } - else { - return $q.resolve(scaffold); + self.scaffolds.push(formatScaffoldData(scaffold)); } })); }); @@ -415,7 +439,7 @@ * @return {Array} array of strings representing alias. */ getAvailableAliasesForBlockContent: function () { - return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentTypeKey).contentTypeAlias); + return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey).contentTypeAlias); }, /** @@ -431,7 +455,7 @@ var blocks = []; this.blockConfigurations.forEach(blockConfiguration => { - var scaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); + var scaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey); if (scaffold) { blocks.push({ blockConfigModel: blockConfiguration, @@ -503,12 +527,12 @@ var contentScaffold; if (blockConfiguration === null) { - console.error("The block entry of " + contentUdi + " is not being initialized because its contentTypeKey is not allowed for this PropertyEditor"); + console.error("The block of " + contentUdi + " is not being initialized because its contentTypeKey('" + dataModel.contentTypeKey + "') is not allowed for this PropertyEditor"); } else { - contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); + contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey); if (contentScaffold === null) { - console.error("The block entry of " + contentUdi + " is not begin initialized cause its Element Type was not loaded."); + console.error("The block of " + contentUdi + " is not begin initialized cause its Element Type was not loaded."); } } @@ -519,13 +543,12 @@ unsupported: true }; contentScaffold = {}; - } var blockObject = {}; // Set an angularJS cloneNode method, to avoid this object begin cloned. blockObject.cloneNode = function () { - return null;// angularJS accept this as a cloned value as long as the + return null;// angularJS accept this as a cloned value as long as the } blockObject.key = String.CreateGuid().replace(/-/g, ""); blockObject.config = Utilities.copy(blockConfiguration); @@ -577,6 +600,9 @@ ensureUdiAndKey(blockObject.settings, settingsUdi); mapToElementModel(blockObject.settings, settingsData); + + // add settings content-app + appendSettingsContentApp(blockObject.content, this.__labels.settingsName); } } @@ -587,8 +613,7 @@ if (this.config.settingsElementTypeKey !== null) { mapElementValues(settings, this.settings); } - } - + }; blockObject.sync = function () { if (this.content !== null) { @@ -597,7 +622,7 @@ if (this.config.settingsElementTypeKey !== null) { mapToPropertyModel(this.settings, this.settingsData); } - } + }; // first time instant update of label. blockObject.label = getBlockLabel(blockObject); @@ -623,7 +648,7 @@ // remove model from isolatedScope. delete this.__scope.blockObjects["_" + this.key]; - // NOTE: It seems like we should call this.__scope.$destroy(); since that is the only way to remove a scope from it's parent, + // NOTE: It seems like we should call this.__scope.$destroy(); since that is the only way to remove a scope from it's parent, // however that is not the case since __scope is actually this.isolatedScope which gets cleaned up when the outer scope is // destroyed. If we do that here it breaks the scope chain and validation. delete this.__scope; @@ -636,7 +661,6 @@ } return blockObject; - }, /** @@ -691,18 +715,18 @@ * @name create * @methodOf umbraco.services.blockEditorModelObject * @description Create a empty layout entry, notice the layout entry is not added to the property editors model layout object, since the layout sturcture depends on the property editor. - * @param {string} contentTypeKey the contentTypeKey of the block you wish to create, if contentTypeKey is not avaiable in the block configuration then ´null´ will be returned. - * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentTypeKey is unavaiaible. + * @param {string} contentElementTypeKey the contentElementTypeKey of the block you wish to create, if contentElementTypeKey is not avaiable in the block configuration then ´null´ will be returned. + * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentElementTypeKey is unavaiaible. */ - create: function (contentTypeKey) { + create: function (contentElementTypeKey) { - var blockConfiguration = this.getBlockConfiguration(contentTypeKey); + var blockConfiguration = this.getBlockConfiguration(contentElementTypeKey); if (blockConfiguration === null) { return null; } var entry = { - contentUdi: createDataEntry(contentTypeKey, this.value.contentData) + contentUdi: createDataEntry(contentElementTypeKey, this.value.contentData) } if (blockConfiguration.settingsElementTypeKey != null) { @@ -723,14 +747,14 @@ elementTypeDataModel = Utilities.copy(elementTypeDataModel); - var contentTypeKey = elementTypeDataModel.contentTypeKey; + var contentElementTypeKey = elementTypeDataModel.contentTypeKey; - var layoutEntry = this.create(contentTypeKey); + var layoutEntry = this.create(contentElementTypeKey); if (layoutEntry === null) { return null; } - var dataModel = getDataByUdi(layoutEntry.udi, this.value.contentData); + var dataModel = getDataByUdi(layoutEntry.contentUdi, this.value.contentData); if (dataModel === null) { return null; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index cb583546a53f..0d2ca6623ba8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -11,13 +11,13 @@ * */ function clipboardService(notificationsService, eventsService, localStorageService, iconHelper) { - + var clearPropertyResolvers = []; - + var STORAGE_KEY = "umbClipboardService"; - + var retriveStorage = function() { if (localStorageService.isSupported === false) { return null; @@ -27,32 +27,32 @@ function clipboardService(notificationsService, eventsService, localStorageServi if (dataString != null) { dataJSON = JSON.parse(dataString); } - + if(dataJSON == null) { dataJSON = new Object(); } - + if(dataJSON.entries === undefined) { dataJSON.entries = []; } - + return dataJSON; } - + var saveStorage = function(storage) { var storageString = JSON.stringify(storage); - + try { var storageJSON = JSON.parse(storageString); localStorageService.set(STORAGE_KEY, storageString); - + eventsService.emit("clipboardService.storageUpdate"); - + return true; } catch(e) { return false; } - + return false; } @@ -86,17 +86,17 @@ function clipboardService(notificationsService, eventsService, localStorageServi var isEntryCompatible = function(entry, type, allowedAliases) { return entry.type === type - && + && ( (entry.alias && allowedAliases.filter(allowedAlias => allowedAlias === entry.alias).length > 0) - || + || (entry.aliases && entry.aliases.filter(entryAlias => allowedAliases.filter(allowedAlias => allowedAlias === entryAlias).length > 0).length === entry.aliases.length) ); } - - + + var service = {}; - + /** * @ngdoc method @@ -160,29 +160,29 @@ function clipboardService(notificationsService, eventsService, localStorageServi * Saves a single JS-object with a type and alias to the clipboard. */ service.copy = function(type, alias, data, displayLabel, displayIcon, uniqueKey, firstLevelClearupMethod) { - + var storage = retriveStorage(); displayLabel = displayLabel || data.name; displayIcon = displayIcon || iconHelper.convertFromLegacyIcon(data.icon); uniqueKey = uniqueKey || data.key || console.error("missing unique key for this content"); - + // remove previous copies of this entry: storage.entries = storage.entries.filter( (entry) => { return entry.unique !== uniqueKey; } ); - - var entry = {unique:uniqueKey, type:type, alias:alias, data:prepareEntryForStorage(data, firstLevelClearupMethod), label:displayLabel, icon:displayIcon}; + + var entry = {unique:uniqueKey, type:type, alias:alias, data:prepareEntryForStorage(data, firstLevelClearupMethod), label:displayLabel, icon:displayIcon, date:Date.now()}; storage.entries.push(entry); - + if (saveStorage(storage) === true) { notificationsService.success("Clipboard", "Copied to clipboard."); } else { notificationsService.error("Clipboard", "Couldnt copy this data to clipboard."); } - + }; @@ -203,32 +203,31 @@ function clipboardService(notificationsService, eventsService, localStorageServi * Saves a single JS-object with a type and alias to the clipboard. */ service.copyArray = function(type, aliases, datas, displayLabel, displayIcon, uniqueKey, firstLevelClearupMethod) { - + var storage = retriveStorage(); - + // Clean up each entry var copiedDatas = datas.map(data => prepareEntryForStorage(data, firstLevelClearupMethod)); - + // remove previous copies of this entry: storage.entries = storage.entries.filter( (entry) => { return entry.unique !== uniqueKey; } ); - - var entry = {unique:uniqueKey, type:type, aliases:aliases, data:copiedDatas, label:displayLabel, icon:displayIcon}; + var entry = {unique:uniqueKey, type:type, aliases:aliases, data:copiedDatas, label:displayLabel, icon:displayIcon, date:Date.now()}; storage.entries.push(entry); - + if (saveStorage(storage) === true) { notificationsService.success("Clipboard", "Copied to clipboard."); } else { notificationsService.error("Clipboard", "Couldnt copy this data to clipboard."); } - + }; - - + + /** * @ngdoc method * @name umbraco.services.supportsCopy#supported @@ -240,7 +239,7 @@ function clipboardService(notificationsService, eventsService, localStorageServi service.isSupported = function() { return localStorageService.isSupported; }; - + /** * @ngdoc method * @name umbraco.services.supportsCopy#hasEntriesOfType @@ -253,14 +252,14 @@ function clipboardService(notificationsService, eventsService, localStorageServi * Determines whether the current clipboard has entries that match a given type and one of the aliases. */ service.hasEntriesOfType = function(type, aliases) { - + if(service.retriveEntriesOfType(type, aliases).length > 0) { return true; } - + return false; }; - + /** * @ngdoc method * @name umbraco.services.supportsCopy#retriveEntriesOfType @@ -268,24 +267,24 @@ function clipboardService(notificationsService, eventsService, localStorageServi * * @param {string} type A string defining the type of data to recive. * @param {string} aliases A array of strings providing the alias of the data you want to recive. - * + * * @description * Returns an array of entries matching the given type and one of the provided aliases. */ service.retriveEntriesOfType = function(type, allowedAliases) { - + var storage = retriveStorage(); - + // Find entries that are fulfilling the criteria for this nodeType and nodeTypesAliases. var filteretEntries = storage.entries.filter( (entry) => { return isEntryCompatible(entry, type, allowedAliases); } ); - + return filteretEntries; }; - + /** * @ngdoc method * @name umbraco.services.supportsCopy#retriveEntriesOfType @@ -293,14 +292,14 @@ function clipboardService(notificationsService, eventsService, localStorageServi * * @param {string} type A string defining the type of data to recive. * @param {string} aliases A array of strings providing the alias of the data you want to recive. - * + * * @description * Returns an array of data of entries matching the given type and one of the provided aliases. */ service.retriveDataOfType = function(type, aliases) { return service.retriveEntriesOfType(type, aliases).map((x) => x.data); }; - + /** * @ngdoc method * @name umbraco.services.supportsCopy#retriveEntriesOfType @@ -308,12 +307,12 @@ function clipboardService(notificationsService, eventsService, localStorageServi * * @param {string} type A string defining the type of data to remove. * @param {string} aliases A array of strings providing the alias of the data you want to remove. - * + * * @description * Removes entries matching the given type and one of the provided aliases. */ service.clearEntriesOfType = function(type, allowedAliases) { - + var storage = retriveStorage(); // Find entries that are NOT fulfilling the criteria for this nodeType and nodeTypesAliases. @@ -322,14 +321,14 @@ function clipboardService(notificationsService, eventsService, localStorageServi return !isEntryCompatible(entry, type, allowedAliases); } ); - + storage.entries = filteretEntries; saveStorage(storage); }; - - - + + + return service; } 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 1fbc438a1f54..6d41ea087d00 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 @@ -117,6 +117,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt return $q.resolve(data); }, function (err) { + + formHelper.resetForm({ scope: args.scope, hasErrors: true }); + self.handleSaveError({ showNotifications: args.showNotifications, softRedirect: args.softRedirect, 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 d9c11770cc58..bd6bbcc5b351 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 @@ -17,10 +17,10 @@ function formHelper(angularHelper, serverValidationManager, notificationsService * @function * * @description - * Called by controllers when submitting a form - this ensures that all client validation is checked, + * Called by controllers when submitting a form - this ensures that all client validation is checked, * server validation is cleared, that the correct events execute and status messages are displayed. * This returns true if the form is valid, otherwise false if form submission cannot continue. - * + * * @param {object} args An object containing arguments for form submission */ submitForm: function (args) { @@ -46,7 +46,12 @@ function formHelper(angularHelper, serverValidationManager, notificationsService args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action }); this.focusOnFirstError(currentForm); - args.scope.$broadcast("postFormSubmitting", { scope: args.scope, action: args.action }); + + // Some property editors need to perform an action after all property editors have reacted to the formSubmitting. + args.scope.$broadcast("formSubmittingFinalPhase", { scope: args.scope, action: args.action }); + + // Set the form state to submitted + currentForm.$setSubmitted(); //then check if the form is valid if (!args.skipValidation) { @@ -101,18 +106,32 @@ function formHelper(angularHelper, serverValidationManager, notificationsService * * @description * Called by controllers when a form has been successfully submitted, this ensures the correct events are raised. - * + * * @param {object} args An object containing arguments for form submission */ resetForm: function (args) { + + var currentForm; + if (!args) { throw "args cannot be null"; } if (!args.scope) { throw "args.scope cannot be null"; } + if (!args.formCtrl) { + //try to get the closest form controller + currentForm = angularHelper.getRequiredCurrentForm(args.scope); + } + else { + currentForm = args.formCtrl; + } - args.scope.$broadcast("formSubmitted", { scope: args.scope }); + // Set the form state to pristine + currentForm.$setPristine(); + currentForm.$setUntouched(); + + args.scope.$broadcast(args.hasErrors ? "formSubmittedValidationFailed" : "formSubmitted", { scope: args.scope }); }, showNotifications: function (args) { @@ -137,7 +156,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService * @description * Needs to be called when a form submission fails, this will wire up all server validation errors in ModelState and * add the correct messages to the notifications. If a server error has occurred this will show a ysod. - * + * * @param {object} err The error object returned from the http promise */ handleError: function (err) { @@ -176,7 +195,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService * * @description * This wires up all of the server validation model state so that valServer and valServerField directives work - * + * * @param {object} err The error object returned from the http promise */ handleServerValidation: function (modelState) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index cc529abb5696..78c8b5fa88d1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -15,7 +15,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe * * @description * This will convert a virtual path (i.e. ~/App_Plugins/Blah/Test.html ) to an absolute path - * + * * @param {string} a virtual path, if this is already an absolute path it will just be returned, if this is a relative path an exception will be thrown */ convertVirtualToAbsolutePath: function(virtualPath) { @@ -31,6 +31,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe return Umbraco.Sys.ServerVariables.application.applicationPath + virtualPath.trimStart("~/"); }, + /** * @ngdoc method * @name umbraco.services.umbRequestHelper#dictionaryToQueryString @@ -39,7 +40,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe * * @description * This will turn an array of key/value pairs or a standard dictionary into a query string - * + * * @param {Array} queryStrings An array of key/value pairs */ dictionaryToQueryString: function (queryStrings) { @@ -76,9 +77,9 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe * * @description * This will return the webapi Url for the requested key based on the servervariables collection - * + * * @param {string} apiName The webapi name that is found in the servervariables["umbracoUrls"] dictionary - * @param {string} actionName The webapi action name + * @param {string} actionName The webapi action name * @param {object} queryStrings Can be either a string or an array containing key/value pairs */ getApiUrl: function (apiName, actionName, queryStrings) { @@ -103,7 +104,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe * * @description * This returns a promise with an underlying http call, it is a helper method to reduce - * the amount of duplicate code needed to query http resources and automatically handle any + * the amount of duplicate code needed to query http resources and automatically handle any * Http errors. See /docs/source/using-promises-resources.md * * @param {object} opts A mixed object which can either be a string representing the error message to be @@ -117,7 +118,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status } */ resourcePromise: function (httpPromise, opts) { - + /** The default success callback used if one is not supplied in the opts */ function defaultSuccess(data, status, headers, config) { //when it's successful, just return the data @@ -151,7 +152,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe return httpPromise.then(function (response) { - //invoke the callback + //invoke the callback var result = callbacks.success.apply(this, [response.data, response.status, response.headers, response.config]); formHelper.showNotifications(response.data); @@ -183,7 +184,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe overlayService.ysod(error); } else { - //show a simple error notification + //show a simple error notification notificationsService.error("Server error", "Contact administrator, see log for full details.
" + result.errorMsg + ""); } @@ -209,7 +210,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe * * @description * Used for saving content/media/members specifically - * + * * @param {Object} args arguments object * @returns {Promise} http promise object. */ @@ -233,7 +234,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe if (args.showNotifications === null || args.showNotifications === undefined) { args.showNotifications = true; } - + //save the active tab id so we can set it when the data is returned. var activeTab = _.find(args.content.tabs, function (item) { return item.active; @@ -298,7 +299,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe overlayService.ysod(error); } else { - //show a simple error notification + //show a simple error notification notificationsService.error("Server error", "Contact administrator, see log for full details.
" + response.data.ExceptionMessage + ""); } @@ -329,7 +330,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe }); } else if (!jsonData.key || !jsonData.value) { throw "jsonData object must have both a key and a value property"; } - + return $http({ method: 'POST', url: url, @@ -364,7 +365,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe return $q.reject(response); }); }, - + /** * @ngdoc method * @name umbraco.resources.contentResource#downloadFile @@ -372,7 +373,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe * * @description * Downloads a file to the client using AJAX/XHR - * + * * @param {string} httpPath the path (url) to the resource being downloaded * @returns {Promise} http promise object. */ @@ -386,7 +387,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe // Use an arraybuffer return $http.get(httpPath, { responseType: 'arraybuffer' }) .then(function (response) { - + var octetStreamMime = 'application/octet-stream'; var success = false; diff --git a/src/Umbraco.Web.UI.Client/src/less/alerts.less b/src/Umbraco.Web.UI.Client/src/less/alerts.less index 3907b59f58de..3539e2106413 100644 --- a/src/Umbraco.Web.UI.Client/src/less/alerts.less +++ b/src/Umbraco.Web.UI.Client/src/less/alerts.less @@ -7,6 +7,7 @@ // ------------------------- .alert { + position: relative; padding: 8px 35px 8px 14px; margin-bottom: @baseLineHeight; background-color: @warningBackground; @@ -98,3 +99,29 @@ .alert-block p + p { margin-top: 5px; } + + +// Property error alerts +// ------------------------- +.alert.property-error { + + display: inline-block; + font-size: 14px; + padding: 6px 16px 6px 12px; + margin-bottom: 6px; + + &::after { + content:''; + position: absolute; + bottom:-6px; + left: 6px; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 6px solid; + } + &.alert-error::after { + border-top-color: @errorBackground; + } +} 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 index 0ebe0513fcad..95625d9e73bd 100644 --- 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 @@ -50,6 +50,19 @@ button.umb-variant-switcher__toggle { font-weight: bold; background-color: @errorBackground; color: @errorText; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-variant-switcher__toggle--badge-bounce; + animation-timing-function: ease; + @keyframes umb-variant-switcher__toggle--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-6px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-3px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } } } } @@ -241,6 +254,19 @@ button.umb-variant-switcher__toggle { font-weight: bold; background-color: @errorBackground; color: @errorText; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-variant-switcher__name--badge-bounce; + animation-timing-function: ease; + @keyframes umb-variant-switcher__name--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-6px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-3px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } } } } 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 315436591a73..641e0dc7a71f 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 @@ -53,6 +53,39 @@ height: 4px; } } + + // Validation + .show-validation &.-has-error { + color: @red; + + &:hover { + color: @red !important; + } + + &::before { + background-color: @red; + } + + &:not(.is-active) { + .badge { + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-sub-views-nav-item--badge-bounce; + animation-timing-function: ease; + @keyframes umb-sub-views-nav-item--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-6px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-3px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + } + .badge.--error-badge { + display: block; + } + } + } } &__action:active, @@ -101,6 +134,10 @@ height: 12px; min-width: 12px; } + &.--error-badge { + display: none; + font-weight: 900; + } } &-text { @@ -182,13 +219,3 @@ } } } - -// Validation -.show-validation .umb-sub-views-nav-item__action.-has-error, -.show-validation .umb-sub-views-nav-item > a.-has-error { - color: @red; - - &::before { - background-color: @red; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index da46e51b9e8c..3bf00fb25ce0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -272,6 +272,7 @@ label:not([for]) { /* CONTROL VALIDATION */ .umb-control-required { color: @controlRequiredColor; + font-weight: 900; } .controls-row { diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index a87080a326cb..9739a90dae7a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -138,7 +138,9 @@ // additional targetting of the ng-invalid class. .formFieldState(@textColor: @gray-4, @borderColor: @gray-7, @backgroundColor: @gray-10) { // Set the text color - .control-label, + > .control-label, + > .umb-el-wrap > .control-label, + > .umb-el-wrap > .control-header > .control-label, .help-block, .help-inline { color: @textColor; diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 3f3ed2e37620..cab0745a427e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -481,7 +481,7 @@ @formErrorText: @errorBackground; @formErrorBackground: lighten(@errorBackground, 55%); -@formErrorBorder: darken(spin(@errorBackground, -10), 3%); +@formErrorBorder: @red; @formSuccessText: @successBackground; @formSuccessBackground: lighten(@successBackground, 48%); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js index f515cbb4bab8..a08a05b0f7eb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js @@ -1,6 +1,6 @@ angular.module("umbraco") .controller("Umbraco.Editors.BlockEditorController", - function ($scope, localizationService, formHelper) { + function ($scope, localizationService, formHelper, overlayService) { var vm = this; vm.model = $scope.model; @@ -23,17 +23,14 @@ angular.module("umbraco") if (contentApp) { if (vm.model.hideContent) { apps.splice(apps.indexOf(contentApp), 1); - } else if (vm.model.openSettings !== true) { - contentApp.active = true; } + contentApp.active = (vm.model.openSettings !== true); } if (vm.model.settings && vm.model.settings.variants) { var settingsApp = apps.find(entry => entry.alias === "settings"); if (settingsApp) { - if (vm.model.openSettings) { - settingsApp.active = true; - } + settingsApp.active = (vm.model.openSettings === true); } } @@ -42,6 +39,7 @@ angular.module("umbraco") vm.submitAndClose = function () { if (vm.model && vm.model.submit) { + // always keep server validations since this will be a nested editor and server validations are global if (formHelper.submitForm({ scope: $scope, @@ -49,13 +47,16 @@ angular.module("umbraco") keepServerValidation: true })) { vm.model.submit(vm.model); + vm.saveButtonState = "success"; + } else { + vm.saveButtonState = "error"; } } } vm.close = function () { if (vm.model && vm.model.close) { - // TODO: At this stage there could very well have been server errors that have been cleared + // TODO: At this stage there could very well have been server errors that have been cleared // but if we 'close' we are basically cancelling the value changes which means we'd want to cancel // all of the server errors just cleared. It would be possible to do that but also quite annoying. // The rudimentary way would be to: @@ -67,6 +68,29 @@ angular.module("umbraco") // * It would have a 'commit' method to commit the removed errors - which we would call in the formHelper.submitForm when it's successful // * It would have a 'rollback' method to reset the removed errors - which we would call here + + if (vm.blockForm.$dirty === true) { + localizationService.localizeMany(["prompt_discardChanges", "blockEditor_blockHasChanges"]).then(function (localizations) { + const confirm = { + title: localizations[0], + view: "default", + content: localizations[1], + submitButtonLabelKey: "general_discard", + submitButtonStyle: "danger", + closeButtonLabelKey: "general_cancel", + submit: function () { + overlayService.close(); + vm.model.close(vm.model); + }, + close: function () { + overlayService.close(); + } + }; + overlayService.open(confirm); + }); + + return; + } // TODO: check if content/settings has changed and ask user if they are sure. vm.model.close(vm.model); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html index de18f13d2c2e..2367771804ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html @@ -6,6 +6,7 @@ @@ -44,7 +46,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html index fb7e946ee78c..6dea4debb61d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html @@ -69,6 +69,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js index 8248d1186374..115e2dd6a327 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js @@ -160,7 +160,7 @@ angular.module("umbraco") }, 2000); }, function (err) { - + formHelper.resetForm({ scope: $scope, hasErrors: true }); formHelper.handleError(err); $scope.changePasswordButtonState = "error"; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less index e4953999fdcf..58794461df01 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less +++ b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less @@ -9,7 +9,7 @@ /* Grid Setup */ display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - grid-auto-rows: minmax(200px, auto); + grid-auto-rows: minmax(160px, auto); grid-gap: 20px; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.html b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.html index f8ccccd1663e..c9cfcb825ed0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.html @@ -1,5 +1,5 @@ -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less index d1cde628e489..3afa32d099a0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less +++ b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less @@ -75,14 +75,14 @@ umb-block-card { .__info { width: 100%; background-color: #fff; - padding-bottom: 6px; + padding-bottom: 11px;// 10 + 1 to compentiate for the -1 substraction in margin-bottom. .__name { font-weight: bold; font-size: 14px; color: @ui-action-type; margin-left: 16px; - margin-top: 8px; + margin-top: 10px; margin-bottom: -1px; } .__subname { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umbBlockCard.component.js b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umbBlockCard.component.js index 41ae03561782..761e7c28aea7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umbBlockCard.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umbBlockCard.component.js @@ -14,9 +14,38 @@ } }); - function BlockCardController() { - + function BlockCardController($scope, umbRequestHelper) { + var vm = this; + vm.styleBackgroundImage = "none"; + + var unwatch = $scope.$watch("vm.blockConfigModel.thumbnail", (newValue, oldValue) => { + if(newValue !== oldValue) { + vm.updateThumbnail(); + } + }); + + vm.$onInit = function () { + + vm.updateThumbnail(); + + } + vm.$onDestroy = function () { + unwatch(); + } + + vm.updateThumbnail = function () { + if (vm.blockConfigModel.thumbnail == null || vm.blockConfigModel.thumbnail === "") { + vm.styleBackgroundImage = "none"; + return; + } + + var path = umbRequestHelper.convertVirtualToAbsolutePath(vm.blockConfigModel.thumbnail); + if (path.toLowerCase().endsWith(".svg") === false) { + path += "?upscale=false&width=400"; + } + vm.styleBackgroundImage = 'url(\''+path+'\')'; + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html index a7a48166c4bf..1f4a47d9472d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html @@ -48,8 +48,8 @@ umb-auto-focus focus-on-filled="true" val-server-field="Name" - required - aria-required="true" + ng-required="nameRequired != null ? nameRequired : true" + aria-required="{{nameRequired != null ? nameRequired : true}}" aria-invalid="{{contentForm.headerNameForm.headerName.$invalid ? true : false}}" autocomplete="off" maxlength="255"/> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html index 484e0175c560..9e5669f443e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html @@ -9,6 +9,7 @@ {{ vm.item.name }}
{{vm.item.badge.count}}
+
!

diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html index 9693f6931152..e854f727173b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html @@ -105,7 +105,7 @@ * + ng-if="!scheduleSelectorForm.$invalid && !(variant.notifications && variant.notifications.length > 0)"> - @@ -181,12 +181,9 @@
-
- - diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html index 4496956d7975..5ab32abf001f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html @@ -47,12 +47,9 @@ -
- - diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/create.controller.js index 15e1a9402eb3..9bc602678edd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/create.controller.js @@ -28,10 +28,11 @@ function DataTypeCreateController($scope, $location, navigationService, dataType var currPath = node.path ? node.path : "-1"; navigationService.syncTree({ tree: "datatypes", path: currPath + "," + folderId, forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderFor }); }, function(err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderFor, hasErrors: true }); // TODO: Handle errors }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js index 66983bbc05a5..a2af8c82392f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js @@ -128,6 +128,7 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic }, function(err) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); //NOTE: in the case of data type values we are setting the orig/new props // to be the same thing since that only really matters for content/media. contentEditingHelper.handleSaveError({ diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/delete.html index a64280669f1f..e27433c739ea 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/delete.html @@ -112,10 +112,10 @@
+ on-confirm="vm.performDelete" + on-cancel="vm.cancel" + confirm-button-style="danger" + confirm-label-key="general_delete"> diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js index 03a4bc85cf6b..716d994809e9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js @@ -27,13 +27,14 @@ function DictionaryCreateController($scope, $location, dictionaryResource, navig navigationService.syncTree({ tree: "dictionary", path: currPath + "," + data, forceReload: true, activate: true }); // reset form state - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createDictionaryForm }); // navigate to edit view var currentSection = appState.getSectionState("currentSection"); $location.path("/" + currentSection + "/dictionary/edit/" + data); }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createDictionaryForm, hasErrors: true }); if (err.data && err.data.message) { notificationsService.error(err.data.message); navigationService.hideMenu(); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js index ea1ca00b21f3..7619c7abfc1c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js @@ -86,7 +86,7 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes dictionaryResource.save(vm.content, vm.nameDirty) .then(function (data) { - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + formHelper.resetForm({ scope: $scope }); bindDictionary(data); @@ -94,6 +94,8 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes }, function (err) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); + contentEditingHelper.handleSaveError({ err: err }); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js index 732aa898a70c..2348b43852d6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js @@ -47,12 +47,13 @@ function DocumentTypesCreateController($scope, $location, navigationService, con activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm }); var section = appState.getSectionState("currentSection"); }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm, hasErrors: true }); $scope.error = err; }); @@ -83,9 +84,7 @@ function DocumentTypesCreateController($scope, $location, navigationService, con $location.search('create', null); $location.search('notemplate', null); - formHelper.resetForm({ - scope: $scope - }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm }); var section = appState.getSectionState("currentSection"); @@ -94,6 +93,7 @@ function DocumentTypesCreateController($scope, $location, navigationService, con }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm, hasErrors: true }); $scope.error = err; //show any notifications diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 123e19751175..c32c4d56c3e9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -161,7 +161,7 @@ }, function (err) { vm.page.saveButtonState = "error"; - + formHelper.resetForm({ scope: $scope, hasErrors: true }); formHelper.handleError(err); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js index e2559741a2ea..e8c5c550d054 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js @@ -25,7 +25,7 @@ function MacrosCreateController($scope, $location, macroResource, navigationServ navigationService.syncTree({ tree: "macros", path: currPath + "," + data, forceReload: true, activate: true }); // reset form state - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createMacroForm }); // navigate to edit view var currentSection = appState.getSectionState("currentSection"); @@ -33,6 +33,7 @@ function MacrosCreateController($scope, $location, macroResource, navigationServ }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createMacroForm, hasErrors: true }); if (err.data && err.data.message) { notificationsService.error(err.data.message); navigationService.hideMenu(); diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js index 3261739d362f..127f5669950b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js @@ -33,10 +33,11 @@ function MacrosEditController($scope, $q, $routeParams, macroResource, editorSta vm.page.saveButtonState = "busy"; macroResource.saveMacro(vm.macro).then(function (data) { - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + formHelper.resetForm({ scope: $scope }); bindMacro(data); vm.page.saveButtonState = "success"; }, function (error) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); contentEditingHelper.handleSaveError({ err: error }); diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index 931aee7e2d16..f41f22a1a9c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -208,6 +208,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat }, function(err) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); contentEditingHelper.handleSaveError({ err: err, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.controller.js index 8ff9106def7f..3b7e16384027 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.controller.js @@ -30,11 +30,12 @@ function MediaTypesCreateController($scope, $location, navigationService, mediaT var currPath = node.path ? node.path : "-1"; navigationService.syncTree({ tree: "mediatypes", path: currPath + "," + folderId, forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm }); var section = appState.getSectionState("currentSection"); - }, function(err) { + }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm, hasErrors: true }); $scope.error = err; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js index 9d756861ca86..25735d59082c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js @@ -228,7 +228,7 @@ function MemberEditController($scope, $routeParams, $location, $http, $q, appSta } }, function(err) { - + formHelper.resetForm({ scope: $scope, hasErrors: true }); contentEditingHelper.handleSaveError({ err: err, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) diff --git a/src/Umbraco.Web.UI.Client/src/views/membergroups/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/membergroups/edit.controller.js index 00c6dfbba82b..4602a5aa25f8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membergroups/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/membergroups/edit.controller.js @@ -88,6 +88,7 @@ function MemberGroupsEditController($scope, $routeParams, appState, navigationSe }, function (err) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); contentEditingHelper.handleSaveError({ err: err }); diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/membertypes/create.controller.js index adf6cbc8a60e..d87544282733 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/create.controller.js @@ -31,10 +31,10 @@ function MemberTypesCreateController($scope, $location, navigationService, membe var currPath = node.path ? node.path : "-1"; navigationService.syncTree({ tree: "membertypes", path: currPath + "," + folderId, forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm }); }, function(err) { - + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm, hasErrors: true }); // TODO: Handle errors }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js index 63750ff0f218..de8ad6d1c497 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js @@ -194,7 +194,7 @@ vm.package = updatedPackage; vm.buttonState = "success"; - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: editPackageForm }); if (create) { //if we are creating, then redirect to the correct url and reload @@ -204,6 +204,7 @@ } }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: editPackageForm, hasErrors: true }); formHelper.handleError(err); vm.buttonState = "error"; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/edit.html b/src/Umbraco.Web.UI.Client/src/views/packages/edit.html index b99a5fa3c91e..ce65d1cff648 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/edit.html @@ -131,7 +131,7 @@ @@ -140,7 +140,7 @@
@@ -148,8 +148,8 @@
-
@@ -157,8 +157,8 @@
-
@@ -167,7 +167,7 @@
@@ -176,7 +176,7 @@
@@ -185,7 +185,7 @@
@@ -194,7 +194,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/create.controller.js index 52b58d094db5..a843f420c88e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialviewmacros/create.controller.js @@ -46,12 +46,13 @@ activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: form }); var section = appState.getSectionState("currentSection"); }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: form, hasErrors: true }); vm.createFolderError = err; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviews/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialviews/create.controller.js index fbde1d5a075c..97aa059806e1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviews/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialviews/create.controller.js @@ -56,12 +56,13 @@ activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: form }); var section = appState.getSectionState("currentSection"); }, function(err) { + formHelper.resetForm({ scope: $scope, formCtrl: form, hasErrors: true }); vm.createFolderError = err; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html index 8c3bced5735a..7a4978320348 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html @@ -1 +1 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js index c2c86ca8242b..06c14248382c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js @@ -12,8 +12,8 @@ // boardcast the formSubmitting event to trigger syncronization or none-live property-editors $scope.$broadcast("formSubmitting", { scope: $scope }); // Some property editors need to performe an action after all property editors have reacted to the formSubmitting. - $scope.$broadcast("postFormSubmitting", { scope: $scope }); - + $scope.$broadcast("formSubmittingFinalPhase", { scope: $scope }); + block.active = false; } else { $scope.api.activateBlock(block); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html index 360eeed8c066..6cf717df8112 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html @@ -5,8 +5,8 @@ ng-click="vm.openBlock(block)" ng-focus="block.focus"> - - {{block.label}} + + {{block.label}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less index 2be20946b8b6..08306deeba85 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less @@ -22,6 +22,7 @@ background-color: white; .caret { + vertical-align: middle; transform: rotate(-90deg); transition: transform 80ms ease-out; } @@ -32,7 +33,8 @@ vertical-align: middle; } - span { + span.name { + position: relative; display: inline-block; vertical-align: middle; } @@ -54,10 +56,55 @@ } } - &.--error { - border-color: @formErrorBorder !important; + + ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & { + > button { + color: @formErrorText; + span.caret { + border-top-color: @formErrorText; + } + } + } + ng-form.ng-invalid-val-server-match-content > .umb-block-list__block:not(.--active) > .umb-block-list__block--content > div > & { + > button { + span.name { + &::after { + content: "!"; + text-align: center; + position: absolute; + top: -6px; + right: -15px; + min-width: 10px; + color: @white; + background-color: @ui-active-type; + border: 2px solid @white; + border-radius: 50%; + font-size: 10px; + font-weight: bold; + padding: 2px; + line-height: 10px; + background-color: @formErrorText; + font-weight: 900; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: blockelement-inlineblock-editor--badge-bounce; + animation-timing-function: ease; + @keyframes blockelement-inlineblock-editor--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-4px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-2px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + } + } + } } } + + .blockelement-inlineblock-editor__inner { border-top: 1px solid @gray-8; background-color: @gray-12; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html index 4aacc32c6245..5e81efec8bb6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html @@ -3,6 +3,6 @@ ng-focus="block.focus" ng-class="{ '--active': block.active, '--error': parentForm.$invalid && valFormManager.isShowingValidation() }" val-server-property-class=""> - + {{block.label}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less index 51fb7242ef1a..f589249f97fa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less @@ -24,6 +24,7 @@ } span { + position: relative; display: inline-block; vertical-align: middle; } @@ -39,7 +40,42 @@ background-color: @ui-active; } - &.--error { - border-color: @formErrorBorder !important; + ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & { + color: @formErrorText; + } + ng-form.ng-invalid-val-server-match-content > .umb-block-list__block:not(.--active) > .umb-block-list__block--content > div > & { + span { + &::after { + content: "!"; + text-align: center; + position: absolute; + top: -6px; + right: -15px; + min-width: 10px; + color: @white; + background-color: @ui-active-type; + border: 2px solid @white; + border-radius: 50%; + font-size: 10px; + font-weight: bold; + padding: 2px; + line-height: 10px; + background-color: @formErrorText; + font-weight: 900; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: blockelement-inlineblock-editor--badge-bounce; + animation-timing-function: ease; + @keyframes blockelement-inlineblock-editor--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-4px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-2px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + } + } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html index 84d5dd17b7fb..d860b44b60bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html @@ -1,12 +1,12 @@
- + {{block.label}}
This Block is no longer supported in this context.
You might want to remove this block, or contact your developer to take actions for making this block available again.

- Learn about this circumstance + Learn about this circumstance
Block data:

     
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js index dd9fc7f83db0..ba0d4415f54e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js @@ -51,7 +51,7 @@ vm.requestRemoveBlockByIndex = function (index) { localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "blockEditor_confirmDeleteBlockNotice"]).then(function (data) { - var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentTypeKey); + var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentElementTypeKey); overlayService.confirmDelete({ title: data[0], content: localizationService.tokenReplace(data[1], [contentElementType.name]), @@ -70,19 +70,19 @@ vm.removeBlockByIndex = function (index) { $scope.model.value.splice(index, 1); }; - + vm.sortableOptions = { "ui-floating": true, items: "umb-block-card", cursor: "grabbing", placeholder: 'umb-block-card --sortable-placeholder' }; - + vm.getAvailableElementTypes = function () { return vm.elementTypes.filter(function (type) { return !$scope.model.value.find(function (entry) { - return type.key === entry.contentTypeKey; + return type.key === entry.contentElementTypeKey; }); }); }; @@ -99,14 +99,14 @@ //we have to add the 'alias' property to the objects, to meet the data requirements of itempicker. var selectedItems = Utilities.copy($scope.model.value).forEach((obj) => { - obj.alias = vm.getElementTypeByKey(obj.contentTypeKey).alias; + obj.alias = vm.getElementTypeByKey(obj.contentElementTypeKey).alias; return obj; }); var availableItems = vm.getAvailableElementTypes() localizationService.localizeMany(["blockEditor_headlineCreateBlock", "blockEditor_labelcreateNewElementType"]).then(function(localized) { - + var elemTypeSelectorOverlay = { view: "itempicker", title: localized[0], @@ -133,7 +133,7 @@ }; overlayService.open(elemTypeSelectorOverlay); - + }); }; @@ -158,7 +158,7 @@ vm.addBlockFromElementTypeKey = function(key) { var entry = { - "contentTypeKey": key, + "contentElementTypeKey": key, "settingsElementTypeKey": null, "labelTemplate": "", "view": null, @@ -178,7 +178,7 @@ vm.openBlockOverlay = function (block) { - localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [vm.getElementTypeByKey(block.contentTypeKey).name]).then(function (data) { + localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [vm.getElementTypeByKey(block.contentElementTypeKey).name]).then(function (data) { var clonedBlockData = Utilities.copy(block); vm.openBlock = block; @@ -209,7 +209,7 @@ $scope.$on('$destroy', function () { unsubscribe.forEach(u => { u(); }); }); - + onInit(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html index 41dea86131ea..2bd4be15054a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html @@ -11,7 +11,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less index f4d9caa73b38..878f6a8ef8c7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less @@ -4,7 +4,7 @@ position: relative; display: inline-flex; width: 100%; - height: auto; + height: 100%; margin-right: 20px; margin-bottom: 20px; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js index 0f58b84ee9b1..bb0fefb55808 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js @@ -20,10 +20,10 @@ loadElementTypes(); function loadElementTypes() { - return elementTypeResource.getAll().then(function (elementTypes) { + return elementTypeResource.getAll().then(function(elementTypes) { vm.elementTypes = elementTypes; - vm.contentPreview = vm.getElementTypeByKey(vm.block.contentTypeKey); + vm.contentPreview = vm.getElementTypeByKey(vm.block.contentElementTypeKey); vm.settingsPreview = vm.getElementTypeByKey(vm.block.settingsElementTypeKey); }); } @@ -46,7 +46,7 @@ } }; editorService.documentTypeEditor(editor); - } + }; vm.createElementTypeAndCallback = function(callback) { const editor = { @@ -62,9 +62,9 @@ } }; editorService.documentTypeEditor(editor); - } + }; - vm.addSettingsForBlock = function ($event, block) { + vm.addSettingsForBlock = function($event, block) { localizationService.localizeMany(["blockEditor_headlineAddSettingsElementType", "blockEditor_labelcreateNewElementType"]).then(function(localized) { @@ -95,11 +95,12 @@ }; overlayService.open(elemTypeSelectorOverlay); - }); }; + vm.applySettingsToBlock = function(block, key) { block.settingsElementTypeKey = key; + vm.settingsPreview = vm.getElementTypeByKey(vm.block.settingsElementTypeKey); }; vm.requestRemoveSettingsForBlock = function(block) { @@ -120,11 +121,11 @@ }); }); }; + vm.removeSettingsForBlock = function(block) { block.settingsElementTypeKey = null; }; - function updateUsedElementTypes(event, args) { var key = args.documentType.key; for (var i = 0; i { u(); }); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html index 9675677c11e5..2b6fdc3983f7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html @@ -106,10 +106,10 @@
-
+
-
@@ -227,6 +227,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html index aa16e6e9a876..4d3031602ad9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html @@ -30,7 +30,7 @@ class="btn-reset umb-block-list__create-button umb-outline" ng-class="{ '--disabled': vm.availableBlockTypes.length === 0 }" ng-click="vm.showCreateDialog(vm.layout.length, $event)"> - + Add content diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less index b6f3ace44c59..a2c124a6ea35 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less @@ -39,6 +39,9 @@ } } } +ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-block-list__block--actions { + opacity: 1; +} .umb-block-list__block--actions { position: absolute; z-index:999999999;// We always want to be on top of custom view, but we need to make sure we still are behind relevant Umbraco CMS UI. ToDo: Needs further testing. @@ -58,15 +61,92 @@ &:hover { color: @ui-action-discreet-type-hover; } + > .__error-badge { + position: absolute; + top: -2px; + right: -2px; + min-width: 8px; + color: @white; + background-color: @ui-active-type; + border: 2px solid @white; + border-radius: 50%; + font-size: 8px; + font-weight: bold; + padding: 2px; + line-height: 8px; + background-color: @red; + display: none; + font-weight: 900; + } + &.--error > .__error-badge { + display: block; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-block-list__action--badge-bounce; + animation-timing-function: ease; + @keyframes umb-block-list__action--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-4px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-2px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + + } } } .umb-block-list__block--content { - position: relative; - width: 100%; - min-height: @umb-block-list__item_minimum_height; - background-color: @white; - border-radius: @baseBorderRadius; + + > div { + position: relative; + width: 100%; + min-height: @umb-block-list__item_minimum_height; + background-color: @white; + border-radius: @baseBorderRadius; + box-sizing: border-box; + } + + &.--show-validation { + ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > & > div { + border: 2px solid @formErrorText; + border-radius: @baseBorderRadius; + &::after { + content: "!"; + position: absolute; + top: -12px; + right: -12px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 50%; + font-size: 13px; + text-align: center; + font-weight: bold; + background-color: @errorBackground; + color: @errorText; + border: 2px solid @white; + font-weight: 900; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-block-list__block--content--badge-bounce; + animation-timing-function: ease; + @keyframes umb-block-list__block--content--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-6px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-3px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + } + } + } } .blockelement__draggable-element { @@ -77,7 +157,7 @@ .umb-block-list__block--create-button { position: absolute; width: 100%; - z-index:1; + z-index: 1; opacity: 0; outline: none; height: 12px; @@ -90,28 +170,27 @@ content: ''; position: absolute; background-color: @blueMid; - border-top:1px solid white; - border-bottom:1px solid white; + border-top: 1px solid white; + border-bottom: 1px solid white; border-radius: 2px; - top:5px; + top: 5px; right: 0; left: 0; height: 2px; animation: umb-block-list__block--create-button_before 400ms ease-in-out alternate infinite; + @keyframes umb-block-list__block--create-button_before { 0% { opacity: 1; } 100% { opacity: 0.5; } } } + > .__plus { position: absolute; - pointer-events: none;// lets stop avoiding the mouse values in JS move event. - margin-left: -18px - 10px; - margin-top: -18px; - margin-bottom: -18px; - width: 28px; - height: 25px; - padding-bottom: 3px; + pointer-events: none; // lets stop avoiding the mouse values in JS move event. + width: 24px; + height: 24px; + padding: 0; border-radius: 3em; border: 2px solid @blueMid; display: flex; @@ -122,25 +201,29 @@ font-weight: 800; background-color: rgba(255, 255, 255, .96); box-shadow: 0 0 0 2px rgba(255, 255, 255, .96); - transform: scale(0); + transform: scale(0) translate(-80%, -50%); transition: transform 240ms ease-in; animation: umb-block-list__block--create-button_after 800ms ease-in-out infinite; + @keyframes umb-block-list__block--create-button_after { 0% { color: rgba(@blueMid, 0.8); } 50% { color: rgba(@blueMid, 1); } 100% { color: rgba(@blueMid, 0.8); } } } + &:focus { > .__plus { border: 2px solid @ui-outline; } } + &:hover, &:focus { opacity: 1; transition-duration: 120ms; + > .__plus { - transform: scale(1); + transform: scale(1) translate(-80%, -50%); transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html index 55f849e4d029..c2657985cf48 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html @@ -3,6 +3,7 @@ Settings +
!