Merge branch temp8 into temp8-11502

This commit is contained in:
Stephan
2018-08-27 09:57:41 +02:00
539 changed files with 12060 additions and 18064 deletions
+5 -1
View File
@@ -298,7 +298,11 @@
# copy libs
Write-Host "Copy SqlCE libraries"
$nugetPackages = [System.Environment]::ExpandEnvironmentVariables("%userprofile%\.nuget\packages")
$nugetPackages = $env:NUGET_PACKAGES
if (-not $nugetPackages)
{
$nugetPackages = [System.Environment]::ExpandEnvironmentVariables("%userprofile%\.nuget\packages")
}
$this.CopyFiles("$nugetPackages\umbraco.sqlserverce\4.0.0.1\runtimes\win-x86\native", "*.*", "$tmp\bin\x86")
$this.CopyFiles("$nugetPackages\umbraco.sqlserverce\4.0.0.1\runtimes\win-x64\native", "*.*", "$tmp\bin\amd64")
$this.CopyFiles("$nugetPackages\umbraco.sqlserverce\4.0.0.1\runtimes\win-x86\native", "*.*", "$tmp\WebApp\bin\x86")
@@ -34,11 +34,6 @@ namespace Umbraco.Core
public const string ListViewPrefix = "List View - ";
}
public static class PropertyGroups
{
public const string ListViewGroupName = "umbContainerView";
}
/// <summary>
/// Constants for Umbraco Content property aliases.
/// </summary>
@@ -17,11 +17,6 @@
/// </summary>
public const string File = "50899F9C-023A-4466-B623-ABA9049885FE";
/// <summary>
/// Guid for a Image PropertyTypeGroup object.
/// </summary>
public const string Contents = "79995FA2-63EE-453C-A29B-2E66F324CDBE";
/// <summary>
/// Guid for a Image PropertyTypeGroup object.
/// </summary>
+1 -4
View File
@@ -14,10 +14,7 @@ namespace Umbraco.Core.Events
_msgs.Add(msg);
}
public int Count
{
get { return _msgs.Count; }
}
public int Count => _msgs.Count;
public IEnumerable<EventMessage> GetAll()
{
@@ -205,7 +205,6 @@ namespace Umbraco.Core.Migrations.Install
{
_database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 3, ContentTypeNodeId = 1032, Text = "Image", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Image) });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 4, ContentTypeNodeId = 1033, Text = "File", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.File) });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 5, ContentTypeNodeId = 1031, Text = "Contents", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Contents) });
//membership property group
_database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 11, ContentTypeNodeId = 1044, Text = "Membership", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Membership) });
}
@@ -220,7 +219,6 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 27, UniqueId = 27.ToGuid(), DataTypeId = Constants.DataTypes.DefaultMediaListView, ContentTypeId = 1031, PropertyTypeGroupId = 5, Alias = "contents", Name = "Contents:", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
//membership property types
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = -89, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
@@ -234,7 +232,7 @@ namespace Umbraco.Core.Migrations.Install
private void CreateLanguageData()
{
_database.Insert(Constants.DatabaseSchema.Tables.Language, "id", false, new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "en-US" });
_database.Insert(Constants.DatabaseSchema.Tables.Language, "id", false, new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)" });
}
private void CreateContentChildTypeData()
@@ -141,13 +141,7 @@ namespace Umbraco.Core.Migrations.Install
//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();
.Select(x => new DbIndexDefinition(x)).ToArray();
result.TableDefinitions.AddRange(OrderedTables
.Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax)));
@@ -160,6 +154,14 @@ namespace Umbraco.Core.Migrations.Install
return result;
}
/// <summary>
/// This validates the Primary/Foreign keys in the database
/// </summary>
/// <param name="result"></param>
/// <remarks>
/// This does not validate any database constraints that are not PKs or FKs because Umbraco does not create a database with non PK/FK contraints.
/// Any unique "constraints" in the database are done with unique indexes.
/// </remarks>
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.
@@ -172,8 +174,7 @@ namespace Umbraco.Core.Migrations.Install
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 =>
@@ -188,7 +189,7 @@ namespace Umbraco.Core.Migrations.Install
// 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))
if (foreignKeysInSchema.InvariantContains(unknown) || primaryKeysInSchema.InvariantContains(unknown))
{
result.ValidConstraints.Add(unknown);
}
@@ -230,23 +231,6 @@ namespace Umbraco.Core.Migrations.Install
result.Errors.Add(new Tuple<string, string>("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<string, string>("Constraint", index));
}
}
private void ValidateDbColumns(DatabaseSchemaResult result)
@@ -1,5 +1,4 @@
using System;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0
{
@@ -12,18 +11,22 @@ namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0
public override void Migrate()
{
var exists = Context.Database.FirstOrDefault<RelationTypeDto>("WHERE alias=@alias", new { alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias });
if (exists == null)
var relationTypeCount = Context.Database.ExecuteScalar<int>("SELECT COUNT(*) FROM umbracoRelationType WHERE alias=@alias",
new { alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias });
if (relationTypeCount > 0)
return;
var uniqueId = (Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias + "____" + Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName).ToGuid();
Insert.IntoTable("umbracoRelationType").Row(new
{
Insert.IntoTable(Constants.DatabaseSchema.Tables.RelationType).Row(new
{
alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias,
name = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName,
childObjectType = Constants.ObjectTypes.MediaType,
parentObjectType = Constants.ObjectTypes.MediaType,
dual = false
typeUniqueId = uniqueId,
alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias,
name = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName,
childObjectType = Constants.ObjectTypes.MediaType,
parentObjectType = Constants.ObjectTypes.MediaType,
dual = false
}).Do();
}
}
}
@@ -1,4 +1,8 @@
namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0
using System.Linq;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.SqlSyntax;
namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0
{
public class IncreaseLanguageIsoCodeColumnLength : MigrationBase
{
@@ -8,7 +12,20 @@
public override void Migrate()
{
Delete.Index("IX_umbracoLanguage_languageISOCode").OnTable("umbracoLanguage").Do();
// Some people seem to have a constraint in their DB instead of an index, we'd need to drop that one
// See: https://our.umbraco.com/forum/using-umbraco-and-getting-started/93282-upgrade-from-711-to-712-fails
var constraints = SqlSyntax.GetConstraintsPerTable(Context.Database).Distinct().ToArray();
if (constraints.Any(x => x.Item2.InvariantEquals("IX_umbracoLanguage_languageISOCode")))
{
Delete.UniqueConstraint("IX_umbracoLanguage_languageISOCode").FromTable("umbracoLanguage").Do();
}
//Now check for indexes of that name and drop that if it exists
var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database);
if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoLanguage_languageISOCode")))
{
Delete.Index("IX_umbracoLanguage_languageISOCode").OnTable("umbracoLanguage").Do();
}
Alter.Table("umbracoLanguage")
.AlterColumn("languageISOCode")
@@ -19,8 +36,11 @@
Create.Index("IX_umbracoLanguage_languageISOCode")
.OnTable("umbracoLanguage")
.OnColumn("languageISOCode")
.Ascending()
.WithOptions()
.Unique()
.Do();
}
}
}
@@ -22,7 +22,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0
if (Context?.Database == null) return;
// We need to get all datatypes with an alias of "umbraco.tags" so we can loop over them and set the missing values if needed
var datatypes = Context.Database.Fetch<DataTypeDto>("SELECT * FROM cmsDataType");
var datatypes = Context.Database.Fetch<DataTypeDto>();
var tagsDataTypes = datatypes.Where(x => string.Equals(x.EditorAlias, Constants.PropertyEditors.Aliases.Tags, StringComparison.InvariantCultureIgnoreCase));
foreach (var datatype in tagsDataTypes)
@@ -9,7 +9,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0
public UpdateUmbracoConsent(IMigrationContext context)
: base(context)
{ }
public override void Migrate() { Alter.Table("umbracoConsent").AlterColumn("comment").AsString().Nullable().Do(); }
public override void Migrate() { Alter.Table(Constants.DatabaseSchema.Tables.Consent).AlterColumn("comment").AsString().Nullable().Do(); }
}
}
+9 -3
View File
@@ -213,17 +213,23 @@ namespace Umbraco.Core.Models
[IgnoreDataMember]
public IEnumerable<string> PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty<string>();
//fixme should this return false if ID == 0?
//fixme should this return false if IsCultureAvailable(culture) is false?
/// <inheritdoc />
public bool IsCulturePublished(string culture)
=> _publishInfos != null && _publishInfos.ContainsKey(culture);
=> _publishInfos != null && _publishInfos.ContainsKey(culture);
//fixme should this return false if ID == 0?
//fixme should this return false if IsCultureAvailable(culture) is false?
/// <inheritdoc />
public bool WasCulturePublished(string culture)
=> _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture);
=> _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture);
//fixme should this return false if ID == 0?
//fixme should this return false if IsCultureAvailable(culture) is false?
/// <inheritdoc />
public bool IsCultureEdited(string culture)
=> !IsCulturePublished(culture) || (_editedCultures != null && _editedCultures.Contains(culture));
=> !IsCulturePublished(culture) || (_editedCultures != null && _editedCultures.Contains(culture));
/// <inheritdoc/>
[IgnoreDataMember]
+1 -24
View File
@@ -376,30 +376,7 @@ namespace Umbraco.Core.Models
#endregion
#region Validation
/// <inheritdoc />
public bool IsValid(string culture = "*")
{
culture = culture.NullOrWhiteSpaceAsNull();
if (culture == null)
{
if (Name.IsNullOrWhiteSpace()) return false;
return ValidateProperties(null).Length == 0;
}
if (culture != "*")
{
var name = GetCultureName(culture);
if (name.IsNullOrWhiteSpace()) return false;
return ValidateProperties(culture).Length == 0;
}
// 'all cultures'
// those that have a name are ok, those without a name... we don't validate
return AvailableCultures.All(c => ValidateProperties(c).Length == 0);
}
/// <inheritdoc />
public virtual Property[] ValidateProperties(string culture = "*")
{
@@ -10,6 +10,20 @@
/// </summary>
public string PropertyAlias { get; set; }
/// <summary>
/// When dealing with content variants, this is the culture for the variant
/// </summary>
public string Culture { get; set; }
/// <summary>
/// An array of metadata that is parsed out from the file info posted to the server which is set on the client.
/// </summary>
/// <remarks>
/// This can be used for property types like Nested Content that need to have special unique identifiers for each file since there might be multiple files
/// per property.
/// </remarks>
public string[] Metadata { get; set; }
/// <summary>
/// Gets or sets the name of the file.
/// </summary>
+1 -8
View File
@@ -137,14 +137,7 @@ namespace Umbraco.Core.Models
void CopyFrom(IContent other, string culture = "*");
// fixme validate published cultures?
/// <summary>
/// Checks if the content and property values are valid in order to be persisted.
/// </summary>
/// <para>If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor
/// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty.</para>
bool IsValid(string culture = "*");
/// <summary>
/// Validates the content item's properties.
/// </summary>
+3
View File
@@ -343,6 +343,8 @@ namespace Umbraco.Core.Models
/// </summary>
internal bool IsValid(string culture = "*", string segment = "*")
{
//fixme - validating values shouldn't be done here, this calls in to IsValidValue
culture = culture.NullOrWhiteSpaceAsNull();
segment = segment.NullOrWhiteSpaceAsNull();
@@ -382,6 +384,7 @@ namespace Umbraco.Core.Models
/// <returns>True is property value is valid, otherwise false</returns>
private bool IsValidValue(object value)
{
//fixme this shouldn't exist here, the model itself shouldn't be responsible for it's own validation and this requires singleton access
return PropertyType.IsPropertyValueValid(value);
}
+2 -2
View File
@@ -378,11 +378,11 @@ namespace Umbraco.Core.Models
}
//fixme - perhaps this and other validation methods should be a service level (not a model) thing?
//fixme - this and other value validation methods should be a service level (not a model) thing. Changing this to internal for now
/// <summary>
/// Determines whether a value is valid for this property type.
/// </summary>
public bool IsPropertyValueValid(object value)
internal bool IsPropertyValueValid(object value)
{
var editor = Current.PropertyEditors[_propertyEditorAlias]; // fixme inject?
var configuration = Current.Services.DataTypeService.GetDataType(_dataTypeId).Configuration; // fixme inject?
@@ -6,13 +6,13 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions
{
public ConstraintDefinition(ConstraintType type)
{
constraintType = type;
_constraintType = type;
}
private ConstraintType constraintType;
public bool IsPrimaryKeyConstraint { get { return ConstraintType.PrimaryKey == constraintType; } }
public bool IsUniqueConstraint { get { return ConstraintType.Unique == constraintType; } }
public bool IsNonUniqueConstraint { get { return ConstraintType.NonUnique == constraintType; } }
private readonly ConstraintType _constraintType;
public bool IsPrimaryKeyConstraint => ConstraintType.PrimaryKey == _constraintType;
public bool IsUniqueConstraint => ConstraintType.Unique == _constraintType;
public bool IsNonUniqueConstraint => ConstraintType.NonUnique == _constraintType;
public string SchemaName { get; set; }
public string ConstraintName { get; set; }
@@ -1,13 +1,23 @@
namespace Umbraco.Core.Persistence.DatabaseModelDefinitions
using System;
namespace Umbraco.Core.Persistence.DatabaseModelDefinitions
{
/// <summary>
/// Represents a database index definition retreived by querying the database
/// </summary>
internal class DbIndexDefinition
{
public virtual string IndexName { get; set; }
public virtual string TableName { get; set; }
public virtual string ColumnName { get; set; }
public virtual bool IsUnique { get; set; }
public DbIndexDefinition(Tuple<string, string, string, bool> data)
{
TableName = data.Item1;
IndexName = data.Item2;
ColumnName = data.Item3;
IsUnique = data.Item4;
}
public string IndexName { get; }
public string TableName { get; }
public string ColumnName { get; }
public bool IsUnique { get; }
}
}
@@ -27,7 +27,11 @@ namespace Umbraco.Core.Persistence.Factories
if (xdtos.TryGetValue(propertyType.Id, out var propDtos))
{
foreach (var propDto in propDtos)
{
property.Id = propDto.Id;
property.FactorySetValue(languageRepository.GetIsoCodeById(propDto.LanguageId), propDto.Segment, propDto.VersionId == publishedVersionId, propDto.Value);
}
}
property.ResetDirtyProperties(false);
@@ -44,6 +44,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax
string TruncateTable { get; }
string CreateConstraint { get; }
string DeleteConstraint { get; }
[Obsolete("This is never used, use the Format(ForeignKeyDefinition) instead")]
string CreateForeignKeyConstraint { get; }
string DeleteDefaultConstraint { get; }
string FormatDateTime(DateTime date, bool includeTime = true);
@@ -80,8 +82,32 @@ namespace Umbraco.Core.Persistence.SqlSyntax
IEnumerable<string> GetTablesInSchema(IDatabase db);
IEnumerable<ColumnInfo> GetColumnsInSchema(IDatabase db);
/// <summary>
/// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not include indexes)
/// </summary>
/// <param name="db"></param>
/// <returns>
/// A Tuple containing: TableName, ConstraintName
/// </returns>
IEnumerable<Tuple<string, string>> GetConstraintsPerTable(IDatabase db);
/// <summary>
/// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not include indexes)
/// </summary>
/// <param name="db"></param>
/// <returns>
/// A Tuple containing: TableName, ColumnName, ConstraintName
/// </returns>
IEnumerable<Tuple<string, string, string>> GetConstraintsPerColumn(IDatabase db);
/// <summary>
/// Returns all defined Indexes in the database excluding primary keys
/// </summary>
/// <param name="db"></param>
/// <returns>
/// A Tuple containing: TableName, IndexName, ColumnName, IsUnique
/// </returns>
IEnumerable<Tuple<string, string, string, bool>> GetDefinedIndexes(IDatabase db);
}
}
@@ -79,6 +79,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax
return list;
}
/// <inheritdoc />
public override IEnumerable<Tuple<string, string>> GetConstraintsPerTable(IDatabase db)
{
List<Tuple<string, string>> list;
@@ -101,6 +102,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax
return list;
}
/// <inheritdoc />
public override IEnumerable<Tuple<string, string, string>> GetConstraintsPerColumn(IDatabase db)
{
List<Tuple<string, string, string>> list;
@@ -127,6 +129,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax
return list;
}
/// <inheritdoc />
public override IEnumerable<Tuple<string, string, string, bool>> GetDefinedIndexes(IDatabase db)
{
List<Tuple<string, string, string, bool>> list;
@@ -107,40 +107,27 @@ namespace Umbraco.Core.Persistence.SqlSyntax
item.IS_NULLABLE, item.DATA_TYPE)).ToList();
}
/// <inheritdoc />
public override IEnumerable<Tuple<string, string>> GetConstraintsPerTable(IDatabase db)
{
var items = db.Fetch<dynamic>("SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS");
var indexItems = db.Fetch<dynamic>("SELECT TABLE_NAME, INDEX_NAME FROM INFORMATION_SCHEMA.INDEXES");
return
items.Select(item => new Tuple<string, string>(item.TABLE_NAME, item.CONSTRAINT_NAME))
.Union(
indexItems.Select(
indexItem => new Tuple<string, string>(indexItem.TABLE_NAME, indexItem.INDEX_NAME)))
.ToList();
return items.Select(item => new Tuple<string, string>(item.TABLE_NAME, item.CONSTRAINT_NAME)).ToList();
}
/// <inheritdoc />
public override IEnumerable<Tuple<string, string, string>> GetConstraintsPerColumn(IDatabase db)
{
var items =
db.Fetch<dynamic>(
"SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE");
var indexItems = db.Fetch<dynamic>("SELECT INDEX_NAME, TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.INDEXES");
return
items.Select(
item => new Tuple<string, string, string>(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME))
.Union(
indexItems.Select(
indexItem =>
new Tuple<string, string, string>(indexItem.TABLE_NAME, indexItem.COLUMN_NAME,
indexItem.INDEX_NAME))).ToList();
var items = db.Fetch<dynamic>("SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE");
return items.Select(item => new Tuple<string, string, string>(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME)).ToList();
}
/// <inheritdoc />
public override IEnumerable<Tuple<string, string, string, bool>> GetDefinedIndexes(IDatabase db)
{
var items =
db.Fetch<dynamic>(
@"SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, [UNIQUE] FROM INFORMATION_SCHEMA.INDEXES
WHERE INDEX_NAME NOT LIKE 'PK_%'
WHERE PRIMARY_KEY=0
ORDER BY TABLE_NAME, INDEX_NAME");
return
items.Select(
@@ -163,6 +163,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax
item.IS_NULLABLE, item.DATA_TYPE)).ToList();
}
/// <inheritdoc />
public override IEnumerable<Tuple<string, string>> GetConstraintsPerTable(IDatabase db)
{
var items =
@@ -171,6 +172,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax
return items.Select(item => new Tuple<string, string>(item.TABLE_NAME, item.CONSTRAINT_NAME)).ToList();
}
/// <inheritdoc />
public override IEnumerable<Tuple<string, string, string>> GetConstraintsPerColumn(IDatabase db)
{
var items =
@@ -179,6 +181,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax
return items.Select(item => new Tuple<string, string, string>(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME)).ToList();
}
/// <inheritdoc />
public override IEnumerable<Tuple<string, string, string, bool>> GetDefinedIndexes(IDatabase db)
{
var items =
@@ -188,7 +191,7 @@ CASE WHEN I.is_unique_constraint = 1 OR I.is_unique = 1 THEN 1 ELSE 0 END AS [U
from sys.tables as T inner join sys.indexes as I on T.[object_id] = I.[object_id]
inner join sys.index_columns as IC on IC.[object_id] = I.[object_id] and IC.[index_id] = I.[index_id]
inner join sys.all_columns as AC on IC.[object_id] = AC.[object_id] and IC.[column_id] = AC.[column_id]
WHERE I.name NOT LIKE 'PK_%'
WHERE I.is_primary_key = 0
order by T.name, I.name");
return items.Select(item => new Tuple<string, string, string, bool>(item.TABLE_NAME, item.INDEX_NAME, item.COLUMN_NAME,
item.UNIQUE == 1)).ToList();
@@ -10,13 +10,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax
public static IEnumerable<DbIndexDefinition> GetDefinedIndexesDefinitions(this ISqlSyntaxProvider sql, IDatabase db)
{
return sql.GetDefinedIndexes(db)
.Select(x => new DbIndexDefinition
{
TableName = x.Item1,
IndexName = x.Item2,
ColumnName = x.Item3,
IsUnique = x.Item4
}).ToArray();
.Select(x => new DbIndexDefinition(x)).ToArray();
}
/// <summary>
@@ -1,6 +1,4 @@
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Web.PropertyEditors
namespace Umbraco.Core.PropertyEditors
{
/// <summary>
/// Represents the configuration for the slider value editor.
@@ -8,7 +6,7 @@ namespace Umbraco.Web.PropertyEditors
public class SliderConfiguration
{
[ConfigurationField("enableRange", "Enable range", "boolean")]
public string Range { get; set; }
public bool EnableRange { get; set; }
[ConfigurationField("orientation", "Orientation", "views/propertyeditors/slider/orientation.prevalues.html")]
public string Orientation { get; set; }
@@ -38,7 +36,7 @@ namespace Umbraco.Web.PropertyEditors
public string Tooltip { get; set; }
[ConfigurationField("tooltipSplit", "Tooltip split", "boolean", Description = "If false show one tootip if true show two tooltips one for each handler")]
public string TooltipSplit { get; set; } // fixme bool?
public bool TooltipSplit { get; set; } // fixme bool?
[ConfigurationField("tooltipFormat", "Tooltip format", "textstring", Description = "The value wanted to be displayed in the tooltip. Use {0} and {1} for current values - {1} is only for range slider and if not using tooltip split.")]
public string TooltipFormat { get; set; }
@@ -47,7 +45,7 @@ namespace Umbraco.Web.PropertyEditors
public string TooltipPosition { get; set; }
[ConfigurationField("reversed", "Reversed", "boolean", Description = "Whether or not the slider should be reversed")]
public string Reversed { get; set; } // fixme bool?
public bool Reversed { get; set; } // fixme bool?
[ConfigurationField("ticks", "Ticks", "textstring", Description = "Comma-separated values. Used to define the values of ticks. Tick marks are indicators to denote special values in the range. This option overwrites min and max options.")]
public string Ticks { get; set; }
@@ -61,4 +59,4 @@ namespace Umbraco.Web.PropertyEditors
[ConfigurationField("ticksSnapBounds", "Ticks snap bounds", "number", Description = "Used to define the snap bounds of a tick. Snaps to the tick if value is within these bounds.")]
public int TicksSnapBounds { get; set; }
}
}
}
@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace Umbraco.Core.PropertyEditors
{
public class SliderPropertyEditorConfiguration
{
[JsonProperty("enableRange")]
public bool EnableRange { get; set; }
}
}
@@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Umbraco.Core.Composing;
using Umbraco.Core.Exceptions;
using Umbraco.Core.Services;
namespace Umbraco.Core.PropertyEditors.Validators
{
@@ -11,6 +13,7 @@ namespace Umbraco.Core.PropertyEditors.Validators
/// </summary>
internal sealed class RegexValidator : IValueFormatValidator, IManifestValueValidator
{
private readonly ILocalizedTextService _textService;
private string _regex;
/// <inheritdoc cref="IManifestValueValidator.ValidationName"/>
@@ -22,8 +25,8 @@ namespace Umbraco.Core.PropertyEditors.Validators
/// <remarks>Use this constructor when the validator is used as an <see cref="IValueFormatValidator"/>,
/// and the regular expression is supplied at validation time. This constructor is also used when
/// the validator is used as an <see cref="IManifestValueValidator"/> and the regular expression
/// is supplied via the <see cref="Configure"/> method.</remarks>
public RegexValidator()
/// is supplied via the <see cref="Configuration"/> method.</remarks>
public RegexValidator() : this(Current.Services.TextService, null)
{ }
/// <summary>
@@ -31,10 +34,9 @@ namespace Umbraco.Core.PropertyEditors.Validators
/// </summary>
/// <remarks>Use this constructor when the validator is used as an <see cref="IValueValidator"/>,
/// and the regular expression must be supplied when the validator is created.</remarks>
public RegexValidator(string regex)
public RegexValidator(ILocalizedTextService textService, string regex)
{
if (string.IsNullOrWhiteSpace(regex))
throw new ArgumentNullOrEmptyException(nameof(regex));
_textService = textService;
_regex = regex;
}
@@ -66,7 +68,7 @@ namespace Umbraco.Core.PropertyEditors.Validators
{
if (string.IsNullOrWhiteSpace(format)) throw new ArgumentNullOrEmptyException(nameof(format));
if (value == null || !new Regex(format).IsMatch(value.ToString()))
yield return new ValidationResult("Value is invalid, it does not match the correct pattern", new[] { "value" });
yield return new ValidationResult(_textService.Localize("validation", "invalidPattern"), new[] { "value" });
}
}
}
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Umbraco.Core.Composing;
using Umbraco.Core.Services;
namespace Umbraco.Core.PropertyEditors.Validators
{
@@ -8,6 +10,17 @@ namespace Umbraco.Core.PropertyEditors.Validators
/// </summary>
internal sealed class RequiredValidator : IValueRequiredValidator, IManifestValueValidator
{
private readonly ILocalizedTextService _textService;
public RequiredValidator() : this(Current.Services.TextService)
{
}
public RequiredValidator(ILocalizedTextService textService)
{
_textService = textService;
}
/// <inheritdoc cref="IManifestValueValidator.ValidationName"/>
public string ValidationName => "Required";
@@ -22,20 +35,20 @@ namespace Umbraco.Core.PropertyEditors.Validators
{
if (value == null)
{
yield return new ValidationResult("Value cannot be null", new[] {"value"});
yield return new ValidationResult(_textService.Localize("validation", "invalidNull"), new[] {"value"});
yield break;
}
if (valueType.InvariantEquals(ValueTypes.Json))
{
if (value.ToString().DetectIsEmptyJson())
yield return new ValidationResult("Value cannot be empty", new[] { "value" });
yield return new ValidationResult(_textService.Localize("validation", "invalidEmpty"), new[] { "value" });
yield break;
}
if (value.ToString().IsNullOrWhiteSpace())
{
yield return new ValidationResult("Value cannot be empty", new[] { "value" });
yield return new ValidationResult(_textService.Localize("validation", "invalidEmpty"), new[] { "value" });
}
}
}
@@ -72,7 +72,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters
return Storages.GetOrAdd(dataTypeId, id =>
{
var dataType = _dataTypeService.GetDataType(id);
var configuration = dataType.ConfigurationAs<SliderPropertyEditorConfiguration>();
var configuration = dataType.ConfigurationAs<SliderConfiguration>();
return configuration.EnableRange;
});
}
@@ -1010,7 +1010,7 @@ namespace Umbraco.Core.Services.Implement
var originalPath = media.Path;
if (scope.Events.DispatchCancelable(Trashing, this, new MoveEventArgs<IMedia>(new MoveEventInfo<IMedia>(media, originalPath, Constants.System.RecycleBinMedia))))
if (scope.Events.DispatchCancelable(Trashing, this, new MoveEventArgs<IMedia>(new MoveEventInfo<IMedia>(media, originalPath, Constants.System.RecycleBinMedia)), nameof(Trashing)))
{
scope.Complete();
return OperationResult.Attempt.Cancel(evtMsgs);
@@ -1022,7 +1022,7 @@ namespace Umbraco.Core.Services.Implement
var moveInfo = moves.Select(x => new MoveEventInfo<IMedia>(x.Item1, x.Item2, x.Item1.ParentId))
.ToArray();
scope.Events.Dispatch(Trashed, this, new MoveEventArgs<IMedia>(false, evtMsgs, moveInfo));
scope.Events.Dispatch(Trashed, this, new MoveEventArgs<IMedia>(false, evtMsgs, moveInfo), nameof(Trashed));
Audit(AuditType.Move, "Move Media to Recycle Bin performed by user", userId, media.Id);
scope.Complete();
@@ -1058,7 +1058,7 @@ namespace Umbraco.Core.Services.Implement
var moveEventInfo = new MoveEventInfo<IMedia>(media, media.Path, parentId);
var moveEventArgs = new MoveEventArgs<IMedia>(moveEventInfo);
if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs))
if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs, nameof(Moving)))
{
scope.Complete();
return;
@@ -1082,7 +1082,7 @@ namespace Umbraco.Core.Services.Implement
.ToArray();
moveEventArgs.MoveInfoCollection = moveInfo;
moveEventArgs.CanCancel = false;
scope.Events.Dispatch(Moved, this, moveEventArgs);
scope.Events.Dispatch(Moved, this, moveEventArgs, nameof(Moved));
Audit(AuditType.Move, "Move Media performed by user", userId, media.Id);
scope.Complete();
}
@@ -1382,7 +1382,7 @@ namespace Umbraco.Core.Services.Implement
/// <para>Deletes media items of the specified type, and only that type. Does *not* handle content types
/// inheritance and compositions, which need to be managed outside of this method.</para>
/// </remarks>
/// <param name="mediaTypeId">Id of the <see cref="IMediaType"/></param>
/// <param name="mediaTypeIds">Id of the <see cref="IMediaType"/></param>
/// <param name="userId">Optional id of the user deleting the media</param>
public void DeleteMediaOfTypes(IEnumerable<int> mediaTypeIds, int userId = 0)
{
@@ -1434,7 +1434,7 @@ namespace Umbraco.Core.Services.Implement
var moveInfos = moves.Select(x => new MoveEventInfo<IMedia>(x.Item1, x.Item2, x.Item1.ParentId))
.ToArray();
if (moveInfos.Length > 0)
scope.Events.Dispatch(Trashed, this, new MoveEventArgs<IMedia>(false, moveInfos), "Trashed");
scope.Events.Dispatch(Trashed, this, new MoveEventArgs<IMedia>(false, moveInfos), nameof(Trashed));
scope.Events.Dispatch(TreeChanged, this, changes.ToEventArgs());
Audit(AuditType.Delete, $"Delete Media of types {string.Join(",", mediaTypeIdsA)} performed by user", userId, Constants.System.Root);
+1 -1
View File
@@ -406,7 +406,7 @@
<Compile Include="PropertyEditors\PropertyEditorCollection.cs" />
<Compile Include="PropertyEditors\PropertyEditorTagsExtensions.cs" />
<Compile Include="PropertyEditors\PropertyValueLevel.cs" />
<Compile Include="PropertyEditors\SliderPropertyEditorConfiguration.cs" />
<Compile Include="PropertyEditors\SliderConfiguration.cs" />
<Compile Include="PropertyEditors\TagConfiguration.cs" />
<Compile Include="PropertyEditors\ValueConverters\ImageCropperValue.cs" />
<Compile Include="PropertyEditors\ValueConverters\ImageCropperValueTypeConverter.cs" />
@@ -389,6 +389,8 @@ namespace Umbraco.Examine
//the value of the field 'as-is'.
foreach (var value in e.IndexItem.ValueSet.Values.ToList()) //ToList here to make a diff collection else we'll get collection modified errors
{
if (value.Value == null) continue;
if (value.Value.Count > 0)
{
if (value.Value.First() is string str)
@@ -272,7 +272,7 @@ AnotherContentFinder
public void Resolves_Actions()
{
var actions = _typeLoader.GetActions();
Assert.AreEqual(35, actions.Count());
Assert.AreEqual(34, actions.Count());
}
[Test]
@@ -67,9 +67,6 @@ namespace Umbraco.Tests.Configurations
Assert.IsTrue(globalSettings.IsReservedPathOrUrl(url));
}
[TestCase("/umbraco_client/Tree/treeIcons.css")]
[TestCase("/umbraco_client/Tree/Themes/umbraco/style.css")]
[TestCase("/umbraco_client/scrollingmenu/style.css")]
[TestCase("/base/somebasehandler")]
[TestCase("/")]
[TestCase("/home.aspx")]
@@ -417,7 +417,6 @@ namespace Umbraco.Tests.CoreXml
{
var source = new TestSource5();
var nav = new NavigableNavigator(source);
TestContent content;
Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position);
Assert.IsTrue(nav.MoveToFirstChild());
@@ -2,14 +2,17 @@
using System.Linq;
using Moq;
using System.Text;
using LightInject;
using NUnit.Framework;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Core.Manifest;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.PropertyEditors.Validators;
using Umbraco.Core.Services;
namespace Umbraco.Tests.Manifest
{
@@ -22,10 +25,25 @@ namespace Umbraco.Tests.Manifest
[SetUp]
public void Setup()
{
Current.Reset();
var container = Mock.Of<IServiceContainer>();
Current.Container = container;
var serviceContext = new ServiceContext(
localizedTextService: Mock.Of<ILocalizedTextService>());
Mock.Get(container)
.Setup(x => x.GetInstance(It.IsAny<Type>()))
.Returns<Type>(x =>
{
if (x == typeof(ServiceContext)) return serviceContext;
throw new Exception("oops");
});
var validators = new IManifestValueValidator[]
{
new RequiredValidator(),
new RegexValidator()
new RequiredValidator(Mock.Of<ILocalizedTextService>()),
new RegexValidator(Mock.Of<ILocalizedTextService>(), null)
};
_parser = new ManifestParser(NullCacheProvider.Instance, new ManifestValueValidatorCollection(validators), Mock.Of<ILogger>());
}
@@ -12,7 +12,6 @@ namespace Umbraco.Tests.Models.Collections
public abstract class Item : IEntity, ICanBeDirty
{
private bool _hasIdentity;
private int? _hash;
private int _id;
private Guid _key;
@@ -184,12 +183,6 @@ namespace Umbraco.Tests.Models.Collections
public static bool operator ==(Item left, Item right)
{
/*if (ReferenceEquals(null, left))
return false;
if(ReferenceEquals(null, right))
return false;*/
return ReferenceEquals(left, right);
}
@@ -56,7 +56,7 @@ namespace Umbraco.Tests.Models.Mapping
var content = MockedMedia.CreateMediaImage(contentType, -1);
FixUsers(content);
var result = Mapper.Map<IMedia, ContentItemBasic<ContentPropertyBasic, IMedia>>(content);
var result = Mapper.Map<IMedia, ContentItemBasic<ContentPropertyBasic>>(content);
AssertBasics(result, content);
@@ -73,7 +73,7 @@ namespace Umbraco.Tests.Models.Mapping
var content = MockedContent.CreateSimpleContent(contentType);
FixUsers(content);
var result = Mapper.Map<IContent, ContentItemBasic<ContentPropertyBasic, IContent>>(content);
var result = Mapper.Map<IContent, ContentItemBasic<ContentPropertyBasic>>(content);
AssertBasics(result, content);
@@ -90,9 +90,12 @@ namespace Umbraco.Tests.Models.Mapping
var content = MockedContent.CreateSimpleContent(contentType);
FixUsers(content);
var result = Mapper.Map<IContent, ContentItemDto<IContent>>(content);
var result = Mapper.Map<IContent, ContentPropertyCollectionDto>(content);
AssertContentItem(result, content);
foreach (var p in content.Properties)
{
AssertProperty(result, p);
}
}
[Test]
@@ -102,9 +105,12 @@ namespace Umbraco.Tests.Models.Mapping
var content = MockedMedia.CreateMediaImage(contentType, -1);
FixUsers(content);
var result = Mapper.Map<IMedia, ContentItemDto<IMedia>>(content);
var result = Mapper.Map<IMedia, ContentPropertyCollectionDto>(content);
AssertContentItem(result, content);
foreach (var p in content.Properties)
{
AssertProperty(result, p);
}
}
[Test]
@@ -123,12 +129,16 @@ namespace Umbraco.Tests.Models.Mapping
AssertBasics(result, content);
var invariantContent = result.Variants.First();
foreach (var p in content.Properties)
AssertDisplayProperty(result, p);
{
AssertBasicProperty(invariantContent, p);
AssertDisplayProperty(invariantContent, p);
}
Assert.AreEqual(content.PropertyGroups.Count(), result.Tabs.Count());
Assert.IsTrue(result.Tabs.First().IsActive);
Assert.IsTrue(result.Tabs.Except(new[] {result.Tabs.First()}).All(x => x.IsActive == false));
Assert.AreEqual(content.PropertyGroups.Count(), invariantContent.Tabs.Count());
Assert.IsTrue(invariantContent.Tabs.First().IsActive);
Assert.IsTrue(invariantContent.Tabs.Except(new[] { invariantContent.Tabs.First() }).All(x => x.IsActive == false));
}
[Test]
@@ -141,11 +151,15 @@ namespace Umbraco.Tests.Models.Mapping
var result = Mapper.Map<IContent, ContentItemDisplay>(content);
AssertBasics(result, content);
var invariantContent = result.Variants.First();
foreach (var p in content.Properties)
{
AssertDisplayProperty(result, p);
AssertBasicProperty(invariantContent, p);
AssertDisplayProperty(invariantContent, p);
}
Assert.AreEqual(content.PropertyGroups.Count(), result.Tabs.Count());
Assert.AreEqual(content.PropertyGroups.Count(), invariantContent.Tabs.Count());
}
[Test]
@@ -186,23 +200,24 @@ namespace Umbraco.Tests.Models.Mapping
var result = Mapper.Map<IContent, ContentItemDisplay>(content);
AssertBasics(result, content);
var invariantContent = result.Variants.First();
foreach (var p in content.Properties)
{
AssertDisplayProperty(result, p);
AssertBasicProperty(invariantContent, p);
AssertDisplayProperty(invariantContent, p);
}
Assert.AreEqual(content.PropertyGroups.Count(), result.Tabs.Count() - 1);
Assert.IsTrue(result.Tabs.Any(x => x.Label == Current.Services.TextService.Localize("general/properties")));
Assert.AreEqual(2, result.Tabs.Where(x => x.Label == Current.Services.TextService.Localize("general/properties")).SelectMany(x => x.Properties.Where(p => p.Alias.StartsWith("_umb_") == false)).Count());
Assert.AreEqual(content.PropertyGroups.Count(), invariantContent.Tabs.Count() - 1);
Assert.IsTrue(invariantContent.Tabs.Any(x => x.Label == Current.Services.TextService.Localize("general/properties")));
Assert.AreEqual(2, invariantContent.Tabs.Where(x => x.Label == Current.Services.TextService.Localize("general/properties")).SelectMany(x => x.Properties.Where(p => p.Alias.StartsWith("_umb_") == false)).Count());
}
#region Assertions
private void AssertDisplayProperty<T, TPersisted>(ContentItemBasic<T, TPersisted> result, Property p)
where T : ContentPropertyDisplay
where TPersisted : IContentBase
private void AssertDisplayProperty<T>(IContentProperties<T> result, Property p)
where T : ContentPropertyBasic
{
AssertBasicProperty(result, p);
var pDto = result.Properties.SingleOrDefault(x => x.Alias == p.Alias);
Assert.IsNotNull(pDto);
@@ -213,7 +228,33 @@ namespace Umbraco.Tests.Models.Mapping
}
private void AssertBasics<T, TPersisted>(ContentItemBasic<T, TPersisted> result, TPersisted content)
private void AssertBasics(ContentItemDisplay result, IContent content)
{
Assert.AreEqual(content.Id, result.Id);
var ownerId = content.CreatorId;
if (ownerId != 0)
{
Assert.IsNotNull(result.Owner);
Assert.AreEqual(Constants.Security.SuperUserId, result.Owner.UserId);
Assert.AreEqual("Administrator", result.Owner.Name);
}
else
{
Assert.IsNull(result.Owner); // because, 0 is no user
}
var invariantContent = result.Variants.First();
Assert.AreEqual(content.ParentId, result.ParentId);
Assert.AreEqual(content.UpdateDate, invariantContent.UpdateDate);
Assert.AreEqual(content.CreateDate, invariantContent.CreateDate);
Assert.AreEqual(content.Name, invariantContent.Name);
Assert.AreEqual(content.Properties.Count(),
((IContentProperties<ContentPropertyDisplay>)invariantContent).Properties.Count(x => x.Alias.StartsWith("_umb_") == false));
}
private void AssertBasics<T, TPersisted>(ContentItemBasic<T> result, TPersisted content)
where T : ContentPropertyBasic
where TPersisted : IContentBase
{
@@ -238,9 +279,8 @@ namespace Umbraco.Tests.Models.Mapping
Assert.AreEqual(content.Properties.Count(), result.Properties.Count(x => x.Alias.StartsWith("_umb_") == false));
}
private void AssertBasicProperty<T, TPersisted>(ContentItemBasic<T, TPersisted> result, Property p)
private void AssertBasicProperty<T>(IContentProperties<T> result, Property p)
where T : ContentPropertyBasic
where TPersisted : IContentBase
{
var pDto = result.Properties.SingleOrDefault(x => x.Alias == p.Alias);
Assert.IsNotNull(pDto);
@@ -255,8 +295,7 @@ namespace Umbraco.Tests.Models.Mapping
Assert.AreEqual(pDto.Value, p.GetValue().ToString());
}
private void AssertProperty<TPersisted>(ContentItemBasic<ContentPropertyDto, TPersisted> result, Property p)
where TPersisted : IContentBase
private void AssertProperty(IContentProperties<ContentPropertyDto> result, Property p)
{
AssertBasicProperty(result, p);
@@ -270,7 +309,7 @@ namespace Umbraco.Tests.Models.Mapping
Assert.AreEqual(Current.PropertyEditors[p.PropertyType.PropertyEditorAlias], pDto.PropertyEditor);
}
private void AssertContentItem<T>(ContentItemBasic<ContentPropertyDto, T> result, T content)
private void AssertContentItem<T>(ContentItemBasic<ContentPropertyDto> result, T content)
where T : IContentBase
{
AssertBasics(result, content);
+5 -1
View File
@@ -46,7 +46,9 @@ namespace Umbraco.Tests.Models
.Setup(x => x.GetDataType(It.IsAny<int>()))
.Returns<int>(x => dataType);
var serviceContext = new ServiceContext(dataTypeService: dataTypeService);
var serviceContext = new ServiceContext(
dataTypeService: dataTypeService,
localizedTextService: Mock.Of<ILocalizedTextService>());
Mock.Get(container)
.Setup(x => x.GetInstance(It.IsAny<Type>()))
@@ -54,8 +56,10 @@ namespace Umbraco.Tests.Models
{
if (x == typeof(PropertyEditorCollection)) return propertyEditors;
if (x == typeof(ServiceContext)) return serviceContext;
if (x == typeof(ILocalizedTextService)) return serviceContext.LocalizationService;
throw new Exception("oops");
});
}
[Test]
@@ -63,7 +63,6 @@ namespace Umbraco.Tests.Routing
// do not test for /base here as it's handled before EnsureUmbracoRoutablePage is called
[TestCase("/umbraco_client/Tree/treeIcons.css", false)]
[TestCase("/umbraco_client/Tree/Themes/umbraco/style.css?cdv=37", false)]
[TestCase("/umbraco_client/scrollingmenu/style.css?cdv=37", false)]
[TestCase("/umbraco/umbraco.aspx", false)]
[TestCase("/umbraco/editContent.aspx", false)]
[TestCase("/install/default.aspx", false)]
@@ -89,7 +88,6 @@ namespace Umbraco.Tests.Routing
[TestCase("/favicon.ico", true)]
[TestCase("/umbraco_client/Tree/treeIcons.css", true)]
[TestCase("/umbraco_client/Tree/Themes/umbraco/style.css?cdv=37", true)]
[TestCase("/umbraco_client/scrollingmenu/style.css?cdv=37", true)]
[TestCase("/base/somebasehandler", false)]
[TestCase("/", false)]
[TestCase("/home.aspx", false)]
@@ -945,8 +945,6 @@ namespace Umbraco.Tests.Scheduling
throw new NotImplementedException();
}
private int i;
public async Task RunAsync(CancellationToken token)
{
Console.WriteLine("boom");
@@ -63,7 +63,7 @@ namespace Umbraco.Tests.Services
/// <summary>
/// Used to list out all ambiguous events that will require dispatching with a name
/// </summary>
[Test]
[Test, Explicit]
public void List_Ambiguous_Events()
{
var events = ServiceContext.ContentService.GetType().GetEvents(BindingFlags.Static | BindingFlags.Public);
@@ -1,16 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Events;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Tests.Testing;
@@ -22,6 +25,32 @@ namespace Umbraco.Tests.Services
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true)]
public class MediaServiceTests : TestWithSomeContentBase
{
/// <summary>
/// Used to list out all ambiguous events that will require dispatching with a name
/// </summary>
[Test, Explicit]
public void List_Ambiguous_Events()
{
var events = ServiceContext.MediaService.GetType().GetEvents(BindingFlags.Static | BindingFlags.Public);
var typedEventHandler = typeof(TypedEventHandler<,>);
foreach (var e in events)
{
//only continue if this is a TypedEventHandler
if (!e.EventHandlerType.IsGenericType) continue;
var typeDef = e.EventHandlerType.GetGenericTypeDefinition();
if (typedEventHandler != typeDef) continue;
//get the event arg type
var eventArgType = e.EventHandlerType.GenericTypeArguments[1];
var found = EventNameExtractor.FindEvent(typeof(MediaService), eventArgType, EventNameExtractor.MatchIngNames);
if (!found.Success && found.Result.Error == EventNameExtractorError.Ambiguous)
{
Console.WriteLine($"Ambiguous event, source: {typeof(MediaService)}, args: {eventArgType}");
}
}
}
[Test]
public void Get_Paged_Children_With_Media_Type_Filter()
{
@@ -8,6 +8,9 @@ using Umbraco.Core.Security;
namespace Umbraco.Tests.TestHelpers.ControllerTesting
{
/// <summary>
/// Ensures there's an admin user assigned to the request
/// </summary>
public class AuthenticateEverythingMiddleware : AuthenticationMiddleware<AuthenticationOptions>
{
public AuthenticateEverythingMiddleware(OwinMiddleware next, IAppBuilder app, AuthenticationOptions options)
@@ -26,7 +29,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
{
var sessionId = Guid.NewGuid().ToString();
var identity = new UmbracoBackOfficeIdentity(
-1, "admin", "Admin", null, null, "en-US", sessionId, sessionId, new[] { "content", "media", "members" }, new[] { "admin" });
-1, "admin", "Admin", new []{-1}, new[] { -1 }, "en-US", sessionId, sessionId, new[] { "content", "media", "members" }, new[] { "admin" });
return Task.FromResult(new AuthenticationTicket(identity,
new AuthenticationProperties()
@@ -22,8 +22,16 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
_controllerFactory = controllerFactory;
}
public async Task<Tuple<HttpResponseMessage, string>> Execute(string controllerName, string actionName, HttpMethod method, HttpContent content = null)
public async Task<Tuple<HttpResponseMessage, string>> Execute(string controllerName, string actionName, HttpMethod method,
HttpContent content = null,
MediaTypeWithQualityHeaderValue mediaTypeHeader = null,
bool assertOkResponse = true)
{
if (mediaTypeHeader == null)
{
mediaTypeHeader = new MediaTypeWithQualityHeaderValue("application/json");
}
var startup = new TestStartup(
configuration =>
{
@@ -44,7 +52,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
if (content != null)
request.Content = content;
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Accept.Add(mediaTypeHeader);
Console.WriteLine(request);
var response = await server.HttpClient.SendAsync(request);
@@ -62,7 +70,11 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
Console.Write(JsonConvert.SerializeObject(deserialized, Formatting.Indented));
}
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
if (assertOkResponse)
{
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
return Tuple.Create(response, json);
}
}
+1
View File
@@ -214,6 +214,7 @@
<Compile Include="Collections\DeepCloneableListTests.cs" />
<Compile Include="Web\Controllers\BackOfficeControllerUnitTests.cs" />
<Compile Include="CoreThings\DelegateExtensionsTests.cs" />
<Compile Include="Web\Controllers\ContentControllerTests.cs" />
<Compile Include="Web\Controllers\UserEditorAuthorizationHelperTests.cs" />
<Compile Include="Web\Controllers\UsersControllerTests.cs" />
<Compile Include="Web\HealthChecks\HealthCheckResultsTests.cs" />
@@ -16,59 +16,66 @@ namespace Umbraco.Tests.Web.AngularIntegration
{
//create 3 tabs with 3 properties each
var tabs = new List<Tab<ContentPropertyDisplay>>();
for (var tabIndex = 0; tabIndex < 3; tabIndex ++)
for (var tabIndex = 0; tabIndex < 3; tabIndex++)
{
var props = new List<ContentPropertyDisplay>();
for (var propertyIndex = 0; propertyIndex < 3; propertyIndex ++)
for (var propertyIndex = 0; propertyIndex < 3; propertyIndex++)
{
props.Add(new ContentPropertyDisplay
{
Alias = "property" + propertyIndex,
Label = "Property " + propertyIndex,
Id = propertyIndex,
Value = "value" + propertyIndex,
Config = new Dictionary<string, object> {{ propertyIndex.ToInvariantString(), "value" }},
Description = "Description " + propertyIndex,
View = "~/Views/View" + propertyIndex,
HideLabel = false
});
{
Alias = "property" + propertyIndex,
Label = "Property " + propertyIndex,
Id = propertyIndex,
Value = "value" + propertyIndex,
Config = new Dictionary<string, object> { { propertyIndex.ToInvariantString(), "value" } },
Description = "Description " + propertyIndex,
View = "~/Views/View" + propertyIndex,
HideLabel = false
});
}
tabs.Add(new Tab<ContentPropertyDisplay>()
{
Alias = "Tab" + tabIndex,
Label = "Tab" + tabIndex,
Properties = props
});
{
Alias = "Tab" + tabIndex,
Label = "Tab" + tabIndex,
Properties = props
});
}
var displayModel = new ContentItemDisplay
{
Id = 1234,
Variants = new List<ContentVariantDisplay>
{
Id = 1234,
Name = "Test",
Tabs = tabs
};
new ContentVariantDisplay
{
Name = "Test",
Tabs = tabs
}
}
};
var json = JsonConvert.SerializeObject(displayModel);
var jObject = JObject.Parse(json);
Assert.AreEqual("1234", jObject["id"].ToString());
Assert.AreEqual("Test", jObject["name"].ToString());
Assert.AreEqual(3, jObject["tabs"].Count());
for (var tab = 0; tab < jObject["tabs"].Count(); tab++)
Assert.AreEqual("Test", jObject["variants"][0]["name"].ToString());
var jsonTabs = jObject["variants"][0]["tabs"];
Assert.AreEqual(3, jsonTabs.Count());
for (var tab = 0; tab < jsonTabs.Count(); tab++)
{
Assert.AreEqual("Tab" + tab, jObject["tabs"][tab]["alias"].ToString());
Assert.AreEqual("Tab" + tab, jObject["tabs"][tab]["label"].ToString());
Assert.AreEqual(3, jObject["tabs"][tab]["properties"].Count());
for (var prop = 0; prop < jObject["tabs"][tab]["properties"].Count(); prop++)
Assert.AreEqual("Tab" + tab, jsonTabs[tab]["alias"].ToString());
Assert.AreEqual("Tab" + tab, jsonTabs[tab]["label"].ToString());
Assert.AreEqual(3, jsonTabs[tab]["properties"].Count());
for (var prop = 0; prop < jsonTabs[tab]["properties"].Count(); prop++)
{
Assert.AreEqual("property" + prop, jObject["tabs"][tab]["properties"][prop]["alias"].ToString());
Assert.AreEqual("Property " + prop, jObject["tabs"][tab]["properties"][prop]["label"].ToString());
Assert.AreEqual(prop, jObject["tabs"][tab]["properties"][prop]["id"].Value<int>());
Assert.AreEqual("value" + prop, jObject["tabs"][tab]["properties"][prop]["value"].ToString());
Assert.AreEqual("{\"" + prop + "\":\"value\"}", jObject["tabs"][tab]["properties"][prop]["config"].ToString(Formatting.None));
Assert.AreEqual("Description " + prop, jObject["tabs"][tab]["properties"][prop]["description"].ToString());
Assert.AreEqual(false, jObject["tabs"][tab]["properties"][prop]["hideLabel"].Value<bool>());
Assert.AreEqual("property" + prop, jsonTabs[tab]["properties"][prop]["alias"].ToString());
Assert.AreEqual("Property " + prop, jsonTabs[tab]["properties"][prop]["label"].ToString());
Assert.AreEqual(prop, jsonTabs[tab]["properties"][prop]["id"].Value<int>());
Assert.AreEqual("value" + prop, jsonTabs[tab]["properties"][prop]["value"].ToString());
Assert.AreEqual("{\"" + prop + "\":\"value\"}", jsonTabs[tab]["properties"][prop]["config"].ToString(Formatting.None));
Assert.AreEqual("Description " + prop, jsonTabs[tab]["properties"][prop]["description"].ToString());
Assert.AreEqual(false, jsonTabs[tab]["properties"][prop]["hideLabel"].Value<bool>());
}
}
}
@@ -0,0 +1,239 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web.Http;
using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.ControllerTesting;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Tests.Testing;
using Umbraco.Web;
using Umbraco.Web.Editors;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.PublishedCache;
using Umbraco.Web._Legacy.Actions;
using Task = System.Threading.Tasks.Task;
namespace Umbraco.Tests.Web.Controllers
{
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.None)]
public class ContentControllerTests : TestWithDatabaseBase
{
protected override void ComposeApplication(bool withApplication)
{
base.ComposeApplication(withApplication);
//Replace with mockable services:
var userServiceMock = new Mock<IUserService>();
userServiceMock.Setup(service => service.GetUserById(It.IsAny<int>()))
.Returns((int id) => id == 1234 ? new User(1234, "Test", "test@test.com", "test@test.com", "", new List<IReadOnlyUserGroup>(), new int[0], new int[0]) : null);
userServiceMock.Setup(service => service.GetPermissionsForPath(It.IsAny<IUser>(), It.IsAny<string>()))
.Returns(new EntityPermissionSet(123, new EntityPermissionCollection(new[]
{
new EntityPermission(0, 123, new[]
{
ActionBrowse.Instance.Letter.ToString(),
ActionUpdate.Instance.Letter.ToString(),
ActionPublish.Instance.Letter.ToString(),
ActionNew.Instance.Letter.ToString()
}),
})));
var entityService = new Mock<IEntityService>();
entityService.Setup(x => x.GetAllPaths(UmbracoObjectTypes.Document, It.IsAny<int[]>()))
.Returns((UmbracoObjectTypes objType, int[] ids) => ids.Select(x => new TreeEntityPath {Path = $"-1,{x}", Id = x}).ToList());
var dataTypeService = new Mock<IDataTypeService>();
dataTypeService.Setup(service => service.GetDataType(It.IsAny<int>()))
.Returns(MockedDataType());
Container.RegisterSingleton(f => Mock.Of<IContentService>());
Container.RegisterSingleton(f => userServiceMock.Object);
Container.RegisterSingleton(f => entityService.Object);
Container.RegisterSingleton(f => dataTypeService.Object);
}
private IDataType MockedDataType()
{
return Mock.Of<IDataType>(type => type.Id == 9876 && type.Name == "text");
}
private MultipartFormDataContent GetMultiPartRequestContent(string json)
{
var multiPartBoundary = "----WebKitFormBoundary123456789";
return new MultipartFormDataContent(multiPartBoundary)
{
new StringContent(json)
{
Headers =
{
ContentDisposition = new ContentDispositionHeaderValue("form-data")
{
Name = "contentItem"
}
}
}
};
}
private const string PublishJson1 = @"{
""id"": 123,
""contentTypeAlias"": ""page"",
""parentId"": -1,
""action"": ""save"",
""variants"": [
{
""name"": null,
""properties"": [
{
""id"": 1,
""alias"": ""title"",
""value"": ""asdf""
}
],
""culture"": ""en-US""
},
{
""name"": null,
""properties"": [
{
""id"": 1,
""alias"": ""title"",
""value"": ""asdf""
}
],
""culture"": ""fr-FR""
},
{
""name"": ""asdf"",
""properties"": [
{
""id"": 1,
""alias"": ""title"",
""value"": ""asdf""
}
],
""culture"": ""es-ES"",
""save"": true,
""publish"": true
}
]
}";
/// <summary>
/// Returns 404 if the content wasn't found based on the ID specified
/// </summary>
/// <returns></returns>
[Test]
public async Task PostSave_Validate_Existing_Content()
{
ApiController Factory(HttpRequestMessage message, UmbracoHelper helper)
{
//var content = MockedContent.CreateSimpleContent(MockedContentTypes.CreateSimpleContentType());
//content.Id = 999999999; //this will not be found
//content.Path = "-1,999999999";
var contentServiceMock = Mock.Get(Current.Services.ContentService);
contentServiceMock.Setup(x => x.GetById(123)).Returns(() => null);
var publishedSnapshot = Mock.Of<IPublishedSnapshotService>();
var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<DataEditor>()));
var usersController = new ContentController(publishedSnapshot, propertyEditorCollection);
Container.InjectProperties(usersController);
return usersController;
}
var runner = new TestRunner(Factory);
var response = await runner.Execute("Content", "PostSave", HttpMethod.Post,
content: GetMultiPartRequestContent(PublishJson1),
mediaTypeHeader: new MediaTypeWithQualityHeaderValue("multipart/form-data"),
assertOkResponse: false);
Assert.AreEqual(HttpStatusCode.NotFound, response.Item1.StatusCode);
Assert.AreEqual(")]}',\n{\"Message\":\"content was not found\"}", response.Item1.Content.ReadAsStringAsync().Result);
//var obj = JsonConvert.DeserializeObject<PagedResult<UserDisplay>>(response.Item2);
//Assert.AreEqual(0, obj.TotalItems);
}
[Test]
public async Task PostSave_Validate_At_Least_One_Variant_Flagged_For_Saving()
{
ApiController Factory(HttpRequestMessage message, UmbracoHelper helper)
{
var contentServiceMock = Mock.Get(Current.Services.ContentService);
contentServiceMock.Setup(x => x.GetById(123)).Returns(() => null);
var publishedSnapshot = Mock.Of<IPublishedSnapshotService>();
var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<DataEditor>()));
var usersController = new ContentController(publishedSnapshot, propertyEditorCollection);
Container.InjectProperties(usersController);
return usersController;
}
var json = JsonConvert.DeserializeObject<JObject>(PublishJson1);
//remove all save flaggs
((JArray)json["variants"])[2]["save"] = false;
var runner = new TestRunner(Factory);
var response = await runner.Execute("Content", "PostSave", HttpMethod.Post,
content: GetMultiPartRequestContent(JsonConvert.SerializeObject(json)),
mediaTypeHeader: new MediaTypeWithQualityHeaderValue("multipart/form-data"),
assertOkResponse: false);
Assert.AreEqual(HttpStatusCode.NotFound, response.Item1.StatusCode);
Assert.AreEqual(")]}',\n{\"Message\":\"No variants flagged for saving\"}", response.Item1.Content.ReadAsStringAsync().Result);
}
/// <summary>
/// Returns 404 if any of the posted properties dont actually exist
/// </summary>
/// <returns></returns>
[Test, Ignore("Not implemented yet")]
public async Task PostSave_Validate_Properties_Exist()
{
//TODO: Make this work! to finish it, we need to include a property in the POST data that doesn't exist on the content type
// or change the content type below to not include one of the posted ones
ApiController Factory(HttpRequestMessage message, UmbracoHelper helper)
{
var content = MockedContent.CreateSimpleContent(MockedContentTypes.CreateSimpleContentType());
content.Id = 123;
content.Path = "-1,123";
var contentServiceMock = Mock.Get(Current.Services.ContentService);
contentServiceMock.Setup(x => x.GetById(123)).Returns(() => null);
var publishedSnapshot = Mock.Of<IPublishedSnapshotService>();
var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<DataEditor>()));
var usersController = new ContentController(publishedSnapshot, propertyEditorCollection);
Container.InjectProperties(usersController);
return usersController;
}
var runner = new TestRunner(Factory);
var response = await runner.Execute("Content", "PostSave", HttpMethod.Post,
content: GetMultiPartRequestContent(PublishJson1),
mediaTypeHeader: new MediaTypeWithQualityHeaderValue("multipart/form-data"),
assertOkResponse: false);
Assert.AreEqual(HttpStatusCode.NotFound, response.Item1.StatusCode);
//var obj = JsonConvert.DeserializeObject<PagedResult<UserDisplay>>(response.Item2);
//Assert.AreEqual(0, obj.TotalItems);
}
}
}
+1 -30
View File
@@ -3,37 +3,8 @@
"browser": true
},
"plugins": [
"angular"
],
"rules": {
"eqeqeq": 2,
"curly": 2,
"no-unused-vars": 1,
"no-eval": 1,
"no-delete-var": 1,
"quotes": 1,
"dot-notation": 1,
"no-use-before-define": 0,
"angular/ng_controller_as": 1,
"angular/ng_controller_as_vm": 1,
"strict": 0,
"no-irregular-whitespace": 0,
"no-mixed-spaces-and-tabs": 0,
"no-multi-spaces": 0,
"key-spacing": 0,
"semi-spacing": 0,
"space-infix-ops": 0,
"comma-spacing": 0,
"no-trailing-spaces": 0,
"eol-last": 0,
"no-underscore-dangle": 0,
"camelcase": 0
"comma-dangle": ["error", "never"]
},
"globals": {
+39 -35
View File
@@ -25,12 +25,14 @@
"angular-i18n": "~1.7.2",
"signalr": "^2.2.1",
"typeahead.js": "~0.10.5",
"underscore": "~1.7.0",
"underscore": "~1.9.1",
"rgrove-lazyload": "*",
"bootstrap-social": "~4.8.0",
"jquery": "2.2.4",
"jquery-ui": "~1.12.0",
"jquery-migrate": "1.4.0",
"jquery-validate": "~1.17.0",
"jquery-validation-unobtrusive": "3.2.10",
"angular-dynamic-locale": "~0.1.36",
"ng-file-upload": "~12.2.13",
"tinymce": "~4.7.1",
@@ -41,7 +43,7 @@
"clipboard": "~2.0.0",
"font-awesome": "~4.2",
"animejs": "^2.2.0",
"angular-ui-sortable": "0.14.3",
"angular-ui-sortable": "0.14.4",
"angular-messages": "^1.7.2"
},
"install": {
@@ -53,39 +55,41 @@
"ace-builds"
],
"sources": {
"moment": [
"bower_components/moment/min/moment.min.js",
"bower_components/moment/min/moment-with-locales.js",
"bower_components/moment/min/moment-with-locales.min.js",
"bower_components/moment/locale/*.js"
],
"underscore": [
"bower_components/underscore/underscore-min.js",
"bower_components/underscore/underscore-min.map"
],
"jquery": [
"bower_components/jquery/dist/jquery.min.js",
"bower_components/jquery/dist/jquery.min.map"
],
"angular-dynamic-locale": [
"bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js",
"bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js.map"
],
"angular-local-storage": [
"bower_components/angular-local-storage/dist/angular-local-storage.min.js",
"bower_components/angular-local-storage/dist/angular-local-storage.min.js.map"
],
"tinymce": [
"bower_components/tinymce/tinymce.min.js"
],
"angular-i18n": "bower_components/angular-i18n/angular-locale_*.js",
"typeahead.js": "bower_components/typeahead.js/dist/typeahead.bundle.min.js",
"rgrove-lazyload": "bower_components/rgrove-lazyload/lazyload.js",
"ng-file-upload": "bower_components/ng-file-upload/ng-file-upload.min.js",
"jquery-ui": "bower_components/jquery-ui/jquery-ui.min.js",
"jquery-migrate": "bower_components/jquery-migrate/jquery-migrate.min.js",
"clipboard": "bower_components/clipboard/dist/clipboard.min.js",
"animejs": "bower_components/animejs/anime.min.js"
"moment": [
"bower_components/moment/min/moment.min.js",
"bower_components/moment/min/moment-with-locales.js",
"bower_components/moment/min/moment-with-locales.min.js",
"bower_components/moment/locale/*.js"
],
"underscore": [
"bower_components/underscore/underscore-min.js",
"bower_components/underscore/underscore-min.map"
],
"jquery": [
"bower_components/jquery/dist/jquery.min.js",
"bower_components/jquery/dist/jquery.min.map"
],
"angular-dynamic-locale": [
"bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js",
"bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js.map"
],
"angular-local-storage": [
"bower_components/angular-local-storage/dist/angular-local-storage.min.js",
"bower_components/angular-local-storage/dist/angular-local-storage.min.js.map"
],
"tinymce": [
"bower_components/tinymce/tinymce.min.js"
],
"angular-i18n": "bower_components/angular-i18n/angular-locale_*.js",
"typeahead.js": "bower_components/typeahead.js/dist/typeahead.bundle.min.js",
"rgrove-lazyload": "bower_components/rgrove-lazyload/lazyload.js",
"ng-file-upload": "bower_components/ng-file-upload/ng-file-upload.min.js",
"jquery-ui": "bower_components/jquery-ui/jquery-ui.min.js",
"jquery-migrate": "bower_components/jquery-migrate/jquery-migrate.min.js",
"clipboard": "bower_components/clipboard/dist/clipboard.min.js",
"animejs": "bower_components/animejs/anime.min.js",
"jquery-validate": "bower_components/jquery-validate/dist/jquery.validate.min.js",
"jquery-validation-unobtrusive": "bower_components/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"
}
},
"devDependencies": {
+8
View File
@@ -12,6 +12,9 @@ const imagemin = require('gulp-imagemin');
var _ = require('lodash');
var MergeStream = require('merge-stream');
// js
const eslint = require('gulp-eslint');
//Less + css
var postcss = require('gulp-postcss');
var less = require('gulp-less');
@@ -30,6 +33,11 @@ Helper functions
function processJs(files, out) {
return gulp.src(files)
// check for js errors
.pipe(eslint())
// outputs the lint results to the console
.pipe(eslint.format())
// sort files in stream by path or any custom sort comparator
.pipe(sort())
.pipe(concat(out))
.pipe(wrap('(function(){\n%= body %\n})();'))
@@ -329,10 +329,15 @@
/** Converts a string/integer/bool to true/false */
Object.toBoolean = function (obj) {
if (obj === undefined || obj === null) {
return false;
}
if ((typeof obj) === "boolean") {
return obj;
}
if (obj === "1" || obj === 1 || obj === "true") {
if (obj === "1" || obj === 1 || obj.toString().toLowerCase() === "true") {
return true;
}
return false;
+1376 -553
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -27,6 +27,7 @@
"gulp": "^3.9.1",
"gulp-concat": "^2.6.0",
"gulp-connect": "5.0.0",
"gulp-eslint": "^5.0.0",
"gulp-imagemin": "^4.1.0",
"gulp-less": "^3.5.0",
"gulp-ngdocs": "^0.3.0",
@@ -1,3 +0,0 @@
#Obsolete directives
Folder contains directives we plan to remove in the next major version of umbraco (8.0) these are not recommended to use.
@@ -1,40 +0,0 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:autoScale
* @element div
* @deprecated
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
* @function
* @description
* Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set
* So if you only want to scale the div to 70 pixels from the bottom you pass "70"
* @example
* <example module="umbraco.directives">
* <file name="index.html">
* <div auto-scale="70" class="input-block-level"></div>
* </file>
* </example>
**/
angular.module("umbraco.directives")
.directive('autoScale', function ($window) {
return function (scope, el, attrs) {
var totalOffset = 0;
var offsety = parseInt(attrs.autoScale, 10);
var window = angular.element($window);
if (offsety !== undefined){
totalOffset += offsety;
}
setTimeout(function () {
el.height(window.height() - (el.offset().top + totalOffset));
}, 500);
window.bind("resize", function () {
el.height(window.height() - (el.offset().top + totalOffset));
});
};
});
@@ -1,83 +0,0 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:detectFold
* @deprecated
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
* @description This is used for the editor buttons to ensure they are displayed correctly if the horizontal overflow of the editor
* exceeds the height of the window
**/
angular.module("umbraco.directives.html")
.directive('detectFold', function ($timeout, $log, windowResizeListener) {
return {
require: "^?umbTabs",
restrict: 'A',
link: function (scope, el, attrs, tabsCtrl) {
var firstRun = false;
var parent = $(".umb-panel-body");
var winHeight = $(window).height();
var calculate = function () {
if (el && el.is(":visible") && !el.hasClass("umb-bottom-bar")) {
//now that the element is visible, set the flag in a couple of seconds,
// this will ensure that loading time of a current tab get's completed and that
// we eventually stop watching to save on CPU time
$timeout(function() {
firstRun = true;
}, 4000);
//var parent = el.parent();
var hasOverflow = parent.innerHeight() < parent[0].scrollHeight;
//var belowFold = (el.offset().top + el.height()) > winHeight;
if (hasOverflow) {
el.addClass("umb-bottom-bar");
//I wish we didn't have to put this logic here but unfortunately we
// do. This needs to calculate the left offest to place the bottom bar
// depending on if the left column splitter has been moved by the user
// (based on the nav-resize directive)
var wrapper = $("#mainwrapper");
var contentPanel = $("#leftcolumn").next();
var contentPanelLeftPx = contentPanel.css("left");
el.css({ left: contentPanelLeftPx });
}
}
return firstRun;
};
var resizeCallback = function(size) {
winHeight = size.height;
el.removeClass("umb-bottom-bar");
calculate();
};
windowResizeListener.register(resizeCallback);
//Only execute the watcher if this tab is the active (first) tab on load, otherwise there's no reason to execute
// the watcher since it will be recalculated when the tab changes!
if (el.closest(".umb-tab-pane").index() === 0) {
//run a watcher to ensure that the calculation occurs until it's firstRun but ensure
// the calculations are throttled to save a bit of CPU
var listener = scope.$watch(_.throttle(calculate, 1000), function (newVal, oldVal) {
if (newVal !== oldVal) {
listener();
}
});
}
//listen for tab changes
if (tabsCtrl != null) {
tabsCtrl.onTabShown(function (args) {
calculate();
});
}
//ensure to unregister
scope.$on('$destroy', function() {
windowResizeListener.unregister(resizeCallback);
});
}
};
});
@@ -1,69 +0,0 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbItemSorter
* @deprecated
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
* @function
* @element ANY
* @restrict E
* @description A re-usable directive for sorting items
**/
function umbItemSorter(angularHelper) {
return {
scope: {
model: "="
},
restrict: "E", // restrict to an element
replace: true, // replace the html element with the template
templateUrl: 'views/directives/_obsolete/umb-item-sorter.html',
link: function(scope, element, attrs, ctrl) {
var defaultModel = {
okButton: "Ok",
successMsg: "Sorting successful",
complete: false
};
//assign user vals to default
angular.extend(defaultModel, scope.model);
//re-assign merged to user
scope.model = defaultModel;
scope.performSort = function() {
scope.$emit("umbItemSorter.sorting", {
sortedItems: scope.model.itemsToSort
});
};
scope.handleCancel = function () {
scope.$emit("umbItemSorter.cancel");
};
scope.handleOk = function() {
scope.$emit("umbItemSorter.ok");
};
//defines the options for the jquery sortable
scope.sortableOptions = {
axis: 'y',
cursor: "move",
placeholder: "ui-sortable-placeholder",
update: function (ev, ui) {
//highlight the item when the position is changed
$(ui.item).effect("highlight", { color: "#049cdb" }, 500);
},
stop: function (ev, ui) {
//the ui-sortable directive already ensures that our list is re-sorted, so now we just
// need to update the sortOrder to the index of each item
angularHelper.safeApply(scope, function () {
angular.forEach(scope.itemsToSort, function (val, index) {
val.sortOrder = index + 1;
});
});
}
};
}
};
}
angular.module('umbraco.directives').directive("umbItemSorter", umbItemSorter);
@@ -1,92 +0,0 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbContentName
* @deprecated
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
* @restrict E
* @function
* @description
* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form.
**/
angular.module("umbraco.directives")
.directive('umbContentName', function ($timeout, localizationService) {
return {
require: "ngModel",
restrict: 'E',
replace: true,
templateUrl: 'views/directives/_obsolete/umb-content-name.html',
scope: {
placeholder: '@placeholder',
model: '=ngModel',
ngDisabled: '='
},
link: function(scope, element, attrs, ngModel) {
var inputElement = element.find("input");
if(scope.placeholder && scope.placeholder[0] === "@"){
localizationService.localize(scope.placeholder.substring(1))
.then(function(value){
scope.placeholder = value;
});
}
var mX, mY, distance;
function calculateDistance(elem, mouseX, mouseY) {
var cx = Math.max(Math.min(mouseX, elem.offset().left + elem.width()), elem.offset().left);
var cy = Math.max(Math.min(mouseY, elem.offset().top + elem.height()), elem.offset().top);
return Math.sqrt((mouseX - cx) * (mouseX - cx) + (mouseY - cy) * (mouseY - cy));
}
var mouseMoveDebounce = _.throttle(function (e) {
mX = e.pageX;
mY = e.pageY;
// not focused and not over element
if (!inputElement.is(":focus") && !inputElement.hasClass("ng-invalid")) {
// on page
if (mX >= inputElement.offset().left) {
distance = calculateDistance(inputElement, mX, mY);
if (distance <= 155) {
distance = 1 - (100 / 150 * distance / 100);
inputElement.css("border", "1px solid rgba(175,175,175, " + distance + ")");
inputElement.css("background-color", "rgba(255,255,255, " + distance + ")");
}
}
}
}, 15);
$(document).bind("mousemove", mouseMoveDebounce);
$timeout(function(){
if(!scope.model){
scope.goEdit();
}
}, 100, false);
scope.goEdit = function(){
scope.editMode = true;
$timeout(function () {
inputElement.focus();
}, 100, false);
};
scope.exitEdit = function(){
if(scope.model && scope.model !== ""){
scope.editMode = false;
}
};
//unbind doc event!
scope.$on('$destroy', function () {
$(document).unbind("mousemove", mouseMoveDebounce);
});
}
};
});
@@ -1,64 +0,0 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbHeader
* @deprecated
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
* @restrict E
* @function
* @description
* The header on an editor that contains tabs using bootstrap tabs - THIS IS OBSOLETE, use umbTabHeader instead
**/
angular.module("umbraco.directives")
.directive('umbHeader', function ($parse, $timeout) {
return {
restrict: 'E',
replace: true,
transclude: 'true',
templateUrl: 'views/directives/_obsolete/umb-header.html',
//create a new isolated scope assigning a tabs property from the attribute 'tabs'
//which is bound to the parent scope property passed in
scope: {
tabs: "="
},
link: function (scope, iElement, iAttrs) {
scope.showTabs = iAttrs.tabs ? true : false;
scope.visibleTabs = [];
//since tabs are loaded async, we need to put a watch on them to determine
// when they are loaded, then we can close the watch
var tabWatch = scope.$watch("tabs", function (newValue, oldValue) {
angular.forEach(newValue, function(val, index){
var tab = {id: val.id, label: val.label};
scope.visibleTabs.push(tab);
});
//don't process if we cannot or have already done so
if (!newValue) {return;}
if (!newValue.length || newValue.length === 0){return;}
//we need to do a timeout here so that the current sync operation can complete
// and update the UI, then this will fire and the UI elements will be available.
$timeout(function () {
//use bootstrap tabs API to show the first one
iElement.find(".nav-tabs a:first").tab('show');
//enable the tab drop
iElement.find('.nav-pills, .nav-tabs').tabdrop();
//ensure to destroy tabdrop (unbinds window resize listeners)
scope.$on('$destroy', function () {
iElement.find('.nav-pills, .nav-tabs').tabdrop("destroy");
});
//stop watching now
tabWatch();
}, 200);
});
}
};
});
@@ -1,19 +0,0 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbLogin
* @deprecated
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
* @function
* @element ANY
* @restrict E
**/
function loginDirective() {
return {
restrict: "E", // restrict to an element
replace: true, // replace the html element with the template
templateUrl: 'views/directives/_obsolete/umb-login.html'
};
}
angular.module('umbraco.directives').directive("umbLogin", loginDirective);
@@ -1,49 +0,0 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbOptionsMenu
* @deprecated
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
* @function
* @element ANY
* @restrict E
**/
angular.module("umbraco.directives")
.directive('umbOptionsMenu', function ($injector, treeService, navigationService, umbModelMapper, appState) {
return {
scope: {
currentSection: "@",
currentNode: "="
},
restrict: 'E',
replace: true,
templateUrl: 'views/directives/_obsolete/umb-optionsmenu.html',
link: function (scope, element, attrs, ctrl) {
//adds a handler to the context menu item click, we need to handle this differently
//depending on what the menu item is supposed to do.
scope.executeMenuItem = function (action) {
navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection);
};
//callback method to go and get the options async
scope.getOptions = function () {
if (!scope.currentNode) {
return;
}
//when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu)
appState.setMenuState("currentNode", scope.currentNode);
if (!scope.actions) {
treeService.getMenu({ treeNode: scope.currentNode })
.then(function (data) {
scope.actions = data.menuItems;
});
}
};
}
};
});
@@ -1,70 +0,0 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbPhotoFolder
* @deprecated
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
* @restrict E
**/
angular.module("umbraco.directives.html")
.directive('umbPhotoFolder', function($compile, $log, $timeout, $filter, umbPhotoFolderHelper) {
return {
restrict: 'E',
replace: true,
require: '?ngModel',
terminate: true,
templateUrl: 'views/directives/_obsolete/umb-photo-folder.html',
link: function(scope, element, attrs, ngModel) {
var lastWatch = null;
ngModel.$render = function() {
if (ngModel.$modelValue) {
$timeout(function() {
var photos = ngModel.$modelValue;
scope.clickHandler = scope.$eval(element.attr('on-click'));
var imagesOnly = element.attr('images-only') === "true";
var margin = element.attr('border') ? parseInt(element.attr('border'), 10) : 5;
var startingIndex = element.attr('baseline') ? parseInt(element.attr('baseline'), 10) : 0;
var minWidth = element.attr('min-width') ? parseInt(element.attr('min-width'), 10) : 420;
var minHeight = element.attr('min-height') ? parseInt(element.attr('min-height'), 10) : 100;
var maxHeight = element.attr('max-height') ? parseInt(element.attr('max-height'), 10) : 300;
var idealImgPerRow = element.attr('ideal-items-per-row') ? parseInt(element.attr('ideal-items-per-row'), 10) : 5;
var fixedRowWidth = Math.max(element.width(), minWidth);
scope.containerStyle = { width: fixedRowWidth + "px" };
scope.rows = umbPhotoFolderHelper.buildGrid(photos, fixedRowWidth, maxHeight, startingIndex, minHeight, idealImgPerRow, margin, imagesOnly);
if (attrs.filterBy) {
//we track the watches that we create, we don't want to create multiple, so clear it
// if it already exists before creating another.
if (lastWatch) {
lastWatch();
}
//TODO: Need to debounce this so it doesn't filter too often!
lastWatch = scope.$watch(attrs.filterBy, function (newVal, oldVal) {
if (newVal && newVal !== oldVal) {
var p = $filter('filter')(photos, newVal, false);
scope.baseline = 0;
var m = umbPhotoFolderHelper.buildGrid(p, fixedRowWidth, maxHeight, startingIndex, minHeight, idealImgPerRow, margin, imagesOnly);
scope.rows = m;
}
});
}
}, 500); //end timeout
} //end if modelValue
}; //end $render
}
};
});
@@ -1,172 +0,0 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbSort
* @deprecated
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
*
* @element div
* @function
*
* @description
* Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set
* So if you only want to scale the div to 70 pixels from the bottom you pass "70"
*
* @example
* <example module="umbraco.directives">
* <file name="index.html">
* <div umb-sort="70" class="input-block-level"></div>
* </file>
* </example>
**/
angular.module("umbraco.directives")
.value('umbSortContextInternal',{})
.directive('umbSort', function($log,umbSortContextInternal) {
return {
require: '?ngModel',
link: function(scope, element, attrs, ngModel) {
var adjustment;
var cfg = scope.$eval(element.attr('umb-sort')) || {};
scope.model = ngModel;
scope.opts = cfg;
scope.opts.containerSelector= cfg.containerSelector || ".umb-" + cfg.group + "-container",
scope.opts.nested= cfg.nested || true,
scope.opts.drop= cfg.drop || true,
scope.opts.drag= cfg.drag || true,
scope.opts.clone = cfg.clone || "<li/>";
scope.opts.mode = cfg.mode || "list";
scope.opts.itemSelectorFull = $.trim(scope.opts.itemPath + " " + scope.opts.itemSelector);
/*
scope.opts.isValidTarget = function(item, container) {
if(container.el.is(".umb-" + scope.opts.group + "-container")){
return true;
}
return false;
};
*/
element.addClass("umb-sort");
element.addClass("umb-" + cfg.group + "-container");
scope.opts.onDrag = function (item, position) {
if(scope.opts.mode === "list"){
item.css({
left: position.left - adjustment.left,
top: position.top - adjustment.top
});
}
};
scope.opts.onDrop = function (item, targetContainer, _super) {
if(scope.opts.mode === "list"){
//list mode
var clonedItem = $(scope.opts.clone).css({height: 0});
item.after(clonedItem);
clonedItem.animate({'height': item.height()});
item.animate(clonedItem.position(), function () {
clonedItem.detach();
_super(item);
});
}
var children = $(scope.opts.itemSelectorFull, targetContainer.el);
var targetIndex = children.index(item);
var targetScope = $(targetContainer.el[0]).scope();
if(targetScope === umbSortContextInternal.sourceScope){
if(umbSortContextInternal.sourceScope.opts.onSortHandler){
var _largs = {
oldIndex: umbSortContextInternal.sourceIndex,
newIndex: targetIndex,
scope: umbSortContextInternal.sourceScope
};
umbSortContextInternal.sourceScope.opts.onSortHandler.call(this, item, _largs);
}
}else{
if(targetScope.opts.onDropHandler){
var args = {
sourceScope: umbSortContextInternal.sourceScope,
sourceIndex: umbSortContextInternal.sourceIndex,
sourceContainer: umbSortContextInternal.sourceContainer,
targetScope: targetScope,
targetIndex: targetIndex,
targetContainer: targetContainer
};
targetScope.opts.onDropHandler.call(this, item, args);
}
if(umbSortContextInternal.sourceScope.opts.onReleaseHandler){
var _args = {
sourceScope: umbSortContextInternal.sourceScope,
sourceIndex: umbSortContextInternal.sourceIndex,
sourceContainer: umbSortContextInternal.sourceContainer,
targetScope: targetScope,
targetIndex: targetIndex,
targetContainer: targetContainer
};
umbSortContextInternal.sourceScope.opts.onReleaseHandler.call(this, item, _args);
}
}
};
scope.changeIndex = function(from, to){
scope.$apply(function(){
var i = ngModel.$modelValue.splice(from, 1)[0];
ngModel.$modelValue.splice(to, 0, i);
});
};
scope.move = function(args){
var from = args.sourceIndex;
var to = args.targetIndex;
if(args.sourceContainer === args.targetContainer){
scope.changeIndex(from, to);
}else{
scope.$apply(function(){
var i = args.sourceScope.model.$modelValue.splice(from, 1)[0];
args.targetScope.model.$modelvalue.splice(to,0, i);
});
}
};
scope.opts.onDragStart = function (item, container, _super) {
var children = $(scope.opts.itemSelectorFull, container.el);
var offset = item.offset();
umbSortContextInternal.sourceIndex = children.index(item);
umbSortContextInternal.sourceScope = $(container.el[0]).scope();
umbSortContextInternal.sourceContainer = container;
//current.item = ngModel.$modelValue.splice(current.index, 1)[0];
var pointer = container.rootGroup.pointer;
adjustment = {
left: pointer.left - offset.left,
top: pointer.top - offset.top
};
_super(item, container);
};
element.sortable( scope.opts );
}
};
});
@@ -1,18 +0,0 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbTabView
* @deprecated
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
*
* @restrict E
**/
angular.module("umbraco.directives")
.directive('umbTabView', function($timeout, $log){
return {
restrict: 'E',
replace: true,
transclude: 'true',
templateUrl: 'views/directives/_obsolete/umb-tab-view.html'
};
});
@@ -1,17 +0,0 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbUploadDropzone
* @deprecated
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
*
* @restrict E
**/
angular.module("umbraco.directives.html")
.directive('umbUploadDropzone', function(){
return {
restrict: 'E',
replace: true,
templateUrl: 'views/directives/_obsolete/umb-upload-dropzone.html'
};
});
@@ -103,7 +103,7 @@
backdropOpacity: "=?",
highlightElement: "=?",
highlightPreventClick: "=?",
disableEventsOnClick: "=?",
disableEventsOnClick: "=?"
}
};
@@ -10,62 +10,62 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se
templateUrl: 'views/components/application/umb-sections.html',
link: function (scope, element, attr, ctrl) {
var sectionItemsWidth = [];
var evts = [];
var maxSections = 7;
var sectionItemsWidth = [];
var evts = [];
var maxSections = 7;
//setup scope vars
scope.maxSections = maxSections;
scope.overflowingSections = 0;
scope.maxSections = maxSections;
scope.overflowingSections = 0;
scope.sections = [];
scope.currentSection = appState.getSectionState("currentSection");
scope.showTray = false; //appState.getGlobalState("showTray");
scope.stickyNavigation = appState.getGlobalState("stickyNavigation");
scope.needTray = false;
function loadSections(){
sectionService.getSectionsForUser()
.then(function (result) {
scope.sections = result;
// store the width of each section so we can hide/show them based on browser width
// we store them because the sections get removed from the dom and then we
// can't tell when to show them gain
$timeout(function(){
$("#applications .sections li").each(function(index) {
sectionItemsWidth.push($(this).outerWidth());
});
});
calculateWidth();
});
}
function calculateWidth(){
$timeout(function(){
//total width minus room for avatar, search, and help icon
var windowWidth = $(window).width()-200;
var sectionsWidth = 0;
scope.totalSections = scope.sections.length;
scope.maxSections = maxSections;
scope.overflowingSections = 0;
scope.needTray = false;
// detect how many sections we can show on the screen
for (var i = 0; i < sectionItemsWidth.length; i++) {
var sectionItemWidth = sectionItemsWidth[i];
sectionsWidth += sectionItemWidth;
function loadSections() {
sectionService.getSectionsForUser()
.then(function (result) {
scope.sections = result;
// store the width of each section so we can hide/show them based on browser width
// we store them because the sections get removed from the dom and then we
// can't tell when to show them gain
$timeout(function () {
$("#applications .sections li").each(function (index) {
sectionItemsWidth.push($(this).outerWidth());
});
});
calculateWidth();
});
}
if(sectionsWidth > windowWidth) {
scope.needTray = true;
scope.maxSections = i - 1;
scope.overflowingSections = scope.maxSections - scope.totalSections;
break;
}
}
});
}
function calculateWidth() {
$timeout(function () {
//total width minus room for avatar, search, and help icon
var windowWidth = $(window).width() - 200;
var sectionsWidth = 0;
scope.totalSections = scope.sections.length;
scope.maxSections = maxSections;
scope.overflowingSections = 0;
scope.needTray = false;
// detect how many sections we can show on the screen
for (var i = 0; i < sectionItemsWidth.length; i++) {
var sectionItemWidth = sectionItemsWidth[i];
sectionsWidth += sectionItemWidth;
if (sectionsWidth > windowWidth) {
scope.needTray = true;
scope.maxSections = i - 1;
scope.overflowingSections = scope.maxSections - scope.totalSections;
break;
}
}
});
}
//Listen for global state changes
evts.push(eventsService.on("appState.globalState.changed", function(e, args) {
evts.push(eventsService.on("appState.globalState.changed", function (e, args) {
if (args.key === "showTray") {
scope.showTray = args.value;
}
@@ -74,44 +74,44 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se
}
}));
evts.push(eventsService.on("appState.sectionState.changed", function(e, args) {
evts.push(eventsService.on("appState.sectionState.changed", function (e, args) {
if (args.key === "currentSection") {
scope.currentSection = args.value;
}
}));
evts.push(eventsService.on("app.reInitialize", function(e, args) {
evts.push(eventsService.on("app.reInitialize", function (e, args) {
//re-load the sections if we're re-initializing (i.e. package installed)
loadSections();
}));
//ensure to unregister from all events!
scope.$on('$destroy', function () {
for (var e in evts) {
eventsService.unsubscribe(evts[e]);
}
});
scope.$on('$destroy', function () {
for (var e in evts) {
eventsService.unsubscribe(evts[e]);
}
});
//on page resize
window.onresize = calculateWidth;
//on page resize
window.onresize = calculateWidth;
scope.sectionClick = function (event, section) {
scope.sectionClick = function (event, section) {
if (event.ctrlKey ||
event.shiftKey ||
event.metaKey || // apple
(event.button && event.button === 1) // middle click, >IE9 + everyone else
) {
return;
}
if (event.ctrlKey ||
event.shiftKey ||
event.metaKey || // apple
(event.button && event.button === 1) // middle click, >IE9 + everyone else
) {
return;
}
if (scope.userDialog) {
closeUserDialog();
}
}
navigationService.hideSearch();
navigationService.showTree(section.alias);
navigationService.hideSearch();
navigationService.showTree(section.alias);
//in some cases the section will have a custom route path specified, if there is one we'll use it
if (section.routePath) {
@@ -123,22 +123,22 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se
$location.path(path);
}
navigationService.clearSearch();
};
scope.sectionDblClick = function(section){
navigationService.reloadSection(section.alias);
};
};
scope.trayClick = function () {
if (appState.getGlobalState("showTray") === true) {
navigationService.hideTray();
} else {
navigationService.showTray();
}
};
scope.sectionDblClick = function (section) {
navigationService.reloadSection(section.alias);
};
loadSections();
scope.trayClick = function () {
if (appState.getGlobalState("showTray") === true) {
navigationService.hideTray();
} else {
navigationService.showTray();
}
};
loadSections();
}
};
@@ -94,6 +94,7 @@ Use this directive to render an umbraco button. The directive can be used to gen
}
});
//TODO: This doesn't seem necessary?
UmbButtonController.$inject = ['$timeout'];
function UmbButtonController($timeout) {
@@ -21,21 +21,18 @@
$scope.page.listViewPath = null;
$scope.page.isNew = $scope.isNew ? true : false;
$scope.page.buttonGroupState = "init";
$scope.page.culture = $scope.culture;
$scope.page.hideActionsMenu = infiniteMode ? true : false;
$scope.page.hideChangeVariant = infiniteMode ? true : false;
$scope.allowOpen = true;
// add all editors to an editors array to support split view
$scope.editors = [];
$scope.splitView = {
"leftIsOpen": true,
"rightIsOpen": false
};
$scope.initVariant = initVariant;
$scope.splitViewChanged = splitViewChanged;
function init(content) {
if(infiniteMode) {
if (infiniteMode) {
createInfiniteModeButtons(content);
} else {
createButtons(content);
@@ -53,7 +50,166 @@
}
}
//init can be called more than once and we don't want to have multiple bound events
bindEvents();
// set first app to active
// We need to track active
$scope.content.apps[0].active = true;
setActiveCulture();
resetVariantFlags();
}
/** This is called when the split view changes based on the umb-variant-content */
function splitViewChanged() {
//send an event downwards
$scope.$broadcast("editors.content.splitViewChanged", { editors: $scope.editors });
}
/**
* This will reset isDirty flags if save is true.
* When working with multiple variants, this will set the save/publish flags of each one to false.
* When working with a single variant, this will set the publish flag to false and the save flag to true.
*/
function resetVariantFlags() {
if ($scope.content.variants.length > 1) {
for (var i = 0; i < $scope.content.variants.length; i++) {
var v = $scope.content.variants[i];
if (v.save) {
v.isDirty = false;
}
v.save = false;
v.publish = false;
}
}
else {
if ($scope.content.variants[0].save) {
$scope.content.variants[0].isDirty = false;
}
$scope.content.variants[0].save = true;
$scope.content.variants[0].publish = false;
}
}
function countDirtyVariants() {
var count = 0;
for (var i = 0; i < $scope.content.variants.length; i++) {
var v = $scope.content.variants[i];
if (v.isDirty) {
count++;
}
}
return count;
}
/** Returns true if the save/publish dialog should be shown when pressing the button */
function showSaveOrPublishDialog() {
return $scope.content.variants.length > 1;
}
/**
* The content item(s) are loaded into an array and this will set the active content item based on the current culture (query string).
* If the content item is invariant, then only one item exists in the array.
*/
function setActiveCulture() {
// set the active variant
var activeVariant = null;
_.each($scope.content.variants, function (v) {
if (v.language && v.language.culture === $scope.culture) {
v.active = true;
activeVariant = v;
}
else {
v.active = false;
}
});
if (!activeVariant) {
// set the first variant to active
$scope.content.variants[0].active = true;
activeVariant = $scope.content.variants[0];
}
initVariant(activeVariant);
//If there are no editors yet then create one with the current content.
//if there's already a main editor then update it with the current content.
if ($scope.editors.length === 0) {
var editor = {
content: activeVariant
};
$scope.editors.push(editor);
}
else {
//this will mean there is only one
$scope.editors[0].content = activeVariant;
if ($scope.editors.length > 1) {
//now re-sync any other editor content (i.e. if split view is open)
for (var s = 1; s < $scope.editors.length; s++) {
//get the variant from the scope model
var variant = _.find($scope.content.variants, function (v) {
return v.language.culture === $scope.editors[s].content.language.culture;
});
$scope.editors[s].content = initVariant(variant);
}
}
}
}
function initVariant(variant) {
//The model that is assigned to the editor contains the current content variant along
//with a copy of the contentApps. This is required because each editor renders it's own
//header and content apps section and the content apps contains the view for editing content itself
//and we need to assign a view model to the subView so that it is scoped to the current
//editor so that split views work.
//copy the apps from the main model if not assigned yet to the variant
if (!variant.apps) {
variant.apps = angular.copy($scope.content.apps);
}
//if this is a variant has a culture/language than we need to assign the language drop down info
if (variant.language) {
//if the variant list that defines the header drop down isn't assigned to the variant then assign it now
if (!variant.variants) {
variant.variants = _.map($scope.content.variants,
function (v) {
return _.pick(v, "active", "language", "state");
});
}
else {
//merge the scope variants on top of the header variants collection (handy when needing to refresh)
angular.extend(variant.variants,
_.map($scope.content.variants,
function (v) {
return _.pick(v, "active", "language", "state");
}));
}
//ensure the current culture is set as the active one
for (var i = 0; i < variant.variants.length; i++) {
if (variant.variants[i].language.culture === variant.language.culture) {
variant.variants[i].active = true;
}
else {
variant.variants[i].active = false;
}
}
}
//then assign the variant to a view model to the content app
var contentApp = _.find(variant.apps, function (a) {
return a.alias === "content";
});
contentApp.viewModel = variant;
return variant;
}
function bindEvents() {
//bindEvents can be called more than once and we don't want to have multiple bound events
for (var e in evts) {
eventsService.unsubscribe(evts[e]);
}
@@ -68,84 +224,19 @@
evts.push(eventsService.on("editors.documentType.saved", function (name, args) {
// if this content item uses the updated doc type we need to reload the content item
if(args && args.documentType && args.documentType.key === content.documentType.key) {
if ($scope.page.culture) {
loadContent($scope.page.culture);
}
else {
loadContent();
}
if (args && args.documentType && args.documentType.key === content.documentType.key) {
loadContent();
}
}));
// We don't get the info tab from the server from version 7.8 so we need to manually add it
//contentEditingHelper.addInfoTab($scope.content.tabs);
// prototype content and info apps
var contentApp = {
"name": "Content",
"alias": "content",
"icon": "icon-document",
"view": "views/content/apps/content/content.html"
};
var infoApp = {
"name": "Info",
"alias": "info",
"icon": "icon-info",
"view": "views/content/apps/info/info.html"
};
var listview = {
"name": "Child items",
"alias": "childItems",
"icon": "icon-list",
"view": "views/content/apps/listview/listview.html"
};
$scope.content.apps = [];
if ($scope.content.isContainer) {
// add list view app
$scope.content.apps.push(listview);
// remove the list view tab
angular.forEach($scope.content.tabs, function (tab, index) {
if (tab.alias === "umbContainerView") {
tab.hide = true;
}
});
}
$scope.content.apps.push(contentApp);
$scope.content.apps.push(infoApp);
// set first app to active
$scope.content.apps[0].active = true;
// create new editor for split view
if ($scope.editors.length === 0) {
var editor = {
content: $scope.content
};
$scope.editors.push(editor);
}
else if ($scope.editors.length === 1) {
$scope.editors[0].content = $scope.content;
}
else {
//fixme - need to fix something here if we are re-loading a content item that is in a split view
}
}
/**
* This does the content loading and initializes everything, called on load and changing variants
* @param {any} culture
* This does the content loading and initializes everything, called on first load
*/
function loadContent(culture) {
function loadContent() {
//we are editing so get the content item from the server
return $scope.getMethod()($scope.contentId, culture)
return $scope.getMethod()($scope.contentId)
.then(function (data) {
$scope.content = data;
@@ -158,14 +249,8 @@
init($scope.content);
//in one particular special case, after we've created a new item we redirect back to the edit
// route but there might be server validation errors in the collection which we need to display
// after the redirect, so we will bind all subscriptions which will show the server validation errors
// if there are any and then clear them so the collection no longer persists them.
serverValidationManager.executeAndClearAllSubscriptions();
if(!infiniteMode) {
syncTreeNode($scope.content, data.path, true);
if (!infiniteMode) {
syncTreeNode($scope.content, true);
}
resetLastListPageNumber($scope.content);
@@ -204,18 +289,20 @@
$scope.page.allowInfiniteSaveAndClose = false;
// check for publish rights
if(_.contains(content.allowedActions, "U")) {
if (_.contains(content.allowedActions, "U")) {
$scope.page.allowInfinitePublishAndClose = true;
// check for save rights
} else if( _.contains(content.allowedActions, "A")) {
// check for save rights
} else if (_.contains(content.allowedActions, "A")) {
$scope.page.allowInfiniteSaveAndClose = true;
}
}
/** Syncs the content item to it's tree node - this occurs on first load and after saving */
function syncTreeNode(content, path, initialLoad) {
function syncTreeNode(content, initialLoad) {
var path = content.path;
if (!$scope.content.isChildOfListView) {
navigationService.syncTree({ tree: $scope.treeAlias, path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) {
@@ -248,13 +335,14 @@
saveMethod: args.saveMethod,
scope: $scope,
content: $scope.content,
action: args.action
action: args.action,
showNotifications: args.showNotifications
}).then(function (data) {
//success
init($scope.content);
if(!infiniteMode) {
syncTreeNode($scope.content, data.path);
if (!infiniteMode) {
syncTreeNode($scope.content);
}
$scope.page.buttonGroupState = "success";
@@ -264,12 +352,17 @@
return $q.when(data);
},
function (err) {
setActiveCulture();
syncTreeNode($scope.content);
//error
if (err) {
editorState.set($scope.content);
}
$scope.page.buttonGroupState = "error";
return $q.reject(err);
});
}
@@ -305,18 +398,11 @@
}
else {
//Browse content nodes based on the selected tree language variant
$scope.page.loading = true;
if ($scope.page.culture) {
loadContent($scope.page.culture).then(function(){
$scope.page.loading = false;
});
}
else {
loadContent().then(function(){
$scope.page.loading = false;
});
}
loadContent().then(function () {
$scope.page.loading = false;
});
}
$scope.unPublish = function () {
@@ -326,15 +412,15 @@
if ($scope.content.variants.length > 0) {
_.each($scope.content.variants,
function (d) {
//set the culture if this is current
if (d.current === true) {
//set the culture if this is active
if (d.active === true) {
culture = d.language.culture;
}
});
}
if (formHelper.submitForm({ scope: $scope, skipValidation: true })) {
$scope.page.buttonGroupState = "busy";
eventsService.emit("content.unpublishing", { content: $scope.content });
@@ -352,8 +438,8 @@
init($scope.content);
if(!infiniteMode) {
syncTreeNode($scope.content, data.path);
if (!infiniteMode) {
syncTreeNode($scope.content);
}
$scope.page.buttonGroupState = "success";
@@ -374,12 +460,12 @@
$scope.saveAndPublish = function () {
// TODO: Add "..." to publish button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant
if ($scope.content.variants.length > 1) {
if (showSaveOrPublishDialog()) {
//before we launch the dialog we want to execute all client side validations first
if (formHelper.submitForm({ scope: $scope, action: "publish"})) {
if (formHelper.submitForm({ scope: $scope, action: "publish" })) {
var dialog = {
view: "publish",
view: "views/content/overlays/publish.html",
variants: $scope.content.variants, //set a model property for the dialog
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
submitButtonLabel: "Publish",
@@ -389,7 +475,8 @@
//we need to return this promise so that the dialog can handle the result and wire up the validation response
return performSave({
saveMethod: contentResource.publish,
action: "publish"
action: "publish",
showNotifications: false
}).then(function (data) {
overlayService.close();
return $q.when(data);
@@ -399,21 +486,6 @@
//re-map the dialog model since we've re-bound the properties
dialog.variants = $scope.content.variants;
//check the error list for specific variant errors, if none exist that means that only server side validation
//for the current variant's properties failed, in this case we want to close the publish dialog since the user
//will need to fix validation errors on the properties
if (err.data && err.data.ModelState) {
var keys = _.keys(err.data.ModelState);
var foundVariantError = _.find(keys,
function (k) {
return k.startsWith("publish_variant_");
});
if (!foundVariantError) {
//no variant errors, close the dialog
overlayService.close();
}
}
return $q.reject(err);
});
},
@@ -426,12 +498,56 @@
}
}
else {
return performSave({ saveMethod: contentResource.publish, action: "publish" }).catch(angular.noop);;
//ensure the publish flag is set
$scope.content.variants[0].publish = true;
return performSave({ saveMethod: contentResource.publish, action: "publish" });
}
};
$scope.save = function () {
return performSave({ saveMethod: $scope.saveMethod(), action: "save" }).catch(angular.noop);
// TODO: Add "..." to save button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant
if (showSaveOrPublishDialog()) {
//before we launch the dialog we want to execute all client side validations first
if (formHelper.submitForm({ scope: $scope, action: "save" })) {
var dialog = {
view: "views/content/overlays/save.html",
variants: $scope.content.variants, //set a model property for the dialog
skipFormValidation: true, //when submitting the overlay form, skip any client side validation
submitButtonLabel: "Save",
submit: function (model) {
model.submitButtonState = "busy";
//we need to return this promise so that the dialog can handle the result and wire up the validation response
return performSave({
saveMethod: $scope.saveMethod(),
action: "save",
showNotifications: false
}).then(function (data) {
overlayService.close();
return $q.when(data);
},
function (err) {
model.submitButtonState = "error";
//re-map the dialog model since we've re-bound the properties
dialog.variants = $scope.content.variants;
return $q.reject(err);
});
},
close: function (oldModel) {
overlayService.close();
}
};
overlayService.open(dialog);
}
}
else {
return performSave({ saveMethod: $scope.saveMethod(), action: "save" });
}
};
$scope.preview = function (content) {
@@ -455,7 +571,7 @@
else {
$scope.save().then(function (data) {
previewWindow.location.href = redirect;
}).catch(angular.noop);
});
}
}
};
@@ -509,69 +625,29 @@
notificationsService.error(error.headline, error.content);
});
};
$scope.closeSplitView = function (index, editor) {
// hacky animation stuff - it will be much better when angular is upgraded
editor.loading = true;
editor.collapsed = true;
$timeout(function () {
$scope.editors.splice(index, 1);
}, 400);
};
$scope.openInSplitView = function (selectedVariant) {
console.log(selectedVariant);
var editor = {};
// hacking animation states - these should hopefully be easier to do when we upgrade angular
editor.collapsed = true;
editor.loading = true;
$scope.editors.push(editor);
var editorIndex = $scope.editors.length - 1;
$timeout(function () {
$scope.editors[editorIndex].collapsed = false;
}, 100);
// fake loading of content
// TODO: Make this real, but how do we deal with saving since currently we only save one variant at a time?!
$timeout(function () {
$scope.editors[editorIndex].content = angular.copy($scope.content);
$scope.editors[editorIndex].content.name = "What a variant";
// set selected variant on split view content
angular.forEach($scope.editors[editorIndex].content.variants, function (variant) {
if (variant.culture === selectedVariant.culture) {
variant.current = true;
} else {
variant.current = false;
}
});
$scope.editors[editorIndex].loading = false;
}, 500);
};
/* publish method used in infinite editing */
$scope.publishAndClose = function(content) {
$scope.publishAndClose = function (content) {
$scope.publishAndCloseButtonState = "busy";
performSave({ saveMethod: contentResource.publish, action: "publish" }).then(function(){
if($scope.infiniteModel.submit) {
performSave({ saveMethod: contentResource.publish, action: "publish" }).then(function () {
if ($scope.infiniteModel.submit) {
$scope.infiniteModel.contentNode = content;
$scope.infiniteModel.submit($scope.infiniteModel);
}
$scope.publishAndCloseButtonState = "success";
}).catch(angular.noop);;
});
};
/* save method used in infinite editing */
$scope.saveAndClose = function(content) {
$scope.saveAndClose = function (content) {
$scope.saveAndCloseButtonState = "busy";
performSave({ saveMethod: $scope.saveMethod(), action: "save" }).then(function(){
if($scope.infiniteModel.submit) {
performSave({ saveMethod: $scope.saveMethod(), action: "save" }).then(function () {
if ($scope.infiniteModel.submit) {
$scope.infiniteModel.contentNode = content;
$scope.infiniteModel.submit($scope.infiniteModel);
}
$scope.saveAndCloseButtonState = "success";
}).catch(angular.noop);;
});
};
function moveNode(node, target) {
@@ -585,7 +661,7 @@
}
// sync the destination node
if(!infiniteMode) {
if (!infiniteMode) {
navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false });
}
@@ -603,17 +679,28 @@
}
// methods for infinite editing
$scope.close = function() {
if($scope.infiniteModel.close) {
$scope.close = function () {
if ($scope.infiniteModel.close) {
$scope.infiniteModel.close($scope.infiniteModel);
}
};
$scope.$watch('culture', function (newVal, oldVal) {
if (newVal !== oldVal) {
setActiveCulture();
}
});
//ensure to unregister from all events!
$scope.$on('$destroy', function () {
for (var e in evts) {
eventsService.unsubscribe(evts[e]);
}
//since we are not notifying and clearing server validation messages when they are received due to how the variant
//switching works, we need to ensure they are cleared when this editor is destroyed
if (!$scope.page.isNew) {
serverValidationManager.clear();
}
});
}
@@ -8,10 +8,11 @@
var evts = [];
var isInfoTab = false;
var labels = {};
scope.publishStatus = {};
scope.publishStatus = [];
scope.disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates;
scope.allowChangeDocumentType = false;
scope.allowChangeTemplate = false;
function onInit() {
@@ -20,6 +21,7 @@
angular.forEach(user.sections, function(section){
if(section.alias === "settings") {
scope.allowChangeDocumentType = true;
scope.allowChangeTemplate = true;
}
});
});
@@ -28,15 +30,17 @@
"general_deleted",
"content_unpublished",
"content_published",
"content_publishedPendingChanges"
"content_publishedPendingChanges",
"content_notCreated"
];
localizationService.localizeMany(keys)
.then(function(data){
labels.deleted = data[0];
labels.unpublished = data[1];
labels.unpublished = data[1]; //aka draft
labels.published = data[2];
labels.publishedPendingChanges = data[3];
labels.notCreated = data[4];
setNodePublishStatus(scope.node);
@@ -92,15 +96,21 @@
};
scope.openTemplate = function () {
var url = "/settings/templates/edit/" + scope.node.templateId;
$location.url(url);
var templateEditor = {
id: scope.node.templateId,
submit: function(model) {
editorService.close();
},
close: function() {
editorService.close();
}
};
editorService.templateEditor(templateEditor);
}
scope.updateTemplate = function (templateAlias) {
// update template value
scope.node.template = templateAlias;
};
scope.datePickerChange = function (event, type) {
@@ -163,31 +173,50 @@
}
function setNodePublishStatus(node) {
scope.publishStatus = [];
// deleted node
if(node.trashed === true) {
scope.publishStatus.label = labels.deleted;
scope.publishStatus.color = "danger";
if (node.trashed === true) {
scope.publishStatus.push({
label: labels.deleted,
color: "danger"
});
return;
}
// unpublished node
if(node.published === false && node.trashed === false) {
scope.publishStatus.label = labels.unpublished;
scope.publishStatus.color = "gray";
}
if (node.variants) {
for (var i = 0; i < node.variants.length; i++) {
// published node
if(node.publishDate && node.published === true) {
scope.publishStatus.label = labels.published;
scope.publishStatus.color = "success";
}
var variant = node.variants[i];
// published node with pending changes
if (node.edited === true && node.publishDate) {
scope.publishStatus.label = labels.publishedPendingChanges;
scope.publishStatus.color = "success"
}
var status = {
culture: variant.language ? variant.language.culture : null
};
if (variant.state === "NotCreated") {
status.label = labels.notCreated;
status.color = "gray";
}
else if (variant.state === "Draft") {
// draft node
status.label = labels.unpublished;
status.color = "gray";
}
else if (variant.state === "Published") {
// published node
status.label = labels.published;
status.color = "success";
}
else if (variant.state === "PublishedPendingChanges") {
// published node with pending changes
status.label = labels.publishedPendingChanges;
status.color = "success";
}
scope.publishStatus.push(status);
}
}
}
function setPublishDate(date) {
@@ -293,7 +322,7 @@
});
}));
// watch for content updates - reload content when node is saved, published etc.
// watch for content state updates
scope.$watch('node.updateDate', function(newValue, oldValue){
if(!newValue) { return; }
@@ -0,0 +1,45 @@
(function () {
'use strict';
/** This directive is used to render out the current variant tabs and properties and exposes an API for other directives to consume */
function tabbedContentDirective() {
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'views/components/content/umb-tabbed-content.html',
controller: function ($scope) {
//expose the property/methods for other directives to use
this.content = $scope.content;
$scope.$watch("tabbedContentForm.$dirty",
function (newValue, oldValue) {
if (newValue === true) {
$scope.content.isDirty = true;
}
});
},
link: function(scope) {
function onInit() {
angular.forEach(scope.content.tabs, function (group) {
group.open = true;
});
}
onInit();
},
scope: {
content: "="
}
};
return directive;
}
angular.module('umbraco.directives').directive('umbTabbedContent', tabbedContentDirective);
})();
@@ -0,0 +1,134 @@
(function () {
'use strict';
/**
* A directive to encapsulate each variant editor which includes the name header and all content apps for a given variant
* @param {any} $timeout
* @param {any} $location
*/
function variantContentDirective($timeout, $location) {
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'views/components/content/umb-variant-content.html',
link: function (scope) {
/**
* Adds a new editor to the editors array to show content in a split view
* @param {any} selectedVariant
*/
scope.openInSplitView = function (selectedVariant) {
var selectedCulture = selectedVariant.language.culture;
//only the content app can be selected since no other apps are shown, and because we copy all of these apps
//to the "editors" we need to update this across all editors
for (var e = 0; e < scope.editors.length; e++) {
var editor = scope.editors[e];
for (var i = 0; i < editor.content.apps.length; i++) {
var app = editor.content.apps[i];
if (app.alias === "content") {
app.active = true;
}
else {
app.active = false;
}
}
}
//Find the whole variant model based on the culture that was chosen
var variant = _.find(scope.content.variants, function (v) {
return v.language.culture === selectedCulture;
});
var editor = {
content: scope.initVariant({ variant: variant})
};
scope.editors.push(editor);
//TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular
editor.collapsed = true;
editor.loading = true;
$timeout(function () {
editor.collapsed = false;
editor.loading = false;
scope.onSplitViewChanged();
}, 100);
};
/**
* Changes the currently selected variant
* @param {any} variantDropDownItem
*/
scope.selectVariant = function (variantDropDownItem) {
var editorIndex = _.findIndex(scope.editors, function (e) {
return e === scope.editor;
});
//if the editor index is zero, then update the query string to track the lang selection, otherwise if it's part
//of a 2nd split view editor then update the model directly.
if (editorIndex === 0) {
//if we've made it this far, then update the query string
$location.search("cculture", variantDropDownItem.language.culture);
}
else {
//set all variant drop down items as inactive for this editor and then set the selected on as active
for (var i = 0; i < scope.editor.content.variants.length; i++) {
scope.editor.content.variants[i].active = false;
}
variantDropDownItem.active = true;
//get the variant content model and initialize the editor with that
var variant = _.find(scope.content.variants, function (v) {
return v.language.culture === variantDropDownItem.language.culture;
});
scope.editor.content = scope.initVariant({ variant: variant });
}
};
/** Closes the split view */
scope.closeSplitView = function () {
//TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular
scope.editor.loading = true;
scope.editor.collapsed = true;
$timeout(function () {
var index = _.findIndex(scope.editors, function(e) {
return e === scope.editor;
});
scope.editors.splice(index, 1);
scope.onSplitViewChanged();
}, 400);
};
//set the content to dirty if the header changes
scope.$watch("contentHeaderForm.$dirty",
function (newValue, oldValue) {
if (newValue === true) {
scope.editor.content.isDirty = true;
}
});
},
scope: {
//TODO: This should be turned into a proper component
page: "=",
content: "=",
editor: "=",
editors: "=",
//TODO: I don't like having this callback defined and would like to make this directive a bit less
// coupled but right now don't have time
initVariant: "&",
onSplitViewChanged: "&"
}
};
return directive;
}
angular.module('umbraco.directives').directive('umbVariantContent', variantContentDirective);
})();
@@ -0,0 +1,22 @@
(function () {
'use strict';
function umbVariantStateController($scope, $element) {
var vm = this;
}
var umbVariantStateComponent = {
templateUrl: 'views/components/content/umb-variant-state.html',
bindings: {
variant: "<"
},
controllerAs: 'vm',
controller: umbVariantStateController
};
angular.module("umbraco.directives")
.component('umbVariantState', umbVariantStateComponent);
})();
@@ -37,34 +37,35 @@ Use this directive to construct a main content area inside the main editor windo
</ul>
**/
(function() {
'use strict';
(function () {
'use strict';
function EditorContainerDirective(overlayHelper) {
function EditorContainerDirective(overlayHelper) {
function link(scope, el, attr, ctrl) {
function link(scope, el, attr, ctrl) {
scope.numberOfOverlays = 0;
scope.numberOfOverlays = 0;
scope.$watch(function(){
return overlayHelper.getNumberOfOverlays();
}, function (newValue) {
scope.numberOfOverlays = newValue;
});
//TODO: this shouldn't be a watch, this should be based on an event handler
scope.$watch(function () {
return overlayHelper.getNumberOfOverlays();
}, function (newValue) {
scope.numberOfOverlays = newValue;
});
}
}
var directive = {
transclude: true,
restrict: 'E',
replace: true,
templateUrl: 'views/components/editor/umb-editor-container.html',
link: link
};
var directive = {
transclude: true,
restrict: 'E',
replace: true,
templateUrl: 'views/components/editor/umb-editor-container.html',
link: link
};
return directive;
}
return directive;
}
angular.module('umbraco.directives').directive('umbEditorContainer', EditorContainerDirective);
angular.module('umbraco.directives').directive('umbEditorContainer', EditorContainerDirective);
})();
@@ -201,67 +201,49 @@ Use this directive to construct a header inside the main editor window.
**/
(function() {
(function () {
'use strict';
function EditorHeaderDirective(iconHelper, $location, editorService) {
function EditorHeaderDirective(iconHelper, editorService, $location, $routeParams) {
function link(scope, el, attr, ctrl) {
scope.vm = {};
scope.vm.dropdownOpen = false;
scope.vm.currentVariant = "";
function onInit() {
setCurrentVariant(scope.variants);
setVariantStatusColor(scope.variants);
setCurrentVariant();
}
function setCurrentVariant(variants) {
angular.forEach(variants, function (variant) {
if(variant.current) {
function setCurrentVariant() {
angular.forEach(scope.variants, function (variant) {
if (variant.active) {
scope.vm.currentVariant = variant;
}
});
}
//TODO: This doesn't really affect any UI currently, need some feedback from mads
function setVariantStatusColor(variants) {
angular.forEach(variants, function (variant) {
//TODO: What about variant.exists? If we are applying colors/styles, this should be one of them
switch (variant.state) {
case "Published":
variant.stateColor = "success";
break;
case "Unpublished":
//TODO: Not sure if these statuses will ever bubble up to the UI?
case "Publishing":
case "Unpublishing":
default:
variant.stateColor = "gray";
}
});
}
scope.goBack = function () {
if (scope.onBack) {
scope.onBack();
$location.path('/' + $routeParams.section + '/' + $routeParams.tree + '/' + $routeParams.method + '/' + scope.menu.currentNode.parentId);
}
};
scope.selectVariant = function (event, variant) {
scope.vm.dropdownOpen = false;
$location.search("cculture", variant.language.culture);
if (scope.onSelectVariant) {
scope.vm.dropdownOpen = false;
scope.onSelectVariant({ "variant": variant });
}
};
scope.openIconPicker = function() {
scope.openIconPicker = function () {
var iconPicker = {
icon: scope.icon.split(' ')[0],
color: scope.icon.split(' ')[1],
submit: function(model) {
submit: function (model) {
if (model.icon) {
if (model.color) {
scope.icon = model.icon + " " + model.color;
@@ -273,37 +255,47 @@ Use this directive to construct a header inside the main editor window.
}
editorService.close();
},
close: function() {
close: function () {
editorService.close();
}
};
editorService.iconPicker(iconPicker);
};
scope.closeSplitView = function() {
if(scope.onCloseSplitView) {
scope.closeSplitView = function () {
if (scope.onCloseSplitView) {
scope.onCloseSplitView();
}
};
scope.openInSplitView = function(event, variant) {
if(scope.onOpenInSplitView) {
scope.openInSplitView = function (event, variant) {
if (scope.onOpenInSplitView) {
scope.vm.dropdownOpen = false;
scope.onOpenInSplitView({"variant": variant});
scope.onOpenInSplitView({ "variant": variant });
}
};
scope.$watch('variants', function(newValue, oldValue){
if(!newValue) return;
if(newValue === oldValue) return;
setCurrentVariant(newValue);
setVariantStatusColor(newValue);
}, true);
onInit();
//watch for the active culture changing, if it changes, update the current variant
if (scope.variants) {
scope.$watch(function () {
for (var i = 0; i < scope.variants.length; i++) {
var v = scope.variants[i];
if (v.active) {
return v.language.culture;
}
}
return scope.vm.currentVariant.language.culture; //should never get here
}, function (newValue, oldValue) {
if (newValue !== scope.vm.currentVariant.language.culture) {
setCurrentVariant();
}
});
}
}
var directive = {
transclude: true,
restrict: 'E',
@@ -331,7 +323,8 @@ Use this directive to construct a header inside the main editor window.
showBackButton: "<?",
splitViewOpen: "=?",
onOpenInSplitView: "&?",
onCloseSplitView: "&?"
onCloseSplitView: "&?",
onSelectVariant: "&?"
},
link: link
};
@@ -1,18 +1,79 @@
(function () {
'use strict';
function EditorNavigationDirective(eventsService) {
function EditorNavigationDirective($window, $timeout, eventsService, windowResizeListener) {
function link(scope, el, attr, ctrl) {
function link(scope) {
scope.showNavigation = true;
scope.showMoreButton = false;
scope.showDropdown = false;
scope.overflowingItems = 0;
scope.itemsLimit = 6;
scope.moreButton = {
alias: "more",
active: false,
name: "More"
};
scope.clickNavigationItem = function (selectedItem) {
setItemToActive(selectedItem);
scope.showDropdown = false;
runItemAction(selectedItem);
eventsService.emit("app.tabChange", selectedItem);
setItemToActive(selectedItem);
};
scope.toggleDropdown = function () {
scope.showDropdown = !scope.showDropdown;
};
scope.hideDropdown = function() {
scope.showDropdown = false;
};
function onInit() {
// hide navigation if there is only 1 item
if (scope.navigation.length <= 1) {
scope.showNavigation = false;
}
$timeout(function(){
if($window && $window.innerWidth) {
calculateVisibleItems($window.innerWidth);
}
});
}
function calculateVisibleItems(windowWidth) {
// if we don't get a windowWidth stick with the default item limit
if(!windowWidth) {
return;
}
scope.itemsLimit = 0;
// set visible items based on browser width
if (windowWidth > 1500) {
scope.itemsLimit = 6;
}
else if (windowWidth > 700) {
scope.itemsLimit = 4;
}
// toggle more button
if(scope.navigation.length > scope.itemsLimit) {
scope.showMoreButton = true;
scope.overflowingItems = scope.itemsLimit - scope.navigation.length;
} else {
scope.showMoreButton = false;
scope.overflowingItems = 0;
}
}
function runItemAction(selectedItem) {
if (selectedItem.action) {
selectedItem.action(selectedItem);
@@ -20,29 +81,41 @@
}
function setItemToActive(selectedItem) {
// set all other views to inactive
if (selectedItem.view) {
for (var index = 0; index < scope.navigation.length; index++) {
var item = scope.navigation[index];
// deselect all items
angular.forEach(scope.navigation, function(item, index){
item.active = false;
});
// set clicked item to active
selectedItem.active = true;
// set more button to active if item in dropdown is clicked
var selectedItemIndex = scope.navigation.indexOf(selectedItem);
if (selectedItemIndex + 1 > scope.itemsLimit) {
scope.moreButton.active = true;
} else {
scope.moreButton.active = false;
}
// set view to active
selectedItem.active = true;
}
}
function activate() {
// hide navigation if there is only 1 item
if (scope.navigation.length <= 1) {
scope.showNavigation = false;
var resizeCallback = function(size) {
if(size && size.width) {
calculateVisibleItems(size.width);
}
};
}
windowResizeListener.register(resizeCallback);
activate();
//ensure to unregister from all events and kill jquery plugins
scope.$on('$destroy', function () {
windowResizeListener.unregister(resizeCallback);
});
onInit();
}
@@ -0,0 +1,33 @@
(function () {
'use strict';
/**
* A directive that renders a defined view with a view model and a the whole content model.
**/
function EditorSubViewDirective() {
function link(scope, el, attr, ctrl) {
//The model can contain: view, viewModel, name, alias, icon
if (!scope.model.view) {
throw "No view defined for the content app";
}
}
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'views/components/editor/umb-editor-sub-view.html',
scope: {
model: "=",
content: "="
},
link: link
};
return directive;
}
angular.module('umbraco.directives').directive('umbEditorSubView', EditorSubViewDirective);
})();
@@ -1,48 +1,31 @@
(function() {
'use strict';
(function () {
'use strict';
function EditorSubViewsDirective() {
/**
* A directive that just repeats over a list of defined views which are all able to access the same common model.
* This is only used in simple cases, whereas media and content use umbEditorSubView (singular) which allows
* passing in a view model specific to the view and the entire content model for support if required.
**/
function EditorSubViewsDirective() {
function link(scope, el, attr, ctrl) {
function link(scope, el, attr, ctrl) {
scope.activeView = {};
}
// set toolbar from selected navigation item
function setActiveView(items) {
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'views/components/editor/umb-editor-sub-views.html',
scope: {
subViews: "=",
model: "="
},
link: link
};
for (var index = 0; index < items.length; index++) {
return directive;
}
var item = items[index];
if (item.active && item.view) {
scope.activeView = item;
}
}
}
// watch for navigation changes
scope.$watch('subViews', function(newValue, oldValue) {
if (newValue) {
setActiveView(newValue);
}
}, true);
}
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'views/components/editor/umb-editor-sub-views.html',
scope: {
subViews: "=",
model: "="
},
link: link
};
return directive;
}
angular.module('umbraco.directives').directive('umbEditorSubViews', EditorSubViewsDirective);
angular.module('umbraco.directives').directive('umbEditorSubViews', EditorSubViewsDirective);
})();
@@ -1,6 +1,6 @@
/*
http://vitalets.github.io/checklist-model/
https://vitalets.github.io/checklist-model/
<label ng-repeat="role in roles">
<input type="checkbox" checklist-model="user.roles" checklist-value="role.id"> {{role.text}}
</label>
@@ -19,7 +19,7 @@ angular.module('umbraco.directives')
return false;
}
// add
// add
function add(arr, item) {
arr = angular.isArray(arr) ? arr : [];
for (var i = 0; i < arr.length; i++) {
@@ -44,7 +44,7 @@ angular.module('umbraco.directives')
return arr;
}
// http://stackoverflow.com/a/19228302/1458162
// https://stackoverflow.com/a/19228302/1458162
function postLinkFn(scope, elem, attrs) {
// compile with `ng-model` pointing to `checked`
$compile(elem)(scope);
@@ -98,4 +98,4 @@ angular.module('umbraco.directives')
return postLinkFn;
}
};
}]);
}]);
@@ -53,7 +53,7 @@ angular.module("umbraco.directives")
var $container = element.find(".crop-container");
//default constraints for drag n drop
var constraints = {left: {max: scope.dimensions.margin, min: scope.dimensions.margin}, top: {max: scope.dimensions.margin, min: scope.dimensions.margin}, };
var constraints = {left: {max: scope.dimensions.margin, min: scope.dimensions.margin}, top: {max: scope.dimensions.margin, min: scope.dimensions.margin} };
scope.constraints = constraints;
@@ -264,7 +264,7 @@ angular.module("umbraco.directives")
});
//ie hack
if(window.navigator.userAgent.indexOf("MSIE ")){
if(window.navigator.userAgent.indexOf("MSIE ") >= 0){
var ranger = element.find("input");
ranger.bind("change",function(){
scope.$apply(function(){
@@ -1,139 +1,199 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:umbImageGravity
* @restrict E
* @function
* @description
**/
angular.module("umbraco.directives")
.directive('umbImageGravity', function ($timeout, localizationService, $log) {
return {
restrict: 'E',
replace: true,
templateUrl: 'views/components/imaging/umb-image-gravity.html',
scope: {
src: '=',
center: "=",
onImageLoaded: "="
},
link: function(scope, element, attrs) {
(function() {
'use strict';
//Internal values for keeping track of the dot and the size of the editor
scope.dimensions = {
width: 0,
height: 0,
left: 0,
top: 0
};
function umbImageGravityController($scope, $element, $timeout) {
scope.loaded = false;
var vm = this;
//elements
var $viewport = element.find(".viewport");
var $image = element.find("img");
var $overlay = element.find(".overlay");
//Internal values for keeping track of the dot and the size of the editor
vm.dimensions = {
width: 0,
height: 0,
left: 0,
top: 0
};
scope.style = function () {
if(scope.dimensions.width <= 0){
setDimensions();
}
var htmlImage = null; //DOM element reference
var htmlOverlay = null; //DOM element reference
var draggable = null;
return {
'top': scope.dimensions.top + 'px',
'left': scope.dimensions.left + 'px'
};
};
vm.loaded = false;
vm.$onInit = onInit;
vm.$onChanges = onChanges;
vm.$postLink = postLink;
vm.$onDestroy = onDestroy;
vm.style = style;
vm.setFocalPoint = setFocalPoint;
scope.setFocalPoint = function(event) {
/** Sets the css style for the Dot */
function style() {
scope.$emit("imageFocalPointStart");
if (vm.dimensions.width <= 0) {
//this initializes the dimensions since when the image element first loads
//there will be zero dimensions
setDimensions();
}
var offsetX = event.offsetX - 10;
var offsetY = event.offsetY - 10;
return {
'top': vm.dimensions.top + 'px',
'left': vm.dimensions.left + 'px'
};
};
calculateGravity(offsetX, offsetY);
function setFocalPoint (event) {
lazyEndEvent();
$scope.$emit("imageFocalPointStart");
};
var offsetX = event.offsetX - 10;
var offsetY = event.offsetY - 10;
var setDimensions = function(){
scope.dimensions.width = $image.width();
scope.dimensions.height = $image.height();
calculateGravity(offsetX, offsetY);
if(scope.center){
scope.dimensions.left = scope.center.left * scope.dimensions.width -10;
scope.dimensions.top = scope.center.top * scope.dimensions.height -10;
}else{
scope.center = { left: 0.5, top: 0.5 };
}
};
lazyEndEvent();
var calculateGravity = function(offsetX, offsetY){
scope.dimensions.left = offsetX;
scope.dimensions.top = offsetY;
};
scope.center.left = (scope.dimensions.left+10) / scope.dimensions.width;
scope.center.top = (scope.dimensions.top+10) / scope.dimensions.height;
};
/** Initializes the component */
function onInit() {
if (!vm.center) {
vm.center = { left: 0.5, top: 0.5 };
}
}
var lazyEndEvent = _.debounce(function(){
scope.$apply(function(){
scope.$emit("imageFocalPointStop");
});
}, 2000);
/** Called when the component has linked everything and the DOM is available */
function postLink() {
//elements
htmlImage = $element.find("img");
htmlOverlay = $element.find(".overlay");
//Drag and drop positioning, using jquery ui draggable
//TODO ensure that the point doesnt go outside the box
$overlay.draggable({
containment: "parent",
start: function(){
scope.$apply(function(){
scope.$emit("imageFocalPointStart");
});
},
stop: function() {
scope.$apply(function(){
var offsetX = $overlay[0].offsetLeft;
var offsetY = $overlay[0].offsetTop;
calculateGravity(offsetX, offsetY);
});
lazyEndEvent();
}
});
//// INIT /////
$image.load(function() {
$timeout(function() {
setDimensions();
scope.loaded = true;
if (angular.isFunction(scope.onImageLoaded)) {
scope.onImageLoaded();
}
});
});
$(window).on('resize.umbImageGravity', function(){
scope.$apply(function(){
$timeout(function(){
setDimensions();
});
// Make sure we can find the offset values for the overlay(dot) before calculating
// fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte)
if($overlay.is(':visible')) {
var offsetX = $overlay[0].offsetLeft;
var offsetY = $overlay[0].offsetTop;
calculateGravity(offsetX, offsetY);
}
});
//Drag and drop positioning, using jquery ui draggable
draggable = htmlOverlay.draggable({
containment: "parent",
start: function () {
$scope.$apply(function () {
$scope.$emit("imageFocalPointStart");
});
},
stop: function () {
$scope.$apply(function () {
var offsetX = htmlOverlay[0].offsetLeft;
var offsetY = htmlOverlay[0].offsetTop;
calculateGravity(offsetX, offsetY);
});
scope.$on('$destroy', function() {
$(window).off('.umbImageGravity');
});
lazyEndEvent();
}
});
}
};
});
$(window).on('resize.umbImageGravity', function () {
$scope.$apply(function () {
resized();
});
});
//if any ancestor directive emits this event, we need to resize
$scope.$on("editors.content.splitViewChanged", function () {
$timeout(resized, 200);
});
//listen for the image DOM element loading
htmlImage.on("load", function () {
$timeout(function () {
setDimensions();
vm.loaded = true;
if (vm.onImageLoaded) {
vm.onImageLoaded();
}
}, 100);
});
}
function onDestroy() {
$(window).off('resize.umbImageGravity');
if (htmlOverlay) {
//TODO: This should be destroyed but this will throw an exception:
// "cannot call methods on draggable prior to initialization; attempted to call method 'destroy'"
// I've tried lots of things and cannot get this to work, we weren't destroying before so hopefully
// there's no mem leaks?
//htmlOverlay.draggable("destroy");
}
if (htmlImage) {
htmlImage.off("load");
}
}
/** Called when we need to resize based on window or DOM dimensions to re-center the focal point */
function resized() {
$timeout(function () {
setDimensions();
});
// Make sure we can find the offset values for the overlay(dot) before calculating
// fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte)
if (htmlOverlay.is(':visible')) {
var offsetX = htmlOverlay[0].offsetLeft;
var offsetY = htmlOverlay[0].offsetTop;
calculateGravity(offsetX, offsetY);
}
}
/** Watches the one way binding changes */
function onChanges(changes) {
if (changes.center && !changes.center.isFirstChange()
&& changes.center.currentValue
&& !angular.equals(changes.center.currentValue, changes.center.previousValue)) {
//when center changes update the dimensions
setDimensions();
}
}
/** Sets the width/height/left/top dimentions based on the image size and the "center" value */
function setDimensions() {
if (htmlImage && vm.center) {
vm.dimensions.width = htmlImage.width();
vm.dimensions.height = htmlImage.height();
vm.dimensions.left = vm.center.left * vm.dimensions.width - 10;
vm.dimensions.top = vm.center.top * vm.dimensions.height - 10;
}
return vm.dimensions.width;
};
/**
* based on the offset selected calculates the "center" value and calls the callback
* @param {any} offsetX
* @param {any} offsetY
*/
function calculateGravity(offsetX, offsetY) {
vm.onValueChanged({
left: (offsetX + 10) / vm.dimensions.width,
top: (offsetY + 10) / vm.dimensions.height
});
//vm.center.left = (offsetX + 10) / scope.dimensions.width;
//vm.center.top = (offsetY + 10) / scope.dimensions.height;
};
var lazyEndEvent = _.debounce(function () {
$scope.$apply(function () {
$scope.$emit("imageFocalPointStop");
});
}, 2000);
}
var umbImageGravityComponent = {
templateUrl: 'views/components/imaging/umb-image-gravity.html',
bindings: {
src: "<",
center: "<",
onImageLoaded: "&?",
onValueChanged: "&"
},
controllerAs: 'vm',
controller: umbImageGravityController
};
angular.module("umbraco.directives")
.component('umbImageGravity', umbImageGravityComponent);
})();
@@ -1,31 +1,32 @@
(function() {
'use strict';
(function () {
'use strict';
function OverlayBackdropDirective(overlayHelper) {
function OverlayBackdropDirective(overlayHelper) {
function link(scope, el, attr, ctrl) {
function link(scope, el, attr, ctrl) {
scope.numberOfOverlays = 0;
scope.numberOfOverlays = 0;
scope.$watch(function(){
return overlayHelper.getNumberOfOverlays();
}, function (newValue) {
scope.numberOfOverlays = newValue;
});
//TODO: this shouldn't be a watch, this should be based on an event handler
scope.$watch(function () {
return overlayHelper.getNumberOfOverlays();
}, function (newValue) {
scope.numberOfOverlays = newValue;
});
}
}
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'views/components/overlays/umb-overlay-backdrop.html',
link: link
};
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'views/components/overlays/umb-overlay-backdrop.html',
link: link
};
return directive;
return directive;
}
}
angular.module('umbraco.directives').directive('umbOverlayBackdrop', OverlayBackdropDirective);
angular.module('umbraco.directives').directive('umbOverlayBackdrop', OverlayBackdropDirective);
})();
@@ -350,6 +350,9 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use
if (args.cacheKey) {
$scope.cachekey = args.cacheKey;
}
if (args.customTreeParams) {
$scope.customtreeparams = args.customTreeParams;
}
}
//load the tree
@@ -161,7 +161,7 @@
/**
* ACE editor session.
* @type object
* @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session}
* @see [EditSession]{@link https://ace.c9.io/#nav=api&api=edit_session}
*/
var session = acee.getSession();
@@ -29,9 +29,7 @@ Use this directive to generate color swatches to pick from.
function link(scope, el, attr, ctrl) {
scope.setColor = function (color) {
//scope.selectedColor({color: color });
scope.selectedColor = color;
if (scope.onSelect) {
scope.onSelect(color);
}
@@ -7,7 +7,7 @@
@description
<b>Added in Umbraco version 7.6</b>
This directive is a wrapper of the bootstrap datetime picker version 3.1.3. Use it to render a date time picker.
For extra details about options and events take a look here: http://eonasdan.github.io/bootstrap-datetimepicker/
For extra details about options and events take a look here: https://eonasdan.github.io/bootstrap-datetimepicker/
Use this directive to render a date time picker
@@ -17,7 +17,7 @@
restrict: 'E',
replace: true,
transclude: true,
templateUrl: 'views/components/umb-dropdown-item.html',
templateUrl: 'views/components/umb-dropdown-item.html'
};
return directive;
@@ -57,7 +57,7 @@
},
stop: function(e, ui) {
updateTabsSortOrder();
},
}
};
scope.sortableOptionsProperty = {
@@ -245,51 +245,21 @@
scope.openCompositionsDialog = function() {
scope.compositionsDialogModel = {
title: "Compositions",
contentType: scope.model,
compositeContentTypes: scope.model.compositeContentTypes,
view: "views/common/overlays/contenttypeeditor/compositions/compositions.html",
confirmSubmit: {
title: "Warning",
description: "Removing a composition will delete all the associated property data. Once you save the document type there's no way back, are you sure?",
checkboxLabel: "I know what I'm doing",
enable: true
},
submit: function(model, oldModel, confirmed) {
view: "views/common/infiniteeditors/compositions/compositions.html",
size: "small",
submit: function() {
// make sure that all tabs has an init property
if (scope.model.groups.length !== 0) {
angular.forEach(scope.model.groups, function(group) {
addInitProperty(group);
});
}
var compositionRemoved = false;
// check if any compositions has been removed
for(var i = 0; oldModel.compositeContentTypes.length > i; i++) {
var oldComposition = oldModel.compositeContentTypes[i];
if(_.contains(model.compositeContentTypes, oldComposition) === false) {
compositionRemoved = true;
}
}
// show overlay confirm box if compositions has been removed.
if(compositionRemoved && confirmed === false) {
scope.compositionsDialogModel.confirmSubmit.show = true;
// submit overlay if no compositions has been removed
// or the action has been confirmed
} else {
// make sure that all tabs has an init property
if (scope.model.groups.length !== 0) {
angular.forEach(scope.model.groups, function(group) {
addInitProperty(group);
});
}
// remove overlay
scope.compositionsDialogModel.show = false;
scope.compositionsDialogModel = null;
}
// remove overlay
editorService.close();
},
close: function(oldModel) {
@@ -299,8 +269,7 @@
scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes;
// remove overlay
scope.compositionsDialogModel.show = false;
scope.compositionsDialogModel = null;
editorService.close();
},
selectCompositeContentType: function (selectedContentType) {
@@ -348,39 +317,40 @@
}
};
//select which resource methods to use, eg document Type or Media Type versions
var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
var whereUsedContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getWhereCompositionIsUsedInContentTypes : mediaTypeResource.getWhereCompositionIsUsedInContentTypes;
var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount;
//get the currently assigned property type aliases - ensure we pass these to the server side filer
var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) {
return _.map(g.properties, function(p) {
return p.alias;
});
})), function(f) {
return f !== null && f !== undefined;
});
$q.all([
//get available composite types
availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) {
setupAvailableContentTypesModel(result);
}),
//get where used document types
whereUsedContentTypeResource(scope.model.id).then(function (whereUsed) {
//pass to the dialog model the content type eg documentType or mediaType
scope.compositionsDialogModel.section = scope.contentType;
//pass the list of 'where used' document types
scope.compositionsDialogModel.whereCompositionUsed = whereUsed;
}),
//get content type count
countContentTypeResource().then(function(result) {
scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10);
})
]).then(function() {
//resolves when both other promises are done, now show it
scope.compositionsDialogModel.show = true;
});
//select which resource methods to use, eg document Type or Media Type versions
var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
var whereUsedContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getWhereCompositionIsUsedInContentTypes : mediaTypeResource.getWhereCompositionIsUsedInContentTypes;
var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount;
//get the currently assigned property type aliases - ensure we pass these to the server side filer
var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) {
return _.map(g.properties, function(p) {
return p.alias;
});
})), function(f) {
return f !== null && f !== undefined;
});
$q.all([
//get available composite types
availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) {
setupAvailableContentTypesModel(result);
}),
//get where used document types
whereUsedContentTypeResource(scope.model.id).then(function (whereUsed) {
//pass to the dialog model the content type eg documentType or mediaType
scope.compositionsDialogModel.section = scope.contentType;
//pass the list of 'where used' document types
scope.compositionsDialogModel.whereCompositionUsed = whereUsed;
}),
//get content type count
countContentTypeResource().then(function(result) {
scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10);
})
]).then(function() {
//resolves when both other promises are done, now show it
editorService.open(scope.compositionsDialogModel);
});
};
@@ -4,7 +4,6 @@
* @restrict E
* @function
* @description
* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form.
**/
/*
@@ -0,0 +1,305 @@
(function() {
'use strict';
/**
* A component to manage file uploads for content properties
* @param {any} $scope
* @param {any} fileManager
* @param {any} mediaHelper
* @param {any} angularHelper
*/
function umbPropertyFileUploadController($scope, $q, fileManager, mediaHelper, angularHelper) {
//NOTE: this component supports multiple files, though currently the uploader does not but perhaps sometime in the future
// we'd want it to, so i'll leave the multiple file support in place
var vm = this;
vm.$onInit = onInit;
vm.$onChanges = onChanges;
vm.$postLink = postLink;
vm.clear = clearFiles;
/** Clears the file collections when content is saving (if we need to clear) or after saved */
function clearFiles() {
//clear the files collection (we don't want to upload any!)
fileManager.setFiles({
propertyAlias: vm.propertyAlias,
culture: vm.culture,
files: []
});
//clear the current files
vm.files = [];
//notify the callback
notifyFilesSelected(null);
notifyFilesChanged(null);
}
function notifyFilesSelected(val, files) {
if (!val) {
val = null;
}
if (!files) {
files = null;
}
//notify the callback
vm.onFilesSelected({ value: val, files: files });
//need to explicity setDirty here to track changes
vm.fileUploadForm.$setDirty();
}
function notifyFilesChanged(files) {
if (!files) {
files = null;
}
//notify the callback
vm.onFilesChanged({ files: files });
}
function notifyInit(val, files) {
if (!val) {
val = null;
}
if (!files) {
files = null;
}
if (vm.onInit) {
vm.onInit({ value: val, files: files });
}
}
/** Called when the component initializes */
function onInit() {
$scope.$on("filesSelected", onFilesSelected);
initialize();
}
/** Called when the component has linked all elements, this is when the form controller is available */
function postLink() {
}
function initialize() {
//normalize culture to null if it's not there
if (!vm.culture) {
vm.culture = null;
}
//TODO: need to figure out what we can do for things like Nested Content
var existingClientFiles = checkPendingClientFiles();
//create the property to show the list of files currently saved
if (existingClientFiles.length > 0) {
updateModelFromSelectedFiles(existingClientFiles).then(function (newVal) {
//notify the callback
notifyInit(newVal, vm.files);
});
}
else if (vm.value) {
var files = vm.value.split(",");
vm.files = _.map(files, function (file) {
var f = {
fileName: file,
isImage: mediaHelper.detectIfImageByExtension(file),
extension: getExtension(file)
};
f.fileSrc = getThumbnail(f);
return f;
});
//notify the callback
notifyInit();
}
else {
vm.files = [];
//notify the callback
notifyInit();
}
}
function checkPendingClientFiles() {
//normalize culture to null if it's not there
if (!vm.culture) {
vm.culture = null;
}
//check the file manager to see if there's already local files pending for this editor
var existingClientFiles = _.map(
_.filter(fileManager.getFiles(),
function (f) {
return f.alias === vm.propertyAlias && f.culture === vm.culture;
}),
function (f) {
return f.file;
});
return existingClientFiles;
}
/**
* Watch for model changes
* @param {any} changes
*/
function onChanges(changes) {
if (changes.value && !changes.value.isFirstChange() && changes.value.currentValue !== changes.value.previousValue) {
if (!changes.value.currentValue && changes.value.previousValue) {
//if the value has been cleared, clear the files (ignore if the previous value is also falsy)
vm.files = [];
}
else if (changes.value.currentValue && !changes.value.previousValue && vm.files.length === 0) {
//if a new value has been added after being cleared
var existingClientFiles = checkPendingClientFiles();
//create the property to show the list of files currently saved
if (existingClientFiles.length > 0) {
updateModelFromSelectedFiles(existingClientFiles).then(function () {
//raise this event which means the files have changed but this wasn't the instance that
//added the file
notifyFilesChanged(vm.files);
});
}
}
}
}
function getThumbnail(file) {
if (!file.isImage) {
return null;
}
var thumbnailUrl = mediaHelper.getThumbnailFromPath(file.fileName);
return thumbnailUrl;
}
function getExtension(fileName) {
var extension = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length);
return extension.toLowerCase();
}
/**
* Updates the vm.files model from the selected files and returns a promise containing the csv of all file names selected
* @param {any} files
*/
function updateModelFromSelectedFiles(files) {
//we return a promise because the FileReader api is async
var promises = [];
//clear the current files
vm.files = [];
var newVal = "";
var reader = new FileReader();
//for each file load in the contents from the file reader and set it as an fileSrc
//property of the vm.files array item
var fileCount = files.length;
for (var i = 0; i < fileCount; i++) {
var index = i; //capture
var isImage = mediaHelper.detectIfImageByExtension(files[i].name);
//save the file object to the files collection
vm.files.push({
isImage: isImage,
extension: getExtension(files[i].name),
fileName: files[i].name,
isClientSide: true
});
newVal += files[i].name + ",";
if (isImage) {
var deferred = $q.defer();
reader.onload = function(e) {
vm.files[index].fileSrc = e.target.result;
deferred.resolve(newVal);
};
promises.push(deferred.promise);
reader.readAsDataURL(files[i]);
}
else {
promises.push($q.when(newVal));
}
}
return $q.all(promises).then(function (p) {
//return the last value in the list of promises which will be the final value
return $q.when(p[p.length - 1]);
});
}
/**
* listen for when a file is selected
* @param {any} event
* @param {any} args
*/
function onFilesSelected(event, args) {
if (args.files && args.files.length > 0) {
//set the files collection
fileManager.setFiles({
propertyAlias: vm.propertyAlias,
files: args.files,
culture: vm.culture
});
updateModelFromSelectedFiles(args.files).then(function(newVal) {
angularHelper.safeApply($scope,
function() {
//pass in the file names and the model files
notifyFilesSelected(newVal, vm.files);
notifyFilesChanged(vm.files);
});
});
}
else {
angularHelper.safeApply($scope);
}
}
};
var umbPropertyFileUploadComponent = {
templateUrl: 'views/components/upload/umb-property-file-upload.html',
bindings: {
culture: "@?",
propertyAlias: "@",
value: "<",
hideSelection: "<",
/**
* Called when a file is selected on this instance
*/
onFilesSelected: "&",
/**
* Called when the file collection changes (i.e. a new file has been selected but maybe it wasn't this instance that caused the change)
*/
onFilesChanged: "&",
onInit: "&"
},
transclude: true,
controllerAs: 'vm',
controller: umbPropertyFileUploadController
};
angular.module("umbraco.directives")
.component('umbPropertyFileUpload', umbPropertyFileUploadComponent);
})();
@@ -0,0 +1,49 @@
/**
* @ngdoc directive
* @name umbraco.directives.directive:autoScale
* @element div
* @function
* @description
* Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set
* So if you only want to scale the div to 70 pixels from the bottom you pass "70"
* @example
* <example module="umbraco.directives">
* <file name="index.html">
* <div auto-scale="70" class="input-block-level"></div>
* </file>
* </example>
**/
angular.module("umbraco.directives")
.directive('autoScale', function ($window, $timeout, windowResizeListener) {
return function (scope, el, attrs) {
var totalOffset = 0;
var offsety = parseInt(attrs.autoScale, 10);
var window = angular.element($window);
if (offsety !== undefined) {
totalOffset += offsety;
}
$timeout(function () {
setElementSize();
});
function setElementSize() {
el.height(window.height() - (el.offset().top + totalOffset));
}
var resizeCallback = function() {
setElementSize();
};
windowResizeListener.register(resizeCallback);
//ensure to unregister from all events and kill jquery plugins
scope.$on('$destroy', function () {
windowResizeListener.unregister(resizeCallback);
});
};
});
@@ -1,7 +1,7 @@
/**
* Konami Code directive for AngularJS
* @version v0.0.1
* @license MIT License, http://www.opensource.org/licenses/MIT
* @license MIT License, https://www.opensource.org/licenses/MIT
*/
angular.module('umbraco.directives')
@@ -59,4 +59,4 @@ angular.module('umbraco.directives')
scope.$on('$destroy', stopListening);
}
};
}]);
}]);
@@ -2,7 +2,7 @@
* @ngdoc directive
* @name umbraco.directives.directive:noDirtyCheck
* @restrict A
* @description Can be attached to form inputs to prevent them from setting the form as dirty (http://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs)
* @description Can be attached to form inputs to prevent them from setting the form as dirty (https://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs)
**/
function noDirtyCheck() {
return {
@@ -20,4 +20,4 @@ function noDirtyCheck() {
}
};
}
angular.module('umbraco.directives.validation').directive("noDirtyCheck", noDirtyCheck);
angular.module('umbraco.directives.validation').directive("noDirtyCheck", noDirtyCheck);
@@ -8,7 +8,14 @@
link: function (scope, element, attr, ctrl) {
element.hide();
//We can either get the form submitted status by the parent directive valFormManager (if we add a property to it)
//or we can just check upwards in the DOM for the css class (easier for now).
//The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot
//reset the status.
var submitted = element.closest(".show-validation").length > 0;
if (!submitted) {
element.hide();
}
var unsubscribe = [];
@@ -20,6 +27,7 @@
element.hide();
}));
//no isolate scope to listen to element destroy
element.bind('$destroy', function () {
for (var u in unsubscribe) {
unsubscribe[u]();
@@ -3,21 +3,27 @@
function SetDirtyOnChange() {
function link(scope, el, attr, ctrl) {
if(attr.ngModel) {
scope.$watch(attr.ngModel, function(newValue, oldValue) {
if (!newValue) {return;}
if (newValue === oldValue) {return;}
ctrl.$setDirty();
}, true);
function link(scope, el, attr, ctrls) {
var formCtrl = ctrls[0];
if (ctrls.length > 1) {
//if an ngModel is supplied, assign a render function which is called when the model is changed
var modelCtrl = ctrls[1];
var oldRender = modelCtrl.$render;
modelCtrl.$render = function () {
formCtrl.$setDirty();
//call any previously set render method
if (oldRender) {
oldRender();
}
};
} else {
var initValue = attr.umbSetDirtyOnChange;
attr.$observe("umbSetDirtyOnChange", function (newValue) {
if(newValue !== initValue) {
ctrl.$setDirty();
formCtrl.$setDirty();
}
});
}
@@ -25,7 +31,7 @@
}
var directive = {
require: "^^form",
require: ["^^form", "?ngModel"],
restrict: 'A',
link: link
};
@@ -12,7 +12,7 @@
* Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will
* be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly.
**/
function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService, $routeParams) {
function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService, $routeParams, navigationService) {
var SHOW_VALIDATION_CLASS_NAME = "show-validation";
var SAVING_EVENT_NAME = "formSubmitting";
@@ -111,22 +111,27 @@ function valFormManager(serverValidationManager, $rootScope, $log, $timeout, not
return;
}
var path = nextLocation.split("#")[1];
if (path) {
if (path.indexOf("%253") || path.indexOf("%252")) {
path = decodeURIComponent(path);
var nextPath = nextLocation.split("#")[1];
if (nextPath) {
if (navigationService.isRouteChangingNavigation(currentLocation, nextLocation)) {
if (!notificationsService.hasView()) {
if (nextPath.indexOf("%253") || nextPath.indexOf("%252")) {
nextPath = decodeURIComponent(nextPath);
}
var msg = { view: "confirmroutechange", args: { path: nextPath, listener: locationEvent } };
notificationsService.add(msg);
}
//prevent the route!
event.preventDefault();
//raise an event
eventsService.emit("valFormManager.pendingChanges", true);
}
if (!notificationsService.hasView()) {
var msg = { view: "confirmroutechange", args: { path: path, listener: locationEvent } };
notificationsService.add(msg);
}
//prevent the route!
event.preventDefault();
//raise an event
eventsService.emit("valFormManager.pendingChanges", true);
}
});
@@ -12,37 +12,38 @@
function valPropertyMsg(serverValidationManager) {
return {
scope: {
property: "="
},
require: ['^^form', '^^valFormManager'],
require: ['^^form', '^^valFormManager', '^^umbProperty'],
replace: true,
restrict: "E",
template: "<div ng-show=\"errorMsg != ''\" class='alert alert-error property-error' >{{errorMsg}}</div>",
scope: {},
link: function (scope, element, attrs, ctrl) {
//the property form controller api
var formCtrl = ctrl[0];
//the valFormManager controller api
var valFormManager = ctrl[1];
//the property controller api
var umbPropCtrl = ctrl[2];
scope.currentProperty = umbPropCtrl.property;
var currentCulture = scope.currentProperty.culture;
var watcher = null;
// Gets the error message to display
function getErrorMsg() {
//this can be null if no property was assigned
if (scope.property) {
if (scope.currentProperty) {
//first try to get the error msg from the server collection
var err = serverValidationManager.getPropertyError(scope.property.alias, "");
var err = serverValidationManager.getPropertyError(scope.currentProperty.alias, null, "");
//if there's an error message use it
if (err && err.errorMsg) {
return err.errorMsg;
}
else {
//TODO: localize
return scope.property.propertyErrorMessage ? scope.property.propertyErrorMessage : "Property has errors";
return scope.currentProperty.propertyErrorMessage ? scope.currentProperty.propertyErrorMessage : "Property has errors";
}
}
@@ -59,29 +60,30 @@ function valPropertyMsg(serverValidationManager) {
function startWatch() {
//if there's not already a watch
if (!watcher) {
watcher = scope.$watch("property.value", function (newValue, oldValue) {
if (!newValue || angular.equals(newValue, oldValue)) {
return;
}
watcher = scope.$watch("currentProperty.value",
function (newValue, oldValue) {
var errCount = 0;
for (var e in formCtrl.$error) {
if (angular.isArray(formCtrl.$error[e])) {
errCount++;
if (!newValue || angular.equals(newValue, oldValue)) {
return;
}
}
//we are explicitly checking for valServer errors here, since we shouldn't auto clear
// based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg
// is the only one, then we'll clear.
var errCount = 0;
for (var e in formCtrl.$error) {
if (angular.isArray(formCtrl.$error[e])) {
errCount++;
}
}
if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) {
scope.errorMsg = "";
formCtrl.$setValidity('valPropertyMsg', true);
stopWatch();
}
}, true);
//we are explicitly checking for valServer errors here, since we shouldn't auto clear
// based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg
// is the only one, then we'll clear.
if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) {
scope.errorMsg = "";
formCtrl.$setValidity('valPropertyMsg', true);
stopWatch();
}
}, true);
}
}
@@ -93,19 +95,7 @@ function valPropertyMsg(serverValidationManager) {
}
}
//if there's any remaining errors in the server validation service then we should show them.
var showValidation = serverValidationManager.items.length > 0;
var hasError = false;
//create properties on our custom scope so we can use it in our template
scope.errorMsg = "";
var unsubscribe = [];
//listen for form validation changes.
//The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then
// subscribing to this single watch.
valFormManager.onValidationStatusChanged(function (evt, args) {
function checkValidationStatus() {
if (formCtrl.$invalid) {
//first we need to check if the valPropertyMsg validity is invalid
if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) {
@@ -130,10 +120,35 @@ function valPropertyMsg(serverValidationManager) {
hasError = false;
scope.errorMsg = "";
}
}
//if there's any remaining errors in the server validation service then we should show them.
var showValidation = serverValidationManager.items.length > 0;
if (!showValidation) {
//We can either get the form submitted status by the parent directive valFormManager (if we add a property to it)
//or we can just check upwards in the DOM for the css class (easier for now).
//The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot
//reset the status.
showValidation = element.closest(".show-validation").length > 0;
}
var hasError = false;
//create properties on our custom scope so we can use it in our template
scope.errorMsg = "";
var unsubscribe = [];
//listen for form validation changes.
//The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then
// subscribing to this single watch.
valFormManager.onValidationStatusChanged(function (evt, args) {
checkValidationStatus();
});
//listen for the forms saving event
unsubscribe.push(scope.$on("formSubmitting", function(ev, args) {
unsubscribe.push(scope.$on("formSubmitting", function (ev, args) {
showValidation = true;
if (hasError && scope.errorMsg === "") {
scope.errorMsg = getErrorMsg();
@@ -145,7 +160,7 @@ function valPropertyMsg(serverValidationManager) {
}));
//listen for the forms saved event
unsubscribe.push(scope.$on("formSubmitted", function(ev, args) {
unsubscribe.push(scope.$on("formSubmitted", function (ev, args) {
showValidation = false;
scope.errorMsg = "";
formCtrl.$setValidity('valPropertyMsg', true);
@@ -160,35 +175,32 @@ function valPropertyMsg(serverValidationManager) {
// indicate that a content property is invalid at the property level since developers may not actually implement
// the correct field validation in their property editors.
if (scope.property) { //this can be null if no property was assigned
serverValidationManager.subscribe(scope.property.alias, "", function (isValid, propertyErrors, allErrors) {
hasError = !isValid;
if (hasError) {
//set the error message to the server message
scope.errorMsg = propertyErrors[0].errorMsg;
//flag that the current validator is invalid
formCtrl.$setValidity('valPropertyMsg', false);
startWatch();
}
else {
scope.errorMsg = "";
//flag that the current validator is valid
formCtrl.$setValidity('valPropertyMsg', true);
stopWatch();
}
});
if (scope.currentProperty) { //this can be null if no property was assigned
unsubscribe.push(serverValidationManager.subscribe(scope.currentProperty.alias,
currentCulture,
"",
function(isValid, propertyErrors, allErrors) {
hasError = !isValid;
if (hasError) {
//set the error message to the server message
scope.errorMsg = propertyErrors[0].errorMsg;
//flag that the current validator is invalid
formCtrl.$setValidity('valPropertyMsg', false);
startWatch();
}
else {
scope.errorMsg = "";
//flag that the current validator is valid
formCtrl.$setValidity('valPropertyMsg', true);
stopWatch();
}
}));
//when the element is disposed we need to unsubscribe!
// NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain
// but they are a different callback instance than the above.
element.bind('$destroy', function () {
stopWatch();
serverValidationManager.unsubscribe(scope.property.alias, "");
});
}
//when the scope is disposed we need to unsubscribe
scope.$on('$destroy', function () {
stopWatch();
for (var u in unsubscribe) {
unsubscribe[u]();
}
@@ -33,11 +33,9 @@ function valPropertyValidator(serverValidationManager) {
if (!scope.valPropertyValidator || !angular.isFunction(scope.valPropertyValidator)) {
throw new Error('val-property-validator directive must specify a function to call');
}
var initResult = scope.valPropertyValidator();
// Validation method
var validate = function (viewValue) {
function validate (viewValue) {
// Calls the validition method
var result = scope.valPropertyValidator();
if (!result.errorKey || result.isValid === undefined || !result.errorMsg) {
@@ -62,6 +60,9 @@ function valPropertyValidator(serverValidationManager) {
// Parsers are called as soon as the value in the form input is modified
modelCtrl.$parsers.push(validate);
//call on init
validate();
}
};
}
@@ -14,8 +14,7 @@ function valRegex() {
var flags = "";
var regex;
var eventBindings = [];
attrs.$observe("valRegexFlags", function (newVal) {
if (newVal) {
flags = newVal;
@@ -39,11 +38,15 @@ function valRegex() {
}
});
eventBindings.push(scope.$watch('ngModel', function(newValue, oldValue){
if(newValue && newValue !== oldValue) {
patternValidator(newValue);
//An ngModel is supplied, assign a render function which is called when the model is changed
var oldRender = ctrl.$render;
ctrl.$render = function () {
patternValidator(ctrl.$viewValue);
//call any previously set render method
if (oldRender) {
oldRender();
}
}));
};
var patternValidator = function (viewValue) {
if (regex) {
@@ -65,13 +68,6 @@ function valRegex() {
}
};
scope.$on('$destroy', function(){
// unbind watchers
for(var e in eventBindings) {
eventBindings[e]();
}
});
}
};
}

Some files were not shown because too many files have changed in this diff Show More