diff --git a/build/NuSpecs/tools/trees.config.install.xdt b/build/NuSpecs/tools/trees.config.install.xdt index dc59b5db10..378d013c22 100644 --- a/build/NuSpecs/tools/trees.config.install.xdt +++ b/build/NuSpecs/tools/trees.config.install.xdt @@ -46,7 +46,7 @@ - public const string Dictionary = "dictionary"; - + public const string Stylesheets = "stylesheets"; /// diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index 94988e2687..9cdbc4bbc3 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -1,448 +1,451 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NPoco; -using Umbraco.Core.Events; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Install -{ - /// - /// Creates the initial database schema during install. - /// - internal class DatabaseSchemaCreator - { - private readonly IUmbracoDatabase _database; - private readonly ILogger _logger; - - public DatabaseSchemaCreator(IUmbracoDatabase database, ILogger logger) - { - _database = database; - _logger = logger; - } - - private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax; - - // all tables, in order - public static readonly List OrderedTables = new List - { - typeof (UserDto), - typeof (NodeDto), - typeof (ContentTypeDto), - typeof (TemplateDto), - typeof (ContentDto), - typeof (ContentVersionDto), - typeof (MediaVersionDto), - typeof (DocumentDto), - typeof (ContentTypeTemplateDto), - typeof (DataTypeDto), - typeof (DictionaryDto), - typeof (LanguageDto), - typeof (LanguageTextDto), - typeof (DomainDto), - typeof (LogDto), - typeof (MacroDto), - typeof (MacroPropertyDto), - typeof (MemberTypeDto), - typeof (MemberDto), - typeof (Member2MemberGroupDto), - typeof (ContentXmlDto), - typeof (PreviewXmlDto), - typeof (PropertyTypeGroupDto), - typeof (PropertyTypeDto), - typeof (PropertyDataDto), - typeof (RelationTypeDto), - typeof (RelationDto), - typeof (TagDto), - typeof (TagRelationshipDto), - typeof (TaskTypeDto), - typeof (TaskDto), - typeof (ContentType2ContentTypeDto), - typeof (ContentTypeAllowedContentTypeDto), - typeof (User2NodeNotifyDto), - typeof (ServerRegistrationDto), - typeof (AccessDto), - typeof (AccessRuleDto), - typeof (CacheInstructionDto), - typeof (ExternalLoginDto), - typeof (RedirectUrlDto), - typeof (LockDto), - typeof (UserGroupDto), - typeof (User2UserGroupDto), - typeof (UserGroup2NodePermissionDto), - typeof (UserGroup2AppDto), - typeof (UserStartNodeDto), - typeof (ContentNuDto), - typeof (DocumentVersionDto), - typeof (KeyValueDto), - typeof (UserLoginDto), - typeof (ConsentDto), - typeof (AuditEntryDto), - typeof (ContentVersionCultureVariationDto), - typeof (DocumentCultureVariationDto) - }; - - /// - /// Drops all Umbraco tables in the db. - /// - internal void UninstallDatabaseSchema() - { - _logger.Info("Start UninstallDatabaseSchema"); - - foreach (var table in OrderedTables.AsEnumerable().Reverse()) - { - var tableNameAttribute = table.FirstAttribute(); - var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; - - _logger.Info(() => $"Uninstall {tableName}"); - - try - { - if (TableExists(tableName)) - DropTable(tableName); - } - catch (Exception ex) - { - //swallow this for now, not sure how best to handle this with diff databases... though this is internal - // and only used for unit tests. If this fails its because the table doesn't exist... generally! - _logger.Error("Could not drop table " + tableName, ex); - } - } - } - - /// - /// Initializes the database by creating the umbraco db schema. - /// - public void InitializeDatabaseSchema() - { - var e = new DatabaseCreationEventArgs(); - FireBeforeCreation(e); - - if (e.Cancel == false) - { - var dataCreation = new DatabaseDataCreator(_database, _logger); - foreach (var table in OrderedTables) - CreateTable(false, table, dataCreation); - } - - FireAfterCreation(e); - } - - /// - /// Validates the schema of the current database. - /// - public DatabaseSchemaResult ValidateSchema() - { - var result = new DatabaseSchemaResult(SqlSyntax); - - //get the db index defs - result.DbIndexDefinitions = SqlSyntax.GetDefinedIndexes(_database) - .Select(x => new DbIndexDefinition - { - TableName = x.Item1, - IndexName = x.Item2, - ColumnName = x.Item3, - IsUnique = x.Item4 - }).ToArray(); - - result.TableDefinitions.AddRange(OrderedTables - .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); - - ValidateDbTables(result); - ValidateDbColumns(result); - ValidateDbIndexes(result); - ValidateDbConstraints(result); - - return result; - } - - private void ValidateDbConstraints(DatabaseSchemaResult result) - { - //MySql doesn't conform to the "normal" naming of constraints, so there is currently no point in doing these checks. - //TODO: At a later point we do other checks for MySql, but ideally it should be necessary to do special checks for different providers. - // ALso note that to get the constraints for MySql we have to open a connection which we currently have not. - if (SqlSyntax is MySqlSyntaxProvider) - return; - - //Check constraints in configured database against constraints in schema - var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); - var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")).Select(x => x.Item3).ToList(); - var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")).Select(x => x.Item3).ToList(); - var indexesInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("IX_")).Select(x => x.Item3).ToList(); - var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); - var unknownConstraintsInDatabase = - constraintsInDatabase.Where( - x => - x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && - x.Item3.InvariantStartsWith("IX_") == false).Select(x => x.Item3).ToList(); - var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).ToList(); - var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)) - .Where(x => x.IsNullOrWhiteSpace() == false).ToList(); - - //Add valid and invalid foreign key differences to the result object - // We'll need to do invariant contains with case insensitivity because foreign key, primary key, and even index naming w/ MySQL is not standardized - // In theory you could have: FK_ or fk_ ...or really any standard that your development department (or developer) chooses to use. - foreach (var unknown in unknownConstraintsInDatabase) - { - if (foreignKeysInSchema.InvariantContains(unknown) || primaryKeysInSchema.InvariantContains(unknown) || indexesInSchema.InvariantContains(unknown)) - { - result.ValidConstraints.Add(unknown); - } - else - { - result.Errors.Add(new Tuple("Unknown", unknown)); - } - } - - //Foreign keys: - - var validForeignKeyDifferences = foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var foreignKey in validForeignKeyDifferences) - { - result.ValidConstraints.Add(foreignKey); - } - var invalidForeignKeyDifferences = - foreignKeysInDatabase.Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var foreignKey in invalidForeignKeyDifferences) - { - result.Errors.Add(new Tuple("Constraint", foreignKey)); - } - - - //Primary keys: - - //Add valid and invalid primary key differences to the result object - var validPrimaryKeyDifferences = primaryKeysInDatabase.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var primaryKey in validPrimaryKeyDifferences) - { - result.ValidConstraints.Add(primaryKey); - } - var invalidPrimaryKeyDifferences = - primaryKeysInDatabase.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var primaryKey in invalidPrimaryKeyDifferences) - { - result.Errors.Add(new Tuple("Constraint", primaryKey)); - } - - //Constaints: - - //NOTE: SD: The colIndex checks above should really take care of this but I need to keep this here because it was here before - // and some schema validation checks might rely on this data remaining here! - //Add valid and invalid index differences to the result object - var validIndexDifferences = indexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var index in validIndexDifferences) - { - result.ValidConstraints.Add(index); - } - var invalidIndexDifferences = - indexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(indexesInSchema.Except(indexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var index in invalidIndexDifferences) - { - result.Errors.Add(new Tuple("Constraint", index)); - } - } - - private void ValidateDbColumns(DatabaseSchemaResult result) - { - //Check columns in configured database against columns in schema - var columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); - var columnsPerTableInDatabase = columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); - var columnsPerTableInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); - //Add valid and invalid column differences to the result object - var validColumnDifferences = columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var column in validColumnDifferences) - { - result.ValidColumns.Add(column); - } - - var invalidColumnDifferences = - columnsPerTableInDatabase.Except(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var column in invalidColumnDifferences) - { - result.Errors.Add(new Tuple("Column", column)); - } - } - - private void ValidateDbTables(DatabaseSchemaResult result) - { - //Check tables in configured database against tables in schema - var tablesInDatabase = SqlSyntax.GetTablesInSchema(_database).ToList(); - var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); - //Add valid and invalid table differences to the result object - var validTableDifferences = tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var tableName in validTableDifferences) - { - result.ValidTables.Add(tableName); - } - - var invalidTableDifferences = - tablesInDatabase.Except(tablesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var tableName in invalidTableDifferences) - { - result.Errors.Add(new Tuple("Table", tableName)); - } - } - - private void ValidateDbIndexes(DatabaseSchemaResult result) - { - //These are just column indexes NOT constraints or Keys - //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); - var colIndexesInDatabase = result.DbIndexDefinitions.Select(x => x.IndexName).ToList(); - var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); - - //Add valid and invalid index differences to the result object - var validColIndexDifferences = colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var index in validColIndexDifferences) - { - result.ValidIndexes.Add(index); - } - - var invalidColIndexDifferences = - colIndexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var index in invalidColIndexDifferences) - { - result.Errors.Add(new Tuple("Index", index)); - } - } - - #region Events - - /// - /// The save event handler - /// - internal delegate void DatabaseEventHandler(DatabaseCreationEventArgs e); - - /// - /// Occurs when [before save]. - /// - internal static event DatabaseEventHandler BeforeCreation; - /// - /// Raises the event. - /// - /// The instance containing the event data. - internal virtual void FireBeforeCreation(DatabaseCreationEventArgs e) - { - BeforeCreation?.Invoke(e); - } - - /// - /// Occurs when [after save]. - /// - internal static event DatabaseEventHandler AfterCreation; - /// - /// Raises the event. - /// - /// The instance containing the event data. - internal virtual void FireAfterCreation(DatabaseCreationEventArgs e) - { - AfterCreation?.Invoke(e); - } - - #endregion - - #region Utilities - - public bool TableExists(string tableName) - { - return SqlSyntax.DoesTableExist(_database, tableName); - } - - // this is used in tests - internal void CreateTable(bool overwrite = false) - where T : new() - { - var tableType = typeof(T); - CreateTable(overwrite, tableType, new DatabaseDataCreator(_database, _logger)); - } - - public void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) - { - var tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); - var tableName = tableDefinition.Name; - - var createSql = SqlSyntax.Format(tableDefinition); - var createPrimaryKeySql = SqlSyntax.FormatPrimaryKey(tableDefinition); - var foreignSql = SqlSyntax.Format(tableDefinition.ForeignKeys); - var indexSql = SqlSyntax.Format(tableDefinition.Indexes); - - var tableExist = TableExists(tableName); - if (overwrite && tableExist) - { - DropTable(tableName); - tableExist = false; - } - - if (tableExist == false) - { - using (var transaction = _database.GetTransaction()) - { - //Execute the Create Table sql - var created = _database.Execute(new Sql(createSql)); - _logger.Info(() => $"Create Table '{tableName}' ({created}):\n {createSql}"); - - //If any statements exists for the primary key execute them here - if (string.IsNullOrEmpty(createPrimaryKeySql) == false) - { - var createdPk = _database.Execute(new Sql(createPrimaryKeySql)); - _logger.Info(() => $"Create Primary Key ({createdPk}):\n {createPrimaryKeySql}"); - } - - //Turn on identity insert if db provider is not mysql - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); - - //Call the NewTable-event to trigger the insert of base/default data - //OnNewTable(tableName, _db, e, _logger); - - dataCreation.InitializeBaseData(tableName); - - //Turn off identity insert if db provider is not mysql - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); - - //Special case for MySql - if (SqlSyntax is MySqlSyntaxProvider && tableName.Equals("umbracoUser")) - { - _database.Update("SET id = @IdAfter WHERE id = @IdBefore AND userLogin = @Login", new { IdAfter = 0, IdBefore = 1, Login = "admin" }); - } - - //Loop through index statements and execute sql - foreach (var sql in indexSql) - { - var createdIndex = _database.Execute(new Sql(sql)); - _logger.Info(() => $"Create Index ({createdIndex}):\n {sql}"); - } - - //Loop through foreignkey statements and execute sql - foreach (var sql in foreignSql) - { - var createdFk = _database.Execute(new Sql(sql)); - _logger.Info(() => $"Create Foreign Key ({createdFk}):\n {sql}"); - } - - transaction.Complete(); - } - } - - _logger.Info(() => $"Created table '{tableName}'"); - } - - public void DropTable(string tableName) - { - var sql = new Sql(string.Format(SqlSyntax.DropTable, SqlSyntax.GetQuotedTableName(tableName))); - _database.Execute(sql); - } - - #endregion - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using NPoco; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Migrations.Install +{ + /// + /// Creates the initial database schema during install. + /// + internal class DatabaseSchemaCreator + { + private readonly IUmbracoDatabase _database; + private readonly ILogger _logger; + + public DatabaseSchemaCreator(IUmbracoDatabase database, ILogger logger) + { + _database = database; + _logger = logger; + } + + private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax; + + // all tables, in order + public static readonly List OrderedTables = new List + { + typeof (UserDto), + typeof (NodeDto), + typeof (ContentTypeDto), + typeof (TemplateDto), + typeof (ContentDto), + typeof (ContentVersionDto), + typeof (MediaVersionDto), + typeof (DocumentDto), + typeof (ContentTypeTemplateDto), + typeof (DataTypeDto), + typeof (DictionaryDto), + typeof (LanguageDto), + typeof (LanguageTextDto), + typeof (DomainDto), + typeof (LogDto), + typeof (MacroDto), + typeof (MacroPropertyDto), + typeof (MemberTypeDto), + typeof (MemberDto), + typeof (Member2MemberGroupDto), + typeof (ContentXmlDto), + typeof (PreviewXmlDto), + typeof (PropertyTypeGroupDto), + typeof (PropertyTypeDto), + typeof (PropertyDataDto), + typeof (RelationTypeDto), + typeof (RelationDto), + typeof (TagDto), + typeof (TagRelationshipDto), + typeof (TaskTypeDto), + typeof (TaskDto), + typeof (ContentType2ContentTypeDto), + typeof (ContentTypeAllowedContentTypeDto), + typeof (User2NodeNotifyDto), + typeof (ServerRegistrationDto), + typeof (AccessDto), + typeof (AccessRuleDto), + typeof (CacheInstructionDto), + typeof (ExternalLoginDto), + typeof (RedirectUrlDto), + typeof (LockDto), + typeof (UserGroupDto), + typeof (User2UserGroupDto), + typeof (UserGroup2NodePermissionDto), + typeof (UserGroup2AppDto), + typeof (UserStartNodeDto), + typeof (ContentNuDto), + typeof (DocumentVersionDto), + typeof (KeyValueDto), + typeof (UserLoginDto), + typeof (ConsentDto), + typeof (AuditEntryDto), + typeof (ContentVersionCultureVariationDto), + typeof (DocumentCultureVariationDto) + }; + + /// + /// Drops all Umbraco tables in the db. + /// + internal void UninstallDatabaseSchema() + { + _logger.Info("Start UninstallDatabaseSchema"); + + foreach (var table in OrderedTables.AsEnumerable().Reverse()) + { + var tableNameAttribute = table.FirstAttribute(); + var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; + + _logger.Info(() => $"Uninstall {tableName}"); + + try + { + if (TableExists(tableName)) + DropTable(tableName); + } + catch (Exception ex) + { + //swallow this for now, not sure how best to handle this with diff databases... though this is internal + // and only used for unit tests. If this fails its because the table doesn't exist... generally! + _logger.Error("Could not drop table " + tableName, ex); + } + } + } + + /// + /// Initializes the database by creating the umbraco db schema. + /// + public void InitializeDatabaseSchema() + { + var e = new DatabaseCreationEventArgs(); + FireBeforeCreation(e); + + if (e.Cancel == false) + { + var dataCreation = new DatabaseDataCreator(_database, _logger); + foreach (var table in OrderedTables) + CreateTable(false, table, dataCreation); + } + + FireAfterCreation(e); + } + + /// + /// Validates the schema of the current database. + /// + public DatabaseSchemaResult ValidateSchema() + { + var result = new DatabaseSchemaResult(SqlSyntax); + + //get the db index defs + result.DbIndexDefinitions = SqlSyntax.GetDefinedIndexes(_database) + .Select(x => new DbIndexDefinition + { + TableName = x.Item1, + IndexName = x.Item2, + ColumnName = x.Item3, + IsUnique = x.Item4 + }).ToArray(); + + result.TableDefinitions.AddRange(OrderedTables + .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); + + ValidateDbTables(result); + ValidateDbColumns(result); + ValidateDbIndexes(result); + ValidateDbConstraints(result); + + return result; + } + + private void ValidateDbConstraints(DatabaseSchemaResult result) + { + //MySql doesn't conform to the "normal" naming of constraints, so there is currently no point in doing these checks. + //TODO: At a later point we do other checks for MySql, but ideally it should be necessary to do special checks for different providers. + // ALso note that to get the constraints for MySql we have to open a connection which we currently have not. + if (SqlSyntax is MySqlSyntaxProvider) + return; + + //Check constraints in configured database against constraints in schema + var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); + var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")).Select(x => x.Item3).ToList(); + var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")).Select(x => x.Item3).ToList(); + var indexesInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("IX_")).Select(x => x.Item3).ToList(); + var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); + var unknownConstraintsInDatabase = + constraintsInDatabase.Where( + x => + x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && + x.Item3.InvariantStartsWith("IX_") == false).Select(x => x.Item3).ToList(); + var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).ToList(); + var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)) + .Where(x => x.IsNullOrWhiteSpace() == false).ToList(); + + //Add valid and invalid foreign key differences to the result object + // We'll need to do invariant contains with case insensitivity because foreign key, primary key, and even index naming w/ MySQL is not standardized + // In theory you could have: FK_ or fk_ ...or really any standard that your development department (or developer) chooses to use. + foreach (var unknown in unknownConstraintsInDatabase) + { + if (foreignKeysInSchema.InvariantContains(unknown) || primaryKeysInSchema.InvariantContains(unknown) || indexesInSchema.InvariantContains(unknown)) + { + result.ValidConstraints.Add(unknown); + } + else + { + result.Errors.Add(new Tuple("Unknown", unknown)); + } + } + + //Foreign keys: + + var validForeignKeyDifferences = foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var foreignKey in validForeignKeyDifferences) + { + result.ValidConstraints.Add(foreignKey); + } + var invalidForeignKeyDifferences = + foreignKeysInDatabase.Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var foreignKey in invalidForeignKeyDifferences) + { + result.Errors.Add(new Tuple("Constraint", foreignKey)); + } + + + //Primary keys: + + //Add valid and invalid primary key differences to the result object + var validPrimaryKeyDifferences = primaryKeysInDatabase.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var primaryKey in validPrimaryKeyDifferences) + { + result.ValidConstraints.Add(primaryKey); + } + var invalidPrimaryKeyDifferences = + primaryKeysInDatabase.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var primaryKey in invalidPrimaryKeyDifferences) + { + result.Errors.Add(new Tuple("Constraint", primaryKey)); + } + + //Constaints: + + //NOTE: SD: The colIndex checks above should really take care of this but I need to keep this here because it was here before + // and some schema validation checks might rely on this data remaining here! + //Add valid and invalid index differences to the result object + var validIndexDifferences = indexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var index in validIndexDifferences) + { + result.ValidConstraints.Add(index); + } + var invalidIndexDifferences = + indexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(indexesInSchema.Except(indexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var index in invalidIndexDifferences) + { + result.Errors.Add(new Tuple("Constraint", index)); + } + } + + private void ValidateDbColumns(DatabaseSchemaResult result) + { + //Check columns in configured database against columns in schema + var columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); + var columnsPerTableInDatabase = columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); + var columnsPerTableInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); + //Add valid and invalid column differences to the result object + var validColumnDifferences = columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var column in validColumnDifferences) + { + result.ValidColumns.Add(column); + } + + var invalidColumnDifferences = + columnsPerTableInDatabase.Except(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var column in invalidColumnDifferences) + { + result.Errors.Add(new Tuple("Column", column)); + } + } + + private void ValidateDbTables(DatabaseSchemaResult result) + { + //Check tables in configured database against tables in schema + var tablesInDatabase = SqlSyntax.GetTablesInSchema(_database).ToList(); + var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); + //Add valid and invalid table differences to the result object + var validTableDifferences = tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var tableName in validTableDifferences) + { + result.ValidTables.Add(tableName); + } + + var invalidTableDifferences = + tablesInDatabase.Except(tablesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var tableName in invalidTableDifferences) + { + result.Errors.Add(new Tuple("Table", tableName)); + } + } + + private void ValidateDbIndexes(DatabaseSchemaResult result) + { + //These are just column indexes NOT constraints or Keys + //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); + var colIndexesInDatabase = result.DbIndexDefinitions.Select(x => x.IndexName).ToList(); + var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); + + //Add valid and invalid index differences to the result object + var validColIndexDifferences = colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var index in validColIndexDifferences) + { + result.ValidIndexes.Add(index); + } + + var invalidColIndexDifferences = + colIndexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var index in invalidColIndexDifferences) + { + result.Errors.Add(new Tuple("Index", index)); + } + } + + #region Events + + /// + /// The save event handler + /// + internal delegate void DatabaseEventHandler(DatabaseCreationEventArgs e); + + /// + /// Occurs when [before save]. + /// + internal static event DatabaseEventHandler BeforeCreation; + /// + /// Raises the event. + /// + /// The instance containing the event data. + internal virtual void FireBeforeCreation(DatabaseCreationEventArgs e) + { + BeforeCreation?.Invoke(e); + } + + /// + /// Occurs when [after save]. + /// + internal static event DatabaseEventHandler AfterCreation; + /// + /// Raises the event. + /// + /// The instance containing the event data. + internal virtual void FireAfterCreation(DatabaseCreationEventArgs e) + { + AfterCreation?.Invoke(e); + } + + #endregion + + #region Utilities + + public bool TableExists(string tableName) + { + return SqlSyntax.DoesTableExist(_database, tableName); + } + + public bool TableExist() { + var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); var tableName = table.Name; return SqlSyntax.DoesTableExist(_database, tableName); } + + // this is used in tests + internal void CreateTable(bool overwrite = false) + where T : new() + { + var tableType = typeof(T); + CreateTable(overwrite, tableType, new DatabaseDataCreator(_database, _logger)); + } + + public void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) + { + var tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); + var tableName = tableDefinition.Name; + + var createSql = SqlSyntax.Format(tableDefinition); + var createPrimaryKeySql = SqlSyntax.FormatPrimaryKey(tableDefinition); + var foreignSql = SqlSyntax.Format(tableDefinition.ForeignKeys); + var indexSql = SqlSyntax.Format(tableDefinition.Indexes); + + var tableExist = TableExists(tableName); + if (overwrite && tableExist) + { + DropTable(tableName); + tableExist = false; + } + + if (tableExist == false) + { + using (var transaction = _database.GetTransaction()) + { + //Execute the Create Table sql + var created = _database.Execute(new Sql(createSql)); + _logger.Info(() => $"Create Table '{tableName}' ({created}):\n {createSql}"); + + //If any statements exists for the primary key execute them here + if (string.IsNullOrEmpty(createPrimaryKeySql) == false) + { + var createdPk = _database.Execute(new Sql(createPrimaryKeySql)); + _logger.Info(() => $"Create Primary Key ({createdPk}):\n {createPrimaryKeySql}"); + } + + //Turn on identity insert if db provider is not mysql + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); + + //Call the NewTable-event to trigger the insert of base/default data + //OnNewTable(tableName, _db, e, _logger); + + dataCreation.InitializeBaseData(tableName); + + //Turn off identity insert if db provider is not mysql + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); + + //Special case for MySql + if (SqlSyntax is MySqlSyntaxProvider && tableName.Equals("umbracoUser")) + { + _database.Update("SET id = @IdAfter WHERE id = @IdBefore AND userLogin = @Login", new { IdAfter = 0, IdBefore = 1, Login = "admin" }); + } + + //Loop through index statements and execute sql + foreach (var sql in indexSql) + { + var createdIndex = _database.Execute(new Sql(sql)); + _logger.Info(() => $"Create Index ({createdIndex}):\n {sql}"); + } + + //Loop through foreignkey statements and execute sql + foreach (var sql in foreignSql) + { + var createdFk = _database.Execute(new Sql(sql)); + _logger.Info(() => $"Create Foreign Key ({createdFk}):\n {sql}"); + } + + transaction.Complete(); + } + } + + _logger.Info(() => $"Created table '{tableName}'"); + } + + public void DropTable(string tableName) + { + var sql = new Sql(string.Format(SqlSyntax.DropTable, SqlSyntax.GetQuotedTableName(tableName))); + _database.Execute(sql); + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 698b77f8bf..698af341f7 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -4,6 +4,7 @@ using Semver; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Upgrade.V_7_10_0; +using Umbraco.Core.Migrations.Upgrade.V_7_12_0; using Umbraco.Core.Migrations.Upgrade.V_7_5_0; using Umbraco.Core.Migrations.Upgrade.V_7_5_5; using Umbraco.Core.Migrations.Upgrade.V_7_6_0; @@ -205,6 +206,9 @@ namespace Umbraco.Core.Migrations.Upgrade // mergin from 7.10.0 Chain("{79591E91-01EA-43F7-AC58-7BD286DB1E77}"); + // mergin from 7.12.0 + Chain("{4BCD4198-6822-4D82-8C69-6CC4086DF46A}"); + // 8.0.0 // AddVariationTables1 has been superceeded by AddVariationTables2 //Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs new file mode 100644 index 0000000000..d8c6e145b1 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 +{ public class UpdateUmbracoConsent : MigrationBase { + public UpdateUmbracoConsent(IMigrationContext context) + : base(context) + { } + + public override void Migrate() { Alter.Table("umbracoConsent").AlterColumn("comment").AsString().Nullable(); } + } +} diff --git a/src/Umbraco.Core/Persistence/Dtos/ConsentDto.cs b/src/Umbraco.Core/Persistence/Dtos/ConsentDto.cs index 763df352de..b66cd2d020 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ConsentDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ConsentDto.cs @@ -37,6 +37,7 @@ namespace Umbraco.Core.Persistence.Dtos public int State { get; set; } [Column("comment")] + [NullSetting(NullSetting = NullSettings.Null)] public string Comment { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/LocalDb.cs b/src/Umbraco.Core/Persistence/LocalDb.cs index 995508ecf7..6eb9cbc443 100644 --- a/src/Umbraco.Core/Persistence/LocalDb.cs +++ b/src/Umbraco.Core/Persistence/LocalDb.cs @@ -141,7 +141,7 @@ namespace Umbraco.Core.Persistence { EnsureAvailable(); var instances = GetInstances(); - return instances != null && instances.Contains(instanceName); + return instances != null && instances.Contains(instanceName, StringComparer.OrdinalIgnoreCase); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs index e171077bb7..5d386d9cb4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs @@ -160,7 +160,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement totalRecords = page.TotalItems; var items = page.Items.Select( - dto => new AuditItem(dto.Id, dto.Comment, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId)).ToArray(); + dto => new AuditItem(dto.Id, dto.Comment, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId)).ToArray(); // map the DateStamp for (var i = 0; i < items.Length; i++) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs new file mode 100644 index 0000000000..878d54f6f2 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class FlexibleDropdownPropertyValueConverter : PropertyValueConverterBase //, IPropertyValueConverterMeta + { + private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); + private readonly IDataTypeService _dataTypeService; + + public FlexibleDropdownPropertyValueConverter(IDataTypeService dataTypeService) + { + _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); + } + + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.DropDownListFlexible); + } + + public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + { + return source != null + ? source.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + : null; + } + + public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + { + if (inter == null) + { + return null; + } + + var selectedValues = (string[])inter; + if (selectedValues.Any()) + { + if (IsMultipleDataType(propertyType.DataType.Id, propertyType.EditorAlias)) + { + return selectedValues; + } + + return selectedValues.First(); + } + + return inter; + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return IsMultipleDataType(propertyType.DataType.Id, propertyType.EditorAlias) + ? typeof(IEnumerable) + : typeof(string); + } + + /// + /// Determines if the "enable multiple choice" prevalue has been ticked. + /// + /// The ID of this particular datatype instance. + /// The property editor alias. + /// true if the data type has been configured to return multiple values. + /// + private bool IsMultipleDataType(int dataTypeId, string propertyEditorAlias) + { + //TODO: Fix this after this commit since we need to move this to the Web proje + return false; + + //// GetPreValuesCollectionByDataTypeId is cached at repository level; + //// still, the collection is deep-cloned so this is kinda expensive, + //// better to cache here + trigger refresh in DataTypeCacheRefresher + //return Storages.GetOrAdd(dataTypeId, id => + //{ + // var dt = _dataTypeService.GetDataType(id); + // var preVals = (DropDownFlexibleConfigurationEditor)dt.Configuration; + + // if (preVals.ContainsKey("multiple")) + // { + // var preValue = preVals + // .FirstOrDefault(x => string.Equals(x.Key, "multiple", + // StringComparison.InvariantCultureIgnoreCase)) + // .Value; + + // return preValue != null && preValue.Value.TryConvertTo().Result; + // } + + // //in some odd cases, the pre-values in the db won't exist but their default pre-values contain this key so check there + // var propertyEditor = PropertyEditorResolver.Current.GetByAlias(propertyEditorAlias); + // if (propertyEditor != null) + // { + // var preValue = propertyEditor.DefaultPreValues + // .FirstOrDefault(x => string.Equals(x.Key, "multiple", + // StringComparison + // .InvariantCultureIgnoreCase)) + // .Value; + + // return preValue != null && preValue.TryConvertTo().Result; + // } + + // return false; + //}); + } + + internal static void ClearCaches() + { + Storages.Clear(); + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index 3117fc0b54..e1729a22de 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -48,12 +48,13 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters //TODO: Change all singleton access to use ctor injection in v8!!! //TODO: That would mean that property value converters would need to be request lifespan, hrm.... + bool isDebug = HttpContext.Current != null && HttpContext.Current.IsDebuggingEnabled; var gridConfig = UmbracoConfig.For.GridConfig( Current.ProfilingLogger.Logger, Current.ApplicationCache.RuntimeCache, new DirectoryInfo(IOHelper.MapPath(SystemDirectories.AppPlugins)), new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Config)), - HttpContext.Current.IsDebuggingEnabled); + isDebug); var sections = GetArray(obj, "sections"); foreach (var section in sections.Cast()) diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 7b49baafe9..e2733a311d 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -189,6 +189,15 @@ namespace Umbraco.Core.Services IEnumerable relations, bool loadBaseType = false); + /// + /// Relates two objects by their entity Ids. + /// + /// Id of the parent + /// Id of the child + /// The type of relation to create + /// The created + IRelation Relate(int parentId, int childId, IRelationType relationType); + /// /// Relates two objects that are based on the interface. /// @@ -198,6 +207,15 @@ namespace Umbraco.Core.Services /// The created IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType); + /// + /// Relates two objects by their entity Ids. + /// + /// Id of the parent + /// Id of the child + /// Alias of the type of relation to create + /// The created + IRelation Relate(int parentId, int childId, string relationTypeAlias); + /// /// Relates two objects that are based on the interface. /// diff --git a/src/Umbraco.Core/Services/Implement/ConsentService.cs b/src/Umbraco.Core/Services/Implement/ConsentService.cs index 21ec5f4434..acc4683d64 100644 --- a/src/Umbraco.Core/Services/Implement/ConsentService.cs +++ b/src/Umbraco.Core/Services/Implement/ConsentService.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Scoping; namespace Umbraco.Core.Services.Implement { /// - /// Implements . + /// Implements . /// internal class ConsentService : ScopeRepositoryService, IConsentService { diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index f3af15c882..14cd63c63d 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -218,6 +218,7 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { + var rtQuery = Query().Where(x => x.Alias == relationTypeAlias); var relationType = _relationTypeRepository.Get(rtQuery).FirstOrDefault(); if (relationType == null) @@ -243,6 +244,7 @@ namespace Umbraco.Core.Services.Implement relationTypeIds = relationTypes.Select(x => x.Id).ToList(); } + return relationTypeIds.Count == 0 ? Enumerable.Empty() : GetRelationsByListOfTypeIds(relationTypeIds); @@ -263,6 +265,7 @@ namespace Umbraco.Core.Services.Implement relationTypeIds = relationTypes.Select(x => x.Id).ToList(); } + return relationTypeIds.Count == 0 ? Enumerable.Empty() : GetRelationsByListOfTypeIds(relationTypeIds); @@ -374,19 +377,19 @@ namespace Umbraco.Core.Services.Implement } /// - /// Relates two objects that are based on the interface. + /// Relates two objects by their entity Ids. /// - /// Parent entity - /// Child entity + /// Id of the parent + /// Id of the child /// The type of relation to create /// The created - public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) + public IRelation Relate(int parentId, int childId, IRelationType relationType) { - //Ensure that the RelationType has an indentity before using it to relate two entities + // Ensure that the RelationType has an indentity before using it to relate two entities if (relationType.HasIdentity == false) Save(relationType); - var relation = new Relation(parent.Id, child.Id, relationType); + var relation = new Relation(parentId, childId, relationType); using (var scope = ScopeProvider.CreateScope()) { @@ -401,9 +404,36 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedRelation, this, saveEventArgs); scope.Complete(); + return relation; } + } - return relation; + /// + /// Relates two objects that are based on the interface. + /// + /// Parent entity + /// Child entity + /// The type of relation to create + /// The created + public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) + { + return Relate(parent.Id, child.Id, relationType); + } + + /// + /// Relates two objects by their entity Ids. + /// + /// Id of the parent + /// Id of the child + /// Alias of the type of relation to create + /// The created + public IRelation Relate(int parentId, int childId, string relationTypeAlias) + { + var relationType = GetRelationTypeByAlias(relationTypeAlias); + if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) + throw new ArgumentNullException(string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); + + return Relate(parentId, childId, relationType); } /// @@ -417,26 +447,9 @@ namespace Umbraco.Core.Services.Implement { var relationType = GetRelationTypeByAlias(relationTypeAlias); if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) - throw new ArgumentNullException($"No RelationType with Alias '{relationTypeAlias}' exists."); + throw new ArgumentNullException(string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); - var relation = new Relation(parent.Id, child.Id, relationType); - - using (var scope = ScopeProvider.CreateScope()) - { - var saveEventArgs = new SaveEventArgs(relation); - if (scope.Events.DispatchCancelable(SavingRelation, this, saveEventArgs)) - { - scope.Complete(); - return relation; // fixme - returning sth that does not exist here?! // fixme - returning sth that does not exist here?! - } - - _relationRepository.Save(relation); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(SavedRelation, this, saveEventArgs); - scope.Complete(); - } - - return relation; + return Relate(parent.Id, child.Id, relationType); } /// @@ -655,6 +668,7 @@ namespace Umbraco.Core.Services.Implement var relations = new List(); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { + foreach (var relationTypeId in relationTypeIds) { var id = relationTypeId; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c9f40e28dd..b1e5d3cf9c 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -316,6 +316,7 @@ + @@ -402,6 +403,7 @@ + @@ -1520,4 +1522,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs b/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs index bc5971a377..058f063f8e 100644 --- a/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs +++ b/src/Umbraco.Tests/FrontEnd/UmbracoHelperTests.cs @@ -1,12 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Text; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Tests.TestHelpers; using Umbraco.Web; namespace Umbraco.Tests.FrontEnd { [TestFixture] public class UmbracoHelperTests + //: BaseUmbracoApplicationTest { [Test] public void Truncate_Simple() @@ -68,7 +72,7 @@ namespace Umbraco.Tests.FrontEnd key2 = "value2", Key3 = "Value3", keY4 = "valuE4" - }; + }; var encryptedRouteString = UmbracoHelper.CreateEncryptedRouteString("FormController", "FormAction", "", additionalRouteValues); var result = encryptedRouteString.DecryptWithMachineKey(); var expectedResult = "c=FormController&a=FormAction&ar=&key1=value1&key2=value2&Key3=Value3&keY4=valuE4"; @@ -146,7 +150,7 @@ namespace Umbraco.Tests.FrontEnd { var text = "Hello world, this is some text with a link"; - string [] tags = {"b"}; + string[] tags = { "b" }; var helper = new UmbracoHelper(); @@ -166,5 +170,233 @@ namespace Umbraco.Tests.FrontEnd Assert.AreEqual("Hello world, is some text with a link", result); } + + // ------- Int32 conversion tests + [Test] + public static void Converting_boxed_34_to_an_int_returns_34() + { + // Arrange + const int sample = 34; + + // Act + bool success = UmbracoHelper.ConvertIdObjectToInt( + sample, + out int result + ); + + // Assert + Assert.IsTrue(success); + Assert.That(result, Is.EqualTo(34)); + } + + [Test] + public static void Converting_string_54_to_an_int_returns_54() + { + // Arrange + const string sample = "54"; + + // Act + bool success = UmbracoHelper.ConvertIdObjectToInt( + sample, + out int result + ); + + // Assert + Assert.IsTrue(success); + Assert.That(result, Is.EqualTo(54)); + } + + [Test] + public static void Converting_hello_to_an_int_returns_false() + { + // Arrange + const string sample = "Hello"; + + // Act + bool success = UmbracoHelper.ConvertIdObjectToInt( + sample, + out int result + ); + + // Assert + Assert.IsFalse(success); + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public static void Converting_unsupported_object_to_an_int_returns_false() + { + // Arrange + var clearlyWillNotConvertToInt = new StringBuilder(0); + + // Act + bool success = UmbracoHelper.ConvertIdObjectToInt( + clearlyWillNotConvertToInt, + out int result + ); + + // Assert + Assert.IsFalse(success); + Assert.That(result, Is.EqualTo(0)); + } + + // ------- GUID conversion tests + [Test] + public static void Converting_boxed_guid_to_a_guid_returns_original_guid_value() + { + // Arrange + Guid sample = Guid.NewGuid(); + + // Act + bool success = UmbracoHelper.ConvertIdObjectToGuid( + sample, + out Guid result + ); + + // Assert + Assert.IsTrue(success); + Assert.That(result, Is.EqualTo(sample)); + } + + [Test] + public static void Converting_string_guid_to_a_guid_returns_original_guid_value() + { + // Arrange + Guid sample = Guid.NewGuid(); + + // Act + bool success = UmbracoHelper.ConvertIdObjectToGuid( + sample.ToString(), + out Guid result + ); + + // Assert + Assert.IsTrue(success); + Assert.That(result, Is.EqualTo(sample)); + } + + [Test] + public static void Converting_hello_to_a_guid_returns_false() + { + // Arrange + const string sample = "Hello"; + + // Act + bool success = UmbracoHelper.ConvertIdObjectToGuid( + sample, + out Guid result + ); + + // Assert + Assert.IsFalse(success); + Assert.That(result, Is.EqualTo(new Guid("00000000-0000-0000-0000-000000000000"))); + } + + [Test] + public static void Converting_unsupported_object_to_a_guid_returns_false() + { + // Arrange + var clearlyWillNotConvertToGuid = new StringBuilder(0); + + // Act + bool success = UmbracoHelper.ConvertIdObjectToGuid( + clearlyWillNotConvertToGuid, + out Guid result + ); + + // Assert + Assert.IsFalse(success); + Assert.That(result, Is.EqualTo(new Guid("00000000-0000-0000-0000-000000000000"))); + } + + // ------- UDI Conversion Tests + /// + /// This requires PluginManager.Current to be initialised before + /// running. + /// + [Test] + public static void Converting_boxed_udi_to_a_udi_returns_original_udi_value() + { + // Arrange + Udi.ResetUdiTypes(); + Udi sample = new GuidUdi(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()); + + // Act + bool success = UmbracoHelper.ConvertIdObjectToUdi( + sample, + out Udi result + ); + + // Assert + Assert.IsTrue(success); + Assert.That(result, Is.EqualTo(sample)); + } + + /// + /// This requires PluginManager.Current to be initialised before + /// running. + /// + [Test] + public static void Converting_string_udi_to_a_udi_returns_original_udi_value() + { + // Arrange + Udi.ResetUdiTypes(); + Udi sample = new GuidUdi(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()); + + // Act + bool success = UmbracoHelper.ConvertIdObjectToUdi( + sample.ToString(), + out Udi result + ); + + // Assert + Assert.IsTrue(success, "Conversion of UDI failed."); + Assert.That(result, Is.EqualTo(sample)); + } + + /// + /// This requires PluginManager.Current to be initialised before + /// running. + /// + [Test] + public static void Converting_hello_to_a_udi_returns_false() + { + // Arrange + Udi.ResetUdiTypes(); + const string sample = "Hello"; + + // Act + bool success = UmbracoHelper.ConvertIdObjectToUdi( + sample, + out Udi result + ); + + // Assert + Assert.IsFalse(success); + Assert.That(result, Is.Null); + } + + /// + /// This requires PluginManager.Current to be initialised before + /// running. + /// + [Test] + public static void Converting_unsupported_object_to_a_udi_returns_false() + { + // Arrange + Udi.ResetUdiTypes(); + + var clearlyWillNotConvertToGuid = new StringBuilder(0); + + // Act + bool success = UmbracoHelper.ConvertIdObjectToUdi( + clearlyWillNotConvertToGuid, + out Udi result + ); + + // Assert + Assert.IsFalse(success); + Assert.That(result, Is.Null); + } } } diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index 24f3c9a9fe..809354bd74 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -285,8 +285,7 @@ AnotherContentFinder public void Resolves_Attributed_Trees() { var trees = _manager.ResolveAttributedTrees(); - // commit 6c5e35ec2cbfa31be6790d1228e0c2faf5f55bc8 brings the count down to 14 - Assert.AreEqual(6, trees.Count()); + Assert.AreEqual(5, trees.Count()); } [Test] @@ -300,7 +299,7 @@ AnotherContentFinder public void Resolves_Trees() { var trees = _manager.ResolveTrees(); - Assert.AreEqual(34, trees.Count()); + Assert.AreEqual(33, trees.Count()); } [Test] diff --git a/src/Umbraco.Tests/PublishedContent/StronglyTypedModels/Home.cs b/src/Umbraco.Tests/PublishedContent/StronglyTypedModels/Home.cs new file mode 100644 index 0000000000..d9a3807c25 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/StronglyTypedModels/Home.cs @@ -0,0 +1,16 @@ +using System; +using System.Web; +using Umbraco.Core.Models; + +namespace Umbraco.Tests.PublishedContent.StronglyTypedModels +{ + /// + /// Used for testing strongly-typed published content extensions that work against the + /// + public class Home : TypedModelBase + { + public Home(IPublishedContent publishedContent) : base(publishedContent) + { + } + } +} diff --git a/src/Umbraco.Tests/Services/ConsentServiceTests.cs b/src/Umbraco.Tests/Services/ConsentServiceTests.cs index 4aa4209673..a27d6a164e 100644 --- a/src/Umbraco.Tests/Services/ConsentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ConsentServiceTests.cs @@ -105,5 +105,20 @@ namespace Umbraco.Tests.Services Assert.Throws(() => consentService.RegisterConsent("user/1234", "app1", "do-something", ConsentState.Granted | ConsentState.Revoked, "no comment")); } + + [Test] + public void CanRegisterConsentWithoutComment() + { + var consentService = ServiceContext.ConsentService; + + // Attept to add consent without a comment + consentService.RegisterConsent("user/1234", "app1", "consentWithoutComment", ConsentState.Granted); + + // Attempt to retrieve the consent we just added without a comment + var consents = consentService.LookupConsent(source: "user/1234", action: "consentWithoutComment").ToArray(); + + // Confirm we got our expected consent record + Assert.AreEqual(1, consents.Length); + } } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js index 55641b6ec7..7e981c065d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js @@ -304,6 +304,12 @@ In the following example you see how to run some custom logic before a step goes scope.elementNotFound = false; $timeout(function () { + // clear element when step as marked as intro, so it always displays in the center + if (scope.model.currentStep && scope.model.currentStep.type === "intro") { + scope.model.currentStep.element = null; + scope.model.currentStep.eventElement = null; + scope.model.currentStep.event = null; + } // if an element isn't set - show the popover in the center if(scope.model.currentStep && !scope.model.currentStep.element) { 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 64bd8dc096..b701dcfee9 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 @@ -172,6 +172,7 @@ return $q.resolve($scope.content); + }); } @@ -324,7 +325,7 @@ } if (formHelper.submitForm({ scope: $scope, skipValidation: true })) { - + $scope.page.buttonGroupState = "busy"; contentResource.unPublish($scope.content.id, culture) 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 f29f41656d..980c2f4453 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 @@ -259,6 +259,8 @@ Use this directive to construct a header inside the main editor window. scope.openIconPicker = function() { var iconPicker = { + icon: scope.icon.split(' ')[0], + color: scope.icon.split(' ')[1], submit: function(model) { if (model.icon) { if (model.color) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js index f21f7b8d3d..26583b108b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js @@ -1,9 +1,9 @@ (function() { 'use strict'; - function ListViewSettingsDirective(contentTypeResource, dataTypeResource, dataTypeHelper, listViewPrevalueHelper) { + function ListViewSettingsDirective(dataTypeResource, dataTypeHelper, listViewPrevalueHelper) { - function link(scope, el, attr, ctrl) { + function link(scope) { scope.dataType = {}; scope.editDataTypeSettings = false; @@ -22,7 +22,6 @@ listViewPrevalueHelper.setPrevalues(dataType.preValues); scope.customListViewCreated = checkForCustomListView(); - }); } else { @@ -111,8 +110,16 @@ }; + scope.toggle = function(){ + if(scope.enableListView){ + scope.enableListView = false; + return; + } + scope.enableListView = true; + }; + /* ----------- SCOPE WATCHERS ----------- */ - var unbindEnableListViewWatcher = scope.$watch('enableListView', function(newValue, oldValue){ + var unbindEnableListViewWatcher = scope.$watch('enableListView', function(newValue){ if(newValue !== undefined) { activate(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js index 556019857b..21714d172c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js @@ -16,6 +16,7 @@ ng-repeat="node in vm.nodes" icon="node.icon" name="node.name" + alias="node.alias" published="node.published" description="node.description" sortable="vm.sortable" @@ -77,6 +78,7 @@ @param {string} icon (binding): The node icon. @param {string} name (binding): The node name. +@param {string} alias (binding): The node document type alias will be displayed on hover if in debug mode or logged in as admin @param {boolean} published (binding): The node published state. @param {string} description (binding): A short description. @param {boolean} sortable (binding): Will add a move cursor on the node preview. Can used in combination with ui-sortable. @@ -91,12 +93,16 @@ (function () { 'use strict'; - function NodePreviewDirective() { + function NodePreviewDirective(userService) { function link(scope, el, attr, ctrl) { if (!scope.editLabelKey) { scope.editLabelKey = "general_edit"; } + userService.getCurrentUser().then(function (u) { + var isAdmin = u.userGroups.indexOf('admin') !== -1; + scope.alias = (Umbraco.Sys.ServerVariables.isDebuggingEnabled === true || isAdmin) ? scope.alias : null; + }); } var directive = { @@ -106,6 +112,7 @@ scope: { icon: "=?", name: "=", + alias: "=?", description: "=?", permissions: "=?", published: "=?", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js index e82e81140f..2139a14b48 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js @@ -176,7 +176,7 @@ Use this directive to generate a pagination. } }; - var unbindPageNumberWatcher = scope.$watch('pageNumber', function(newValue, oldValue){ + var unbindPageNumberWatcher = scope.$watchCollection('[pageNumber, totalPages]', function (newValues, oldValues) { activate(); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index bf32cf0ad6..d69777464c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -290,14 +290,22 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to copy content'); }, - createContainer: function(parentId, name) { + createContainer: function (parentId, name) { return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateContainer", { parentId: parentId, name: name })), + $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateContainer", { parentId: parentId, name: name })), 'Failed to create a folder under parent id ' + parentId); }, + createCollection: function (parentId, collectionName, collectionItemName, collectionIcon, collectionItemIcon) { + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateCollection", { parentId: parentId, collectionName: collectionName, collectionItemName: collectionItemName, collectionIcon: collectionIcon, collectionItemIcon: collectionItemIcon})), + 'Failed to create collection under ' + parentId); + + }, + renameContainer: function(id, name) { return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/dictionary.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/dictionary.resource.js new file mode 100644 index 0000000000..3bb01fbe92 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/dictionary.resource.js @@ -0,0 +1,162 @@ +/** + * @ngdoc service + * @name umbraco.resources.dictionaryResource + * @description Loads in data for dictionary items +**/ +function dictionaryResource($q, $http, $location, umbRequestHelper, umbDataFormatter) { + + /** + * @ngdoc method + * @name umbraco.resources.dictionaryResource#deleteById + * @methodOf umbraco.resources.dictionaryResource + * + * @description + * Deletes a dictionary item with a given id + * + * ##usage + *
+         * dictionaryResource.deleteById(1234)
+         *    .then(function() {
+         *        alert('its gone!');
+         *    });
+         * 
+ * + * @param {Int} id id of dictionary item to delete + * @returns {Promise} resourcePromise object. + * + **/ + function deleteById(id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "dictionaryApiBaseUrl", + "DeleteById", + [{ id: id }])), + "Failed to delete item " + id); + } + + /** + * @ngdoc method + * @name umbraco.resources.dictionaryResource#create + * @methodOf umbraco.resources.dictionaryResource + * + * @description + * Creates a dictionary item with the gieven key and parent id + * + * ##usage + *
+         * dictionaryResource.create(1234,"Item key")
+         *    .then(function() {
+         *        alert('its created!');
+         *    });
+         * 
+ * + * @param {Int} parentid the parentid of the new dictionary item + * @param {String} key the key of the new dictionary item + * @returns {Promise} resourcePromise object. + * + **/ + function create(parentid, key) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "dictionaryApiBaseUrl", + "Create", + { parentId: parentid, key : key })), + "Failed to create item "); + } + + /** + * @ngdoc method + * @name umbraco.resources.dictionaryResource#deleteById + * @methodOf umbraco.resources.dictionaryResource + * + * @description + * Gets a dictionary item with a given id + * + * ##usage + *
+         * dictionaryResource.getById(1234)
+         *    .then(function() {
+         *        alert('Found it!');
+         *    });
+         * 
+ * + * @param {Int} id id of dictionary item to get + * @returns {Promise} resourcePromise object. + * + **/ + function getById(id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "dictionaryApiBaseUrl", + "GetById", + [{ id: id }])), + "Failed to get item " + id); + } + + /** + * @ngdoc method + * @name umbraco.resources.dictionaryResource#save + * @methodOf umbraco.resources.dictionaryResource + * + * @description + * Updates a dictionary + * + * @param {Object} dictionary dictionary object to update + * @param {Bool} nameIsDirty set to true if the name has been changed + * @returns {Promise} resourcePromise object. + * + */ + function save(dictionary, nameIsDirty) { + + var saveModel = umbDataFormatter.formatDictionaryPostData(dictionary, nameIsDirty); + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("dictionaryApiBaseUrl", "PostSave"), saveModel), + "Failed to save data for dictionary id " + dictionary.id); + } + + /** + * @ngdoc method + * @name umbraco.resources.dictionaryResource#getList + * @methodOf umbraco.resources.dictionaryResource + * + * @description + * Gets a list of all dictionary items + * + * ##usage + *
+         * dictionaryResource.getList()
+         *    .then(function() {
+         *        alert('Found it!');
+         *    });
+         * 
+ * + * @returns {Promise} resourcePromise object. + * + **/ + function getList() { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "dictionaryApiBaseUrl", + "getList")), + "Failed to get list"); + } + + var resource = { + deleteById: deleteById, + create: create, + getById: getById, + save: save, + getList : getList + }; + + return resource; + + +} + +angular.module("umbraco.resources").factory("dictionaryResource", dictionaryResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 056948ccd6..e3af5124f8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -406,7 +406,7 @@ function entityResource($q, $http, umbRequestHelper) { * * ##usage *
-          * entityResource.getPagedDescendants(1234, "Content", {pageSize: 10, pageNumber: 2})
+          * entityResource.getPagedDescendants(1234, "Document", {pageSize: 10, pageNumber: 2})
           *    .then(function(contentArray) {
           *        var children = contentArray; 
           *        alert('they are here!');
@@ -416,8 +416,8 @@ function entityResource($q, $http, umbRequestHelper) {
           * @param {Int} parentid id of content item to return descendants of
           * @param {string} type Object type name
           * @param {Object} options optional options object
-          * @param {Int} options.pageSize if paging data, number of nodes per page, default = 1
-          * @param {Int} options.pageNumber if paging data, current page index, default = 100
+          * @param {Int} options.pageSize if paging data, number of nodes per page, default = 100
+          * @param {Int} options.pageNumber if paging data, current page index, default = 1
           * @param {String} options.filter if provided, query will only return those with names matching the filter
           * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending`
           * @param {String} options.orderBy property to order items by, default: `SortOrder`
@@ -427,8 +427,8 @@ function entityResource($q, $http, umbRequestHelper) {
         getPagedDescendants: function (parentId, type, options) {
 
             var defaults = {
-                pageSize: 1,
-                pageNumber: 100,
+                pageSize: 100,
+                pageNumber: 1,
                 filter: '',
                 orderDirection: "Ascending",
                 orderBy: "SortOrder"
diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js
index 72564398c0..0fd308ffd0 100644
--- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js
+++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js
@@ -280,7 +280,7 @@
           *    });
           * 
* - * @param {Array} id user id. + * @param {Int} userId user id. * @returns {Promise} resourcePromise object containing the user. * */ @@ -406,6 +406,36 @@ "Failed to save user"); } + /** + * @ngdoc method + * @name umbraco.resources.usersResource#deleteNonLoggedInUser + * @methodOf umbraco.resources.usersResource + * + * @description + * Deletes a user that hasn't already logged in (and hence we know has made no content updates that would create related records) + * + * ##usage + *
+          * usersResource.deleteNonLoggedInUser(1)
+          *    .then(function() {
+          *        alert("user was deleted");
+          *    });
+          * 
+ * + * @param {Int} userId user id. + * @returns {Promise} resourcePromise object. + * + */ + function deleteNonLoggedInUser(userId) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostDeleteNonLoggedInUser", { id: userId })), + 'Failed to delete the user ' + userId); + } + var resource = { disableUsers: disableUsers, @@ -417,6 +447,7 @@ createUser: createUser, inviteUser: inviteUser, saveUser: saveUser, + deleteNonLoggedInUser: deleteNonLoggedInUser, clearAvatar: clearAvatar }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 70004900f9..18d37ea5b2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -99,6 +99,28 @@ return saveModel; }, + /** formats the display model used to display the dictionary to the model used to save the dictionary */ + formatDictionaryPostData : function(dictionary, nameIsDirty) { + var saveModel = { + parentId: dictionary.parentId, + id: dictionary.id, + name: dictionary.name, + nameIsDirty: nameIsDirty, + translations: [], + key : dictionary.key + }; + + for(var i = 0; i < dictionary.translations.length; i++) { + saveModel.translations.push({ + isoCode: dictionary.translations[i].isoCode, + languageId: dictionary.translations[i].languageId, + translation: dictionary.translations[i].translation + }); + } + + return saveModel; + }, + /** formats the display model used to display the user to the model used to save the user */ formatUserPostData: function (displayModel) { diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html index 8fbe4263e8..ff0ec56620 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html @@ -32,7 +32,6 @@ ng-pattern="passwordPattern" autocorrect="off" autocapitalize="off" - autocomplete="off" required ng-model="installer.current.model.password" id="password" /> At least {{installer.current.model.minCharLength}} characters long diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less index 0ecdd4f824..8ed034b403 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less @@ -27,7 +27,8 @@ } .umb-iconpicker-item a:hover, -.umb-iconpicker-item a:focus{ +.umb-iconpicker-item a:focus, +.umb-iconpicker-item.-selected { background: @gray-10; outline: none; } diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index a219cb500a..4df1bd9b8d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -545,6 +545,9 @@ input[type="checkbox"][readonly] { padding-left: 5px; } +div.help { + margin-top: 5px; +} // INPUT GROUPS diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 5d6eca7cda..d869d1d9af 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -188,6 +188,14 @@ h5.-black { margin-left: 0; } +label[for=""] { + cursor: default; +} + +label:not([for]) { + cursor: default; +} + /* CONTROL VALIDATION */ .umb-control-required { color: @controlRequiredColor; diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index 5443134dd2..c894784dec 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -117,37 +117,31 @@ } .password-toggle { - position: relative; - display: block; + position: relative; + text-align: right; user-select: none; - input::-ms-clear, input::-ms-reveal { - display: none; - } - a { opacity: .5; cursor: pointer; display: inline-block; - position: absolute; - height: 1px; - width: 45px; - height: 75%; - font-size: 0; - background-repeat: no-repeat; - background-size: 50%; - background-position: center; - top: 0; - margin-left: -45px; z-index: 1; -webkit-tap-highlight-color: transparent; - } - [type="text"] + a { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='%23444' d='M29.6.4C29 0 28 0 27.4.4L21 6.8c-1.4-.5-3-.8-5-.8C9 6 3 10 0 16c1.3 2.6 3 4.8 5.4 6.5l-5 5c-.5.5-.5 1.5 0 2 .3.4.7.5 1 .5s1 0 1.2-.4l27-27C30 2 30 1 29.6.4zM13 10c1.3 0 2.4 1 2.8 2L12 15.8c-1-.4-2-1.5-2-2.8 0-1.7 1.3-3 3-3zm-9.6 6c1.2-2 2.8-3.5 4.7-4.7l.7-.2c-.4 1-.6 2-.6 3 0 1.8.6 3.4 1.6 4.7l-2 2c-1.6-1.2-3-2.7-4-4.4zM24 13.8c0-.8 0-1.7-.4-2.4l-10 10c.7.3 1.6.4 2.4.4 4.4 0 8-3.6 8-8z'/%3E%3Cpath fill='%23444' d='M26 9l-2.2 2.2c2 1.3 3.6 3 4.8 4.8-1.2 2-2.8 3.5-4.7 4.7-2.7 1.5-5.4 2.3-8 2.3-1.4 0-2.6 0-3.8-.4L10 25c2 .6 4 1 6 1 7 0 13-4 16-10-1.4-2.8-3.5-5.2-6-7z'/%3E%3C/svg%3E"); - } + .password-text { + background-repeat: no-repeat; + background-size: 18px; + background-position: left center; + padding-left: 26px; - [type="password"] + a { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='%23444' d='M16 6C9 6 3 10 0 16c3 6 9 10 16 10s13-4 16-10c-3-6-9-10-16-10zm8 5.3c1.8 1.2 3.4 2.8 4.6 4.7-1.2 2-2.8 3.5-4.7 4.7-3 1.5-6 2.3-8 2.3s-6-.8-8-2.3C6 19.5 4 18 3 16c1.5-2 3-3.5 5-4.7l.6-.2C8 12 8 13 8 14c0 4.5 3.5 8 8 8s8-3.5 8-8c0-1-.3-2-.6-2.6l.4.3zM16 13c0 1.7-1.3 3-3 3s-3-1.3-3-3 1.3-3 3-3 3 1.3 3 3z'/%3E%3C/svg%3E"); + &.show { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='%23444' d='M16 6C9 6 3 10 0 16c3 6 9 10 16 10s13-4 16-10c-3-6-9-10-16-10zm8 5.3c1.8 1.2 3.4 2.8 4.6 4.7-1.2 2-2.8 3.5-4.7 4.7-3 1.5-6 2.3-8 2.3s-6-.8-8-2.3C6 19.5 4 18 3 16c1.5-2 3-3.5 5-4.7l.6-.2C8 12 8 13 8 14c0 4.5 3.5 8 8 8s8-3.5 8-8c0-1-.3-2-.6-2.6l.4.3zM16 13c0 1.7-1.3 3-3 3s-3-1.3-3-3 1.3-3 3-3 3 1.3 3 3z'/%3E%3C/svg%3E"); + } + + &.hide { + display: none; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='%23444' d='M29.6.4C29 0 28 0 27.4.4L21 6.8c-1.4-.5-3-.8-5-.8C9 6 3 10 0 16c1.3 2.6 3 4.8 5.4 6.5l-5 5c-.5.5-.5 1.5 0 2 .3.4.7.5 1 .5s1 0 1.2-.4l27-27C30 2 30 1 29.6.4zM13 10c1.3 0 2.4 1 2.8 2L12 15.8c-1-.4-2-1.5-2-2.8 0-1.7 1.3-3 3-3zm-9.6 6c1.2-2 2.8-3.5 4.7-4.7l.7-.2c-.4 1-.6 2-.6 3 0 1.8.6 3.4 1.6 4.7l-2 2c-1.6-1.2-3-2.7-4-4.4zM24 13.8c0-.8 0-1.7-.4-2.4l-10 10c.7.3 1.6.4 2.4.4 4.4 0 8-3.6 8-8z'/%3E%3Cpath fill='%23444' d='M26 9l-2.2 2.2c2 1.3 3.6 3 4.8 4.8-1.2 2-2.8 3.5-4.7 4.7-2.7 1.5-5.4 2.3-8 2.3-1.4 0-2.6 0-3.8-.4L10 25c2 .6 4 1 6 1 7 0 13-4 16-10-1.4-2.8-3.5-5.2-6-7z'/%3E%3C/svg%3E"); + } + } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 1dc69d3a0d..d18277737a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -23,6 +23,7 @@ $scope.togglePassword = function () { var elem = $("form[name='loginForm'] input[name='password']"); elem.attr("type", (elem.attr("type") === "text" ? "password" : "text")); + $(".password-text.show, .password-text.hide").toggle(); } function init() { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index e8e8ccf6a3..8d8a68f147 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -22,7 +22,7 @@ New password {{invitedUserPasswordModel.passwordPolicyText}} - + Your new password cannot be blank! Minimum {{invitedUserPasswordModel.passwordPolicies.minPasswordLength}} characters @@ -32,7 +32,7 @@
- + Required The confirmed password doesn't match the new password! @@ -154,8 +154,12 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmunpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmunpublish.controller.js new file mode 100644 index 0000000000..885df4b3a9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmunpublish.controller.js @@ -0,0 +1,9 @@ +angular.module("umbraco").controller("Umbraco.Notifications.ConfirmUnpublishController", + function ($scope, notificationsService, eventsService) { + + $scope.confirm = function(not, action){ + eventsService.emit('content.confirmUnpublish', action); + notificationsService.remove(not); + }; + + }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmunpublish.html b/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmunpublish.html new file mode 100644 index 0000000000..4bd3e2b964 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmunpublish.html @@ -0,0 +1,6 @@ +
+

Are you sure?

+

Unpublishing will remove this page and all its descendants from the site

+ + +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.controller.js new file mode 100644 index 0000000000..7a186f3d6b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.controller.js @@ -0,0 +1,42 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.PropertyController + * @function + * + * @description + * The controller for the content type editor property dialog + */ +function IconPickerOverlay($scope, iconHelper, localizationService) { + + $scope.loading = true; + $scope.model.hideSubmitButton = false; + + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectIcon"); + }; + + if ($scope.model.color) { + $scope.color = $scope.model.color; + }; + + if ($scope.model.icon) { + $scope.icon = $scope.model.icon; + }; + + iconHelper.getIcons().then(function(icons) { + $scope.icons = icons; + $scope.loading = false; + }); + + $scope.selectIcon = function(icon, color) { + $scope.model.icon = icon; + $scope.model.color = color; + $scope.submitForm($scope.model); + }; + + $scope.changeColor = function (color) { + $scope.model.color = color; + }; +} + +angular.module("umbraco").controller("Umbraco.Overlays.IconPickerOverlay", IconPickerOverlay); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.html new file mode 100644 index 0000000000..78a215d009 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.html @@ -0,0 +1,58 @@ +
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + No icons were found. + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index a6131f55cf..d2d2178861 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -84,17 +84,21 @@ angular.module("umbraco") //media object so we need to look it up var id = $scope.target.udi ? $scope.target.udi : $scope.target.id var altText = $scope.target.altText; - mediaResource.getById(id) - .then(function (node) { - $scope.target = node; - if (ensureWithinStartNode(node)) { - selectImage(node); - $scope.target.url = mediaHelper.resolveFile(node); - $scope.target.altText = altText; - $scope.openDetailsDialog(); - } - }, - gotoStartNode); + if (id) { + mediaResource.getById(id) + .then(function (node) { + $scope.target = node; + if (ensureWithinStartNode(node)) { + selectImage(node); + $scope.target.url = mediaHelper.resolveFile(node); + $scope.target.altText = altText; + $scope.openDetailsDialog(); + } + }, + gotoStartNode); + } else { + gotoStartNode(); + } } } @@ -320,20 +324,25 @@ angular.module("umbraco") mediaItem.thumbnail = mediaHelper.resolveFileFromEntity(mediaItem, true); mediaItem.image = mediaHelper.resolveFileFromEntity(mediaItem, false); // set properties to match a media object - if (mediaItem.metaData && - mediaItem.metaData.umbracoWidth && - mediaItem.metaData.umbracoHeight) { - - mediaItem.properties = [ - { + mediaItem.properties = []; + if (mediaItem.metaData) { + if (mediaItem.metaData.umbracoWidth && mediaItem.metaData.umbracoHeight) { + mediaItem.properties.push({ alias: "umbracoWidth", value: mediaItem.metaData.umbracoWidth.Value - }, - { + }); + mediaItem.properties.push({ alias: "umbracoHeight", value: mediaItem.metaData.umbracoHeight.Value - } - ]; + }); + } + if (mediaItem.metaData.umbracoFile) { + mediaItem.properties.push({ + alias: "umbracoFile", + editor: mediaItem.metaData.umbracoFile.PropertyEditorAlias, + value: mediaItem.metaData.umbracoFile.Value + }); + } } }); // update images diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js new file mode 100644 index 0000000000..1f12536d1b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -0,0 +1,507 @@ +//used for the media picker dialog +angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", + function ($scope, $q, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService, contentResource, mediaResource, memberResource) { + + var tree = null; + var dialogOptions = $scope.model; + $scope.treeReady = false; + $scope.dialogTreeEventHandler = $({}); + $scope.section = dialogOptions.section; + $scope.treeAlias = dialogOptions.treeAlias; + $scope.multiPicker = dialogOptions.multiPicker; + $scope.hideHeader = (typeof dialogOptions.hideHeader) === "boolean" ? dialogOptions.hideHeader : true; + // if you need to load a not initialized tree set this value to false - default is true + $scope.onlyInitialized = dialogOptions.onlyInitialized; + $scope.searchInfo = { + searchFromId: dialogOptions.startNodeId, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + } + + $scope.model.selection = []; + + //Used for toggling an empty-state message + //Some trees can have no items (dictionary & forms email templates) + $scope.hasItems = true; + $scope.emptyStateMessage = dialogOptions.emptyStateMessage; + var node = dialogOptions.currentNode; + + //This is called from ng-init + //it turns out it is called from the angular html : / Have a look at views/common / overlays / contentpicker / contentpicker.html you'll see ng-init. + //this is probably an anti pattern IMO and shouldn't be used + $scope.init = function (contentType) { + + if (contentType === "content") { + $scope.entityType = "Document"; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectContent"); + } + } else if (contentType === "member") { + $scope.entityType = "Member"; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectMember"); + } + } else if (contentType === "media") { + $scope.entityType = "Media"; + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); + } + } + } + + var searchText = "Search..."; + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + // Allow the entity type to be passed in but defaults to Document for backwards compatibility. + $scope.entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; + + + //min / max values + if (dialogOptions.minNumber) { + dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); + } + if (dialogOptions.maxNumber) { + dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); + } + + if (dialogOptions.section === "member") { + $scope.entityType = "Member"; + } + else if (dialogOptions.section === "media") { + $scope.entityType = "Media"; + } + + // Search and listviews is only working for content, media and member section + var searchableSections = ["content", "media", "member"]; + + $scope.enableSearh = searchableSections.indexOf($scope.section) !== -1; + + //if a alternative startnode is used, we need to check if it is a container + if ($scope.enableSearh && dialogOptions.startNodeId && dialogOptions.startNodeId !== -1 && dialogOptions.startNodeId !== "-1") { + entityResource.getById(dialogOptions.startNodeId, $scope.entityType).then(function(node) { + if (node.metaData.IsContainer) { + openMiniListView(node); + } + initTree(); + }); + } + else { + initTree(); + } + + //Configures filtering + if (dialogOptions.filter) { + + dialogOptions.filterExclude = false; + dialogOptions.filterAdvanced = false; + + //used advanced filtering + if (angular.isFunction(dialogOptions.filter)) { + dialogOptions.filterAdvanced = true; + } + else if (angular.isObject(dialogOptions.filter)) { + dialogOptions.filterAdvanced = true; + } + else { + if (dialogOptions.filter.startsWith("!")) { + dialogOptions.filterExclude = true; + dialogOptions.filter = dialogOptions.filter.substring(1); + } + + //used advanced filtering + if (dialogOptions.filter.startsWith("{")) { + dialogOptions.filterAdvanced = true; + //convert to object + dialogOptions.filter = angular.fromJson(dialogOptions.filter); + } + } + } + + function initTree() { + //create the custom query string param for this tree + $scope.customTreeParams = dialogOptions.startNodeId ? "startNodeId=" + dialogOptions.startNodeId : ""; + $scope.customTreeParams += dialogOptions.customTreeParams ? "&" + dialogOptions.customTreeParams : ""; + $scope.treeReady = true; + } + + function nodeExpandedHandler(ev, args) { + + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + + if (angular.isArray(args.children)) { + + //iterate children + _.each(args.children, function (child) { + + //now we need to look in the already selected search results and + // toggle the check boxes for those ones that are listed + var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { + return child.id == selected.id; + }); + if (exists) { + child.selected = true; + } + }); + + //check filter + performFiltering(args.children); + } + } + + //gets the tree object when it loads + function treeLoadedHandler(ev, args) { + //args.tree contains children (args.tree.root.children) + $scope.hasItems = args.tree.root.children.length > 0; + + tree = args.tree; + + if (node && node.path) { + $scope.dialogTreeEventHandler.syncTree({ path: node.path, activate: false }); + } + + } + + //wires up selection + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if (args.node.metaData.isSearchResult) { + //check if the item selected was a search result from a list view + + //unselect + select(args.node.name, args.node.id); + + //remove it from the list view children + var listView = args.node.parent(); + listView.children = _.reject(listView.children, function (child) { + return child.id == args.node.id; + }); + + //remove it from the custom tracked search result list + $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { + return i.id == args.node.id; + }); + } + else { + eventsService.emit("dialogs.treePickerController.select", args); + + if (args.node.filtered) { + return; + } + + //This is a tree node, so we don't have an entity to pass in, it will need to be looked up + //from the server in this method. + if ($scope.model.select) { + $scope.model.select(args.node) + } else { + select(args.node.name, args.node.id); + //toggle checked state + args.node.selected = args.node.selected === true ? false : true; + } + + } + } + + /** Method used for selecting a node */ + function select(text, id, entity) { + //if we get the root, we just return a constructed entity, no need for server data + if (id < 0) { + + var rootNode = { + alias: null, + icon: "icon-folder", + id: id, + name: text + }; + + if ($scope.multiPicker) { + if (entity) { + multiSelectItem(entity); + } else { + multiSelectItem(rootNode); + } + } + else { + $scope.model.selection.push(rootNode); + $scope.model.submit($scope.model); + } + } + else { + + if ($scope.multiPicker) { + + if (entity) { + multiSelectItem(entity); + } else { + //otherwise we have to get it from the server + entityResource.getById(id, $scope.entityType).then(function (ent) { + multiSelectItem(ent); + }); + } + + } + + else { + + $scope.hideSearch(); + + //if an entity has been passed in, use it + if (entity) { + $scope.model.selection.push(entity); + $scope.model.submit($scope.model); + } else { + //otherwise we have to get it from the server + entityResource.getById(id, $scope.entityType).then(function (ent) { + $scope.model.selection.push(ent); + $scope.model.submit($scope.model); + }); + } + } + } + } + + function multiSelectItem(item) { + + var found = false; + var foundIndex = 0; + + if ($scope.model.selection.length > 0) { + for (i = 0; $scope.model.selection.length > i; i++) { + var selectedItem = $scope.model.selection[i]; + if (selectedItem.id === item.id) { + found = true; + foundIndex = i; + } + } + } + + if (found) { + $scope.model.selection.splice(foundIndex, 1); + } else { + $scope.model.selection.push(item); + } + + } + + function performFiltering(nodes) { + + if (!dialogOptions.filter) { + return; + } + + //remove any list view search nodes from being filtered since these are special nodes that always must + // be allowed to be clicked on + nodes = _.filter(nodes, function (n) { + return !angular.isObject(n.metaData.listViewNode); + }); + + if (dialogOptions.filterAdvanced) { + + //filter either based on a method or an object + var filtered = angular.isFunction(dialogOptions.filter) + ? _.filter(nodes, dialogOptions.filter) + : _.where(nodes, dialogOptions.filter); + + angular.forEach(filtered, function (value, key) { + value.filtered = true; + if (dialogOptions.filterCssClass) { + if (!value.cssClasses) { + value.cssClasses = []; + } + value.cssClasses.push(dialogOptions.filterCssClass); + } + }); + } else { + var a = dialogOptions.filter.toLowerCase().replace(/\s/g, '').split(','); + angular.forEach(nodes, function (value, key) { + + var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; + + if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { + value.filtered = true; + + if (dialogOptions.filterCssClass) { + if (!value.cssClasses) { + value.cssClasses = []; + } + value.cssClasses.push(dialogOptions.filterCssClass); + } + } + }); + } + } + + $scope.multiSubmit = function (result) { + entityResource.getByIds(result, $scope.entityType).then(function (ents) { + $scope.submit(ents); + }); + }; + + /** method to select a search result */ + $scope.selectResult = function (evt, result) { + + if (result.filtered) { + return; + } + + result.selected = result.selected === true ? false : true; + + //since result = an entity, we'll pass it in so we don't have to go back to the server + select(result.name, result.id, result); + + //add/remove to our custom tracked list of selected search results + if (result.selected) { + $scope.searchInfo.selectedSearchResults.push(result); + } + else { + $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { + return i.id == result.id; + }); + } + + //ensure the tree node in the tree is checked/unchecked if it already exists there + if (tree) { + var found = treeService.getDescendantNode(tree.root, result.id); + if (found) { + found.selected = result.selected; + } + } + + }; + + $scope.hideSearch = function () { + + //Traverse the entire displayed tree and update each node to sync with the selected search results + if (tree) { + + //we need to ensure that any currently displayed nodes that get selected + // from the search get updated to have a check box! + function checkChildren(children) { + _.each(children, function (child) { + //check if the id is in the selection, if so ensure it's flagged as selected + var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { + return child.id == selected.id; + }); + //if the curr node exists in selected search results, ensure it's checked + if (exists) { + child.selected = true; + } + //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result + else if (child.metaData.isSearchResult) { + //if this tree node is under a list view it means that the node was added + // to the tree dynamically under the list view that was searched, so we actually want to remove + // it all together from the tree + var listView = child.parent(); + listView.children = _.reject(listView.children, function (c) { + return c.id == child.id; + }); + } + + //check if the current node is a list view and if so, check if there's any new results + // that need to be added as child nodes to it based on search results selected + if (child.metaData.isContainer) { + + child.cssClasses = _.reject(child.cssClasses, function (c) { + return c === 'tree-node-slide-up-hide-active'; + }); + + var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { + return i.parentId == child.id; + }); + _.each(listViewResults, function (item) { + var childExists = _.find(child.children, function (c) { + return c.id == item.id; + }); + if (!childExists) { + var parent = child; + child.children.unshift({ + id: item.id, + name: item.name, + cssClass: "icon umb-tree-icon sprTree " + item.icon, + level: child.level + 1, + metaData: { + isSearchResult: true + }, + hasChildren: false, + parent: function () { + return parent; + } + }); + } + }); + } + + //recurse + if (child.children && child.children.length > 0) { + checkChildren(child.children); + } + }); + } + checkChildren(tree.root.children); + } + + + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = dialogOptions.startNodeId; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + } + + $scope.onSearchResults = function (results) { + + //filter all items - this will mark an item as filtered + performFiltering(results); + + //now actually remove all filtered items so they are not even displayed + results = _.filter(results, function (item) { + return !item.filtered; + }); + + $scope.searchInfo.results = results; + + //sync with the curr selected results + _.each($scope.searchInfo.results, function (result) { + var exists = _.find($scope.model.selection, function (selectedId) { + return result.id == selectedId; + }); + if (exists) { + result.selected = true; + } + }); + + $scope.searchInfo.showSearch = true; + }; + + $scope.dialogTreeEventHandler.bind("treeLoaded", treeLoadedHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeLoaded", treeLoadedHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + + $scope.selectListViewNode = function (node) { + select(node.name, node.id); + //toggle checked state + node.selected = node.selected === true ? false : true; + }; + + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + + function openMiniListView(node) { + $scope.miniListView = node; + } + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index c9e4af4062..a6183435ae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -33,9 +33,8 @@
- +
@@ -48,12 +47,11 @@
- +
@@ -64,13 +62,16 @@
- - {{ item.logType }} + + + {{ item.logType }} - {{ item.comment }} + + {{ item.comment }} + + +
@@ -79,11 +80,10 @@
- +
@@ -101,10 +101,9 @@
- +
@@ -133,10 +132,9 @@
- +
@@ -184,12 +182,13 @@ - + - - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html index d04de47757..2d3d2cfdae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html @@ -2,7 +2,7 @@
-
{{ name }}
+
{{ name }}
{{ description }}
@@ -13,9 +13,9 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html index 39363be827..7b6c7ca0e1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html @@ -31,8 +31,7 @@ class="input-block-level umb-textstring textstring" required val-server-field="oldPassword" - no-dirty-check - autocomplete="off" /> + no-dirty-check /> Required {{passwordForm.oldPassword.errorMsg}} @@ -45,8 +44,7 @@ required val-server-field="password" ng-minlength="{{config.minPasswordLength}}" - no-dirty-check - autocomplete="off" /> + no-dirty-check /> Required Minimum {{config.minPasswordLength}} characters @@ -58,8 +56,7 @@ + no-dirty-check /> The confirmed password doesn't match the new password! 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 c3cbb63704..85ec8461f2 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 @@ -50,8 +50,9 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig $scope.preValues = []; if ($routeParams.create) { - + $scope.page.loading = true; + $scope.showIdentifier = false; //we are creating so get an empty data type item dataTypeResource.getScaffold($routeParams.id) @@ -77,6 +78,8 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig $scope.page.loading = true; + $scope.showIdentifier = true; + //we are editing so get the content item from the server dataTypeResource.getById($routeParams.id) .then(function(data) { diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/create.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/create.html new file mode 100644 index 0000000000..5b73df3f86 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/create.html @@ -0,0 +1,19 @@ +
+ +
+
Create an item under {{currentNode.name}}
+
+ +
+
+ + + + + + +
+
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/delete.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/delete.html new file mode 100644 index 0000000000..61c26c09a7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/delete.html @@ -0,0 +1,12 @@ +
+
+ +

+ Are you sure you want to delete {{currentNode.name}} ? +

+ + + + +
+
\ No newline at end of file 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 new file mode 100644 index 0000000000..f47244bb10 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js @@ -0,0 +1,44 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.Dictionary.CreateController + * @function + * + * @description + * The controller for creating dictionary items + */ +function DictionaryCreateController($scope, $location, dictionaryResource, navigationService, notificationsService, formHelper, appState) { + var vm = this; + + vm.itemKey = ""; + + function createItem() { + + var node = $scope.dialogOptions.currentNode; + + dictionaryResource.create(node.id, vm.itemKey).then(function (data) { + navigationService.hideMenu(); + + // set new item as active in tree + var currPath = node.path ? node.path : "-1"; + navigationService.syncTree({ tree: "dictionary", path: currPath + "," + data, forceReload: true, activate: true }); + + // reset form state + formHelper.resetForm({ scope: $scope }); + + // navigate to edit view + var currentSection = appState.getSectionState("currentSection"); + $location.path("/" + currentSection + "/dictionary/edit/" + data); + + + }, function (err) { + if (err.data && err.data.message) { + notificationsService.error(err.data.message); + navigationService.hideMenu(); + } + }); + } + + vm.createItem = createItem; +} + +angular.module("umbraco").controller("Umbraco.Editors.Dictionary.CreateController", DictionaryCreateController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.delete.controller.js new file mode 100644 index 0000000000..43d6bac401 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.delete.controller.js @@ -0,0 +1,50 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.Dictionary.DeleteController + * @function + * + * @description + * The controller for deleting dictionary items + */ +function DictionaryDeleteController($scope, $location, dictionaryResource, treeService, navigationService, appState) { + var vm = this; + + function cancel() { + navigationService.hideDialog(); + } + + function performDelete() { + // stop from firing again on double-click + if ($scope.busy) { return false; } + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + $scope.busy = true; + + dictionaryResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + // get the parent id + var parentId = $scope.currentNode.parentId; + + treeService.removeNode($scope.currentNode); + + navigationService.hideMenu(); + + var currentSection = appState.getSectionState("currentSection"); + if (parentId !== "-1") { + // set the view of the parent item + $location.path("/" + currentSection + "/dictionary/edit/" + parentId); + } else { + // we have no parent, so redirect to section + $location.path("/" + currentSection + "/"); + } + + }); + } + + vm.cancel = cancel; + vm.performDelete = performDelete; +} + +angular.module("umbraco").controller("Umbraco.Editors.Dictionary.DeleteController", DictionaryDeleteController); 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 new file mode 100644 index 0000000000..1b9d5c3f4c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js @@ -0,0 +1,115 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.Dictionary.EditController + * @function + * + * @description + * The controller for editing dictionary items + */ +function DictionaryEditController($scope, $routeParams, dictionaryResource, treeService, navigationService, appState, editorState, contentEditingHelper, formHelper, notificationsService, localizationService) { + var vm = this; + + //setup scope vars + vm.nameDirty = false; + vm.page = {}; + vm.page.loading = false; + vm.page.nameLocked = false; + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; + vm.description = ""; + + function loadDictionary() { + + vm.page.loading = true; + + //we are editing so get the content item from the server + dictionaryResource.getById($routeParams.id) + .then(function (data) { + + bindDictionary(data); + + vm.page.loading = false; + }); + } + + function createTranslationProperty(translation) { + return { + alias: translation.isoCode, + label: translation.displayName, + hideLabel : false + } + } + + function bindDictionary(data) { + localizationService.localize("dictionaryItem_description").then(function (value) { + vm.description = value.replace("%0%", data.name); + }); + + // create data for umb-property displaying + for (var i = 0; i < data.translations.length; i++) { + data.translations[i].property = createTranslationProperty(data.translations[i]); + } + + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data + }); + + // set content + vm.content = data; + + //share state + editorState.set(vm.content); + + navigationService.syncTree({ tree: "dictionary", path: data.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + function onInit() { + loadDictionary(); + } + + function saveDictionary() { + if (formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { + + vm.page.saveButtonState = "busy"; + + dictionaryResource.save(vm.content, vm.nameDirty) + .then(function (data) { + + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + + bindDictionary(data); + + + vm.page.saveButtonState = "success"; + }, + function (err) { + + contentEditingHelper.handleSaveError({ + redirectOnFailure: false, + err: err + }); + + notificationsService.error(err.data.message); + + vm.page.saveButtonState = "error"; + }); + } + } + + vm.save = saveDictionary; + + $scope.$watch("vm.content.name", function (newVal, oldVal) { + //when the value changes, we need to set the name dirty + if (newVal && (newVal !== oldVal) && typeof(oldVal) !== "undefined") { + vm.nameDirty = true; + } + }); + + onInit(); +} + +angular.module("umbraco").controller("Umbraco.Editors.Dictionary.EditController", DictionaryEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.list.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.list.controller.js new file mode 100644 index 0000000000..35739b3db7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.list.controller.js @@ -0,0 +1,47 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.Dictionary.ListController + * @function + * + * @description + * The controller for listting dictionary items + */ +function DictionaryListController($scope, $location, dictionaryResource, localizationService, appState) { + var vm = this; + vm.title = "Dictionary overview"; + vm.loading = false; + vm.items = []; + + function loadList() { + + vm.loading = true; + + dictionaryResource.getList() + .then(function (data) { + + vm.items = data; + + vm.loading = false; + }); + } + + function clickItem(id) { + var currentSection = appState.getSectionState("currentSection"); + $location.path("/" + currentSection + "/dictionary/edit/" + id); + } + + vm.clickItem = clickItem; + + function onInit() { + localizationService.localize("dictionaryItem_overviewTitle").then(function (value) { + vm.title = value; + }); + + loadList(); + } + + onInit(); +} + + +angular.module("umbraco").controller("Umbraco.Editors.Dictionary.ListController", DictionaryListController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html new file mode 100644 index 0000000000..c016d37eca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html @@ -0,0 +1,43 @@ +
+ + +
+ + + + + +

+ + + +
+ + + + + + + + + + + +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html new file mode 100644 index 0000000000..82b088df49 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html @@ -0,0 +1,46 @@ +
+ + + + + + + +
+
+
+
+
+ Name +
+
+ + + +
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
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 10e04c0384..80fb69aa47 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 @@ -6,12 +6,13 @@ * @description * The controller for the doc type creation dialog */ -function DocumentTypesCreateController($scope, $location, navigationService, contentTypeResource, formHelper, appState, notificationsService, localizationService) { +function DocumentTypesCreateController($scope, $location, navigationService, contentTypeResource, formHelper, appState, notificationsService, localizationService, iconHelper) { $scope.model = { allowCreateFolder: $scope.dialogOptions.currentNode.parentId === null || $scope.dialogOptions.currentNode.nodeType === "container", folderName: "", creatingFolder: false, + creatingDoctypeCollection: false }; var disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates; @@ -23,6 +24,10 @@ function DocumentTypesCreateController($scope, $location, navigationService, con $scope.model.creatingFolder = true; }; + $scope.showCreateDocTypeCollection = function () { + $scope.model.creatingDoctypeCollection = true; + }; + $scope.createContainer = function () { if (formHelper.submitForm({ scope: $scope, formCtrl: this.createFolderForm })) { @@ -52,6 +57,54 @@ function DocumentTypesCreateController($scope, $location, navigationService, con } }; + $scope.createCollection = function () { + + if (formHelper.submitForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm, statusMessage: "Creating Doctype Collection..." })) { + + // see if we can find matching icons + var collectionIcon = "icon-folders", collectionItemIcon = "icon-document"; + iconHelper.getIcons().then(function (icons) { + + for (var i = 0; i < icons.length; i++) { + // for matching we'll require a full match for collection, partial match for item + if (icons[i].substring(5) == $scope.model.collectionName.toLowerCase()) { + collectionIcon = icons[i]; + } else if (icons[i].substring(5).indexOf($scope.model.collectionItemName.toLowerCase()) > -1) { + collectionItemIcon = icons[i]; + } + } + + contentTypeResource.createCollection(node.id, $scope.model.collectionName, $scope.model.collectionItemName, collectionIcon, collectionItemIcon).then(function (collectionData) { + + navigationService.hideMenu(); + $location.search('create', null); + $location.search('notemplate', null); + + formHelper.resetForm({ + scope: $scope + }); + + var section = appState.getSectionState("currentSection"); + + // redirect to the item id + $location.path("/settings/documenttypes/edit/" + collectionData.itemId); + + }, function (err) { + + $scope.error = err; + + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }); + } + + }; + // Disabling logic for creating document type with template if disableTemplates is set to true if (!disableTemplates) { $scope.createDocType = function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html index e5043be785..549ad0452b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html @@ -1,6 +1,6 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js index 2608ddf300..14b3a56c9b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js @@ -182,7 +182,7 @@ contentTypeHelper.generateModels().then(function (result) { - if (result.success) { + if (!result.lastError) { //re-check model status contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js index 758bc8cc1c..e424d58929 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js @@ -13,6 +13,7 @@ vm.addChild = addChild; vm.removeChild = removeChild; + vm.toggle = toggle; /* ---------- INIT ---------- */ @@ -67,6 +68,18 @@ $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); } + /** + * Toggle the $scope.model.allowAsRoot value to either true or false + */ + function toggle(){ + if($scope.model.allowAsRoot){ + $scope.model.allowAsRoot = false; + return; + } + + $scope.model.allowAsRoot = true; + } + } angular.module("umbraco").controller("Umbraco.Editors.MediaType.PermissionsController", PermissionsController); diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html index d34b31c8ea..9fa2efe9fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html @@ -8,10 +8,12 @@
- + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js index 27fc36bc0c..947269a98a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js @@ -108,7 +108,7 @@ contentTypeHelper.generateModels().then(function (result) { - if (result.success) { + if (!result.lastError) { //re-check model status contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js index 1e7d9836f9..729d899439 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js @@ -16,6 +16,10 @@ function booleanEditorController($scope, $rootScope, assetsService) { setupViewModel(); + if( $scope.model && !$scope.model.value ) { + $scope.model.value = ($scope.renderModel.value === true) ? '1' : '0'; + } + //here we declare a special method which will be called whenever the value has changed from the server //this is instead of doing a watch on the model.value = faster $scope.model.onValueChanged = function (newVal, oldVal) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 919f0fee39..0543fc0c66 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -120,6 +120,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper entityType: entityType, filterCssClass: "not-allowed not-published", startNodeId: null, + currentNode: editorState ? editorState.current : null, callback: function (data) { if (angular.isArray(data)) { _.each(data, function (item, i) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 728c820ca9..efafb9066d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -106,7 +106,8 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.overlayMenu = { show: false, - style: {} + style: {}, + showFilter: false }; // helper to force the current form into the dirty state @@ -344,6 +345,8 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.currentNode = $scope.nodes[0]; } + $scope.overlayMenu.showFilter = $scope.scaffolds.length > 15; + inited = true; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html index bc3e4547b9..eb7c98d016 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html @@ -47,8 +47,20 @@
+ + +
    -
  • +
  • {{scaffold.name}} diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index 8eb4488a9c..0f95f78adc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -543,9 +543,13 @@ var availableMasterTemplates = []; // filter out the current template and the selected master template - angular.forEach(vm.templates, function(template){ - if(template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { - availableMasterTemplates.push(template); + angular.forEach(vm.templates, function (template) { + if (template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { + var templatePathArray = template.path.split(','); + // filter descendant templates of current template + if (templatePathArray.indexOf(String(vm.template.id)) === -1) { + availableMasterTemplates.push(template); + } } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index a65b9602ec..48ccef0e51 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -31,6 +31,8 @@ vm.disableUser = disableUser; vm.enableUser = enableUser; vm.unlockUser = unlockUser; + vm.resendInvite = resendInvite; + vm.deleteNonLoggedInUser = deleteNonLoggedInUser; vm.changeAvatar = changeAvatar; vm.clearAvatar = clearAvatar; vm.save = save; @@ -49,7 +51,9 @@ "sections_users", "content_contentRoot", "media_mediaRoot", - "user_noStartNodes" + "user_noStartNodes", + "user_defaultInvitationMessage", + "user_deleteUserConfirmation" ]; localizationService.localizeMany(labelKeys).then(function (values) { @@ -61,6 +65,8 @@ vm.labels.contentRoot = values[5]; vm.labels.mediaRoot = values[6]; vm.labels.noStartNodes = values[7]; + vm.labels.defaultInvitationMessage = values[8]; + vm.labels.deleteUserConfirmation = values[9]; }); // get user @@ -330,6 +336,44 @@ }); } + function resendInvite() { + vm.resendInviteButtonState = "busy"; + + if (vm.resendInviteMessage) { + vm.user.message = vm.resendInviteMessage; + } + else { + vm.user.message = vm.labels.defaultInvitationMessage; + } + + usersResource.inviteUser(vm.user).then(function (data) { + vm.resendInviteButtonState = "success"; + vm.resendInviteMessage = ""; + formHelper.showNotifications(data); + }, function (error) { + vm.resendInviteButtonState = "error"; + formHelper.showNotifications(error.data); + }); + } + + function deleteNonLoggedInUser() { + vm.deleteNotLoggedInUserButtonState = "busy"; + + var confirmationMessage = vm.labels.deleteUserConfirmation; + if (!confirm(confirmationMessage)) { + vm.deleteNotLoggedInUserButtonState = "danger"; + return; + } + + usersResource.deleteNonLoggedInUser(vm.user.id).then(function (data) { + formHelper.showNotifications(data); + goToPage(vm.breadcrumbs[0]); + }, function (error) { + vm.deleteNotLoggedInUserButtonState = "error"; + formHelper.showNotifications(error.data); + }); + } + function clearAvatar() { // get user usersResource.clearAvatar(vm.user.id).then(function (data) { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index 599389a9f0..a6ff881f7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -8,7 +8,7 @@ - + @@ -250,7 +251,7 @@
    -
    - + + + +
    + + + +
    +
    Last login: diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 271d9eaf42..7ee98a4c12 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -241,6 +241,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml index 3b486e626a..a3fb34d4bf 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml @@ -47,7 +47,7 @@ @if (success) { @* This message will show if RedirectOnSucces is set to false (default) *@ -

    Registration succeeeded.

    +

    Registration succeeded.

    } else { @@ -101,4 +101,4 @@ else } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI/Umbraco/config/create/UI.Release.xml b/src/Umbraco.Web.UI/Umbraco/config/create/UI.Release.xml index b24cb65fea..52abf37133 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/create/UI.Release.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/create/UI.Release.xml @@ -1,229 +1,90 @@ - - -
    Template
    - /create/simple.ascx - - - - -
    - -
    Template
    - /create/simple.ascx - - - -
    - -
    Macro
    - /create/simple.ascx - - - -
    - -
    Macro
    - /create/simple.ascx - - - - -
    - -
    User
    - /create/user.ascx - - - - -
    - -
    User
    - /create/simple.ascx - - - -
    - -
    membergroup
    - /create/simple.ascx - - - -
    - -
    Stylesheet
    - /create/simple.ascx - - - -
    - -
    member
    - /create/member.ascx - - - -
    - -
    member
    - /create/member.ascx - - - -
    - -
    membergroup
    - /create/simple.ascx - - - -
    - -
    Stylesheet editor egenskab
    - /create/simple.ascx - - - - -
    - -
    Stylesheet editor egenskab
    - /create/simple.ascx - - - -
    - -
    Dictionary editor egenskab
    - /create/simple.ascx - - - - -
    - - - - - - -
    Scripting file
    - /create/DLRScripting.ascx - - - -
    - -
    Macro
    - /create/DLRScripting.ascx - - - -
    - -
    Scripting file
    - /create/DLRScripting.ascx - - - -
    - -
    Macro
    - /create/DLRScripting.ascx - - - -
    - -
    Script file
    - /create/script.ascx - - - -
    - -
    Script file
    - /create/script.ascx - - - - -
    - -
    Macro
    - /create/script.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    User Types
    - /create/simple.ascx - - - - -
    - -
    Macro
    - /Create/PartialView.ascx - - - - -
    - -
    Macro
    - /Create/PartialViewMacro.ascx - - - - -
    - -
    Macro
    - /Create/PartialView.ascx - - - - -
    - -
    Macro
    - /Create/PartialViewMacro.ascx - - - - -
    + + +
    Macro
    + /create/simple.ascx + + + +
    + +
    Macro
    + /create/simple.ascx + + + + +
    + +
    membergroup
    + /create/simple.ascx + + + +
    + +
    membergroup
    + /create/simple.ascx + + + +
    + +
    Stylesheet
    + /create/simple.ascx + + + +
    + +
    Stylesheet
    + /create/simple.ascx + + + + +
    + +
    Stylesheet editor egenskab
    + /create/simple.ascx + + + + +
    + +
    Stylesheet editor egenskab
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    diff --git a/src/Umbraco.Web.UI/Umbraco/config/create/UI.xml b/src/Umbraco.Web.UI/Umbraco/config/create/UI.xml index 370bc441bd..82adc71525 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/create/UI.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/create/UI.xml @@ -1,238 +1,90 @@  - - -
    Template
    - /create/simple.ascx - - - - -
    - -
    Template
    - /create/simple.ascx - - - -
    - -
    Macro
    - /create/simple.ascx - - - -
    - -
    Macro
    - /create/simple.ascx - - - - -
    - - -
    User
    - /create/user.ascx - - - - -
    - -
    User
    - /create/simple.ascx - - - -
    - -
    membergroup
    - /create/simple.ascx - - - -
    - -
    Stylesheet
    - /create/simple.ascx - - - -
    - -
    Stylesheet
    - /create/simple.ascx - - - - -
    - -
    membergroup
    - /create/simple.ascx - - - -
    - -
    Stylesheet editor egenskab
    - /create/simple.ascx - - - - -
    - -
    Stylesheet editor egenskab
    - /create/simple.ascx - - - -
    - -
    Dictionary editor egenskab
    - /create/simple.ascx - - - - -
    - - - - - - -
    Scripting file
    - /create/DLRScripting.ascx - - - -
    - -
    Macro
    - /create/DLRScripting.ascx - - - -
    - -
    Scripting file
    - /create/DLRScripting.ascx - - - -
    - -
    Macro
    - /create/DLRScripting.ascx - - - -
    - -
    Script file
    - /create/script.ascx - - - -
    - -
    Script file
    - /create/script.ascx - - - - -
    - -
    Macro
    - /create/script.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    User Types
    - /create/simple.ascx - - - - -
    - -
    Macro
    - /Create/PartialView.ascx - - - - -
    - -
    Macro
    - /Create/PartialView.ascx - - - -
    - -
    Macro
    - /Create/PartialViewMacro.ascx - - - - -
    - -
    Macro
    - /Create/PartialViewMacro.ascx - - - -
    - -
    Macro
    - /Create/PartialView.ascx - - - - -
    - -
    Macro
    - /Create/PartialViewMacro.ascx - - - - -
    + + +
    Macro
    + /create/simple.ascx + + + +
    + +
    Macro
    + /create/simple.ascx + + + + +
    + +
    membergroup
    + /create/simple.ascx + + + +
    + +
    membergroup
    + /create/simple.ascx + + + +
    + +
    Stylesheet
    + /create/simple.ascx + + + +
    + +
    Stylesheet
    + /create/simple.ascx + + + + +
    + +
    Stylesheet editor egenskab
    + /create/simple.ascx + + + + +
    + +
    Stylesheet editor egenskab
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 2614c3d6f5..5d1746466b 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -97,9 +97,6 @@ Domæner - - For - Ryd valg Vælg @@ -128,8 +125,8 @@ Gem og planlæg Gem og send til udgivelse Gem listevisning - Se siden - Preview er deaktiveret fordi der ikke er nogen skabelon tildelt + Forhåndsvisning + Forhåndsvisning er deaktiveret fordi der ikke er nogen skabelon tildelt Vælg formattering Vis koder Indsæt tabel @@ -138,6 +135,27 @@ Fortryd Genskab + + For + Brugeren har slettet indholdet + Brugeren har afpubliceret indholdet + Brugeren har gemt og udgivet indholdet + Brugeren har gemt indholdet + Brugeren har flyttet indholdet + Brugeren har kopieret indholdet + Brugeren har tilbagerullet indholdet til en tidligere tilstand + Brugeren har sendt indholdet til udgivelse + Brugeren har sendt indholdet til oversættelse + Kopieret + Udgivet + Flyttet + Gemt + Slettet + Afpubliceret + Indhold tilbagerullet + Sendt til udgivelse + Sendt til oversættelse + For at skifte det valgte indholds dokumenttype, skal du først vælge en ny dokumenttype, som er gyldig på denne placering. Kontroller derefter, at alle egenskaber bliver overført rigtigt til den nye dokumenttype, og klik derefter på Gem. @@ -193,7 +211,10 @@ Upd: dette dokument er udgiver, men er ikke i cachen (intern fejl) Kunne ikke hente url'en Dette dokument er udgivet, men dets url ville kollidere med indholdet %0% - Udgivet + Udgiv + Udgivet + Udgivet (Ventede ændringer) + Udgivelsesstatus Udgivelsesdato Afpubliceringsdato @@ -206,6 +227,7 @@ Alternativ tekst (valgfri) Type Afpublicér + Afpubliceret Sidst redigeret Tidspunkt for seneste redigering Fjern fil @@ -379,6 +401,7 @@ Vælg brugere Ingen ikoner blev fundet Der er ingen parametre for denne makro + Der er ikke tilføjet nogen makroer Link dit Fjern link fra dit konto @@ -645,6 +668,16 @@ Brug listevisning Tillad på rodniveau + + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + + General + Editor @@ -752,6 +785,8 @@ Glemt adgangskode? En e-mail vil blive sendt til den angivne adresse med et link til at nulstille din adgangskode En e-mail med instruktioner for nulstilling af adgangskoden vil blive sendt til den angivne adresse, hvis det matcher vores optegnelser + Vis adgangskode + Skjul adgangskode Tilbage til login formular Angiv en ny adgangskode Din adgangskode er blevet opdateret @@ -888,7 +923,7 @@ Mange hilsner fra Umbraco robotten Rollebaseret beskyttelse Hvis du ønsker at kontrollere adgang til siden ved hjælp af rollebaseret godkendelse via Umbracos medlemsgrupper. - rollebaseret godkendelse]]> + Du skal oprette en medlemsgruppe før du kan bruge rollebaseret godkendelse Fejlside Brugt når folk er logget ind, men ingen adgang Vælg hvordan siden skal beskyttes @@ -1093,31 +1128,131 @@ Mange hilsner fra Umbraco robotten Forhåndsvisning Styles + Rediger skabelon + + Sektioner Indsæt indholdsområde - Indsæt indholdsområdemarkering - Indsæt ordbogselement - Indsæt makro - Indsæt Umbraco sidefelt + Indsæt pladsholder for indholdsområde + + Indsæt + Hvad vil du indsætte? + + Oversættelse + Indsætter en oversætbar tekst, som skifter efter det sprog, som websitet vises i. + + Makro + + En makro er et element, som kan have forskellige indstillinger, når det indsættes. + Brug det som en genbrugelig del af dit design såsom gallerier, formularer og lister. + + + Sideværdi + + Viser værdien af et felt fra den nuværende side. Kan indstilles til at bruge rekursive værdier eller + vise en standardværdi i tilfælde af, at feltet er tomt. + + + Partial view + + Et Partial View er et skabelonelement, som kan indsættes i andre skabeloner og derved + genbruges og deles på tværs af sideskabelonerne. + + Master skabelon Lynguide til Umbracos skabelontags + Ingen masterskabelon + Ingen master + + Indsæt en underliggende skabelon + + @RenderBody() element. + ]]> + + + + Definer en sektion + + @section { ... }. Herefter kan denne sektion flettes ind i + overliggende skabelon ved at indsætte et @RenderSection element. + ]]> + + + Indsæt en sektion + + @RenderSection(name) element. Den underliggende skabelon skal have + defineret en sektion via et @section [name]{ ... } element. + ]]> + + + Sektionsnavn + Sektionen er obligatorisk + + + Hvis obligatorisk, skal underskabelonen indeholde en @section -definition. + + + + Query builder + sider returneret, på + + Returner + alt indhold + indhold af typen "%0%" + + fra + mit website + hvor + og + + er + ikke er + er før + er før (inkl. valgte dato) + er efter + er efter (inkl. valgte dato) + er + ikke er + indeholder + ikke indeholder + er større end + er større end eller det samme som + er mindre end + er mindre end eller det samme som + + Id + Navn + Oprettelsesdato + Sidste opdatering + + Sortér efter + stigende rækkefølge + faldende rækkefølge + Skabelon + Rich Text Editor - Image + Billede Macro Embed - Headline - Quote + Overskrift + Citat Vælg indholdstype Vælg layout Tilføj række Tilføj indhold Slip indhold Indstillinger tilføjet - + Indholdet er ikke tilladt her Indholdet er tilladt her @@ -1126,9 +1261,9 @@ Mange hilsner fra Umbraco robotten Billedtekst... Skriv her... - Gitterlayout - Et layout er det overordnede arbejdsområde til dit gitter - du vil typisk kun behøve ét eller to - Tilføj gitterlayout + Grid layout + Et layout er det overordnede arbejdsområde til dit grid - du vil typisk kun behøve ét eller to + Tilføj grid layout Juster dit layout ved at justere kolonnebredder og tilføj yderligere sektioner Rækkekonfigurationer @@ -1137,7 +1272,7 @@ Mange hilsner fra Umbraco robotten Juster rækken ved at indstille cellebredder og tilføje yderligere celler Kolonner - Det totale antaller kolonner i gitteret + Det totale antal kolonner i dit grid Indstillinger Konfigurer, hvilket indstillinger, brugeren kan ændre @@ -1239,10 +1374,10 @@ Mange hilsner fra Umbraco robotten Fjern paragraf-tags Fjerner eventuelle &lt;P&gt; omkring teksten Standard felter - Uppercase + Store bogstaver URL encode Hvis indholdet af felterne skal sendes til en url, skal denne slåes til så specialtegn formateres - Denne tekst vil blive brugt hvis ovenstående felter er tomme + Denne tekst bruges hvis ovenstående felter er tomme Dette felt vil blive brugt hvis ovenstående felt er tomt Ja, med klokkeslæt. Dato/tid separator: @@ -1298,6 +1433,8 @@ Mange hilsner fra Umbraco robotten Relationstyper Pakker Pakker + Partial Views + Partial View Makro Filer Python Installer fra "repository" Installer Runway @@ -1425,7 +1562,7 @@ Mange hilsner fra Umbraco robotten - Validation + Validering Valider som e-mail Valider som tal Valider som Url @@ -1460,4 +1597,7 @@ Mange hilsner fra Umbraco robotten Ingen ordbog elementer at vælge imellem - + + Karakterer tilbage + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 568b5ac693..382cb29d9f 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -34,7 +34,7 @@ Restore Set permissions for the page %0% Choose where to move - to in the tree structure below + In the tree structure below Permissions Rollback Send To Publish @@ -46,7 +46,34 @@ Update Set permissions Unlock - Default value + Create Content Template + Resend Invitation + + + Content + Administration + Structure + Other + + + Allow access to assign culture and hostnames + Allow access to view a node's history log + Allow access to view a node + Allow access to change document type for a node + Allow access to copy a node + Allow access to create nodes + Allow access to delete nodes + Allow access to move a node + Allow access to set and change public access for a node + Allow access to publish a node + Allow access to change permissions for a node + Allow access to roll back a node to a previous state + Allow access to send a node for approval before publishing + Allow access to send a node for translation + Allow access to change the sort order for nodes + Allow access to translate a node + Allow access to save a node + Allow access to create a Content Template Permission denied. @@ -62,18 +89,19 @@ Domain '%0%' has already been assigned Domain '%0%' has been updated Edit Current Domains - + + should be avoided. Better use the culture setting above.]]> + Inherit Culture - or inherit culture from parent nodes. Will also apply
    - to the current node, unless a domain below applies too.]]>
    + + or inherit culture from parent nodes. Will also apply
    + to the current node, unless a domain below applies too.]]> +
    Domains - - Viewing for - Clear selection Select @@ -98,7 +126,7 @@ Edit relations Return to list Save - Publish + Save and publish Save and schedule Save and send for approval Save list view @@ -109,6 +137,29 @@ Insert table Generate models Save and generate models + Undo + Redo + + + Viewing for + Delete Content performed by user + UnPublish performed by user + Save and Publish performed by user + Save Content performed by user + Move Content performed by user + Copy Content performed by user + Content rollback performed by user + Content Send To Publish performed by user + Content Send To Translation performed by user + Copy + Publish + Move + Save + Delete + Unpublish + Rollback + Send To Publish + Send To Translation To change the document type for the selected content, first select from the list of valid types for this location. @@ -196,9 +247,23 @@ Target This translates to the following time on the server: What does this mean?
    ]]> + Are you sure you want to delete this item? + Property %0% uses editor %1% which is not supported by Nested Content. + Add another text box + Remove this text box + Content root This value is hidden. If you need access to view this value please contact your website administrator. This value is hidden. + + Create a new Content Template from '%0%' + Blank + Select a Content Template + Content Template created + A Content Template was created from '%0%' + Another Content Template with the same name already exists + A Content Template is pre-defined content that an editor can select to use as the basis for creating new content + Click to upload Drop your files here... @@ -207,6 +272,7 @@ Only allowed file types are Cannot upload this file, it does not have an approved file type Max file size is + Media root Create a new member @@ -223,6 +289,13 @@ Document Type without a template New folder New data type + New javascript file + New empty partial view + New partial view macro + New partial view from snippet + New empty partial view macro + New partial view macro from snippet + New partial view macro (without macro) Browse your website @@ -238,6 +311,7 @@ Discard changes You have unsaved changes Are you sure you want to navigate away from this page? - you have unsaved changes + Unpublishing will remove this page and all its descendants from the site. Done @@ -312,10 +386,14 @@ The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. Number of columns Number of rows - Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, - by referring this ID using a <asp:content /> element.]]> - Select a placeholder id from the list below. You can only - choose Id's from the current template's master.]]> + + Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, + by referring this ID using a <asp:content /> element.]]> + + + Select a placeholder id from the list below. You can only + choose Id's from the current template's master.]]> + Click on the image to see full size Pick item View Cache Item @@ -326,6 +404,7 @@ Link to page Opens the linked document in a new window or tab Link to media + Link to file Select content start node Select media Select icon @@ -341,6 +420,7 @@ Select users No icons were found There are no parameters for this macro + There are no macros available to insert External login providers Exception Details Stacktrace @@ -349,11 +429,14 @@ Un-link your account Select editor + Select snippet - + %0%' below
    You can add additional languages under the 'languages' in the menu on the left - ]]>
    + ]]> + Culture Name Edit the key of the dictionary item. @@ -361,6 +444,7 @@ The key '%0%' already exists. ]]> + Dictionary overview Enter your username @@ -375,7 +459,7 @@ Type to search... Type to filter... Type to add tags (press enter after each tag)... - Enter your email + Enter your email... Enter a message... Your username is usually your email @@ -414,6 +498,13 @@ Related stylesheets Show label Width and height + All property types & property data + using this data type will be deleted permanently, please confirm you want to delete these as well + Yes, delete + and all property types & property data using this data type + Select the folder to move + to in the tree structure below + was moved underneath Your data has been saved, but before you can publish this page there are some errors you need to fix first: @@ -469,6 +560,7 @@ Close Window Comment Confirm + Constrain Constrain proportions Continue Copy @@ -480,6 +572,7 @@ Deleted Deleting... Design + Dictionary Dimensions Down Download @@ -489,6 +582,7 @@ Email Error Find + First General Groups Height @@ -505,6 +599,7 @@ Justify Label Language + Last Layout Links Loading @@ -538,13 +633,17 @@ Recycle Bin Your recycle bin is empty Remaining + Remove Rename Renew Required + Retrieve Retry Permissions Scheduled Publishing Search + Sorry, we can not find what you are looking for + No items have been added Server Settings Show @@ -622,6 +721,15 @@ Toggle list view Toggle allow as root + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + + General + Editor Background colour @@ -638,28 +746,37 @@ Could not save the web.config file. Please modify the connection string manually. Your database has been found and is identified as Database configuration - + install button to install the Umbraco %0% database - ]]> + ]]> + Next to proceed.]]> - Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

    + + Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

    To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

    Click the retry button when done.
    - More information on editing web.config here.

    ]]>
    - + More information on editing web.config here.

    ]]> +
    + + Please contact your ISP if necessary. - If you're installing on a local machine or server you might need information from your system administrator.]]> - + + + Press the upgrade button to upgrade your database to Umbraco %0%

    Don't worry - no content will be deleted and everything will continue working afterwards!

    + ]]>
    - Press Next to proceed. ]]> + Press Next to + proceed. ]]> next to continue the configuration wizard]]> The Default users' password needs to be changed!]]> @@ -672,41 +789,56 @@ Affected files and folders More information on setting up permissions for Umbraco here You need to grant ASP.NET modify permissions to the following files/folders - Your permission settings are almost perfect!

    - You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
    + + Your permission settings are almost perfect!

    + You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]> +
    How to Resolve Click here to read the text version video tutorial on setting up folder permissions for Umbraco or read the text version.]]> - Your permission settings might be an issue! + + Your permission settings might be an issue!

    - You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
    - Your permission settings are not ready for Umbraco! + You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]> + + + Your permission settings are not ready for Umbraco!

    - In order to run Umbraco, you'll need to update your permission settings.]]>
    - Your permission settings are perfect!

    - You are ready to run Umbraco and install packages!]]>
    + In order to run Umbraco, you'll need to update your permission settings.]]> +
    + + Your permission settings are perfect!

    + You are ready to run Umbraco and install packages!]]> +
    Resolving folder issue Follow this link for more information on problems with ASP.NET and creating folders Setting up folder permissions - + + ]]> +
    I want to start from scratch - + learn how) You can still choose to install Runway later on. Please go to the Developer section and choose Packages. - ]]> + ]]> + You've just set up a clean Umbraco platform. What do you want to do next? Runway is installed - + This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules - ]]> + ]]> + Only recommended for experienced users I want to start with a simple website - + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, @@ -717,7 +849,8 @@ Included with Runway: Home page, Getting Started page, Installing Modules page.
    Optional Modules: Top Navigation, Sitemap, Contact, Gallery. - ]]>
    + ]]> + What is Runway Step 1/5 Accept license Step 2/5: Database configuration @@ -725,24 +858,36 @@ Step 4/5: Check Umbraco security Step 5/5: Umbraco is ready to get you started Thank you for choosing Umbraco - Browse your new site -You installed Runway, so why not see how your new website looks.]]> - Further help and information -Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + + Browse your new site +You installed Runway, so why not see how your new website looks.]]> + + + Further help and information +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + Umbraco %0% is installed and ready for use - /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - started instantly by clicking the "Launch Umbraco" button below.
    If you are new to Umbraco, -you can find plenty of resources on our getting started pages.]]>
    - Launch Umbraco -To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + + /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> + + + started instantly by clicking the "Launch Umbraco" button below.
    If you are new to Umbraco, +you can find plenty of resources on our getting started pages.]]> +
    + + Launch Umbraco +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + Connection to database failed. Umbraco Version 3 Umbraco Version 4 Watch - Umbraco %0% for a fresh install or upgrading from version 3.0. + + Umbraco %0% for a fresh install or upgrading from version 3.0.

    - Press "next" to start the wizard.]]>
    + Press "next" to start the wizard.]]> +
    Culture Code @@ -767,6 +912,8 @@ To manage your website, simply open the Umbraco back office and start adding con Forgotten password? An email will be sent to the address specified with a link to reset your password An email with password reset instructions will be sent to the specified address if it matched our records + Show password + Hide password Return to login form Please provide a new password Your Password has been updated @@ -774,41 +921,41 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco: Reset Password - - - - - - + + + + + + + - - + +
    +
    - - - + + + -
    - -
    - -
    + +
    + +
    -
    + - + - - - + + +
    +

    - - + + -
    +
    - - + +
    +

    Password reset requested

    @@ -818,12 +965,12 @@ To manage your website, simply open the Umbraco back office and start adding con

    - - + + +
    +
    Click this link to reset your password - -
    @@ -835,22 +982,22 @@ To manage your website, simply open the Umbraco back office and start adding con %1% -

    -
    -
    + - -


    -
    - - + +


    +
    + + - + ]]> @@ -877,7 +1024,8 @@ To manage your website, simply open the Umbraco back office and start adding con Edit your notification for %0% - + - - - - - - + + + + + + + - - + +
    +
    - - + + -
    - -
    - -
    + +
    + +
    -
    + - + - - - + + -
    +

    - - + - +
    +
    - - +
    +

    Hi %0%,

    -

    +

    This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%'

    - - @@ -949,7 +1097,7 @@ To manage your website, simply open the Umbraco back office and start adding con

    Update summary:

    + +
    EDIT
    %6% -
    +

    Have a nice day!

    @@ -961,10 +1109,10 @@ To manage your website, simply open the Umbraco back office and start adding con

    -


    +


    @@ -1292,6 +1440,9 @@ To manage your website, simply open the Umbraco back office and start adding con An error occurred while unlocking the user Member was exported to file An error occurred while exporting the member + User %0% was deleted + Invite user + Invitation has been re-sent to %0% Uses CSS syntax ex: h1, .redHeader, .blueTex @@ -1525,7 +1676,7 @@ To manage your website, simply open the Umbraco back office and start adding con Allow this property value to be displayed on the member profile page tab has no sort order - + Where is this composition used? This composition is currently used in the composition of the following content types: @@ -1779,41 +1930,41 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco: Invitation - - - - - - + + + + + + + - - + +
    +
    - - - + + + -
    - -
    - -
    + +
    + +
    -
    +
    + - - - + + +
    +

    - - + + -
    +
    - - + +
    +

    Hi %0%,

    @@ -1831,12 +1982,12 @@ To manage your website, simply open the Umbraco back office and start adding con
    - - + + +
    +
    Click this link to accept the invite - -
    @@ -1851,26 +2002,29 @@ To manage your website, simply open the Umbraco back office and start adding con %3% -

    -
    -
    + - -


    - - - + +


    + + + - + ]]>
    Invite + Resending invitation... + Delete User + Are you sure you wish to delete this user account? @@ -1931,6 +2085,10 @@ To manage your website, simply open the Umbraco back office and start adding con Media - Total XML: %0%, Total: %1%, Total invalid: %2% Content - Total XML: %0%, Total published: %1%, Total invalid: %2% + Database - The database schema is correct for this version of Umbraco + %0% problems were detected with your database schema (Check the log for details) + Some errors were detected while validating the database schema against the current version of Umbraco. + Your website's certificate is valid. Certificate validation error: '%0%' Your website's SSL certificate has expired. diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 6586f05480..89f9c7df00 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -46,6 +46,7 @@ Set permissions Unlock Create Content Template + Resend Invitation Content @@ -100,9 +101,6 @@
    Domains - - Viewing for - Clear selection Select @@ -128,8 +126,9 @@ Edit relations Return to list Save + Save and close - Publish + Save and publish Publish… Save and schedule Save and send for approval @@ -145,6 +144,28 @@ Undo Redo + + Viewing for + Delete Content performed by user + UnPublish performed by user + Save and Publish performed by user + Save Content performed by user + Move Content performed by user + Copy Content performed by user + Content rollback performed by user + Content Send To Publish performed by user + Content Send To Translation performed by user + Copy + Publish + Move + Save + Delete + Unpublish + Rollback + Send To Publish + Send To Translation + + To change the document type for the selected content, first select from the list of valid types for this location. Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. @@ -298,6 +319,7 @@ Discard changes You have unsaved changes Are you sure you want to navigate away from this page? - you have unsaved changes + Unpublishing will remove this page and all its descendants from the site. Done @@ -430,6 +452,7 @@ The key '%0%' already exists. ]]> + Dictionary overview Enter your username @@ -895,6 +918,8 @@ To manage your website, simply open the Umbraco back office and start adding con Forgotten password? An email will be sent to the address specified with a link to reset your password An email with password reset instructions will be sent to the specified address if it matched our records + Show password + Hide password Return to login form Please provide a new password Your Password has been updated @@ -902,41 +927,41 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco: Reset Password - - - - - - + + + + + + + - - + +
    +
    - - - + + + -
    - -
    - -
    + +
    + +
    -
    + - + - - - + + +
    +

    - - + + -
    +
    - - + +
    +

    Password reset requested

    @@ -946,12 +971,12 @@ To manage your website, simply open the Umbraco back office and start adding con

    - - + + +
    +
    Click this link to reset your password - -
    @@ -963,22 +988,22 @@ To manage your website, simply open the Umbraco back office and start adding con %1% -

    -
    -
    + - -


    - - - + +


    + + + - + ]]> @@ -1022,53 +1047,53 @@ To manage your website, simply open the Umbraco back office and start adding con
    - - - - - - + + + + + + + - - + +
    +
    - - + + -
    - -
    - -
    + +
    + +
    -
    + - + - - - + + -
    +

    - - + - +
    +
    - - +
    +

    Hi %0%,

    -

    +

    This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%'

    - - @@ -1078,7 +1103,7 @@ To manage your website, simply open the Umbraco back office and start adding con

    Update summary:

    + +
    EDIT
    %6% -
    +

    Have a nice day!

    @@ -1090,10 +1115,10 @@ To manage your website, simply open the Umbraco back office and start adding con

    -


    +


    @@ -1422,6 +1447,9 @@ To manage your website, simply open the Umbraco back office and start adding con An error occurred while unlocking the user Member was exported to file An error occurred while exporting the member + User %0% was deleted + Invite user + Invitation has been re-sent to %0% Cannot publish the document since the required '%0%' is not published Validation failed for language '%0%' Unexpected validation failed for language '%0%' @@ -1653,7 +1681,7 @@ To manage your website, simply open the Umbraco back office and start adding con Allow this property value to be displayed on the member profile page tab has no sort order - + Where is this composition used? This composition is currently used in the composition of the following content types: @@ -1916,41 +1944,41 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco: Invitation - - - - - - + + + + + + + - - + +
    +
    - - - + + + -
    - -
    - -
    + +
    + +
    -
    +
    + - - - + + +
    +

    - - + + -
    +
    - - + +
    +

    Hi %0%,

    @@ -1968,12 +1996,12 @@ To manage your website, simply open the Umbraco back office and start adding con
    - - + + +
    +
    Click this link to accept the invite - -
    @@ -1988,26 +2016,29 @@ To manage your website, simply open the Umbraco back office and start adding con %3% -

    -
    -
    + - -


    - - - + +


    + + + - + ]]>
    Invite + Resending invitation... + Delete User + Are you sure you wish to delete this user account? Validation @@ -2067,6 +2098,10 @@ To manage your website, simply open the Umbraco back office and start adding con Media - Total XML: %0%, Total: %1%, Total invalid: %2% Content - Total XML: %0%, Total published: %1%, Total invalid: %2% + Database - The database schema is correct for this version of Umbraco + %0% problems were detected with your database schema (Check the log for details) + Some errors were detected while validating the database schema against the current version of Umbraco. + Your website's certificate is valid. Certificate validation error: '%0%' Your website's SSL certificate has expired. diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml index 0e856a286c..cd2eeb2a51 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml @@ -128,38 +128,38 @@ Benvenuto - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes + Rimani + Scarta le modifiche + Hai delle modifiche non salvate + Sei sicuro di voler lasciare questa pagina? - hai delle modifiche non salvate - Done + Fatto - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items + Elimianto %0% elemento + Elimianto %0% elementi + Eliminato %0% su %1% elemento + Eliminato %0% su %1% elementi - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items + Pubblicato %0% elemento + Pubblicato %0% elementi + Pubblicato %0% su %1% elemento + Pubblicato %0% su %1% elementi - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items + %0% elemento non pubblicato + %0% elementi non pubblicati + Elementi non pubblicati - %0% su %1% + Elementi non pubblicati - %0% su %1% - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items + Spostato %0% elemento + Spsotato %0% elementi + Spostato %0% su %1% elemento + Spostato %0% su %1% elementi - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items + Copiato %0% elemento + Copiato %0% elementi + Copiato %0% su %1% elemento + Copiato %0% su %1% elementi Titolo del Link @@ -571,7 +571,7 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i usando i gruppi di membri di Umbraco.]]> - l'autenticazione basata sui ruoli.]]> + Devi creare un gruppo di membri prima di utilizzare l'autenticazione basata sui ruoli @@ -732,43 +732,43 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i Embed Headline Quote - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied + Seleziona il tipo di contenuto + Seleziona un layout + Aggiungi una riga + Aggiungi contenuto + Elimina contenuto + Impostazioni applicati - This content is not allowed here - This content is allowed here + Questo contenuto non è consentito qui + Questo contenuto è consentito qui - Click to embed - Click to insert image - Image caption... - Write here... + Clicca per incorporare + Clicca per inserire l'immagine + Didascalia dell'immagine... + Scrivi qui... - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells + I Grid Layout + I layout sono l'area globale di lavoro per il grid editor, di solito ti serve solo uno o due layout differenti + Aggiungi un Grid Layout + Sistema il layout impostando la larghezza della colonna ed aggiungendo ulteriori sezioni + Configurazioni della riga + Le righe sono le colonne predefinite disposte orizzontalmente + Aggiungi configurazione della riga + Sistema la riga impostando la larghezza della colonna ed aggiungendo ulteriori colonne - Columns - Total combined number of columns in the grid layout + Colonne + Totale combinazioni delle colonne nel grid layout - Settings - Configure what settings editors can change + Impostazioni + Configura le impostazioni che possono essere cambiate dai editori - Styles - Configure what styling editors can change + Stili + Configura i stili che possono essere cambiati dai editori - Settings will only save if the entered json configuration is valid + Le impostazioni verranno salvate soltanto se è valido il json inserito - Allow all editors - Allow all row configurations + Permetti tutti i editor + Permetti tutte le configurazioni della riga diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml index d0d90216c7..72ece3d46e 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml @@ -172,6 +172,13 @@ Doel Dit betekend de volgende tijd op de server: Wat houd dit in?]]> + Ben je er zeker van dat je dit item wilt verwijderen? + Eigenschap %0% gebruikt editor %1% welke niet wordt ondersteund door Nested Content. + Voeg nog een tekstvak toe + Verwijder dit tekstvak + Content root + Deze waarde is verborgen. Indien u toegang nodig heeft om deze waarde te bekijken, contacteer dan uw website administrator. + Deze waarde is verborgen Klik om te uploaden @@ -214,6 +221,7 @@ Negeer wijzigingen Wijzigingen niet opgeslagen Weet je zeker dat deze pagina wilt verlaten? - er zijn onopgeslagen wijzigingen + Depubliceren zal deze pagina en alle onderliggend paginas verwijderen van de site. Done @@ -1137,7 +1145,12 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je die gebruik maken van deze editor zullen geupdate worden met deze nieuwe instellingen Lid kan bewerken + Toestaan dat deze eigenschap kan worden gewijzigd door het lid op zijn profiel pagina. + Omvat gevoelige gegevens + Verberg deze eigenschap voor de content editor die geen toegang heeft tot het bekijken van gevoelige informatie. Toon in het profiel van leden + Toelaten dat deze eigenschap wordt getoond op de profiel pagina van het lid. + tab heeft geen sorteervolgorde diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml index 8f9fa63244..36a4a8fa01 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml @@ -19,6 +19,7 @@ Отключить Очистить корзину Включить + Экспорт Экспортировать Импортировать Импортировать пакет @@ -158,8 +159,9 @@ Вернуться к списку Сохранить Сохранить и построить модели - Сохранить и опубликовать - Сохранить и направить на публикацию + Опубликовать + Запланировать + Направить на публикацию Сохранить список Выбрать Выбрать текущую папку @@ -201,7 +203,20 @@ Желтый Оранжевый Синий + Серо-синий + Серый + Коричневый + Светло-синий + Голубой + Светло-зеленый + Лайм + Янтарный + Рыжий Красный + Розовый + Лиловый + Темно-лиловый + Индиго Об этой странице @@ -221,6 +236,8 @@ Скрыть ВНИМАНИЕ: невозможно получить URL документа (внутренняя ошибка - подробности в системном журнале) Опубликовано + Это значение скрыто. Если Вам нужен доступ к просмотру этого значения, свяжитесь с администратором веб-сайта. + Это значение скрыто. Этот документ был изменен после публикации Этот документ не опубликован Документ опубликован @@ -236,19 +253,24 @@ Тип участника Вы уверены, что хотите удалить этот элемент? Свойство '%0%' использует редактор '%1%', который не поддерживается для вложенного содержимого. + Не было сделано никаких изменений Дата не указана Заголовок страницы + Этот медиа-элемент не содержит ссылки Доступные группы Свойства Этот документ опубликован, но скрыт, потому что его родительский документ '%0%' не опубликован ВНИМАНИЕ: этот документ опубликован, но его нет в глобальном кэше (внутренняя ошибка - подробности в системном журнале) Опубликовать + Опубликовано + Опубликовано (есть измененения) Состояние публикации Опубликовать Очистить дату ВНИМАНИЕ: этот документ опубликован, но его URL вступает в противоречие с документом %0% Это время будет соответствовать следующему времени на сервере: Что это означает?]]> + Задать дату Порядок сортировки обновлен Для сортировки узлов просто перетаскивайте узлы или нажмите на заголовке столбца. Вы можете выбрать несколько узлов, удерживая клавиши "shift" или "ctrl" при пометке Статистика @@ -256,6 +278,7 @@ Заголовок (необязательно) Тип Скрыть + Распубликовано Распубликовать Последняя правка Дата/время редактирования документа @@ -265,6 +288,10 @@ Добавить новое поле текста Удалить это поле текста + + Выбран элемент содержимого, который в настоящее время удален или находится в корзине + Выбраны элементы содержимого, которые в настоящее время удалены или находятся в корзине + Композиции Вы не добавили ни одной вкладки @@ -289,6 +316,8 @@ Выбрать дочерний узел Унаследовать вкладки и свойства из уже существующего типа документов. Вкладки будут либо добавлены в создаваемый тип, либо в случае совпадения названий вкладок будут добавлены наследуемые свойства. Этот тип документов уже участвует в композиции другого типа, поэтому сам не может быть композицией. + Где используется эта композиция? + Эта композиция сейчас используется при создании следующих типов документов: В настоящее время нет типов документов, допустимых для построения композиции. Доступные редакторы @@ -320,7 +349,11 @@ , использующие этот редактор, будут обновлены с применением этих установок Участник может изменить + Разрешает редактирование значение данного свойства участником в своем профиле + Конфеденциальные данные + Скрывает значение это свойства от редакторов содержимого, не имеющих доступа к конфеденциальной информации Показать в профиле участника + Разрешает показ данного свойства в профиле участника для вкладки не указан порядок сортировки @@ -447,6 +480,7 @@ Ключ '%0%' уже существует в словаре. ]]> + Обзор словаря Допустим как корневой @@ -565,13 +599,16 @@ Ошибка Найти Начало + Общее Группы Папка Высота Справка Скрыть + История Иконка Импорт + Инфо Внутренний отступ Вставить Установить @@ -581,6 +618,7 @@ Язык Конец Макет + Ссылки Загрузка БЛОКИРОВКА Войти @@ -601,6 +639,7 @@ Ok Открыть Вкл + Варианты или Сортировка по Пароль @@ -620,6 +659,7 @@ Получить Повторить Разрешения + Публикация по расписанию Поиск К сожалению, ничего подходящего не нашлось Результаты поиска @@ -653,7 +693,8 @@ Сохранение... текущий выбрано - Внедрить + Встроить + Получить Цвет фона @@ -663,12 +704,12 @@ Текст - Rich Text Editor - Image - Macro - Embed - Headline - Quote + Редактор текста + Изображение + Макрос + Встраивание + Заголовок + Цитата Добавить содержимое Сбросить содержимое Добавить шаблон сетки @@ -750,9 +791,10 @@ Медиа - всего в XML: %0%, всего: %1%Б с ошибками: %2% Содержимое - всего в XML: %0%, всего опубликовано: %1%, с ошибками: %2% + Ошибка проверки адреса URL %0% - '%1%' + Сертификат Вашего веб-сайта отмечен как проверенный. Ошибка проверки сертификата: '%0%' - Ошибка проверки адреса URL %0% - '%1%' Сейчас Вы %0% просматриваете сайт, используя протокол HTTPS. Параметр 'umbracoUseSSL' в секции 'appSetting' установлен в 'false' в файле web.config. Если Вам необходим доступ к сайту по протоколу HTTPS, нужно установить данный параметр в 'true'. Параметр 'umbracoUseSSL' в секции 'appSetting' в файле установлен в '%0%', значения cookies %1% маркированы как безопасные. @@ -792,11 +834,27 @@ X-Frame-Options, использующийся для управления возможностью помещать сайт в IFRAME на другом сайте.]]> X-Frame-Options, использующийся для управления возможностью помещать сайт в IFRAME на другом сайте, не обнаружен.]]> - Установить заголовок в файле конфигурации Добавляет значение в секцию 'httpProtocol/customHeaders' файла web.config, препятствующее возможному использованию этого сайта внутри IFRAME на другом сайте. Значение, добавляющее заголовок, препятствующий использованию этого сайта внутри IFRAME другого сайта, успешно добавлено в файл web.config. + + Установить заголовок в файле конфигурации Невозможно обновить файл web.config. Ошибка: %0% + X-Content-Type-Options, использующиеся для защиты от MIME-уязвимостей, обнаружены.]]> + X-Content-Type-Options, использующиеся для защиты от MIME-уязвимостей, не найдены.]]> + Добавляет значение в секцию httpProtocol/customHeaders файла web.config, препятствующее использованию MIME-уязвимостей. + Значение, добавляющее заголовок, препятствующий использованию MIME-уязвимостей, успешно добавлено в файл web.config. + + Strict-Transport-Security, известный также как HSTS-header, обнаружен.]]> + Strict-Transport-Security не найден.]]> + Добавляет заголовок 'Strict-Transport-Security' и его значение 'max-age=10886400; preload' в секцию httpProtocol/customHeaders файла web.config. Применяйте этот способ только в случае, если доступ к Вашим сайтам будет осуществляться по протоколу https как минимум ближайшие 18 недель. + Заголовок HSTS-header успешно добавлен в файл web.config. + + X-XSS-Protection обнаружен.]]> + X-XSS-Protection не найден.]]> + Добавляет заголовок 'X-XSS-Protection' и его значение '1; mode=block' в секцию httpProtocol/customHeaders файла web.config. + Заголовок X-XSS-Protection успешно добавлен в файл web.config. + @@ -966,7 +1024,87 @@ Ссылка, по которой Вы попали сюда, неверна или устарела Umbraco: сброс пароля -

    Ваше имя пользователя для входа в панель администрирования Umbraco: %0%

    Перейдите по этой ссылке для того, чтобы сбросить Ваш пароль, или скопируйте текст ссылки и вставьте в адресную строку своего браузера:

    %1%

    ]]> + + + + + + + + + + + +
    + + + + + +
    + +
    + +
    +
    + + + + + + +
    +
    +
    + + + + +
    + + + + +
    +

    + Запрошен сброс пароля +

    +

    + Ваше имя пользователя для входа в административную панель Umbraco: %0% +

    +

    + + + + + + +
    + + Нажмите на эту ссылку для того, чтобы сбросить пароль + +
    +

    +

    Если Вы не имеете возможности нажать на сслыку, скопируйте следующий адрес (URL) и вставьте в адресную строку Вашего браузера:

    + + + + +
    + + %1% + +
    +

    +
    +
    +


    +
    +
    + + + ]]>
    @@ -984,6 +1122,11 @@ Максимально допустимый размер файла: Начальный узел медиа + + Выбран медиа-элемент, который в настоящее время удален или находится в корзине + Выбраны медиа-элементы, которые в настоящее время удалены или находятся в корзине + Удаленный элемент + Создать нового участника Все участники @@ -1023,34 +1166,88 @@ Удачи! Генератор уведомлений Umbraco. - ]]> - Здравствуйте, %0%

    - -

    Это автоматически сгенерированное уведомление. Операция '%1%' - была произведена на странице '%2%' - пользователем '%3%'.

    - - - -

    -

    Сводка обновлений:

    - - %6% -
    -

    - - - -

    Удачи!

    Генератор уведомлений Umbraco -

    ]]>
    + ]]> + + + + + + + + + + + + + +
    + + + + + +
    + +
    + +
    +
    + + + + + + +
    +
    +
    + + + + +
    + + + + +
    +

    + Здравствуйте, %0%, +

    +

    + Это автоматически сгенерированное сообщение, отправленное, чтобы уведомить Вас о том, что операция '%1%' была выполнена на странице '%2%' пользователем '%3%' +

    + + + + + + +
    + +
    + ВНЕСТИ ИЗМЕНЕНИЯ
    +
    +

    +

    Обзор обновления:

    + + %6% +
    +

    +

    + Удачного дня!

    + К Вашим услугам, почтовый робот Umbraco +

    +
    +
    +


    +
    +
    + + + ]]> +
    [%0%] Уведомление об операции %1% над документом %2% Уведомления @@ -1400,6 +1597,8 @@ При разблокировке пользователей произошла ошибка '%0%' сейчас разблокирован При разблокировке пользователя произошла ошибка + Данные участника успешно экспортированы в файл + Во время экспортирования данных участника произошла ошибка Используется синтаксис селекторов CSS, например: h1, .redHeader, .blueTex @@ -1672,9 +1871,103 @@ Неудачных попыток входа К профилю пользователя Добавьте пользователя в группу(ы) для задания прав доступа + Пригласить Приглашение в панель администрирования Umbraco

    Здравствуйте, %0%,

    Вы были приглашены пользователем %1%, и Вам предоставлен доступ в панель администрирования Umbraco.

    Сообщение от %1%: %2%

    Перейдите по этой ссылке, чтобы принять приглашение.

    Если Вы не имеете возможности перейти по ссылке, скопируйте нижеследующий текст ссылки и вставьте в адресную строку Вашего браузера.

    %3%

    ]]> + + + + + + + + + + + + +
    + + + + + +
    + +
    + +
    +
    + + + + + + +
    +
    +
    + + + + +
    + + + + +
    +

    + Здравствуйте, %0%, +

    +

    + Вы были приглашены пользователем %1% в панель администрирования веб-сайта. +

    +

    + Сообщение от пользователя %1%: +
    + %2% +

    + + + + + + +
    + + + + + + +
    + + Нажмите на эту ссылку, чтобы принять приглашение + +
    +
    +

    Если Вы не имеете возможности нажать на ссылку, скопируйте следующий адрес (URL) и вставьте в адресную строку Вашего браузера:

    + + + + +
    + + %3% + +
    +

    +
    +
    +


    +
    +
    + + + ]]>
    Пригласить еще одного пользователя Пригласить пользователя @@ -1725,7 +2018,7 @@ Имя (Я-А) Сначала новые Сначала старые - Недавно зашедшие + Недавно заходившие Активные Все Отключенные diff --git a/src/Umbraco.Web.UI/Umbraco/developer/Packages/installer.aspx b/src/Umbraco.Web.UI/Umbraco/developer/Packages/installer.aspx new file mode 100644 index 0000000000..37dca8400e --- /dev/null +++ b/src/Umbraco.Web.UI/Umbraco/developer/Packages/installer.aspx @@ -0,0 +1,10 @@ +<%@ Page Language="c#" MasterPageFile="../../masterpages/umbracoPage.Master" +AutoEventWireup="True" Inherits="umbraco.presentation.developer.packages.Installer" Trace="false" ValidateRequest="false" %> +<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> + + + + + + + diff --git a/src/Umbraco.Web.UI/config/EmbeddedMedia.Release.config b/src/Umbraco.Web.UI/config/EmbeddedMedia.Release.config index 46f366a75a..df0919b0f0 100644 --- a/src/Umbraco.Web.UI/config/EmbeddedMedia.Release.config +++ b/src/Umbraco.Web.UI/config/EmbeddedMedia.Release.config @@ -26,37 +26,45 @@ - + - + - + - + - + + + + + + json + + + - + @@ -116,4 +124,11 @@ + + + + + + + diff --git a/src/Umbraco.Web.UI/config/EmbeddedMedia.config b/src/Umbraco.Web.UI/config/EmbeddedMedia.config index cec2dd17b3..0bbb0f15a7 100644 --- a/src/Umbraco.Web.UI/config/EmbeddedMedia.config +++ b/src/Umbraco.Web.UI/config/EmbeddedMedia.config @@ -26,37 +26,45 @@ - + - + - + - + - + + + + + + json + + + - + @@ -116,4 +124,11 @@ + + + + + + + diff --git a/src/Umbraco.Web.UI/config/trees.Release.config b/src/Umbraco.Web.UI/config/trees.Release.config index 95123c0ce0..bbf382a3ed 100644 --- a/src/Umbraco.Web.UI/config/trees.Release.config +++ b/src/Umbraco.Web.UI/config/trees.Release.config @@ -1,37 +1,39 @@  - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 007604e3a0..f07d73e363 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -1,41 +1,42 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 8754ca9103..7632111b63 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -14,7 +14,7 @@ - umbracoWidth + umbracoWidth umbracoHeight umbracoBytes umbracoExtension diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 1a37c75dd6..c2cf80f296 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -373,7 +373,7 @@ namespace Umbraco.Web.Editors { if (loginInfo == null) throw new ArgumentNullException("loginInfo"); if (response == null) throw new ArgumentNullException("response"); - + ExternalSignInAutoLinkOptions autoLinkOptions = null; //Here we can check if the provider associated with the request has been configured to allow // new users (auto-linked external accounts). This would never be used with public providers such as @@ -384,8 +384,10 @@ namespace Umbraco.Web.Editors { Logger.Warn("Could not find external authentication provider registered: " + loginInfo.Login.LoginProvider); } - - var autoLinkOptions = authType.GetExternalAuthenticationOptions(); + else + { + autoLinkOptions = authType.GetExternalAuthenticationOptions(); + } // Sign in the user with this external login provider if the user already has a login var user = await UserManager.FindAsync(loginInfo.Login); diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index e550083f66..88cd4905d5 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -277,6 +277,10 @@ namespace Umbraco.Web.Editors "publishedStatusBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetPublishedStatusUrl()) }, + { + "dictionaryApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.DeleteById(int.MaxValue)) + }, { "nuCacheStatusBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetStatus()) @@ -293,6 +297,7 @@ namespace Umbraco.Web.Editors "languageApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetAllLanguages()) } + } }, { diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index ea5a0da6b5..7365c4f4f3 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -184,6 +184,62 @@ namespace Umbraco.Web.Editors ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); } + + public CreatedContentTypeCollectionResult PostCreateCollection(int parentId, string collectionName, string collectionItemName, string collectionIcon, string collectionItemIcon) + { + var storeInContainer = false; + var allowUnderDocType = -1; + // check if it's a folder + if (Services.ContentTypeService.Get(parentId) == null) + { + storeInContainer = true; + } else + { + // if it's not a container, we'll change the parentid to the root, + // and use the parent id as the doc type the collection should be allowed under + allowUnderDocType = parentId; + parentId = -1; + } + + // create item doctype + var itemDocType = new ContentType(parentId); + itemDocType.Name = collectionItemName; + itemDocType.Alias = collectionItemName.ToSafeAlias(); + itemDocType.Icon = collectionItemIcon; + Services.ContentTypeService.Save(itemDocType); + + // create collection doctype + var collectionDocType = new ContentType(parentId); + collectionDocType.Name = collectionName; + collectionDocType.Alias = collectionName.ToSafeAlias(); + collectionDocType.Icon = collectionIcon; + collectionDocType.IsContainer = true; + collectionDocType.AllowedContentTypes = new List() + { + new ContentTypeSort(itemDocType.Id, 0) + }; + Services.ContentTypeService.Save(collectionDocType); + + // test if the parent id exist and then allow the collection underneath + if (storeInContainer == false && allowUnderDocType != -1) + { + var parentCt = Services.ContentTypeService.Get(allowUnderDocType); + if (parentCt != null) + { + var allowedCts = parentCt.AllowedContentTypes.ToList(); + allowedCts.Add(new ContentTypeSort(collectionDocType.Id, allowedCts.Count())); + parentCt.AllowedContentTypes = allowedCts; + Services.ContentTypeService.Save(parentCt); + } + } + + + return new CreatedContentTypeCollectionResult + { + CollectionId = collectionDocType.Id, + ContainerId = itemDocType.Id + }; + } public DocumentTypeDisplay PostSave(DocumentTypeSave contentTypeSave) { diff --git a/src/Umbraco.Web/Editors/DictionaryController.cs b/src/Umbraco.Web/Editors/DictionaryController.cs new file mode 100644 index 0000000000..faaad9407c --- /dev/null +++ b/src/Umbraco.Web/Editors/DictionaryController.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using AutoMapper; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using Umbraco.Web.UI; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; +using Notification = Umbraco.Web.Models.ContentEditing.Notification; + +namespace Umbraco.Web.Editors +{ + /// + /// + /// The API controller used for editing dictionary items + /// + /// + /// The security for this controller is defined to allow full CRUD access to dictionary if the user has access to either: + /// Dictionar + /// + [PluginController("UmbracoApi")] + [UmbracoTreeAuthorize(Constants.Trees.Dictionary)] + [EnableOverrideAuthorization] + public class DictionaryController : BackOfficeNotificationsController + { + /// + /// Deletes a data type wth a given ID + /// + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundDictionary = Services.LocalizationService.GetDictionaryItemById(id); + + if (foundDictionary == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + Services.LocalizationService.Delete(foundDictionary, Security.CurrentUser.Id); + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Creates a new dictoinairy item + /// + /// + /// The parent id. + /// + /// + /// The key. + /// + /// + /// The . + /// + [HttpPost] + public HttpResponseMessage Create(int parentId, string key) + { + if (string.IsNullOrEmpty(key)) + return Request + .CreateNotificationValidationErrorResponse("Key can not be empty;"); // TODO translate + + if (Services.LocalizationService.DictionaryItemExists(key)) + { + var message = Services.TextService.Localize( + "dictionaryItem/changeKeyError", + Security.CurrentUser.GetUserCulture(Services.TextService, GlobalSettings), + new Dictionary { { "0", key } }); + return Request.CreateNotificationValidationErrorResponse(message); + } + + try + { + Guid? parentGuid = null; + + if (parentId > 0) + parentGuid = Services.LocalizationService.GetDictionaryItemById(parentId).Key; + + var item = Services.LocalizationService.CreateDictionaryItemWithIdentity( + key, + parentGuid, + string.Empty); + + + return Request.CreateResponse(HttpStatusCode.OK, item.Id); + } + catch (Exception exception) + { + Logger.Error(GetType(), "Error creating dictionary", exception); + return Request.CreateNotificationValidationErrorResponse("Error creating dictionary item"); + } + } + + /// + /// Gets a dictionary item by id + /// + /// + /// The id. + /// + /// + /// The . + /// + /// + /// Returrns a not found response when dictionary item does not exist + /// + public DictionaryDisplay GetById(int id) + { + var dictionary = Services.LocalizationService.GetDictionaryItemById(id); + + if (dictionary == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + return Mapper.Map(dictionary); + } + + /// + /// Saves a dictionary item + /// + /// + /// The dictionary. + /// + /// + /// The . + /// + public DictionaryDisplay PostSave(DictionarySave dictionary) + { + var dictionaryItem = + Services.LocalizationService.GetDictionaryItemById(int.Parse(dictionary.Id.ToString())); + + if (dictionaryItem == null) + throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Dictionary item does not exist")); + + var userCulture = Security.CurrentUser.GetUserCulture(Services.TextService, GlobalSettings); + + if (dictionary.NameIsDirty) + { + // if the name (key) has changed, we need to check if the new key does not exist + var dictionaryByKey = Services.LocalizationService.GetDictionaryItemByKey(dictionary.Name); + + if (dictionaryByKey != null && dictionaryItem.Id != dictionaryByKey.Id) + { + + var message = Services.TextService.Localize( + "dictionaryItem/changeKeyError", + userCulture, + new Dictionary { { "0", dictionary.Name } }); + ModelState.AddModelError("Name", message); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + + dictionaryItem.ItemKey = dictionary.Name; + } + + foreach (var translation in dictionary.Translations) + { + Services.LocalizationService.AddOrUpdateDictionaryValue(dictionaryItem, + Services.LocalizationService.GetLanguageById(translation.LanguageId), translation.Translation); + } + + try + { + Services.LocalizationService.Save(dictionaryItem); + + var model = Mapper.Map(dictionaryItem); + + model.Notifications.Add(new Notification( + Services.TextService.Localize("speechBubbles/dictionaryItemSaved", userCulture), string.Empty, + SpeechBubbleIcon.Success)); + + return model; + } + catch (Exception e) + { + Logger.Error(GetType(), "Error saving dictionary", e); + throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Something went wrong saving dictionary")); + } + } + + /// + /// Retrieves a list with all dictionary items + /// + /// + /// The . + /// + public IEnumerable GetList() + { + var list = new List(); + + const int level = 0; + + foreach (var dictionaryItem in Services.LocalizationService.GetRootDictionaryItems()) + { + var item = Mapper.Map(dictionaryItem); + item.Level = 0; + list.Add(item); + + GetChildItemsForList(dictionaryItem, level + 1, list); + } + + return list; + } + + /// + /// Get child items for list. + /// + /// + /// The dictionary item. + /// + /// + /// The level. + /// + /// + /// The list. + /// + private void GetChildItemsForList(IDictionaryItem dictionaryItem, int level, List list) + { + foreach (var childItem in Services.LocalizationService.GetDictionaryItemChildren( + dictionaryItem.Key)) + { + var item = Mapper.Map(childItem); + item.Level = level; + list.Add(item); + + GetChildItemsForList(childItem, level + 1, list); + } + } + } +} diff --git a/src/Umbraco.Web/Editors/MacroController.cs b/src/Umbraco.Web/Editors/MacroController.cs index 88b78e6a81..d625e3a575 100644 --- a/src/Umbraco.Web/Editors/MacroController.cs +++ b/src/Umbraco.Web/Editors/MacroController.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Text; +using System.Threading; using System.Web.Http; using System.Web.SessionState; using AutoMapper; @@ -121,7 +123,18 @@ namespace Umbraco.Web.Editors //the 'easiest' way might be to create an IPublishedContent manually and populate the legacy 'page' object with that //and then set the legacy parameters. + // When rendering the macro in the backoffice the default setting would be to use the Culture of the logged in user. + // Since a Macro might contain thing thats related to the culture of the "IPublishedContent" (ie Dictionary keys) we want + // to set the current culture to the culture related to the content item. This is hacky but it works. + var publishedContent = UmbracoContext.ContentCache.GetById(doc.Id); + var culture = publishedContent?.GetCulture(); + if (culture != null) + { + Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture.Culture); + } + var legacyPage = new global::umbraco.page(doc, _variationContextAccessor); + UmbracoContext.HttpContext.Items["pageID"] = doc.Id; UmbracoContext.HttpContext.Items["pageElements"] = legacyPage.Elements; UmbracoContext.HttpContext.Items[global::Umbraco.Core.Constants.Conventions.Url.AltTemplate] = null; diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index a230a6af75..513f69f778 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -124,13 +124,16 @@ namespace Umbraco.Web.Editors /// public MemberListDisplay GetListNodeDisplay(string listName) { + var foundType = Services.MemberTypeService.Get(listName); + var name = foundType != null ? foundType.Name : listName; + var display = new MemberListDisplay { ContentTypeAlias = listName, - ContentTypeName = listName, + ContentTypeName = name, Id = listName, IsContainer = true, - Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : listName, + Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : name, Path = "-1," + listName, ParentId = -1 }; diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index cabf4364b1..73a4d9c910 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -387,6 +387,8 @@ namespace Umbraco.Web.Editors await SendUserInviteEmailAsync(display, Security.CurrentUser.Name, Security.CurrentUser.Email, user, userSave.Message); + display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/resendInviteHeader"), Services.TextService.Localize("speechBubbles/resendInviteSuccess", new[] { user.Name })); + return display; } @@ -675,6 +677,36 @@ namespace Umbraco.Web.Editors Services.TextService.Localize("speechBubbles/setUserGroupOnUsersSuccess")); } + /// + /// Deletes the non-logged in user provided id + /// + /// User Id + /// + /// Limited to users that haven't logged in to avoid issues with related records constrained + /// with a foreign key on the user Id + /// + public async Task PostDeleteNonLoggedInUser(int id) + { + var user = Services.UserService.GetUserById(id); + if (user == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + // Check user hasn't logged in. If they have they may have made content changes which will mean + // the Id is associated with audit trails, versions etc. and can't be removed. + if (user.LastLoginDate != default(DateTime)) + { + throw new HttpResponseException(HttpStatusCode.BadRequest); + } + + var userName = user.Name; + Services.UserService.Delete(user, true); + + return Request.CreateNotificationSuccessResponse( + Services.TextService.Localize("speechBubbles/deleteUserSuccess", new[] { userName })); + } + public class PagedUserResult : PagedResult { public PagedUserResult(long totalItems, long pageNumber, long pageSize) : base(totalItems, pageNumber, pageSize) diff --git a/src/Umbraco.Web/HealthCheck/Checks/DataIntegrity/DatabaseSchemaValidationHealthCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/DataIntegrity/DatabaseSchemaValidationHealthCheck.cs new file mode 100644 index 0000000000..6ea562b2c7 --- /dev/null +++ b/src/Umbraco.Web/HealthCheck/Checks/DataIntegrity/DatabaseSchemaValidationHealthCheck.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Services; + +namespace Umbraco.Web.HealthCheck.Checks.DataIntegrity +{ + /// + /// U4-9544 Health check to detect if the database has any missing indexes or constraints + /// + [HealthCheck( + "0873D589-2064-4EA3-A152-C43417FE00A4", + "Database Schema Validation", + Description = "This checks the Umbraco database by doing a comparison of current indexes and schema items with the current state of the database and returns any problems it found. Useful to detect if the database hasn't been upgraded correctly.", + Group = "Data Integrity")] + public class DatabaseSchemaValidationHealthCheck : HealthCheck + { + private readonly DatabaseContext _databaseContext; + private readonly ILocalizedTextService _textService; + + public DatabaseSchemaValidationHealthCheck(HealthCheckContext healthCheckContext) : base(healthCheckContext) + { + _databaseContext = HealthCheckContext.ApplicationContext.DatabaseContext; + _textService = healthCheckContext.ApplicationContext.Services.TextService; + } + + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + { + return CheckDatabase(); + } + + public override IEnumerable GetStatus() + { + //return the statuses + return new[] { CheckDatabase() }; + } + + private HealthCheckStatus CheckDatabase() + { + var results = _databaseContext.ValidateDatabaseSchema(); + + LogHelper.Warn(typeof(DatabaseSchemaValidationHealthCheck), _textService.Localize("databaseSchemaValidationCheckDatabaseLogMessage")); + foreach(var error in results.Errors) + { + LogHelper.Warn(typeof(DatabaseSchemaValidationHealthCheck), error.Item1 + ": " + error.Item2); + } + + if(results.Errors.Count > 0) + return new HealthCheckStatus(_textService.Localize("healthcheck/databaseSchemaValidationCheckDatabaseErrors", new[] { results.Errors.Count.ToString() })) + { + ResultType = StatusResultType.Error, + View = "Umbraco.Dashboard.DatabaseSchemaValidationController" + }; + + return new HealthCheckStatus(_textService.Localize("healthcheck/databaseSchemaValidationCheckDatabaseOk")) + { + ResultType = StatusResultType.Success + }; + } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs b/src/Umbraco.Web/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs new file mode 100644 index 0000000000..cbf23743ba --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// The result of creating a content type collection in the UI + /// + [DataContract(Name = "contentTypeCollection", Namespace = "")] + public class CreatedContentTypeCollectionResult + { + [DataMember(Name = "collectionId")] + public int CollectionId { get; set; } + + [DataMember(Name = "containerId")] + public int ContainerId { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionaryDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DictionaryDisplay.cs new file mode 100644 index 0000000000..bb93c72ac7 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionaryDisplay.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// The dictionary display model + /// + [DataContract(Name = "dictionary", Namespace = "")] + public class DictionaryDisplay : EntityBasic, INotificationModel + { + /// + /// Initializes a new instance of the class. + /// + public DictionaryDisplay() + { + this.Notifications = new List(); + this.Translations = new List(); + } + + /// + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + + /// + /// Gets or sets the parent id. + /// + [DataMember(Name = "parentId")] + public new Guid ParentId { get; set; } + + /// + /// Gets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; private set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewDisplay.cs new file mode 100644 index 0000000000..7941b0ac44 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewDisplay.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// The dictionary overview display. + /// + [DataContract(Name = "dictionary", Namespace = "")] + public class DictionaryOverviewDisplay + { + /// + /// Initializes a new instance of the class. + /// + public DictionaryOverviewDisplay() + { + Translations = new List(); + } + + /// + /// Gets or sets the key. + /// + [DataMember(Name = "name")] + public string Name { get; set; } + + /// + /// Gets or sets the id. + /// + [DataMember(Name = "id")] + public int Id { get; set; } + + /// + /// Gets or sets the level. + /// + [DataMember(Name = "level")] + public int Level { get; set; } + + /// + /// Gets or sets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs new file mode 100644 index 0000000000..9f08617921 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs @@ -0,0 +1,23 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// The dictionary translation overview display. + /// + [DataContract(Name = "dictionaryTranslation", Namespace = "")] + public class DictionaryOverviewTranslationDisplay + { + /// + /// Gets or sets the display name. + /// + [DataMember(Name = "displayName")] + public string DisplayName { get; set; } + + /// + /// Gets or sets a value indicating whether has translation. + /// + [DataMember(Name = "hasTranslation")] + public bool HasTranslation { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionarySave.cs b/src/Umbraco.Web/Models/ContentEditing/DictionarySave.cs new file mode 100644 index 0000000000..e54d1fab45 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionarySave.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Dictionary Save model + /// + [DataContract(Name = "dictionary", Namespace = "")] + public class DictionarySave : EntityBasic + { + /// + /// Initializes a new instance of the class. + /// + public DictionarySave() + { + this.Translations = new List(); + } + + /// + /// Gets or sets a value indicating whether name is dirty. + /// + [DataMember(Name = "nameIsDirty")] + public bool NameIsDirty { get; set; } + + /// + /// Gets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; private set; } + + /// + /// Gets or sets the parent id. + /// + [DataMember(Name = "parentId")] + public new Guid ParentId { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationDisplay.cs new file mode 100644 index 0000000000..2437de6ffd --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationDisplay.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// + /// The dictionary translation display model + /// + [DataContract(Name = "dictionaryTranslation", Namespace = "")] + public class DictionaryTranslationDisplay : DictionaryTranslationSave + { + /// + /// Gets or sets the display name. + /// + [DataMember(Name = "displayName")] + public string DisplayName { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationSave.cs b/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationSave.cs new file mode 100644 index 0000000000..a0ab02768c --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationSave.cs @@ -0,0 +1,29 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// The dictionary translation save model + /// + [DataContract(Name = "dictionaryTranslation", Namespace = "")] + public class DictionaryTranslationSave + { + /// + /// Gets or sets the iso code. + /// + [DataMember(Name = "isoCode")] + public string IsoCode { get; set; } + + /// + /// Gets or sets the translation. + /// + [DataMember(Name = "translation")] + public string Translation { get; set; } + + /// + /// Gets or sets the language id. + /// + [DataMember(Name = "languageId")] + public int LanguageId { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/DictionaryModelMapper.cs b/src/Umbraco.Web/Models/Mapping/DictionaryModelMapper.cs new file mode 100644 index 0000000000..4fe05d9039 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/DictionaryModelMapper.cs @@ -0,0 +1,130 @@ +using AutoMapper; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Mapping; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// + /// The dictionary model mapper. + /// + internal class DictionaryModelMapper : MapperConfiguration + { + public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) + { + var lazyDictionaryService = new Lazy(() => applicationContext.Services.LocalizationService); + + config.CreateMap() + .ForMember(x => x.Translations, expression => expression.Ignore()) + .ForMember(x => x.Notifications, expression => expression.Ignore()) + .ForMember(x => x.Icon, expression => expression.Ignore()) + .ForMember(x => x.Trashed, expression => expression.Ignore()) + .ForMember(x => x.Alias, expression => expression.Ignore()) + .ForMember(x => x.Path, expression => expression.Ignore()) + .ForMember(x => x.AdditionalData, expression => expression.Ignore()) + .ForMember( + x => x.Udi, + expression => expression.MapFrom( + content => Udi.Create(Constants.UdiEntityType.DictionaryItem, content.Key))).ForMember( + x => x.Name, + expression => expression.MapFrom(content => content.ItemKey)) + .AfterMap( + (src, dest) => + { + // build up the path to make it possible to set active item in tree + // TODO check if there is a better way + if (src.ParentId.HasValue) + { + var ids = new List { -1 }; + + + var parentIds = new List(); + + this.GetParentId(src.ParentId.Value, lazyDictionaryService.Value, parentIds); + + parentIds.Reverse(); + + ids.AddRange(parentIds); + + ids.Add(src.Id); + + dest.Path = string.Join(",", ids); + } + else + { + dest.Path = "-1," + src.Id; + } + + // add all languages and the translations + foreach (var lang in lazyDictionaryService.Value.GetAllLanguages()) + { + var langId = lang.Id; + var translation = src.Translations.FirstOrDefault(x => x.LanguageId == langId); + + dest.Translations.Add(new DictionaryTranslationDisplay + { + IsoCode = lang.IsoCode, + DisplayName = lang.CultureInfo.DisplayName, + Translation = (translation != null) ? translation.Value : string.Empty, + LanguageId = lang.Id + }); + } + }); + + config.CreateMap() + .ForMember(dest => dest.Level, expression => expression.Ignore()) + .ForMember(dest => dest.Translations, expression => expression.Ignore()) + .ForMember( + x => x.Name, + expression => expression.MapFrom(content => content.ItemKey)) + .AfterMap( + (src, dest) => + { + // add all languages and the translations + foreach (var lang in lazyDictionaryService.Value.GetAllLanguages()) + { + var langId = lang.Id; + var translation = src.Translations.FirstOrDefault(x => x.LanguageId == langId); + + dest.Translations.Add( + new DictionaryOverviewTranslationDisplay + { + DisplayName = lang.CultureInfo.DisplayName, + HasTranslation = translation != null && string.IsNullOrEmpty(translation.Value) == false + }); + } + }); + } + + /// + /// Goes up the dictoinary tree to get all parent ids + /// + /// + /// The parent id. + /// + /// + /// The localization service. + /// + /// + /// The ids. + /// + private void GetParentId(Guid parentId, ILocalizationService localizationService, List ids) + { + var dictionary = localizationService.GetDictionaryItemById(parentId); + + if (dictionary == null) + return; + + ids.Add(dictionary.Id); + + if (dictionary.ParentId.HasValue) + GetParentId(dictionary.ParentId.Value, localizationService, ids); + } + } +} diff --git a/src/Umbraco.Web/Models/RegisterModel.cs b/src/Umbraco.Web/Models/RegisterModel.cs index bcf88d2a11..47c34eef1f 100644 --- a/src/Umbraco.Web/Models/RegisterModel.cs +++ b/src/Umbraco.Web/Models/RegisterModel.cs @@ -43,7 +43,7 @@ namespace Umbraco.Web.Models { } [Required] - [RegularExpression(@"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", + [RegularExpression(@"[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?", ErrorMessage = "Please enter a valid e-mail address")] public string Email { get; set; } diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index 1d55072bd7..145a0f5947 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Trees /// This authorizes based on access to the content section even though it exists in the settings /// [UmbracoApplicationAuthorize(Constants.Applications.Content)] - [Tree(Constants.Applications.Settings, Constants.Trees.ContentBlueprints, null, sortOrder: 8)] + [Tree(Constants.Applications.Settings, Constants.Trees.ContentBlueprints, null, sortOrder: 10)] [PluginController("UmbracoTrees")] [CoreTree] public class ContentBlueprintTreeController : TreeController diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 6a16cf67ca..da5ba8fd74 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -55,12 +55,14 @@ namespace Umbraco.Web.Trees //Special check to see if it ia a container, if so then we'll hide children. var isContainer = entity.IsContainer; // && (queryStrings.Get("isDialog") != "true"); + var hasChildren = ShouldRenderChildrenOfContainer(entity); + var node = CreateTreeNode( entity, Constants.ObjectTypes.Document, parentId, queryStrings, - entity.HasChildren && !isContainer); + hasChildren); // entity is either a container, or a document if (isContainer) @@ -156,16 +158,13 @@ namespace Umbraco.Web.Trees return menu; } - var nodeMenu = GetAllNodeMenuItems(item); - var allowedMenuItems = GetAllowedUserMenuItemsForNode(item); - - FilterUserAllowedMenuItems(nodeMenu, allowedMenuItems); - - //if the media item is in the recycle bin, don't have a default menu, just show the regular menu + var nodeMenu = GetAllNodeMenuItems(item); + + //if the content node is in the recycle bin, don't have a default menu, just show the regular menu if (item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) { nodeMenu.DefaultMenuAlias = null; - nodeMenu.Items.Insert(2, new MenuItem(ActionRestore.Instance, Services.TextService.Localize("actions", ActionRestore.Instance.Alias))); + nodeMenu = GetNodeMenuItemsForDeletedContent(item); } else { @@ -173,6 +172,8 @@ namespace Umbraco.Web.Trees nodeMenu.DefaultMenuAlias = ActionNew.Instance.Alias; } + var allowedMenuItems = GetAllowedUserMenuItemsForNode(item); + FilterUserAllowedMenuItems(nodeMenu, allowedMenuItems); return nodeMenu; } @@ -244,6 +245,23 @@ namespace Umbraco.Web.Trees return menu; } + /// + /// Returns a collection of all menu items that can be on a deleted (in recycle bin) content node + /// + /// + /// + protected MenuItemCollection GetNodeMenuItemsForDeletedContent(IUmbracoEntity item) + { + var menu = new MenuItemCollection(); + menu.Items.Add(Services.TextService.Localize("actions", ActionRestore.Instance.Alias)); + menu.Items.Add(Services.TextService.Localize("actions", ActionDelete.Instance.Alias)); + + menu.Items.Add(Services.TextService.Localize("actions", ActionRefresh.Instance.Alias), true); + + return menu; + } + + /// /// set name according to variations /// diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index e530ce3aed..4d4f1be483 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -280,6 +280,37 @@ namespace Umbraco.Web.Trees return GetTreeNodesInternal(id, queryStrings); } + /// + /// Check to see if we should return children of a container node + /// + /// + /// + /// + /// This is required in case a user has custom start nodes that are children of a list view since in that case we'll need to render the tree node. In normal cases we don't render + /// children of a list view. + /// + protected bool ShouldRenderChildrenOfContainer(IEntitySlim e) + { + var isContainer = e.IsContainer; + + var renderChildren = e.HasChildren && (isContainer == false); + + //Here we need to figure out if the node is a container and if so check if the user has a custom start node, then check if that start node is a child + // of this container node. If that is true, the HasChildren must be true so that the tree node still renders even though this current node is a container/list view. + if (isContainer && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + { + var startNodes = Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes); + //if any of these start nodes' parent is current, then we need to render children normally so we need to switch some logic and tell + // the UI that this node does have children and that it isn't a container + if (startNodes.Any(x => x.ParentId == e.Id)) + { + renderChildren = true; + } + } + + return renderChildren; + } + /// /// Before we make a call to get the tree nodes we have to check if they can actually be rendered /// @@ -296,7 +327,7 @@ namespace Umbraco.Web.Trees //before we get the children we need to see if this is a container node //test if the parent is a listview / container - if (current != null && current.IsContainer) + if (current != null && ShouldRenderChildrenOfContainer(current) == false) { //no children! return new TreeNodeCollection(); diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index a4cfe33e5b..99b94b544c 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -17,7 +17,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.DataTypes)] - [Tree(Constants.Applications.Settings, Constants.Trees.DataTypes, null, sortOrder:1)] + [Tree(Constants.Applications.Settings, Constants.Trees.DataTypes, null, sortOrder:7)] [PluginController("UmbracoTrees")] [CoreTree] public class DataTypeTreeController : TreeController, ISearchableTree diff --git a/src/Umbraco.Web/Trees/DictionaryBaseController.cs b/src/Umbraco.Web/Trees/DictionaryBaseController.cs new file mode 100644 index 0000000000..2046bdbf05 --- /dev/null +++ b/src/Umbraco.Web/Trees/DictionaryBaseController.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using System.Net.Http.Formatting; +using umbraco.BusinessLogic.Actions; +using Umbraco.Core; +using Umbraco.Core.Services; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.Web.Trees +{ + [UmbracoTreeAuthorize(Constants.Trees.Dictionary)] + [Mvc.PluginController("UmbracoTrees")] + [CoreTree] + [Tree(Constants.Applications.Settings, Constants.Trees.Dictionary, null, sortOrder: 3)] + public class DictionaryTreeController : TreeController + { + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) + { + var root = base.CreateRootNode(queryStrings); + + // the default section is settings, falling back to this if we can't + // figure out where we are from the querystring parameters + var section = Constants.Applications.Settings; + if (queryStrings["application"] != null) + section = queryStrings["application"]; + + // this will load in a custom UI instead of the dashboard for the root node + root.RoutePath = $"{section}/{Constants.Trees.Dictionary}/list"; + + return root; + } + + /// + /// The method called to render the contents of the tree structure + /// + /// The id of the tree item + /// + /// All of the query string parameters passed from jsTree + /// + /// + /// We are allowing an arbitrary number of query strings to be pased in so that developers are able to persist custom data from the front-end + /// to the back end to be used in the query for model data. + /// + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + { + var intId = id.TryConvertTo(); + if (intId == false) + throw new InvalidOperationException("Id must be an integer"); + + var nodes = new TreeNodeCollection(); + + if (id == Constants.System.Root.ToInvariantString()) + { + nodes.AddRange( + Services.LocalizationService.GetRootDictionaryItems().Select( + x => CreateTreeNode( + x.Id.ToInvariantString(), + id, + queryStrings, + x.ItemKey, + "icon-book-alt", + Services.LocalizationService.GetDictionaryItemChildren(x.Key).Any()))); + } + else + { + // maybe we should use the guid as url param to avoid the extra call for getting dictionary item + var parentDictionary = Services.LocalizationService.GetDictionaryItemById(intId.Result); + if (parentDictionary == null) + return nodes; + + nodes.AddRange(Services.LocalizationService.GetDictionaryItemChildren(parentDictionary.Key).ToList().OrderByDescending(item => item.Key).Select( + x => CreateTreeNode( + x.Id.ToInvariantString(), + id, + queryStrings, + x.ItemKey, + "icon-book-alt", + Services.LocalizationService.GetDictionaryItemChildren(x.Key).Any()))); + } + + return nodes; + } + + /// + /// Returns the menu structure for the node + /// + /// The id of the tree item + /// + /// All of the query string parameters passed from jsTree + /// + /// + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + menu.Items.Add(Services.TextService.Localize($"actions/{ActionNew.Instance.Alias}")); + + if (id != Constants.System.Root.ToInvariantString()) + menu.Items.Add(Services.TextService.Localize( + $"actions/{ActionDelete.Instance.Alias}"), true); + + menu.Items.Add(Services.TextService.Localize( + $"actions/{ActionRefresh.Instance.Alias}"), true); + + return menu; + } + } +} diff --git a/src/Umbraco.Web/Trees/DictionaryTreeController.cs b/src/Umbraco.Web/Trees/DictionaryTreeController.cs index a70eba29e2..aff6495e05 100644 --- a/src/Umbraco.Web/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web/Trees/DictionaryTreeController.cs @@ -13,7 +13,7 @@ using Umbraco.Web._Legacy.Actions; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.Dictionary)] - [Tree(Constants.Applications.Settings, Constants.Trees.Dictionary, null, sortOrder: 3)] + [Tree(Constants.Applications.Settings, Constants.Trees.Dictionary, null, sortOrder: 8)] [Mvc.PluginController("UmbracoTrees")] [CoreTree] public class DictionaryTreeController : TreeController diff --git a/src/Umbraco.Web/Trees/LanguageTreeController.cs b/src/Umbraco.Web/Trees/LanguageTreeController.cs index 9a9f32128c..eadb5c50d0 100644 --- a/src/Umbraco.Web/Trees/LanguageTreeController.cs +++ b/src/Umbraco.Web/Trees/LanguageTreeController.cs @@ -7,7 +7,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.Languages)] - [Tree(Constants.Applications.Settings, Constants.Trees.Languages, null, sortOrder: 4)] + [Tree(Constants.Applications.Settings, Constants.Trees.Languages, null, sortOrder: 5)] [PluginController("UmbracoTrees")] [CoreTree] public class LanguageTreeController : TreeController diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index e853ff7c7d..086c1a5194 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -16,7 +16,7 @@ using Umbraco.Web.Search; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.MediaTypes)] - [Tree(Constants.Applications.Settings, Constants.Trees.MediaTypes, null, sortOrder:8)] + [Tree(Constants.Applications.Settings, Constants.Trees.MediaTypes, null, sortOrder:9)] [Mvc.PluginController("UmbracoTrees")] [CoreTree] public class MediaTypeTreeController : TreeController, ISearchableTree diff --git a/src/Umbraco.Web/Trees/ScriptsTreeController.cs b/src/Umbraco.Web/Trees/ScriptsTreeController.cs index 47d7aa6b8f..57a50cde5d 100644 --- a/src/Umbraco.Web/Trees/ScriptsTreeController.cs +++ b/src/Umbraco.Web/Trees/ScriptsTreeController.cs @@ -6,7 +6,7 @@ using Umbraco.Web.Models.Trees; namespace Umbraco.Web.Trees { - [Tree(Constants.Applications.Settings, Constants.Trees.Scripts, "Scripts", "icon-folder", "icon-folder", sortOrder: 2)] + [Tree(Constants.Applications.Settings, Constants.Trees.Scripts, "Scripts", "icon-folder", "icon-folder", sortOrder: 4)] public class ScriptsTreeController : FileSystemTreeController { protected override IFileSystem FileSystem => Current.FileSystems.ScriptsFileSystem; // fixme inject diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index f4debe5ccf..ebf2f74e07 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -233,6 +233,7 @@ namespace Umbraco.Web.Trees { var contentTypeIcon = entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, contentTypeIcon); + treeNode.Path = entity.Path; treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key); treeNode.HasChildren = hasChildren; return treeNode; @@ -252,6 +253,7 @@ namespace Umbraco.Web.Trees { var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, icon); treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key); + treeNode.Path = entity.Path; treeNode.HasChildren = hasChildren; return treeNode; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a57222dd4f..01e92981da 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -117,6 +117,7 @@ + @@ -206,6 +207,13 @@ + + + + + + + @@ -427,6 +435,9 @@ + + ASPXCodeBehind + ASPXCodeBehind diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index ca76924b23..e83d5adaea 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -1,24 +1,22 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.IO; +using System.Linq; using System.Web; -using System.Web.Security; +using System.Web.Mvc; using System.Xml.XPath; using Umbraco.Core; using Umbraco.Core.Dictionary; -using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Xml; using Umbraco.Web.Routing; using Umbraco.Web.Security; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Web.Mvc; -using Umbraco.Core.Cache; using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Composing; +using Umbraco.Core.Cache; namespace Umbraco.Web { @@ -180,29 +178,44 @@ namespace Umbraco.Web ?? (_componentRenderer = new UmbracoComponentRenderer(UmbracoContext)); /// - /// Returns the current IPublishedContent item assigned to the UmbracoHelper + /// Returns the current item + /// assigned to the UmbracoHelper. /// /// - /// Note that this is the assigned IPublishedContent item to the UmbracoHelper, this is not necessarily the Current IPublishedContent item - /// being rendered. This IPublishedContent object is contextual to the current UmbracoHelper instance. - /// - /// In some cases accessing this property will throw an exception if there is not IPublishedContent assigned to the Helper - /// this will only ever happen if the Helper is constructed with an UmbracoContext and it is not a front-end request + /// + /// Note that this is the assigned IPublishedContent item to the + /// UmbracoHelper, this is not necessarily the Current IPublishedContent + /// item being rendered. This IPublishedContent object is contextual to + /// the current UmbracoHelper instance. + /// + /// + /// In some cases accessing this property will throw an exception if + /// there is not IPublishedContent assigned to the Helper this will + /// only ever happen if the Helper is constructed with an UmbracoContext + /// and it is not a front-end request. + /// /// - /// Thrown if the UmbracoHelper is constructed with an UmbracoContext and it is not a front-end request + /// Thrown if the + /// UmbracoHelper is constructed with an UmbracoContext and it is not a + /// front-end request. public IPublishedContent AssignedContentItem { get { - if (_currentPage == null) - throw new InvalidOperationException("Cannot return the " + typeof(IPublishedContent).Name + " because the " + typeof(UmbracoHelper).Name + " was constructed with an " + typeof(UmbracoContext).Name + " and the current request is not a front-end request."); + if (_currentPage != null) + { + return _currentPage; + } + + throw new InvalidOperationException( + $"Cannot return the {nameof(IPublishedContent)} because the {nameof(UmbracoHelper)} was constructed with an {nameof(UmbracoContext)} and the current request is not a front-end request." + ); - return _currentPage; } } /// - /// Renders the template for the specified pageId and an optional altTemplateId. + /// Renders the template for the specified pageId and an optional altTemplateId /// /// /// If not specified, will use the template assigned to the node @@ -601,37 +614,40 @@ namespace Umbraco.Web return ContentQuery.ContentAtRoot(); } - private static bool ConvertIdObjectToInt(object id, out int intId) + /// Had to change to internal for testing. + internal static bool ConvertIdObjectToInt(object id, out int intId) { - var s = id as string; - if (s != null) + switch (id) { - return int.TryParse(s, out intId); - } + case string s: + return int.TryParse(s, out intId); - if (id is int) - { - intId = (int) id; - return true; + case int i: + intId = i; + return true; + + default: + intId = default; + return false; } - intId = default(int); - return false; } - private static bool ConvertIdObjectToGuid(object id, out Guid guidId) + /// Had to change to internal for testing. + internal static bool ConvertIdObjectToGuid(object id, out Guid guidId) { - var s = id as string; - if (s != null) + switch (id) { - return Guid.TryParse(s, out guidId); + case string s: + return Guid.TryParse(s, out guidId); + + case Guid g: + guidId = g; + return true; + + default: + guidId = default; + return false; } - if (id is Guid) - { - guidId = (Guid) id; - return true; - } - guidId = default(Guid); - return false; } private static bool ConvertIdsObjectToInts(IEnumerable ids, out IEnumerable intIds) @@ -665,19 +681,25 @@ namespace Umbraco.Web return true; } - private static bool ConvertIdObjectToUdi(object id, out Udi guidId) + /// Had to change to internal for testing. + internal static bool ConvertIdObjectToUdi(object id, out Udi guidId) { - if (id is string s) - return Udi.TryParse(s, out guidId); - if (id is Udi) + switch (id) { - guidId = (Udi) id; - return true; + case string s: + return Udi.TryParse(s, out guidId); + + case Udi u: + guidId = u; + return true; + + default: + guidId = default; + return false; } - guidId = null; - return false; } + #endregion #region Media diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs new file mode 100644 index 0000000000..2d1f36ce40 --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs @@ -0,0 +1,100 @@ +using System; +using System.Web.UI; +using umbraco.cms.presentation.Trees; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Web; +using Umbraco.Web._Legacy.Controls; +using Umbraco.Web.UI.Pages; + +namespace umbraco.presentation.developer.packages +{ + /// + /// Summary description for packager. + /// + [Obsolete("This should not be used and will be removed in v8, this is kept here only for backwards compat reasons, this page should never be rendered/used")] + public class Installer : UmbracoEnsuredPage + { + + private Control _configControl; + private readonly cms.businesslogic.packager.Installer _installer; + protected Pane pane_installing; + protected Pane pane_optional; + + public Installer() + { + CurrentApp = Constants.Applications.Developer; + _installer = new cms.businesslogic.packager.Installer(Security.CurrentUser.Id); + } + + protected void Page_Load(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(Request.GetItemAsString("installing"))) + return; + + pane_optional.Visible = false; + pane_installing.Visible = true; + ProcessInstall(Request.GetItemAsString("installing")); + } + + private void ProcessInstall(string currentStep) + { + var dir = Request.GetItemAsString("dir"); + + int.TryParse(Request.GetItemAsString("pId"), out var packageId); + + switch (currentStep.ToLowerInvariant()) + { + case "custominstaller": + var customControl = Request.GetItemAsString("customControl"); + + if (customControl.IsNullOrWhiteSpace() == false) + { + pane_optional.Visible = false; + + _configControl = LoadControl(SystemDirectories.Root + customControl); + _configControl.ID = "packagerConfigControl"; + + pane_optional.Controls.Add(_configControl); + pane_optional.Visible = true; + + if (IsPostBack == false) + { + //We still need to clean everything up which is normally done in the Finished Action + PerformPostInstallCleanup(packageId, dir); + } + + } + else + { + //if the custom installer control is empty here (though it should never be because we've already checked for it previously) + //then we should run the normal FinishedAction + PerformFinishedAction(packageId, dir); + } + break; + default: + break; + } + } + + private void PerformPostInstallCleanup(int packageId, string dir) + { + _installer.InstallCleanUp(packageId, dir); + + // Update ClientDependency version + var clientDependencyConfig = new Umbraco.Core.Configuration.ClientDependencyConfiguration(Logger); + clientDependencyConfig.IncreaseVersionNumber(); + + //clear the tree cache - we'll do this here even though the browser will reload, but just in case it doesn't can't hurt. + ClientTools.ClearClientTreeCache().RefreshTree("packager"); + TreeDefinitionCollection.Instance.ReRegisterTrees(); + } + + private void PerformFinishedAction(int packageId, string dir) + { + pane_optional.Visible = false; + PerformPostInstallCleanup(packageId, dir); + } + } +}